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.

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

  * arrayBufferViewGetBuffer
  * arrayBufferViewGetByteLength
  * arrayBufferViewGetByteOffset
  * 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.

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).
@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 75.00000% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.33%. Comparing base (74ccf38) to head (c232ba8).
⚠️ Report is 34 commits behind head on main.

Files with missing lines Patch % Lines
src/node_webstreams.cc 70.66% 6 Missing and 16 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #63570    +/-   ##
========================================
  Coverage   90.32%   90.33%            
========================================
  Files         730      731     +1     
  Lines      234209   234514   +305     
  Branches    43934    43922    -12     
========================================
+ Hits       211558   211850   +292     
- Misses      14372    14376     +4     
- Partials     8279     8288     +9     
Files with missing lines Coverage Δ
lib/internal/webstreams/util.js 99.52% <100.00%> (-0.05%) ⬇️
src/node_binding.cc 82.74% <ø> (ø)
src/node_external_reference.h 100.00% <ø> (ø)
src/node_webstreams.cc 70.66% <70.66%> (ø)

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

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.

4 participants