Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>VN Debug Dashboard</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script> | |
| <style> | |
| :root { | |
| --bg: #0f1117; | |
| --surface: #1a1d27; | |
| --border: #252836; | |
| --text: #dde1f0; | |
| --muted: #6e7891; | |
| --blue: #4c9be8; | |
| --red: #e05c5c; | |
| --green: #56b87a; | |
| --yellow: #e0a840; | |
| --orange: #e09440; | |
| --purple: #b07de8; | |
| --gray: #4e5568; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| font-size: 13px; | |
| padding: 16px; | |
| min-height: 100vh; | |
| } | |
| /* ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 16px; | |
| } | |
| header h1 { | |
| font-size: 17px; | |
| font-weight: 700; | |
| letter-spacing: -.02em; | |
| } | |
| header h1 span { color: var(--blue); } | |
| .status { | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| font-size: 12px; | |
| color: var(--muted); | |
| } | |
| .dot { | |
| width: 8px; height: 8px; | |
| border-radius: 50%; | |
| background: var(--gray); | |
| flex-shrink: 0; | |
| transition: background 0.3s; | |
| } | |
| .dot.live { background: var(--green); box-shadow: 0 0 6px var(--green); } | |
| .dot.error { background: var(--red); } | |
| /* ββ KPI cards ββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .kpi-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 10px; | |
| margin-bottom: 12px; | |
| } | |
| @media (max-width: 800px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } } | |
| .kpi { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 12px 14px; | |
| } | |
| .kpi-label { | |
| font-size: 10px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: .07em; | |
| color: var(--muted); | |
| margin-bottom: 5px; | |
| } | |
| .kpi-value { | |
| font-size: 28px; | |
| font-weight: 800; | |
| line-height: 1; | |
| font-variant-numeric: tabular-nums; | |
| color: var(--text); | |
| } | |
| .kpi-sub { | |
| font-size: 11px; | |
| color: var(--muted); | |
| margin-top: 5px; | |
| } | |
| .c-blue { color: var(--blue) ; } | |
| .c-green { color: var(--green) ; } | |
| .c-yellow { color: var(--yellow) ; } | |
| .c-red { color: var(--red) ; } | |
| /* ββ Charts ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .charts-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-bottom: 12px; | |
| } | |
| @media (max-width: 900px) { .charts-grid { grid-template-columns: 1fr; } } | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 14px; | |
| } | |
| .card h2 { | |
| font-size: 10px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: .07em; | |
| color: var(--muted); | |
| margin-bottom: 10px; | |
| } | |
| /* ββ Operations table ββββββββββββββββββββββββββββββββββββββββββ */ | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 12px; | |
| } | |
| th { | |
| text-align: right; | |
| font-size: 10px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: .05em; | |
| color: var(--muted); | |
| padding: 0 8px 8px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| th:first-child { text-align: left; } | |
| td { | |
| padding: 6px 8px; | |
| border-bottom: 1px solid var(--border); | |
| text-align: right; | |
| font-variant-numeric: tabular-nums; | |
| color: var(--text); | |
| } | |
| td:first-child { text-align: left; color: var(--muted); } | |
| tr:last-child td { border-bottom: none; } | |
| tr:hover td { background: rgba(255,255,255,.025); } | |
| .total-row td { font-weight: 700; color: var(--text) ; } | |
| /* ββ Actions βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-top: 12px; | |
| } | |
| button { | |
| background: var(--blue); | |
| color: #fff; | |
| border: none; | |
| border-radius: 6px; | |
| padding: 7px 14px; | |
| font-size: 12px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| opacity: 1; | |
| transition: opacity .15s; | |
| } | |
| button:hover:not(:disabled) { opacity: .82; } | |
| button:disabled { opacity: .35; cursor: default; } | |
| #report-msg { font-size: 12px; color: var(--green); } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <header> | |
| <h1>VN <span>Debug</span> Dashboard</h1> | |
| <div class="status"> | |
| <div class="dot" id="dot"></div> | |
| <span id="status-text">Connectingβ¦</span> | |
| </div> | |
| </header> | |
| <!-- ββ KPI cards ββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="kpi-grid"> | |
| <div class="kpi"> | |
| <div class="kpi-label">Turns</div> | |
| <div class="kpi-value c-blue" id="kpi-turns">β</div> | |
| <div class="kpi-sub">completed</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-label">LLM avg</div> | |
| <div class="kpi-value" id="kpi-llm">β</div> | |
| <div class="kpi-sub" id="kpi-llm-sub">p95 β</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-label">Image cache</div> | |
| <div class="kpi-value" id="kpi-cache">β</div> | |
| <div class="kpi-sub" id="kpi-cache-sub">hits / total</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-label">Session</div> | |
| <div class="kpi-value c-blue" id="kpi-elapsed">β</div> | |
| <div class="kpi-sub" id="kpi-gpu">β</div> | |
| </div> | |
| </div> | |
| <!-- ββ Charts βββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="charts-grid"> | |
| <div class="card"> | |
| <h2>System Resources β last 2 min</h2> | |
| <canvas id="res-chart" height="170"></canvas> | |
| </div> | |
| <div class="card"> | |
| <h2>Latency per Turn (ms, stacked)</h2> | |
| <canvas id="lat-chart" height="170"></canvas> | |
| </div> | |
| </div> | |
| <!-- ββ Operations table βββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="card"> | |
| <h2>Operations</h2> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Operation</th> | |
| <th>n</th> | |
| <th>avg ms</th> | |
| <th>p95 ms</th> | |
| <th>max ms</th> | |
| </tr> | |
| </thead> | |
| <tbody id="ops-body"></tbody> | |
| </table> | |
| </div> | |
| <!-- ββ Actions ββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="actions"> | |
| <button id="save-btn" onclick="saveReport()">Save report now</button> | |
| <span id="report-msg"></span> | |
| </div> | |
| <script> | |
| // ββ Chart defaults βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Chart.defaults.color = '#6e7891'; | |
| Chart.defaults.borderColor = '#252836'; | |
| Chart.defaults.font.size = 10; | |
| const GRID_COLOR = 'rgba(255,255,255,.04)'; | |
| const BASE_SCALES = { | |
| x: { ticks: { color: '#555', maxTicksLimit: 8 }, grid: { color: GRID_COLOR } }, | |
| y: { ticks: { color: '#555' }, grid: { color: GRID_COLOR } }, | |
| }; | |
| // ββ Resource chart (line) ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const resCtx = document.getElementById('res-chart').getContext('2d'); | |
| const resChart = new Chart(resCtx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [ | |
| { | |
| label: 'RAM %', yAxisID: 'yPct', data: [], | |
| borderColor: '#4c9be8', backgroundColor: 'rgba(76,155,232,.06)', | |
| borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3, | |
| }, | |
| { | |
| label: 'CPU %', yAxisID: 'yPct', data: [], | |
| borderColor: '#e05c5c', backgroundColor: 'rgba(224,92,92,.05)', | |
| borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3, | |
| }, | |
| { | |
| label: 'VRAM', yAxisID: 'yVram', data: [], | |
| borderColor: '#e09440', backgroundColor: 'rgba(224,148,64,.05)', | |
| borderWidth: 1.5, pointRadius: 0, fill: false, tension: 0.3, | |
| }, | |
| ] | |
| }, | |
| options: { | |
| animation: false, | |
| responsive: true, | |
| interaction: { mode: 'index', intersect: false }, | |
| plugins: { | |
| legend: { labels: { color: '#888', boxWidth: 10, font: { size: 10 } } }, | |
| }, | |
| scales: { | |
| x: BASE_SCALES.x, | |
| yPct: { | |
| type: 'linear', position: 'left', min: 0, suggestedMax: 100, | |
| ticks: { color: '#555', callback: v => v + '%' }, | |
| grid: { color: GRID_COLOR }, | |
| }, | |
| yVram: { | |
| type: 'linear', position: 'right', min: 0, | |
| ticks: { color: '#7a6040' }, | |
| grid: { drawOnChartArea: false }, | |
| display: false, // shown only when VRAM data present | |
| }, | |
| }, | |
| }, | |
| }); | |
| // ββ Latency stacked bar ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const latCtx = document.getElementById('lat-chart').getContext('2d'); | |
| const latChart = new Chart(latCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: [], | |
| datasets: [ | |
| { label: 'LLM', data: [], backgroundColor: '#4c9be8', stack: 's' }, | |
| { label: 'Painter', data: [], backgroundColor: '#56b87a', stack: 's' }, | |
| { label: 'STT', data: [], backgroundColor: '#b07de8', stack: 's' }, | |
| { label: 'Compact', data: [], backgroundColor: '#e09440', stack: 's' }, | |
| { label: 'Other', data: [], backgroundColor: '#4e5568', stack: 's' }, | |
| ] | |
| }, | |
| options: { | |
| animation: false, | |
| responsive: true, | |
| plugins: { | |
| legend: { labels: { color: '#888', boxWidth: 10, font: { size: 10 } } }, | |
| tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${Math.round(ctx.raw)} ms` } }, | |
| }, | |
| scales: { | |
| x: { ...BASE_SCALES.x, stacked: true }, | |
| y: { | |
| ...BASE_SCALES.y, stacked: true, | |
| title: { display: true, text: 'ms', color: '#555', font: { size: 10 } }, | |
| }, | |
| }, | |
| }, | |
| }); | |
| // ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function fmtMs(ms) { | |
| if (ms == null) return 'β'; | |
| return ms >= 1000 ? (ms / 1000).toFixed(1) + 's' : Math.round(ms) + 'ms'; | |
| } | |
| function fmtElapsed(s) { | |
| if (s == null) return 'β'; | |
| const m = Math.floor(s / 60), sec = Math.floor(s % 60); | |
| return m > 0 ? `${m}m ${sec}s` : `${sec}s`; | |
| } | |
| function fmtTime(ts) { | |
| const d = new Date(ts * 1000); | |
| return d.getMinutes().toString().padStart(2, '0') + ':' + d.getSeconds().toString().padStart(2, '0'); | |
| } | |
| function latencyClass(ms) { | |
| if (ms == null) return ''; | |
| if (ms < 2000) return 'c-green'; | |
| if (ms < 6000) return 'c-yellow'; | |
| return 'c-red'; | |
| } | |
| function cacheClass(rate) { | |
| if (rate == null) return ''; | |
| if (rate >= 0.7) return 'c-green'; | |
| if (rate >= 0.4) return 'c-yellow'; | |
| return 'c-red'; | |
| } | |
| const GPU_LABELS = { nvml: 'NVIDIA GPU', rocm: 'AMD ROCm', mps: 'Apple MPS (unified)' }; | |
| const OP_ORDER = [ | |
| 'total_turn', 'llm_direct', 'compact_memory', 'context_assembly', | |
| 'apply_directives', 'save_memory', 'painter_backdrop', 'painter_sprite', 'stt', | |
| ]; | |
| const OP_NAMES = { | |
| total_turn: 'Turn total', | |
| llm_direct: 'LLM direct_turn', | |
| compact_memory: 'Compact memory (LLM)', | |
| context_assembly: 'Context assembly', | |
| apply_directives: 'apply_directives', | |
| save_memory: 'save_memory', | |
| painter_backdrop: 'Painter β backdrop', | |
| painter_sprite: 'Painter β sprite', | |
| stt: 'STT transcribe', | |
| }; | |
| // ββ Main update ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function update(data) { | |
| // KPI: turns | |
| document.getElementById('kpi-turns').textContent = data.turns_completed ?? 'β'; | |
| // KPI: LLM | |
| const llm = data.ops?.llm_direct; | |
| const kpiLlm = document.getElementById('kpi-llm'); | |
| kpiLlm.textContent = fmtMs(llm?.avg_ms); | |
| kpiLlm.className = 'kpi-value ' + latencyClass(llm?.avg_ms); | |
| document.getElementById('kpi-llm-sub').textContent = | |
| llm ? `p95 ${fmtMs(llm.p95_ms)} Β· max ${fmtMs(llm.max_ms)}` : 'no data yet'; | |
| // KPI: cache | |
| const rate = data.cache_hit_rate; | |
| const kpiCache = document.getElementById('kpi-cache'); | |
| kpiCache.textContent = rate != null ? Math.round(rate * 100) + '%' : 'β'; | |
| kpiCache.className = 'kpi-value ' + cacheClass(rate); | |
| const tot = (data.cache_hits ?? 0) + (data.cache_misses ?? 0); | |
| document.getElementById('kpi-cache-sub').textContent = | |
| tot > 0 ? `${data.cache_hits} hits / ${tot} total` : 'no renders yet'; | |
| // KPI: elapsed + GPU backend | |
| document.getElementById('kpi-elapsed').textContent = fmtElapsed(data.session_elapsed_s); | |
| document.getElementById('kpi-gpu').textContent = | |
| data.gpu_backend ? (GPU_LABELS[data.gpu_backend] ?? data.gpu_backend) : 'No GPU detected'; | |
| // Resource chart | |
| const samples = data.samples ?? []; | |
| resChart.data.labels = samples.map(s => fmtTime(s.ts)); | |
| resChart.data.datasets[0].data = samples.map(s => s.ram_pct ?? null); | |
| resChart.data.datasets[1].data = samples.map(s => s.cpu_pct ?? null); | |
| const hasVram = samples.some(s => s.vram_mb != null); | |
| if (hasVram) { | |
| // For nvml/rocm: use vram_pct on left axis if available. | |
| // For MPS (unified): express process RSS as % of total system RAM (ram_mb / ram_pct * 100). | |
| resChart.data.datasets[2].data = samples.map(s => { | |
| if (s.vram_mb == null) return null; | |
| if (s.vram_pct != null) return s.vram_pct; // nvml / rocm | |
| // MPS: derive total RAM from ram_mb + ram_pct, express RSS as % | |
| if (s.ram_pct > 0) return (s.vram_mb / (s.ram_mb / (s.ram_pct / 100))) * 100; | |
| return null; | |
| }); | |
| const vramLabel = data.gpu_backend === 'mps' ? 'RSS % (MPS)' : 'VRAM %'; | |
| resChart.data.datasets[2].label = vramLabel; | |
| resChart.options.scales.yVram.display = false; // keep right axis hidden, same % scale | |
| } else { | |
| resChart.data.datasets[2].data = samples.map(() => null); | |
| } | |
| resChart.update('none'); | |
| // Latency stacked bar | |
| const turns = data.turn_breakdown ?? []; | |
| latChart.data.labels = turns.map(t => `T${t.turn}`); | |
| latChart.data.datasets[0].data = turns.map(t => t.llm_direct ?? 0); | |
| latChart.data.datasets[1].data = turns.map(t => (t.painter_backdrop ?? 0) + (t.painter_sprite ?? 0)); | |
| latChart.data.datasets[2].data = turns.map(t => t.stt ?? 0); | |
| latChart.data.datasets[3].data = turns.map(t => t.compact_memory ?? 0); | |
| latChart.data.datasets[4].data = turns.map(t => { | |
| const acc = (t.llm_direct ?? 0) + (t.painter_backdrop ?? 0) + (t.painter_sprite ?? 0) | |
| + (t.stt ?? 0) + (t.compact_memory ?? 0) | |
| + (t.apply_directives ?? 0) + (t.save_memory ?? 0); | |
| return Math.max(0, (t.total_turn ?? 0) - acc); | |
| }); | |
| latChart.update('none'); | |
| // Ops table | |
| const ops = data.ops ?? {}; | |
| const keys = [...OP_ORDER.filter(k => k in ops), ...Object.keys(ops).filter(k => !OP_ORDER.includes(k))]; | |
| const tbody = document.getElementById('ops-body'); | |
| tbody.innerHTML = keys.map(k => { | |
| const o = ops[k]; | |
| if (!o) return ''; | |
| const isTot = k === 'total_turn'; | |
| return `<tr class="${isTot ? 'total-row' : ''}"> | |
| <td>${OP_NAMES[k] ?? k}</td> | |
| <td>${o.count}</td> | |
| <td class="${latencyClass(o.avg_ms)}">${Math.round(o.avg_ms)}</td> | |
| <td>${Math.round(o.p95_ms)}</td> | |
| <td>${Math.round(o.max_ms)}</td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| // ββ SSE connection with auto-reconnect βββββββββββββββββββββββββββββββββββ | |
| const dot = document.getElementById('dot'); | |
| const statusTxt = document.getElementById('status-text'); | |
| let src = null; | |
| function connect() { | |
| if (src) { src.close(); src = null; } | |
| dot.className = 'dot'; | |
| statusTxt.textContent = 'Connectingβ¦'; | |
| src = new EventSource('/debug/stream'); | |
| src.onopen = () => { | |
| dot.className = 'dot live'; | |
| statusTxt.textContent = 'Live'; | |
| }; | |
| src.onmessage = e => { | |
| try { update(JSON.parse(e.data)); } catch { /* ignore parse errors */ } | |
| }; | |
| src.onerror = () => { | |
| dot.className = 'dot error'; | |
| statusTxt.textContent = 'Reconnecting in 3sβ¦'; | |
| src.close(); src = null; | |
| setTimeout(connect, 3000); | |
| }; | |
| } | |
| // ββ Save report ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function saveReport() { | |
| const btn = document.getElementById('save-btn'); | |
| const msg = document.getElementById('report-msg'); | |
| btn.disabled = true; | |
| msg.style.color = 'var(--muted)'; | |
| msg.textContent = 'Savingβ¦'; | |
| try { | |
| const r = await fetch('/debug/report'); | |
| const d = await r.json(); | |
| msg.style.color = 'var(--green)'; | |
| msg.textContent = d.status === 'ok' ? 'β Saved to runs/debug_*/' : 'β Save failed'; | |
| } catch { | |
| msg.style.color = 'var(--red)'; | |
| msg.textContent = 'β Request failed'; | |
| } | |
| setTimeout(() => { msg.textContent = ''; btn.disabled = false; }, 4000); | |
| } | |
| connect(); | |
| </script> | |
| </body> | |
| </html> | |