Skip to content

stream: keep webstreams nil requests in fast mode - #66230

Open
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:webstream-perf-round19
Open

mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:webstream-perf-round19

Conversation

@mcollina

@mcollina mcollina commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Round 19 of the webstreams performance work (follows #66154). A small one: two shared sentinel objects on the writable side were in dictionary mode, and every write and every pipe paid for it.

Nil requests in fast mode

The shared "no pending request" records in writablestream.js (kNilRequest, kNilPendingAbortRequest) were { __proto__: null, ... } literals, which V8 creates in dictionary mode (the same trap #65625 removed from the per-stream state records). They sit in inFlightWriteRequest, closeRequest and pendingAbortRequest whenever nothing is pending, and their promise field is checked several times per write, so each of those loads was a hash lookup. They are now built as plain literals and get their null prototype afterwards with ObjectSetPrototypeOf(), which keeps them in fast mode (only an object created with a null prototype starts in dictionary mode).

Readable controllers: no throwaway state object

ReadableStreamDefaultController and ReadableByteStreamController initialized [kState] with an empty object that setup replaced immediately. The field initializer is gone, matching the writable and transform controllers; every construction site goes straight into setup.

New benchmark

benchmark/webstreams/writable-write.js: nothing in benchmark/webstreams drove WritableStreamDefaultWriter.write() directly (await each write, or queue them all).

Benchmarks

node benchmark/compare.js --runs 20 on the final code (pipe-to, pipe-through, lifecycle, writable-write), significant rows only:

                                                                        confidence improvement accuracy
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000               ***     12.44 %       ±2.20%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1024 n=500000            ***     14.33 %       ±2.84%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=4096 n=500000            ***     13.70 %       ±2.27%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1 n=500000            ***     12.32 %       ±2.76%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000         ***     13.61 %       ±1.98%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000         ***     14.53 %       ±2.38%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1 n=500000            ***     14.80 %       ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000         ***     14.91 %       ±1.93%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000         ***     13.10 %       ±2.29%
webstreams/pipe-through.js kind='transform' n=500000                           ***      6.78 %       ±1.91%
webstreams/pipe-through.js kind='default' n=500000                               *      2.59 %       ±2.47%
webstreams/lifecycle.js kind='readable' n=50000                                  *      3.14 %       ±2.98%
webstreams/writable-write.js type='await' n=100000                             ***     17.57 %       ±4.32%

writable-write is noisy on this machine: an earlier 30-run pass measured await at +6.5 % () and queued at +3.6 % (); here queued is +2.6 % ±3.0 %.

An earlier full-suite run (creation, tee, readable-read, readable-read-buffered, readable-async-iterator, from, js_transfer as well) showed no other significant change; its one negative flag, creation.js kind='ReadableStream.tee' at −3.6 % (*), re-ran at 30 runs as −2.05 % ±2.14 %, not significant.

No behavior change: a 48-scenario microtask-ordering stress logs identically against main, and WPT streams plus the webstreams parallel batch are green.


AI generated, humanly reviewed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API. labels Sep 23, 2026
The shared "no pending request" records in the writable stream were
`__proto__: null` literals, which V8 creates in dictionary mode. They
sit in inFlightWriteRequest, closeRequest and pendingAbortRequest
whenever nothing is pending, and their promise field is checked several
times per write, so those loads did a hash lookup on every write and
every pipe. They are now built as plain literals and get their null
prototype afterwards, which keeps them in fast mode.

The readable controllers also initialized their state slot with an
empty object that setup replaced immediately. That throwaway allocation
is gone, matching the writable and transform controllers.

Add a writable-write benchmark: nothing in benchmark/webstreams drove
WritableStreamDefaultWriter.write() directly.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina
mcollina force-pushed the webstream-perf-round19 branch from 992b90b to bc320cd Compare September 23, 2026 06:34

class ReadableStreamDefaultController {
[kType] = 'ReadableStreamDefaultController';
[kState] = {};

@MattiasBuelens MattiasBuelens Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would this also work? That should ensure that all constructed objects already have the correct "shape", I think?

Suggested change
[kState] = {};
[kState];

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don’t know, I should test.

Comment thread lib/internal/webstreams/writablestream.js
@mcollina
mcollina marked this pull request as ready for review September 24, 2026 16:41
@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.37%. Comparing base (3d85c94) to head (42256de).
⚠️ Report is 107 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #66230      +/-   ##
==========================================
+ Coverage   90.29%   90.37%   +0.08%     
==========================================
  Files         790      790              
  Lines      272880   274499    +1619     
  Branches    52103    52562     +459     
==========================================
+ Hits       246385   248078    +1693     
+ Misses      16936    16891      -45     
+ Partials     9559     9530      -29     
Files with missing lines Coverage Δ
lib/internal/webstreams/readablestream.js 98.07% <100.00%> (ø)
lib/internal/webstreams/transformstream.js 98.72% <100.00%> (+<0.01%) ⬆️
lib/internal/webstreams/writablestream.js 99.52% <100.00%> (+<0.01%) ⬆️

... and 122 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.

Controller instances declared only [kType] and set [kState] later in the
setup function, so each construction took a shape transition when setup
added the slot. Declaring [kState] keeps the slot in the initial map and
makes the setup assignment an in-place store. Applied to the readable
default, readable byte, writable, and transform controllers so all four
share the same instance shape.

Assisted-by: pi
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. web streams Issues and PRs related to the Web Streams API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants