stream: keep webstream stream states in fast-mode objects - #65625
stream: keep webstream stream states in fast-mode objects#65625mcollina wants to merge 2 commits into
Conversation
The per-stream state records were built as object literals with
__proto__: null, which V8 creates as dictionary-mode objects: roughly
50x slower to allocate, and every subsequent property load on them is a
dictionary lookup. These records back every hot path, so both stream
construction and per-chunk field accesses were paying for it.
Replace the literals with classes whose prototype has a null prototype,
so instances stay in fast mode while Object.prototype remains excluded
from the lookup chain. Every field ever assigned is declared up front
so the shape never transitions.
Also add benchmark/webstreams/lifecycle.js covering the short-lived
stream pattern (create, few chunks, close) that first exposed this.
confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream' n=50000 *** 138.13 % Β±14.29% Β±19.14% Β±25.17%
webstreams/creation.js kind='TransformStream' n=50000 *** 133.64 % Β±8.61% Β±11.46% Β±14.94%
webstreams/creation.js kind='WritableStream' n=50000 *** 204.00 % Β±7.70% Β±10.26% Β±13.37%
webstreams/lifecycle.js kind='pipe-through' n=50000 *** 96.41 % Β±3.41% Β±4.58% Β±6.03%
webstreams/lifecycle.js kind='readable' n=50000 *** 80.04 % Β±3.10% Β±4.15% Β±5.45%
webstreams/pipe-through.js kind='default' n=500000 *** 91.16 % Β±2.56% Β±3.43% Β±4.52%
webstreams/pipe-to.js highWaterMarkW=1 highWaterMarkR=1 n=500000 *** 110.48 % Β±2.91% Β±3.89% Β±5.11%
webstreams/readable-read.js type='normal' n=100000 *** 34.63 % Β±4.79% Β±6.38% Β±8.31%
Signed-off-by: Matteo Collina <hello@matteocollina.com>
A transform sink write arriving under backpressure parked the chunk together with a PromiseWithResolvers record whose promise was returned to the writable controller and later resolved with the perform-transform promise. The writable's write reactions already exist before the write algorithm runs, so the parked write can instead return the parked-result sentinel and have the continuation wire the perform-transform promise directly to those reactions, dropping the per-chunk promise record and the thenable adoption hop. Failures while erroring are delivered in a microtask, preserving the old rejection position. Signed-off-by: Matteo Collina <hello@matteocollina.com>
|
Review requested:
|
Codecov Reportβ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65625 +/- ##
==========================================
- Coverage 90.13% 90.07% -0.07%
==========================================
Files 751 751
Lines 253639 255020 +1381
Branches 47790 48157 +367
==========================================
+ Hits 228618 229703 +1085
- Misses 16264 16482 +218
- Partials 8757 8835 +78
π New features to boost your workflow:
|
| await new ReadableStream(makeSource()) | ||
| .pipeTo(new WritableStream({ write() {} })); |
There was a problem hiding this comment.
nit: unlikely but we can add asserts for dead-code elimination
| // The state records are classes whose prototype chain ends at null | ||
| // instead of `__proto__: null` object literals: the literals fall back | ||
| // to dictionary-mode objects in V8 (~50x slower to create, and every | ||
| // later property load is a dictionary lookup), while class instances | ||
| // stay in fast mode with the same protection against Object.prototype | ||
| // pollution. Every field ever assigned is declared so the shape never | ||
| // transitions. |
There was a problem hiding this comment.
nit:
I remember to read few comments like this in the past due to assumptions of V8 at the time, but they usually are very niche/unique/different than a normal implementation
This one does not look weird/hacky, so I would say we could just remove those comments since they don't agreggate much value and you have added benchmarks so we can catch regressions
Abhirup0
left a comment
There was a problem hiding this comment.
LGTM. Verified with %HasFastProperties() that state instances maintain fast-mode Maps without falling back to dictionary mode.
One minor observation: the writableState.state === 'erroring' branch in writeContinuation (lib/internal/webstreams/transformstream.js:628β633) is currently unhit in tests. It might be worth adding a quick test case where a write is parked under backpressure and the writable stream is aborted prior to the next read/pull.
While profiling short-lived streams (the per-request create β few chunks β close pattern),
createReadableStreamStateshowed up at 20% self time. The cause: the per-stream state records are object literals with__proto__: null, which V8 creates as dictionary-mode objects β roughly 50x slower to allocate (~500-1500ns vs ~30ns), and every subsequent property load on them is a dictionary lookup. These records back every hot path, so both construction and per-chunk[kState]accesses have been paying for it.The first commit replaces the four per-stream state literals (and their nested transfer records) with classes whose prototype has a null prototype: instances stay in fast mode, while
Object.prototyperemains excluded from the lookup chain, preserving the pollution protection the literals were there for. Every field ever assigned is declared up front so the shape never transitions. (The controller and reader states were already plain literals and unaffected.) It also addsbenchmark/webstreams/lifecycle.jscovering the short-lived stream pattern that exposed this.The second commit removes the per-chunk
PromiseWithResolversfor transform sink writes parked on backpressure: the writable controller's write reactions exist before the write algorithm runs, so the parked-write continuation can wire the perform-transform promise directly to them via the parked-result sentinel introduced in #65143, dropping the promise record and the thenable adoption hop.Results (30 runs, all 36 rows significant at
***, no regressions β full table below):Validated with the full webstreams test suite, WPT streams/compression/encoding, and two differential stress harnesses (adapter scenarios and transform parked-write error/abort/terminate/reentrancy scenarios) whose observable event logs are byte-identical to the previous implementation.
Other
lib/modules use the same__proto__: nullliteral pattern on hot paths; I'll follow up separately after auditing them.