Disposing Engines – WebGL out of memory

Dear BabylonJS community,

I am running into WebGL memory issues when disposing BabylonJS engines. BabylonJS is only used within one module of my SPA. That module is not necessarily being used when using the app. This is why the BabylonJS engine and therefore all their scenes/meshes/… are disposed when exiting that part of the app. Unfortunately they seem to remain in memory:

Each new BabylonJS engine creates a new WebGL context within the browser. Because the engines are not correctly disposed, the amount of WebGL contexts keeps growing until app crash or browser warning.

Unfortunately, I could not reproduce this in a BabylonJS playground. The engines are correctly disposed when hitting the “Run” button.

Here is what I have tried:

  • Disabling features that might reference the engine indirectly and prevent disposal by the garbage collector
  • Explicitly clearing WebGL contexts with the WebGL API loseContext
  • Explicitly reusing the WebGL context on engine creation

Are there any other potential causes to this?

Thanks and best regards

I guess the reference to the engine is on your side :frowning: are you calling engine.dispose() ?

Then ensure you are not keeping any ref to engine anywhere in your code and you could also remove the canvas from the dom ?

Yes, I’m calling engine.dispose() and the canvas is deleted from the DOM as well.

so something else is holding on to the created engine :frowning: do you keep them somewhere in array ?

Normally if you select it, on the bottom panel in Chrome Dev tools, you should be able overriding the retention points, to see where is the code holding onto them

1 Like

I could not find any reference. I tried reproducing in the playground and found strange behaviour. Please try the following code within the browser console on a random playground (https://www.babylonjs-playground.com):

let canvas = document.createElement('canvas');
canvas.id = 'ourTestCanvas';
canvas.style.position = 'absolute';
canvas.style.border = '5px solid red';
canvas.style.top = 0
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
let ourEngine = new BABYLON.Engine(canvas);
let ourScene = new BABYLON.Scene(ourEngine);
let camera = new BABYLON.FreeCamera("ourCamera", new BABYLON.Vector3(0, 5, -10), ourScene);
ourEngine.runRenderLoop(() => ourScene.render());
ourEngine.stopRenderLoop();
canvas.remove();
ourEngine.dispose();
canvas = null; ourScene = null; ourEngine = null; camera = null;

As a result, Safari DevTools still shows ourTestCanvas. Did I do something wrong?

I have no idea about the Safari dev tools and we have so many issues on their latest release I wonder if that could be on their side ???

Your code should definitely work and it might be the devtools holding onto it ?

1 Like

I wasn’t able to repro this on MacOS 11.6 and Safari 15.0 (16612.1.29.41.4, 16612). The testCanvas doesn’t show up for me in the Graphics tab after running your code in the console. What versions are you running?

1 Like

The root symptoms of my app were naturally independent of the browser dev tools. They occurred in Google Chrome (Version 94.0.4606.81 (Official Build) (x86_64)), Google Chrome Canary (97.0.4666.0 (Official Build) canary (x86_64)), Safari Version 15.0 (16612.1.29.41.4, 16612) and Safari Technology Preview (Release 133 (Safari 15.4, WebKit 16613.1.2.2)).

Interestingly, the engine seems to be correctly disposed when running all the code at once (even on my machine with said browsers). But if I split it into multiple parts (especially separating the running and disposing), it doesn’t work and still shows the second WebGL context after disposing the engine and setting the variables to null. For example:

let canvas = document.createElement('canvas');
canvas.id = 'ourTestCanvas';
canvas.style.position = 'absolute';
canvas.style.border = '5px solid red';
canvas.style.top = 0
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
let ourEngine = new BABYLON.Engine(canvas);
let ourScene = new BABYLON.Scene(ourEngine);
let camera = new BABYLON.FreeCamera("ourCamera", new BABYLON.Vector3(0, 5, -10), ourScene);
ourEngine.runRenderLoop(() => ourScene.render());
ourEngine.stopRenderLoop();
canvas.remove();
ourEngine.dispose();
canvas = null; ourScene = null; ourEngine = null; camera = null;

And the engine is still visible within the dev tools of Safari.

This was a really interesting one to debug. It is very weird behavior. It looks like it’s actually caused by the Safari Web Inspector! See this Stack Overflow for an explanation: javascript - Cannot remove canvas elements from memory that are created but never appended to the page - Stack Overflow

If you run the code in the answer without the inspector open, you will see the normal number of canvases. But if you have the inspector open when the code runs, it will keep a reference to every single canvas that gets created, resulting in a huge memory usage. I guess it is preventing the canvases from getting garbage collected, for debug purposes.

Can you try running your code to create and dispose an engine without the inspector open, and then check at the end if the canvas was still retained?

2 Likes