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.

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