Skip to content

stream: speed up async iteration over WHATWG byte streams#64291

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

stream: speed up async iteration over WHATWG byte streams#64291
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:webstream-byte-iteration-perf

Conversation

@mcollina

@mcollina mcollina commented Jul 4, 2026

Copy link
Copy Markdown
Member

for await / reader.read() loops over byte streams were ~4x slower than over default streams:

  • replace the ReflectGet(view.constructor.prototype, ...)-based ArrayBufferView getters with primordial getters (~3.5x faster at the call level, and no longer spoofable via a user-defined .constructor)
  • extend the buffered fast paths in ReadableStreamDefaultReader.read() and the async iterator to byte controllers, skipping the per-chunk read request + PromiseWithResolvers when data is queued
  • consolidate the reader predicate cascade in the byte controller's enqueue into a single pass; reuse the async iterator's read request object

benchmark/webstreams (--runs 10): readable-async-iterator type=bytes +16.3% (***), readable-read byob +9.1% (***), all other rows neutral. WPT streams/compression/encoding results unchanged.

@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 labels Jul 4, 2026
mcollina added 2 commits July 4, 2026 23:29
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina mcollina force-pushed the webstream-byte-iteration-perf branch from 2782884 to fa8ad29 Compare July 4, 2026 21:29
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 5, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jul 5, 2026
@nodejs-github-bot

nodejs-github-bot commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74561/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1881/
Benchmark GHA: https://github.com/nodejs/node/actions/runs/28751317798

Results
                                                                       confidence improvement accuracy (*)   (**)  (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000                              -0.02 %       ±0.50% ±0.67% ±0.87%
webstreams/creation.js kind='ReadableStream' n=50000                                   3.76 %       ±5.22% ±6.95% ±9.05%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000                        -0.20 %       ±1.58% ±2.11% ±2.75%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000                     -0.60 %       ±1.53% ±2.04% ±2.66%
webstreams/creation.js kind='TransformStream' n=50000                                  0.50 %       ±1.39% ±1.85% ±2.41%
webstreams/creation.js kind='WritableStream' n=50000                                   0.27 %       ±1.57% ±2.09% ±2.71%
webstreams/js_transfer.js n=10000 payload='ReadableStream'                             0.13 %       ±0.53% ±0.70% ±0.92%
webstreams/js_transfer.js n=10000 payload='TransformStream'                            0.23 %       ±0.47% ±0.63% ±0.81%
webstreams/js_transfer.js n=10000 payload='WritableStream'                             0.20 %       ±0.60% ±0.80% ±1.04%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000                -0.26 %       ±1.52% ±2.03% ±2.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000                 0.42 %       ±1.42% ±1.89% ±2.46%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000         **     -1.53 %       ±1.13% ±1.51% ±1.96%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000                 -1.05 %       ±1.20% ±1.60% ±2.09%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000                -0.86 %       ±1.26% ±1.68% ±2.19%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000                 0.25 %       ±1.36% ±1.80% ±2.35%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000                 0.53 %       ±1.02% ±1.36% ±1.77%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000                  0.08 %       ±1.35% ±1.80% ±2.34%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000                 0.00 %       ±1.30% ±1.73% ±2.26%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000                -0.75 %       ±1.26% ±1.68% ±2.19%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000                -1.00 %       ±1.51% ±2.01% ±2.61%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000                  0.13 %       ±1.33% ±1.77% ±2.30%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000                  0.25 %       ±1.21% ±1.61% ±2.09%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000                  0.00 %       ±1.29% ±1.72% ±2.24%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000                  0.64 %       ±1.22% ±1.63% ±2.12%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000                   0.04 %       ±1.14% ±1.51% ±1.97%
webstreams/readable-async-iterator.js type='bytes' n=100000                   ***     13.42 %       ±0.83% ±1.11% ±1.44%
webstreams/readable-async-iterator.js type='normal' n=100000                           0.87 %       ±3.92% ±5.22% ±6.80%
webstreams/readable-read-buffered.js bufferSize=1 n=100000                            -0.95 %       ±2.10% ±2.79% ±3.64%
webstreams/readable-read-buffered.js bufferSize=10 n=100000                           -1.40 %       ±4.38% ±5.83% ±7.59%
webstreams/readable-read-buffered.js bufferSize=100 n=100000                          -1.80 %       ±5.12% ±6.81% ±8.86%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000                         -2.79 %       ±4.64% ±6.17% ±8.04%
webstreams/readable-read.js type='byob' n=100000                              ***      8.58 %       ±0.97% ±1.29% ±1.69%
webstreams/readable-read.js type='normal' n=100000                                    -0.42 %       ±2.33% ±3.10% ±4.04%

Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 33 comparisons, you can thus
expect the following amount of false-positive results:
  1.65 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.33 false positives, when considering a   1% risk acceptance (**, ***),
  0.03 false positives, when considering a 0.1% risk acceptance (***)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants