stream: avoid per-write allocations in stream/iter writers

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 <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:46 +00:00
1 parent 778306c001
commit 724190cfdc
2 files changed
+16 -27

No files matched your search

+11 -12
View File
@@ -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));
}
/**
+5 -15
View File
@@ -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(