grame-cncm / faust2webaudio

faust2webaudio for npm
GNU General Public License v3.0
21 stars 2 forks source link

Creating ~50 nodes via .getNode() yields "RangeError: WebAssembly.Instance(): Out of memory: wasm memory" #20

Open dxinteractive opened 2 years ago

dxinteractive commented 2 years ago

I'm unsure if this is a usage problem by me, an underlying limitation or a bug (doubtful).

If I create ~50 AudioWorkletNodes via getNode(), I get the out of memory error mentioned in the title on latest Chrome on macOS.

image

Here is a reproduction based on test/mono.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <script src="../dist/index.min.js"></script>
    </head>
    <body style="position: absolute; width: 100%; height: 100%; margin: 0px">
    </body>
    <script>
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        const faust = new Faust2WebAudio.Faust({ debug: true, wasmLocation: "../dist/libfaust-wasm.wasm", dataLocation: "../dist/libfaust-wasm.data" });
        window.faust = faust;
        faust.ready
        .then(async () => {
            const code = `
import("stdfaust.lib");
process = ba.pulsen(1, 10000) : pm.djembe(60, 0.3, 0.4, 1) <: dm.freeverb_demo;`;

            const node = await faust.getNode(code, { audioCtx, useWorklet: true, args: { "-I": "libraries/" } });
            node.connect(audioCtx.destination);
            window.node = node;

            let i = 1;
            const another = async () => {
                console.log('five seconds have elapsed, make another', i++);
                const node = await faust.getNode(code, { audioCtx, useWorklet: true, args: { "-I": "libraries/" } });
                await new Promise(r => setTimeout(r, 5000));
                node.destroy();
                another();
            };
            another();
        })
        const unlockAudioContext = (audioCtx) => {
            if (audioCtx.state !== "suspended") return;
            const b = document.body;
            const events = ["touchstart", "touchend", "mousedown", "keydown"];
            const unlock = () => audioCtx.resume().then(clean);
            const clean = () => events.forEach(e => b.removeEventListener(e, unlock));
            events.forEach(e => b.addEventListener(e, unlock, false));
        }
        unlockAudioContext(audioCtx);
    </script>
</html>

Normally I wouldn't want 50 nodes connected at the same time, but I would like to be able to compile 50 nodes over the course of half an hour without having to refresh the page. I'm calling node.destroy(); but perhaps this isn't enough to clean up the memory used by each new AudioWorklet. In this example I'm not connecting the new nodes to the audio graph so there should be no hanging references on the js side (honestly I'm not sure how WebAssembly memory allocation and js's regular garbage colllection interact, if at all). Is there anything I can do to overcome this limitation?

dxinteractive commented 2 years ago

Further research makes it seems like this might be just how memory allocation behaves with WebAssembly. I don't know enough to say definitively though. I can make Faust IDE stop working with the same error by compiling a patch ~50 times, or compiling ~50 different patches.

dxinteractive commented 2 years ago

Silly hack that circumvents the issue: put all the faust audio compilation stuff in a newly-created iframe, and remove the iframe and recreate the iframe after every compile. When the iframe is gone, it seems the resources attached to that iframe are gone too, or at the very least you get a new memory pool to use when the new iframe is loaded. I'm sure this is potentially unsuitable for many reasons, but I'm going to see how far I can take this idea 😂

How I got here: I'm making a web app that renders audio in an OfflineAudioContext, and most nodes are dynamically compiled Faust AudioWorkletNodes (although not using this library). I'm using a mix of mainly Faust, and a few other nodes to do things that Faust can't do like web audio's native convolution node, which is why I'm interested in OfflineAudioContexts in the first place. But after ~50 offline renders I have been hitting the same limitations as described above. It's much easier to hit this limit with offline rendering because OfflineAudioContexts can't be reused, and new audio nodes need to be created for every new render, even if they are functionally identical, which I find to be an awkward situation. And creating all those new nodes leads to the "out of memory" error I described above. I then searched to see if anyone else hit this issue, found this library, but then realised the issue still exists here too.