mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-13 14:02:16 +00:00
bench: live-attach kit — sample/profile a running TUI session (Ink or OpenTUI)
This commit is contained in:
parent
e3973050df
commit
fe50861c2e
3 changed files with 120 additions and 0 deletions
59
bench/live-attach.sh
Executable file
59
bench/live-attach.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env bash
|
||||
# live-attach.sh — plug into a RUNNING hermes TUI (Ink or OpenTUI) and measure it.
|
||||
#
|
||||
# bench/live-attach.sh <pid> [out-dir] # sample memory+cpu until Ctrl-C
|
||||
# bench/live-attach.sh <pid> --profile [secs] # also grab a CPU profile window (default 30s)
|
||||
# bench/live-attach.sh <pid> --heap # grab a heap snapshot (large file!)
|
||||
#
|
||||
# Find your TUI pid: pgrep -af 'dist/main.js' (OpenTUI)
|
||||
# pgrep -af 'dist/entry.js' (Ink)
|
||||
# Works on any live session — no restart, no flags needed at launch:
|
||||
# profiling uses SIGUSR1 (Node opens an inspector port on demand).
|
||||
# In-TUI complements (OpenTUI only): /mem (live stats line), /heapdump.
|
||||
set -euo pipefail
|
||||
PID="${1:?usage: live-attach.sh <pid> [outdir|--profile [secs]|--heap]}"
|
||||
shift || true
|
||||
OUT="${1:-/tmp/tui-live-$PID}"; MODE="sample"; SECS=30
|
||||
[[ "${1:-}" == "--profile" ]] && { MODE=profile; OUT="/tmp/tui-live-$PID"; SECS="${2:-30}"; }
|
||||
[[ "${1:-}" == "--heap" ]] && { MODE=heap; OUT="/tmp/tui-live-$PID"; }
|
||||
mkdir -p "$OUT"
|
||||
echo "target pid=$PID cmd=$(tr '\0' ' ' </proc/$PID/cmdline | cut -c1-80)"
|
||||
echo "out: $OUT"
|
||||
|
||||
sample() {
|
||||
local f="$OUT/samples.jsonl"
|
||||
echo "sampling 1Hz → $f (Ctrl-C to stop; render: node bench/live-render.mjs $OUT)"
|
||||
local prev_cpu=0 hz; hz=$(getconf CLK_TCK)
|
||||
while kill -0 "$PID" 2>/dev/null; do
|
||||
local rss pss pdirty hwm cpu t
|
||||
rss=$(awk '/^Rss:/{print $2}' /proc/$PID/smaps_rollup 2>/dev/null || echo 0)
|
||||
pss=$(awk '/^Pss:/{print $2}' /proc/$PID/smaps_rollup 2>/dev/null || echo 0)
|
||||
pdirty=$(awk '/^Private_Dirty:/{print $2}' /proc/$PID/smaps_rollup 2>/dev/null || echo 0)
|
||||
hwm=$(awk '/^VmHWM:/{print $2}' /proc/$PID/status 2>/dev/null || echo 0)
|
||||
cpu=$(awk '{print $14+$15}' /proc/$PID/stat 2>/dev/null || echo 0)
|
||||
t=$(date +%s.%N)
|
||||
printf '{"t":%s,"rss_kb":%s,"pss_kb":%s,"private_dirty_kb":%s,"vmhwm_kb":%s,"cpu_ticks":%s,"cpu_hz":%s}\n' \
|
||||
"$t" "$rss" "$pss" "$pdirty" "$hwm" "$cpu" "$hz" >> "$f"
|
||||
sleep 1
|
||||
done
|
||||
echo "process exited; $(wc -l <"$f") samples in $f"
|
||||
}
|
||||
|
||||
cdp() { # open inspector on demand, find the ws url
|
||||
kill -USR1 "$PID"; sleep 0.7
|
||||
local port; port=$(ss -tlnp 2>/dev/null | grep "pid=$PID" | grep -oE ':(92[0-9]{2})' | head -1 | tr -d ':')
|
||||
[[ -z "$port" ]] && port=9229
|
||||
curl -s "http://127.0.0.1:$port/json" | grep -oE 'ws://[^"]+' | head -1
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
sample) sample ;;
|
||||
profile)
|
||||
WS=$(cdp); echo "CDP: $WS — profiling ${SECS}s (interact with the TUI now!)"
|
||||
node "$(dirname "$0")/live-cdp.mjs" "$WS" profile "$SECS" "$OUT/live.cpuprofile"
|
||||
echo "→ $OUT/live.cpuprofile (open in https://speedscope.app or chrome://inspect)" ;;
|
||||
heap)
|
||||
WS=$(cdp); echo "CDP: $WS — heap snapshot (may pause the TUI briefly)"
|
||||
node "$(dirname "$0")/live-cdp.mjs" "$WS" heap 0 "$OUT/live.heapsnapshot"
|
||||
echo "→ $OUT/live.heapsnapshot (Chrome DevTools → Memory → Load)" ;;
|
||||
esac
|
||||
44
bench/live-cdp.mjs
Normal file
44
bench/live-cdp.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env node
|
||||
// live-cdp.mjs — minimal CDP client for live-attach.sh (no deps; Node ws via raw socket
|
||||
// is overkill — use the built-in WebSocket of Node >=22).
|
||||
// usage: node live-cdp.mjs <ws-url> profile <secs> <out> | heap 0 <out>
|
||||
const [, , url, mode, secsArg, out] = process.argv
|
||||
const { writeFileSync, appendFileSync } = await import('node:fs')
|
||||
const ws = new WebSocket(url)
|
||||
let id = 0
|
||||
const pending = new Map()
|
||||
const send = (method, params = {}) =>
|
||||
new Promise((res, rej) => {
|
||||
const i = ++id
|
||||
pending.set(i, { res, rej })
|
||||
ws.send(JSON.stringify({ id: i, method, params }))
|
||||
})
|
||||
const chunks = []
|
||||
ws.onmessage = e => {
|
||||
const m = JSON.parse(e.data)
|
||||
if (m.id && pending.has(m.id)) {
|
||||
const { res, rej } = pending.get(m.id)
|
||||
pending.delete(m.id)
|
||||
m.error ? rej(new Error(m.error.message)) : res(m.result)
|
||||
} else if (m.method === 'HeapProfiler.addHeapSnapshotChunk') chunks.push(m.params.chunk)
|
||||
}
|
||||
ws.onopen = async () => {
|
||||
try {
|
||||
if (mode === 'profile') {
|
||||
await send('Profiler.enable')
|
||||
await send('Profiler.start')
|
||||
await new Promise(r => setTimeout(r, Number(secsArg) * 1000))
|
||||
const { profile } = await send('Profiler.stop')
|
||||
writeFileSync(out, JSON.stringify(profile))
|
||||
} else {
|
||||
await send('HeapProfiler.enable')
|
||||
await send('HeapProfiler.takeHeapSnapshot', { reportProgress: false })
|
||||
writeFileSync(out, chunks.join(''))
|
||||
}
|
||||
process.exit(0)
|
||||
} catch (err) {
|
||||
console.error(String(err))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
ws.onerror = err => { console.error('ws error', err.message ?? err); process.exit(1) }
|
||||
17
bench/live-render.mjs
Normal file
17
bench/live-render.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env node
|
||||
// live-render.mjs — quick chart from live-attach samples: node bench/live-render.mjs <dir>
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
const dir = process.argv[2] ?? '.'
|
||||
const rows = readFileSync(`${dir}/samples.jsonl`, 'utf8').trim().split('\n').map(l => JSON.parse(l))
|
||||
const t0 = rows[0].t
|
||||
const pts = rows.map(r => ({ t: r.t - t0, rss: r.rss_kb / 1024, hwm: r.vmhwm_kb / 1024 }))
|
||||
const W = 900, H = 360, mt = (v, max) => H - 30 - (v / max) * (H - 60)
|
||||
const maxY = Math.max(...pts.map(p => p.hwm)) * 1.1
|
||||
const path = k => pts.map((p, i) => `${i ? 'L' : 'M'}${30 + (p.t / pts.at(-1).t) * (W - 60)},${mt(p[k], maxY)}`).join('')
|
||||
const cpu = rows.map((r, i) => i ? (r.cpu_ticks - rows[i-1].cpu_ticks) / r.cpu_hz / (r.t - rows[i-1].t) : 0)
|
||||
writeFileSync(`${dir}/live.svg`, `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" style="background:#0d0d12">
|
||||
<text x="30" y="20" fill="#ccc" font-family="monospace">live session: RSS (gold) / VmHWM (grey) MB · avg cpu ${(cpu.reduce((a,b)=>a+b,0)/Math.max(1,cpu.length-1)*100).toFixed(1)}% · ${rows.length}s</text>
|
||||
<path d="${path('hwm')}" stroke="#888" fill="none"/><path d="${path('rss')}" stroke="#F5B820" fill="none" stroke-width="2"/>
|
||||
<text x="30" y="${H-10}" fill="#888" font-family="monospace">0s</text><text x="${W-80}" y="${H-10}" fill="#888" font-family="monospace">${Math.round(pts.at(-1).t)}s</text>
|
||||
<text x="${W-120}" y="${mt(pts.at(-1).rss,maxY)}" fill="#F5B820" font-family="monospace">${pts.at(-1).rss.toFixed(0)}MB</text></svg>`)
|
||||
console.log(`${dir}/live.svg`)
|
||||
Loading…
Add table
Add a link
Reference in a new issue