Skip to content

Bound measureText/wrapText caches (LRU) to stop unbounded growth - #987

Open
fudianchn wants to merge 1 commit into
vadimdemedes:masterfrom
fudianchn:fix-bound-caches-lru
Open

Bound measureText/wrapText caches (LRU) to stop unbounded growth#987
fudianchn wants to merge 1 commit into
vadimdemedes:masterfrom
fudianchn:fix-bound-caches-lru

Conversation

@fudianchn

Copy link
Copy Markdown

Summary

measureText() and wrapText() each keep a module-level cache with no eviction, keyed by the full text:

  • src/measure-text.tsconst cache = new Map()
  • src/wrap-text.tsconst cache = {}

Every distinct string an app ever renders is retained for the lifetime of the process, so any app whose text changes over time (a streaming/typing indicator, a growing log, a clock, a progress line) leaks monotonically until it OOMs (#986). Both caches are fed from the Yoga measure function on every layout pass, so entries accumulate even with renderThrottleMs.

Fix

Replace the unbounded Map/Record with a small bounded LRU cache (src/lru-cache.ts):

  • capped (currently 1000 entries each — recomputation on eviction is cheap),
  • refresh-on-read, so recently-used entries stay hot.

Normal apps (which measure/wrap a bounded set of strings) are unaffected; only ever-growing distinct-string sets are now bounded.

Verification

  • npm run typecheck ✅ (ink's erasableSyntaxOnly tsconfig — note the helper uses an explicit field, not a constructor parameter property)
  • npm run lint (xo) ✅
  • ava ✅ — added test/lru-cache.ts (eviction + refresh-on-read + missing-key recency); existing measure-text/flex-wrap (which exercises wrapText) tests still pass.

This matches the LRU-cap approach validated in the issue (costajhnt: capping the caches flat-lines the retained heap).

Closes #986

measureText() and wrapText() each kept a module-level cache with no
eviction, so every distinct string an app ever rendered was retained for
the lifetime of the process. Apps whose text changes over time (a
streaming/typing indicator, a growing log, a clock, a progress line) thus
leaked monotonically until OOM (vadimdemedes#986).

Replace the unbounded Map/Record with a small bounded LRU cache (capped,
refresh-on-read). Recomputation on eviction is cheap and recently-used
entries stay hot, so normal apps are unaffected; only ever-growing
distinct-string sets are now bounded.

Closes vadimdemedes#986
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.

measureText / wrapText caches are unbounded and never evicted, leaking on every distinct string rendered

1 participant