Material._eventInfo preventing GC of disposed textures

Hello!

We are currently working on trying to clean out unused resources in our app. One thing that we are now facing is that it seems that materials _eventInfo seem to prevent Textures from being GCed. Once a new model is loaded the Texture can be collected.

In our use case we load glTFs to AssetContainers that we add to our scene. When a user deletes the last reference in the scene, we dispose the AssetContainer to free the memory. But if I take a memory snapshot in Chrome after this i can see that other materials in our scene still hold a reference to the ArrayBuffer used by the AssetContainer through their _eventInfo preventing the ArrayBuffer to be GCed.

Could someone maybe enlighten me on if this is the expected behaviour?
And if it is, any ideas of what I can do to clear it without loading a new model?

Thanks for the help!

Could you add an observer to your materials and set the _eventInfo to null to see if that helps?

mat.onDisposeObservable.add(() => {
    this._eventInfo = null;
});

Iā€™m a bit suprised that this property would prevent GCing because it is not shared with anyone, so when the material is disposed and not reachable by any path, the data pointed to by this._eventInfo should not prevent the GC from doing its jobā€¦

If possible, are you able to setup a repro in the Playground?

So to clarify, we currently do set this._eventInfo = {} in our materials dispose, the issue is that other materials we have in out scene, scene.defaultMaterial for example, still hold a reference to the ArrayBuffer through their respective _eventInfo.

I will try and see if I can get a minimal repo on the playground as well, but it sounds like this is unexpected so will continue digging.

Iā€™m not sure how an ArrayBuffer created by AssetContainer can end up in material._eventInfo. Do you know what this array buffer is used for in AssetContainer, or where it is coming from?

As far as I can tell the ArrayBuffer is the backing memory for the AssetContainer that we load our glTF into. The _eventInfo in turn is holding a reference to a texture._buffer UInt8Array.
Here is a snip from my memory snapshot

Ok, so you could try something like that (to be done in material.onDisposeObservable):

this._eventInfo.texture = null;
this._eventInfo.animatables = null;
this._eventInfo.activeTextures = null;
this._eventInfo.renderTargets = null;
this._eventInfo = null;

Hello again,
I have made a small reproduction of the issue now: https://playground.babylonjs.com/#XJW2HL#14

If you let the PG run until the mesh disappears and take a memory snapshot after that you should be able to see that there is a 3 774 032 bytes large ArrayBuffer in there. Iā€™m not able to get it collected at all in the PG since spector seems to hold some reference to it (but if spector is the only thing preventing it would still work for me in production since then we donā€™t have spector running).

I donā€™t see an array buffer of 3774032 bytes when I take a snapshot as you describeā€¦ How do you know itā€™s a leak? There are a number of objects that are still in memory, because Babylon.js maintains some caches. For eg, there is the effect cache, which you can release completely by calling engine.releaseEffects().

// Manual deletion of compiledEffect, is this even safe?
delete engine._compiledEffects[Object.keys(engine._compiledEffects)[2]]

You should call engine._releaseEffect(effet) instead, as the pipeline context of the effect should also be released.

The properties with SPECTOR in the name exist even if you donā€™t use Spector. When Spector is enabled in a page, it uses these properties to provide additional features (like automatic shader recompilation when you change the vertex of fragment shader).

After testing it on another machine, it seems like the size can vary, on my desktop at home I get an array buffer of size 3 774 028 bytes instead. anyway, on both platforms its the largest object in the ArrayBuffer category for me by a wide margin, second being 305 036 (retained size). I also deducted that itā€™s the backing resource for the AssetContainer since it only shows up on snapshots once I do load the asset, and loading different assets give ArrayBuffers of a different size.

If I try using the engine._releaseEffect() I get Uncaught TypeError: e.getPipelineContext is not a function This was a typo on my end. But releasing the effect did not let me collect the ArrayBuffer.

But to take a step back since I think we are going a bit off road here.
Should calling dispose and releasing all references to an AssetContainer allow them to be GCed? or are there other things I as a user have to perform to have all the resources collected?

Ok, hereā€™s what happens.

The _eventInfo does prevent GCing, because of the customCode property. So, I will update this PR (which is already related to materials) to clear this property:

Regarding your PG, where you are clearing the _eventInfo yourself, what keeps some effects alive are:

  • the ground, because of its material
  • the light, because of its uniform buffer and the effect bound to it
  • the scene material cache, because it holds a reference to the last effect used
  • the default material created at the scene level
  • the scene uniform buffer and the effect bound to it
  • the cache of effects, at the engine level

So, for everything to be GCed correctly, you need to address all these points. Hereā€™s a fixed PG:

This one will work once the PR is merged:

Note that delete engine._compiledEffects[Object.keys(engine._compiledEffects)[2]] is not enough, because the pipeline context associated to the effect should also be released. Use engine._releaseEffect instead, to release a single effect, as it will correctly dispose of the pipeline context.

Thanks for getting back to me!

I totally understand that delete engine._compiledEffects[Object.keys(engine._compiledEffects)[2]] is not something that should be done. But as i stated in my original PG with this comment ā€˜// Doing only things up to this point is what I expect to have to do to have the ArrayBuffer collectedā€™ itā€™s was just something I did to showcase what kind of properties in Babylon.js was blocking GC. Lets ignore those parts of my PG since they seem to only cause confusion.

Cool that you found and have a fix for an issue with the _eventInfo blocking GC.

From the PG you provided it looks like we are talking past each other so I will try to clarify my question.
I do not want to empty my scene, I only want the resources related to the model that I loaded to an AssetContainer to be released from memory. Calling engine.releaseEffects() seems to have some at least for me, unexpected consequences where my scene is utterly broken after that.
What I want to achieve is removing and disposing a model (Stored in an AssetContainer) release the memory from my application. I still want all other parts of my scene to remain the same, and other models stored in separate AssetContainers to still work in the scene.

So my question boils down to:
Should calling dispose and releasing all references to an AssetContainer allow them to be GCed?

What you did is ok.

However, because of the cache of effects, some references are not released even if you release the AssetContainer. By calling engine._releaseEffect on the material effects, you can get rid of all the references and have a proper GC:

Maybe we could have a parameter to AssetContainer.removeAllFromScene to force removing the effect of the materials, given that engine._releaseEffect is an internal method that should not be called by the userā€¦ What do you think @sebavan ?

That works, thanks for the help! :slight_smile:

I will use the engine._releaseEffect() workaround for now, please keep me posted on what you decide for as the long term solution.

Checked with @Evgeni_Popov offline a new PR is cooking as we speak :slight_smile:

1 Like

Great, thanks so much for the help! Looking forward for the fixes :slight_smile:

Just updated to Babylon 5.43.1 and everything seems works as expected now!
Thanks @Evgeni_Popov :slight_smile: