stream: reduce webstreams encoding and iteration overhead - #65414
Open
mcollina wants to merge 3 commits into
Open
stream: reduce webstreams encoding and iteration overhead#65414mcollina wants to merge 3 commits into
mcollina wants to merge 3 commits into
Conversation
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com>
The encode-and-enqueue transform walked the chunk code unit by code
unit, materializing a single-character string per index and building
the output with string concatenation. The only state that crosses
chunks is a trailing high (leading) surrogate, and TextEncoder.encode's
USVString conversion already replaces every interior lone surrogate
with U+FFFD, which is exactly what the spec loop produces. Join a
pending high surrogate with the incoming chunk, hold back a new
trailing high surrogate, and encode the rest in a single native call.
The streaming decode path also reuses a single options object instead
of allocating { stream: true } per chunk.
An encoding-streams benchmark is added since the suite had no
TextEncoderStream/TextDecoderStream row. Encoding improves by ~546%
with 1KB string chunks and ~20% with 16-character chunks; decode is
unchanged.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
The pull algorithm was an async function that awaited iterator.next() and then the produced value, costing an async-function frame plus two await wrappers and a controller-side reaction per chunk. Rewrite it callback-style: the next() result is adopted exactly like the previous awaits (including the observable .then lookup on plain object values), the reaction steps are created once per stream, and completion is delivered straight to the controller's cached pull reactions using the parked-algorithm-result contract. The iterator's next method is also looked up once at setup, per the spec's GetIteratorDirect. A from benchmark is added since the suite had no ReadableStream.from row. Iterating a stream built from a sync generator improves by ~27% and from an async generator by ~23%; all other rows are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com>
Collaborator
|
Review requested:
|
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65414 +/- ##
==========================================
- Coverage 90.11% 90.10% -0.01%
==========================================
Files 752 752
Lines 252208 252358 +150
Branches 47447 47456 +9
==========================================
+ Hits 227274 227397 +123
- Misses 16219 16264 +45
+ Partials 8715 8697 -18
🚀 New features to boost your workflow:
|
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.
This PR is stacked on #65143 — please review the last two commits only.
Two independent optimizations for WHATWG streams:
TextEncoderStream: encode whole chunks natively. The encode-and-enqueue transform walked every code unit in JS, materializing a one-character string per index and building the output via string concatenation. The only state that crosses chunks is a trailing high (leading) surrogate, andTextEncoder.encode()'s USVString conversion already replaces interior lone surrogates with U+FFFD exactly like the spec loop. The transform now joins a pending high surrogate, holds back a new trailing one, and encodes the rest in a single native call. Verified byte-identical to the previous algorithm over 200k randomized surrogate-heavy chunk sequences, plus the full WPT encoding suite. The streaming decode path also stops allocating a{ stream: true }options object per chunk.ReadableStream.from(): drop the async pull machinery. The pull algorithm was an async function awaitingiterator.next()and then the value — an async frame, two await wrappers, and a controller-side reaction per chunk. It is now callback-style with per-stream cached reaction steps, delivering completion directly to the controller's cached pull reactions (the parked-algorithm-result contract from #65143). Thenable adoption is preserved, including the observable.thenlookup on plain object values. The iterator'snextmethod is now looked up once at setup, matching the spec's GetIteratorDirect.Benchmark results (
benchmark/compare.js --runs 10, new rows added since the suite covered neither path):All other webstreams rows (pipe-to ×9, pipe-through, read/read-buffered, async-iterator, tee, creation, js_transfer, decode) are unchanged. Full WPT streams + encoding suites and the parallel webstreams/whatwg test batches pass.