Skip to content

stream: reduce allocations in StreamBase reads and writes#64455

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants