Skip to content

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

Closed
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:streambase-perf
Closed

stream: reduce allocations in StreamBase reads and writes#64455
mcollina wants to merge 2 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 involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Jul 12, 2026
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Aug 15, 2026
@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.73109% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.05%. Comparing base (7aaf9b4) to head (ba6a523).
⚠️ Report is 269 commits behind head on main.

Files with missing lines Patch % Lines
src/stream_base.cc 53.24% 24 Missing and 12 partials ⚠️
src/env.cc 80.00% 8 Missing and 5 partials ⚠️
lib/internal/stream_base_commons.js 95.89% 3 Missing ⚠️
src/env.h 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64455      +/-   ##
==========================================
- Coverage   90.13%   90.05%   -0.09%     
==========================================
  Files         751      755       +4     
  Lines      253639   257245    +3606     
  Branches    47790    48761     +971     
==========================================
+ Hits       228618   231654    +3036     
- Misses      16264    16685     +421     
- Partials     8757     8906     +149     
Files with missing lines Coverage Δ
lib/internal/webstreams/adapters.js 86.72% <100.00%> (+0.11%) ⬆️
src/stream_base.h 62.06% <ø> (ø)
src/stream_wrap.cc 88.15% <100.00%> (+0.60%) ⬆️
src/env.h 97.01% <88.88%> (-1.20%) ⬇️
lib/internal/stream_base_commons.js 94.86% <95.89%> (-1.31%) ⬇️
src/env.cc 82.19% <80.00%> (-3.16%) ⬇️
src/stream_base.cc 76.30% <53.24%> (-3.70%) ⬇️

... and 111 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.

@mcollina mcollina added request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. and removed request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. labels Aug 19, 2026
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>
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Sep 7, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell jasnell added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-rebase PRs the Commit Queue should land as multiple self-contained commits. labels Sep 7, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in a701540...b113d09

nodejs-github-bot pushed a commit that referenced this pull request Sep 7, 2026
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>
PR-URL: #64455
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
nodejs-github-bot pushed a commit that referenced this pull request Sep 7, 2026
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>
PR-URL: #64455
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
@nodejs-github-bot nodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 7, 2026
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++. commit-queue-rebase PRs the Commit Queue should land as multiple self-contained commits. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants