hermes-agent/ui-tui/src
Brooklyn Nicholson c370e2e1e5 perf(tui): cache stringWidth/wrapText/sliceAnsi + skip-slice when line fits clip
CPU profile (Apr 2026, real-user scroll on 11k-line session) showed three
hot loops in the per-frame render path:

  Output.get() per-frame walk:                 24% total
  └─ sliceAnsi(line, from, to) per write:     18% total
  stringWidth(line) chain (cached + JS):      14% total

All three were re-doing identical work every frame: same string → same
clipped slice → same width.

Fixes:

1. Memoize stringWidth (8k-entry LRU) for non-ASCII strings; ASCII fast-path
   skips the cache (inline scan beats Map.get for short ASCII, the >90%
   case). String.charCodeAt scan up to 64 chars is cheaper than the regex
   fallback.

2. Memoize wrapText (4k-entry LRU keyed by maxWidth|wrapType|text) — wrapAnsi
   is pure and the same content reflows identically every frame.

3. Memoize sliceAnsi (4k-entry LRU keyed by start|end|str) for the
   end-defined hot path used by Output.get().

4. Skip the slice entirely in Output.get() when the line already fits the
   clip box (startsBefore=false && endsAfter=false). Most transcript lines
   never exceed their container width, and tokenizing them just to slice
   (line, 0, width) was pure overhead. This single fast-path drops
   sliceAnsi from 18% → ~0% in the profile.

Also tighten virtualization constants (MAX_MOUNTED 260→120, OVERSCAN 40→20,
SLIDE_STEP 25→12) and cap historical-message render at 800 chars / 16
lines via HISTORY_RENDER_MAX_*; messages inside the FULL_RENDER_TAIL_ITEMS
window still render in full so reading-zone behavior is unchanged.

Validation, real-user CPU profile, page-up scroll on 11k-line session:

  Output.get() self-time:     24%   →   0.3%
  sliceAnsi total:            18%   →   not in top 25
  stringWidth family:         14%   →   ~3%
  idle:                     60.7%   →  77.3%

Frame timings (synthetic page-up profile harness):
  dur p95:   ~10ms   →  4.87ms
  dur p99:   25ms+   → 12.80ms
  yoga p99:  ~20ms   →  1.87ms

The remaining CPU in the profile is Yoga layoutNode + React commit,
which is the irreducible work for this UI tree size.
2026-04-26 19:28:09 -05:00
..
__tests__ perf(tui): cache stringWidth/wrapText/sliceAnsi + skip-slice when line fits clip 2026-04-26 19:28:09 -05:00
app perf(tui): cache stringWidth/wrapText/sliceAnsi + skip-slice when line fits clip 2026-04-26 19:28:09 -05:00
components perf(tui): cache stringWidth/wrapText/sliceAnsi + skip-slice when line fits clip 2026-04-26 19:28:09 -05:00
config perf(tui): cache stringWidth/wrapText/sliceAnsi + skip-slice when line fits clip 2026-04-26 19:28:09 -05:00
content fix(tui): address Copilot review on editor handoff 2026-04-25 20:34:24 -05:00
domain fix(tui): apply details mode live 2026-04-26 13:34:33 -05:00
hooks perf(tui): cache stringWidth/wrapText/sliceAnsi + skip-slice when line fits clip 2026-04-26 19:28:09 -05:00
lib perf(tui): cache stringWidth/wrapText/sliceAnsi + skip-slice when line fits clip 2026-04-26 19:28:09 -05:00
protocol refactor(tui): /clean pass across ui-tui — 49 files, −217 LOC 2026-04-16 22:32:53 -05:00
types feat(tui): port claude-code's wheel accel state machine 2026-04-26 17:16:11 -05:00
app.tsx fix(tui): apply ui-tui fix pass and restore type-check 2026-04-25 14:08:54 -05:00
banner.ts refactor(tui): /clean pass across ui-tui — 49 files, −217 LOC 2026-04-16 22:32:53 -05:00
entry.tsx feat(tui): HERMES_TUI_FPS=1 shows live fps counter 2026-04-26 17:20:47 -05:00
gatewayClient.ts fix(tui): address PR #13231 review comments 2026-04-20 19:09:09 -05:00
gatewayTypes.ts fix(tui): stabilize live progress rendering 2026-04-26 15:23:43 -05:00
theme.ts feat(tui): subagent spawn observability overlay 2026-04-22 10:38:17 -05:00
types.ts feat(tui): collapse completed todo panel on turn end 2026-04-26 16:24:15 -05:00