perf(tui): unified Ink cache eviction on memory pressure + session reset

Adds an `evictInkCaches(level)` API that prunes the four hot module-level
caches (`widthCache`, `wrapCache`, `sliceCache`, `lineWidthCache`) with
either a half-keep LRU pass or a full clear. Wired into:

- memoryMonitor: half-prune on 'high', full drop on 'critical', before
  the heap dump / auto-restart path. Gives long sessions a shot at
  recovering RSS instead of hard-exiting.
- useSessionLifecycle.resetSession: half-prune so a /new session starts
  with a half-warm pool and the prior session can resume cheaply.

Also: lineWidthCache now uses LRU half-eviction on overflow instead of a
full `cache.clear()`, matching the other three caches.

Comparison vs claude-code: both forks now share the same `prevScreen`
blit + dirty-cascade machinery in render-node-to-output. Their smoothness
came from sibling-memo discipline (every chrome pane memo'd so dirty
cascade doesn't disable transcript blit) — already in place in our
appLayout.tsx (TranscriptPane / ComposerPane / StatusRulePane all memo'd).
Alt-screen is not the cause; both use it. The remaining gap was per-row
CPU on width/wrap/slice, which the previous commit closed.
This commit is contained in:
Brooklyn Nicholson 2026-04-26 19:41:53 -05:00
parent c370e2e1e5
commit 25767513f2
10 changed files with 147 additions and 4 deletions

View file

@ -0,0 +1,46 @@
// Unified cache eviction for the four hot Ink module-level caches:
// - widthCache (stringWidth.ts)
// - wrapCache (wrap-text.ts)
// - sliceCache (sliceAnsi.ts)
// - lineWidthCache (line-width-cache.ts)
//
// Used by the host (TUI) under memory pressure or on session swap to drop
// content-keyed entries that won't recur. All caches are content-keyed
// (not session-keyed), so cross-session sharing is normally beneficial —
// only evict when memory tightens or when the user explicitly resets.
import { evictLineWidthCache, lineWidthCacheSize } from './line-width-cache.js'
import { evictWidthCache, widthCacheSize } from './stringWidth.js'
import { evictWrapCache, wrapCacheSize } from './wrap-text.js'
import { evictSliceCache, sliceCacheSize } from '../utils/sliceAnsi.js'
export interface InkCacheSizes {
lineWidth: number
slice: number
width: number
wrap: number
}
export function inkCacheSizes(): InkCacheSizes {
return {
lineWidth: lineWidthCacheSize(),
slice: sliceCacheSize(),
width: widthCacheSize(),
wrap: wrapCacheSize()
}
}
export type EvictLevel = 'all' | 'half'
export function evictInkCaches(level: EvictLevel = 'half'): InkCacheSizes {
const before = inkCacheSizes()
const keep = level === 'half' ? 0.5 : 0
evictWidthCache(keep)
evictWrapCache(keep)
evictSliceCache(keep)
evictLineWidthCache(keep)
return before
}