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 [
Meshmesh.ts ):this._instanceDataStorage.useMonoDataStorageRenderPass = !this._instanceDataStorage.engine.isWebGPU;
WebGL: uses dataStorageRenderPass (single object, cleared via _preActivate setting visibleInstances = null)
WebGPU: uses renderPasses[renderPassId] map — one [
_InstanceDataStorageRenderPass]mesh.ts ) per render pass. Each pass has its own visibleInstances object.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. ![]()
I would be grateful if anyone could help with this. I am always happy to help test out any changes. Thank you!!