From 724190cfdccd3a4d2e2dca5b0c5de188d04e39db Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 4 Oct 2026 02:13:10 +0000 Subject: [PATCH] stream: avoid per-write allocations in stream/iter writers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every write() and writeSync() allocated a `{ __proto__: null, context }` options object for the WebIDL chunk conversion, and for a Uint8Array chunk a second, six-property one with [AllowShared] and [AllowResizable]. Both are created in V8 dictionary mode. Return Uint8Array chunks directly from the WriterChunk converter: with [AllowShared] and [AllowResizable], the Uint8Array conversion cannot reject a value isUint8Array() accepts and returns the same object. Use shared, frozen conversion contexts for chunks, chunk sequences and write options; the converters only read them to build error messages. Writing 1e6 16-byte Uint8Array chunks into a push() stream and reading them back is about 27% faster with writeSync() and with writevSync() (4 chunks per call). String chunks are unaffected. Behavior and error messages are unchanged. 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 | 23 +++++++++++------------ lib/internal/streams/iter/webidl.js | 20 +++++--------------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/lib/internal/streams/iter/utils.js b/lib/internal/streams/iter/utils.js index 0c6e1daaa32..d50ec639ab7 100644 --- a/lib/internal/streams/iter/utils.js +++ b/lib/internal/streams/iter/utils.js @@ -412,6 +412,14 @@ function concatBytes(chunks) { return concatenated; } +// Conversion contexts for the per-write paths. The converters only read +// them (to build error messages), so they are shared rather than allocated +// on every write. +const kChunkContext = ObjectFreeze({ __proto__: null, context: 'chunk' }); +const kChunksContext = ObjectFreeze({ __proto__: null, context: 'chunks' }); +const kWriteOptionsContext = + ObjectFreeze({ __proto__: null, context: 'options' }); + /** * Convert an array of chunks (strings or Uint8Arrays) to a Uint8Array[]. * Always returns a fresh copy of the array. @@ -419,10 +427,7 @@ function concatBytes(chunks) { * @returns {Uint8Array[]} */ function convertChunks(chunks) { - chunks = converters.WriterChunkSequence(chunks, { - __proto__: null, - context: 'chunks', - }); + chunks = converters.WriterChunkSequence(chunks, kChunksContext); const len = chunks.length; const result = new Array(len); for (let i = 0; i < len; i++) { @@ -437,17 +442,11 @@ function convertChunks(chunks) { * @returns {AbortSignal|undefined} */ function getWriterSignal(options) { - return converters.WriteOptions(options, { - __proto__: null, - context: 'options', - }).signal; + return converters.WriteOptions(options, kWriteOptionsContext).signal; } function toWriterUint8Array(chunk) { - return toUint8Array(converters.WriterChunk(chunk, { - __proto__: null, - context: 'chunk', - })); + return toUint8Array(converters.WriterChunk(chunk, kChunkContext)); } /** diff --git a/lib/internal/streams/iter/webidl.js b/lib/internal/streams/iter/webidl.js index ef61abb7794..6ee1afd622a 100644 --- a/lib/internal/streams/iter/webidl.js +++ b/lib/internal/streams/iter/webidl.js @@ -27,17 +27,6 @@ function enforceRangeUnsignedLongLong(value, options = { __proto__: null }) { }); } -function allowStreamBufferOptions(options) { - return { - __proto__: null, - prefix: options.prefix, - context: options.context, - code: options.code, - allowShared: true, - allowResizable: true, - }; -} - converters.AbortSignal = baseConverters.AbortSignal; converters.BackpressurePolicy = createEnumConverter('BackpressurePolicy', [ 'strict', @@ -48,10 +37,11 @@ converters.BackpressurePolicy = createEnumConverter('BackpressurePolicy', [ converters.unsignedLongLong = unsignedLongLong; converters.enforceRangeUnsignedLongLong = enforceRangeUnsignedLongLong; converters.WriterChunk = (value, options = { __proto__: null }) => { - if (isUint8Array(value)) { - return baseConverters.Uint8Array( - value, allowStreamBufferOptions(options)); - } + // The chunk is a Uint8Array with [AllowShared] and [AllowResizable]. The + // Uint8Array conversion cannot reject a value isUint8Array() accepts when + // both are allowed, and returns the same object, so skip it: writes are + // hot, and the conversion would need its own options object. + if (isUint8Array(value)) return value; return baseConverters.USVString(value, options); }; converters.WriterChunkSequence = createSequenceConverter(