Freeing memory from Ammo.js

Hey,

I’ve experienced problems with Ammo.js memory leakage leading to OOM and failure in creating colliders. It doesen’t seem I’m the first to run into this problem (which is not too encouraging). Following this post, it is supposed to free it’s memory usage automatically when all references are released.

I’ve tried everything I can think of so far, but with no success. Anyone have some trick up their sleeve? Or am I missing something?

Here’s what I tried so far:

// Code for creating the collider
let mesh = createCustomMesh();
let node = new pc.GraphNode();
let meshInstance = new pc.MeshInstance(node, mesh, new pc.StandardMaterial());
let collisionModel = new pc.Model();
collisionModel.graph = node;
collisionModel.meshInstances.push(meshInstance);
this.entity.collision.model = collisionModel;

// Code for freening up memory (which does not work apparently)
this.entity.on('destroy', ()=> {
    mesh.indexBuffer[0].destroy();
    mesh.vertexBuffer.destroy();
    mesh.destroy();
    node.destroy();
    meshInstance.destroy();
    collisionModel.destroy();
});

You are only destroying visual mesh here. You don’t need to do anything of that. Simply disable or destroy the entity that has the collision component and the engine will handle the rest. Your OOM crash happens because some Ammo objects are not cleared inside the Wasm instance, when an entity with a mesh collider is destroyed. As a workaround, you have to manually destroy the Ammo objects stored in the trimesh cache of the collision component system.

More details here:

1 Like

Hey,
Thx for your reply!!

I had a closer look at the discussion you linked. It seems like the Ammo interface in the Main branch is a bit different from the branch you linked right? Also, the changes either never made it or has yet to be merged into Main?

Anyhow, after further investigation I found that this seems to be the way to free Ammo mesh collider memory in the current version of PlayCanvas:

let mesh = createCustomMesh(); 
let trimeshCache = this.entity.collision.system._triMeshCache[mesh.id];

this.entity.on('destroy', () => {
      // Free Ammo memory
      Ammo.destroy(trimeshCache);
  }) 

Doing this for my custom mesh colliders seems to have done the trick!
… Mostly. Somehow there still seems to be some leaks, tho reduced. What befuddles me even more is that apparently this should’ve already been done automatically when the collision component is destroyed with the rest of the entity (even if said component was added at runtime), if I understand this part of the engine code correctly: Main branch collision destroy

Is this a known issue? Am I completely misunderstanding? (I’m on very rocky ground about all of this)

Oh, and as an attempt to remove all leaks I also tried manually clearing the cache for “normal” mesh colliders (in which I had not generated the mesh by code, but simply by adding a render component to the attributes, and then loading the collider as a template),. The weird thing was that doing this threw no errors, but when I re-loaded the colliders (or rather the templates containing the colliders), the whole Ammo system crashed.

Yeah, it is a known issue that is waiting for me or someone to make a PR :slight_smile:

Your best approach for now is to manually clear the trimesh cache, like you’ve already done.

The other point is that not all created Ammo objects are in the cache. Only trimesh is added there, but for example, Ammo.btBvhTriangleMeshShape is allocated but never gets destroyed anywhere, so it leaks.

Edit: but this is an assumption. I haven’t studied the Bullet sources. It might be ref counted and destroyed by Ammo when shape is destroyed.

So, I dug a bit deeper, and found a couple of things.

Firstly I realized why I experienced the behavior I described in my second comment, so I modified my code a bit, and I now successfully remove all btTriangleMesh at runtime, without eny errors or hichups. For those that are interested, I made this script which can be attached to any entity with a mesh collider (using the render attribute), and it will clean out the relevant btTriangleMesh ammo cache when the entity is destroyed (for current version of PlayCanvas):

var FreeAmmoMeshColliderMemory = pc.createScript('freeAmmoMeshColliderMemory');

FreeAmmoMeshColliderMemory.prototype.initialize = function() {
    let meshID = this.entity.collision.render.meshes[0].id; // <-- NB! Assumes the collision component uses the "render" attribute
    let trimeshCache = this.entity.collision.system._triMeshCache;

    this.entity.on('destroy', () => {
        // Free Ammo memory
        Ammo.destroy(trimeshCache[meshID]);
        // Remove reference so future collission components using the same mesh does not think it is already in the Ammo memory
        delete trimeshCache[meshID];
        // NB! There is still some leakage, most likely due to Ammo.btBvhTriangleMeshShape not being freed.
        // see https://github.com/playcanvas/engine/blob/4075e5b7c292d66ff0a872f4cb604468430298cf/src/framework/components/collision/system.js#L437
        // and https://forum.playcanvas.com/t/freeing-memory-from-ammo-js/35859/5
        // If mesh is expected to be used as a collider many times repeatedly, it might better not to remove it from the cache at all.
    })  
};

This helps reduce the Ammo memory leakage, but I can confirm there is still quite significant leakage, as this fix only postponed the inevitable OOM crash. I managed to keep track of the btBvhTriangleMeshShape that are created, and attempted to remove them from cache in a similar way (using Ammo.destroy()) when they were no longer in use. However, this caused Ammo to throw a lot of errors and eventually crash completely:

I tried looking at the Ammo and Bullet documentation, but nothing seemed to explain why this happened, except perhaps an issue in the Ammo repo where Maksims reported a problem with Ammo itself not handling btBvhTriangleMeshShape cache properly.

You wouldn’t happen to know more about this?

For now I made the stupidest hack for my game, where I catch the Ammo OOM error when it happens, save the users progress, and completely refresh the entire page, efficiently cleaning the entire cache (luckily this is most likely to happen during scene transitions, so it’s not as horrible as it sounds for the user).

Yeah, I don’t have a solution here, unfortunately. This needs a javascript weakref and finalizers support in WebIDL, which is not coming any time soon. Embind supports those, but it is slower than WebIDL, so Ammo is not going to switch to it. We stopped using Ammo in our team a while ago.

Hmm… IC why now X)

Well, thanks for all the help anyways! If anything I learned a lot, and it did help a bit.

1 Like