From 36a6e2242711c0a5359c3c12d86ff8f6a375db2c Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jun 2026 21:48:11 +0200 Subject: [PATCH] live: cap animation-canvas DPR at 1.5 and redraw at ~60fps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live-map animation overlay re-clears and re-draws its full backing store every animation frame. Two unbounded multipliers make that expensive: 1. devicePixelRatio is uncapped in updateAnimCanvas(). The canvas is already ~1.4x the screen area (20% pad per side), so at DPR 2-3 it allocates and fills 5-12x the screen's pixels per frame. Cap at 1.5 — lines stay crisp, per-frame fill cost drops up to ~4x on hi-DPI displays. 2. renderAnimations() reschedules via rAF with no rate limit, so on 120/144Hz displays it does 2-2.4x the work for no visible gain. Add a ~60fps guard. Progress is time-based (tickDt, itself capped at 32ms), so skipping frames preserves motion exactly. Paused frames fall through to the existing sleep. No behavior change on a standard 60Hz / 1x-DPI display. Existing animation tests (test-live-dt-cap-1524, test-live-anims) unaffected. --- public/live.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/public/live.js b/public/live.js index e8e45f67..7bda25be 100644 --- a/public/live.js +++ b/public/live.js @@ -34,6 +34,10 @@ let packetCount = 0; let activeAnims = 0; const MAX_CONCURRENT_ANIMS = 20; + // Perf caps for the animation overlay (see updateAnimCanvas / renderAnimations). + const ANIM_MAX_DPR = 1.5; // cap backing-store pixels on hi-DPI displays + const ANIM_MIN_FRAME_MS = 15; // don't redraw faster than ~60fps (high-refresh guard) + let _lastAnimFrame = 0; let nodeActivity = {}; let recentPaths = []; let showGhostHops = localStorage.getItem('live-ghost-hops') !== 'false'; @@ -1291,7 +1295,11 @@ const w = size.x + padX * 2; const h = size.y + padY * 2; - const dpr = window.devicePixelRatio || 1; + // Cap the backing-store DPR. The animation canvas is ~1.4x the screen area + // (20% pad per side); at native devicePixelRatio 2-3 that means clearing and + // redrawing 5-12x the screen's pixels every frame. Capping at 1.5 keeps lines + // crisp while cutting per-frame fill cost by up to ~4x on hi-DPI displays. + const dpr = Math.min(window.devicePixelRatio || 1, ANIM_MAX_DPR); // Updating width/height automatically clears the canvas animCanvas.width = w * dpr; @@ -3855,6 +3863,17 @@ const isPaused = VCR.mode === 'PAUSED' || VCR.speed === 0; + // High-refresh guard: cap redraw at ~60fps. Progress is time-based + // (tickDt reads each object's own elapsed delta, itself capped at 32ms), + // so skipping a frame preserves motion exactly — this only stops 120/144Hz + // displays from doing 2-2.4x the per-frame work for no visible benefit. + // Skip only while running; paused frames fall through to the sleep path below. + if (!isPaused && now - _lastAnimFrame < ANIM_MIN_FRAME_MS) { + requestAnimationFrame(renderAnimations); + return; + } + _lastAnimFrame = now; + // Clear the canvas for this frame animCtx.clearRect(0, 0, animCanvas.clientWidth, animCanvas.clientHeight);