Skip to content

Commit de2ced4

Browse files
jasnelladuh95
authored andcommitted
stream: make broadcast waiters use SafeSet
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode PR-URL: #66030 Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 1d0b6c6 commit de2ced4

2 files changed

Lines changed: 45 additions & 8 deletions

File tree

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class BroadcastImpl {
104104
#buffer = new RingBuffer();
105105
#bufferStart = 0;
106106
#consumers = new SafeSet();
107-
#waiters = []; // Consumers with pending resolve (subset of #consumers)
107+
#waiters = new SafeSet(); // Consumers with pending resolve
108108
#ended = false;
109109
#error;
110110
#cancelled = false;
@@ -190,6 +190,7 @@ class BroadcastImpl {
190190

191191
function detach() {
192192
state.detached = true;
193+
self.#waiters.delete(state);
193194
if (state.resolve) {
194195
state.resolve({ __proto__: null, done: true, value: undefined });
195196
}
@@ -247,7 +248,7 @@ class BroadcastImpl {
247248
const { promise, resolve, reject } = PromiseWithResolvers();
248249
state.resolve = resolve;
249250
state.reject = reject;
250-
ArrayPrototypePush(self.#waiters, state);
251+
self.#waiters.add(state);
251252
return promise;
252253
},
253254

@@ -295,6 +296,7 @@ class BroadcastImpl {
295296
consumer.detached = true;
296297
}
297298
this.#consumers.clear();
299+
this.#waiters.clear();
298300
this.#cachedMinCursorConsumers = 0;
299301
}
300302

@@ -373,6 +375,7 @@ class BroadcastImpl {
373375
}
374376
}
375377
}
378+
this.#waiters.clear();
376379
this.#notifyEndDrained();
377380
}
378381

@@ -392,6 +395,7 @@ class BroadcastImpl {
392395
consumer.detached = true;
393396
}
394397
this.#consumers.clear();
398+
this.#waiters.clear();
395399
this.#cachedMinCursorConsumers = 0;
396400
}
397401

@@ -457,12 +461,11 @@ class BroadcastImpl {
457461

458462
#notifyConsumers() {
459463
const waiters = this.#waiters;
460-
if (waiters.length === 0) return;
464+
if (waiters.size === 0) return;
461465
// Swap out the waiters list so consumers that re-wait during
462466
// resolve don't get processed twice in this cycle.
463-
this.#waiters = [];
464-
for (let i = 0; i < waiters.length; i++) {
465-
const consumer = waiters[i];
467+
this.#waiters = new SafeSet();
468+
for (const consumer of waiters) {
466469
if (consumer.resolve) {
467470
const bufferIndex = consumer.cursor - this.#bufferStart;
468471
if (bufferIndex < this.#buffer.length) {
@@ -480,11 +483,11 @@ class BroadcastImpl {
480483
if (consumer.detached && this.#deleteConsumer(consumer)) {
481484
this.#tryTrimBuffer();
482485
} else if (this.#promotePending(consumer)) {
483-
ArrayPrototypePush(this.#waiters, consumer);
486+
this.#waiters.add(consumer);
484487
}
485488
} else {
486489
// Still waiting -- put back
487-
ArrayPrototypePush(this.#waiters, consumer);
490+
this.#waiters.add(consumer);
488491
}
489492
}
490493
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Flags: --experimental-stream-iter --expose-gc
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const { broadcast } = require('stream/iter');
7+
8+
async function detachConsumers(shared, count) {
9+
for (let i = 0; i < count; i++) {
10+
const iterator = shared.push()[Symbol.asyncIterator]();
11+
const pending = iterator.next();
12+
await iterator.return();
13+
await pending;
14+
}
15+
}
16+
17+
async function testDetachedWaitersAreReleased() {
18+
const { broadcast: shared } = broadcast();
19+
20+
await detachConsumers(shared, 100);
21+
global.gc();
22+
const before = process.memoryUsage().heapUsed;
23+
24+
await detachConsumers(shared, 50_000);
25+
global.gc();
26+
const retained = process.memoryUsage().heapUsed - before;
27+
28+
assert.ok(retained < 8 * 1024 * 1024,
29+
`Detached Broadcast waiters retained ${retained} bytes`);
30+
assert.strictEqual(shared.consumerCount, 0);
31+
shared.cancel();
32+
}
33+
34+
testDetachedWaitersAreReleased().then(common.mustCall());

0 commit comments

Comments
 (0)