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
2026-05-31 22:45:02 +00:00
parent 6d5d439434
commit 498a2dcbb5
+22 -9
View File
@@ -19,6 +19,10 @@
let activeFades = [];
let isFading = false;
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 = [];
const CLICKABLE_PATH_TTL_MS = 30000;
const CLICKABLE_PATH_MAX = 50;
@@ -3585,15 +3589,14 @@
const fromLayerPt = map.latLngToLayerPoint(anim.from);
const toLayerPt = map.latLngToLayerPoint(anim.to);
// Offset by the canvas's position within the pane to get drawable pixels
const fromPt = {
x: fromLayerPt.x - canvasTopLeft.x,
y: fromLayerPt.y - canvasTopLeft.y
};
const toPt = {
x: toLayerPt.x - canvasTopLeft.x,
y: toLayerPt.y - canvasTopLeft.y
};
// Offset by the canvas's position within the pane to get drawable pixels.
// #1514 S2 — reuse module-scoped scratch objects instead of allocating per frame.
const fromPt = _scratchFrom;
fromPt.x = fromLayerPt.x - canvasTopLeft.x;
fromPt.y = fromLayerPt.y - canvasTopLeft.y;
const toPt = _scratchTo;
toPt.x = toLayerPt.x - canvasTopLeft.x;
toPt.y = toLayerPt.y - canvasTopLeft.y;
const W = animCanvas.clientWidth;
const H = animCanvas.clientHeight;
@@ -4013,6 +4016,16 @@
}
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;
activeFades.length = 0;
isAnimating = false;