Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoiding 10 future regrets for deno #241

Closed
jorangreef opened this issue Jun 11, 2018 · 8 comments
Closed

Avoiding 10 future regrets for deno #241

jorangreef opened this issue Jun 11, 2018 · 8 comments

Comments

@jorangreef
Copy link

@ry Thanks for deno! Your goals for deno are spot on.

Just wanted to share some thoughts I've had working with Node.

  1. 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.

  2. 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).

  3. 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.

  4. 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.

  5. Personally, I think Rust will be better than Go down the line for native bindings. One GC in v8 is enough!

  6. 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!

@ry
Copy link
Member

ry commented Jun 11, 2018

@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

@piscisaureus
Copy link
Member

@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.

  • "Reading into a user-supplied target buffer" and "zero-copy" are somewhat at odds with one another, in particular when it comes to javascript. It works when we can do poll + nonblocking read/write, but once we have to do something in the thread pool (e.g. crypto, gzip, filesystem I/O), it means that buffer contents are going to be modified by a different thread, and if the buffer is user-specified this effect becomes directly observable. The v8 people always told me that what node is doing is technically against its API rules (although it works in practice), and TC39+DOM standards people have traditionally resisted APIs which would make multithreading effects observable (although nowadays there's SharedArrayBuffer so maybe this is no longer that relevant.)
  • In libuv, we made the user responsible for allocating memory that libuv used to store "internal" state associated with a handle (e.g socket, timer, etc). This is somewhat nice because it reduces the number of heap allocations, but it also makes the API very leaky. Now the user needs to know how much space libuv needs, and if libuv changes something about how it manages state internally, this immediately becomes an API/ABI changes. In hindsight I regret this decision.

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. nodejs/node#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!

@piscisaureus
Copy link
Member

piscisaureus commented Jun 11, 2018

@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.

@jorangreef
Copy link
Author

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

@FossPrime
Copy link

FossPrime commented Aug 9, 2018

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

@zhaoyao91
Copy link

@rayfoss I like flow, but its ecosystem and supports are far less than ts.

@mattyclarkson
Copy link

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.

piscisaureus pushed a commit to piscisaureus/deno that referenced this issue Oct 7, 2019
@alevosia
Copy link

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?

hardfist pushed a commit to hardfist/deno that referenced this issue Aug 7, 2024
Supported using the syntax: #[arraybuffer] buffer: &[u8] (or &mut [u8]). All buffer types supported by #[buffer] are supported by #[arraybuffer].

This is slightly slower than typed arrays during fastcalls because we need to extract the data pointer from the underlying v8 ArrayBuffer handle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants