Summary
During OfflineAudioContext.startRendering(), an AudioBufferSourceNode.onended handler that disconnects the finished source graph can make rendering much slower than leaving the graph connected.
This may be related to #168, but the symptom here is render-time performance rather than retained memory.
Environment
node-web-audio-api: 2.0.0
- Node.js: v24.13.1
- OS: macOS arm64 (
Darwin 25.3.0)
Reproduction
import { AudioBufferSourceNode, GainNode, OfflineAudioContext } from 'node-web-audio-api';
import { performance } from 'node:perf_hooks';
const sampleRate = 44_100;
const durationSeconds = 60;
const eventCount = 2_000;
async function bench(mode) {
const ctx = new OfflineAudioContext(1, sampleRate * durationSeconds, sampleRate);
const buffer = ctx.createBuffer(1, Math.round(sampleRate * 0.01), sampleRate);
buffer.getChannelData(0)[0] = 1;
let endedCount = 0;
const activeSources = new Set();
for (let i = 0; i < eventCount; i += 1) {
const source = new AudioBufferSourceNode(ctx, { buffer });
const gain = new GainNode(ctx, { gain: 1 });
source.connect(gain);
gain.connect(ctx.destination);
if (mode === 'onended-disconnect') {
activeSources.add(source);
source.onended = () => {
endedCount += 1;
activeSources.delete(source);
source.disconnect();
gain.disconnect();
};
} else if (mode === 'onended-noop') {
source.onended = () => {
endedCount += 1;
};
}
source.start((i / eventCount) * durationSeconds);
}
const t0 = performance.now();
await ctx.startRendering();
const ms = performance.now() - t0;
console.log(`${mode}: ${Math.round(ms)}ms ended=${endedCount} active=${activeSources.size}`);
}
await bench('none');
await bench('onended-noop');
await bench('onended-disconnect');
Result on my machine
none: 6439ms ended=0 active=0
onended-noop: 6630ms ended=2000 active=0
onended-disconnect: 54124ms ended=2000 active=0
A larger 120s / 4,000-source version rendered the no-cleanup and no-op-handler cases in about 38-39s, but the disconnecting onended case was still running after more than two minutes, so I aborted it.
Expected
I expected disconnecting finished source subgraphs from their ended handlers during an offline render to be roughly comparable to leaving them connected, or at least not ~8x slower for this case.
In an application render path with many short sample hits, avoiding offline onended cleanup changed the native offline render from roughly 90s to 32s for the same audio. The workaround is fine for one-shot offline render processes, but a fix here would allow spec-shaped cleanup/pruning strategies without that render-time cliff.
Summary
During
OfflineAudioContext.startRendering(), anAudioBufferSourceNode.onendedhandler that disconnects the finished source graph can make rendering much slower than leaving the graph connected.This may be related to #168, but the symptom here is render-time performance rather than retained memory.
Environment
node-web-audio-api: 2.0.0Darwin 25.3.0)Reproduction
Result on my machine
A larger 120s / 4,000-source version rendered the no-cleanup and no-op-handler cases in about 38-39s, but the disconnecting
onendedcase was still running after more than two minutes, so I aborted it.Expected
I expected disconnecting finished source subgraphs from their ended handlers during an offline render to be roughly comparable to leaving them connected, or at least not ~8x slower for this case.
In an application render path with many short sample hits, avoiding offline
onendedcleanup changed the native offline render from roughly 90s to 32s for the same audio. The workaround is fine for one-shot offline render processes, but a fix here would allow spec-shaped cleanup/pruning strategies without that render-time cliff.