The [[queue]] backing the controllers became a ring buffer, but the
read and write request queues (readRequests, readIntoRequests,
writeRequests) were left as plain arrays consumed with
ArrayPrototypeShift, which is O(n) and, even for the single pending
request of the await-each regime, far slower than an indexed head
advance. Back them with the same Queue, materialized lazily from the
shared empty queue so acquiring a reader or constructing a writer
allocates no request storage until a read or write actually parks.
pipe-to: +4.7% to +9.4% (all 16 configs, ***)
readable-read type=byob: +2.3% (**)
parked read loop: +15%, write loop: +11% (local harness)
Follow-up to https://github.com/nodejs/node/pull/64312.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/64431
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
The [[queue]] backing every default readable/writable controller was a
plain array of { value, size } wrappers consumed with
ArrayPrototypeShift, so each buffered chunk allocated a wrapper object
and each dequeue moved (or forced the engine to re-linearize) the
remaining elements; the byte controller queue paid the same shift cost
for its chunk descriptor records.
Replace the array with a power-of-two ring buffer. Default controller
queues store each entry as (value, size) in two consecutive slots, so
the per-chunk wrapper allocation disappears; the byte controller keeps
its descriptor records (they are mutated in place at the head) in
single slots. Controllers start from (and are reset to) a shared
immutable empty queue, so constructing a stream allocates no queue
storage until a chunk is actually buffered. Enqueues measured by the
internal default size algorithm (never observable by user code, always
returns 1, cannot throw) skip the algorithm call and its try/catch
entirely.
The layout mirrors what Bun/WebKit use for the same spec structure:
[[queue]] as a ring-buffer deque (WTF::Deque in Bun's
src/jsc/bindings/webcore/streams/StreamQueue.h), the pure-JS ring
buffer in Bun's src/js/internal/fifo.ts, and the trivial-size-algorithm
bypass in
src/jsc/bindings/webcore/streams/JSReadableStreamDefaultController.cpp.
benchmark/compare.js against the unmodified baseline (30-run capture
plus an independent 15-run repeat, Welch t-test, all p < 1e-5):
webstreams/pipe-to.js +12-17% across all sixteen high-water-mark
configurations, readable-read-buffered +20% (bufferSize=1) to +49%
(bufferSize=1000), readable-async-iterator +21%. No stable significant
regression across the rest of the webstreams suite: the creation.js and
readable-read.js deltas seen in the full-suite capture disappear in
isolated 60-run rechecks.
Refs: https://github.com/oven-sh/bun/blob/main/src/js/internal/fifo.ts
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Assisted-by: Grok (Grok Build)
PR-URL: https://github.com/nodejs/node/pull/64312
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
for await / reader.read() loops over byte streams were ~4x slower than
over default streams. Three per-chunk costs, none required by the spec:
- ArrayBufferViewGetBuffer/ByteLength/ByteOffset went through
ReflectGet(view.constructor.prototype, ...), a reflective get that is
~3.5x slower than the original prototype getters from primordials and
spoofable through a user-defined .constructor to boot.
- The buffered fast paths in ReadableStreamDefaultReader.read() and the
async iterator only covered default controllers, so byte streams with
queued data still allocated a read request and PromiseWithResolvers
per chunk. Byte-queue dequeue is fully synchronous (it is the
queue-filled arm of the byte controller's pull steps), so both fast
paths now resolve directly from the byte queue.
- readableByteStreamControllerEnqueue re-ran the reader brand check and
re-loaded the read request list four times per chunk across
HasDefaultReader / ProcessReadRequestsUsingQueue / GetNumReadRequests
/ FulfillReadRequest; it now does a single pass. The async iterator
also reuses its read request object across reads (at most one is ever
in flight).
benchmark/webstreams interleaved same-day A/B, --runs 10:
readable-async-iterator bytes +16.3% (***), readable-read byob +9.1%
(***), all other rows neutral. Profiler harness: parked byte iteration
+14%, buffered byte iteration +37%, buffered byte read loop +18%,
default-stream rows at parity. WPT streams/compression/encoding
subtests identical to baseline.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/64291
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Consolidate the spec's per-chunk predicate chains (CanCloseOrEnqueue,
IsLocked, HasDefaultReader, GetNumReadRequests, GetDesiredSize and the
writable-side equivalents) into single passes over the controller and
stream state, mirror "close queued or in flight" as a boolean flag
maintained at the few close-request transition sites, and materialize
the TransformStream [[backpressureChangePromise]] record lazily on
first observation so backpressure flips nobody is waiting on allocate
nothing.
Assisted-by: Claude Code
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/64252
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Pure-JavaScript optimizations to lib/internal/webstreams/* that reduce
per-chunk and per-construction allocations on hot paths without
observable behavior change.
Per-chunk: reuse promise-reaction closures per controller, add buffered
fast path for async iterator, specialize callback wrappers by arity,
and share immutable nil records for writable stream resets.
Per-construction: use queueMicrotask for non-object start results,
materialize reader/writer .closed and .ready records lazily, and remove
dead allocations.
Assisted-by: Claude Fable 5
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/63876
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Mattias Buelens <mattias@buelens.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>