stream: construct stream/iter byte view snapshots and batch entries

createBatchEntry() and recordChunk() snapshot every chunk in a
seven-property `{ __proto__: null, ... }` literal, and every batch gets
a `{ __proto__: null, views, byteLength }` record. V8 creates such
literals in dictionary mode, which is expensive for objects created
for every chunk.

Create them with constructors whose prototype is an empty, frozen,
null-prototype object instead, as for IterResult. They have fast
properties and a single shape, and are only used internally.

Writing 1e6 16-byte chunks into a push() stream and reading them back
is about 6x faster with writeSync(), 3.7x faster with writevSync() (4
chunks per call) and 1.7x faster with string chunks.
benchmark/streams/iter-throughput-share-sync.js improves by 67-93%,
iter-throughput-share.js by 4-8%, and iter-throughput-broadcast.js
with 4 consumers by 8%. pipeTo() with small chunks is about 1.5x
faster.

Assisted-by: OpenCode
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66648
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
This commit is contained in:
James M Snell authored and Node.js GitHub Bot committed 2026-10-15 13:26:47 +00:00
1 parent 724190cfdc
commit e6c9c8eecd
1 file changed
+28 -17
+28 -17
View File
@@ -217,23 +217,35 @@ function toUint8Array(chunk) {
return chunk;
}
// Byte view snapshots and batch entries are created for every chunk and
// every batch, so like IterResult they are constructed rather than created
// as `{ __proto__: null, ... }` literals (which are dictionary-mode objects),
// and their prototype is an empty null-prototype object.
function ByteViewSnapshot(value, buffer, sharedBufferView) {
this.value = value;
this.buffer = buffer;
this.bufferByteLength = sharedBufferView === undefined ?
ArrayBufferPrototypeGetByteLength(buffer) :
TypedArrayPrototypeGetByteLength(sharedBufferView);
this.byteLength = TypedArrayPrototypeGetByteLength(value);
this.byteOffset = TypedArrayPrototypeGetByteOffset(value);
this.detached = sharedBufferView === undefined &&
ArrayBufferPrototypeGetDetached(buffer);
this.sharedBufferView = sharedBufferView;
}
ByteViewSnapshot.prototype = ObjectFreeze({ __proto__: null });
function BatchEntry(views, byteLength) {
this.views = views;
this.byteLength = byteLength;
}
BatchEntry.prototype = ObjectFreeze({ __proto__: null });
function snapshotByteView(value) {
const buffer = TypedArrayPrototypeGetBuffer(value);
const sharedBufferView = isSharedArrayBuffer(buffer) ?
new Uint8Array(buffer) : undefined;
return {
__proto__: null,
value,
buffer,
bufferByteLength: sharedBufferView === undefined ?
ArrayBufferPrototypeGetByteLength(buffer) :
TypedArrayPrototypeGetByteLength(sharedBufferView),
byteLength: TypedArrayPrototypeGetByteLength(value),
byteOffset: TypedArrayPrototypeGetByteOffset(value),
detached: sharedBufferView === undefined &&
ArrayBufferPrototypeGetDetached(buffer),
sharedBufferView,
};
return new ByteViewSnapshot(value, buffer, sharedBufferView);
}
function validateByteView(snapshot) {
@@ -325,7 +337,7 @@ function createBatchEntry(chunks) {
views[i] = view;
byteLength += view.byteLength;
}
return { __proto__: null, views, byteLength };
return new BatchEntry(views, byteLength);
}
/**
@@ -345,15 +357,14 @@ function splitBatchEntry(entry, limit) {
for (let i = 0; i < views.length; i++) {
const view = views[i];
if (current.length > 0 && byteLength + view.byteLength >= limit) {
ArrayPrototypePush(entries,
{ __proto__: null, views: current, byteLength });
ArrayPrototypePush(entries, new BatchEntry(current, byteLength));
current = [];
byteLength = 0;
}
ArrayPrototypePush(current, view);
byteLength += view.byteLength;
}
ArrayPrototypePush(entries, { __proto__: null, views: current, byteLength });
ArrayPrototypePush(entries, new BatchEntry(current, byteLength));
return entries;
}