Skip to content

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

Draft
mcollina wants to merge 1 commit into
nodejs:mainfrom
mcollina:webstream-perf-round19
Draft

mcollina wants to merge 1 commit 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];

Comment on lines +108 to +110
// state reset (one per write on the hot path). The prototype is nulled
// after creation: a `__proto__: null` literal is created in dictionary
// mode, and these fields are loaded several times per write.

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.

We're using { __proto__: null, ... } a lot across the entire codebase... Should we do a broader review of how we're using this pattern, or is this specific to how webstreams is using these particular objects?

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.

It could certainly use an audit, I think. It's a bit tricky trying to balance performance with prototype polution safety.

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