| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
Review requested:
|
Sorry, something went wrong.
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #65273 +/- ##
========================================
Coverage 90.12% 90.13%
========================================
Files 752 752
Lines 252297 252414 +117
Branches 47432 47452 +20
========================================
+ Hits 227393 227523 +130
+ Misses 16217 16199 -18
- Partials 8687 8692 +5
... and 31 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Sorry, something went wrong.
|
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. |
Sorry, something went wrong.
|
@jasnell I believe semver-major is no longer needed. |
Sorry, something went wrong.
|
Benchmark GHA (webstreams): https://github.com/nodejs/node/actions/runs/31808780686 |
Sorry, something went wrong.
|
|
||
| // Materialize the deferred default controller for `new ReadableStream()`. | ||
| // started is true immediately: start is a no-op and there is no initial pull. | ||
| function ensureEmptyDefaultController(stream) { |
There was a problem hiding this comment.
Is this correctly handled in subclasses? A subclass could end up calling cancel, getReader, etc before the constructor finishes, causing the controller to be materialized. Worth documenting and tests.
Sorry, something went wrong.
There was a problem hiding this comment.
Looks okay to me? As explained in anonrig's previous comment, the controller is materialized in all those methods:
Subclasses don't really affect this: if a subclass wanted to get access to the controller, they'd still need to pass a source object with a start/pull method, which pushes them off the empty-argument path.
Sorry, something went wrong.
There was a problem hiding this comment.
Some additional review comments. Will review again once merge conflicts are resolved and I'd like @MattiasBuelens to review before this proceeds.
Sorry, something went wrong.
| } | ||
|
|
||
| // Materialize the deferred default controller for `new ReadableStream()`. | ||
| // started is true immediately: start is a no-op and there is no initial pull. |
There was a problem hiding this comment.
I'm actually impressed that this still passes WPT? 😅 I know I've found a lot of subtle issues in the tests where the behavior changed slightly depending on whether or not the test waits for the stream to be started.
Sorry, something went wrong.
| return; | ||
| } | ||
| if (isReadableStreamDefaultController(controller)) | ||
| controller.error(error); |
There was a problem hiding this comment.
Off-topic, but this should really call the abstract op instead of going through a method lookup.
| controller.error(error); | |
| readableStreamDefaultControllerError(controller, error); |
Sorry, something went wrong.
|
|
||
| // Materialize the deferred default controller for `new ReadableStream()`. | ||
| // started is true immediately: start is a no-op and there is no initial pull. | ||
| function ensureEmptyDefaultController(stream) { |
There was a problem hiding this comment.
Looks okay to me? As explained in anonrig's previous comment, the controller is materialized in all those methods:
Subclasses don't really affect this: if a subclass wanted to get access to the controller, they'd still need to pass a source object with a start/pull method, which pushes them off the empty-argument path.
Sorry, something went wrong.
|
Since I'm buried in a few other things and it looks like this is mostly being updated by AI anyway, I had my agent draft up a review. I skimmed it over and can't disagree with any part of it: Details0. Reconstructed history (since the squash hid it)I fetched all four force-pushed heads. The PR was never a single commit until the last push:
The rebase matters more than the squash: a large fraction of this PR already landed on main as #65138 Three things happened in the squash that reviewers can't see:
Please push the review responses as fixup commits and let the commit-queue squash them. The 1. The PR description no longer describes the PR
The commit message says "Behavior-preserving" while the PR carries semver-major. Per 2. Breaking changes2.1 resolvedRecord() leaks one shared promise to userland// lib/internal/webstreams/util.js:433
function resolvedRecord() {
return { promise: kResolvedPromise, /* was: PromiseResolve() */ ... };
}There are five resolvedRecord() call sites. Three of them are reached from public getters:
¹ kIsClosedPromise is SymbolFor('nodejs.webstream.isClosedPromise') — a registered symbol, so There are two directly reachable spec violations: // 1. cross-stream identity leak — no closing or backpressure needed
const a = new WritableStream().getWriter();
const b = new WritableStream().getWriter();
a.ready === b.ready // true after this PR; false on main
// 2. two distinct internal slots collapse to one object
const w = /* writer on a closed WritableStream */;
w.closed === w.ready // true after this PR; false on mainPer spec [[closedPromise]] and [[readyPromise]] are separate slots, each initialised to "a new WPT's aborting.any.js:43 / :1128 compare resolved-vs-pending and resolved-vs-rejected promises, One more thing worth confirming (I have not executed it): kResolvedPromise is also the object the Suggested fix. The constraint is narrow: writerClosedPromise() and writerReadyPromise() back
I'd expect either to be unmeasurable. Every consumer is cold: the two public getters, [kInspect] Directly exposing the shared promise is not only breaking, it is potentially exploitable. 2.2 BLOCKER — two mutually inconsistent timing-compensation helpersfunction promiseFromAlgorithmResult(result) { // 0 extra ticks
if (isNonThenable(result)) return kResolvedPromise;
return PromiseResolve(result);
}
function delayedAlgorithmResult(result) { // +1 tick vs. old
if (isNonThenable(result)) return kResolvedPromise;
return PromisePrototypeThen(kResolvedPromise, () => result);
}createPromiseCallback{NoParams,1Param,2Params} stopped being async. For a non-thenable user
Reachable, user-observable, on public API:
The comment on delayedAlgorithmResult gives the game away:
That's a compensation reverse-engineered from a failing test, not derived from the spec. Two helpers 2.3 The write-side batching is the same deviation fillSync was removed forwritableStreamDefaultControllerDrainWriteQueue() (writablestream.js:1201) processes consecutive if (stream[kState].inFlightWriteRequest.promise === null) {
writableStreamDefaultControllerCompleteWrite(controller);
continue; // <-- no microtask between writes
}Spec WritableStreamDefaultControllerProcessWrite ends with "Upon fulfillment of sinkWritePromise … It's also a layering violation: writablestream.js now branches on promise === null, a sentinel 2.4 cloneAsUint8Array changes the detached-buffer errorOld path threw V8's TypeError: Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer. 2.5 Lazy AbortController — looks correct, but check [kControllerErrorFunction](this[kState].abortController ??= new AbortController()) in both the getter (writablestream.js:547) 3. Complexity vs. gain3.1 The benchmarks measure the special cases this PR addsbenchmark/webstreams/creation.js times new ReadableStream() / new WritableStream() with no Worse, it likely pessimizes the real path: createReadableStreamState() (readablestream.js:1439) benchmark/webstreams/pipe-to.js uses write(chunk, controller) {} — a fully synchronous sink. That 3.2 writableStreamDefaultControllerDrainWriteQueue duplicates two spec algorithmsIt is a copy of writableStreamDefaultControllerAdvanceQueueIfNeeded (:1372) plus an inlined
3.3 The native binding is not justifiedsrc/node_webstreams.cc exists for two functions. isNonThenable replaces this inline JS: result === null || (typeof result !== 'object' && typeof result !== 'function')TurboFan compiles that to a couple of map/instance-type checks. A Fast API call cannot beat it, and in
— assumes the alternative is a C++ call. It isn't; the alternative is two inlined typeofs. Please Also, the implementation is dead-code-y and not quite equivalent to the JS: return value->IsNullOrUndefined() || (!value->IsObject() && !value->IsFunction());v8::Value::IsObject() is IsJSReceiver(), which is already true for functions — !value->IsObject() cloneAsUint8Array is a more plausible win (one binding call instead of Either way, adding a whole new internalBinding + node.gyp entry + external-reference registration + 3.4 Ask: per-change benchmark attributionThis PR bundles at least six independent optimizations behind one aggregate ratio. Given that #65138
My prior is that (2) is the only one with a defensible number, (1) is the only one with a real 4. Test gapstest/parallel/test-whatwg-webstreams-hotpath.js is decent on isNonThenable/Proxy and the subclass
5. Nits
6. RecommendationRequest changes. Concretely, before this can be re-reviewed:
|
Sorry, something went wrong.
|
@jasnell do you think that this is still semver major or can we remove the label? |
Sorry, something went wrong.
|
Still semver-major |
Sorry, something went wrong.
|
@nodejs/tsc since this is semver major, it requires your review. |
Sorry, something went wrong.
|
@anonrig ... see the details in #65273 (comment) for a longer review. i'll try to do a line-by-line review later this week. |
Sorry, something went wrong.
Sorry, something went wrong.
There was a problem hiding this comment.
Code looks okay. I don't think this is semver-major anymore.
That said, since the scope of this PR is now reduced, the PR description should reflect that.
Sorry, something went wrong.
Avoid per-chunk async wrappers for sync pull/write/start 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. Use the shared kResolvedPromise on the pull/write hot path instead of allocating PromiseResolve(). Assisted-by: Grok Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Stop sharing kResolvedPromise on writer.ready/closed and cancel(). Restore async wrappers for cancel/close/abort/flush/transform so thenable results keep the previous microtask count. Remove the write-queue drain loop so each write stays one microtask apart. Drop the native webstreams binding. isNonThenable and cloneAsUint8Array stay in JS so a detached buffer still throws TypeError. Initialize the deferred controller field and skip materializing it on cancel of a non-readable empty stream. Assisted-by: Grok Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
| Back | FazBrowse Home | New Git URL |
Leftover construction-path work on node:stream/web after #65138 landed the shared hot-path reductions (thenAlgorithmResult, kResolvedPromise, raw pull/write callbacks, parkOnReady). Rebased onto current main, including #65143 (transform backpressure decoupling). That PR's raw defaultTransformAlgorithm / createRawCallback2Params path is kept.
The earlier body described work that is no longer in this tree (pipeTo sync-fill, write-queue drain, native webstreams binding, shared kResolvedPromise on public slots). This is what remains.
What remains
Review follow-up (removed)
Addressed #65273 (comment):
Semver
The earlier semver-major concerns were the removed fillSync / drain / shared-promise / timing-helper changes. Public constructors, methods, and WHATWG Streams behavior (backpressure, BYOB, pipeTo, tee, errors, transfer) are intended to be unchanged. @MattiasBuelens noted the remaining scope does not look semver-major.
Benchmarks
The numbers previously posted here were measured against a base that predates #65138 and included the removed optimizations. They are not this PR's remaining delta. A full webstreams/ compare against current main still needs to be posted.
Tests
AI assistance
This change was developed with assistance from Grok. I reviewed, tested, and take responsibility for the submitted code.