Skip to content

stream: move WHATWG byte-stream helpers to C++#63570

Open
mcollina wants to merge 1 commit into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 1 commit into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers

Conversation

@mcollina
Copy link
Copy Markdown
Member

Implement five defensive helpers from lib/internal/webstreams/util.js in a new internal binding (src/node_webstreams.cc) using v8::ArrayBufferView / v8::ArrayBuffer APIs directly:

  • arrayBufferViewGet{Buffer,ByteLength,ByteOffset}
  • canCopyArrayBuffer
  • cloneAsUint8Array

The previous JS versions used Reflect.get against view.constructor.prototype and ArrayBuffer.prototype.{slice,getDetached,getByteLength} via primordials to survive prototype tampering. The C++ versions preserve the same defensive semantics without the JS-side overhead.

benchmark/webstreams/readable-read.js type=bytes (BYOB read path, which exercises these helpers on every chunk) improves by ~15% on my workstation. WPT streams parity preserved: 1403 subtests passing, 0 unexpected failures.

@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/gyp

@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 May 25, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 25, 2026

Codecov Report

❌ Patch coverage is 80.61224% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.34%. Comparing base (74ccf38) to head (e2c7b65).
⚠️ Report is 85 commits behind head on main.

Files with missing lines Patch % Lines
src/node_webstreams.cc 71.21% 6 Missing and 13 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #63570      +/-   ##
==========================================
+ Coverage   90.32%   90.34%   +0.01%     
==========================================
  Files         730      733       +3     
  Lines      234209   236495    +2286     
  Branches    43934    44542     +608     
==========================================
+ Hits       211558   213652    +2094     
- Misses      14372    14549     +177     
- Partials     8279     8294      +15     
Files with missing lines Coverage Δ
lib/internal/webstreams/readablestream.js 98.54% <100.00%> (+<0.01%) ⬆️
lib/internal/webstreams/util.js 99.51% <100.00%> (-0.06%) ⬇️
src/node_binding.cc 82.74% <ø> (ø)
src/node_external_reference.h 100.00% <ø> (ø)
src/node_webstreams.cc 71.21% <71.21%> (ø)

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

Comment thread src/node_webstreams.cc
size_t from_byte_length = BufferByteLength(args[2]);

bool ok = static_cast<uint64_t>(to_index) + count <= to_byte_length &&
static_cast<uint64_t>(from_index) + count <= from_byte_length;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this guard against overflows?

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

Comment thread lib/internal/webstreams/util.js Outdated
Comment on lines +25 to +27
arrayBufferViewGetBuffer: ArrayBufferViewGetBuffer,
arrayBufferViewGetByteLength: ArrayBufferViewGetByteLength,
arrayBufferViewGetByteOffset: ArrayBufferViewGetByteOffset,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen to performance if we made this a single getArrayBufferView that returned a { buffer, byteLength, byteOffset }? I'm just wondering if cutting the number of binding traversals would be cheaper, even if it does occasionally fetch unneeded properties.

Comment thread src/node_webstreams.cc Outdated
Comment on lines +31 to +36
inline size_t BufferByteLength(Local<Value> buffer) {
if (buffer->IsArrayBuffer()) {
return buffer.As<ArrayBuffer>()->ByteLength();
}
return buffer.As<SharedArrayBuffer>()->ByteLength();
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, this isn't necessary: SAB handles are ArrayBuffer handles in V8, and calling sab.As<ArrayBuffer>()->ByteLength() is legal. The V8 API has this behaviour baked in (calling abv->Buffer() on a SAB-backed view returns the SAB as an ArrayBuffer handle), and we already rely on this behaviour ourselves (when passing a SAB-backed view to ArrayBufferViewContents).

Comment thread src/node_webstreams.cc Outdated
Comment on lines +23 to +29
inline bool BufferIsDetached(Local<Value> buffer) {
if (buffer->IsArrayBuffer()) {
return buffer.As<ArrayBuffer>()->WasDetached();
}
// SharedArrayBuffers cannot be detached.
return false;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also doesn't need an AB/SAB typeguard, sab.As<ArrayBuffer>().WasDetached() is legal, and will always return false.

Comment thread src/node_webstreams.cc Outdated
CHECK(args[0]->IsArrayBufferView());
Local<ArrayBufferView> view = args[0].As<ArrayBufferView>();
size_t byte_length = view->ByteLength();
Local<ArrayBuffer> new_buffer = ArrayBuffer::New(isolate, byte_length);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably use MaybeNew(), otherwise it'll crash the process on OOM.

Implement three defensive helpers from lib/internal/webstreams/util.js
in a new internal binding (src/node_webstreams.cc):

  * getArrayBufferView
  * canCopyArrayBuffer
  * cloneAsUint8Array

The previous JavaScript versions used Reflect.get on
view.constructor.prototype and called ArrayBuffer.prototype methods
through primordials so they would survive prototype tampering. The C++
versions use the V8 ArrayBufferView and ArrayBuffer APIs directly,
preserving the same robustness without the JS-side overhead.

getArrayBufferView returns { buffer, byteOffset, byteLength } in a
single binding crossing, replacing three separate accessors.
cloneAsUint8Array uses ArrayBuffer::MaybeNew so the process is not
killed on allocation failure.

These functions sit on the byte-stream hot paths
(ReadableByteStreamController enqueue/read, pull-into descriptor copy,
tee clones). ReadableStream type='bytes' throughput on
benchmark/webstreams/readable-read.js improves by ~15% on the BYOB
read path on my workstation.

WPT streams parity is preserved (1403 subtests passing, 0 unexpected
failures, identical to baseline).

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina mcollina force-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65 Compare May 31, 2026 10:38
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.

5 participants