stream: reduce webstreams encoding and iteration overhead - #65414
Open
mcollina wants to merge 2 commits into
Open
stream: reduce webstreams encoding and iteration overhead#65414mcollina wants to merge 2 commits into
mcollina wants to merge 2 commits into
Conversation
Collaborator
|
Review requested:
|
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65414 +/- ##
==========================================
- Coverage 90.12% 90.09% -0.03%
==========================================
Files 752 752
Lines 252317 252380 +63
Branches 47435 47437 +2
==========================================
- Hits 227401 227389 -12
- Misses 16209 16311 +102
+ Partials 8707 8680 -27
🚀 New features to boost your workflow:
|
jasnell
reviewed
Aug 20, 2026
| // operation and takes responsibility for delivering the fulfilled (or | ||
| // rejected) continuation itself later, instead of settling a promise | ||
| // (see the transform stream source pull algorithm). | ||
| const kParkedAlgorithmResult = { __proto__: null }; |
mcollina
force-pushed
the
webstream-perf-round14
branch
2 times, most recently
from
August 20, 2026 17:29
fde1b0b to
4a02249
Compare
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>
mcollina
force-pushed
the
webstream-perf-round14
branch
from
August 21, 2026 01:08
4a02249 to
c8e5339
Compare
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.