Memory Leak Investigation — src/live/index.html

Symptom

Chrome tab memory grows from ~150MB to ~500MB+ over 30 minutes. Observed on /live/?kiosk page.

Root cause

Globe.gl's ring digest never releases ring data objects. Every ring object passed to globe.ringsData() is retained in the digest's internal Map permanently, even after being removed from subsequent calls.

Verified with FinalizationRegistry:

Bug report filed at ../globe.gl/BUG.md.

Fix

Replaced dynamic ring creation/destruction with a fixed object pool. The pool is passed to globe.ringsData() once at init. On updates, we mutate pool objects' lat/lng properties in place. Since object identity never changes, the digest's add/remove path is never triggered, avoiding the leak entirely.

Additional fix: Tween leak

globe.pointOfView(target, duration) accumulated Tween objects in an internal group that never pruned finished tweens. Each call with duration > 0 added a tween permanently. In kiosk mode this was called on every shield event.

Replaced with smoothPointOfView() — custom requestAnimationFrame animation that calls globe.pointOfView(coords, 0) (duration=0 = no tween created).

Investigation approach

Systematic isolation via console overrides, monitoring performance.memory and renderer.info.memory every 30s, and FinalizationRegistry tracking.

Round 1: Isolate subsystem

Testjs:total trendConclusion
Baseline+5–25 MB/min, acceleratingLeak confirmed
eventSource.close()FlatLeak tied to SSE event handling
eventSource.close() freshFlat at 132Render loop, audio, clock don't leak
onmessage = noop~0.3 MB/minNot EventSource buffer
onmessage = parse-onlyFlatNot JSON.parse

Round 2: Isolate handler function

Testjs:total trendConclusion
updateEventHistory = noopGrowingNot DOM updates
smoothPointOfView = noopGrowingNot camera animation
processShieldData = noopFlatLeak is in processShieldData
processEcomscanData aloneFlatecomscan not leaking

Round 3: Isolate within processShieldData

Testjs:total trendConclusion
globe.ringsData = noopGrowingNot just ringsData call
All globe methods + handlers noopedGrowingNot globe.gl API calls
All nooped, fresh reloadFlat at 128Contaminated by prior state

Round 4: Object-level GC tracking

Object typeCreated/FinalizedLeaking?
Arc52/44No
Glow52/44No
Ring52/0Yes — never GC'd

Round 5: Confirm ring digest is the cause

TestRing GCjs:totalConclusion
globe.ringsData = noop68/51 (normal)FlatRing digest retains objects
Ring pool fix (mutate in place)291/263 (normal)Stable ~110MBFixed