Skip to content

perf(ext/web): shared underlying-source algorithms, hoist resource-bridge read request - #35810

Merged
bartlomieju merged 2 commits into
mainfrom
perf/web-streams-source-dispatch
Jul 6, 2026
Merged

perf(ext/web): shared underlying-source algorithms, hoist resource-bridge read request#35810
bartlomieju merged 2 commits into
mainfrom
perf/web-streams-source-dispatch

Conversation

@bartlomieju

@bartlomieju bartlomieju commented Jul 6, 2026

Copy link
Copy Markdown
Member

Two per-allocation-site reductions on the ReadableStream construction
and server-body paths, continuing #35768.

Constructing a ReadableStream from an underlying source allocated up to
three wrapper closures (start/pull/cancel around
webidl.invokeCallbackFunction). The controller now carries the
underlying source and its converted dict in two slots, and the wrappers
become shared module-level functions; cancel receives the controller as
a second argument from cancelSteps (internal cancel algorithms ignore
the extra argument), and the byte controller's start invocation now
passes the controller like the default one already did. The slots are
cleared in clearAlgorithms so no user references outlive the stream.
This mainly matters for servers creating a body stream per request.

readableStreamReadFn, the resource bridge that pumps a JS-source stream
into a Deno.serve response body, allocated a read request object with
three closure methods plus two write-continuation closures per chunk;
these are now allocated once per stream, with per-iteration state in
reassigned slots (exactly one read is in flight per iteration).

Performance

These are per-allocation-site reductions, so the win is in GC pressure, not
best-pass latency (a min-time microbenchmark deliberately excludes GC).
Isolated A/B: release builds of this PR's HEAD vs. its parent on main
(the two differ only in ext/web/06_streams.js), macOS arm64 (M-series).

Constructing 3,000,000 new ReadableStream({ pull, cancel }) (drained
between batches), best of 3 runs:

Metric Baseline (main) This PR Δ
Total time (incl. GC) 747 ms 687 ms −8.0%
Minor GCs (scavenges) 224 202 −10%

Where it is (correctly) flat, within noise:

Benchmark Baseline This PR Δ
construct (best-pass latency) ~113 ns/op ~112 ns/op flat
construct byte (best-pass) ~129 ns/op ~127 ns/op noise
Deno.serve streamed body (32 MiB / 8192 chunks per resp) ~0.49 M chunks/s ~0.49 M chunks/s noise

So the benefit is reduced allocation/GC pressure on the construction and
per-chunk serve-body paths (matters for servers churning a body stream per
request under load), not raw latency or single-connection throughput.

Benchmarks used:

// construction (GC-pressure): total wall time + `--v8-flags=--trace-gc`
for (let done = 0; done < 3_000_000; done += 5000) {
  for (let i = 0; i < 5000; i++) new ReadableStream({ pull(c) {}, cancel(r) {} });
  await null; // drain pull microtasks so streams are collectable
}

// serve JS-source streamed body (readableStreamReadFn per chunk)
const CHUNK = new Uint8Array(4096);
const server = Deno.serve({ port: 8765, onListen() {} }, () => {
  let i = 0;
  return new Response(new ReadableStream({
    pull(c) { if (i++ < 8192) c.enqueue(CHUNK.slice()); else c.close(); },
  }));
});
// fetch the 32 MiB body, measure best-pass chunk throughput

WPT streams, fetch/api/response, and fetch/api/body should be run
alongside (the reused read request and the shared cancel path are the
sensitive parts, plus Deno.serve streamed-body integration tests).

Towards #35768

â€Ķidge read request

Constructing a ReadableStream from an underlying source allocated up to
three wrapper closures (start/pull/cancel around
webidl.invokeCallbackFunction). The controller now carries the
underlying source and its converted dict in two slots, and the wrappers
become shared module-level functions; cancel receives the controller as
a second argument from cancelSteps (internal cancel algorithms ignore
it), and the byte controller's start invocation now passes the
controller like the default one does. The slots are cleared together
with the other algorithms so no user references outlive
clearAlgorithms. This matters for servers that create a body stream per
request.

readableStreamReadFn (the resource bridge that pumps a JS stream into a
Deno.serve response body) allocated a read request object with three
closure methods plus two write-continuation closures per chunk; they are
now allocated once per stream, with per-iteration state in reassigned
slots (safe: exactly one read is in flight per iteration).

Towards #35768
â€Ķce-dispatch

# Conflicts:
#	ext/web/06_streams.js
@bartlomieju
bartlomieju enabled auto-merge (squash) July 6, 2026 14:40
@bartlomieju
bartlomieju merged commit 02b41b4 into main Jul 6, 2026
268 of 270 checks passed
@bartlomieju
bartlomieju deleted the perf/web-streams-source-dispatch branch July 6, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant