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

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 4 commits 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-botnodejs-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 May 25, 2026
@codecov

codecovBot commented May 25, 2026

Copy link
Copy Markdown

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 linesPatch %Lines
src/node_webstreams.cc71.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 linesCoverage Δ
lib/internal/webstreams/readablestream.js98.54% <100.00%> (+<0.01%)⬆️
lib/internal/webstreams/util.js99.51% <100.00%> (-0.06%)⬇️
src/node_binding.cc82.74% <ø> (ø)
src/node_external_reference.h100.00% <ø> (ø)
src/node_webstreams.cc71.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 threadsrc/node_webstreams.cc Outdated
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?

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.

defensively, probably. In practice overflow is extremely unlikely so I'd say it's likely optional.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actionsgithub-actionsBot 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 threadlib/internal/webstreams/util.js Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65CompareMay 31, 2026 10:38
Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +56 to +60
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

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.

Note that this will crash the process with large buffers, eg.

newReadableStream({type: 'bytes',pull(controller){controller.enqueue(newUint8Array(2**32))},}).getReader({mode: 'byob'}).read(newUint8Array(2**32))

The byte lengths should probably be validated as Numbers and read as int64_t with IntegerValue().

Comment threadsrc/node_webstreams.cc Outdated
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));

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.

Using a cached DictionaryTemplate is going to be faster.

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.

Alternatively, returning these as an Array might also be faster.

Comment threadsrc/node_webstreams.cc Outdated
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {

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.

Given that these aren't specific to web streams, it might make sense to just include in them in the existing utils internal binding rathe than creating Yet Another Internal Binding.

Comment threadsrc/node_webstreams.cc Outdated
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "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.

If you're not going to use a cached DictionaryTemplate or Array, then these should use the existing env->buffer_string() and add env->byte_offset_string() and env->byte_length_string() to avoid the additional allocations on every call.

Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +8 to +23
namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

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.

Nit: matching convention....

Suggested change
namespacenode {
namespacewebstreams {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacenode {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacewebstreams {

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from c20a8cc to 606d472CompareJune 15, 2026 06:34
@mcollina

Copy link
Copy Markdown
MemberAuthor

@jasnell@addaleax@anonrig PTAL

Comment threadsrc/node_buffer.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch 2 times, most recently from e9b8ad1 to ee8c2edCompareJune 16, 2026 09:20
@mcollina

Copy link
Copy Markdown
MemberAuthor

cc @Renegade334@jasnell

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
Comment threadsrc/node_buffer.cc Outdated
Comment threadsrc/node_buffer.cc Outdated
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from ee8c2ed to 6f829bbCompareJune 17, 2026 12:51
nodejs-github-bot pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

mcollinaand others added 4 commits June 20, 2026 16:22
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
- Replace magic number literal with static_cast expression
- Remove unnecessary intermediate variables in CopyArrayBuffer
@aduh95
aduh95force-pushed the stream/webstreams-cpp-helpers branch from d48e8b7 to fb477ffCompareJune 20, 2026 14:25
@nodejs-github-bot

nodejs-github-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74307/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1874/

Benchmark results
 confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/creation.js kind='ReadableStream' n=50000 1.14 % ±5.00% ±6.66% ±8.67%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000 0.30 % ±1.51% ±2.01% ±2.63%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000 -0.38 % ±1.35% ±1.80% ±2.35%
webstreams/creation.js kind='TransformStream' n=50000 0.28 % ±1.21% ±1.60% ±2.09%
webstreams/creation.js kind='WritableStream' n=50000 -0.27 % ±1.62% ±2.16% ±2.81%
webstreams/js_transfer.js n=10000 payload='ReadableStream' 0.61 % ±0.62% ±0.82% ±1.07%
webstreams/js_transfer.js n=10000 payload='TransformStream' -0.11 % ±0.37% ±0.49% ±0.63%
webstreams/js_transfer.js n=10000 payload='WritableStream' -0.11 % ±0.48% ±0.64% ±0.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 ** -1.79 % ±1.12% ±1.49% ±1.94%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000 ** -1.40 % ±0.86% ±1.14% ±1.49%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000 ** -1.64 % ±1.05% ±1.40% ±1.83%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000 * -1.61 % ±1.25% ±1.67% ±2.17%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000 ** -1.63 % ±1.14% ±1.51% ±1.97%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000 * -1.09 % ±1.04% ±1.39% ±1.81%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000 * -1.08 % ±0.90% ±1.19% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** -1.40 % ±0.90% ±1.20% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000 * -1.42 % ±1.14% ±1.52% ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * -1.36 % ±1.03% ±1.38% ±1.79%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000 * -1.56 % ±1.25% ±1.66% ±2.16%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000 ** -1.69 % ±1.12% ±1.49% ±1.94%
webstreams/readable-async-iterator.js n=100000 -0.79 % ±1.80% ±2.40% ±3.12%
webstreams/readable-read-buffered.js bufferSize=1 n=100000 -0.92 % ±1.96% ±2.61% ±3.41%
webstreams/readable-read-buffered.js bufferSize=10 n=100000 -2.71 % ±3.74% ±4.97% ±6.47%
webstreams/readable-read-buffered.js bufferSize=100 n=100000 -0.61 % ±2.69% ±3.58% ±4.67%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000 1.90 % ±3.10% ±4.13% ±5.37%
webstreams/readable-read.js type='byob' n=100000 -0.98 % ±1.03% ±1.37% ±1.78%
webstreams/readable-read.js type='normal' n=100000 -0.16 % ±1.49% ±1.99% ±2.59%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
1.60 false positives, when considering a 5% risk acceptance (*, **, ***),
0.32 false positives, when considering a 1% risk acceptance (**, ***),
0.03 false positives, when considering a 0.1% risk acceptance (***)

@aduh95

Copy link
Copy Markdown
Contributor

Benchmark CI is showing perf regressions 🤔

webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%

aduh95 pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@mcollina

Copy link
Copy Markdown
MemberAuthor

Very weird. It's not the first time that locally I get consistent/reproducible perf improvements that are not materialized in the benchmark CI.

@mcollina

Copy link
Copy Markdown
MemberAuthor

What makes it even more interesting is that these changes should not be affecting pipe-to.

@aduh95

Copy link
Copy Markdown
Contributor

Interestingly, https://github.com/aduh95/node/actions/runs/27939962137 seems to indicate that those perf regression are x64 Linux specific – that being said, it wasn't able to replicate any non-negligible perf improvement on other platforms :/

aduh95 pushed a commit to aduh95/node that referenced this pull request Jul 30, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#63973
Refs: nodejs#63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 6, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Backport-PR-URL: #64675
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for freeto 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 / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@addaleax@anonrig@Renegade334
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 4 commits 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-botnodejs-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 May 25, 2026
@codecov

codecovBot commented May 25, 2026

Copy link
Copy Markdown

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 linesPatch %Lines
src/node_webstreams.cc71.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 linesCoverage Δ
lib/internal/webstreams/readablestream.js98.54% <100.00%> (+<0.01%)⬆️
lib/internal/webstreams/util.js99.51% <100.00%> (-0.06%)⬇️
src/node_binding.cc82.74% <ø> (ø)
src/node_external_reference.h100.00% <ø> (ø)
src/node_webstreams.cc71.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 threadsrc/node_webstreams.cc Outdated
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?

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.

defensively, probably. In practice overflow is extremely unlikely so I'd say it's likely optional.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actionsgithub-actionsBot 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 threadlib/internal/webstreams/util.js Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65CompareMay 31, 2026 10:38
Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +56 to +60
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

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.

Note that this will crash the process with large buffers, eg.

newReadableStream({type: 'bytes',pull(controller){controller.enqueue(newUint8Array(2**32))},}).getReader({mode: 'byob'}).read(newUint8Array(2**32))

The byte lengths should probably be validated as Numbers and read as int64_t with IntegerValue().

Comment threadsrc/node_webstreams.cc Outdated
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));

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.

Using a cached DictionaryTemplate is going to be faster.

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.

Alternatively, returning these as an Array might also be faster.

Comment threadsrc/node_webstreams.cc Outdated
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {

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.

Given that these aren't specific to web streams, it might make sense to just include in them in the existing utils internal binding rathe than creating Yet Another Internal Binding.

Comment threadsrc/node_webstreams.cc Outdated
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "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.

If you're not going to use a cached DictionaryTemplate or Array, then these should use the existing env->buffer_string() and add env->byte_offset_string() and env->byte_length_string() to avoid the additional allocations on every call.

Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +8 to +23
namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

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.

Nit: matching convention....

Suggested change
namespacenode {
namespacewebstreams {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacenode {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacewebstreams {

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from c20a8cc to 606d472CompareJune 15, 2026 06:34
@mcollina

Copy link
Copy Markdown
MemberAuthor

@jasnell@addaleax@anonrig PTAL

Comment threadsrc/node_buffer.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch 2 times, most recently from e9b8ad1 to ee8c2edCompareJune 16, 2026 09:20
@mcollina

Copy link
Copy Markdown
MemberAuthor

cc @Renegade334@jasnell

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
Comment threadsrc/node_buffer.cc Outdated
Comment threadsrc/node_buffer.cc Outdated
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from ee8c2ed to 6f829bbCompareJune 17, 2026 12:51
nodejs-github-bot pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

mcollinaand others added 4 commits June 20, 2026 16:22
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
- Replace magic number literal with static_cast expression
- Remove unnecessary intermediate variables in CopyArrayBuffer
@aduh95
aduh95force-pushed the stream/webstreams-cpp-helpers branch from d48e8b7 to fb477ffCompareJune 20, 2026 14:25
@nodejs-github-bot

nodejs-github-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74307/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1874/

Benchmark results
 confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/creation.js kind='ReadableStream' n=50000 1.14 % ±5.00% ±6.66% ±8.67%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000 0.30 % ±1.51% ±2.01% ±2.63%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000 -0.38 % ±1.35% ±1.80% ±2.35%
webstreams/creation.js kind='TransformStream' n=50000 0.28 % ±1.21% ±1.60% ±2.09%
webstreams/creation.js kind='WritableStream' n=50000 -0.27 % ±1.62% ±2.16% ±2.81%
webstreams/js_transfer.js n=10000 payload='ReadableStream' 0.61 % ±0.62% ±0.82% ±1.07%
webstreams/js_transfer.js n=10000 payload='TransformStream' -0.11 % ±0.37% ±0.49% ±0.63%
webstreams/js_transfer.js n=10000 payload='WritableStream' -0.11 % ±0.48% ±0.64% ±0.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 ** -1.79 % ±1.12% ±1.49% ±1.94%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000 ** -1.40 % ±0.86% ±1.14% ±1.49%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000 ** -1.64 % ±1.05% ±1.40% ±1.83%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000 * -1.61 % ±1.25% ±1.67% ±2.17%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000 ** -1.63 % ±1.14% ±1.51% ±1.97%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000 * -1.09 % ±1.04% ±1.39% ±1.81%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000 * -1.08 % ±0.90% ±1.19% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** -1.40 % ±0.90% ±1.20% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000 * -1.42 % ±1.14% ±1.52% ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * -1.36 % ±1.03% ±1.38% ±1.79%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000 * -1.56 % ±1.25% ±1.66% ±2.16%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000 ** -1.69 % ±1.12% ±1.49% ±1.94%
webstreams/readable-async-iterator.js n=100000 -0.79 % ±1.80% ±2.40% ±3.12%
webstreams/readable-read-buffered.js bufferSize=1 n=100000 -0.92 % ±1.96% ±2.61% ±3.41%
webstreams/readable-read-buffered.js bufferSize=10 n=100000 -2.71 % ±3.74% ±4.97% ±6.47%
webstreams/readable-read-buffered.js bufferSize=100 n=100000 -0.61 % ±2.69% ±3.58% ±4.67%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000 1.90 % ±3.10% ±4.13% ±5.37%
webstreams/readable-read.js type='byob' n=100000 -0.98 % ±1.03% ±1.37% ±1.78%
webstreams/readable-read.js type='normal' n=100000 -0.16 % ±1.49% ±1.99% ±2.59%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
1.60 false positives, when considering a 5% risk acceptance (*, **, ***),
0.32 false positives, when considering a 1% risk acceptance (**, ***),
0.03 false positives, when considering a 0.1% risk acceptance (***)

@aduh95

Copy link
Copy Markdown
Contributor

Benchmark CI is showing perf regressions 🤔

webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%

aduh95 pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@mcollina

Copy link
Copy Markdown
MemberAuthor

Very weird. It's not the first time that locally I get consistent/reproducible perf improvements that are not materialized in the benchmark CI.

@mcollina

Copy link
Copy Markdown
MemberAuthor

What makes it even more interesting is that these changes should not be affecting pipe-to.

@aduh95

Copy link
Copy Markdown
Contributor

Interestingly, https://github.com/aduh95/node/actions/runs/27939962137 seems to indicate that those perf regression are x64 Linux specific – that being said, it wasn't able to replicate any non-negligible perf improvement on other platforms :/

aduh95 pushed a commit to aduh95/node that referenced this pull request Jul 30, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#63973
Refs: nodejs#63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 6, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Backport-PR-URL: #64675
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for freeto 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 / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@addaleax@anonrig@Renegade334
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 4 commits 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-botnodejs-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 May 25, 2026
@codecov

codecovBot commented May 25, 2026

Copy link
Copy Markdown

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 linesPatch %Lines
src/node_webstreams.cc71.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 linesCoverage Δ
lib/internal/webstreams/readablestream.js98.54% <100.00%> (+<0.01%)⬆️
lib/internal/webstreams/util.js99.51% <100.00%> (-0.06%)⬇️
src/node_binding.cc82.74% <ø> (ø)
src/node_external_reference.h100.00% <ø> (ø)
src/node_webstreams.cc71.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 threadsrc/node_webstreams.cc Outdated
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?

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.

defensively, probably. In practice overflow is extremely unlikely so I'd say it's likely optional.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actionsgithub-actionsBot 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 threadlib/internal/webstreams/util.js Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65CompareMay 31, 2026 10:38
Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +56 to +60
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

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.

Note that this will crash the process with large buffers, eg.

newReadableStream({type: 'bytes',pull(controller){controller.enqueue(newUint8Array(2**32))},}).getReader({mode: 'byob'}).read(newUint8Array(2**32))

The byte lengths should probably be validated as Numbers and read as int64_t with IntegerValue().

Comment threadsrc/node_webstreams.cc Outdated
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));

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.

Using a cached DictionaryTemplate is going to be faster.

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.

Alternatively, returning these as an Array might also be faster.

Comment threadsrc/node_webstreams.cc Outdated
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {

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.

Given that these aren't specific to web streams, it might make sense to just include in them in the existing utils internal binding rathe than creating Yet Another Internal Binding.

Comment threadsrc/node_webstreams.cc Outdated
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "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.

If you're not going to use a cached DictionaryTemplate or Array, then these should use the existing env->buffer_string() and add env->byte_offset_string() and env->byte_length_string() to avoid the additional allocations on every call.

Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +8 to +23
namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

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.

Nit: matching convention....

Suggested change
namespacenode {
namespacewebstreams {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacenode {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacewebstreams {

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from c20a8cc to 606d472CompareJune 15, 2026 06:34
@mcollina

Copy link
Copy Markdown
MemberAuthor

@jasnell@addaleax@anonrig PTAL

Comment threadsrc/node_buffer.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch 2 times, most recently from e9b8ad1 to ee8c2edCompareJune 16, 2026 09:20
@mcollina

Copy link
Copy Markdown
MemberAuthor

cc @Renegade334@jasnell

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
Comment threadsrc/node_buffer.cc Outdated
Comment threadsrc/node_buffer.cc Outdated
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from ee8c2ed to 6f829bbCompareJune 17, 2026 12:51
nodejs-github-bot pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

mcollinaand others added 4 commits June 20, 2026 16:22
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
- Replace magic number literal with static_cast expression
- Remove unnecessary intermediate variables in CopyArrayBuffer
@aduh95
aduh95force-pushed the stream/webstreams-cpp-helpers branch from d48e8b7 to fb477ffCompareJune 20, 2026 14:25
@nodejs-github-bot

nodejs-github-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74307/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1874/

Benchmark results
 confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/creation.js kind='ReadableStream' n=50000 1.14 % ±5.00% ±6.66% ±8.67%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000 0.30 % ±1.51% ±2.01% ±2.63%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000 -0.38 % ±1.35% ±1.80% ±2.35%
webstreams/creation.js kind='TransformStream' n=50000 0.28 % ±1.21% ±1.60% ±2.09%
webstreams/creation.js kind='WritableStream' n=50000 -0.27 % ±1.62% ±2.16% ±2.81%
webstreams/js_transfer.js n=10000 payload='ReadableStream' 0.61 % ±0.62% ±0.82% ±1.07%
webstreams/js_transfer.js n=10000 payload='TransformStream' -0.11 % ±0.37% ±0.49% ±0.63%
webstreams/js_transfer.js n=10000 payload='WritableStream' -0.11 % ±0.48% ±0.64% ±0.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 ** -1.79 % ±1.12% ±1.49% ±1.94%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000 ** -1.40 % ±0.86% ±1.14% ±1.49%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000 ** -1.64 % ±1.05% ±1.40% ±1.83%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000 * -1.61 % ±1.25% ±1.67% ±2.17%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000 ** -1.63 % ±1.14% ±1.51% ±1.97%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000 * -1.09 % ±1.04% ±1.39% ±1.81%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000 * -1.08 % ±0.90% ±1.19% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** -1.40 % ±0.90% ±1.20% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000 * -1.42 % ±1.14% ±1.52% ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * -1.36 % ±1.03% ±1.38% ±1.79%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000 * -1.56 % ±1.25% ±1.66% ±2.16%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000 ** -1.69 % ±1.12% ±1.49% ±1.94%
webstreams/readable-async-iterator.js n=100000 -0.79 % ±1.80% ±2.40% ±3.12%
webstreams/readable-read-buffered.js bufferSize=1 n=100000 -0.92 % ±1.96% ±2.61% ±3.41%
webstreams/readable-read-buffered.js bufferSize=10 n=100000 -2.71 % ±3.74% ±4.97% ±6.47%
webstreams/readable-read-buffered.js bufferSize=100 n=100000 -0.61 % ±2.69% ±3.58% ±4.67%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000 1.90 % ±3.10% ±4.13% ±5.37%
webstreams/readable-read.js type='byob' n=100000 -0.98 % ±1.03% ±1.37% ±1.78%
webstreams/readable-read.js type='normal' n=100000 -0.16 % ±1.49% ±1.99% ±2.59%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
1.60 false positives, when considering a 5% risk acceptance (*, **, ***),
0.32 false positives, when considering a 1% risk acceptance (**, ***),
0.03 false positives, when considering a 0.1% risk acceptance (***)

@aduh95

Copy link
Copy Markdown
Contributor

Benchmark CI is showing perf regressions 🤔

webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%

aduh95 pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@mcollina

Copy link
Copy Markdown
MemberAuthor

Very weird. It's not the first time that locally I get consistent/reproducible perf improvements that are not materialized in the benchmark CI.

@mcollina

Copy link
Copy Markdown
MemberAuthor

What makes it even more interesting is that these changes should not be affecting pipe-to.

@aduh95

Copy link
Copy Markdown
Contributor

Interestingly, https://github.com/aduh95/node/actions/runs/27939962137 seems to indicate that those perf regression are x64 Linux specific – that being said, it wasn't able to replicate any non-negligible perf improvement on other platforms :/

aduh95 pushed a commit to aduh95/node that referenced this pull request Jul 30, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#63973
Refs: nodejs#63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 6, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Backport-PR-URL: #64675
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for freeto 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 / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@addaleax@anonrig@Renegade334
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 4 commits 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-botnodejs-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 May 25, 2026
@codecov

codecovBot commented May 25, 2026

Copy link
Copy Markdown

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 linesPatch %Lines
src/node_webstreams.cc71.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 linesCoverage Δ
lib/internal/webstreams/readablestream.js98.54% <100.00%> (+<0.01%)⬆️
lib/internal/webstreams/util.js99.51% <100.00%> (-0.06%)⬇️
src/node_binding.cc82.74% <ø> (ø)
src/node_external_reference.h100.00% <ø> (ø)
src/node_webstreams.cc71.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 threadsrc/node_webstreams.cc Outdated
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?

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.

defensively, probably. In practice overflow is extremely unlikely so I'd say it's likely optional.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actionsgithub-actionsBot 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 threadlib/internal/webstreams/util.js Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65CompareMay 31, 2026 10:38
Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +56 to +60
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

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.

Note that this will crash the process with large buffers, eg.

newReadableStream({type: 'bytes',pull(controller){controller.enqueue(newUint8Array(2**32))},}).getReader({mode: 'byob'}).read(newUint8Array(2**32))

The byte lengths should probably be validated as Numbers and read as int64_t with IntegerValue().

Comment threadsrc/node_webstreams.cc Outdated
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));

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.

Using a cached DictionaryTemplate is going to be faster.

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.

Alternatively, returning these as an Array might also be faster.

Comment threadsrc/node_webstreams.cc Outdated
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {

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.

Given that these aren't specific to web streams, it might make sense to just include in them in the existing utils internal binding rathe than creating Yet Another Internal Binding.

Comment threadsrc/node_webstreams.cc Outdated
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "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.

If you're not going to use a cached DictionaryTemplate or Array, then these should use the existing env->buffer_string() and add env->byte_offset_string() and env->byte_length_string() to avoid the additional allocations on every call.

Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +8 to +23
namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

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.

Nit: matching convention....

Suggested change
namespacenode {
namespacewebstreams {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacenode {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacewebstreams {

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from c20a8cc to 606d472CompareJune 15, 2026 06:34
@mcollina

Copy link
Copy Markdown
MemberAuthor

@jasnell@addaleax@anonrig PTAL

Comment threadsrc/node_buffer.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch 2 times, most recently from e9b8ad1 to ee8c2edCompareJune 16, 2026 09:20
@mcollina

Copy link
Copy Markdown
MemberAuthor

cc @Renegade334@jasnell

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
Comment threadsrc/node_buffer.cc Outdated
Comment threadsrc/node_buffer.cc Outdated
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from ee8c2ed to 6f829bbCompareJune 17, 2026 12:51
nodejs-github-bot pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

mcollinaand others added 4 commits June 20, 2026 16:22
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
- Replace magic number literal with static_cast expression
- Remove unnecessary intermediate variables in CopyArrayBuffer
@aduh95
aduh95force-pushed the stream/webstreams-cpp-helpers branch from d48e8b7 to fb477ffCompareJune 20, 2026 14:25
@nodejs-github-bot

nodejs-github-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74307/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1874/

Benchmark results
 confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/creation.js kind='ReadableStream' n=50000 1.14 % ±5.00% ±6.66% ±8.67%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000 0.30 % ±1.51% ±2.01% ±2.63%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000 -0.38 % ±1.35% ±1.80% ±2.35%
webstreams/creation.js kind='TransformStream' n=50000 0.28 % ±1.21% ±1.60% ±2.09%
webstreams/creation.js kind='WritableStream' n=50000 -0.27 % ±1.62% ±2.16% ±2.81%
webstreams/js_transfer.js n=10000 payload='ReadableStream' 0.61 % ±0.62% ±0.82% ±1.07%
webstreams/js_transfer.js n=10000 payload='TransformStream' -0.11 % ±0.37% ±0.49% ±0.63%
webstreams/js_transfer.js n=10000 payload='WritableStream' -0.11 % ±0.48% ±0.64% ±0.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 ** -1.79 % ±1.12% ±1.49% ±1.94%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000 ** -1.40 % ±0.86% ±1.14% ±1.49%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000 ** -1.64 % ±1.05% ±1.40% ±1.83%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000 * -1.61 % ±1.25% ±1.67% ±2.17%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000 ** -1.63 % ±1.14% ±1.51% ±1.97%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000 * -1.09 % ±1.04% ±1.39% ±1.81%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000 * -1.08 % ±0.90% ±1.19% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** -1.40 % ±0.90% ±1.20% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000 * -1.42 % ±1.14% ±1.52% ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * -1.36 % ±1.03% ±1.38% ±1.79%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000 * -1.56 % ±1.25% ±1.66% ±2.16%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000 ** -1.69 % ±1.12% ±1.49% ±1.94%
webstreams/readable-async-iterator.js n=100000 -0.79 % ±1.80% ±2.40% ±3.12%
webstreams/readable-read-buffered.js bufferSize=1 n=100000 -0.92 % ±1.96% ±2.61% ±3.41%
webstreams/readable-read-buffered.js bufferSize=10 n=100000 -2.71 % ±3.74% ±4.97% ±6.47%
webstreams/readable-read-buffered.js bufferSize=100 n=100000 -0.61 % ±2.69% ±3.58% ±4.67%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000 1.90 % ±3.10% ±4.13% ±5.37%
webstreams/readable-read.js type='byob' n=100000 -0.98 % ±1.03% ±1.37% ±1.78%
webstreams/readable-read.js type='normal' n=100000 -0.16 % ±1.49% ±1.99% ±2.59%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
1.60 false positives, when considering a 5% risk acceptance (*, **, ***),
0.32 false positives, when considering a 1% risk acceptance (**, ***),
0.03 false positives, when considering a 0.1% risk acceptance (***)

@aduh95

Copy link
Copy Markdown
Contributor

Benchmark CI is showing perf regressions 🤔

webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%

aduh95 pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@mcollina

Copy link
Copy Markdown
MemberAuthor

Very weird. It's not the first time that locally I get consistent/reproducible perf improvements that are not materialized in the benchmark CI.

@mcollina

Copy link
Copy Markdown
MemberAuthor

What makes it even more interesting is that these changes should not be affecting pipe-to.

@aduh95

Copy link
Copy Markdown
Contributor

Interestingly, https://github.com/aduh95/node/actions/runs/27939962137 seems to indicate that those perf regression are x64 Linux specific – that being said, it wasn't able to replicate any non-negligible perf improvement on other platforms :/

aduh95 pushed a commit to aduh95/node that referenced this pull request Jul 30, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#63973
Refs: nodejs#63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 6, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Backport-PR-URL: #64675
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for freeto 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 / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@addaleax@anonrig@Renegade334
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 4 commits 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-botnodejs-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 May 25, 2026
@codecov

codecovBot commented May 25, 2026

Copy link
Copy Markdown

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 linesPatch %Lines
src/node_webstreams.cc71.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 linesCoverage Δ
lib/internal/webstreams/readablestream.js98.54% <100.00%> (+<0.01%)⬆️
lib/internal/webstreams/util.js99.51% <100.00%> (-0.06%)⬇️
src/node_binding.cc82.74% <ø> (ø)
src/node_external_reference.h100.00% <ø> (ø)
src/node_webstreams.cc71.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 threadsrc/node_webstreams.cc Outdated
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?

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.

defensively, probably. In practice overflow is extremely unlikely so I'd say it's likely optional.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actionsgithub-actionsBot 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 threadlib/internal/webstreams/util.js Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65CompareMay 31, 2026 10:38
Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +56 to +60
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

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.

Note that this will crash the process with large buffers, eg.

newReadableStream({type: 'bytes',pull(controller){controller.enqueue(newUint8Array(2**32))},}).getReader({mode: 'byob'}).read(newUint8Array(2**32))

The byte lengths should probably be validated as Numbers and read as int64_t with IntegerValue().

Comment threadsrc/node_webstreams.cc Outdated
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));

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.

Using a cached DictionaryTemplate is going to be faster.

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.

Alternatively, returning these as an Array might also be faster.

Comment threadsrc/node_webstreams.cc Outdated
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {

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.

Given that these aren't specific to web streams, it might make sense to just include in them in the existing utils internal binding rathe than creating Yet Another Internal Binding.

Comment threadsrc/node_webstreams.cc Outdated
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "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.

If you're not going to use a cached DictionaryTemplate or Array, then these should use the existing env->buffer_string() and add env->byte_offset_string() and env->byte_length_string() to avoid the additional allocations on every call.

Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +8 to +23
namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

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.

Nit: matching convention....

Suggested change
namespacenode {
namespacewebstreams {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacenode {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacewebstreams {

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from c20a8cc to 606d472CompareJune 15, 2026 06:34
@mcollina

Copy link
Copy Markdown
MemberAuthor

@jasnell@addaleax@anonrig PTAL

Comment threadsrc/node_buffer.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch 2 times, most recently from e9b8ad1 to ee8c2edCompareJune 16, 2026 09:20
@mcollina

Copy link
Copy Markdown
MemberAuthor

cc @Renegade334@jasnell

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
Comment threadsrc/node_buffer.cc Outdated
Comment threadsrc/node_buffer.cc Outdated
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from ee8c2ed to 6f829bbCompareJune 17, 2026 12:51
nodejs-github-bot pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

mcollinaand others added 4 commits June 20, 2026 16:22
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
- Replace magic number literal with static_cast expression
- Remove unnecessary intermediate variables in CopyArrayBuffer
@aduh95
aduh95force-pushed the stream/webstreams-cpp-helpers branch from d48e8b7 to fb477ffCompareJune 20, 2026 14:25
@nodejs-github-bot

nodejs-github-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74307/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1874/

Benchmark results
 confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/creation.js kind='ReadableStream' n=50000 1.14 % ±5.00% ±6.66% ±8.67%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000 0.30 % ±1.51% ±2.01% ±2.63%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000 -0.38 % ±1.35% ±1.80% ±2.35%
webstreams/creation.js kind='TransformStream' n=50000 0.28 % ±1.21% ±1.60% ±2.09%
webstreams/creation.js kind='WritableStream' n=50000 -0.27 % ±1.62% ±2.16% ±2.81%
webstreams/js_transfer.js n=10000 payload='ReadableStream' 0.61 % ±0.62% ±0.82% ±1.07%
webstreams/js_transfer.js n=10000 payload='TransformStream' -0.11 % ±0.37% ±0.49% ±0.63%
webstreams/js_transfer.js n=10000 payload='WritableStream' -0.11 % ±0.48% ±0.64% ±0.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 ** -1.79 % ±1.12% ±1.49% ±1.94%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000 ** -1.40 % ±0.86% ±1.14% ±1.49%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000 ** -1.64 % ±1.05% ±1.40% ±1.83%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000 * -1.61 % ±1.25% ±1.67% ±2.17%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000 ** -1.63 % ±1.14% ±1.51% ±1.97%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000 * -1.09 % ±1.04% ±1.39% ±1.81%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000 * -1.08 % ±0.90% ±1.19% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** -1.40 % ±0.90% ±1.20% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000 * -1.42 % ±1.14% ±1.52% ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * -1.36 % ±1.03% ±1.38% ±1.79%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000 * -1.56 % ±1.25% ±1.66% ±2.16%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000 ** -1.69 % ±1.12% ±1.49% ±1.94%
webstreams/readable-async-iterator.js n=100000 -0.79 % ±1.80% ±2.40% ±3.12%
webstreams/readable-read-buffered.js bufferSize=1 n=100000 -0.92 % ±1.96% ±2.61% ±3.41%
webstreams/readable-read-buffered.js bufferSize=10 n=100000 -2.71 % ±3.74% ±4.97% ±6.47%
webstreams/readable-read-buffered.js bufferSize=100 n=100000 -0.61 % ±2.69% ±3.58% ±4.67%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000 1.90 % ±3.10% ±4.13% ±5.37%
webstreams/readable-read.js type='byob' n=100000 -0.98 % ±1.03% ±1.37% ±1.78%
webstreams/readable-read.js type='normal' n=100000 -0.16 % ±1.49% ±1.99% ±2.59%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
1.60 false positives, when considering a 5% risk acceptance (*, **, ***),
0.32 false positives, when considering a 1% risk acceptance (**, ***),
0.03 false positives, when considering a 0.1% risk acceptance (***)

@aduh95

Copy link
Copy Markdown
Contributor

Benchmark CI is showing perf regressions 🤔

webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%

aduh95 pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@mcollina

Copy link
Copy Markdown
MemberAuthor

Very weird. It's not the first time that locally I get consistent/reproducible perf improvements that are not materialized in the benchmark CI.

@mcollina

Copy link
Copy Markdown
MemberAuthor

What makes it even more interesting is that these changes should not be affecting pipe-to.

@aduh95

Copy link
Copy Markdown
Contributor

Interestingly, https://github.com/aduh95/node/actions/runs/27939962137 seems to indicate that those perf regression are x64 Linux specific – that being said, it wasn't able to replicate any non-negligible perf improvement on other platforms :/

aduh95 pushed a commit to aduh95/node that referenced this pull request Jul 30, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#63973
Refs: nodejs#63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 6, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Backport-PR-URL: #64675
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for freeto 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 / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@addaleax@anonrig@Renegade334
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 4 commits 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-botnodejs-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 May 25, 2026
@codecov

codecovBot commented May 25, 2026

Copy link
Copy Markdown

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 linesPatch %Lines
src/node_webstreams.cc71.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 linesCoverage Δ
lib/internal/webstreams/readablestream.js98.54% <100.00%> (+<0.01%)⬆️
lib/internal/webstreams/util.js99.51% <100.00%> (-0.06%)⬇️
src/node_binding.cc82.74% <ø> (ø)
src/node_external_reference.h100.00% <ø> (ø)
src/node_webstreams.cc71.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 threadsrc/node_webstreams.cc Outdated
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?

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.

defensively, probably. In practice overflow is extremely unlikely so I'd say it's likely optional.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actionsgithub-actionsBot 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 threadlib/internal/webstreams/util.js Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65CompareMay 31, 2026 10:38
Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +56 to +60
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

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.

Note that this will crash the process with large buffers, eg.

newReadableStream({type: 'bytes',pull(controller){controller.enqueue(newUint8Array(2**32))},}).getReader({mode: 'byob'}).read(newUint8Array(2**32))

The byte lengths should probably be validated as Numbers and read as int64_t with IntegerValue().

Comment threadsrc/node_webstreams.cc Outdated
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));

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.

Using a cached DictionaryTemplate is going to be faster.

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.

Alternatively, returning these as an Array might also be faster.

Comment threadsrc/node_webstreams.cc Outdated
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {

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.

Given that these aren't specific to web streams, it might make sense to just include in them in the existing utils internal binding rathe than creating Yet Another Internal Binding.

Comment threadsrc/node_webstreams.cc Outdated
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "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.

If you're not going to use a cached DictionaryTemplate or Array, then these should use the existing env->buffer_string() and add env->byte_offset_string() and env->byte_length_string() to avoid the additional allocations on every call.

Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +8 to +23
namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

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.

Nit: matching convention....

Suggested change
namespacenode {
namespacewebstreams {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacenode {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacewebstreams {

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from c20a8cc to 606d472CompareJune 15, 2026 06:34
@mcollina

Copy link
Copy Markdown
MemberAuthor

@jasnell@addaleax@anonrig PTAL

Comment threadsrc/node_buffer.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch 2 times, most recently from e9b8ad1 to ee8c2edCompareJune 16, 2026 09:20
@mcollina

Copy link
Copy Markdown
MemberAuthor

cc @Renegade334@jasnell

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
Comment threadsrc/node_buffer.cc Outdated
Comment threadsrc/node_buffer.cc Outdated
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from ee8c2ed to 6f829bbCompareJune 17, 2026 12:51
nodejs-github-bot pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

mcollinaand others added 4 commits June 20, 2026 16:22
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
- Replace magic number literal with static_cast expression
- Remove unnecessary intermediate variables in CopyArrayBuffer
@aduh95
aduh95force-pushed the stream/webstreams-cpp-helpers branch from d48e8b7 to fb477ffCompareJune 20, 2026 14:25
@nodejs-github-bot

nodejs-github-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74307/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1874/

Benchmark results
 confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/creation.js kind='ReadableStream' n=50000 1.14 % ±5.00% ±6.66% ±8.67%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000 0.30 % ±1.51% ±2.01% ±2.63%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000 -0.38 % ±1.35% ±1.80% ±2.35%
webstreams/creation.js kind='TransformStream' n=50000 0.28 % ±1.21% ±1.60% ±2.09%
webstreams/creation.js kind='WritableStream' n=50000 -0.27 % ±1.62% ±2.16% ±2.81%
webstreams/js_transfer.js n=10000 payload='ReadableStream' 0.61 % ±0.62% ±0.82% ±1.07%
webstreams/js_transfer.js n=10000 payload='TransformStream' -0.11 % ±0.37% ±0.49% ±0.63%
webstreams/js_transfer.js n=10000 payload='WritableStream' -0.11 % ±0.48% ±0.64% ±0.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 ** -1.79 % ±1.12% ±1.49% ±1.94%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000 ** -1.40 % ±0.86% ±1.14% ±1.49%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000 ** -1.64 % ±1.05% ±1.40% ±1.83%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000 * -1.61 % ±1.25% ±1.67% ±2.17%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000 ** -1.63 % ±1.14% ±1.51% ±1.97%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000 * -1.09 % ±1.04% ±1.39% ±1.81%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000 * -1.08 % ±0.90% ±1.19% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** -1.40 % ±0.90% ±1.20% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000 * -1.42 % ±1.14% ±1.52% ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * -1.36 % ±1.03% ±1.38% ±1.79%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000 * -1.56 % ±1.25% ±1.66% ±2.16%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000 ** -1.69 % ±1.12% ±1.49% ±1.94%
webstreams/readable-async-iterator.js n=100000 -0.79 % ±1.80% ±2.40% ±3.12%
webstreams/readable-read-buffered.js bufferSize=1 n=100000 -0.92 % ±1.96% ±2.61% ±3.41%
webstreams/readable-read-buffered.js bufferSize=10 n=100000 -2.71 % ±3.74% ±4.97% ±6.47%
webstreams/readable-read-buffered.js bufferSize=100 n=100000 -0.61 % ±2.69% ±3.58% ±4.67%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000 1.90 % ±3.10% ±4.13% ±5.37%
webstreams/readable-read.js type='byob' n=100000 -0.98 % ±1.03% ±1.37% ±1.78%
webstreams/readable-read.js type='normal' n=100000 -0.16 % ±1.49% ±1.99% ±2.59%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
1.60 false positives, when considering a 5% risk acceptance (*, **, ***),
0.32 false positives, when considering a 1% risk acceptance (**, ***),
0.03 false positives, when considering a 0.1% risk acceptance (***)

@aduh95

Copy link
Copy Markdown
Contributor

Benchmark CI is showing perf regressions 🤔

webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%

aduh95 pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@mcollina

Copy link
Copy Markdown
MemberAuthor

Very weird. It's not the first time that locally I get consistent/reproducible perf improvements that are not materialized in the benchmark CI.

@mcollina

Copy link
Copy Markdown
MemberAuthor

What makes it even more interesting is that these changes should not be affecting pipe-to.

@aduh95

Copy link
Copy Markdown
Contributor

Interestingly, https://github.com/aduh95/node/actions/runs/27939962137 seems to indicate that those perf regression are x64 Linux specific – that being said, it wasn't able to replicate any non-negligible perf improvement on other platforms :/

aduh95 pushed a commit to aduh95/node that referenced this pull request Jul 30, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#63973
Refs: nodejs#63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 6, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Backport-PR-URL: #64675
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for freeto 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 / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@addaleax@anonrig@Renegade334
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 4 commits 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-botnodejs-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 May 25, 2026
@codecov

codecovBot commented May 25, 2026

Copy link
Copy Markdown

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 linesPatch %Lines
src/node_webstreams.cc71.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 linesCoverage Δ
lib/internal/webstreams/readablestream.js98.54% <100.00%> (+<0.01%)⬆️
lib/internal/webstreams/util.js99.51% <100.00%> (-0.06%)⬇️
src/node_binding.cc82.74% <ø> (ø)
src/node_external_reference.h100.00% <ø> (ø)
src/node_webstreams.cc71.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 threadsrc/node_webstreams.cc Outdated
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?

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.

defensively, probably. In practice overflow is extremely unlikely so I'd say it's likely optional.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actionsgithub-actionsBot 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 threadlib/internal/webstreams/util.js Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65CompareMay 31, 2026 10:38
Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +56 to +60
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

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.

Note that this will crash the process with large buffers, eg.

newReadableStream({type: 'bytes',pull(controller){controller.enqueue(newUint8Array(2**32))},}).getReader({mode: 'byob'}).read(newUint8Array(2**32))

The byte lengths should probably be validated as Numbers and read as int64_t with IntegerValue().

Comment threadsrc/node_webstreams.cc Outdated
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));

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.

Using a cached DictionaryTemplate is going to be faster.

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.

Alternatively, returning these as an Array might also be faster.

Comment threadsrc/node_webstreams.cc Outdated
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {

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.

Given that these aren't specific to web streams, it might make sense to just include in them in the existing utils internal binding rathe than creating Yet Another Internal Binding.

Comment threadsrc/node_webstreams.cc Outdated
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "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.

If you're not going to use a cached DictionaryTemplate or Array, then these should use the existing env->buffer_string() and add env->byte_offset_string() and env->byte_length_string() to avoid the additional allocations on every call.

Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +8 to +23
namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

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.

Nit: matching convention....

Suggested change
namespacenode {
namespacewebstreams {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacenode {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacewebstreams {

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from c20a8cc to 606d472CompareJune 15, 2026 06:34
@mcollina

Copy link
Copy Markdown
MemberAuthor

@jasnell@addaleax@anonrig PTAL

Comment threadsrc/node_buffer.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch 2 times, most recently from e9b8ad1 to ee8c2edCompareJune 16, 2026 09:20
@mcollina

Copy link
Copy Markdown
MemberAuthor

cc @Renegade334@jasnell

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
Comment threadsrc/node_buffer.cc Outdated
Comment threadsrc/node_buffer.cc Outdated
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from ee8c2ed to 6f829bbCompareJune 17, 2026 12:51
nodejs-github-bot pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

mcollinaand others added 4 commits June 20, 2026 16:22
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
- Replace magic number literal with static_cast expression
- Remove unnecessary intermediate variables in CopyArrayBuffer
@aduh95
aduh95force-pushed the stream/webstreams-cpp-helpers branch from d48e8b7 to fb477ffCompareJune 20, 2026 14:25
@nodejs-github-bot

nodejs-github-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74307/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1874/

Benchmark results
 confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/creation.js kind='ReadableStream' n=50000 1.14 % ±5.00% ±6.66% ±8.67%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000 0.30 % ±1.51% ±2.01% ±2.63%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000 -0.38 % ±1.35% ±1.80% ±2.35%
webstreams/creation.js kind='TransformStream' n=50000 0.28 % ±1.21% ±1.60% ±2.09%
webstreams/creation.js kind='WritableStream' n=50000 -0.27 % ±1.62% ±2.16% ±2.81%
webstreams/js_transfer.js n=10000 payload='ReadableStream' 0.61 % ±0.62% ±0.82% ±1.07%
webstreams/js_transfer.js n=10000 payload='TransformStream' -0.11 % ±0.37% ±0.49% ±0.63%
webstreams/js_transfer.js n=10000 payload='WritableStream' -0.11 % ±0.48% ±0.64% ±0.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 ** -1.79 % ±1.12% ±1.49% ±1.94%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000 ** -1.40 % ±0.86% ±1.14% ±1.49%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000 ** -1.64 % ±1.05% ±1.40% ±1.83%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000 * -1.61 % ±1.25% ±1.67% ±2.17%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000 ** -1.63 % ±1.14% ±1.51% ±1.97%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000 * -1.09 % ±1.04% ±1.39% ±1.81%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000 * -1.08 % ±0.90% ±1.19% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** -1.40 % ±0.90% ±1.20% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000 * -1.42 % ±1.14% ±1.52% ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * -1.36 % ±1.03% ±1.38% ±1.79%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000 * -1.56 % ±1.25% ±1.66% ±2.16%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000 ** -1.69 % ±1.12% ±1.49% ±1.94%
webstreams/readable-async-iterator.js n=100000 -0.79 % ±1.80% ±2.40% ±3.12%
webstreams/readable-read-buffered.js bufferSize=1 n=100000 -0.92 % ±1.96% ±2.61% ±3.41%
webstreams/readable-read-buffered.js bufferSize=10 n=100000 -2.71 % ±3.74% ±4.97% ±6.47%
webstreams/readable-read-buffered.js bufferSize=100 n=100000 -0.61 % ±2.69% ±3.58% ±4.67%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000 1.90 % ±3.10% ±4.13% ±5.37%
webstreams/readable-read.js type='byob' n=100000 -0.98 % ±1.03% ±1.37% ±1.78%
webstreams/readable-read.js type='normal' n=100000 -0.16 % ±1.49% ±1.99% ±2.59%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
1.60 false positives, when considering a 5% risk acceptance (*, **, ***),
0.32 false positives, when considering a 1% risk acceptance (**, ***),
0.03 false positives, when considering a 0.1% risk acceptance (***)

@aduh95

Copy link
Copy Markdown
Contributor

Benchmark CI is showing perf regressions 🤔

webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%

aduh95 pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@mcollina

Copy link
Copy Markdown
MemberAuthor

Very weird. It's not the first time that locally I get consistent/reproducible perf improvements that are not materialized in the benchmark CI.

@mcollina

Copy link
Copy Markdown
MemberAuthor

What makes it even more interesting is that these changes should not be affecting pipe-to.

@aduh95

Copy link
Copy Markdown
Contributor

Interestingly, https://github.com/aduh95/node/actions/runs/27939962137 seems to indicate that those perf regression are x64 Linux specific – that being said, it wasn't able to replicate any non-negligible perf improvement on other platforms :/

aduh95 pushed a commit to aduh95/node that referenced this pull request Jul 30, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#63973
Refs: nodejs#63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 6, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Backport-PR-URL: #64675
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for freeto 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 / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@addaleax@anonrig@Renegade334
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Open
mcollina wants to merge 4 commits into
nodejs:mainfrom
mcollina:stream/webstreams-cpp-helpers
Open

stream: move WHATWG byte-stream helpers to C++#63570
mcollina wants to merge 4 commits 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-botnodejs-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 May 25, 2026
@codecov

codecovBot commented May 25, 2026

Copy link
Copy Markdown

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 linesPatch %Lines
src/node_webstreams.cc71.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 linesCoverage Δ
lib/internal/webstreams/readablestream.js98.54% <100.00%> (+<0.01%)⬆️
lib/internal/webstreams/util.js99.51% <100.00%> (-0.06%)⬇️
src/node_binding.cc82.74% <ø> (ø)
src/node_external_reference.h100.00% <ø> (ø)
src/node_webstreams.cc71.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 threadsrc/node_webstreams.cc Outdated
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?

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.

defensively, probably. In practice overflow is extremely unlikely so I'd say it's likely optional.

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 26, 2026
@github-actionsgithub-actionsBot 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 threadlib/internal/webstreams/util.js Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
Comment threadsrc/node_webstreams.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from a71a3f7 to e2c7b65CompareMay 31, 2026 10:38
Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +56 to +60
CHECK(args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsArrayBuffer() || args[2]->IsSharedArrayBuffer());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsUint32());

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.

Note that this will crash the process with large buffers, eg.

newReadableStream({type: 'bytes',pull(controller){controller.enqueue(newUint8Array(2**32))},}).getReader({mode: 'byob'}).read(newUint8Array(2**32))

The byte lengths should probably be validated as Numbers and read as int64_t with IntegerValue().

Comment threadsrc/node_webstreams.cc Outdated
Number::New(isolate, static_cast<double>(view->ByteLength())),
};
args.GetReturnValue().Set(
Object::New(isolate, Null(isolate), names, values, arraysize(names)));

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.

Using a cached DictionaryTemplate is going to be faster.

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.

Alternatively, returning these as an Array might also be faster.

Comment threadsrc/node_webstreams.cc Outdated
// Reflect.get(view.constructor.prototype, ..., view). Uses the V8 API
// directly so it is immune to prototype tampering and avoids the JS-side
// overhead of the defensive accessors in lib/internal/.
void GetArrayBufferView(const FunctionCallbackInfo<Value>& args) {

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.

Given that these aren't specific to web streams, it might make sense to just include in them in the existing utils internal binding rathe than creating Yet Another Internal Binding.

Comment threadsrc/node_webstreams.cc Outdated
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "buffer"),
FIXED_ONE_BYTE_STRING(isolate, "byteOffset"),
FIXED_ONE_BYTE_STRING(isolate, "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.

If you're not going to use a cached DictionaryTemplate or Array, then these should use the existing env->buffer_string() and add env->byte_offset_string() and env->byte_length_string() to avoid the additional allocations on every call.

Comment threadsrc/node_webstreams.cc Outdated
Comment on lines +8 to +23
namespace node {
namespace webstreams {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

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.

Nit: matching convention....

Suggested change
namespacenode {
namespacewebstreams {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacenode {
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
namespacewebstreams {

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from c20a8cc to 606d472CompareJune 15, 2026 06:34
@mcollina

Copy link
Copy Markdown
MemberAuthor

@jasnell@addaleax@anonrig PTAL

Comment threadsrc/node_buffer.cc Outdated
@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch 2 times, most recently from e9b8ad1 to ee8c2edCompareJune 16, 2026 09:20
@mcollina

Copy link
Copy Markdown
MemberAuthor

cc @Renegade334@jasnell

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
Comment threadsrc/node_buffer.cc Outdated
Comment threadsrc/node_buffer.cc Outdated
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina
mcollinaforce-pushed the stream/webstreams-cpp-helpers branch from ee8c2ed to 6f829bbCompareJune 17, 2026 12:51
nodejs-github-bot pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

mcollinaand others added 4 commits June 20, 2026 16:22
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>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
- Replace magic number literal with static_cast expression
- Remove unnecessary intermediate variables in CopyArrayBuffer
@aduh95
aduh95force-pushed the stream/webstreams-cpp-helpers branch from d48e8b7 to fb477ffCompareJune 20, 2026 14:25
@nodejs-github-bot

nodejs-github-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74307/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1874/

Benchmark results
 confidence improvement accuracy (*) (**) (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/creation.js kind='ReadableStream' n=50000 1.14 % ±5.00% ±6.66% ±8.67%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000 0.30 % ±1.51% ±2.01% ±2.63%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000 -0.38 % ±1.35% ±1.80% ±2.35%
webstreams/creation.js kind='TransformStream' n=50000 0.28 % ±1.21% ±1.60% ±2.09%
webstreams/creation.js kind='WritableStream' n=50000 -0.27 % ±1.62% ±2.16% ±2.81%
webstreams/js_transfer.js n=10000 payload='ReadableStream' 0.61 % ±0.62% ±0.82% ±1.07%
webstreams/js_transfer.js n=10000 payload='TransformStream' -0.11 % ±0.37% ±0.49% ±0.63%
webstreams/js_transfer.js n=10000 payload='WritableStream' -0.11 % ±0.48% ±0.64% ±0.83%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000 ** -1.79 % ±1.12% ±1.49% ±1.94%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000 ** -1.40 % ±0.86% ±1.14% ±1.49%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000 ** -1.64 % ±1.05% ±1.40% ±1.83%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000 * -1.61 % ±1.25% ±1.67% ±2.17%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000 ** -1.63 % ±1.14% ±1.51% ±1.97%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000 * -1.09 % ±1.04% ±1.39% ±1.81%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000 * -1.08 % ±0.90% ±1.19% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000 ** -1.40 % ±0.90% ±1.20% ±1.56%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000 * -1.42 % ±1.14% ±1.52% ±1.98%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000 * -1.36 % ±1.03% ±1.38% ±1.79%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000 * -1.56 % ±1.25% ±1.66% ±2.16%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000 ** -1.69 % ±1.12% ±1.49% ±1.94%
webstreams/readable-async-iterator.js n=100000 -0.79 % ±1.80% ±2.40% ±3.12%
webstreams/readable-read-buffered.js bufferSize=1 n=100000 -0.92 % ±1.96% ±2.61% ±3.41%
webstreams/readable-read-buffered.js bufferSize=10 n=100000 -2.71 % ±3.74% ±4.97% ±6.47%
webstreams/readable-read-buffered.js bufferSize=100 n=100000 -0.61 % ±2.69% ±3.58% ±4.67%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000 1.90 % ±3.10% ±4.13% ±5.37%
webstreams/readable-read.js type='byob' n=100000 -0.98 % ±1.03% ±1.37% ±1.78%
webstreams/readable-read.js type='normal' n=100000 -0.16 % ±1.49% ±1.99% ±2.59%
Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
1.60 false positives, when considering a 5% risk acceptance (*, **, ***),
0.32 false positives, when considering a 1% risk acceptance (**, ***),
0.03 false positives, when considering a 0.1% risk acceptance (***)

@aduh95

Copy link
Copy Markdown
Contributor

Benchmark CI is showing perf regressions 🤔

webstreams/creation.js kind='ReadableStream.tee' n=50000 *** 0.61 % ±0.32% ±0.42% ±0.55%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000 *** -2.32 % ±1.05% ±1.40% ±1.82%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000 *** -1.77 % ±0.80% ±1.06% ±1.38%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000 *** -2.19 % ±1.16% ±1.55% ±2.02%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000 *** -2.65 % ±1.25% ±1.67% ±2.18%

aduh95 pushed a commit that referenced this pull request Jun 20, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@mcollina

Copy link
Copy Markdown
MemberAuthor

Very weird. It's not the first time that locally I get consistent/reproducible perf improvements that are not materialized in the benchmark CI.

@mcollina

Copy link
Copy Markdown
MemberAuthor

What makes it even more interesting is that these changes should not be affecting pipe-to.

@aduh95

Copy link
Copy Markdown
Contributor

Interestingly, https://github.com/aduh95/node/actions/runs/27939962137 seems to indicate that those perf regression are x64 Linux specific – that being said, it wasn't able to replicate any non-negligible perf improvement on other platforms :/

aduh95 pushed a commit to aduh95/node that referenced this pull request Jul 30, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#63973
Refs: nodejs#63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request Aug 6, 2026
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: #63973
Backport-PR-URL: #64675
Refs: #63570
Refs: nodejs/undici#5002
Reviewed-By: Jithil P Ponnan <jithil@outlook.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for freeto 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 / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mcollina@nodejs-github-bot@aduh95@jasnell@addaleax@anonrig@Renegade334