From e6c9c8eecdb49cfa4994f9fcc9bb5a7ff3f8d2ea Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 4 Oct 2026 02:21:50 +0000 Subject: [PATCH] stream: construct stream/iter byte view snapshots and batch entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 PR-URL: https://github.com/nodejs/node/pull/66648 Reviewed-By: Trivikram Kamat Reviewed-By: Gürgün Dayıoğlu --- lib/internal/streams/iter/utils.js | 45 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/lib/internal/streams/iter/utils.js b/lib/internal/streams/iter/utils.js index d50ec639ab7..915e5340ceb 100644 --- a/lib/internal/streams/iter/utils.js +++ b/lib/internal/streams/iter/utils.js @@ -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; }