stream: cut promise churn in webstreams hot paths - #65138

Merged
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12
Aug 15, 2026
Merged

stream: cut promise churn in webstreams hot paths#65138
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12

Conversation

@mcollina

Copy link
Copy Markdown
Member

Twelfth round of pure-JS webstreams optimizations, following #64890.

Profiling pipeTo at the default highWaterMark showed roughly 10% of the profile in queueMicrotask plus its native binding (one call per pump batch), a fresh writer.ready promise record plus reaction per backpressure flip on the pipeThrough shape, and an implicit async-wrapper promise per sink.write/source.pull invocation. Three changes, one commit:

  • Non-thenable algorithm fast path. User sink.write and source.pull callbacks are wrapped without coercing their result into a promise. A non-thenable result (the common synchronous case) means fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had. Thenable results go through PromiseResolve(), matching the reference implementation's promiseCall (identity for native promises).
  • pipeTo parks on backpressure via a ready-record hook. The pump installs a record that duck-types the writer's lazily-materialized [[readyPromise]] record; writableStreamUpdateBackpressure resolving it re-enters the pump directly. This removes the per-flip promise record + reaction and the per-batch queueMicrotask. The backpressure state field is now published before the ready record is resolved so the hook observes the new value (the resolve of a real ready record only settles a promise, so the reorder is unobservable otherwise).
  • Chunk-forwarding microtasks via a shared resolved promise. The pipeTo/tee forwardChunk hops and the pump's between-batch yield use a reaction on the shared resolved promise instead of queueMicrotask, which enqueues at the same position with less overhead.

A separate first commit fixes the pipe-to.js benchmark: the highWaterMark values were passed inside the underlying source/sink dictionaries where they are ignored, so all 16 configurations measured the identical workload at the default highWaterMark of 1. The strategies are now passed as the constructors' second argument, with the matrix covering the default (1) and buffered (1024, 4096) configurations.

Benchmark results with the fixed benchmark (30 runs):

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 * 9.34 % ±8.20% ±10.90% ±14.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 ** 12.81 % ±8.69% ±11.57% ±15.06%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 ** 14.09 % ±8.45% ±11.24% ±14.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 * 8.87 % ±7.41% ±9.86% ±12.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 * 9.51 % ±8.64% ±11.50% ±14.97%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 * 8.01 % ±7.93% ±10.55% ±13.74%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 * 9.36 % ±8.27% ±11.01% ±14.33%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** 11.51 % ±8.56% ±11.39% ±14.82%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * 10.78 % ±8.37% ±11.14% ±14.50%

The full-suite run showed no regressions in any other family; readable-read normal, tee normal, and pipeThrough passthrough spot runs also improve (~+6-18%). Verified with the WPT streams/compression/encoding suites, the full parallel webstream/whatwg test set, and a shutdown-ordering stress (abort mid-write, close with pending writes, sync-throwing and rejecting sinks, error propagation) whose event log is byte-identical to main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Aug 8, 2026
Comment threadlib/internal/webstreams/util.js
Comment threadlib/internal/webstreams/util.js Outdated

@jasnelljasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, otherwise LGTM

@codecov

codecovBot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (4551732) to head (2929417).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65138 +/- ##
==========================================
+ Coverage 90.30% 90.34% +0.03% 
==========================================
Files 751 751 Lines 250235 250330 +95 Branches 47305 47321 +16 ==========================================
+ Hits 225987 226171 +184 + Misses 15619 15557 -62 + Partials 8629 8602 -27 
Files with missing linesCoverage Δ
lib/internal/webstreams/readablestream.js98.18% <100.00%> (+0.02%)⬆️
lib/internal/webstreams/util.js97.89% <100.00%> (+0.17%)⬆️
lib/internal/webstreams/writablestream.js99.52% <100.00%> (+<0.01%)⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gurgundaygurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically on 0ff1bfc, WDYT about this?

https://github.com/whatwg/streams/blob/29997074ffcfbe5b72a4451775398a54cef5b297/reference-implementation/lib/helpers/webidl.js#L45-L48

I think there was a reason Promise.resolve wasn't used here, and I'm not sure what the ramifications of using it might be

@mcollina

Copy link
Copy Markdown
MemberAuthor

As long as WPT passes, I'm good if we can avoid a promise allocation.

That specifically seems non-observable behavior.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@mcollina
mcollina requested review from aduh95 and anonrigAugust 12, 2026 03:54
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/internal/webstreams/readablestream.js Outdated
@github-actions

github-actionsBot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams / pipe-to): https://github.com/nodejs/node/actions/runs/31826854651

Results

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

Benchmark results:

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 25.57 % ±7.64% ±10.07% ±12.92%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 *** 29.57 % ±7.55% ±9.95% ±12.78%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 *** 28.22 % ±7.35% ±9.69% ±12.44%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 *** 19.26 % ±7.14% ±9.42% ±12.09%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 *** 20.69 % ±7.10% ±9.36% ±12.01%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** 20.21 % ±6.83% ±9.00% ±11.55%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 *** 21.79 % ±7.11% ±9.37% ±12.03%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 *** 20.12 % ±7.17% ±9.45% ±12.13%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 *** 19.61 % ±7.01% ±9.25% ±11.87%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 9 comparisons, you can thus
expect the following amount of false-positive results:
0.45 false positives, when considering a 5% risk acceptance (*, **, ***),
0.09 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95aduh95 added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65138
✔ Done loading data for nodejs/node/pull/65138
----------------------------------- PR info ------------------------------------
Title stream: cut promise churn in webstreams hot paths (#65138)
Author Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch mcollina:webstream-perf-round12 -> nodejs:main
Labels author ready, needs-ci, commit-queue, web streams
Commits 4
- benchmark: apply highWaterMark in webstreams pipe-to
- stream: cut promise churn in webstreams hot paths
- stream: consolidate non-op algorithm callbacks
- Update lib/internal/webstreams/readablestream.js
Committers 2
- Matteo Collina <hello@matteocollina.com>
- GitHub <noreply@github.com>
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sat, 08 Aug 2026 15:01:49 GMT
✔ Approvals: 4
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4889126958
✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/65138#pullrequestreview-4941618229
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4937946571
✔ - Antoine du Hamel (@aduh95) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4943235120
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-15T04:53:41Z: https://ci.nodejs.org/job/node-test-pull-request/75855/
- Querying data for job/node-test-pull-request/75855/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ No git cherry-pick in progress
✔ No git am in progress
✔ No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
* branch main -> FETCH_HEAD
✔ origin/main is now up-to-date
- Downloading patch for 65138
From https://github.com/nodejs/node
* branch refs/pull/65138/merge -> FETCH_HEAD
✔ Fetched commits as 9e23066b8af4..0c0d5beaa88d
--------------------------------------------------------------------------------
[main eaaf996401] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 1d82311a65] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 6bb61acbad] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main e67bb74dca] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
✔ Patches applied
There are 4 commits in the PR. Attempting autorebase.
(node:400) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: apply highWaterMark in webstreams pipe-to

The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.

Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 5979438444] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Rebasing (3/8)
Rebasing (4/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: cut promise churn in webstreams hot paths

Three related reductions on the per-chunk paths:

Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).

Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.

Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.

pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 6536e7de9b] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Rebasing (5/8)
Rebasing (6/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: consolidate non-op algorithm callbacks

The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 2900aa12f7] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Rebasing (7/8)
Rebasing (8/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
Update lib/internal/webstreams/readablestream.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD f1d7d5568c] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/31872533736

@aduh95aduh95 added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. labels Aug 15, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
@aduh95
aduh95force-pushed the webstream-perf-round12 branch from 0c0d5be to 2929417CompareAugust 15, 2026 08:00
@aduh95

Copy link
Copy Markdown
Contributor

Landed in 4551732...2929417

@aduh95
aduh95 merged commit 2929417 into nodejs:mainAug 15, 2026
18 of 19 checks passed
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.needs-ciPRs that need a full CI run.web streamsIssues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@anonrig@gurgunday@bjohansebas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

stream: cut promise churn in webstreams hot paths - #65138

Merged
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12
Aug 15, 2026
Merged

stream: cut promise churn in webstreams hot paths#65138
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12

Conversation

@mcollina

Copy link
Copy Markdown
Member

Twelfth round of pure-JS webstreams optimizations, following #64890.

Profiling pipeTo at the default highWaterMark showed roughly 10% of the profile in queueMicrotask plus its native binding (one call per pump batch), a fresh writer.ready promise record plus reaction per backpressure flip on the pipeThrough shape, and an implicit async-wrapper promise per sink.write/source.pull invocation. Three changes, one commit:

  • Non-thenable algorithm fast path. User sink.write and source.pull callbacks are wrapped without coercing their result into a promise. A non-thenable result (the common synchronous case) means fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had. Thenable results go through PromiseResolve(), matching the reference implementation's promiseCall (identity for native promises).
  • pipeTo parks on backpressure via a ready-record hook. The pump installs a record that duck-types the writer's lazily-materialized [[readyPromise]] record; writableStreamUpdateBackpressure resolving it re-enters the pump directly. This removes the per-flip promise record + reaction and the per-batch queueMicrotask. The backpressure state field is now published before the ready record is resolved so the hook observes the new value (the resolve of a real ready record only settles a promise, so the reorder is unobservable otherwise).
  • Chunk-forwarding microtasks via a shared resolved promise. The pipeTo/tee forwardChunk hops and the pump's between-batch yield use a reaction on the shared resolved promise instead of queueMicrotask, which enqueues at the same position with less overhead.

A separate first commit fixes the pipe-to.js benchmark: the highWaterMark values were passed inside the underlying source/sink dictionaries where they are ignored, so all 16 configurations measured the identical workload at the default highWaterMark of 1. The strategies are now passed as the constructors' second argument, with the matrix covering the default (1) and buffered (1024, 4096) configurations.

Benchmark results with the fixed benchmark (30 runs):

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 * 9.34 % ±8.20% ±10.90% ±14.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 ** 12.81 % ±8.69% ±11.57% ±15.06%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 ** 14.09 % ±8.45% ±11.24% ±14.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 * 8.87 % ±7.41% ±9.86% ±12.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 * 9.51 % ±8.64% ±11.50% ±14.97%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 * 8.01 % ±7.93% ±10.55% ±13.74%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 * 9.36 % ±8.27% ±11.01% ±14.33%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** 11.51 % ±8.56% ±11.39% ±14.82%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * 10.78 % ±8.37% ±11.14% ±14.50%

The full-suite run showed no regressions in any other family; readable-read normal, tee normal, and pipeThrough passthrough spot runs also improve (~+6-18%). Verified with the WPT streams/compression/encoding suites, the full parallel webstream/whatwg test set, and a shutdown-ordering stress (abort mid-write, close with pending writes, sync-throwing and rejecting sinks, error propagation) whose event log is byte-identical to main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Aug 8, 2026
Comment threadlib/internal/webstreams/util.js
Comment threadlib/internal/webstreams/util.js Outdated

@jasnelljasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, otherwise LGTM

@codecov

codecovBot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (4551732) to head (2929417).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65138 +/- ##
==========================================
+ Coverage 90.30% 90.34% +0.03% 
==========================================
Files 751 751 Lines 250235 250330 +95 Branches 47305 47321 +16 ==========================================
+ Hits 225987 226171 +184 + Misses 15619 15557 -62 + Partials 8629 8602 -27 
Files with missing linesCoverage Δ
lib/internal/webstreams/readablestream.js98.18% <100.00%> (+0.02%)⬆️
lib/internal/webstreams/util.js97.89% <100.00%> (+0.17%)⬆️
lib/internal/webstreams/writablestream.js99.52% <100.00%> (+<0.01%)⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gurgundaygurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically on 0ff1bfc, WDYT about this?

https://github.com/whatwg/streams/blob/29997074ffcfbe5b72a4451775398a54cef5b297/reference-implementation/lib/helpers/webidl.js#L45-L48

I think there was a reason Promise.resolve wasn't used here, and I'm not sure what the ramifications of using it might be

@mcollina

Copy link
Copy Markdown
MemberAuthor

As long as WPT passes, I'm good if we can avoid a promise allocation.

That specifically seems non-observable behavior.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@mcollina
mcollina requested review from aduh95 and anonrigAugust 12, 2026 03:54
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/internal/webstreams/readablestream.js Outdated
@github-actions

github-actionsBot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams / pipe-to): https://github.com/nodejs/node/actions/runs/31826854651

Results

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

Benchmark results:

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 25.57 % ±7.64% ±10.07% ±12.92%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 *** 29.57 % ±7.55% ±9.95% ±12.78%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 *** 28.22 % ±7.35% ±9.69% ±12.44%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 *** 19.26 % ±7.14% ±9.42% ±12.09%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 *** 20.69 % ±7.10% ±9.36% ±12.01%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** 20.21 % ±6.83% ±9.00% ±11.55%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 *** 21.79 % ±7.11% ±9.37% ±12.03%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 *** 20.12 % ±7.17% ±9.45% ±12.13%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 *** 19.61 % ±7.01% ±9.25% ±11.87%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 9 comparisons, you can thus
expect the following amount of false-positive results:
0.45 false positives, when considering a 5% risk acceptance (*, **, ***),
0.09 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95aduh95 added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65138
✔ Done loading data for nodejs/node/pull/65138
----------------------------------- PR info ------------------------------------
Title stream: cut promise churn in webstreams hot paths (#65138)
Author Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch mcollina:webstream-perf-round12 -> nodejs:main
Labels author ready, needs-ci, commit-queue, web streams
Commits 4
- benchmark: apply highWaterMark in webstreams pipe-to
- stream: cut promise churn in webstreams hot paths
- stream: consolidate non-op algorithm callbacks
- Update lib/internal/webstreams/readablestream.js
Committers 2
- Matteo Collina <hello@matteocollina.com>
- GitHub <noreply@github.com>
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sat, 08 Aug 2026 15:01:49 GMT
✔ Approvals: 4
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4889126958
✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/65138#pullrequestreview-4941618229
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4937946571
✔ - Antoine du Hamel (@aduh95) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4943235120
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-15T04:53:41Z: https://ci.nodejs.org/job/node-test-pull-request/75855/
- Querying data for job/node-test-pull-request/75855/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ No git cherry-pick in progress
✔ No git am in progress
✔ No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
* branch main -> FETCH_HEAD
✔ origin/main is now up-to-date
- Downloading patch for 65138
From https://github.com/nodejs/node
* branch refs/pull/65138/merge -> FETCH_HEAD
✔ Fetched commits as 9e23066b8af4..0c0d5beaa88d
--------------------------------------------------------------------------------
[main eaaf996401] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 1d82311a65] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 6bb61acbad] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main e67bb74dca] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
✔ Patches applied
There are 4 commits in the PR. Attempting autorebase.
(node:400) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: apply highWaterMark in webstreams pipe-to

The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.

Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 5979438444] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Rebasing (3/8)
Rebasing (4/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: cut promise churn in webstreams hot paths

Three related reductions on the per-chunk paths:

Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).

Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.

Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.

pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 6536e7de9b] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Rebasing (5/8)
Rebasing (6/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: consolidate non-op algorithm callbacks

The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 2900aa12f7] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Rebasing (7/8)
Rebasing (8/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
Update lib/internal/webstreams/readablestream.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD f1d7d5568c] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/31872533736

@aduh95aduh95 added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. labels Aug 15, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
@aduh95
aduh95force-pushed the webstream-perf-round12 branch from 0c0d5be to 2929417CompareAugust 15, 2026 08:00
@aduh95

Copy link
Copy Markdown
Contributor

Landed in 4551732...2929417

@aduh95
aduh95 merged commit 2929417 into nodejs:mainAug 15, 2026
18 of 19 checks passed
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.needs-ciPRs that need a full CI run.web streamsIssues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@anonrig@gurgunday@bjohansebas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

stream: cut promise churn in webstreams hot paths - #65138

Merged
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12
Aug 15, 2026
Merged

stream: cut promise churn in webstreams hot paths#65138
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12

Conversation

@mcollina

Copy link
Copy Markdown
Member

Twelfth round of pure-JS webstreams optimizations, following #64890.

Profiling pipeTo at the default highWaterMark showed roughly 10% of the profile in queueMicrotask plus its native binding (one call per pump batch), a fresh writer.ready promise record plus reaction per backpressure flip on the pipeThrough shape, and an implicit async-wrapper promise per sink.write/source.pull invocation. Three changes, one commit:

  • Non-thenable algorithm fast path. User sink.write and source.pull callbacks are wrapped without coercing their result into a promise. A non-thenable result (the common synchronous case) means fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had. Thenable results go through PromiseResolve(), matching the reference implementation's promiseCall (identity for native promises).
  • pipeTo parks on backpressure via a ready-record hook. The pump installs a record that duck-types the writer's lazily-materialized [[readyPromise]] record; writableStreamUpdateBackpressure resolving it re-enters the pump directly. This removes the per-flip promise record + reaction and the per-batch queueMicrotask. The backpressure state field is now published before the ready record is resolved so the hook observes the new value (the resolve of a real ready record only settles a promise, so the reorder is unobservable otherwise).
  • Chunk-forwarding microtasks via a shared resolved promise. The pipeTo/tee forwardChunk hops and the pump's between-batch yield use a reaction on the shared resolved promise instead of queueMicrotask, which enqueues at the same position with less overhead.

A separate first commit fixes the pipe-to.js benchmark: the highWaterMark values were passed inside the underlying source/sink dictionaries where they are ignored, so all 16 configurations measured the identical workload at the default highWaterMark of 1. The strategies are now passed as the constructors' second argument, with the matrix covering the default (1) and buffered (1024, 4096) configurations.

Benchmark results with the fixed benchmark (30 runs):

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 * 9.34 % ±8.20% ±10.90% ±14.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 ** 12.81 % ±8.69% ±11.57% ±15.06%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 ** 14.09 % ±8.45% ±11.24% ±14.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 * 8.87 % ±7.41% ±9.86% ±12.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 * 9.51 % ±8.64% ±11.50% ±14.97%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 * 8.01 % ±7.93% ±10.55% ±13.74%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 * 9.36 % ±8.27% ±11.01% ±14.33%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** 11.51 % ±8.56% ±11.39% ±14.82%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * 10.78 % ±8.37% ±11.14% ±14.50%

The full-suite run showed no regressions in any other family; readable-read normal, tee normal, and pipeThrough passthrough spot runs also improve (~+6-18%). Verified with the WPT streams/compression/encoding suites, the full parallel webstream/whatwg test set, and a shutdown-ordering stress (abort mid-write, close with pending writes, sync-throwing and rejecting sinks, error propagation) whose event log is byte-identical to main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Aug 8, 2026
Comment threadlib/internal/webstreams/util.js
Comment threadlib/internal/webstreams/util.js Outdated

@jasnelljasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, otherwise LGTM

@codecov

codecovBot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (4551732) to head (2929417).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65138 +/- ##
==========================================
+ Coverage 90.30% 90.34% +0.03% 
==========================================
Files 751 751 Lines 250235 250330 +95 Branches 47305 47321 +16 ==========================================
+ Hits 225987 226171 +184 + Misses 15619 15557 -62 + Partials 8629 8602 -27 
Files with missing linesCoverage Δ
lib/internal/webstreams/readablestream.js98.18% <100.00%> (+0.02%)⬆️
lib/internal/webstreams/util.js97.89% <100.00%> (+0.17%)⬆️
lib/internal/webstreams/writablestream.js99.52% <100.00%> (+<0.01%)⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gurgundaygurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically on 0ff1bfc, WDYT about this?

https://github.com/whatwg/streams/blob/29997074ffcfbe5b72a4451775398a54cef5b297/reference-implementation/lib/helpers/webidl.js#L45-L48

I think there was a reason Promise.resolve wasn't used here, and I'm not sure what the ramifications of using it might be

@mcollina

Copy link
Copy Markdown
MemberAuthor

As long as WPT passes, I'm good if we can avoid a promise allocation.

That specifically seems non-observable behavior.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@mcollina
mcollina requested review from aduh95 and anonrigAugust 12, 2026 03:54
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/internal/webstreams/readablestream.js Outdated
@github-actions

github-actionsBot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams / pipe-to): https://github.com/nodejs/node/actions/runs/31826854651

Results

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

Benchmark results:

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 25.57 % ±7.64% ±10.07% ±12.92%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 *** 29.57 % ±7.55% ±9.95% ±12.78%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 *** 28.22 % ±7.35% ±9.69% ±12.44%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 *** 19.26 % ±7.14% ±9.42% ±12.09%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 *** 20.69 % ±7.10% ±9.36% ±12.01%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** 20.21 % ±6.83% ±9.00% ±11.55%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 *** 21.79 % ±7.11% ±9.37% ±12.03%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 *** 20.12 % ±7.17% ±9.45% ±12.13%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 *** 19.61 % ±7.01% ±9.25% ±11.87%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 9 comparisons, you can thus
expect the following amount of false-positive results:
0.45 false positives, when considering a 5% risk acceptance (*, **, ***),
0.09 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95aduh95 added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65138
✔ Done loading data for nodejs/node/pull/65138
----------------------------------- PR info ------------------------------------
Title stream: cut promise churn in webstreams hot paths (#65138)
Author Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch mcollina:webstream-perf-round12 -> nodejs:main
Labels author ready, needs-ci, commit-queue, web streams
Commits 4
- benchmark: apply highWaterMark in webstreams pipe-to
- stream: cut promise churn in webstreams hot paths
- stream: consolidate non-op algorithm callbacks
- Update lib/internal/webstreams/readablestream.js
Committers 2
- Matteo Collina <hello@matteocollina.com>
- GitHub <noreply@github.com>
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sat, 08 Aug 2026 15:01:49 GMT
✔ Approvals: 4
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4889126958
✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/65138#pullrequestreview-4941618229
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4937946571
✔ - Antoine du Hamel (@aduh95) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4943235120
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-15T04:53:41Z: https://ci.nodejs.org/job/node-test-pull-request/75855/
- Querying data for job/node-test-pull-request/75855/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ No git cherry-pick in progress
✔ No git am in progress
✔ No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
* branch main -> FETCH_HEAD
✔ origin/main is now up-to-date
- Downloading patch for 65138
From https://github.com/nodejs/node
* branch refs/pull/65138/merge -> FETCH_HEAD
✔ Fetched commits as 9e23066b8af4..0c0d5beaa88d
--------------------------------------------------------------------------------
[main eaaf996401] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 1d82311a65] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 6bb61acbad] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main e67bb74dca] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
✔ Patches applied
There are 4 commits in the PR. Attempting autorebase.
(node:400) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: apply highWaterMark in webstreams pipe-to

The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.

Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 5979438444] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Rebasing (3/8)
Rebasing (4/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: cut promise churn in webstreams hot paths

Three related reductions on the per-chunk paths:

Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).

Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.

Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.

pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 6536e7de9b] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Rebasing (5/8)
Rebasing (6/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: consolidate non-op algorithm callbacks

The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 2900aa12f7] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Rebasing (7/8)
Rebasing (8/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
Update lib/internal/webstreams/readablestream.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD f1d7d5568c] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/31872533736

@aduh95aduh95 added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. labels Aug 15, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
@aduh95
aduh95force-pushed the webstream-perf-round12 branch from 0c0d5be to 2929417CompareAugust 15, 2026 08:00
@aduh95

Copy link
Copy Markdown
Contributor

Landed in 4551732...2929417

@aduh95
aduh95 merged commit 2929417 into nodejs:mainAug 15, 2026
18 of 19 checks passed
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.needs-ciPRs that need a full CI run.web streamsIssues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@anonrig@gurgunday@bjohansebas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

stream: cut promise churn in webstreams hot paths - #65138

Merged
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12
Aug 15, 2026
Merged

stream: cut promise churn in webstreams hot paths#65138
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12

Conversation

@mcollina

Copy link
Copy Markdown
Member

Twelfth round of pure-JS webstreams optimizations, following #64890.

Profiling pipeTo at the default highWaterMark showed roughly 10% of the profile in queueMicrotask plus its native binding (one call per pump batch), a fresh writer.ready promise record plus reaction per backpressure flip on the pipeThrough shape, and an implicit async-wrapper promise per sink.write/source.pull invocation. Three changes, one commit:

  • Non-thenable algorithm fast path. User sink.write and source.pull callbacks are wrapped without coercing their result into a promise. A non-thenable result (the common synchronous case) means fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had. Thenable results go through PromiseResolve(), matching the reference implementation's promiseCall (identity for native promises).
  • pipeTo parks on backpressure via a ready-record hook. The pump installs a record that duck-types the writer's lazily-materialized [[readyPromise]] record; writableStreamUpdateBackpressure resolving it re-enters the pump directly. This removes the per-flip promise record + reaction and the per-batch queueMicrotask. The backpressure state field is now published before the ready record is resolved so the hook observes the new value (the resolve of a real ready record only settles a promise, so the reorder is unobservable otherwise).
  • Chunk-forwarding microtasks via a shared resolved promise. The pipeTo/tee forwardChunk hops and the pump's between-batch yield use a reaction on the shared resolved promise instead of queueMicrotask, which enqueues at the same position with less overhead.

A separate first commit fixes the pipe-to.js benchmark: the highWaterMark values were passed inside the underlying source/sink dictionaries where they are ignored, so all 16 configurations measured the identical workload at the default highWaterMark of 1. The strategies are now passed as the constructors' second argument, with the matrix covering the default (1) and buffered (1024, 4096) configurations.

Benchmark results with the fixed benchmark (30 runs):

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 * 9.34 % ±8.20% ±10.90% ±14.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 ** 12.81 % ±8.69% ±11.57% ±15.06%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 ** 14.09 % ±8.45% ±11.24% ±14.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 * 8.87 % ±7.41% ±9.86% ±12.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 * 9.51 % ±8.64% ±11.50% ±14.97%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 * 8.01 % ±7.93% ±10.55% ±13.74%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 * 9.36 % ±8.27% ±11.01% ±14.33%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** 11.51 % ±8.56% ±11.39% ±14.82%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * 10.78 % ±8.37% ±11.14% ±14.50%

The full-suite run showed no regressions in any other family; readable-read normal, tee normal, and pipeThrough passthrough spot runs also improve (~+6-18%). Verified with the WPT streams/compression/encoding suites, the full parallel webstream/whatwg test set, and a shutdown-ordering stress (abort mid-write, close with pending writes, sync-throwing and rejecting sinks, error propagation) whose event log is byte-identical to main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Aug 8, 2026
Comment threadlib/internal/webstreams/util.js
Comment threadlib/internal/webstreams/util.js Outdated

@jasnelljasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, otherwise LGTM

@codecov

codecovBot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (4551732) to head (2929417).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65138 +/- ##
==========================================
+ Coverage 90.30% 90.34% +0.03% 
==========================================
Files 751 751 Lines 250235 250330 +95 Branches 47305 47321 +16 ==========================================
+ Hits 225987 226171 +184 + Misses 15619 15557 -62 + Partials 8629 8602 -27 
Files with missing linesCoverage Δ
lib/internal/webstreams/readablestream.js98.18% <100.00%> (+0.02%)⬆️
lib/internal/webstreams/util.js97.89% <100.00%> (+0.17%)⬆️
lib/internal/webstreams/writablestream.js99.52% <100.00%> (+<0.01%)⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gurgundaygurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically on 0ff1bfc, WDYT about this?

https://github.com/whatwg/streams/blob/29997074ffcfbe5b72a4451775398a54cef5b297/reference-implementation/lib/helpers/webidl.js#L45-L48

I think there was a reason Promise.resolve wasn't used here, and I'm not sure what the ramifications of using it might be

@mcollina

Copy link
Copy Markdown
MemberAuthor

As long as WPT passes, I'm good if we can avoid a promise allocation.

That specifically seems non-observable behavior.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@mcollina
mcollina requested review from aduh95 and anonrigAugust 12, 2026 03:54
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/internal/webstreams/readablestream.js Outdated
@github-actions

github-actionsBot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams / pipe-to): https://github.com/nodejs/node/actions/runs/31826854651

Results

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

Benchmark results:

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 25.57 % ±7.64% ±10.07% ±12.92%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 *** 29.57 % ±7.55% ±9.95% ±12.78%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 *** 28.22 % ±7.35% ±9.69% ±12.44%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 *** 19.26 % ±7.14% ±9.42% ±12.09%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 *** 20.69 % ±7.10% ±9.36% ±12.01%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** 20.21 % ±6.83% ±9.00% ±11.55%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 *** 21.79 % ±7.11% ±9.37% ±12.03%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 *** 20.12 % ±7.17% ±9.45% ±12.13%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 *** 19.61 % ±7.01% ±9.25% ±11.87%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 9 comparisons, you can thus
expect the following amount of false-positive results:
0.45 false positives, when considering a 5% risk acceptance (*, **, ***),
0.09 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95aduh95 added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65138
✔ Done loading data for nodejs/node/pull/65138
----------------------------------- PR info ------------------------------------
Title stream: cut promise churn in webstreams hot paths (#65138)
Author Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch mcollina:webstream-perf-round12 -> nodejs:main
Labels author ready, needs-ci, commit-queue, web streams
Commits 4
- benchmark: apply highWaterMark in webstreams pipe-to
- stream: cut promise churn in webstreams hot paths
- stream: consolidate non-op algorithm callbacks
- Update lib/internal/webstreams/readablestream.js
Committers 2
- Matteo Collina <hello@matteocollina.com>
- GitHub <noreply@github.com>
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sat, 08 Aug 2026 15:01:49 GMT
✔ Approvals: 4
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4889126958
✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/65138#pullrequestreview-4941618229
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4937946571
✔ - Antoine du Hamel (@aduh95) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4943235120
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-15T04:53:41Z: https://ci.nodejs.org/job/node-test-pull-request/75855/
- Querying data for job/node-test-pull-request/75855/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ No git cherry-pick in progress
✔ No git am in progress
✔ No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
* branch main -> FETCH_HEAD
✔ origin/main is now up-to-date
- Downloading patch for 65138
From https://github.com/nodejs/node
* branch refs/pull/65138/merge -> FETCH_HEAD
✔ Fetched commits as 9e23066b8af4..0c0d5beaa88d
--------------------------------------------------------------------------------
[main eaaf996401] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 1d82311a65] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 6bb61acbad] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main e67bb74dca] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
✔ Patches applied
There are 4 commits in the PR. Attempting autorebase.
(node:400) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: apply highWaterMark in webstreams pipe-to

The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.

Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 5979438444] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Rebasing (3/8)
Rebasing (4/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: cut promise churn in webstreams hot paths

Three related reductions on the per-chunk paths:

Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).

Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.

Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.

pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 6536e7de9b] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Rebasing (5/8)
Rebasing (6/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: consolidate non-op algorithm callbacks

The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 2900aa12f7] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Rebasing (7/8)
Rebasing (8/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
Update lib/internal/webstreams/readablestream.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD f1d7d5568c] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/31872533736

@aduh95aduh95 added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. labels Aug 15, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
@aduh95
aduh95force-pushed the webstream-perf-round12 branch from 0c0d5be to 2929417CompareAugust 15, 2026 08:00
@aduh95

Copy link
Copy Markdown
Contributor

Landed in 4551732...2929417

@aduh95
aduh95 merged commit 2929417 into nodejs:mainAug 15, 2026
18 of 19 checks passed
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.needs-ciPRs that need a full CI run.web streamsIssues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@anonrig@gurgunday@bjohansebas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

stream: cut promise churn in webstreams hot paths - #65138

Merged
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12
Aug 15, 2026
Merged

stream: cut promise churn in webstreams hot paths#65138
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12

Conversation

@mcollina

Copy link
Copy Markdown
Member

Twelfth round of pure-JS webstreams optimizations, following #64890.

Profiling pipeTo at the default highWaterMark showed roughly 10% of the profile in queueMicrotask plus its native binding (one call per pump batch), a fresh writer.ready promise record plus reaction per backpressure flip on the pipeThrough shape, and an implicit async-wrapper promise per sink.write/source.pull invocation. Three changes, one commit:

  • Non-thenable algorithm fast path. User sink.write and source.pull callbacks are wrapped without coercing their result into a promise. A non-thenable result (the common synchronous case) means fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had. Thenable results go through PromiseResolve(), matching the reference implementation's promiseCall (identity for native promises).
  • pipeTo parks on backpressure via a ready-record hook. The pump installs a record that duck-types the writer's lazily-materialized [[readyPromise]] record; writableStreamUpdateBackpressure resolving it re-enters the pump directly. This removes the per-flip promise record + reaction and the per-batch queueMicrotask. The backpressure state field is now published before the ready record is resolved so the hook observes the new value (the resolve of a real ready record only settles a promise, so the reorder is unobservable otherwise).
  • Chunk-forwarding microtasks via a shared resolved promise. The pipeTo/tee forwardChunk hops and the pump's between-batch yield use a reaction on the shared resolved promise instead of queueMicrotask, which enqueues at the same position with less overhead.

A separate first commit fixes the pipe-to.js benchmark: the highWaterMark values were passed inside the underlying source/sink dictionaries where they are ignored, so all 16 configurations measured the identical workload at the default highWaterMark of 1. The strategies are now passed as the constructors' second argument, with the matrix covering the default (1) and buffered (1024, 4096) configurations.

Benchmark results with the fixed benchmark (30 runs):

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 * 9.34 % ±8.20% ±10.90% ±14.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 ** 12.81 % ±8.69% ±11.57% ±15.06%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 ** 14.09 % ±8.45% ±11.24% ±14.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 * 8.87 % ±7.41% ±9.86% ±12.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 * 9.51 % ±8.64% ±11.50% ±14.97%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 * 8.01 % ±7.93% ±10.55% ±13.74%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 * 9.36 % ±8.27% ±11.01% ±14.33%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** 11.51 % ±8.56% ±11.39% ±14.82%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * 10.78 % ±8.37% ±11.14% ±14.50%

The full-suite run showed no regressions in any other family; readable-read normal, tee normal, and pipeThrough passthrough spot runs also improve (~+6-18%). Verified with the WPT streams/compression/encoding suites, the full parallel webstream/whatwg test set, and a shutdown-ordering stress (abort mid-write, close with pending writes, sync-throwing and rejecting sinks, error propagation) whose event log is byte-identical to main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Aug 8, 2026
Comment threadlib/internal/webstreams/util.js
Comment threadlib/internal/webstreams/util.js Outdated

@jasnelljasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, otherwise LGTM

@codecov

codecovBot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (4551732) to head (2929417).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65138 +/- ##
==========================================
+ Coverage 90.30% 90.34% +0.03% 
==========================================
Files 751 751 Lines 250235 250330 +95 Branches 47305 47321 +16 ==========================================
+ Hits 225987 226171 +184 + Misses 15619 15557 -62 + Partials 8629 8602 -27 
Files with missing linesCoverage Δ
lib/internal/webstreams/readablestream.js98.18% <100.00%> (+0.02%)⬆️
lib/internal/webstreams/util.js97.89% <100.00%> (+0.17%)⬆️
lib/internal/webstreams/writablestream.js99.52% <100.00%> (+<0.01%)⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gurgundaygurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically on 0ff1bfc, WDYT about this?

https://github.com/whatwg/streams/blob/29997074ffcfbe5b72a4451775398a54cef5b297/reference-implementation/lib/helpers/webidl.js#L45-L48

I think there was a reason Promise.resolve wasn't used here, and I'm not sure what the ramifications of using it might be

@mcollina

Copy link
Copy Markdown
MemberAuthor

As long as WPT passes, I'm good if we can avoid a promise allocation.

That specifically seems non-observable behavior.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@mcollina
mcollina requested review from aduh95 and anonrigAugust 12, 2026 03:54
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/internal/webstreams/readablestream.js Outdated
@github-actions

github-actionsBot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams / pipe-to): https://github.com/nodejs/node/actions/runs/31826854651

Results

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

Benchmark results:

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 25.57 % ±7.64% ±10.07% ±12.92%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 *** 29.57 % ±7.55% ±9.95% ±12.78%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 *** 28.22 % ±7.35% ±9.69% ±12.44%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 *** 19.26 % ±7.14% ±9.42% ±12.09%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 *** 20.69 % ±7.10% ±9.36% ±12.01%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** 20.21 % ±6.83% ±9.00% ±11.55%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 *** 21.79 % ±7.11% ±9.37% ±12.03%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 *** 20.12 % ±7.17% ±9.45% ±12.13%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 *** 19.61 % ±7.01% ±9.25% ±11.87%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 9 comparisons, you can thus
expect the following amount of false-positive results:
0.45 false positives, when considering a 5% risk acceptance (*, **, ***),
0.09 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95aduh95 added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65138
✔ Done loading data for nodejs/node/pull/65138
----------------------------------- PR info ------------------------------------
Title stream: cut promise churn in webstreams hot paths (#65138)
Author Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch mcollina:webstream-perf-round12 -> nodejs:main
Labels author ready, needs-ci, commit-queue, web streams
Commits 4
- benchmark: apply highWaterMark in webstreams pipe-to
- stream: cut promise churn in webstreams hot paths
- stream: consolidate non-op algorithm callbacks
- Update lib/internal/webstreams/readablestream.js
Committers 2
- Matteo Collina <hello@matteocollina.com>
- GitHub <noreply@github.com>
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sat, 08 Aug 2026 15:01:49 GMT
✔ Approvals: 4
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4889126958
✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/65138#pullrequestreview-4941618229
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4937946571
✔ - Antoine du Hamel (@aduh95) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4943235120
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-15T04:53:41Z: https://ci.nodejs.org/job/node-test-pull-request/75855/
- Querying data for job/node-test-pull-request/75855/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ No git cherry-pick in progress
✔ No git am in progress
✔ No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
* branch main -> FETCH_HEAD
✔ origin/main is now up-to-date
- Downloading patch for 65138
From https://github.com/nodejs/node
* branch refs/pull/65138/merge -> FETCH_HEAD
✔ Fetched commits as 9e23066b8af4..0c0d5beaa88d
--------------------------------------------------------------------------------
[main eaaf996401] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 1d82311a65] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 6bb61acbad] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main e67bb74dca] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
✔ Patches applied
There are 4 commits in the PR. Attempting autorebase.
(node:400) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: apply highWaterMark in webstreams pipe-to

The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.

Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 5979438444] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Rebasing (3/8)
Rebasing (4/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: cut promise churn in webstreams hot paths

Three related reductions on the per-chunk paths:

Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).

Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.

Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.

pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 6536e7de9b] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Rebasing (5/8)
Rebasing (6/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: consolidate non-op algorithm callbacks

The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 2900aa12f7] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Rebasing (7/8)
Rebasing (8/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
Update lib/internal/webstreams/readablestream.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD f1d7d5568c] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/31872533736

@aduh95aduh95 added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. labels Aug 15, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
@aduh95
aduh95force-pushed the webstream-perf-round12 branch from 0c0d5be to 2929417CompareAugust 15, 2026 08:00
@aduh95

Copy link
Copy Markdown
Contributor

Landed in 4551732...2929417

@aduh95
aduh95 merged commit 2929417 into nodejs:mainAug 15, 2026
18 of 19 checks passed
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.needs-ciPRs that need a full CI run.web streamsIssues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@anonrig@gurgunday@bjohansebas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

stream: cut promise churn in webstreams hot paths - #65138

Merged
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12
Aug 15, 2026
Merged

stream: cut promise churn in webstreams hot paths#65138
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12

Conversation

@mcollina

Copy link
Copy Markdown
Member

Twelfth round of pure-JS webstreams optimizations, following #64890.

Profiling pipeTo at the default highWaterMark showed roughly 10% of the profile in queueMicrotask plus its native binding (one call per pump batch), a fresh writer.ready promise record plus reaction per backpressure flip on the pipeThrough shape, and an implicit async-wrapper promise per sink.write/source.pull invocation. Three changes, one commit:

  • Non-thenable algorithm fast path. User sink.write and source.pull callbacks are wrapped without coercing their result into a promise. A non-thenable result (the common synchronous case) means fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had. Thenable results go through PromiseResolve(), matching the reference implementation's promiseCall (identity for native promises).
  • pipeTo parks on backpressure via a ready-record hook. The pump installs a record that duck-types the writer's lazily-materialized [[readyPromise]] record; writableStreamUpdateBackpressure resolving it re-enters the pump directly. This removes the per-flip promise record + reaction and the per-batch queueMicrotask. The backpressure state field is now published before the ready record is resolved so the hook observes the new value (the resolve of a real ready record only settles a promise, so the reorder is unobservable otherwise).
  • Chunk-forwarding microtasks via a shared resolved promise. The pipeTo/tee forwardChunk hops and the pump's between-batch yield use a reaction on the shared resolved promise instead of queueMicrotask, which enqueues at the same position with less overhead.

A separate first commit fixes the pipe-to.js benchmark: the highWaterMark values were passed inside the underlying source/sink dictionaries where they are ignored, so all 16 configurations measured the identical workload at the default highWaterMark of 1. The strategies are now passed as the constructors' second argument, with the matrix covering the default (1) and buffered (1024, 4096) configurations.

Benchmark results with the fixed benchmark (30 runs):

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 * 9.34 % ±8.20% ±10.90% ±14.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 ** 12.81 % ±8.69% ±11.57% ±15.06%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 ** 14.09 % ±8.45% ±11.24% ±14.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 * 8.87 % ±7.41% ±9.86% ±12.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 * 9.51 % ±8.64% ±11.50% ±14.97%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 * 8.01 % ±7.93% ±10.55% ±13.74%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 * 9.36 % ±8.27% ±11.01% ±14.33%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** 11.51 % ±8.56% ±11.39% ±14.82%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * 10.78 % ±8.37% ±11.14% ±14.50%

The full-suite run showed no regressions in any other family; readable-read normal, tee normal, and pipeThrough passthrough spot runs also improve (~+6-18%). Verified with the WPT streams/compression/encoding suites, the full parallel webstream/whatwg test set, and a shutdown-ordering stress (abort mid-write, close with pending writes, sync-throwing and rejecting sinks, error propagation) whose event log is byte-identical to main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Aug 8, 2026
Comment threadlib/internal/webstreams/util.js
Comment threadlib/internal/webstreams/util.js Outdated

@jasnelljasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, otherwise LGTM

@codecov

codecovBot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (4551732) to head (2929417).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65138 +/- ##
==========================================
+ Coverage 90.30% 90.34% +0.03% 
==========================================
Files 751 751 Lines 250235 250330 +95 Branches 47305 47321 +16 ==========================================
+ Hits 225987 226171 +184 + Misses 15619 15557 -62 + Partials 8629 8602 -27 
Files with missing linesCoverage Δ
lib/internal/webstreams/readablestream.js98.18% <100.00%> (+0.02%)⬆️
lib/internal/webstreams/util.js97.89% <100.00%> (+0.17%)⬆️
lib/internal/webstreams/writablestream.js99.52% <100.00%> (+<0.01%)⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gurgundaygurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically on 0ff1bfc, WDYT about this?

https://github.com/whatwg/streams/blob/29997074ffcfbe5b72a4451775398a54cef5b297/reference-implementation/lib/helpers/webidl.js#L45-L48

I think there was a reason Promise.resolve wasn't used here, and I'm not sure what the ramifications of using it might be

@mcollina

Copy link
Copy Markdown
MemberAuthor

As long as WPT passes, I'm good if we can avoid a promise allocation.

That specifically seems non-observable behavior.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@mcollina
mcollina requested review from aduh95 and anonrigAugust 12, 2026 03:54
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/internal/webstreams/readablestream.js Outdated
@github-actions

github-actionsBot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams / pipe-to): https://github.com/nodejs/node/actions/runs/31826854651

Results

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

Benchmark results:

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 25.57 % ±7.64% ±10.07% ±12.92%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 *** 29.57 % ±7.55% ±9.95% ±12.78%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 *** 28.22 % ±7.35% ±9.69% ±12.44%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 *** 19.26 % ±7.14% ±9.42% ±12.09%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 *** 20.69 % ±7.10% ±9.36% ±12.01%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** 20.21 % ±6.83% ±9.00% ±11.55%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 *** 21.79 % ±7.11% ±9.37% ±12.03%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 *** 20.12 % ±7.17% ±9.45% ±12.13%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 *** 19.61 % ±7.01% ±9.25% ±11.87%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 9 comparisons, you can thus
expect the following amount of false-positive results:
0.45 false positives, when considering a 5% risk acceptance (*, **, ***),
0.09 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95aduh95 added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65138
✔ Done loading data for nodejs/node/pull/65138
----------------------------------- PR info ------------------------------------
Title stream: cut promise churn in webstreams hot paths (#65138)
Author Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch mcollina:webstream-perf-round12 -> nodejs:main
Labels author ready, needs-ci, commit-queue, web streams
Commits 4
- benchmark: apply highWaterMark in webstreams pipe-to
- stream: cut promise churn in webstreams hot paths
- stream: consolidate non-op algorithm callbacks
- Update lib/internal/webstreams/readablestream.js
Committers 2
- Matteo Collina <hello@matteocollina.com>
- GitHub <noreply@github.com>
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sat, 08 Aug 2026 15:01:49 GMT
✔ Approvals: 4
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4889126958
✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/65138#pullrequestreview-4941618229
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4937946571
✔ - Antoine du Hamel (@aduh95) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4943235120
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-15T04:53:41Z: https://ci.nodejs.org/job/node-test-pull-request/75855/
- Querying data for job/node-test-pull-request/75855/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ No git cherry-pick in progress
✔ No git am in progress
✔ No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
* branch main -> FETCH_HEAD
✔ origin/main is now up-to-date
- Downloading patch for 65138
From https://github.com/nodejs/node
* branch refs/pull/65138/merge -> FETCH_HEAD
✔ Fetched commits as 9e23066b8af4..0c0d5beaa88d
--------------------------------------------------------------------------------
[main eaaf996401] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 1d82311a65] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 6bb61acbad] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main e67bb74dca] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
✔ Patches applied
There are 4 commits in the PR. Attempting autorebase.
(node:400) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: apply highWaterMark in webstreams pipe-to

The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.

Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 5979438444] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Rebasing (3/8)
Rebasing (4/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: cut promise churn in webstreams hot paths

Three related reductions on the per-chunk paths:

Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).

Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.

Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.

pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 6536e7de9b] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Rebasing (5/8)
Rebasing (6/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: consolidate non-op algorithm callbacks

The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 2900aa12f7] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Rebasing (7/8)
Rebasing (8/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
Update lib/internal/webstreams/readablestream.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD f1d7d5568c] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/31872533736

@aduh95aduh95 added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. labels Aug 15, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
@aduh95
aduh95force-pushed the webstream-perf-round12 branch from 0c0d5be to 2929417CompareAugust 15, 2026 08:00
@aduh95

Copy link
Copy Markdown
Contributor

Landed in 4551732...2929417

@aduh95
aduh95 merged commit 2929417 into nodejs:mainAug 15, 2026
18 of 19 checks passed
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.needs-ciPRs that need a full CI run.web streamsIssues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@anonrig@gurgunday@bjohansebas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

stream: cut promise churn in webstreams hot paths - #65138

Merged
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12
Aug 15, 2026
Merged

stream: cut promise churn in webstreams hot paths#65138
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12

Conversation

@mcollina

Copy link
Copy Markdown
Member

Twelfth round of pure-JS webstreams optimizations, following #64890.

Profiling pipeTo at the default highWaterMark showed roughly 10% of the profile in queueMicrotask plus its native binding (one call per pump batch), a fresh writer.ready promise record plus reaction per backpressure flip on the pipeThrough shape, and an implicit async-wrapper promise per sink.write/source.pull invocation. Three changes, one commit:

  • Non-thenable algorithm fast path. User sink.write and source.pull callbacks are wrapped without coercing their result into a promise. A non-thenable result (the common synchronous case) means fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had. Thenable results go through PromiseResolve(), matching the reference implementation's promiseCall (identity for native promises).
  • pipeTo parks on backpressure via a ready-record hook. The pump installs a record that duck-types the writer's lazily-materialized [[readyPromise]] record; writableStreamUpdateBackpressure resolving it re-enters the pump directly. This removes the per-flip promise record + reaction and the per-batch queueMicrotask. The backpressure state field is now published before the ready record is resolved so the hook observes the new value (the resolve of a real ready record only settles a promise, so the reorder is unobservable otherwise).
  • Chunk-forwarding microtasks via a shared resolved promise. The pipeTo/tee forwardChunk hops and the pump's between-batch yield use a reaction on the shared resolved promise instead of queueMicrotask, which enqueues at the same position with less overhead.

A separate first commit fixes the pipe-to.js benchmark: the highWaterMark values were passed inside the underlying source/sink dictionaries where they are ignored, so all 16 configurations measured the identical workload at the default highWaterMark of 1. The strategies are now passed as the constructors' second argument, with the matrix covering the default (1) and buffered (1024, 4096) configurations.

Benchmark results with the fixed benchmark (30 runs):

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 * 9.34 % ±8.20% ±10.90% ±14.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 ** 12.81 % ±8.69% ±11.57% ±15.06%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 ** 14.09 % ±8.45% ±11.24% ±14.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 * 8.87 % ±7.41% ±9.86% ±12.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 * 9.51 % ±8.64% ±11.50% ±14.97%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 * 8.01 % ±7.93% ±10.55% ±13.74%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 * 9.36 % ±8.27% ±11.01% ±14.33%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** 11.51 % ±8.56% ±11.39% ±14.82%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * 10.78 % ±8.37% ±11.14% ±14.50%

The full-suite run showed no regressions in any other family; readable-read normal, tee normal, and pipeThrough passthrough spot runs also improve (~+6-18%). Verified with the WPT streams/compression/encoding suites, the full parallel webstream/whatwg test set, and a shutdown-ordering stress (abort mid-write, close with pending writes, sync-throwing and rejecting sinks, error propagation) whose event log is byte-identical to main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Aug 8, 2026
Comment threadlib/internal/webstreams/util.js
Comment threadlib/internal/webstreams/util.js Outdated

@jasnelljasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, otherwise LGTM

@codecov

codecovBot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (4551732) to head (2929417).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65138 +/- ##
==========================================
+ Coverage 90.30% 90.34% +0.03% 
==========================================
Files 751 751 Lines 250235 250330 +95 Branches 47305 47321 +16 ==========================================
+ Hits 225987 226171 +184 + Misses 15619 15557 -62 + Partials 8629 8602 -27 
Files with missing linesCoverage Δ
lib/internal/webstreams/readablestream.js98.18% <100.00%> (+0.02%)⬆️
lib/internal/webstreams/util.js97.89% <100.00%> (+0.17%)⬆️
lib/internal/webstreams/writablestream.js99.52% <100.00%> (+<0.01%)⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gurgundaygurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically on 0ff1bfc, WDYT about this?

https://github.com/whatwg/streams/blob/29997074ffcfbe5b72a4451775398a54cef5b297/reference-implementation/lib/helpers/webidl.js#L45-L48

I think there was a reason Promise.resolve wasn't used here, and I'm not sure what the ramifications of using it might be

@mcollina

Copy link
Copy Markdown
MemberAuthor

As long as WPT passes, I'm good if we can avoid a promise allocation.

That specifically seems non-observable behavior.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@mcollina
mcollina requested review from aduh95 and anonrigAugust 12, 2026 03:54
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/internal/webstreams/readablestream.js Outdated
@github-actions

github-actionsBot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams / pipe-to): https://github.com/nodejs/node/actions/runs/31826854651

Results

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

Benchmark results:

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 25.57 % ±7.64% ±10.07% ±12.92%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 *** 29.57 % ±7.55% ±9.95% ±12.78%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 *** 28.22 % ±7.35% ±9.69% ±12.44%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 *** 19.26 % ±7.14% ±9.42% ±12.09%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 *** 20.69 % ±7.10% ±9.36% ±12.01%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** 20.21 % ±6.83% ±9.00% ±11.55%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 *** 21.79 % ±7.11% ±9.37% ±12.03%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 *** 20.12 % ±7.17% ±9.45% ±12.13%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 *** 19.61 % ±7.01% ±9.25% ±11.87%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 9 comparisons, you can thus
expect the following amount of false-positive results:
0.45 false positives, when considering a 5% risk acceptance (*, **, ***),
0.09 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95aduh95 added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65138
✔ Done loading data for nodejs/node/pull/65138
----------------------------------- PR info ------------------------------------
Title stream: cut promise churn in webstreams hot paths (#65138)
Author Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch mcollina:webstream-perf-round12 -> nodejs:main
Labels author ready, needs-ci, commit-queue, web streams
Commits 4
- benchmark: apply highWaterMark in webstreams pipe-to
- stream: cut promise churn in webstreams hot paths
- stream: consolidate non-op algorithm callbacks
- Update lib/internal/webstreams/readablestream.js
Committers 2
- Matteo Collina <hello@matteocollina.com>
- GitHub <noreply@github.com>
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sat, 08 Aug 2026 15:01:49 GMT
✔ Approvals: 4
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4889126958
✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/65138#pullrequestreview-4941618229
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4937946571
✔ - Antoine du Hamel (@aduh95) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4943235120
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-15T04:53:41Z: https://ci.nodejs.org/job/node-test-pull-request/75855/
- Querying data for job/node-test-pull-request/75855/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ No git cherry-pick in progress
✔ No git am in progress
✔ No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
* branch main -> FETCH_HEAD
✔ origin/main is now up-to-date
- Downloading patch for 65138
From https://github.com/nodejs/node
* branch refs/pull/65138/merge -> FETCH_HEAD
✔ Fetched commits as 9e23066b8af4..0c0d5beaa88d
--------------------------------------------------------------------------------
[main eaaf996401] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 1d82311a65] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 6bb61acbad] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main e67bb74dca] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
✔ Patches applied
There are 4 commits in the PR. Attempting autorebase.
(node:400) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: apply highWaterMark in webstreams pipe-to

The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.

Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 5979438444] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Rebasing (3/8)
Rebasing (4/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: cut promise churn in webstreams hot paths

Three related reductions on the per-chunk paths:

Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).

Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.

Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.

pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 6536e7de9b] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Rebasing (5/8)
Rebasing (6/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: consolidate non-op algorithm callbacks

The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 2900aa12f7] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Rebasing (7/8)
Rebasing (8/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
Update lib/internal/webstreams/readablestream.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD f1d7d5568c] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/31872533736

@aduh95aduh95 added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. labels Aug 15, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
@aduh95
aduh95force-pushed the webstream-perf-round12 branch from 0c0d5be to 2929417CompareAugust 15, 2026 08:00
@aduh95

Copy link
Copy Markdown
Contributor

Landed in 4551732...2929417

@aduh95
aduh95 merged commit 2929417 into nodejs:mainAug 15, 2026
18 of 19 checks passed
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.needs-ciPRs that need a full CI run.web streamsIssues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@anonrig@gurgunday@bjohansebas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

stream: cut promise churn in webstreams hot paths - #65138

Merged
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12
Aug 15, 2026
Merged

stream: cut promise churn in webstreams hot paths#65138
aduh95 merged 3 commits into
nodejs:mainfrom
mcollina:webstream-perf-round12

Conversation

@mcollina

Copy link
Copy Markdown
Member

Twelfth round of pure-JS webstreams optimizations, following #64890.

Profiling pipeTo at the default highWaterMark showed roughly 10% of the profile in queueMicrotask plus its native binding (one call per pump batch), a fresh writer.ready promise record plus reaction per backpressure flip on the pipeThrough shape, and an implicit async-wrapper promise per sink.write/source.pull invocation. Three changes, one commit:

  • Non-thenable algorithm fast path. User sink.write and source.pull callbacks are wrapped without coercing their result into a promise. A non-thenable result (the common synchronous case) means fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had. Thenable results go through PromiseResolve(), matching the reference implementation's promiseCall (identity for native promises).
  • pipeTo parks on backpressure via a ready-record hook. The pump installs a record that duck-types the writer's lazily-materialized [[readyPromise]] record; writableStreamUpdateBackpressure resolving it re-enters the pump directly. This removes the per-flip promise record + reaction and the per-batch queueMicrotask. The backpressure state field is now published before the ready record is resolved so the hook observes the new value (the resolve of a real ready record only settles a promise, so the reorder is unobservable otherwise).
  • Chunk-forwarding microtasks via a shared resolved promise. The pipeTo/tee forwardChunk hops and the pump's between-batch yield use a reaction on the shared resolved promise instead of queueMicrotask, which enqueues at the same position with less overhead.

A separate first commit fixes the pipe-to.js benchmark: the highWaterMark values were passed inside the underlying source/sink dictionaries where they are ignored, so all 16 configurations measured the identical workload at the default highWaterMark of 1. The strategies are now passed as the constructors' second argument, with the matrix covering the default (1) and buffered (1024, 4096) configurations.

Benchmark results with the fixed benchmark (30 runs):

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 * 9.34 % ±8.20% ±10.90% ±14.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 ** 12.81 % ±8.69% ±11.57% ±15.06%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 ** 14.09 % ±8.45% ±11.24% ±14.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 * 8.87 % ±7.41% ±9.86% ±12.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 * 9.51 % ±8.64% ±11.50% ±14.97%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 * 8.01 % ±7.93% ±10.55% ±13.74%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 * 9.36 % ±8.27% ±11.01% ±14.33%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** 11.51 % ±8.56% ±11.39% ±14.82%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * 10.78 % ±8.37% ±11.14% ±14.50%

The full-suite run showed no regressions in any other family; readable-read normal, tee normal, and pipeThrough passthrough spot runs also improve (~+6-18%). Verified with the WPT streams/compression/encoding suites, the full parallel webstream/whatwg test set, and a shutdown-ordering stress (abort mid-write, close with pending writes, sync-throwing and rejecting sinks, error propagation) whose event log is byte-identical to main.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-botnodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Aug 8, 2026
Comment threadlib/internal/webstreams/util.js
Comment threadlib/internal/webstreams/util.js Outdated

@jasnelljasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, otherwise LGTM

@codecov

codecovBot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (4551732) to head (2929417).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #65138 +/- ##
==========================================
+ Coverage 90.30% 90.34% +0.03% 
==========================================
Files 751 751 Lines 250235 250330 +95 Branches 47305 47321 +16 ==========================================
+ Hits 225987 226171 +184 + Misses 15619 15557 -62 + Partials 8629 8602 -27 
Files with missing linesCoverage Δ
lib/internal/webstreams/readablestream.js98.18% <100.00%> (+0.02%)⬆️
lib/internal/webstreams/util.js97.89% <100.00%> (+0.17%)⬆️
lib/internal/webstreams/writablestream.js99.52% <100.00%> (+<0.01%)⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gurgundaygurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically on 0ff1bfc, WDYT about this?

https://github.com/whatwg/streams/blob/29997074ffcfbe5b72a4451775398a54cef5b297/reference-implementation/lib/helpers/webidl.js#L45-L48

I think there was a reason Promise.resolve wasn't used here, and I'm not sure what the ramifications of using it might be

@mcollina

Copy link
Copy Markdown
MemberAuthor

As long as WPT passes, I'm good if we can avoid a promise allocation.

That specifically seems non-observable behavior.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@mcollina
mcollina requested review from aduh95 and anonrigAugust 12, 2026 03:54
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 12, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment threadlib/internal/webstreams/readablestream.js Outdated
@github-actions

github-actionsBot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams / pipe-to): https://github.com/nodejs/node/actions/runs/31826854651

Results

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

Benchmark results:

 confidence improvement accuracy (*) (**) (***)
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 25.57 % ±7.64% ±10.07% ±12.92%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000 *** 29.57 % ±7.55% ±9.95% ±12.78%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000 *** 28.22 % ±7.35% ±9.69% ±12.44%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000 *** 19.26 % ±7.14% ±9.42% ±12.09%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 *** 20.69 % ±7.10% ±9.36% ±12.01%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** 20.21 % ±6.83% ±9.00% ±11.55%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000 *** 21.79 % ±7.11% ±9.37% ±12.03%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 *** 20.12 % ±7.17% ±9.45% ±12.13%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 *** 19.61 % ±7.01% ±9.25% ±11.87%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 9 comparisons, you can thus
expect the following amount of false-positive results:
0.45 false positives, when considering a 5% risk acceptance (*, **, ***),
0.09 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)

[!WARNING]
Do not take GHA benchmark results as face value, always confirm them
using a dedicated machine, e.g. Jenkins CI.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 14, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95aduh95 added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-botnodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Aug 15, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator
Commit Queue failed
- Loading data for nodejs/node/pull/65138
✔ Done loading data for nodejs/node/pull/65138
----------------------------------- PR info ------------------------------------
Title stream: cut promise churn in webstreams hot paths (#65138)
Author Matteo Collina <matteo.collina@gmail.com> (@mcollina)
Branch mcollina:webstream-perf-round12 -> nodejs:main
Labels author ready, needs-ci, commit-queue, web streams
Commits 4
- benchmark: apply highWaterMark in webstreams pipe-to
- stream: cut promise churn in webstreams hot paths
- stream: consolidate non-op algorithm callbacks
- Update lib/internal/webstreams/readablestream.js
Committers 2
- Matteo Collina <hello@matteocollina.com>
- GitHub <noreply@github.com>
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
--------------------------------------------------------------------------------
ℹ This PR was created on Sat, 08 Aug 2026 15:01:49 GMT
✔ Approvals: 4
✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4889126958
✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/65138#pullrequestreview-4941618229
✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4937946571
✔ - Antoine du Hamel (@aduh95) (TSC): https://github.com/nodejs/node/pull/65138#pullrequestreview-4943235120
✔ Last GitHub CI successful
ℹ Last Full PR CI on 2026-08-15T04:53:41Z: https://ci.nodejs.org/job/node-test-pull-request/75855/
- Querying data for job/node-test-pull-request/75855/
✔ Build data downloaded
✔ Last Jenkins CI successful
--------------------------------------------------------------------------------
✔ No git cherry-pick in progress
✔ No git am in progress
✔ No git rebase in progress
--------------------------------------------------------------------------------
- Bringing origin/main up to date...
From https://github.com/nodejs/node
* branch main -> FETCH_HEAD
✔ origin/main is now up-to-date
- Downloading patch for 65138
From https://github.com/nodejs/node
* branch refs/pull/65138/merge -> FETCH_HEAD
✔ Fetched commits as 9e23066b8af4..0c0d5beaa88d
--------------------------------------------------------------------------------
[main eaaf996401] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 1d82311a65] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main 6bb61acbad] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Auto-merging lib/internal/webstreams/readablestream.js
[main e67bb74dca] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
✔ Patches applied
There are 4 commits in the PR. Attempting autorebase.
(node:400) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: apply highWaterMark in webstreams pipe-to

The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.

Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 5979438444] benchmark: apply highWaterMark in webstreams pipe-to
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:33 2026 +0200
1 file changed, 4 insertions(+), 6 deletions(-)
Rebasing (3/8)
Rebasing (4/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: cut promise churn in webstreams hot paths

Three related reductions on the per-chunk paths:

Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).

Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.

Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.

pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 6536e7de9b] stream: cut promise churn in webstreams hot paths
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 17:00:48 2026 +0200
3 files changed, 126 insertions(+), 25 deletions(-)
Rebasing (5/8)
Rebasing (6/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
stream: consolidate non-op algorithm callbacks

The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD 2900aa12f7] stream: consolidate non-op algorithm callbacks
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Aug 8 19:32:35 2026 +0200
3 files changed, 19 insertions(+), 25 deletions(-)
Rebasing (7/8)
Rebasing (8/8)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
Update lib/internal/webstreams/readablestream.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

[detached HEAD f1d7d5568c] Update lib/internal/webstreams/readablestream.js
Author: Matteo Collina <matteo.collina@gmail.com>
Date: Fri Aug 14 10:03:20 2026 -0400
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.

ℹ Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

https://github.com/nodejs/node/actions/runs/31872533736

@aduh95aduh95 added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. labels Aug 15, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
@aduh95
aduh95force-pushed the webstream-perf-round12 branch from 0c0d5be to 2929417CompareAugust 15, 2026 08:00
@aduh95

Copy link
Copy Markdown
Contributor

Landed in 4551732...2929417

@aduh95
aduh95 merged commit 2929417 into nodejs:mainAug 15, 2026
18 of 19 checks passed
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 25, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The highWaterMark values were passed as properties of the underlying
source and sink dictionaries, where they are ignored: a queuing
strategy's highWaterMark is read from the constructors' second argument.
Every configuration therefore measured the identical workload at the
default highWaterMark of 1, which also explains the historically high
run-to-run variance of this benchmark family.
Pass the strategies as the constructors' second argument and cover the
default (1) alongside buffered (1024, 4096) configurations.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
Three related reductions on the per-chunk paths:
Wrap user sink.write and source.pull callbacks without coercing their
result into a promise. When the callback returns a non-thenable (the
common synchronous case), fulfillment is guaranteed and no then() lookup
is observable, so the fulfilled reaction is enqueued through a single
shared resolved promise at the exact microtask position the coerced
promise's reaction would have had, skipping the implicit async-wrapper
promise per chunk. Thenable results go through PromiseResolve(), which
matches the spec's "a promise resolved with" conversion (identity for
native promises).
Park pipeTo's pump on backpressure by installing a record that
duck-types the writer's lazily-materialized [[readyPromise]] record and
whose resolve function is the pump continuation itself. Backpressure
clearing then resumes the pump directly instead of materializing a fresh
promise record plus reaction per flip, and the pump no longer schedules
a microtask per batch. writableStreamUpdateBackpressure publishes the
new backpressure state before resolving the ready record so the pump
observes the updated value.
Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths
with a reaction on the shared resolved promise, which enqueues the
continuation at the same position without the per-call scheduling
overhead.
pipe-to improves by 8-14% across all benchmark configurations, with
readable-read and tee also improving in spot runs.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 27, 2026
The start, pull, and write non-op algorithms are all raw callbacks with
an identical empty body now, so a single shared nonOpCallback replaces
nonOpStart, nonOpPull, and nonOpWrite.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #65138
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.needs-ciPRs that need a full CI run.web streamsIssues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@anonrig@gurgunday@bjohansebas