A source without pull() has nothing observable left to do in its
post-start step, since the started flag only gates calls into the pull
algorithm. Set the flag right away instead of from a microtask, so a
push-style ReadableStream allocates neither the closure nor the task
and is not kept alive until the next microtask checkpoint.
pipeTo and tee hold the only references to their reader and writer, so
their [[closedPromise]] records are never observed as promises. Install
the watchers as the records themselves, as pipeTo's ready hook already
does, instead of materializing a promise plus reaction per side, and
hand the erroring/release probes one shared pending promise. The tee's
cancel promise is likewise materialized by the first branch cancel.
Microtask ordering is unchanged: each hook enqueues its watcher at the
position the promise reaction would have had.
node benchmark/compare.js --runs 20 over benchmark/webstreams (46 rows,
all others within the confidence interval):
webstreams/creation.js kind='ReadableStream' *** +172.40%
webstreams/creation.js kind='ReadableStream.tee' *** +20.47%
webstreams/creation.js kind='ReadableStreamBYOBReader' *** +15.66%
webstreams/creation.js kind='ReadableStreamDefaultReader' *** +14.53%
webstreams/lifecycle.js kind='pipe-to' (40 runs) ** +10.98%
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Round 21 of the webstreams performance work (follows #66392). It targets what a stream costs before its first chunk and after its last one: construction,
tee()andpipeTo()setup and shutdown.No post-start microtask for sources without
pull()setupReadableStreamDefaultControllerqueued a microtask after everystart()to flip the controller'sstartedflag and callpull()if needed. That flag only gates calls into the pull algorithm, so for a source withoutpull()(push-style sources that enqueue fromstart(), ornew ReadableStream()) the step has nothing observable left to do. The flag is now set synchronously in that case and no closure or task is allocated. Sources withpull()keep the microtask, at the same position. Byte streams get the same treatment.This also stops a burst of short-lived streams from being kept alive until the next microtask checkpoint, which is where most of the time in the creation benchmark went.
Closed watchers as records instead of promises
pipeTo()holds the only references to its reader and writer, andtee()to its reader, so their[[closedPromise]]records are never observed as promises. Each pipe still materialized both records plus a reaction to watch for close and error, and each tee one record plus a reaction. The watchers are now installed as the records themselves (ClosedPromiseHook), the way the pipe's ready hook already worked: the settle sites resolve or reject the record where they would have settled the promise, and the hook enqueues the watcher at the microtask position its reaction would have had. The erroring and release paths probe the record'spromise, so every hook carries one shared forever-pending promise, whichsetPromiseHandled()skips.The tee's cancel promise is materialized by the first branch cancel only.
Tests
test/parallel/test-whatwg-readablestream-tee-cancel-settle.jscovers the cancel promise settling before and after materialization (cancel one branch then close or error the source, close then cancel both, cancel both) and a tee of an already errored source, for default and byte streams.A 53-scenario microtask-ordering stress (pull, push, iterators, tee incl. byte tee, pipeTo incl. every shutdown path, transform backpressure,
ReadableStream.from, writers) logs identically againstmain, also with--no-async-context-frame. WPT streams and the webstreams parallel batch are green.Benchmark
node benchmark/compare.js --runs 20overbenchmark/webstreams:The
lifecycle.jsrows re-run with 40 samples, since the pipe-to row has a wide interval:The long-running rows (pipe-to, pipe-through, tee, read, from) are flat as expected: the savings are per stream, per pipe and per tee, not per chunk. A local harness that creates and drains a 4-chunk push-style stream measured +22 %, and a 4-chunk pipe +10 %.
AI generated, humanly reviewed.