From d79f7c51c83cfe220ca687ae48a8615949593e65 Mon Sep 17 00:00:00 2001 From: rakadu1 Date: Fri, 28 Nov 2025 16:41:49 +0530 Subject: [PATCH] The Fix: The new loop iterates through the data and draws rectangles directly to the canvas. It changes ctx.fillStyle more often (once per column), but this cost is negligible to the cost of allocating and collecting memory 60 times a second. This should resolve the heap memory issue you observed. --- steps/src/dom.js | 57 +++++++++++++++++------------- steps/src/drawUtils.js | 78 +++++++++++++++++++++++++----------------- 2 files changed, 80 insertions(+), 55 deletions(-) diff --git a/steps/src/dom.js b/steps/src/dom.js index f7065b4..72a4438 100644 --- a/steps/src/dom.js +++ b/steps/src/dom.js @@ -252,6 +252,7 @@ function getCurrentColorMode() { // Cache for DOM elements to avoid querySelector/getElementById every frame let overlayCache = null; +let videoOverlayCache = null; // Cache for conditional rendering let lastDrawnFrame = -1; @@ -381,8 +382,10 @@ export function updatePersistentOverlays(currentMediaTime) { ctx.clearRect(0, 0, w, h); - // --- 1. Batching Phase --- - const batches = {}; + // --- Optimization: Immediate Mode Drawing (No Allocations) --- + // Instead of batching into objects, we draw directly. + // We iterate through columns. To minimize state changes, we could pre-sort, + // but simply drawing column-by-column is fast enough and avoids GC. for (let offset = -centerCol; offset < centerCol; offset++) { const targetFrameIndex = appState.currentFrame + offset; @@ -396,30 +399,22 @@ export function updatePersistentOverlays(currentMediaTime) { const numBlocks = Math.min(10, Math.max(1, Math.round(ift / msPerBlock))); const color = getTimingColor(ift); - if (!batches[color]) batches[color] = []; - + ctx.fillStyle = color; + ctx.beginPath(); + for (let d = 0; d < numBlocks; d++) { const y = h - (d * (blockSize + vGap)) - 3; - batches[color].push({x, y}); + ctx.rect(x, y, blockSize, blockSize); } + ctx.fill(); } else { // Placeholder blocks - const color = "rgba(255, 255, 255, 0.1)"; - if (!batches[color]) batches[color] = []; + ctx.fillStyle = "rgba(255, 255, 255, 0.1)"; + ctx.beginPath(); const y = h - 3; - batches[color].push({x, y}); - } - } - - // --- 2. Drawing Phase --- - // Draw all blocks of the same color in one pass - for (const [color, points] of Object.entries(batches)) { - ctx.fillStyle = color; - ctx.beginPath(); - for (const p of points) { - ctx.rect(p.x, p.y, blockSize, blockSize); + ctx.rect(x, y, blockSize, blockSize); + ctx.fill(); } - ctx.fill(); } // Draw Center Indicator (Triangle at column 60) @@ -447,11 +442,25 @@ export function updatePersistentOverlays(currentMediaTime) { timeDisplay += ` / ${videoPlayer.duration.toFixed(2)}s`; } - videoInfoOverlay.innerHTML = ` - Frame: ${videoFrame} - | ${timeDisplay} - | Abs Time: ${formatUTCTime(absVideoTime)} - `; + // --- OPTIMIZATION: Video Overlay Caching --- + if (!videoOverlayCache) { + videoInfoOverlay.innerHTML = ` + Frame: + | + | Abs Time: + `; + videoOverlayCache = { + frame: document.getElementById("ov-vid-frame"), + time: document.getElementById("ov-vid-time"), + abs: document.getElementById("ov-vid-abs") + }; + } + + if (videoOverlayCache) { + videoOverlayCache.frame.textContent = videoFrame; + videoOverlayCache.time.textContent = timeDisplay; + videoOverlayCache.abs.textContent = formatUTCTime(absVideoTime); + } } const customTtcInputs = [ diff --git a/steps/src/drawUtils.js b/steps/src/drawUtils.js index bede29d..65ac282 100644 --- a/steps/src/drawUtils.js +++ b/steps/src/drawUtils.js @@ -437,18 +437,18 @@ export function drawTrackMarkers(p, plotScales) { const localStationaryColor = stationaryColor(p); const localMovingColor = movingColor(p); + // Optimization: Batch drawing commands + // We collect all text labels to draw them in a single pass at the end. + // This avoids switching between stroke/fill and push/pop for every track. + const textLabels = []; + + p.push(); + p.strokeWeight(2); + for (const track of appState.vizData.tracks) { - // --- START: Add the Same Safeguard Here --- - // This robust check ensures the track and its historyLog are valid before use. - if (toggleConfirmedOnly.checked && track.isConfirmed === false) { - continue; - } - if (!track || !track.historyLog || !Array.isArray(track.historyLog)) { - // We don't need to log a warning here again, as drawTrajectories already did. - // We can just safely skip this malformed track. - continue; - } - // --- END: Add the Same Safeguard Here --- + if (toggleConfirmedOnly.checked && track.isConfirmed === false) continue; + // Robust check for malformed tracks (same as drawTrajectories) + if (!track || !track.historyLog || !Array.isArray(track.historyLog)) continue; const log = track.historyLog.find( (log) => log.frameIdx === appState.currentFrame @@ -460,63 +460,79 @@ export function drawTrackMarkers(p, plotScales) { const size = 5; const x = pos[0] * plotScales.plotScaleX; const y = pos[1] * plotScales.plotScaleY; - let velocityColor = p.color(255, 0, 255, 200); - - p.push(); - p.strokeWeight(2); + + // --- Draw Marker Shape --- if (useStationary && log.isStationary === true) { p.stroke(localStationaryColor); p.noFill(); p.rectMode(p.CENTER); p.square(x, y, size * 1.5); - velocityColor = localStationaryColor; } else { let markerColor = p.color(0, 0, 255); if (useStationary && log.isStationary === false) { markerColor = localMovingColor; - velocityColor = localMovingColor; } p.stroke(markerColor); p.line(x - size, y, x + size, y); p.line(x, y - size, x, y + size); } - p.pop(); + // --- Draw Velocity Vector & Collect Text --- if ( showDetails && log.predictedVelocity && log.predictedVelocity[0] !== null ) { const [vx, vy] = log.predictedVelocity; + + // Draw velocity line immediately (shares stroke context) if (log.isStationary === false) { - p.push(); - p.stroke(velocityColor); - p.strokeWeight(2); - p.line( + // Determine color again (optimization: could be refactored to avoid recalc) + let velocityColor = p.color(255, 0, 255, 200); + if (useStationary) velocityColor = localMovingColor; + + p.stroke(velocityColor); + p.line( x, y, (pos[0] + vx) * plotScales.plotScaleX, (pos[1] + vy) * plotScales.plotScaleY ); - p.pop(); } - const speed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1); + + // Defer Text Drawing + const speed = (Math.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1); const ttc = log.ttc !== null && isFinite(log.ttc) && log.ttc < 100 ? `TTC: ${log.ttc.toFixed(1)}s` : ""; const text = `ID: ${track.id} | ${speed} km/h\n${ttc}`; - p.push(); - p.fill(textColor); - p.noStroke(); - p.scale(1, -1); - p.textSize(12); - p.text(text, x + 10, -y); - p.pop(); + + textLabels.push({ x, y, text }); } } } } + p.pop(); // End shape drawing context + + // --- Batch Draw Text --- + if (textLabels.length > 0) { + p.push(); + p.fill(textColor); + p.noStroke(); + p.textSize(12); + // Set alignment once + // Note: we handle the flip manually + + for (const label of textLabels) { + p.push(); + p.translate(label.x + 10, label.y); + p.scale(1, -1); // Flip text back up + p.text(label.text, 0, 0); + p.pop(); + } + p.pop(); + } }