stream: cut per-chunk overhead in web streams pipelines - #65437
Open
anonrig wants to merge 3 commits into
Open
Conversation
Collaborator
|
Review requested:
|
anonrig
marked this pull request as ready for review
August 20, 2026 16:29
cursor
Bot
force-pushed
the
cursor/webstreams-pipeline-perf-93a7
branch
from
August 20, 2026 16:30
796edc7 to
8019292
Compare
Rewrite CompressionStream and DecompressionStream on top of a TransformStream that drives the zlib (or brotli) handle synchronously, instead of wrapping a zlib stream.Duplex in the web streams adapters. The previous implementation dispatched every written chunk to the threadpool and waited for the event loop to observe its completion, which dominates the cost of streaming small chunks: a 64 MiB body written in 4 KiB chunks paid for 16384 threadpool round trips plus the Transform and adapter machinery around them. Processing the chunks inline removes that latency entirely while performing the same work. Inputs larger than 64 KiB are processed in slices with a turn of the event loop in between so that huge chunks cannot block the loop for their full duration. Output is emitted in up to 64 KiB chunks, either as zero-copy views or as right-sized copies (so small outputs do not retain large buffers), which also reduces the per-chunk overhead imposed on the rest of the pipeline downstream. Assisted-by: Cursor Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
The encode-and-enqueue algorithm was implemented as a literal transcription of the spec's per-code-unit loop: it extracted a single-character string, called charCodeAt(), and appended to an accumulator string for every code unit of every chunk, allocating millions of temporary strings for large payloads. The only observable effects of that loop are that a high surrogate at the end of a chunk is held back to pair with a low surrogate starting the next chunk, and that unpaired surrogates encode as U+FFFD, which TextEncoder already does. Handle the chunk boundary explicitly and encode the rest of the chunk with a single encode() call. Assisted-by: Cursor Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
cursor
Bot
force-pushed
the
cursor/webstreams-pipeline-perf-93a7
branch
from
August 20, 2026 16:39
8019292 to
9d691c1
Compare
jasnell
previously requested changes
Aug 20, 2026
jasnell
left a comment
Member
There was a problem hiding this comment.
AI agents cannot used Signed-off-by
Member
Author
Sorry for the frustration. My AI keeps making the same mistake. Fixing it now. |
cursor
Bot
force-pushed
the
cursor/webstreams-pipeline-perf-93a7
branch
from
August 20, 2026 16:42
9d691c1 to
eea0fe9
Compare
cursor
Bot
force-pushed
the
cursor/webstreams-pipeline-perf-93a7
branch
from
August 20, 2026 16:45
eea0fe9 to
936186f
Compare
The kValidateChunk and kDestroyOnSyncError hooks existed only for the previous stream.Duplex-based CompressionStream implementation, which no longer uses the adapters. Assisted-by: Cursor Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
cursor
Bot
force-pushed
the
cursor/webstreams-pipeline-perf-93a7
branch
from
August 20, 2026 17:11
936186f to
e3f6bd2
Compare
anonrig
requested review from
RafaelGSS,
aduh95,
benjamingr,
jasnell,
mcollina and
ronag
August 20, 2026 18:10
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65437 +/- ##
==========================================
- Coverage 90.31% 90.11% -0.20%
==========================================
Files 751 752 +1
Lines 249956 252618 +2662
Branches 47204 47503 +299
==========================================
+ Hits 225745 227649 +1904
- Misses 15612 16252 +640
- Partials 8599 8717 +118
🚀 New features to boost your workflow:
|
Collaborator
Collaborator
Collaborator
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.
Reduces per-chunk overhead across common web streams pipelines (
fetchbody decompression/upload,TextDecoderStream/TextEncoderStreamtranscoding), targeting workloads that move large payloads in small (e.g. 4 KiB) chunks.Changes
stream: process CompressionStream chunks without threadpool round trips
CompressionStream/DecompressionStreamwrapped a zlibstream.Duplexin the web streams adapters, so every written chunk was dispatched to the threadpool and its completion observed on a later event loop turn. For a 64 MiB body written in 4 KiB chunks that is 16384 threadpool round trips plus theTransformand adapter machinery around them. The classes are now built on aTransformStreamthat drives the raw zlib/brotli handle synchronously, mirroring the write loop of the zlib streams (same handle setup, same error mapping, same trailing-garbage rejection). Inputs larger than 64 KiB are processed in slices with an event-loop turn in between, so a huge chunk cannot block the loop for its full duration. Output is emitted in up to 64 KiB chunks, either as zero-copy views or as right-sized copies so tiny chunks do not retain large buffers.stream: fast-path TextEncoderStream chunk encoding
The encode-and-enqueue algorithm was a literal transcription of the spec's per-code-unit loop: it extracted a single-character string, called
charCodeAt(), and appended to an accumulator string for every code unit of every chunk (~67 million temporary strings for a 64 MiB payload). The loop's only observable effects are the surrogate hand-off at chunk boundaries and U+FFFD replacement of unpaired surrogates, whichTextEncoder.encode()already performs; the transform now handles the chunk boundary explicitly and encodes the rest of the chunk with a singleencode()call.stream: remove unused sync-error destroy plumbing from adapters
The
kValidateChunk/kDestroyOnSyncErrorhooks existed only for the previousDuplex-based compression implementation.Benchmarks
64 MiB of log-like text, 4 KiB chunks, best of 5, idle Linux x64. Baseline is
v27.0.0-nightly20260819(currentmain); "this PR" is a from-source build of this branch.End-to-end pipelines
fetch→ gunzip → decode →for await)fetchPOST)fetch→cat→for await)Isolated stages (64 MiB through just the stage, 4 KiB chunks)
TextEncoderStreamCompressionStream('gzip')DecompressionStream('gzip')End-to-end wall time is capped by physical floors on this machine (
gzipSyncof this payload is ~620 ms of raw CPU;gunzipSyncis ~139 ms; UTF-8 decode+encode of 64 MiB is ~84 ms). After this PR the remaining non-codec overhead on upload is ~130 ms (~8 µs/chunk) and on download is well under the gunzip floor. A literal 10x on wall time is not available for upload/download/subprocess; the one place a full 10x+ existed was the encoder stage.Adds
benchmark/webstreams/compression.jsandbenchmark/webstreams/encoding.js.Behavior notes
Uint8Arrayviews over a largerArrayBuffer(when at least half the buffer is filled) rather than always exact-sized copies. Those views never share memory with one another: the backing buffer is retired after a view is emitted.nullrejects withERR_STREAM_NULL_VALUES, non-BufferSource chunks reject withERR_INVALID_ARG_TYPE, trailing garbage rejects withERR_TRAILING_JUNK_AFTER_STREAM_END(allTypeErrors), and'brotli'remains supported.Testing
Local
out/Release/node(this branch): 30 targeted tests, all passing, includingtest-whatwg-webstreams-compression,test-webstreams-compression-bad-chunks,test-webstreams-decompression-reject-trailing,test-webstreams-compression-buffer-source,test-compression-decompression-stream,test-zlib-type-error,test-whatwg-webstreams-encoding, theReadable.toWeb/ adapters tests, and WPTcompression,encoding, andstreams.