diff --git a/apps/desktop/scripts/probe-model-picker.mjs b/apps/desktop/scripts/probe-model-picker.mjs new file mode 100644 index 0000000000000..d5585a3683518 --- /dev/null +++ b/apps/desktop/scripts/probe-model-picker.mjs @@ -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() diff --git a/apps/desktop/scripts/profile-model-picker.mjs b/apps/desktop/scripts/profile-model-picker.mjs new file mode 100644 index 0000000000000..26e16d6e7c9ef --- /dev/null +++ b/apps/desktop/scripts/profile-model-picker.mjs @@ -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()