@ry Thanks for deno! Your goals for deno are spot on.
Just wanted to share some thoughts I've had working with Node.
Interfaces should allow for zero-copy and leave memory allocation to the user as much as possible, e.g. suppose deno ever adds a core crypto api (and hopefully it never will!), but then a cipher method would allow the user to pass in not just a source buffer, but also a target buffer and a target buffer offset.
Ideally, sockets should never allocate buffers, streams should always be pull-only (instant flow control that way), and reading from a stream should always support reading into a user-supplied target buffer (at a target offset).
Propagate the idea of a single-threaded event loop control plane with a multi-core data plane, much more than Node has done. Node has sometimes had a tendency to conflate the two, so people end up doing all their crypto in the event loop without regard to throughput. Sometimes it makes sense to avoid threadpool overhead (e.g. hashing 32 bytes shouldn't be in the threadpool), but most of the time things like base64, hex, string encoding etc. should be able to run in the threadpool where it makes sense (e.g. decoding a 10 MB base64 MIME attachment), and should be async by default.
Make deno a superset of all supported platforms, not a leaky lowest-common-denominator. Node has sometimes wanted to normalize things across platforms far too much (e.g. force Unicode NFC everywhere - bad idea), or been slow to add file btime
setters on platforms that support it, just because other platforms didn't. Something as simple as setting ulimit
on Linux is just not possible with Node out of the box because of this kind of LCD thinking. Electron for all its faults is a good example of exposing platforms as they really are.
Personally, I think Rust will be better than Go down the line for native bindings. One GC in v8 is enough!
Keep the core small. Native add ons are the way to go. Node's N-API is actually brilliant here.
Actually, that's it. Avoiding 6 future regrets should be enough!
@jorangreef Thank you for voicing these. I agree with you everywhere.
Interfaces should allow for zero-copy and leave memory allocation
Absolutely - it has been a major design goal. Thanks to modern V8 support of ArrayBuffers and protobufs this is very possible.
sockets should never allocate buffer
I would have disagreed with you years ago, but I do agree now. (I thought to best support Windows was to use IOCP, which preallocates buffers. But these days fast polling is available on windows, so it makes sense to expose a non-blocking API and leave the alloc to users.
Personally, I think Rust will be better than Go down the line for native bindings. One GC in v8 is enough!
Yes, I am also very concerned about the double GC that will come with Go. I'm also concerned with how non-minimal Rust is. I'm still researching it...
cc @piscisaureus
@jorangreef
Interfaces should allow for zero-copy and leave memory allocation to the user as much as possible, e.g. suppose deno ever adds a core crypto api (and hopefully it never will!), but then a cipher method would allow the user to pass in not just a source buffer, but also a target buffer and a target buffer offset.
Ideally, sockets should never allocate buffers,
and reading from a stream should always support reading into a user-supplied target buffer (at a target offset).
While I am fairly sympathetic to this idea, a few counterpoints the record.
streams should always be pull-only (instant flow control that way),
Very much yes.
Propagate the idea of a single-threaded event loop control plane with a multi-core data plane, much more than Node has done. Node has sometimes had a tendency to conflate the two, so people end up doing all their crypto in the event loop without regard to throughput. Sometimes it makes sense to avoid threadpool overhead (e.g. hashing 32 bytes shouldn't be in the threadpool), but most of the time things like base64, hex, string encoding etc. should be able to run in the threadpool where it makes sense (e.g. decoding a 10 MB base64 MIME attachment), and should be async by default.
While I agree that node's support for off-the-main-thread data processing is severely lacking, I am not convinced that moving work to the thread pool is really the solution. A counterexample would be gzip - it can be done in the thread pool in node, now people expect it to be effectively free (see e.g. https://github.com/nodejs/node/issues/8871).
Make deno a superset of all supported platforms, not a leaky lowest-common-denominator. Node has sometimes wanted to normalize things across platforms far too much (e.g. force Unicode NFC everywhere - bad idea), or been slow to add file btime setters on platforms that support it, just because other platforms didn't. Something as simple as setting ulimit on Linux is just not possible with Node out of the box because of this kind of LCD thinking. Electron for all its faults is a good example of exposing platforms as they really are.
I agree we screwed up a bit in node sometimes (e.g. LCD approach in fs.watch really gave us worst-of-both-worlds). However I stand by our general approach -- trying to reconcile the differences between platforms in order to expose a single API that works everywhere. Otherwise you end up in the situation that software is effectively never portable, because platform-specific APIs sneaks in without the developer being aware of it. It may have taken a while to add btime
, but if we had done it fast-and-loose then btime
would have been called ctime
on windows (hello python, php).
I'll look into what electron does differently.
Personally, I think Rust will be better than Go down the line for native bindings. One GC in v8 is enough!
Keep the core small. Native add ons are the way to go. Node's N-API is actually brilliant here.
Agree on all that!
@ry
I would have disagreed with you years ago, but I do agree now. (I thought to best support Windows was to use IOCP, which preallocates buffers.
I think I have clear up a misconception here - libuv doesn't preallocate buffers for incoming data, not even on windows. It used to have an option to do this for the first N connections, but this feature was never used in node.js (in other words, N=0). Instead, it always calls two callbacks in rapid succession (alloc_cb to ask the user for a buffer, and read_cb to return the buffer to the user).
But these days fast polling is available on windows,
The practical difference here is that polling is now also available for other events - incoming connections, outgoing data.
so it makes sense to expose a non-blocking API and leave the alloc to users.
I think the dilemma is more about whether you want to provide a single stream API (that also works for files) vs stay close to the Berkeley sockets abstraction.
Personally, I think Rust will be better than Go down the line for native bindings. One GC in v8 is enough!
I still think the GC in Go is not ideal, but then again Rust's syntax is not the most RSI friendly. Zig might be the sweet spot: https://github.com/ziglang/zig/wiki/Why-Zig-When-There-is-Already-CPP%2C-D%2C-and-Rust%3F
Funny that this came up when I searched for Flow... Flow is a better typed Js then Typescript. Forcing TS support is a regret in the making
@rayfoss I like flow, but its ecosystem and supports are far less than ts.
Native add ons are the way to go
It'd be brilliant if deno native modules are WASM with WASI. Then we free up the ecosystem to use various languages for the plugins.
Funny that this came up when I searched for Flow... Flow is a better typed Js then Typescript. Forcing TS support is a regret in the making
How's flow?
Most helpful comment
@rayfoss I like flow, but its ecosystem and supports are far less than ts.