Skip to content

Commit 13d0167

Browse files
jasnellnodejs-github-bot
authored andcommitted
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: #66648 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent e6c9c8e commit 13d0167

5 files changed

Lines changed: 59 additions & 33 deletions

File tree

‎lib/internal/streams/iter/broadcast.js‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const {
5858

5959
const {
6060
IterResult,
61+
PendingRequest,
62+
PendingWrite,
6163
kMultiConsumerDefaultBudget,
6264
kResolvedPromise,
6365
convertChunks,
@@ -264,7 +266,7 @@ class BroadcastImpl {
264266
if (state.resolve) {
265267
const { promise, resolve, reject } = PromiseWithResolvers();
266268
ArrayPrototypePush(state.pending,
267-
{ __proto__: null, resolve, reject });
269+
new PendingRequest(resolve, reject));
268270
return promise;
269271
}
270272

@@ -625,7 +627,7 @@ class BroadcastWriter {
625627
if (canWrite === null) return null;
626628
if (canWrite) return PromiseResolve(true);
627629
const { promise, resolve, reject } = PromiseWithResolvers();
628-
ArrayPrototypePush(this.#pendingDrains, { __proto__: null, resolve, reject });
630+
ArrayPrototypePush(this.#pendingDrains, new PendingRequest(resolve, reject));
629631
return promise;
630632
}
631633

@@ -802,7 +804,7 @@ class BroadcastWriter {
802804
*/
803805
#createPendingWrite(batch, signal) {
804806
const { promise, resolve, reject } = PromiseWithResolvers();
805-
const entry = { __proto__: null, batch, resolve, reject };
807+
const entry = new PendingWrite(batch, resolve, reject);
806808
this.#pendingWrites.push(entry);
807809
if (signal) {
808810
wireBroadcastWriteSignal(entry, signal, resolve, reject, this);

‎lib/internal/streams/iter/classic.js‎

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
const {
1515
ArrayPrototypePush,
1616
FunctionPrototypeCall,
17+
ObjectFreeze,
1718
Promise,
1819
PromisePrototypeThen,
1920
PromiseReject,
@@ -61,6 +62,7 @@ const {
6162
} = require('internal/streams/iter/types');
6263

6364
const {
65+
PendingRequest,
6466
convertChunks,
6567
getWriterSignal,
6668
onSignalAbort,
@@ -522,6 +524,18 @@ function toReadableSync(source, options = kNullPrototype) {
522524
// Cache: one Writer adapter per Writable instance.
523525
const fromWritableCache = new SafeWeakMap();
524526

527+
// A write queued by fromWritable() until the writable can accept it. Like
528+
// the other per-write records, constructed rather than created as a
529+
// `{ __proto__: null, ... }` literal (a dictionary-mode object).
530+
function QueuedWrite(chunks, resolve, reject) {
531+
this.chunks = chunks;
532+
this.resolve = resolve;
533+
this.reject = reject;
534+
this.signal = undefined;
535+
this.onAbort = undefined;
536+
}
537+
QueuedWrite.prototype = ObjectFreeze({ __proto__: null });
538+
525539
/**
526540
* Create a stream/iter Writer adapter from a classic Writable (or duck-type).
527541
*
@@ -643,7 +657,7 @@ function fromWritable(writable, options = kNullPrototype) {
643657
if (preserveReason) {
644658
pending[i].reject(reason);
645659
} else {
646-
pending[i].close();
660+
pending[i].resolve(false);
647661
}
648662
}
649663

@@ -755,14 +769,7 @@ function fromWritable(writable, options = kNullPrototype) {
755769

756770
function queueWrite(chunks, signal) {
757771
const { promise, resolve, reject } = PromiseWithResolvers();
758-
const entry = {
759-
__proto__: null,
760-
chunks,
761-
resolve,
762-
reject,
763-
signal: undefined,
764-
onAbort: undefined,
765-
};
772+
const entry = new QueuedWrite(chunks, resolve, reject);
766773
pendingWrites.push(entry);
767774
installDrainListener();
768775

@@ -1038,12 +1045,7 @@ function fromWritable(writable, options = kNullPrototype) {
10381045
return PromiseResolve(true);
10391046
}
10401047
const { promise, resolve, reject } = PromiseWithResolvers();
1041-
ArrayPrototypePush(drainWaiters, {
1042-
__proto__: null,
1043-
resolve,
1044-
reject,
1045-
close() { resolve(false); },
1046-
});
1048+
ArrayPrototypePush(drainWaiters, new PendingRequest(resolve, reject));
10471049
installDrainListener();
10481050
return promise;
10491051
};

‎lib/internal/streams/iter/consumers.js‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
ArrayPrototypeShift,
1818
ArrayPrototypeSlice,
1919
FunctionPrototypeCall,
20+
ObjectFreeze,
2021
Promise,
2122
PromisePrototypeThen,
2223
SafePromiseAllReturnVoid,
@@ -456,6 +457,16 @@ function ondrain(drainable) {
456457

457458
const kNoMergeError = Symbol('kNoMergeError');
458459

460+
// An entry in merge()'s ready queue: a value from `iterator`, or, for a
461+
// source that failed, `reason` with no iterator. Created for every merged
462+
// chunk, so constructed rather than created as a dictionary-mode literal.
463+
function MergeEntry(iterator, value, reason) {
464+
this.iterator = iterator;
465+
this.value = value;
466+
this.reason = reason;
467+
}
468+
MergeEntry.prototype = ObjectFreeze({ __proto__: null });
469+
459470
/**
460471
* Merge multiple async iterables by yielding values in temporal order.
461472
* @param {...(AsyncIterable<Uint8Array[]>|object)} args
@@ -531,12 +542,8 @@ function merge(...args) {
531542
if (result.done) {
532543
activeCount--;
533544
} else {
534-
ArrayPrototypePush(ready, {
535-
__proto__: null,
536-
kind: 'value',
537-
iterator,
538-
value: result.value,
539-
});
545+
ArrayPrototypePush(ready,
546+
new MergeEntry(iterator, result.value, undefined));
540547
}
541548
if (waitResolve) {
542549
waitResolve();
@@ -547,11 +554,7 @@ function merge(...args) {
547554
const onRejected = (iterator, reason) => {
548555
pendingPulls.delete(iterator);
549556
if (stopped) return;
550-
ArrayPrototypePush(ready, {
551-
__proto__: null,
552-
kind: 'error',
553-
reason,
554-
});
557+
ArrayPrototypePush(ready, new MergeEntry(undefined, undefined, reason));
555558
if (waitResolve) {
556559
waitResolve();
557560
waitResolve = null;
@@ -579,7 +582,7 @@ function merge(...args) {
579582
// Drain ready queue synchronously
580583
while (ready.length > 0) {
581584
const item = ArrayPrototypeShift(ready);
582-
if (item.kind === 'error') {
585+
if (item.iterator === undefined) {
583586
throw item.reason;
584587
}
585588
yield item.value;

‎lib/internal/streams/iter/push.js‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const {
3030

3131
const {
3232
IterResult,
33+
PendingRequest,
34+
PendingWrite,
3335
kPushDefaultBudget,
3436
kResolvedPromise,
3537
createBatchEntry,
@@ -294,7 +296,7 @@ class PushQueue {
294296
*/
295297
#createPendingWrite(batch, signal) {
296298
const { promise, resolve, reject } = PromiseWithResolvers();
297-
const entry = { __proto__: null, batch, resolve, reject };
299+
const entry = new PendingWrite(batch, resolve, reject);
298300
this.#pendingWrites.push(entry);
299301

300302
if (signal) {
@@ -429,7 +431,7 @@ class PushQueue {
429431
*/
430432
waitForDrain() {
431433
const { promise, resolve, reject } = PromiseWithResolvers();
432-
ArrayPrototypePush(this.#pendingDrains, { __proto__: null, resolve, reject });
434+
ArrayPrototypePush(this.#pendingDrains, new PendingRequest(resolve, reject));
433435
return promise;
434436
}
435437

@@ -467,7 +469,7 @@ class PushQueue {
467469
}
468470

469471
const { promise, resolve, reject } = PromiseWithResolvers();
470-
this.#pendingReads.push({ __proto__: null, resolve, reject });
472+
this.#pendingReads.push(new PendingRequest(resolve, reject));
471473
return promise;
472474
}
473475

‎lib/internal/streams/iter/utils.js‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,21 @@ function BatchEntry(views, byteLength) {
241241
}
242242
BatchEntry.prototype = ObjectFreeze({ __proto__: null });
243243

244+
// Waiters for reads, writes and drains are queued whenever a stream has
245+
// to wait, which can be once per chunk.
246+
function PendingRequest(resolve, reject) {
247+
this.resolve = resolve;
248+
this.reject = reject;
249+
}
250+
PendingRequest.prototype = ObjectFreeze({ __proto__: null });
251+
252+
function PendingWrite(batch, resolve, reject) {
253+
this.batch = batch;
254+
this.resolve = resolve;
255+
this.reject = reject;
256+
}
257+
PendingWrite.prototype = ObjectFreeze({ __proto__: null });
258+
244259
function snapshotByteView(value) {
245260
const buffer = TypedArrayPrototypeGetBuffer(value);
246261
const sharedBufferView = isSharedArrayBuffer(buffer) ?
@@ -576,6 +591,8 @@ function validateBackpressure(value) {
576591

577592
module.exports = {
578593
IterResult,
594+
PendingRequest,
595+
PendingWrite,
579596
kMultiConsumerDefaultBudget,
580597
kPushDefaultBudget,
581598
kResolvedPromise,

0 commit comments

Comments
 (0)