mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-15 04:12:25 +00:00
chore(tui): /clean recent perf work — KISS/DRY pass
24 files, -319 LoC. Behaviour preserved, 369/369 tests green. - hermes-ink caches: shared lruEvict helper for the four parallel LRU caches (stringWidth, wrapText, sliceAnsi, lineWidth); touch-on-read stays inlined per cache; tightened output.ts skip-slice fast path. - wheelAccel: trimmed provenance header, collapsed env parsing, ternary dispatch in computeWheelStep. - perfPane: folded ensureLogDir into once-flag, spread-with-overrides for fastPath/phases instead of full rebuilds. - env: extracted truthy() (used 4×). - virtualHeights: collapsed user/diff/slash height bumps; trail+todos estimate. - useInputHandlers: scrollIdleTimer cleanup on unmount, ?? undefined shorthand. - useMainApp: dropped dead liveTailVisible IIFE and liveProgress indirection. - appLayout, markdown, messageLine, entry: vertical rhythm, dropped narration comments, inlined one-shot vars. - fix: empty catch blocks → /* best-effort */ for no-empty lint.
This commit is contained in:
parent
527ac351b4
commit
b1c49d5e73
32 changed files with 259 additions and 547 deletions
|
|
@ -1,43 +1,15 @@
|
|||
// Perf instrumentation for the full render pipeline.
|
||||
//
|
||||
// Two sources of timing:
|
||||
// 1. React.Profiler wrapper (PerfPane) → per-pane commit times. Shows
|
||||
// which subtree is reconciling and for how long.
|
||||
// 2. Ink onFrame callback (logFrameEvent) → per-frame pipeline phases:
|
||||
// yoga (calculateLayout), renderer (DOM → screen buffer), diff
|
||||
// (prev vs current screen → patches), optimize (patch merge/dedupe),
|
||||
// write (serialize → ANSI → stdout), plus yoga counters (visited,
|
||||
// measured, cacheHits, live). Shows where the time goes BELOW React.
|
||||
// PerfPane (React.Profiler) → per-pane commit times
|
||||
// logFrameEvent (ink.onFrame) → yoga / renderer / diff / optimize / write
|
||||
// phases + yoga counters + scroll fast-path
|
||||
//
|
||||
// Both sources gate on HERMES_DEV_PERF=1 and dump JSON-lines to the same
|
||||
// log (default ~/.hermes/perf.log, override via HERMES_DEV_PERF_LOG).
|
||||
// Events are tagged { src: 'react' | 'frame' } so jq can split them.
|
||||
// Both gate on HERMES_DEV_PERF=1 and dump JSON-lines (default ~/.hermes/perf.log,
|
||||
// override HERMES_DEV_PERF_LOG). Tagged { src: 'react' | 'frame' } for jq.
|
||||
// HERMES_DEV_PERF_MS (default 2) skips sub-ms idle frames; set 0 to capture all.
|
||||
//
|
||||
// Threshold HERMES_DEV_PERF_MS (default 2ms) skips sub-millisecond idle
|
||||
// frames. For the 2fps-during-PageUp investigation, set
|
||||
// HERMES_DEV_PERF_MS=0 to capture everything, then filter with jq.
|
||||
//
|
||||
// Zero cost when the env var is unset: PerfPane returns children
|
||||
// directly (no Profiler fiber), logFrameEvent is a noop on the onFrame
|
||||
// callback — the ink instance isn't given the callback at all.
|
||||
//
|
||||
// Usage:
|
||||
// # entry.tsx wires logFrameEvent into render()
|
||||
// import { logFrameEvent, PerfPane } from './lib/perfPane.js'
|
||||
// render(<App/>, { onFrame: logFrameEvent })
|
||||
//
|
||||
// Analysis helpers (once you've captured a session):
|
||||
// tail -f ~/.hermes/perf.log | jq -c 'select(.src=="frame" and .durationMs > 16)'
|
||||
// # p50/p99 per phase across frame events:
|
||||
// jq -s '[.[] | select(.src=="frame")] |
|
||||
// {n: length,
|
||||
// dur_p50: (sort_by(.durationMs) | .[length/2|floor].durationMs),
|
||||
// dur_p99: (sort_by(.durationMs) | .[length*0.99|floor].durationMs),
|
||||
// yoga_p99: (sort_by(.phases.yoga) | .[length*0.99|floor].phases.yoga),
|
||||
// write_p99: (sort_by(.phases.write) | .[length*0.99|floor].phases.write),
|
||||
// diff_p99: (sort_by(.phases.diff) | .[length*0.99|floor].phases.diff),
|
||||
// patches_p99: (sort_by(.phases.patches) | .[length*0.99|floor].phases.patches)}' \
|
||||
// ~/.hermes/perf.log
|
||||
// Zero cost when unset: PerfPane returns children directly, logFrameEvent is
|
||||
// undefined so ink doesn't pay the timing cost.
|
||||
|
||||
import { appendFileSync, mkdirSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
|
|
@ -51,31 +23,23 @@ const ENABLED = /^(?:1|true|yes|on)$/i.test((process.env.HERMES_DEV_PERF ?? '').
|
|||
const THRESHOLD_MS = Number(process.env.HERMES_DEV_PERF_MS ?? '2') || 0
|
||||
const LOG_PATH = process.env.HERMES_DEV_PERF_LOG?.trim() || join(homedir(), '.hermes', 'perf.log')
|
||||
|
||||
let initialized = false
|
||||
|
||||
const ensureLogDir = () => {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
initialized = true
|
||||
|
||||
try {
|
||||
mkdirSync(dirname(LOG_PATH), { recursive: true })
|
||||
} catch {
|
||||
// Best-effort — if we can't create the dir (readonly fs, /tmp, etc.)
|
||||
// the appendFileSync calls below will throw silently and we drop the
|
||||
// sample. Perf logging should never crash the TUI.
|
||||
}
|
||||
}
|
||||
let logReady = false
|
||||
|
||||
const writeRow = (row: Record<string, unknown>) => {
|
||||
ensureLogDir()
|
||||
if (!logReady) {
|
||||
logReady = true
|
||||
|
||||
try {
|
||||
mkdirSync(dirname(LOG_PATH), { recursive: true })
|
||||
} catch {
|
||||
// Best-effort — never crash the TUI to log a sample.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
appendFileSync(LOG_PATH, `${JSON.stringify(row)}\n`)
|
||||
} catch {
|
||||
// Same rationale as ensureLogDir — never crash the UI to log a sample.
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,59 +74,27 @@ export function PerfPane({ children, id }: { children: ReactNode; id: string })
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ink onFrame handler. Captures the FULL render pipeline: yoga calculateLayout,
|
||||
* DOM → screen buffer, screen diff, patch optimize, and stdout write.
|
||||
*
|
||||
* Returns `undefined` when disabled so `render()` doesn't attach the callback —
|
||||
* ink only pays the timing cost when the callback is truthy.
|
||||
*/
|
||||
export const logFrameEvent = ENABLED
|
||||
? (event: FrameEvent) => {
|
||||
if (event.durationMs < THRESHOLD_MS) {
|
||||
return
|
||||
}
|
||||
|
||||
// Snapshot the fast-path counters each frame. Cumulative values —
|
||||
// consumers diff pairs to get per-frame deltas. Written verbatim
|
||||
// so we can also see "last*" fields (which decline reason fired,
|
||||
// and what the height math looked like).
|
||||
const fastPath = {
|
||||
captured: scrollFastPathStats.captured,
|
||||
taken: scrollFastPathStats.taken,
|
||||
declined: {
|
||||
heightDeltaMismatch: scrollFastPathStats.declined.heightDeltaMismatch,
|
||||
noPrevScreen: scrollFastPathStats.declined.noPrevScreen,
|
||||
other: scrollFastPathStats.declined.other
|
||||
},
|
||||
lastDeclineReason: scrollFastPathStats.lastDeclineReason,
|
||||
lastHeightDelta: scrollFastPathStats.lastHeightDelta,
|
||||
lastHintDelta: scrollFastPathStats.lastHintDelta,
|
||||
lastPrevHeight: scrollFastPathStats.lastPrevHeight,
|
||||
lastScrollHeight: scrollFastPathStats.lastScrollHeight
|
||||
}
|
||||
|
||||
writeRow({
|
||||
durationMs: round2(event.durationMs),
|
||||
fastPath,
|
||||
// Cumulative counters — consumers diff pairs to get per-frame deltas.
|
||||
fastPath: { ...scrollFastPathStats, declined: { ...scrollFastPathStats.declined } },
|
||||
flickers: event.flickers.length ? event.flickers : undefined,
|
||||
phases: event.phases
|
||||
? {
|
||||
backpressure: event.phases.backpressure,
|
||||
...event.phases,
|
||||
commit: round2(event.phases.commit),
|
||||
diff: round2(event.phases.diff),
|
||||
optimize: round2(event.phases.optimize),
|
||||
optimizedPatches: event.phases.optimizedPatches,
|
||||
patches: event.phases.patches,
|
||||
prevFrameDrainMs: round2(event.phases.prevFrameDrainMs),
|
||||
renderer: round2(event.phases.renderer),
|
||||
write: round2(event.phases.write),
|
||||
writeBytes: event.phases.writeBytes,
|
||||
yoga: round2(event.phases.yoga),
|
||||
yogaCacheHits: event.phases.yogaCacheHits,
|
||||
yogaLive: event.phases.yogaLive,
|
||||
yogaMeasured: event.phases.yogaMeasured,
|
||||
yogaVisited: event.phases.yogaVisited
|
||||
yoga: round2(event.phases.yoga)
|
||||
}
|
||||
: undefined,
|
||||
src: 'frame',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue