stream: speed up WHATWG web streams - #65273
Conversation
|
Review requested:
|
Avoid per-chunk async wrappers for sync pull/write/start, fill default readable queues in pipeTo, and complete pipeTo writes without one microtask per chunk. Add a native webstreams binding with a Fast API isNonThenable check on the data plane and a memcpy clone for byte views. Empty stream construction skips redundant validation and lazily creates the writable AbortController, materializing it on abort() so controller.signal still reflects the abort reason. Assisted-by: Grok Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Declare isNonThenable and cloneAsUint8Array on the new webstreams binding and register it in InternalBindingMap. Assisted-by: Grok Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
6a3f89b to
c796760
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #65273 +/- ##
==========================================
+ Coverage 90.31% 90.32% +0.01%
==========================================
Files 751 752 +1
Lines 249840 250072 +232
Branches 47180 47240 +60
==========================================
+ Hits 225645 225882 +237
+ Misses 15587 15559 -28
- Partials 8608 8631 +23
🚀 New features to boost your workflow:
|
| // The empty-argument constructor is the creation.js / `new | ||
| // ReadableStream()` hot path: skip validateObject and strategy/source | ||
| // extraction when both arguments are the shared default sentinel. | ||
| if (source === kEmptyObject && strategy === kEmptyObject) { |
There was a problem hiding this comment.
In this case, we ought to be able to fully elide the creation of the standard controller entirely. The result here is a completely useless stream whose reads will never resolve. It's a degenerate case that is likely quite unlikely, but if it happens, we may as not waste the additional allocations.
| (error) => readableStreamDefaultControllerError(controller, error); | ||
| } | ||
| const result = controller[kState].pullAlgorithm(controller); | ||
| if (isNonThenable(result)) { |
There was a problem hiding this comment.
This should be clearly documented as a non-standard and potentially breaking behavior. The pull algorithm is defined by the spec as "a promise-returning algorithm". The elimination of the microtask continuation likely makes this semver-major.
| } | ||
| const result = controller[kState].pullAlgorithm(controller); | ||
| if (isNonThenable(result)) { | ||
| queueMicrotask(controller[kState].pullFulfilled); |
There was a problem hiding this comment.
Care should be taken here. If the pullFulfilled happens to throw for whatever reason, the error is going to be propagated differently than in the promise case below. Not a significant issue since pullfulfilled really shouldn't throw but the case needs to be carefully evaluated.
| validateFunction(fn, name); | ||
| return async () => FunctionPrototypeCall(fn, thisArg); | ||
| return () => { | ||
| try { |
| function nonOpCancel() {} | ||
|
|
||
| async function nonOpWrite() {} | ||
| function nonOpWrite() {} |
There was a problem hiding this comment.
Just have a single no-op function. There's no reason to create multiple functions that do nothing.
| static bool IsNonThenableValue(Local<Value> value) { | ||
| return value->IsNullOrUndefined() || | ||
| (!value->IsObject() && !value->IsFunction()); | ||
| } |
There was a problem hiding this comment.
I don't understand why this needs to be a C++ function. These are all checks that can be done cheaply and easily in JS. Does this handle Proxy?
There was a problem hiding this comment.
Proxy is handled fine, but would add a test.
|
Defensively marking this semver-major. If you can show that the optimization does not change observable behavior, that can be dropped, but the change in microtask timing from one pull to the next is likely observable. |
| state.pulling = true; | ||
| const before = state.queue.length; | ||
| const result = state.pullAlgorithm(controller); | ||
| if (isNonThenable(result)) { |
There was a problem hiding this comment.
The name here is ever-so-slightly-misleading. An object with no then property is technically not a "thenable" ... Pedantic, yes.
Behavior-preserving performance work on
node:stream/web. Specorchestration and brand checks stay in JS; the per-chunk data plane
drops Promise/microtask churn for the common sync pull/write case
and gets a small native helper on the hot path.
asyncwrappers on sync source/sink algorithms.createPromiseCallback*now calls the user function and returnsthe raw result. Non-thenable results settle via
queueMicrotask(same position as
Promise.resolve().then) instead of allocatinga Promise per pull/write/start.
pipeTofills a default readable queue from sync pulls andstill batches already-queued chunks into the destination. Further
spec pull-fulfillment (tee, WPT) stays one pull per microtask.
the fulfillment turn. Regular
writer.write()keeps the specone-completion-per-microtask order.
internalBinding('webstreams'): Fast APIisNonThenable()on every pull/write/start result, andcloneAsUint8Array()as a single memcpy for byte-stream / teeclones.
new ReadableStream()/new WritableStream()skipvalidateObjecton the shared emptysentinels. The writable
AbortControlleris created lazily andmaterialized on
abort(), socontroller.signalobserved afterabort is still aborted with that reason.
Public constructors, methods, and WHATWG Streams behavior
(backpressure, BYOB, pipeTo, tee, errors, transfer) are unchanged.
Benchmarks
benchmark/compare.js --runs 10of the in-repowebstreams/suiteon the same machine, same
out/Release/nodefamily (pre-changebinary vs this tree). Rates are ops/sec.
Hot-path geometric mean of
new/oldacross all configs ofpipe-to.js,readable-read.js,readable-read-buffered.js,creation.js,readable-async-iterator.js, andtee.js:1.94x (32 configs). Full suite including
js_transfer.js:1.84x (35 configs). No config has
mean(new)/mean(old) < 1.0(min 1.03 on
js_transferReadableStream).Tests
test/wpt/test-streams.jstest/parallel/test-whatwg-readable*,writable*,transform*,webstreams*,test-webstreams*,test-global-webstreams.jstest/parallel/test-whatwg-webstreams-hotpath.js(publicread()/pipeTo, native helpers, abort-before-signal)test-whatwg-writablestream.jsAI assistance
This change was developed with assistance from Grok. I reviewed,
tested, and take responsibility for the submitted code.