mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
⌘K is an overlay that is stateful to itself — pressing it owes the user a frame immediately, whatever else the shell is doing. It was not built that way. `CommandPalette` is mounted for the life of the app, and its body ran unconditionally: a dozen store subscriptions (connection, desktop version, client + backend update status/apply, keybinds, worktrees, theme, i18n), three `useQuery`s, and the group builders that assemble a few hundred rows. `<Portal>` renders nothing while closed, so none of it was ever visible — but all of it still ran. An in-flight update rewrites `$updateApply` on every progress line, and each of those rebuilt the entire row set for a surface nobody could see. Split the body into `CommandPaletteBody`, mounted only while the palette is on screen. A closed palette is now one store subscription. The body is keyed by open count, so per-open state (search, sub-page) resets by remount and the explicit close-reset effect goes away, and `mounted` lags `open` by the 150ms exit animation so Radix can still play `data-[state=closed]` instead of the overlay vanishing. Rows additionally move behind `useDeferredValue` in their own memo component. Because that component mounts with the portal, the deferred initial value applies per open: the first commit is the frame + input, and the several-hundred-row list arrives in an interruptible follow-up render rather than blocking the frame the keypress asked for. The empty state is suppressed while rows are still pending so opening doesn't flash "no results". The `enabled: open` gates on the three queries are dropped — the component only exists when open, so they are inherently lazy, and react-query still serves a reopen from cache while revalidating.
146 lines
4.4 KiB
JavaScript
146 lines
4.4 KiB
JavaScript
// ⌘K open latency, measured in-page (no CDP round-trip in the number).
|
|
//
|
|
// node scripts/probe-command-palette.mjs [--port 9222] [--rounds 8]
|
|
//
|
|
// Reports, per round, the time from the keydown the app actually receives to:
|
|
// frame_ms — the dialog frame + input in the DOM and painted (what "instant"
|
|
// means: the overlay owes you a frame immediately)
|
|
// rows_ms — the row list painted (may lag frame_ms; rows are deferred)
|
|
// plus any long tasks in the window, so a slow open is attributable.
|
|
import { CDP, sleep } from './perf/lib/cdp.mjs'
|
|
|
|
const args = process.argv.slice(2)
|
|
const flag = name => {
|
|
const i = args.indexOf(`--${name}`)
|
|
|
|
return i >= 0 ? args[i + 1] : undefined
|
|
}
|
|
|
|
const port = Number(flag('port') ?? 9222)
|
|
const rounds = Number(flag('rounds') ?? 8)
|
|
|
|
const cdp = await CDP.connect({ port })
|
|
|
|
await cdp.send('Runtime.enable')
|
|
|
|
const INSTALL = `
|
|
(() => {
|
|
if (window.__CMDK__) window.__CMDK__.stop()
|
|
|
|
const state = { t0: null, frame: null, rows: 0, rowsAt: null, tasks: [], armed: false }
|
|
|
|
// Time from the keydown the APP receives — excludes CDP transport, so the
|
|
// number is what a user's finger actually experiences.
|
|
const onKey = e => {
|
|
if (state.armed && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
state.t0 = performance.now()
|
|
state.armed = false
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', onKey, true)
|
|
|
|
const obs = new MutationObserver(() => {
|
|
if (state.t0 === null) return
|
|
if (state.frame === null && document.querySelector('[cmdk-input]')) {
|
|
state.frame = performance.now() - state.t0
|
|
}
|
|
const n = document.querySelectorAll('[cmdk-item]').length
|
|
if (n > state.rows) { state.rows = n; state.rowsAt = performance.now() - state.t0 }
|
|
})
|
|
|
|
obs.observe(document.body, { childList: true, subtree: true })
|
|
|
|
const po = new PerformanceObserver(list => {
|
|
for (const e of list.getEntries()) state.tasks.push({ start: e.startTime, dur: Math.round(e.duration) })
|
|
})
|
|
|
|
try { po.observe({ entryTypes: ['longtask'] }) } catch {}
|
|
|
|
window.__CMDK__ = {
|
|
arm: () => { state.t0 = null; state.frame = null; state.rows = 0; state.rowsAt = null; state.tasks = []; state.armed = true },
|
|
read: () => ({
|
|
frame_ms: state.frame === null ? -1 : Math.round(state.frame),
|
|
rows_ms: state.rowsAt === null ? -1 : Math.round(state.rowsAt),
|
|
rows: state.rows,
|
|
longtask_ms: state.t0 === null ? 0 : state.tasks.filter(t => t.start >= state.t0).reduce((s, t) => s + t.dur, 0)
|
|
}),
|
|
stop: () => { window.removeEventListener('keydown', onKey, true); obs.disconnect(); po.disconnect() }
|
|
}
|
|
|
|
return true
|
|
})()
|
|
`
|
|
|
|
// Settle: frame painted AND rows stopped growing for two frames.
|
|
const WAIT = `
|
|
new Promise(resolve => {
|
|
let stable = 0
|
|
let last = -1
|
|
const started = performance.now()
|
|
const tick = () => {
|
|
const r = window.__CMDK__.read()
|
|
if (r.frame_ms >= 0 && r.rows === last && r.rows > 0) {
|
|
if (++stable >= 2) { resolve(r); return }
|
|
} else { stable = 0 }
|
|
last = r.rows
|
|
if (performance.now() - started > 8000) { resolve(window.__CMDK__.read()); return }
|
|
requestAnimationFrame(tick)
|
|
}
|
|
requestAnimationFrame(tick)
|
|
})
|
|
`
|
|
|
|
const key = async type =>
|
|
cdp.send('Input.dispatchKeyEvent', {
|
|
type,
|
|
key: 'k',
|
|
code: 'KeyK',
|
|
windowsVirtualKeyCode: 75,
|
|
nativeVirtualKeyCode: 75,
|
|
modifiers: 4
|
|
})
|
|
|
|
const esc = async () => {
|
|
for (const type of ['keyDown', 'keyUp']) {
|
|
await cdp.send('Input.dispatchKeyEvent', { type, key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 })
|
|
}
|
|
|
|
await sleep(400)
|
|
}
|
|
|
|
await cdp.eval(INSTALL)
|
|
await esc()
|
|
|
|
const samples = []
|
|
|
|
for (let i = 0; i < rounds; i++) {
|
|
await sleep(250)
|
|
await cdp.eval('window.__CMDK__.arm()')
|
|
await key('rawKeyDown')
|
|
await key('keyUp')
|
|
const r = await cdp.eval(WAIT)
|
|
samples.push(r)
|
|
console.log(`round ${i}:`, r)
|
|
await esc()
|
|
}
|
|
|
|
await cdp.eval('window.__CMDK__.stop()')
|
|
|
|
const stat = k => {
|
|
const v = samples.map(s => s[k]).filter(n => n >= 0).sort((a, b) => a - b)
|
|
|
|
if (!v.length) return null
|
|
|
|
return {
|
|
min: v[0],
|
|
median: v[Math.floor(v.length / 2)],
|
|
max: v[v.length - 1]
|
|
}
|
|
}
|
|
|
|
console.log('\nkeydown → dialog frame painted (ms):', stat('frame_ms'))
|
|
console.log('keydown → rows painted (ms):', stat('rows_ms'))
|
|
console.log('long-task time in window (ms):', stat('longtask_ms'))
|
|
|
|
cdp.close()
|