Web Audio API Memory Leak

Viewed 1579

How should a web-audio-api AudioNode be cleaned up so that its memory is released? I am calling oscillatorNode.stop() and oscillatorNode.disconnect() based on this post, but it doesn't seem to help, and I end up with memory leaks. This post doesn't apply because I'm removing the references as soon as I stop the oscillatorNode.

I created a sample website that shows the issue. Here are the steps to reproduce.

  1. Create a local html file and run the below code snippet in chrome on a desktop or laptop with dev tools open.
  2. Create a heap snapshot.
  3. Click the "Go" button.
  4. Periodically create another heap snapshot.
  5. Notice that the memory keeps rising, even after running the garbage collector. Why?

enter image description here

<html>
<body>
    <button onclick="go()">Go</button>
    <button onclick="cancel=true">Cancel</button>
    <div id="status"></div>

    <script>
        var cancel = false;
        var statusEl = document.getElementById('status');

        async function go() {
            cancel = false;
            for (var i = 0; i < 100000; i++) {
                if (cancel) {
                    return;
                }
                statusEl.innerHTML = i;
                play();
                await new Promise((resolve) => { setTimeout(resolve, 1); });
                stop();
            }
        }

        var ctx = new AudioContext();
        var data = {
            oscillatorNode: null
        };

        function play() {
            if (!data.oscillatorNode) {
                // create an oscillator
                data.oscillatorNode = ctx.createOscillator();
                data.oscillatorNode.frequency.value = 220.0;
                data.oscillatorNode.connect(ctx.destination);
                data.oscillatorNode.start(ctx.currentTime);
            }
        }

        function stop() {
            if (data.oscillatorNode) {
                data.oscillatorNode.stop();
                data.oscillatorNode.disconnect();
                delete data.oscillatorNode;
            }
        } 
    </script>
</body>

</html>
2 Answers

According to this post, "When the oscillator stops it should automatically disconnect itself from any downstream nodes". However, due to this bug in chrome (thanks James Lawruk for finding this), it doesn't actually clean itself up.

This comment from that bug report mentions

The issue is that because disconnect() is called right after stop(), the oscillator is disconnected from the destination, so any processing associated with stop() is never done. This also includes not actually stopping the oscillator because it takes at least one render quantum to do that. Since it was disconnected, that render quantum never happens.

So with that in mind, I attached to the oscillatorNode.onended event and call disconnect in that callback, and no more memory leaks!

Here's the code:

function stop() {
    return new Promise((resolve) => {
        //whenever the node actually finishes playing, disconnect it
        data.oscillatorNode.onended = function () {
            data.oscillatorNode.disconnect();
            delete data.oscillatorNode;
            resolve();
        }
        //stop the oscillator
        data.oscillatorNode.stop();
    });
} 

And the heap snapshots: enter image description here

The comment in the 717528 bug says:

The issue is that because disconnect() is called right after stop(), the oscillator is disconnected from the destination, so any processing associated with stop() is never done. This also includes not actually stopping the oscillator because it takes at least one render quantum to do that. Since it was disconnected, that render quantum never happens.

So if you add a delay before calling disconnect(), it should stay at a consistent memory level.

function stop() {
    if (data.oscillatorNode) {
        data.oscillatorNode.stop();
        var oscillatorNode = data.oscillatorNode;
        setTimeout(function() { 
           oscillatorNode.disconnect(); 
           delete oscillatorNode;
        }, 100);    
    }
} 
Related