Skip to content

Offline rendering is much slower when ended handlers disconnect source graphs #189

Description

@yoniaiz

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions