Memory Leak when dynamically assigning Collision Mesh using renderAsset

Hi everyone,

we have been working with dynamic collision meshes lately and ran into a problem with CollisionComponent / RenderComponent.

I have created a minimal reproducer here:
https://playcanvas.com/project/931187/overview/collision-mesh-leak

We basically create a new Entity from script, assign a Collision-, Render- and Rigidbody Component to it and set the RenderComponent’s asset to a RenderAsset (Editor Attribute) and the CollisionComponent’s renderAsset to the same asset.

This is the code:

    this.spawnedEntity = new pc.Entity();

    this.spawnedEntity.addComponent("collision", {
        type: "mesh"
    });

    this.spawnedEntity.addComponent("rigidbody", { type: pc.BODYTYPE_STATIC });

    this.spawnedEntity.addComponent("render", {type: "asset"});
    this.spawnedEntity.render.asset = this.meshAsset;
    this.spawnedEntity.render.layers = [this.app.scene.layers.getLayerByName("World").id];
    this.spawnedEntity.render.material = this.meshMaterial;

    this.spawnedEntity.collision.renderAsset = this.meshAsset;

    this.entity.addChild(this.spawnedEntity);
    this.spawnedEntity.setPosition(0,0,0);
    this.spawnedEntity.setLocalPosition(0,0,0);

We later destroy that spawned entity again by calling Entity.destroy().

We would expect the destroy call to clean up all allocated memory accordingly. However, when creating and destroying a lot of entities in this manner, Ammo will report a OOM Exception after a while. In the reproducer we create and destroy 10 000 entities each frame and ammo will run out of memory after about 2 seconds. We used a simple mesh of a box. With more complex meshes, this will happen even faster.

I have dug into the engine code for a couple of hours now but can’t seem to find an easy fix for this.
So I’m now asking for your help.
Am I doing something wrong during entity creation?
Am I supposed to manually clean up any resources when dynamically creating an entity like this?

Some technical details that might be helpful / of interest to you:
By digging through memory snapshots and the engine code, I noticed that when Components get added to an Entity, their data is registered in an internal store of the corresponding system. I.e. the RigidbodyComponentSystem will store RigidbodyComponentData for each RigidbodyComponent that is created. I noticed that in this scenario the Rigidbody does not contain a body entry in it’s data at creation. However, the initialization of the CollisionComponent will cause recreatePhysicalShapes of the CollisionComponentSystem to be called, which will call createBody for the RigidbodyComponent of the Entity. The created body will be allocated and stored in the RigidbodyComponent, but the RigidbodyComponentData in the System’s store is never updated. Usually the allocated body will be freed in the onRemove, when a RigidbodyComponent is removed from an Entity, but since the data does not contain a body entry, the memory allocated by the body is leaked.
I tried to workaround this by manually setting the body information later (see commented line in the reproducer), but that only delayed the OOM error by a couple of seconds:

this.spawnedEntity.rigidbody.system.store[this.spawnedEntity.getGuid()].data.body = this.spawnedEntity.rigidbody.body;
1 Like

Hi @Sam_RDX,

I think the issue is that the triMeshCache shape isn’t ever cleared. You can see that in the browser console by following this property:

pc.app.systems.collision._triMeshCache

The engine will create a trimesh shape and cache it, the first time a mesh collider is requested. The shape will be reused for all colliders using the same mesh.

If all colliders using the same mesh get destroyed, I think there isn’t a system currently to remove the cached shape.

There is feature request about it in the engine repo and a relevant PR in the works:

1 Like

In the meantime what you can do is to patch the following engine method, add your own control over when something is cached and when the cache is cleared:

We’ve done something similar in the following project, to have control over caching (allow mesh colliders to scale), and also clear the cache when it’s required (terrain colliders deleted):

2 Likes

Hi @Leonidas,

thanks for the quick reply! We also ran into problems with the triMeshCache earlier in prototyping and were able to workaround that by clearing the cache manually. However, we don’t think this problem is related to that cache this time. When Ammo runs OOM the triMeshCache actually only contains a single mesh, since I re-use the same renderAsset for each Entity that I create and destroy (I checked this via console as suggested by you).
So PlayCanvas already re-uses that mesh using the cache, but memory is leaked elsewhere. I currently suspect the memory being leaked everytime the Ammo rigidbody is created by PlayCanvas when recreatePhysicalShapes is called for a new CollisionComponent with a RenderAsset assigned.

1 Like

Good point, it may be something else. I’ll try and give your sample project a try and see if I find something.

1 Like

@LeXXik any idea?

Yes, there is a bug in rigidbody component, which doesn’t remove the body. You can add your voice to the issue, so it gets prioritized:

Edit:
The reported issue about memory leak is when you remove rigidbody component from the entity. It should get destroyed fine, if you just disable entity. If you remove the component, then it will leak.

2 Likes

You can also try to use a debug drawer, to see if you spot anything unusual in the physics world:
https://playcanvas.com/project/744419/overview/ammo-debug-draw

1 Like

Hi @LeXXik,

thanks for the insights! That issue describes exactly what we also encountered.

In regards to your edit, do you mean that disabling the rigidbody before removing it, would solve the problem? I tried that, unfortunately it still leaks memory.

I also tried setting the body field in the component-data manually, so that it is properly cleaned up in the onRemove:

this.spawnedEntity.rigidbody.system.store[this.spawnedEntity.getGuid()].data.body = this.spawnedEntity.rigidbody.body;

Unfortunately, this will still not clean up everything accordingly. It seems to me that when RigidbodyComponentSystem.destroyBody is called and the motion state is retrieved with body.getMotionState, the wrong destructor of the motionState is called (the base class’ destructor is called instead). Do you have any ideas on how we could work around this issue until it gets fixed properly?

Thanks a lot for your help! We really appreciate that!

1 Like

Let’s assume there are no issues in the rigidbody component system. What is your normal way of creating and destroying the entities with physics components? Do you clone a predefined entity and then entity.destroy() when no longer needed? Or do you add/remove components via code? Other ways?

1 Like

We create the entity + components completely via code, similar to the reproducer project I have linked:

    this.spawnedEntity = new pc.Entity();

    this.spawnedEntity.addComponent("collision", {
        type: "mesh"
    });

    this.spawnedEntity.addComponent("rigidbody", { type: pc.BODYTYPE_STATIC });

    this.spawnedEntity.addComponent("render", {type: "asset"});
    this.spawnedEntity.render.asset = this.meshAsset;
    this.spawnedEntity.render.layers = [this.app.scene.layers.getLayerByName("World").id];
    this.spawnedEntity.render.material = this.meshMaterial;

    this.spawnedEntity.collision.renderAsset = this.meshAsset;

    this.entity.addChild(this.spawnedEntity);
    this.spawnedEntity.setPosition(0,0,0);
    this.spawnedEntity.setLocalPosition(0,0,0);

meshAsset and meshMaterial are editor attributes.

When no longer needed (on scene change), we simply destroy the entity.

Could you please specify what do you mean by destroying here? What do you actually do?

In the reproducer we call

this.spawnedEntity.destroy();

I’ve also tried:

this.spawnedEntity.rigidbody.enabled = false;
this.spawnedEntity.destroy();

We first create the entity as described above and then immediately destroy it in the next line of code.
We repeat this 10 000 times per frame. After about 2 seconds, Ammo will run OOM.

In our actual use-case we also do some other cleanup, but those shouldn’t be relevant, since we see the exact same memory leaked in the reproducer (looking at a heap snapshot).

1 Like

I hope you are only doing 10 000 times per frame only for the sake of reproducing the OOM, and not for any other reason :slight_smile: Nevertheless, thanks to it, I was able to reproduce your case. After some debugging, I found the cause, which is a new issue actually.

I made a ticket for it, so you can track the progress:

As a workaround, you can grab the override.js file from this project, and set it to load After Engine in Editor. It will patch the Collision Component System:
https://playcanvas.com/project/932338/overview/temp-ccs-patch

Thank you for bringing this to our attention, @Sam_RDX !

6 Likes

Haha, yeah that’s only for reproducing purposes :sweat_smile:

That’s awesome, thanks for creating the GH issue and for the workaround!
I’ve tried it and this definitely already fixes my reproducer project. I will try our actual use case next.

JFYI: I still had to also include the other workaround for the rigidbody leak as well in order to get it working without OOM errors:

this.spawnedEntity.rigidbody.system.store[this.spawnedEntity.getGuid()].data.body = this.spawnedEntity.rigidbody.body;

I have added both workarounds to my reproducer project linked at the start of this thread for documentation purposes, so that project is now running without OOM.

Thanks a lot @LeXXik!

3 Likes

I will try our actual use case next.

Worked very well, except for two points. I’m also adding these to the thread for documentation purposes:

  1. I had to add some missing variables in your override.js for Trigger to work correctly. Simply adding this line at the top solved that:
let ammoVec1, ammoQuat, ammoTransform;
  1. I had to be careful with my placement of the rigidbody workaround in order for it to work… I had to place it right before destroying the entity, otherwise changing the mesh at runtime instead of destroying it caused issues when actually calling destroy afterwards.
    I think that workaround is not ideal and still doesn’t properly cleanup everything, but it will work for our use-case for now until this gets properly fixed in the engine.

Thanks again for your help everyone!

3 Likes