mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
Blob.prototype.stream() registered a wakeup callback on the underlying source's start() and never released it. The strong Reader::wakeup_ handle kept the reader -- and through it the blob's DataQueue and backing store -- reachable as a GC root, so the source buffer leaked on every stream() call. On Node 26+, streaming a 1 MiB blob 300 times retained ~300 MiB in process.memoryUsage().arrayBuffers while the V8 heap stayed small. Register the wakeup lazily in pull() and clear it on every terminal or idle path (EOS, error, cancel, backpressure), mirroring the cleanup already done by the async iterator path. The strong handle now only lives while a pull is in flight, so the reader and its backing store become collectable once the stream finishes, errors, is cancelled, or goes idle under backpressure. Fixes: https://github.com/nodejs/node/issues/63574 Signed-off-by: semimikoh <ejffjeosms@gmail.com> PR-URL: https://github.com/nodejs/node/pull/63577 Fixes: https://github.com/nodejs/node/issues/63574 Reviewed-By: James M Snell <jasnell@gmail.com>
57 lines
1.8 KiB
JavaScript
57 lines
1.8 KiB
JavaScript
// Flags: --expose-gc --no-concurrent-array-buffer-sweeping
|
|
'use strict';
|
|
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const { setImmediate: setImmediatePromise } = require('timers/promises');
|
|
|
|
const MiB = 1024 * 1024;
|
|
const iterations = 64;
|
|
const maxRetained = 16 * MiB;
|
|
|
|
async function collectArrayBuffers() {
|
|
for (let i = 0; i < 3; i++) {
|
|
global.gc();
|
|
await setImmediatePromise();
|
|
}
|
|
}
|
|
|
|
async function assertNoBlobStreamRetention(name, fn) {
|
|
const buffer = Buffer.alloc(MiB);
|
|
|
|
await collectArrayBuffers();
|
|
const before = process.memoryUsage().arrayBuffers;
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
await fn(buffer);
|
|
}
|
|
|
|
await collectArrayBuffers();
|
|
const retained = process.memoryUsage().arrayBuffers - before;
|
|
|
|
assert(
|
|
retained < maxRetained,
|
|
`${name} retained ${retained} bytes in arrayBuffers`,
|
|
);
|
|
}
|
|
|
|
(async () => {
|
|
await assertNoBlobStreamRetention('unused Blob streams',
|
|
common.mustCall(async (buffer) => {
|
|
new Blob([buffer]).stream();
|
|
}, iterations));
|
|
|
|
await assertNoBlobStreamRetention('cancelled Blob streams',
|
|
common.mustCall(async (buffer) => {
|
|
await new Blob([buffer]).stream()
|
|
.cancel();
|
|
}, iterations));
|
|
|
|
await assertNoBlobStreamRetention('drained Blob streams',
|
|
common.mustCall(async (buffer) => {
|
|
await new Response(
|
|
new Blob([buffer]).stream(),
|
|
).arrayBuffer();
|
|
}, iterations));
|
|
})().then(common.mustCall());
|