hermes-agent/apps/desktop/scripts/diag-overlay-sweep.mjs
Brooklyn Nicholson 6fc01f8ab8 perf(desktop): memoize sidebar rows + status dots + artifact cells
The sidebar stays mounted beneath every overlay/page, and it subscribes to
$sessions + $workingSessionIds — both tick on every streaming token. An
unmemoized SidebarSessionRow re-rendered the whole list (Codicon, labels,
status dots) on each delta, and that churn bled into every overlay opened on
top: Cron, Profiles, Agents, Starmap, Webhooks, Command Center, Settings.

- SidebarSessionRow: memo() with a custom comparator that ignores the pure
  id-forwarding callbacks (fresh closures by design) and compares only the
  data that changes what the row paints. Rows bail out while siblings stream.
- SessionStatusDot: the 5 $...SessionIds arrays now read via useStoreSelector
  returning this session's boolean, so a dot repaints only when ITS OWN
  membership flips, not on every array tick.
- Artifacts: stable cellCtx (useMemo) + memoized Primary/Location/Session
  cells so a link-title fetch on one row stops re-rendering the whole table.

Measured before/after (2s idle, sessions streaming), sidebar-fed overlays:
  Cron 407->~30 wasted, Profiles 732->~80, Agents 132->9, Starmap 188->56,
  Webhooks 154->22.
2026-07-28 20:23:10 -05:00

110 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
// Full surface sweep: every overlay, every settings sub-page, every system overlay.
// Read-only — navigates and clicks, never closes the app.
import WebSocket from 'ws'
const WS_URL = process.env.CDP_WS || '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 = 300) {
await eval_(ws, `__RENDER_COUNTS__.start(); true`)
await fn(ws)
await sleep(settleMs)
const report = JSON.parse(await eval_(ws, `JSON.stringify(__RENDER_COUNTS__.report())`))
const totalRenders = report.reduce((s, c) => s + c.renders, 0)
const totalWasted = report.reduce((s, c) => s + c.wasted, 0)
const totalMs = report.reduce((s, c) => s + c.totalMs, 0)
const top = report.filter(c => c.wasted > 0).sort((a,b) => b.wasted - a.wasted).slice(0, 6)
console.log(`\n${label}`)
console.log(` ${totalRenders} renders · ${totalWasted} wasted · ${totalMs.toFixed(1)}ms`)
for (const c of top) console.log(` ${c.name}: ${c.renders}r / ${c.wasted}w / ${c.totalMs.toFixed(1)}ms`)
if (top.length === 0) console.log(` (clean)`)
return { label, totalRenders, totalWasted, totalMs }
}
async function nav(ws, hash) {
await eval_(ws, `window.location.hash = '#${hash}'; true`)
}
async function clickText(ws, text, tag='button') {
return eval_(ws, `
const el = Array.from(document.querySelectorAll('${tag}')).find(b => b.textContent?.trim() === ${JSON.stringify(text)})
if (el) { el.click(); true } else false
`)
}
async function main() {
const ws = new WebSocket(WS_URL)
await new Promise((res, rej) => { ws.on('open', res); ws.on('error', rej) })
await send(ws, 'Runtime.enable')
if (await eval_(ws, `typeof __RENDER_COUNTS__`) !== 'object') { console.error('counter not loaded'); process.exit(1) }
const results = []
// System overlays — open each, idle 2s (streaming underneath is the churn test)
for (const [label, route] of [
['Artifacts', '/artifacts'],
['Messaging', '/messaging'],
['Cron', '/cron'],
['Profiles', '/profiles'],
['Agents', '/agents'],
['Starmap', '/starmap'],
['Webhooks', '/webhooks'],
]) {
results.push(await measure(ws, `OVERLAY: ${label} (open + 2s idle)`, async (ws) => {
await nav(ws, route)
await sleep(2200)
}, 100))
await nav(ws, '/')
await sleep(300)
}
// Settings sub-pages — open settings, click each nav item, idle 1.5s on it
await nav(ws, '/settings')
await sleep(600)
for (const label of ['Model','Session','Appearance','Notifications','Providers','Gateway','Keybinds','API Keys','Plugins','Archived Chats','About']) {
results.push(await measure(ws, `SETTINGS: ${label} (1.5s idle)`, async (ws) => {
await clickText(ws, label)
await sleep(1500)
}, 100))
}
await nav(ws, '/')
await sleep(300)
// Messaging sub-tabs (platform detail) — click through if present
await nav(ws, '/messaging')
await sleep(800)
results.push(await measure(ws, 'MESSAGING: idle 2s w/ platform list', async (ws) => {
await sleep(2000)
}, 100))
await nav(ws, '/')
await sleep(300)
console.log('\n\n=== SUMMARY (wasted renders) ===')
console.log('Surface'.padEnd(48) + 'Renders'.padStart(9) + 'Wasted'.padStart(9) + 'ms'.padStart(9))
console.log('-'.repeat(75))
for (const r of results) {
console.log(r.label.padEnd(48) + String(r.totalRenders).padStart(9) + String(r.totalWasted).padStart(9) + r.totalMs.toFixed(1).padStart(9))
}
ws.close()
}
main().catch(e => { console.error(e); process.exit(1) })