Skip to content

stream: reduce allocations in StreamBase reads and writes - #64455

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:streambase-perf
Open

stream: reduce allocations in StreamBase reads and writes#64455
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:streambase-perf

Conversation

@mcollina

Copy link
Copy Markdown
Member

Read buffers are now allocated from a 64KB slab and handed to JS as views (no per-read allocation, map bookkeeping, or resize copy), and WriteWrap objects are only created when a write does not complete synchronously.

net/net-pipe.js len=1024 type=buf                  +28.4%  (t=22.8)
net/net-c2s.js len=1024 type=buf                   +21.2%  (t=18.0)
net/net-pipe.js len=65536 type=buf                  +4.4%  (t=7.4)
tls/throughput-c2s.js size=16384 type=buf           +2.9%  (t=3.0)
net/tcp-raw-pipe.js, net-s2c, tls s2c              neutral

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Jul 12, 2026
mcollina added 2 commits July 12, 2026 17:46
Read buffers for streams that emit their data to JS were allocated per
read: a 64KB backing store, tracked in a map, and then - since reads
rarely fill the whole buffer - reallocated to the right size and copied.

Allocate read buffers from a 64KB slab instead. Reads reserve the
suggested size from the slab and JS receives a view over the slab's
ArrayBuffer at the read's offset, using the offset mechanism that
onStreamRead already supports. Unused reservation space is rewound when
a read returns less than was reserved, so small reads (e.g. TLS records)
share a slab.

This removes the per-read allocations, the map bookkeeping and the
resize copy.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
Every stream write created a WriteWrap JS object up front, even though
most writes complete synchronously via uv_try_write() and never use it.

Let stream_base_commons pass null instead of a request object.
StreamBase::Write() already creates the wrap object only when the write
does not complete synchronously; return that object to JS (which
attaches oncomplete/callback to it) and a plain error code otherwise.

Writes that complete synchronously now cross the JS/C++ boundary once
and allocate nothing. Callers that pass in a request object
(child_process IPC, webstreams adapters) behave as before.

Since Http2Stream::DoWrite() can invoke the completion callback
synchronously - before JS has attached oncomplete - such completions
are now recorded on the request object's writeStatus field and replayed
by stream_base_commons after dispatch. This also replaces a Has() plus
name-based MakeCallback() pair with a single Get().

Also pre-create the JS fields of WriteWrap instances in the object
template, as was already done for ShutdownWrap, so that they are
in-object properties.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
StreamBase read buffers are now views over a shared 64KB slab
(ArrayBuffer plus offset) rather than right-sized copies, so
newReadableStreamFromStreamBase must slice nread bytes out of the
slab at streamBaseState[kArrayBufferOffset] before enqueueing the
chunk, instead of enqueueing the whole slab ArrayBuffer.

Fixes test-whatwg-webstreams-adapters-streambase.js failing CI.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 15, 2026
@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.50000% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.30%. Comparing base (c8996ea) to head (91250fb).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
src/stream_base.cc 52.63% 24 Missing and 12 partials ⚠️
src/env.cc 78.78% 8 Missing and 6 partials ⚠️
lib/internal/stream_base_commons.js 95.89% 3 Missing ⚠️
src/env.h 90.90% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64455      +/-   ##
==========================================
- Coverage   90.33%   90.30%   -0.03%     
==========================================
  Files         751      751              
  Lines      250048   250476     +428     
  Branches    47254    47347      +93     
==========================================
+ Hits       225877   226193     +316     
- Misses      15553    15655     +102     
- Partials     8618     8628      +10     
Files with missing lines Coverage Δ
lib/internal/webstreams/adapters.js 86.72% <100.00%> (+0.02%) ⬆️
src/stream_base.h 62.06% <ø> (ø)
src/stream_wrap.cc 88.15% <100.00%> (+0.60%) ⬆️
src/env.h 97.01% <90.90%> (-1.20%) ⬇️
lib/internal/stream_base_commons.js 94.86% <95.89%> (-1.31%) ⬇️
src/env.cc 85.09% <78.78%> (-0.17%) ⬇️
src/stream_base.cc 76.26% <52.63%> (-3.67%) ⬇️

... and 34 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. request-ci Add this label to start a Jenkins CI on a PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants