mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-14 04:02:26 +00:00
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.
This commit is contained in:
parent
85e9a23efb
commit
c370e2e1e5
14 changed files with 450 additions and 42 deletions
58
ui-tui/src/lib/virtualHeights.ts
Normal file
58
ui-tui/src/lib/virtualHeights.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { Msg } from '../types.js'
|
||||
|
||||
import { boundedHistoryRenderText } from './text.js'
|
||||
|
||||
export const hashText = (text: string) => {
|
||||
let h = 5381
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
h = ((h << 5) + h) ^ text.charCodeAt(i)
|
||||
}
|
||||
|
||||
return (h >>> 0).toString(36)
|
||||
}
|
||||
|
||||
export const messageHeightKey = (msg: Msg) =>
|
||||
[msg.role, msg.kind ?? '', hashText([msg.text, msg.thinking ?? '', msg.tools?.join('\n') ?? ''].join('\0'))].join(':')
|
||||
|
||||
export const wrappedLines = (text: string, width: number) => {
|
||||
const w = Math.max(1, width)
|
||||
|
||||
return text.split('\n').reduce((n, line) => n + Math.max(1, Math.ceil(line.length / w)), 0)
|
||||
}
|
||||
|
||||
export const estimatedMsgHeight = (
|
||||
msg: Msg,
|
||||
cols: number,
|
||||
{ compact, details, limitHistory = false }: { compact: boolean; details: boolean; limitHistory?: boolean }
|
||||
) => {
|
||||
if (msg.kind === 'intro') {
|
||||
return msg.info?.version ? 9 : 5
|
||||
}
|
||||
|
||||
if (msg.kind === 'panel') {
|
||||
return Math.max(3, (msg.panelData?.sections.length ?? 1) * 2 + 1)
|
||||
}
|
||||
|
||||
const bodyWidth = Math.max(20, cols - 5)
|
||||
const text = msg.role === 'assistant' && limitHistory ? boundedHistoryRenderText(msg.text) : msg.text
|
||||
let h = wrappedLines(text || ' ', bodyWidth)
|
||||
|
||||
if (!compact && msg.role === 'assistant') {
|
||||
h += Math.min(6, (text.match(/\n\s*\n/g) ?? []).length)
|
||||
}
|
||||
|
||||
if (details) {
|
||||
h += (msg.tools?.length ?? 0) + wrappedLines(msg.thinking ?? '', bodyWidth)
|
||||
}
|
||||
|
||||
if (msg.role === 'user' || msg.kind === 'slash' || msg.kind === 'diff') {
|
||||
h++
|
||||
}
|
||||
|
||||
if (msg.role === 'user' || msg.kind === 'diff') {
|
||||
h++
|
||||
}
|
||||
|
||||
return Math.max(1, h)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue