Repository navigation
Conversation
Constructing a TransformStream allocated five closures for the sink and source algorithms of its two sides, and each side adopted the start promise through a wrapper promise plus a thenable job, so a stream without a start() left about three kilobytes pending in the microtask queue until the started steps ran. Per-stream creation bursts spend most of their time copying that graph through the scavenger. The sink and source algorithms are now shared functions that reach the transform stream through a field on their controller state (the controller is passed to the close, abort and cancel algorithms for that), and the post-start steps of both sides are delivered by one reaction chain on a shared promise, taking the same microtask hops as the spec's start promise adoption: three after construction for a non-thenable start result, two after the adopting promise settles for a thenable one. The readable and writable transfer state records are materialized on first transfer instead of per stream. Signed-off-by: Matteo Collina <hello@matteocollina.com>
The close and abort methods of an underlying sink and the flush and cancel methods of a transformer were each wrapped at construction in an async closure that the controller kept for the stream's lifetime: two per WritableStream and two per TransformStream, each with its context. Store these one-shot methods as-is in the controller state next to the object they belong to, and call them as that object's methods with the spec's argument list through a promiseCall helper that keeps the reference implementation's adoption ticks. The per-chunk methods (write, transform) keep their closures, which make the method a constant for the optimizing compiler on the hot paths; so does the source's cancel wrapper, since the readable stream's construction path is sensitive to any change in its inlining budget. In the byte controller, the transfer of a pull-into descriptor's buffer when it is committed is dropped: nothing can reach that buffer any more, since it was transferred when the descriptor was created and the BYOB request that exposed it is only ever invalidated together with a transfer. The transfer of the first pending descriptor's buffer on enqueue is likewise only needed while a BYOB request exposes it. Allocating an ArrayBuffer costs about half a microsecond, so those two transfers were a large part of a BYOB read. Bytes allocated per stream with user methods: WritableStream 1040 -> 844, TransformStream 1816 -> 1720. The microtask ordering of every user method call is unchanged. Benchmark results (20 runs, vs. the previous commit): webstreams/readable-read.js type='byob' n=100000 *** 17.14 % ±1.80% webstreams/readable-async-iterator.js type='normal' n=100000 3.79 % ±4.49% webstreams/from.js kind='sync' n=1000000 * -2.18 % ±2.02% webstreams/js_transfer.js n=10000 payload='TransformStream' * -1.85 % ±1.58% The other 42 rows are within their confidence intervals; the two rows at the 5% level are the expected number of false positives for 46 comparisons. Signed-off-by: Matteo Collina <hello@matteocollina.com>
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 23 of the webstreams performance work. It is stacked on #66643 (the first commit here is that PR's commit); only the second commit,
stream: trim webstreams sink wrappers and BYOB transfers, is new.Sink and transformer one-shot methods without wrappers
The
closeandabortmethods of an underlying sink and theflushandcancelmethods of a transformer were each wrapped at construction in an async closure kept by the controller for the stream's lifetime (two perWritableStream, two perTransformStream, with their contexts). They are now stored as-is in the controller state next to the object they belong to and called as that object's methods, with the spec's argument list, through apromiseCallhelper that reproduces the reference implementation's adoption ticks (a synchronous throw becomes a rejection, a returned promise settles the caller two microtasks later, a plain value settles it at once).The per-chunk methods (
write,transform) keep their closures: the captured method is a constant for TurboFan there, and replacing the closure with aFunctionPrototypeCallon a stored method lost the inlining of the user function on the pipe-to rows. The source'scancelwrapper is also kept, because every variant that touched the readable stream's construction path shifted its inlining budget enough to show up in the creation rows.Bytes allocated per stream with user methods:
WritableStream1040 → 844,TransformStream1816 → 1720.Unobservable BYOB transfers
Each BYOB read performed up to four
ArrayBuffertransfers, and allocating anArrayBuffercosts about half a microsecond here. Two of them are dropped:ReadableByteStreamControllerConvertPullIntoDescriptorno longer transfers the descriptor's buffer before building the result view. By then nothing else can reach that buffer: it was transferred when the descriptor was created, and the BYOB request that exposes it is only ever invalidated together with a transfer.enqueue(), the first pending descriptor's buffer is only transferred when a BYOB request exposes it; without one there is no view to detach.Verification
test-whatwg-webstreams-underlying-methods.js) pass.start/pull/cancel/write/close/abort/transform/flush) returning a value, a native promise, a thenable, throwing, or returning a rejected promise logs the same microtask tick for every observable step on the base and patched builds, as do the ordering probes from the previous rounds.Benchmarks
benchmark/compare.js, 20 runs, against the previous commit:The other 42 rows are within their confidence intervals. The two rows at the 5% level are the expected number of false positives for 46 comparisons; the transfer row is the one that could be real (it was between -1.5% and -2.7% at the 5% level in three runs), and I could not find a mechanism for it, since the transfer path only loses two closures per
WritableStream.AI Generated, reviewed by me.