mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Driving HER real instance (real profile, real transcripts, streams live)
via CDP instead of synthetic tiles finally exposed the remaining stall.
The timeline on a real 60-frame sash drag:
style recalc 2736ms | script 1027ms | layout 89ms
top callsite: pin @ fallback.tsx — 927ms
Two pin-to-bottom ResizeObservers (the bounded tool window's and the
reasoning preview's) pinned on EVERY resize delivery. A sash drag changes
every message's WIDTH once per frame, so each frame ran scrollTop write ->
scrollHeight read across every tool group: a forced write-read reflow
cascade that the render counters could never see (zero React involvement).
Both pins are now height-gated off the RO entry (reflow-free): only
content GROWTH pins. Width-only deliveries return immediately.
Measured on the live app, same drag, before -> after:
fps 11.5 -> 59-60
p95 101ms -> 18ms
slow>33 60/60 -> 1/60
Also in this batch (each was verified live before the next was attempted):
- thread/list: split messageSignature into STRUCTURAL (ids/roles — keys
boundaries + row identity) and WEIGHT (part counts — budget only), and
memoize groups + row JSX. A streamed part-append re-rendered every
turn's boundary via its resetKey prop; explain() measured 540-865
wasted Block renders per drag/stream sample, now {}.
- message-render-boundary: document the structural-only resetKey contract.
- tool/fallback: memoize ToolFallback's part object + ToolEntry/ToolTitle/
ToolGlyph (151 renders each, 100% wasted, on real transcripts).
- use-message-stream: ADAPTIVE flush floor — next flush waits 3x the
measured cost of the last one (33ms floor, 250ms cap), so multi-stream
load degrades text update rate instead of input latency.
- tree-split: preview sash drags with inline flex on the two seam
wrappers, committing the store ONCE on release (fixed-zone sides get
flexBasis only, so a hidden sidebar can't leave a phantom gap).
- debug/: perf-live LoAF long-frame attribution, explain() cascade walker
with changed-hook indices, diag-real-loop/key-latency/switch-trace
probes that drive the real app over CDP.
Typing during 2 live streams: keystroke->paint p50 3.3ms, p95 18.4ms,
zero frames over 33ms. Session switch p50 ~35ms settled; the remaining
~1.3s outlier tail is streaming-session switches (React work-loop, not
style/layout) — next target.
47 lines
2 KiB
JavaScript
47 lines
2 KiB
JavaScript
// Typing latency, isolated: keystroke -> next paint, with and without an
|
|
// active stream. Distinguishes "input is slow" from "the frame budget is
|
|
// consumed by streaming flushes" — the fix differs completely.
|
|
import { attach } from './perf/lib/launch.mjs'
|
|
|
|
const { cdp, teardown } = await attach({ port: 9222 })
|
|
|
|
const TYPE = `
|
|
(async () => {
|
|
const el = [...document.querySelectorAll('[contenteditable="true"]')].find(e => e.offsetParent)
|
|
if (!el) return JSON.stringify({ error: 'no composer' })
|
|
el.focus()
|
|
const perKey = []
|
|
for (let i = 0; i < 30; i++) {
|
|
const ch = 'abcdefghij'[i % 10]
|
|
const t0 = performance.now()
|
|
el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: ch }))
|
|
el.textContent += ch
|
|
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: ch, inputType: 'insertText' }))
|
|
el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: ch }))
|
|
await new Promise(r => requestAnimationFrame(r))
|
|
perKey.push(performance.now() - t0)
|
|
// Human-ish 80ms cadence so streaming flushes interleave realistically.
|
|
await new Promise(r => setTimeout(r, 80))
|
|
}
|
|
el.textContent = ''
|
|
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' }))
|
|
const sorted = [...perKey].sort((a, b) => a - b)
|
|
const pct = p => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))]
|
|
const busy = (() => { try { return document.querySelectorAll('[data-status="running"]').length } catch { return -1 } })()
|
|
return JSON.stringify({
|
|
keyToPaint_p50: Math.round(pct(0.5) * 10) / 10,
|
|
keyToPaint_p95: Math.round(pct(0.95) * 10) / 10,
|
|
worst: Math.round(sorted[sorted.length - 1] * 10) / 10,
|
|
over16: perKey.filter(f => f > 16.7).length,
|
|
over33: perKey.filter(f => f > 33).length,
|
|
streamingParts: busy
|
|
})
|
|
})()
|
|
`
|
|
|
|
try {
|
|
await cdp.send('Runtime.enable')
|
|
console.log(await cdp.eval(TYPE))
|
|
} finally {
|
|
teardown?.()
|
|
}
|