From c0fde5f846b224493ab06aabc01108e5976d9c08 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Tue, 4 Aug 2026 18:01:53 -0400 Subject: [PATCH] fix(inspector): release animation and texture state on teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two retention paths, both only reachable with the inspector enabled: - Animations still running when their node is destroyed never emit `stopped`, so their entry — and the IAnimationController it holds — stayed in the static `activeAnimations` map for the lifetime of the page. Destroy now ends them as `cancelled`, which also keeps `getAnimationStats()` consistent instead of silently dropping them. - `textureMetrics` was a strong `Map` on an object that lives as long as the renderer, and entries were only dropped for the texture a node happened to hold at destroy time. `setupTextureListeners` detaches the old texture's listeners on every swap but never dropped its metrics entry, so every texture ever swapped off a node stayed reachable, bitmap included. `WeakMap` instead — nothing iterates it, so the API is unchanged. Co-Authored-By: Claude Opus 5 --- src/main-api/Inspector.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main-api/Inspector.ts b/src/main-api/Inspector.ts index b8aae5a..724a416 100644 --- a/src/main-api/Inspector.ts +++ b/src/main-api/Inspector.ts @@ -297,7 +297,10 @@ export class Inspector { private width = 1920; private scaleX = 1; private scaleY = 1; - private textureMetrics = new Map(); + // Keyed weakly: a texture that is swapped off a node is never revisited by + // the inspector, and a strong map would pin it (and its bitmap) for the + // lifetime of the renderer. + private textureMetrics = new WeakMap(); // Stable reference so the same function can be passed to both // add/removeEventListener — `.bind()` returns a new fn each call, so binding // inline in destroy() would never actually remove the listener. @@ -1042,6 +1045,15 @@ export class Inspector { }); coreNodeListeners.clear(); + // Animations still running when the node goes away never emit + // `stopped`, so their entry — and the controller it holds — would stay + // in the static activeAnimations map for the lifetime of the page. + Inspector.activeAnimations.forEach((animation, animationId) => { + if (animation.nodeId === node.id) { + this.trackAnimationEnd(animationId, 'cancelled'); + } + }); + this.destroyNode(node); originalDestroy.call(node, isChild); },