Conversation
ReadableStream.prototype.values() built each iterator from an object literal with a computed symbol-key method plus five closures. Such a literal is rebuilt through the runtime on every evaluation, costing close to a microsecond per iterator, which dominates iterating a short-lived stream. Move next() and return() to a shared ReadableStreamAsyncIterator prototype, as for any WebIDL async iterator, and keep the per-iterator state in its read request. The prototype chain and property shape are the ones WPT checks; the placeholder AsyncIterator object in util.js is no longer needed. As in WebIDL, next() and return() now reject when called on something that is not a ReadableStream async iterator, and iterators no longer carry own next/return properties. Add an async-iterator kind to benchmark/webstreams/lifecycle.js. webstreams/lifecycle.js kind='async-iterator' *** +16.22% Signed-off-by: Matteo Collina <hello@matteocollina.com>
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>
Collaborator
|
Review requested:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.