Repository navigation
Conversation
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>
transformStreamDefaultControllerPerformTransform chained a rejection step onto the result of every transform() call. A non-thenable result cannot reject, so return the shared resolved promise directly and save a promise allocation and a microtask per chunk. The write promise of a transform whose transform() returns a non-thenable now settles one microtask earlier than in the reference implementation. The relative order of all stream events is unchanged. 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 24 of the webstreams performance work. It is stacked on #66702 (the first commit here is that PR's commit); only the second commit,
stream: drop rejection step for sync transforms, is new.Rejection step skipped for non-thenable transform results
transformStreamDefaultControllerPerformTransformchained.then(undefined, rejectionSteps)onto the result of everytransform()call, mirroring the reference implementation'spromiseCall(transformAlgorithm, ...).then(undefined, ...). Whentransform()returns a value that is not a thenable (the commontransform(chunk, controller) { controller.enqueue(chunk); }shape, and the identity transform), that result cannot reject, so the rejection step can never run. The shared resolved promise is now returned directly, saving a promise and a microtask per chunk. Promise and thenable results keep the rejection step unchanged.Observable timing
The write promise of such a transform settles one microtask earlier than in the reference implementation. The relative order of every observable stream event is unchanged: a probe of 8 transformer shapes (sync,
async, returned promise, thenable, throwing, rejecting,controller.error(),controller.terminate()) at both readable high water marks, logging writes, reads,ready,closeand bothclosedpromises, produces the same event sequence on the base and patched builds; only the tick numbers of the sync,error()andterminate()shapes move. This is only observable by counting microtasks.I also tried returning the user's promise for
async transform()(with a non-rethrowing error handler registered first): it measured +0..1.6% because the async function's own promise dominates, so it is not worth the broader timing change.Verification
write/close/abortsucceeding and failing, transformertransform/flush/cancelsucceeding, throwing and rejecting, parked writes at both high water marks,error()/terminate(), thenables,pipeThroughwith and without an abort signal; sync and async variants) records theAsyncLocalStoragestore seen by every user callback and every reaction on the returned promises. The callback → store mapping is identical to the base build in every scenario, and no callback runs outside its context.Benchmarks
benchmark/compare.js, 20 runs, against the previous commit:The other 10 rows (creation, js_transfer, the remaining lifecycle kinds) are within their confidence intervals. The two creation rows at the 5% level do not touch the changed code and match the expected number of false positives for 19 comparisons.
AI Generated, reviewed by me.