perf(desktop): add model-picker open-latency probes

probe-model-picker.mjs times pill-click → menu painted over N rounds;
profile-model-picker.mjs wraps one open in a CPU profile with a
top-self-time table. Both attach to the perf:serve instance.
This commit is contained in:
Brooklyn Nicholson 2026-07-26 01:10:22 -05:00
parent 0b745bc993
commit 7dec2c9640
2 changed files with 140 additions and 0 deletions

View File

@ -0,0 +1,80 @@
// Model picker open latency probe: click the composer model pill, time until
// the dropdown/dialog content paints, repeat. Run with --cpuprofile via the
// harness runner once promoted; standalone for iteration.
// node scripts/perf/probe-model-picker.mjs [--port 9222] [--rounds 5]
import { CDP } 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') ?? 5)
const sleep = ms => new Promise(r => setTimeout(r, ms))
const cdp = await CDP.connect({ port })
await cdp.send('Runtime.enable')
// Find the pill (dropdown path) — the composer model selector button.
const PILL = `(() => {
const btns = [...document.querySelectorAll('button[aria-label]')]
const pill = btns.find(b => /model/i.test(b.getAttribute('aria-label') || '') && b.closest('[data-slot]'))
return pill ? (pill.getAttribute('aria-label') || 'found') : null
})()`
console.log('pill:', await cdp.eval(PILL))
const MEASURE = `
(async () => {
const btns = [...document.querySelectorAll('button[aria-label]')]
const pill = btns.find(b => /model/i.test(b.getAttribute('aria-label') || ''))
if (!pill) return JSON.stringify({ error: 'no pill' })
const t0 = performance.now()
pill.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
pill.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
pill.click()
// Wait for menu/dialog content to exist AND paint (double rAF after found).
const found = await new Promise(resolve => {
const deadline = performance.now() + 5000
const check = () => {
const menu = document.querySelector('[role="menu"], [role="dialog"] [cmdk-list]')
if (menu && menu.childElementCount > 0) {
requestAnimationFrame(() => requestAnimationFrame(() => resolve(performance.now())))
return
}
if (performance.now() > deadline) { resolve(null); return }
requestAnimationFrame(check)
}
check()
})
const openMs = found ? found - t0 : null
const rows = document.querySelectorAll('[role="menu"] [role="menuitem"], [cmdk-item]').length
// Close: Escape.
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
const menu = document.querySelector('[role="menu"], [role="dialog"]')
menu?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
await new Promise(r => setTimeout(r, 300))
return JSON.stringify({ openMs, rows })
})()
`
const samples = []
for (let i = 0; i < rounds; i++) {
const raw = await cdp.eval(MEASURE, { awaitPromise: true })
const r = JSON.parse(raw)
console.log(`round ${i}:`, r)
if (typeof r.openMs === 'number') samples.push(r.openMs)
await sleep(500)
}
samples.sort((a, b) => a - b)
console.log('\nopen latency ms — min/median/max:',
Math.round(samples[0]), '/', Math.round(samples[Math.floor(samples.length / 2)]), '/', Math.round(samples.at(-1)))
cdp.close()

View File

@ -0,0 +1,60 @@
// CPU-profile one model-picker open.
// node scripts/profile-model-picker.mjs [--port 9222]
import { writeFileSync } from 'node:fs'
import { CDP } from './perf/lib/cdp.mjs'
import { cpuProfileTopSelf } from './perf/lib/stats.mjs'
const port = Number(process.argv.includes('--port') ? process.argv[process.argv.indexOf('--port') + 1] : 9222)
const cdp = await CDP.connect({ port })
await cdp.send('Runtime.enable')
await cdp.send('Profiler.enable')
await cdp.send('Profiler.setSamplingInterval', { interval: 100 })
const OPEN = `
(async () => {
// Reset: close any open menu first.
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
await new Promise(r => setTimeout(r, 250))
const btns = [...document.querySelectorAll('button[aria-label]')]
const pill = btns.find(b => /model/i.test(b.getAttribute('aria-label') || ''))
if (!pill) return -1
const t0 = performance.now()
pill.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
pill.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
pill.click()
const found = await new Promise(resolve => {
const deadline = performance.now() + 8000
const check = () => {
const menu = document.querySelector('[role="menu"]')
if (menu && menu.childElementCount > 0) {
requestAnimationFrame(() => requestAnimationFrame(() => resolve(performance.now())))
return
}
if (performance.now() > deadline) { resolve(-1); return }
requestAnimationFrame(check)
}
check()
})
return found < 0 ? -1 : found - t0
})()
`
await cdp.send('Profiler.start')
const openMs = await cdp.eval(OPEN)
const { profile } = await cdp.send('Profiler.stop')
console.log('openMs:', Math.round(openMs))
const out = `/tmp/model-picker-open.cpuprofile`
writeFileSync(out, JSON.stringify(profile))
console.log('wrote', out)
console.log('top self-time (ms):')
for (const r of cpuProfileTopSelf(profile, 20)) {
console.log(` ${r.ms.toFixed(1).padStart(7)} ${r.name.padEnd(44)} ${r.url.split('/').slice(-2).join('/')}:${r.line}`)
}
// Close the menu again.
await cdp.eval(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`)
cdp.close()