Conversation
mcollina
force-pushed
the
webstream-perf-round17
branch
from
September 16, 2026 00:35
ba231e2 to
9a19fef
Compare
Short-lived streams (create, a few chunks, close) pay a fixed cost per stream that dominates once the per-chunk path is lean. Streams created internally (transform stream sides, tee branches, ReadableStream.from, transferred streams) were built by wrapper constructors that swapped the prototype of every instance and then assigned an own, enumerable `constructor` property to look like a public stream. Each internal stream therefore had its own hidden class and `Object.keys(stream)` reported `['constructor']`. The public constructors now accept the internal construction sentinel and leave controller setup to the caller, so every ReadableStream and WritableStream shares one hidden class and no per-instance prototype swap or own property is needed. The queue ring buffer grew after a push filled it, so the initial 8-slot ring held only three (value, size) pairs and a four-chunk stream reallocated every time. Growing before the push lets the ring hold four pairs. pipeTo observed the source's closed promise with two reactions and, on teardown, let the reader and writer release paths probe and reject promise records that only the pipe could have observed. One reaction pair now watches the source, and finalize drops the records before release. Signed-off-by: Matteo Collina <hello@matteocollina.com>
mcollina
force-pushed
the
webstream-perf-round17
branch
from
September 16, 2026 00:36
9a19fef to
906ea04
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Round 17 of the webstreams performance work (follows #65625). This one targets the fixed per-stream cost of short-lived streams (create, a few chunks, close), which dominates once the per-chunk path is lean.
stream: unify internal webstream construction
Streams created internally (transform stream sides, tee branches,
ReadableStream.from, transferred streams) were built by wrapper constructors that swapped the prototype of every instance and then assigned an own, enumerableconstructorproperty. Each internal stream had its own hidden class, andObject.keys(new TransformStream().readable)reported['constructor']. The public constructors now accept the internal construction sentinel and leave controller setup to the caller: one hidden class per stream type, no per-instance prototype swap, no own property. A test pins the observable part.stream: trim webstream setup and teardown work
pipeTowatched the source's closed promise with two reactions and, on teardown, let the reader and writer release paths probe and reject promise records only the pipe could observe. One reaction pair now watches the source, and finalize drops the records before release.Benchmarks
node benchmark/compare.js --runs 30 webstreams, all 43 rows; only the significant ones listed, everything else is within noise:The last row is untouched code and sits at the edge of its interval; with 43 rows one such
*is the expected false positive.The lifecycle benchmark drives its source through
pull()one chunk at a time. A source that enqueues its chunks fromstart()(the common "body already in memory" shape) gains more, because it also hits the queue growth and the batch path: create → 4×1KB → close measured at +16% (getReaderloop), +17% (pipeTo) and +20% (pipeThrough+pipeTo) on this machine, three interleaved runs each.Ordering is unchanged: a 28-scenario microtask-ordering stress (start variants, transform start/cancel/error interleavings, pipeTo shutdown paths, tee, byte streams) logs identically against
main, apart from the removedconstructorkey. WPT streams/compression/encoding and the webstreams parallel batch are green.