Commit 13d0167013f for nodejs
commit 13d0167013fa64d256c016fb918a3d300de102d0
Author: James M Snell <jasnell@gmail.com>
Date: Sun Oct 4 02:28:24 2026 +0000
stream: construct stream/iter wait and merge records
The records queued when a stream/iter read, write or drain has to wait,
and merge()'s ready-queue entries, were `{ __proto__: null, ... }`
literals, which V8 creates in dictionary mode. They can be created once
per chunk: whenever the consumer is ahead of the producer, every read
waits, and with a full budget every write does.
Create them with constructors whose prototype is an empty, frozen,
null-prototype object: PendingRequest and PendingWrite (push(),
broadcast() and fromWritable()), QueuedWrite (fromWritable()) and
MergeEntry (merge()). fromWritable() drain waiters now settle through
resolve(false) instead of a per-waiter close() closure. merge() tells
error entries apart by their missing iterator rather than by a kind
string.
When every push() read waits for data, or every write waits behind a
full budget, reading or writing 16-byte chunks is about 30% faster.
fromWritable() with queued writes is about 18% faster, and merge() of
two sources about 9% 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>
diff --git a/lib/internal/streams/iter/broadcast.js b/lib/internal/streams/iter/broadcast.js
index df2a701f20c..e395b5e84f2 100644
--- a/lib/internal/streams/iter/broadcast.js
+++ b/lib/internal/streams/iter/broadcast.js
@@ -58,6 +58,8 @@ const {
const {
IterResult,
+ PendingRequest,
+ PendingWrite,
kMultiConsumerDefaultBudget,
kResolvedPromise,
convertChunks,
@@ -264,7 +266,7 @@ class BroadcastImpl {
if (state.resolve) {
const { promise, resolve, reject } = PromiseWithResolvers();
ArrayPrototypePush(state.pending,
- { __proto__: null, resolve, reject });
+ new PendingRequest(resolve, reject));
return promise;
}
@@ -625,7 +627,7 @@ class BroadcastWriter {
if (canWrite === null) return null;
if (canWrite) return PromiseResolve(true);
const { promise, resolve, reject } = PromiseWithResolvers();
- ArrayPrototypePush(this.#pendingDrains, { __proto__: null, resolve, reject });
+ ArrayPrototypePush(this.#pendingDrains, new PendingRequest(resolve, reject));
return promise;
}
@@ -802,7 +804,7 @@ class BroadcastWriter {
*/
#createPendingWrite(batch, signal) {
const { promise, resolve, reject } = PromiseWithResolvers();
- const entry = { __proto__: null, batch, resolve, reject };
+ const entry = new PendingWrite(batch, resolve, reject);
this.#pendingWrites.push(entry);
if (signal) {
wireBroadcastWriteSignal(entry, signal, resolve, reject, this);
diff --git a/lib/internal/streams/iter/classic.js b/lib/internal/streams/iter/classic.js
index 2c1fa640714..2a7b818efd9 100644
--- a/lib/internal/streams/iter/classic.js
+++ b/lib/internal/streams/iter/classic.js
@@ -14,6 +14,7 @@
const {
ArrayPrototypePush,
FunctionPrototypeCall,
+ ObjectFreeze,
Promise,
PromisePrototypeThen,
PromiseReject,
@@ -61,6 +62,7 @@ const {
} = require('internal/streams/iter/types');
const {
+ PendingRequest,
convertChunks,
getWriterSignal,
onSignalAbort,
@@ -522,6 +524,18 @@ function toReadableSync(source, options = kNullPrototype) {
// Cache: one Writer adapter per Writable instance.
const fromWritableCache = new SafeWeakMap();
+// A write queued by fromWritable() until the writable can accept it. Like
+// the other per-write records, constructed rather than created as a
+// `{ __proto__: null, ... }` literal (a dictionary-mode object).
+function QueuedWrite(chunks, resolve, reject) {
+ this.chunks = chunks;
+ this.resolve = resolve;
+ this.reject = reject;
+ this.signal = undefined;
+ this.onAbort = undefined;
+}
+QueuedWrite.prototype = ObjectFreeze({ __proto__: null });
+
/**
* Create a stream/iter Writer adapter from a classic Writable (or duck-type).
*
@@ -643,7 +657,7 @@ function fromWritable(writable, options = kNullPrototype) {
if (preserveReason) {
pending[i].reject(reason);
} else {
- pending[i].close();
+ pending[i].resolve(false);
}
}
@@ -755,14 +769,7 @@ function fromWritable(writable, options = kNullPrototype) {
function queueWrite(chunks, signal) {
const { promise, resolve, reject } = PromiseWithResolvers();
- const entry = {
- __proto__: null,
- chunks,
- resolve,
- reject,
- signal: undefined,
- onAbort: undefined,
- };
+ const entry = new QueuedWrite(chunks, resolve, reject);
pendingWrites.push(entry);
installDrainListener();
@@ -1038,12 +1045,7 @@ function fromWritable(writable, options = kNullPrototype) {
return PromiseResolve(true);
}
const { promise, resolve, reject } = PromiseWithResolvers();
- ArrayPrototypePush(drainWaiters, {
- __proto__: null,
- resolve,
- reject,
- close() { resolve(false); },
- });
+ ArrayPrototypePush(drainWaiters, new PendingRequest(resolve, reject));
installDrainListener();
return promise;
};
diff --git a/lib/internal/streams/iter/consumers.js b/lib/internal/streams/iter/consumers.js
index a18794aa732..8678e8235ac 100644
--- a/lib/internal/streams/iter/consumers.js
+++ b/lib/internal/streams/iter/consumers.js
@@ -17,6 +17,7 @@ const {
ArrayPrototypeShift,
ArrayPrototypeSlice,
FunctionPrototypeCall,
+ ObjectFreeze,
Promise,
PromisePrototypeThen,
SafePromiseAllReturnVoid,
@@ -456,6 +457,16 @@ function ondrain(drainable) {
const kNoMergeError = Symbol('kNoMergeError');
+// An entry in merge()'s ready queue: a value from `iterator`, or, for a
+// source that failed, `reason` with no iterator. Created for every merged
+// chunk, so constructed rather than created as a dictionary-mode literal.
+function MergeEntry(iterator, value, reason) {
+ this.iterator = iterator;
+ this.value = value;
+ this.reason = reason;
+}
+MergeEntry.prototype = ObjectFreeze({ __proto__: null });
+
/**
* Merge multiple async iterables by yielding values in temporal order.
* @param {...(AsyncIterable<Uint8Array[]>|object)} args
@@ -531,12 +542,8 @@ function merge(...args) {
if (result.done) {
activeCount--;
} else {
- ArrayPrototypePush(ready, {
- __proto__: null,
- kind: 'value',
- iterator,
- value: result.value,
- });
+ ArrayPrototypePush(ready,
+ new MergeEntry(iterator, result.value, undefined));
}
if (waitResolve) {
waitResolve();
@@ -547,11 +554,7 @@ function merge(...args) {
const onRejected = (iterator, reason) => {
pendingPulls.delete(iterator);
if (stopped) return;
- ArrayPrototypePush(ready, {
- __proto__: null,
- kind: 'error',
- reason,
- });
+ ArrayPrototypePush(ready, new MergeEntry(undefined, undefined, reason));
if (waitResolve) {
waitResolve();
waitResolve = null;
@@ -579,7 +582,7 @@ function merge(...args) {
// Drain ready queue synchronously
while (ready.length > 0) {
const item = ArrayPrototypeShift(ready);
- if (item.kind === 'error') {
+ if (item.iterator === undefined) {
throw item.reason;
}
yield item.value;
diff --git a/lib/internal/streams/iter/push.js b/lib/internal/streams/iter/push.js
index d15f5752fa6..b5db45a1a32 100644
--- a/lib/internal/streams/iter/push.js
+++ b/lib/internal/streams/iter/push.js
@@ -30,6 +30,8 @@ const {
const {
IterResult,
+ PendingRequest,
+ PendingWrite,
kPushDefaultBudget,
kResolvedPromise,
createBatchEntry,
@@ -294,7 +296,7 @@ class PushQueue {
*/
#createPendingWrite(batch, signal) {
const { promise, resolve, reject } = PromiseWithResolvers();
- const entry = { __proto__: null, batch, resolve, reject };
+ const entry = new PendingWrite(batch, resolve, reject);
this.#pendingWrites.push(entry);
if (signal) {
@@ -429,7 +431,7 @@ class PushQueue {
*/
waitForDrain() {
const { promise, resolve, reject } = PromiseWithResolvers();
- ArrayPrototypePush(this.#pendingDrains, { __proto__: null, resolve, reject });
+ ArrayPrototypePush(this.#pendingDrains, new PendingRequest(resolve, reject));
return promise;
}
@@ -467,7 +469,7 @@ class PushQueue {
}
const { promise, resolve, reject } = PromiseWithResolvers();
- this.#pendingReads.push({ __proto__: null, resolve, reject });
+ this.#pendingReads.push(new PendingRequest(resolve, reject));
return promise;
}
diff --git a/lib/internal/streams/iter/utils.js b/lib/internal/streams/iter/utils.js
index 915e5340ceb..8b0c074b4e1 100644
--- a/lib/internal/streams/iter/utils.js
+++ b/lib/internal/streams/iter/utils.js
@@ -241,6 +241,21 @@ function BatchEntry(views, byteLength) {
}
BatchEntry.prototype = ObjectFreeze({ __proto__: null });
+// Waiters for reads, writes and drains are queued whenever a stream has
+// to wait, which can be once per chunk.
+function PendingRequest(resolve, reject) {
+ this.resolve = resolve;
+ this.reject = reject;
+}
+PendingRequest.prototype = ObjectFreeze({ __proto__: null });
+
+function PendingWrite(batch, resolve, reject) {
+ this.batch = batch;
+ this.resolve = resolve;
+ this.reject = reject;
+}
+PendingWrite.prototype = ObjectFreeze({ __proto__: null });
+
function snapshotByteView(value) {
const buffer = TypedArrayPrototypeGetBuffer(value);
const sharedBufferView = isSharedArrayBuffer(buffer) ?
@@ -576,6 +591,8 @@ function validateBackpressure(value) {
module.exports = {
IterResult,
+ PendingRequest,
+ PendingWrite,
kMultiConsumerDefaultBudget,
kPushDefaultBudget,
kResolvedPromise,