From fe50861c2eeb945e0f92dbc79952715e4cc9e996 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Thu, 11 Jun 2026 07:34:08 +0530 Subject: [PATCH] =?UTF-8?q?bench:=20live-attach=20kit=20=E2=80=94=20sample?= =?UTF-8?q?/profile=20a=20running=20TUI=20session=20(Ink=20or=20OpenTUI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bench/live-attach.sh | 59 +++++++++++++++++++++++++++++++++++++++++++ bench/live-cdp.mjs | 44 ++++++++++++++++++++++++++++++++ bench/live-render.mjs | 17 +++++++++++++ 3 files changed, 120 insertions(+) create mode 100755 bench/live-attach.sh create mode 100644 bench/live-cdp.mjs create mode 100644 bench/live-render.mjs diff --git a/bench/live-attach.sh b/bench/live-attach.sh new file mode 100755 index 00000000000..7adc3805548 --- /dev/null +++ b/bench/live-attach.sh @@ -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 [out-dir] # sample memory+cpu until Ctrl-C +# bench/live-attach.sh --profile [secs] # also grab a CPU profile window (default 30s) +# bench/live-attach.sh --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 [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' ' ' /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 diff --git a/bench/live-cdp.mjs b/bench/live-cdp.mjs new file mode 100644 index 00000000000..fddf615ee9e --- /dev/null +++ b/bench/live-cdp.mjs @@ -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 profile | heap 0 +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) } diff --git a/bench/live-render.mjs b/bench/live-render.mjs new file mode 100644 index 00000000000..a360463d555 --- /dev/null +++ b/bench/live-render.mjs @@ -0,0 +1,17 @@ +#!/usr/bin/env node +// live-render.mjs — quick chart from live-attach samples: node bench/live-render.mjs +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`, ` +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 + +0s${Math.round(pts.at(-1).t)}s +${pts.at(-1).rss.toFixed(0)}MB`) +console.log(`${dir}/live.svg`)