Potential WebGPU `visibleInstances` leak?

Hi folks! I’m seeing a potential memory leak in WebGPU. After several minutes, Chrome eventually crashes with the “Aw, Snap!” out of memory error.

To test, I captured 2 Heap snapshots taken a few seconds apart and compared them. Several tens of thousands of new Arrays were generated and retained (several Megabytes), and they were from here in the source:

class _InstanceDataStorageRenderPass {
    /** @internal */
    public visibleInstances: Nullable<IInstanceDataStorageRenderPassVisibleInstances>;
    // ...
}

If I leave it running for several minutes, there will be millions of these new Arrays generated and retained (few Gigabytes) until the “Aw, Snap” out of memory crash.

Please see the video below of the Heap snapshot capturing and analyzing:


I asked Claude AI if there could be a leak with visibleInstances, and it said:

Looking at the [_InstanceDataStorageRenderPass.visibleInstances]mesh.ts ) and how it’s managed, I can see a potential leak specific to WebGPU (non-mono render pass mode).

The Leak

In [_registerInstanceForRenderId()]mesh.ts ):

if (!instanceDataStorage.visibleInstances[renderId]) {
    if (instanceDataStorage.previousRenderId !== undefined && this._instanceDataStorage.isFrozen) {
        instanceDataStorage.visibleInstances[instanceDataStorage.previousRenderId] = null;
    }
    instanceDataStorage.previousRenderId = renderId;
    instanceDataStorage.visibleInstances[renderId] = new Array<InstancedMesh>();
}

The cleanup (visibleInstances[previousRenderId] = null) only fires when isFrozen is true. When not frozen, old render IDs accumulate indefinitely on the visibleInstances object — one entry per render ID, every frame.

Why It’s WebGPU-Specific

In the [Mesh mesh.ts ):

this._instanceDataStorage.useMonoDataStorageRenderPass = !this._instanceDataStorage.engine.isWebGPU;

In [_preActivate()]mesh.ts ):

if (this._instanceDataStorage.useMonoDataStorageRenderPass) {
    this._instanceDataStorage.dataStorageRenderPass.visibleInstances = null; // ← WebGL path: clears entire object
} else {
    this._getInstanceDataStorage().visibleInstances = null; // ← WebGPU path: only clears CURRENT render pass
}

On WebGPU, _preActivate only resets visibleInstances for the currently active render pass at pre-activation time. But _registerInstanceForRenderId can be called for different render IDs within the same render pass object. Since the cleanup in _registerInstanceForRenderId is gated on isFrozen, those old renderId keys accumulate on the visibleInstances index object and are never cleaned up.

The Fix

The cleanup should not be gated on isFrozen:

// ...existing code...
    public _registerInstanceForRenderId(instance: InstancedMesh, renderId: number): Mesh {
        const instanceDataStorage = this._instanceDataStorage.useMonoDataStorageRenderPass ? this._instanceDataStorage.dataStorageRenderPass : this._getInstanceDataStorage();
        if (!instanceDataStorage.visibleInstances) {
            instanceDataStorage.visibleInstances = {
                defaultRenderId: renderId,
                selfDefaultRenderId: this._renderId,
                intermediateDefaultRenderId: -1,
            };
        }

        if (!instanceDataStorage.visibleInstances[renderId]) {
            if (instanceDataStorage.previousRenderId !== undefined) { // Remove the isFrozen guard
                instanceDataStorage.visibleInstances[instanceDataStorage.previousRenderId] = null;
            }
            instanceDataStorage.previousRenderId = renderId;
            instanceDataStorage.visibleInstances[renderId] = new Array<InstancedMesh>();
        }

        instanceDataStorage.visibleInstances[renderId].push(instance);
        return this;
    }
// ...existing code...

The isFrozen guard was likely added to preserve the cached batch when frozen, but visibleInstances is separate from previousBatch — the batch cache is preserved via [instanceDataStorage.previousBatch]mesh.ts ) in _getInstancesRenderList(), so nulling out old render ID entries in visibleInstances is safe even when frozen.


I’m totally new to WebGPU and am unsure if Claude is on the right path here.

It’s interesting how Claude mentioned this leak only applies to WebGPU. Indeed, when I switched to WebGL2 and performed the same Heap snapshots, I did not see this leak. :check_box_with_check:

I would be grateful if anyone could help with this. I am always happy to help test out any changes. Thank you!!

Well spotted!

There was also another minor leak, this PR fixes them all:

3 Likes

OMG thank you so much, Evgeni!!