Skip to content

fix(text): bound SDF layout cache eagerly; debug overlay uses SDF#78

Merged
chiefcll merged 1 commit into
mainfrom
fix-debug-overlay-canvas-text-leak
Jun 8, 2026
Merged

fix(text): bound SDF layout cache eagerly; debug overlay uses SDF#78
chiefcll merged 1 commit into
mainfrom
fix-debug-overlay-canvas-text-leak

Conversation

@chiefcll

@chiefcll chiefcll commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

The bug

Running the debug overlay over animated text for several minutes on a low-RAM device leaked JS heap until OOM → the renderer's context died → black screen / text stops drawing. Repro: ?test=stress-single-level-text&debug=true&multiplier=10.

Investigation (measured, not guessed)

Sampled performance.memory across controlled variations:

Scenario heap growth
test, no debug ~0.09 MB/s (flat) — the test itself does not leak
test, debug=true ~0.9 MB/s
contextSpy+fps, no overlay flat → ruled out the spy/StatTracker
both fixes below ~0.004 MB/s (flat)

The leak only appears with debug=true, i.e. the debug overlay — not the test.

Fixes

1. Debug overlay → SDF text (examples/index.ts) — the dominant leak (~0.9 MB/s)
The overlay's text node specified no font, so it used the Canvas text renderer, which rasterizes a fresh ImageTexture (a full bitmap, ~390 KB at this size) on every text change (CoreTextNode.ts:254) — and the overlay rewrites its text every interval (~2/s). Forcing fontFamily: 'Ubuntu' + textRendererOverride: 'sdf' makes it draw from the shared atlas with no per-update texture allocation.

2. Bound the SDF layout cache eagerly (SdfTextRenderer.ts) — a real renderer bug
The SDF layout cache only evicted on idle (cleanup, by design "so eviction never competes with rendering"). But a continuously animating scene never goes idle, so any app with ever-changing SDF text — a clock, counter, score, or fps readout — grew the cache without bound. Now it's also bounded on insert: a single Map delete on a cache miss (only when new text is laid out), which doesn't compete with steady-state rendering. Idle cleanup is retained for bulk trimming. Updated the cache test to assert the eager-eviction behavior.

Notes

Testing

  • pnpm build, pnpm lint clean; pnpm test 314/314 (rewrote the SDF cache test for eager eviction).
  • Browser-verified the original repro: ~0.9 MB/s → flat (~0.004 MB/s) over a multi-minute run.

🤖 Generated with Claude Code

Two fixes for a JS-heap leak (OOM / black screen) observed running the debug
overlay over animated text for several minutes on a low-RAM device.

1. examples debug overlay (?debug=true) used the Canvas text renderer (no
   fontFamily/SDF specified). Canvas text rasterizes a fresh ImageTexture (a
   full bitmap) on every text change, and the overlay rewrites its text every
   interval — ~390KB x ~2/s. Force SDF so the per-interval update draws from the
   shared atlas with no texture allocation. This was the dominant leak (~0.9MB/s).

2. The SDF layout cache only evicted on idle. A continuously animating scene
   never goes idle, so any app with ever-changing SDF text (clock, counter,
   score, fps readout) grew the cache without bound. Bound it eagerly on insert
   too — a single Map delete on a cache miss, so it doesn't compete with
   steady-state rendering. Idle cleanup is retained for bulk trimming.

Verified: the original repro (stress-single-level-text&debug=true&multiplier=10)
went from ~0.9 MB/s heap growth to flat (~0.004 MB/s) over a multi-minute run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chiefcll chiefcll merged commit 66be804 into main Jun 8, 2026
1 check passed
@chiefcll chiefcll deleted the fix-debug-overlay-canvas-text-leak branch June 8, 2026 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 [BUG] - CanvasTextRenderer tints color-emoji glyphs with node.color, crushing native emoji colors

1 participant