mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
chore: full overlay render-churn measurement probe
This commit is contained in:
parent
1073ae2b46
commit
efe76378c2
1 changed files with 201 additions and 0 deletions
201
apps/desktop/scripts/diag-overlay-full.mjs
Normal file
201
apps/desktop/scripts/diag-overlay-full.mjs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env node
|
||||
// Comprehensive overlay render-churn measurement on the live instance.
|
||||
// Measures: cmdk root + submenus, settings, command center, capabilities, system overlays.
|
||||
// Read-only — never closes the app, only navigates and clicks.
|
||||
import WebSocket from 'ws'
|
||||
|
||||
const WS_URL = 'ws://127.0.0.1:9222/devtools/page/6E095DBE024BD280C674D00023C01201'
|
||||
let msgId = 1
|
||||
|
||||
function send(ws, method, params = {}) {
|
||||
const id = msgId++
|
||||
return new Promise((resolve, reject) => {
|
||||
const handler = (data) => {
|
||||
const msg = JSON.parse(data.toString())
|
||||
if (msg.id === id) {
|
||||
ws.off('message', handler)
|
||||
msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result)
|
||||
}
|
||||
}
|
||||
ws.on('message', handler)
|
||||
ws.send(JSON.stringify({ id, method, params }))
|
||||
})
|
||||
}
|
||||
|
||||
async function eval_(ws, expr) {
|
||||
const r = await send(ws, 'Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
|
||||
return r?.result?.value
|
||||
}
|
||||
|
||||
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
|
||||
|
||||
async function measure(ws, label, fn, settleMs = 400) {
|
||||
await eval_(ws, `__RENDER_COUNTS__.start(); true`)
|
||||
await fn(ws)
|
||||
await sleep(settleMs)
|
||||
const report = await eval_(ws, `JSON.stringify(__RENDER_COUNTS__.report())`)
|
||||
const parsed = JSON.parse(report)
|
||||
const totalRenders = parsed.reduce((s, c) => s + c.renders, 0)
|
||||
const totalWasted = parsed.reduce((s, c) => s + c.wasted, 0)
|
||||
const totalMs = parsed.reduce((s, c) => s + c.totalMs, 0)
|
||||
const top5 = parsed.filter(c => c.wasted > 0).sort((a,b) => b.wasted - a.wasted).slice(0, 5)
|
||||
console.log(`\n${label}`)
|
||||
console.log(` total: ${totalRenders} renders, ${totalWasted} wasted, ${totalMs.toFixed(1)}ms`)
|
||||
if (top5.length > 0) {
|
||||
for (const c of top5) {
|
||||
console.log(` ${c.name}: ${c.renders} renders, ${c.wasted} wasted, ${c.totalMs.toFixed(1)}ms`)
|
||||
}
|
||||
} else {
|
||||
console.log(` (no wasted renders)`)
|
||||
}
|
||||
return { label, totalRenders, totalWasted, totalMs, top: top5 }
|
||||
}
|
||||
|
||||
async function clickByText(ws, text, tag = 'button') {
|
||||
return eval_(ws, `
|
||||
const el = Array.from(document.querySelectorAll('${tag}')).find(b => b.textContent?.trim() === '${text}')
|
||||
if (el) { el.click(); true } else false
|
||||
`)
|
||||
}
|
||||
|
||||
async function clickByHref(ws, partial) {
|
||||
return eval_(ws, `
|
||||
const el = Array.from(document.querySelectorAll('a,button')).find(e => e.getAttribute('href')?.includes('${partial}'))
|
||||
if (el) { el.click(); true } else false
|
||||
`)
|
||||
}
|
||||
|
||||
async function typeInInput(ws, text) {
|
||||
for (const ch of text) {
|
||||
await eval_(ws, `
|
||||
const el = document.querySelector('[cmdk-input]') || document.querySelector('input[type="text"]') || document.querySelector('input')
|
||||
if (el) { el.focus(); el.value = el.value + '${ch}'; el.dispatchEvent(new Event('input', {bubbles:true})) }
|
||||
true
|
||||
`)
|
||||
await sleep(50)
|
||||
}
|
||||
}
|
||||
|
||||
async function pressKey(ws, key, mods = {}) {
|
||||
await eval_(ws, `
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: '${key}', ${Object.entries(mods).map(([k,v]) => `${k}:${v}`).join(', ')}, bubbles: true }))
|
||||
true
|
||||
`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ws = new WebSocket(WS_URL)
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.on('open', resolve)
|
||||
ws.on('error', reject)
|
||||
})
|
||||
await send(ws, 'Runtime.enable')
|
||||
|
||||
const avail = await eval_(ws, `typeof __RENDER_COUNTS__`)
|
||||
console.log(`__RENDER_COUNTS__: ${avail}`)
|
||||
if (avail !== 'object') {
|
||||
console.error('Render counter not loaded. Reload the page.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const results = []
|
||||
|
||||
// === 1. CmdK root: open, type "setting", close ===
|
||||
results.push(await measure(ws, '1. CmdK root (type "setting")', async (ws) => {
|
||||
await pressKey(ws, 'k', { metaKey: true })
|
||||
await sleep(300)
|
||||
await typeInInput(ws, 'setting')
|
||||
}))
|
||||
await pressKey(ws, 'Escape')
|
||||
await sleep(300)
|
||||
|
||||
// === 2. CmdK submenu: open, navigate to theme picker, click a theme, back ===
|
||||
results.push(await measure(ws, '2. CmdK theme submenu', async (ws) => {
|
||||
await pressKey(ws, 'k', { metaKey: true })
|
||||
await sleep(300)
|
||||
await typeInInput(ws, 'theme')
|
||||
await sleep(200)
|
||||
// Click first theme item
|
||||
await eval_(ws, `
|
||||
const item = document.querySelector('[cmdk-item]')
|
||||
if (item) item.click()
|
||||
true
|
||||
`)
|
||||
await sleep(300)
|
||||
}))
|
||||
await pressKey(ws, 'Escape')
|
||||
await sleep(300)
|
||||
|
||||
// === 3. CmdK color-mode submenu ===
|
||||
results.push(await measure(ws, '3. CmdK color-mode submenu', async (ws) => {
|
||||
await pressKey(ws, 'k', { metaKey: true })
|
||||
await sleep(300)
|
||||
await typeInInput(ws, 'color mode')
|
||||
await sleep(200)
|
||||
await eval_(ws, `const item = document.querySelector('[cmdk-item]'); if (item) item.click(); true`)
|
||||
await sleep(300)
|
||||
}))
|
||||
await pressKey(ws, 'Escape')
|
||||
await sleep(300)
|
||||
|
||||
// === 4. Settings: open, click through nav items ===
|
||||
results.push(await measure(ws, '4. Settings (5 nav clicks)', async (ws) => {
|
||||
await clickByHref(ws, 'settings')
|
||||
await sleep(500)
|
||||
for (const label of ['Appearance', 'Gateway', 'Keys', 'Notifications', 'About']) {
|
||||
await clickByText(ws, label)
|
||||
await sleep(100)
|
||||
}
|
||||
}))
|
||||
|
||||
// === 5. Command Center: open to System tab, idle 2s ===
|
||||
results.push(await measure(ws, '5. Command Center (System tab, 2s idle)', async (ws) => {
|
||||
await clickByHref(ws, 'command-center')
|
||||
await sleep(500)
|
||||
await clickByText(ws, 'System')
|
||||
await sleep(2000)
|
||||
}, 100))
|
||||
|
||||
// === 6. Command Center: Sessions tab ===
|
||||
results.push(await measure(ws, '6. Command Center (Sessions tab, 1s)', async (ws) => {
|
||||
await clickByText(ws, 'Sessions')
|
||||
await sleep(1000)
|
||||
}, 100))
|
||||
|
||||
// === 7. Capabilities/Skills page ===
|
||||
results.push(await measure(ws, '7. Capabilities (Skills tab, 1s)', async (ws) => {
|
||||
await clickByHref(ws, 'skills')
|
||||
await sleep(800)
|
||||
// Click Skills tab if not already
|
||||
await clickByText(ws, 'Skills')
|
||||
await sleep(1000)
|
||||
}, 100))
|
||||
|
||||
// === 8. Capabilities Toolsets tab ===
|
||||
results.push(await measure(ws, '8. Capabilities (Toolsets tab, 1s)', async (ws) => {
|
||||
await clickByText(ws, 'Tools')
|
||||
await sleep(1000)
|
||||
}, 100))
|
||||
|
||||
// === 9. Capabilities MCP tab ===
|
||||
results.push(await measure(ws, '9. Capabilities (MCP tab, 1s)', async (ws) => {
|
||||
await clickByText(ws, 'MCP')
|
||||
await sleep(1000)
|
||||
}, 100))
|
||||
|
||||
// Navigate back to chat
|
||||
await eval_(ws, `window.location.hash = '#/'; true`)
|
||||
await sleep(300)
|
||||
|
||||
// Summary table
|
||||
console.log('\n\n=== SUMMARY ===')
|
||||
console.log('Surface'.padEnd(45) + 'Renders'.padStart(10) + 'Wasted'.padStart(10) + 'ms'.padStart(10))
|
||||
console.log('-'.repeat(75))
|
||||
for (const r of results) {
|
||||
console.log(r.label.padEnd(45) + String(r.totalRenders).padStart(10) + String(r.totalWasted).padStart(10) + r.totalMs.toFixed(1).padStart(10))
|
||||
}
|
||||
|
||||
ws.close()
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exit(1) })
|
||||
Loading…
Add table
Add a link
Reference in a new issue