stream: cut per-chunk allocations in pipeTo - #64890
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #64890 +/- ##
========================================
Coverage 90.28% 90.29%
========================================
Files 760 760
Lines 247076 247220 +144
Branches 46594 46618 +24
========================================
+ Hits 223081 223224 +143
Misses 15458 15458
- Partials 8537 8538 +1
🚀 New features to boost your workflow:
|
mcollina
force-pushed
the
webstream-perf-round11
branch
from
August 1, 2026 06:51
c084df5 to
40a4e44
Compare
readableStreamPipeTo allocated, for every chunk written to the
destination, a { promise, resolve, reject } write request record that
it immediately marked as handled, and drove its loop with an async
step()/run() pair whose implicit promises cost one allocation and one
reaction per iteration. The parked-read path additionally allocated a
read request object, a PromiseWithResolvers record, and a microtask
closure per chunk; this is the steady state for pipeThrough, since a
TransformStream's readable side has a high water mark of zero.
Replace the per-write records with a single per-pipe tracker that the
write request queue holds once per pending write and whose
resolve()/reject() methods maintain a pending-write count, drive the
pump loop with plain callbacks instead of async functions, and reuse
one read request and one forwarding function across all chunks, the
same pattern tee uses since c543cfb.
Benchmark results (benchmark/compare.js --runs 20):
webstreams/pipe-to.js +29.9% to +35.8% across all 16 configurations
(all 99.9% confidence); a pipeThrough(TransformStream) passthrough
loop improves ~17%; every other webstreams benchmark is unchanged.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
mcollina
force-pushed
the
webstream-perf-round11
branch
from
August 2, 2026 07:30
40a4e44 to
d838534
Compare
jasnell
approved these changes
Aug 2, 2026
anonrig
approved these changes
Aug 3, 2026
Collaborator
Contributor
|
Benchmark GHA (webstreams): https://github.com/nodejs/node/actions/runs/30892026992 Results
Benchmark results:
|
Collaborator
|
Landed in 3fc98b8 |
4 tasks
aduh95
pushed a commit
that referenced
this pull request
Aug 4, 2026
readableStreamPipeTo allocated, for every chunk written to the
destination, a { promise, resolve, reject } write request record that
it immediately marked as handled, and drove its loop with an async
step()/run() pair whose implicit promises cost one allocation and one
reaction per iteration. The parked-read path additionally allocated a
read request object, a PromiseWithResolvers record, and a microtask
closure per chunk; this is the steady state for pipeThrough, since a
TransformStream's readable side has a high water mark of zero.
Replace the per-write records with a single per-pipe tracker that the
write request queue holds once per pending write and whose
resolve()/reject() methods maintain a pending-write count, drive the
pump loop with plain callbacks instead of async functions, and reuse
one read request and one forwarding function across all chunks, the
same pattern tee uses since c543cfb.
Benchmark results (benchmark/compare.js --runs 20):
webstreams/pipe-to.js +29.9% to +35.8% across all 16 configurations
(all 99.9% confidence); a pipeThrough(TransformStream) passthrough
loop improves ~17%; every other webstreams benchmark is unchanged.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #64890
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
aduh95
pushed a commit
that referenced
this pull request
Aug 5, 2026
readableStreamPipeTo allocated, for every chunk written to the
destination, a { promise, resolve, reject } write request record that
it immediately marked as handled, and drove its loop with an async
step()/run() pair whose implicit promises cost one allocation and one
reaction per iteration. The parked-read path additionally allocated a
read request object, a PromiseWithResolvers record, and a microtask
closure per chunk; this is the steady state for pipeThrough, since a
TransformStream's readable side has a high water mark of zero.
Replace the per-write records with a single per-pipe tracker that the
write request queue holds once per pending write and whose
resolve()/reject() methods maintain a pending-write count, drive the
pump loop with plain callbacks instead of async functions, and reuse
one read request and one forwarding function across all chunks, the
same pattern tee uses since c543cfb.
Benchmark results (benchmark/compare.js --runs 20):
webstreams/pipe-to.js +29.9% to +35.8% across all 16 configurations
(all 99.9% confidence); a pipeThrough(TransformStream) passthrough
loop improves ~17%; every other webstreams benchmark is unchanged.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #64890
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
aduh95
pushed a commit
that referenced
this pull request
Aug 7, 2026
readableStreamPipeTo allocated, for every chunk written to the
destination, a { promise, resolve, reject } write request record that
it immediately marked as handled, and drove its loop with an async
step()/run() pair whose implicit promises cost one allocation and one
reaction per iteration. The parked-read path additionally allocated a
read request object, a PromiseWithResolvers record, and a microtask
closure per chunk; this is the steady state for pipeThrough, since a
TransformStream's readable side has a high water mark of zero.
Replace the per-write records with a single per-pipe tracker that the
write request queue holds once per pending write and whose
resolve()/reject() methods maintain a pending-write count, drive the
pump loop with plain callbacks instead of async functions, and reuse
one read request and one forwarding function across all chunks, the
same pattern tee uses since c543cfb.
Benchmark results (benchmark/compare.js --runs 20):
webstreams/pipe-to.js +29.9% to +35.8% across all 16 configurations
(all 99.9% confidence); a pipeThrough(TransformStream) passthrough
loop improves ~17%; every other webstreams benchmark is unchanged.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #64890
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
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.
readableStreamPipeTo allocated, for every chunk written to the destination, a
{ promise, resolve, reject }write request record that it immediately marked as handled, and drove its loop with an asyncstep()/run()pair whose implicit promises cost one allocation and one reaction per iteration. The parked-read path additionally allocated a read request object, aPromiseWithResolversrecord, and a microtask closure per chunk; this is the steady state forpipeThrough, since a TransformStream's readable side has a high water mark of zero.This PR replaces the per-write records with a single per-pipe tracker that the write request queue holds once per pending write and whose
resolve()/reject()methods maintain a pending-write count, drives the pump loop with plain callbacks instead of async functions, and reuses one read request and one forwarding function across all chunks — the same patternteeuses since c543cfb.Semantics preserved: shutdown still waits for all pending writes before finalizing (the tracker arms a stall promise only during shutdown), a write that cannot proceed latches its error exactly like the old rejected-and-marked-handled
currentWritedid, and the spec-required microtask before writing a parked chunk is kept.Benchmark results
benchmark/compare.js --runs 20vs current main:A spot
pipeThrough(new TransformStream())passthrough loop (500k chunks) improves ~17% (0.41 → 0.49 M chunks/s), since the outer pipe of a passthrough runs the parked-read path for every chunk. All other webstreams benchmarks are unchanged (verified with a 20-run recheck of the two rows that initially flagged, both phantom).Gates: WPT streams/compression/encoding, the full parallel whatwg/webstream suites, and the blob/fetch/filehandle/duplex adapter tests all pass.