Skip to content

perf(ink): replace cache.clear() with LRU eviction in line-width cache#2322

Open
HUQIANTAO wants to merge 1 commit into
esengine:v1from
HUQIANTAO:perf/line-width-lru-eviction
Open

perf(ink): replace cache.clear() with LRU eviction in line-width cache#2322
HUQIANTAO wants to merge 1 commit into
esengine:v1from
HUQIANTAO:perf/line-width-lru-eviction

Conversation

@HUQIANTAO
Copy link
Copy Markdown
Contributor

@HUQIANTAO HUQIANTAO commented May 30, 2026

Summary

The lineWidth cache in packages/ink/src/line-width-cache.ts used cache.clear() when the Map reached 4096 entries, which discards ALL cached widths at once. This creates a sawtooth hit-rate pattern:

  1. Cache slowly fills up with computed widths
  2. At 4096 entries → cache.clear() drops every entry
  3. Next 4096 unique lines are ALL cold-start misses
  4. Repeat

The renderer calls lineWidth() ~100k times per frame with high temporal locality — the same lines appear across consecutive frames.

Fix

Replace cache.clear() with LRU eviction using plain Map insertion-order semantics:

  • On cache hit: delete + set promotes the entry to the end, so it survives future eviction scans
  • On cache miss with full cache: evict only the single oldest entry (first key in iteration order), keeping the other 4095 warm

This is a zero-dependency change — no new imports, no new data structures. Map already provides stable insertion order per the ES2015 spec.

Before/After

// Before (sawtooth — catastrophic cache churn)
if (cache.size >= CACHE_LIMIT) {
  cache.clear();  // drops ALL 4096 entries
}

// After (LRU — gradual, keeps cache warm)
if (cache.size >= CACHE_LIMIT) {
  const oldest = cache.keys().next().value;
  if (oldest !== undefined) cache.delete(oldest);  // evict only ONE
}

Verification

  • TypeScript: tsc --noEmit -p packages/ink/tsconfig.json passes
  • No public API change — lineWidth() signature is unchanged
  • Existing rendering behavior is preserved (only the cache hit rate improves)

The old code called cache.clear() when the Map hit 4096 entries,
discarding ALL cached widths at once. This created a sawtooth hit-rate
pattern: slowly build cache → clear everything → cold-start 4096 misses
→ build again.

Replace with LRU eviction via plain Map insertion-order semantics:
- On hit: delete + re-set promotes the entry (survives next eviction).
- On miss with full cache: evict only the single oldest entry.

The renderer calls lineWidth ~100k times per frame with high temporal
locality, so keeping 4095 warm entries instead of zero makes a
measurable difference.
@esengine esengine added the v1 Legacy TypeScript line (0.x) — v1 branch, maintenance only label May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v1 Legacy TypeScript line (0.x) — v1 branch, maintenance only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants