mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
305 lines
8.7 KiB
TypeScript
305 lines
8.7 KiB
TypeScript
// Live interaction profiler — for driving the app by hand and seeing what the
|
|
// app actually does under YOUR sessions, not a synthetic scenario's.
|
|
//
|
|
// The synthetic scenarios seed toy transcripts (short prose, no tool calls, no
|
|
// code blocks). A real session is heavier in ways that matter, so a scenario
|
|
// can report 57fps on a gesture that visibly lags in the real app. This closes
|
|
// that gap by measuring the real thing.
|
|
//
|
|
// It arms itself on any pointerdown on a resize handle and on composer typing,
|
|
// records frames + render attribution for the duration of the interaction, and
|
|
// prints a table when the interaction ends. Idle cost is zero: nothing is
|
|
// observed until an interaction starts.
|
|
//
|
|
// window.__PERF_LIVE__.on() start watching (also: ?perflive=1)
|
|
// window.__PERF_LIVE__.off()
|
|
// window.__PERF_LIVE__.last() the most recent report as an object
|
|
//
|
|
// Dev-only; the whole debug/ graph is aliased out of production builds.
|
|
|
|
interface Sample {
|
|
kind: string
|
|
ms: number
|
|
frames: number
|
|
fps: number
|
|
p95: number
|
|
worst: number
|
|
slow33: number
|
|
commits: number
|
|
top: Array<{ name: string; renders: number; wasted: number; totalMs: number }>
|
|
longFrames: LongFrame[]
|
|
}
|
|
|
|
/** One Long Animation Frame, attributed. `styleMs` is the engine's style+layout
|
|
* time inside the frame; `scripts` names who ran JS and for how long. This is
|
|
* the half the render counter cannot see — a frame can cost 900ms with almost
|
|
* no React in it, and only LoAF says whether that was layout, a ResizeObserver
|
|
* callback loop, or some timer. */
|
|
interface LongFrame {
|
|
ms: number
|
|
styleMs: number
|
|
blockingMs: number
|
|
scripts: Array<{ invoker: string; ms: number; src: string }>
|
|
}
|
|
|
|
const RESIZE_SELECTOR =
|
|
'[role="separator"], [data-slot="pane-resize-handle"], [class*="cursor-col-resize"], [class*="cursor-row-resize"]'
|
|
const TYPING_SELECTOR = '[contenteditable="true"], textarea, input[type="text"]'
|
|
|
|
// A gesture is "over" once this long passes with no further input events.
|
|
const IDLE_END_MS = 350
|
|
// Don't report trivial blips (a click, a single keypress).
|
|
const MIN_FRAMES = 6
|
|
|
|
let watching = false
|
|
|
|
let active: null | {
|
|
kind: string
|
|
startedAt: number
|
|
frames: number[]
|
|
last: number
|
|
raf: number
|
|
endTimer: ReturnType<typeof setTimeout> | null
|
|
} = null
|
|
|
|
let lastReport: null | Sample = null
|
|
|
|
// Long Animation Frames observed while a gesture is active. LoAF entries name
|
|
// the scripts inside each long frame and split out style/layout time — the
|
|
// half of a frame the render counter cannot see.
|
|
let longFrames: LongFrame[] = []
|
|
|
|
const loafObserver =
|
|
typeof PerformanceObserver !== 'undefined' &&
|
|
PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')
|
|
? new PerformanceObserver(list => {
|
|
if (!active) {
|
|
return
|
|
}
|
|
|
|
for (const entry of list.getEntries()) {
|
|
const e = entry as PerformanceEntry & {
|
|
blockingDuration?: number
|
|
styleAndLayoutStart?: number
|
|
renderStart?: number
|
|
scripts?: Array<{
|
|
duration: number
|
|
invoker?: string
|
|
invokerType?: string
|
|
sourceURL?: string
|
|
sourceFunctionName?: string
|
|
}>
|
|
}
|
|
|
|
longFrames.push({
|
|
blockingMs: Math.round(e.blockingDuration ?? 0),
|
|
ms: Math.round(e.duration),
|
|
scripts: (e.scripts ?? [])
|
|
.filter(s => s.duration >= 5)
|
|
.map(s => ({
|
|
invoker: `${s.invokerType ?? ''}:${s.invoker ?? s.sourceFunctionName ?? '?'}`,
|
|
ms: Math.round(s.duration),
|
|
src: (s.sourceURL ?? '').split('/').pop() ?? ''
|
|
})),
|
|
// styleAndLayoutStart -> frame end is the engine's style+layout tail.
|
|
styleMs: e.styleAndLayoutStart ? Math.round(e.startTime + e.duration - e.styleAndLayoutStart) : 0
|
|
})
|
|
}
|
|
})
|
|
: null
|
|
|
|
const pct = (sorted: number[], p: number) =>
|
|
sorted.length ? sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))] : 0
|
|
|
|
function finish() {
|
|
if (!active) {
|
|
return
|
|
}
|
|
|
|
const { kind, startedAt, frames, raf } = active
|
|
active = null
|
|
cancelAnimationFrame(raf)
|
|
loafObserver?.disconnect()
|
|
const capturedLongFrames = longFrames
|
|
longFrames = []
|
|
|
|
const counter = window.__RENDER_COUNTS__
|
|
const commits = counter?.commits() ?? 0
|
|
|
|
const top = (counter?.report(12) ?? []).map(r => ({
|
|
name: r.name,
|
|
renders: r.renders,
|
|
wasted: r.wasted,
|
|
totalMs: r.totalMs
|
|
}))
|
|
|
|
counter?.stop()
|
|
|
|
if (frames.length < MIN_FRAMES) {
|
|
return
|
|
}
|
|
|
|
const total = frames.reduce((a, b) => a + b, 0)
|
|
const sorted = [...frames].sort((a, b) => a - b)
|
|
|
|
const report: Sample = {
|
|
commits,
|
|
fps: Math.round((frames.length / total) * 1000 * 10) / 10,
|
|
frames: frames.length,
|
|
kind,
|
|
longFrames: capturedLongFrames,
|
|
ms: Math.round(performance.now() - startedAt),
|
|
p95: Math.round(pct(sorted, 0.95) * 10) / 10,
|
|
slow33: frames.filter(f => f > 33).length,
|
|
top,
|
|
worst: Math.round(sorted[sorted.length - 1] * 10) / 10
|
|
}
|
|
|
|
lastReport = report
|
|
|
|
const headline =
|
|
`%c${kind}%c ${report.fps}fps · ${report.frames} frames in ${report.ms}ms · ` +
|
|
`p95 ${report.p95}ms worst ${report.worst}ms · ${report.slow33} slow · ${commits} commits`
|
|
|
|
console.log(
|
|
headline,
|
|
`background:${report.fps < 45 ? '#c0392b' : '#27ae60'};color:#fff;padding:1px 6px;border-radius:3px`,
|
|
'color:inherit'
|
|
)
|
|
|
|
if (top.length) {
|
|
console.table(top)
|
|
}
|
|
|
|
// The frame-engine side: what each long frame actually spent its time on.
|
|
for (const lf of capturedLongFrames.slice(0, 8)) {
|
|
const scripts = lf.scripts.map(s => `${s.invoker}@${s.src} ${s.ms}ms`).join(' | ') || '(no script ≥5ms)'
|
|
console.log(` ⏱ longframe ${lf.ms}ms style+layout ${lf.styleMs}ms block ${lf.blockingMs}ms → ${scripts}`)
|
|
}
|
|
}
|
|
|
|
function begin(kind: string) {
|
|
if (active) {
|
|
// Same gesture continuing — just push the end deadline out.
|
|
if (active.endTimer) {
|
|
clearTimeout(active.endTimer)
|
|
}
|
|
|
|
active.endTimer = setTimeout(finish, IDLE_END_MS)
|
|
|
|
return
|
|
}
|
|
|
|
window.__RENDER_COUNTS__?.start()
|
|
|
|
try {
|
|
// Buffered so a long frame already in flight when the gesture starts is
|
|
// still attributed to it.
|
|
loafObserver?.observe({ buffered: true, type: 'long-animation-frame' })
|
|
} catch {
|
|
// Older runtime without LoAF — headline still works, attribution is empty.
|
|
}
|
|
|
|
const now = performance.now()
|
|
|
|
const state = {
|
|
endTimer: setTimeout(finish, IDLE_END_MS) as ReturnType<typeof setTimeout> | null,
|
|
frames: [] as number[],
|
|
kind,
|
|
last: now,
|
|
raf: 0,
|
|
startedAt: now
|
|
}
|
|
|
|
const tick = () => {
|
|
if (active !== state) {
|
|
return
|
|
}
|
|
|
|
const t = performance.now()
|
|
state.frames.push(t - state.last)
|
|
state.last = t
|
|
state.raf = requestAnimationFrame(tick)
|
|
}
|
|
|
|
active = state
|
|
state.raf = requestAnimationFrame(tick)
|
|
}
|
|
|
|
function onPointerDown(event: PointerEvent) {
|
|
const target = event.target as Element | null
|
|
|
|
if (target?.closest?.(RESIZE_SELECTOR)) {
|
|
begin('resize')
|
|
}
|
|
}
|
|
|
|
function onPointerMove() {
|
|
if (active?.kind === 'resize') {
|
|
begin('resize')
|
|
}
|
|
}
|
|
|
|
function onKeyDown(event: KeyboardEvent) {
|
|
const target = event.target as Element | null
|
|
|
|
// Ignore pure modifiers and navigation — we want real text entry.
|
|
if (event.key.length !== 1 && event.key !== 'Backspace') {
|
|
return
|
|
}
|
|
|
|
if (target?.closest?.(TYPING_SELECTOR)) {
|
|
begin('typing')
|
|
}
|
|
}
|
|
|
|
function on() {
|
|
if (watching) {
|
|
return 'already watching'
|
|
}
|
|
|
|
watching = true
|
|
window.addEventListener('pointerdown', onPointerDown, true)
|
|
window.addEventListener('pointermove', onPointerMove, true)
|
|
window.addEventListener('keydown', onKeyDown, true)
|
|
|
|
console.log(
|
|
'%cperf-live%c armed — resize a pane or type in the composer; a report prints when you stop.',
|
|
'background:#5a7db0;color:#fff;padding:1px 6px;border-radius:3px',
|
|
'color:inherit'
|
|
)
|
|
|
|
return 'watching'
|
|
}
|
|
|
|
function off() {
|
|
watching = false
|
|
window.removeEventListener('pointerdown', onPointerDown, true)
|
|
window.removeEventListener('pointermove', onPointerMove, true)
|
|
window.removeEventListener('keydown', onKeyDown, true)
|
|
finish()
|
|
|
|
return 'stopped'
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
__PERF_LIVE__?: {
|
|
on: () => string
|
|
off: () => string
|
|
last: () => null | Sample
|
|
watching: () => boolean
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof window !== 'undefined' && !window.__PERF_LIVE__) {
|
|
window.__PERF_LIVE__ = { last: () => lastReport, off, on, watching: () => watching }
|
|
|
|
// Opt in for a whole session with ?perflive=1 so a reload keeps measuring.
|
|
if (new URLSearchParams(window.location.search).get('perflive') === '1') {
|
|
on()
|
|
}
|
|
}
|
|
|
|
export {}
|