perf(live): hoist scratch points + drain onComplete on destroy (#1514 S2+S3)

S2 — renderAnimations() previously allocated two object literals per anim
per frame. At 50 concurrent anims × 60fps that is ~6000 garbage objects
per second. Hoist to two module-scoped scratch points reused each frame.

S3 — destroy() previously cleared activeAnimations without firing pending
onComplete callbacks. Audio onHop hooks depend on those firing; navigating
away mid-flight would silently swallow them. Iterate the queue and call
onComplete (with try/catch so a misbehaving handler can't block teardown)
BEFORE truncating the array.

Refs #1514
This commit is contained in:
OpenClaw Bot committed 2026-05-31 22:45:02 +00:00
1 parent 6d5d439434
commit 498a2dcbb5
1 file changed
+22 -9
+22 -9
View File
@@ -19,6 +19,10 @@
let activeFades = []; let activeFades = [];
let isFading = false; let isFading = false;
let canvasTopLeft; let canvasTopLeft;
// #1514 S2 — scratch points reused per-frame in renderAnimations() to avoid
// 2 object allocations per anim per frame (50 anims × 60fps = ~6000/sec).
const _scratchFrom = { x: 0, y: 0 };
const _scratchTo = { x: 0, y: 0 };
let clickablePaths = []; let clickablePaths = [];
const CLICKABLE_PATH_TTL_MS = 30000; const CLICKABLE_PATH_TTL_MS = 30000;
const CLICKABLE_PATH_MAX = 50; const CLICKABLE_PATH_MAX = 50;
@@ -3585,15 +3589,14 @@
const fromLayerPt = map.latLngToLayerPoint(anim.from); const fromLayerPt = map.latLngToLayerPoint(anim.from);
const toLayerPt = map.latLngToLayerPoint(anim.to); const toLayerPt = map.latLngToLayerPoint(anim.to);
// Offset by the canvas's position within the pane to get drawable pixels // Offset by the canvas's position within the pane to get drawable pixels.
const fromPt = { // #1514 S2 — reuse module-scoped scratch objects instead of allocating per frame.
x: fromLayerPt.x - canvasTopLeft.x, const fromPt = _scratchFrom;
y: fromLayerPt.y - canvasTopLeft.y fromPt.x = fromLayerPt.x - canvasTopLeft.x;
}; fromPt.y = fromLayerPt.y - canvasTopLeft.y;
const toPt = { const toPt = _scratchTo;
x: toLayerPt.x - canvasTopLeft.x, toPt.x = toLayerPt.x - canvasTopLeft.x;
y: toLayerPt.y - canvasTopLeft.y toPt.y = toLayerPt.y - canvasTopLeft.y;
};
const W = animCanvas.clientWidth; const W = animCanvas.clientWidth;
const H = animCanvas.clientHeight; const H = animCanvas.clientHeight;
@@ -4013,6 +4016,16 @@
} }
function destroy() { function destroy() {
// #1514 S3 — drain onComplete callbacks BEFORE clearing the array. Audio
// `onHop` hooks rely on these firing exactly once per queued animation;
// previously destroy() dropped them silently when navigating away with
// packets in flight.
for (let i = 0; i < activeAnimations.length; i++) {
const a = activeAnimations[i];
if (a && typeof a.onComplete === 'function') {
try { a.onComplete(); } catch (_) {}
}
}
activeAnimations.length = 0; activeAnimations.length = 0;
activeFades.length = 0; activeFades.length = 0;
isAnimating = false; isAnimating = false;