feat: add constrained-device resource diagnostics
This commit is contained in:
parent
e18bc6a8cc
commit
90e74149d9
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Resource Diagnostics.** Performance now exposes read-only server RSS, configured/temp disk usage, persisted-job
|
||||
count, DOM/audio counts, and browser heap when available; host Docker metrics remain explicitly outside the panel's scope.
|
||||
- **Throttled Desktop Smoke Coverage.** Chromium CDP now exercises app navigation at 4× CPU throttling and
|
||||
asserts bounded DOM/audio counts while capturing the browser heap metric when available.
|
||||
- **Background Job History Panel.** The Benchmark section now shows sanitized, restart-safe background-job status,
|
||||
|
||||
6
TODO.md
6
TODO.md
@ -206,8 +206,10 @@ the first block before investing in architecture or polish.
|
||||
asserts navigation plus bounded DOM/audio counts, with JS heap captured when Chromium exposes it.
|
||||
- [x] Test the current image in a container limited to 4 GB RAM; the app served `/` with a 4 GiB cgroup limit,
|
||||
~50.6 MiB idle memory, 0.09% CPU, and no OOM kill.
|
||||
- [ ] Monitor browser-tab memory, JavaScript heap, DOM-node count, active audio blobs, Docker memory, and temporary disk usage.
|
||||
- [ ] Add a memory diagnostics page showing browser heap where supported, rendered rows, cache count/size, active jobs, server/container memory, and largest recent operations.
|
||||
- [x] Monitor browser-tab memory, JavaScript heap where supported, DOM-node count, audio elements, persisted jobs,
|
||||
server RSS, configured-data disk, and temporary disk usage through the read-only Performance diagnostics panel.
|
||||
- [ ] Add a memory diagnostics page showing cache count/size, Docker/container memory, and largest recent operations;
|
||||
the current panel covers browser/server/temp basics without pretending to measure unavailable host-level metrics.
|
||||
|
||||
## P1 — Speaker attribution: measured state and next steps (paused 2026-09-02)
|
||||
|
||||
|
||||
@ -2,15 +2,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import resource
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from core.database import background_job_artifact_path, background_job_get, background_job_list
|
||||
from core.constants import CONFIG_DIR
|
||||
from core.registry import TEMP_DIR
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _directory_bytes(root: Path) -> int:
|
||||
total = 0
|
||||
try:
|
||||
for path in root.rglob("*"):
|
||||
if path.is_file():
|
||||
try: total += path.stat().st_size
|
||||
except OSError: pass
|
||||
except OSError:
|
||||
pass
|
||||
return total
|
||||
|
||||
|
||||
def _public_job(job: dict) -> dict:
|
||||
payload = dict(job)
|
||||
payload.pop("fingerprint", None)
|
||||
@ -37,6 +54,17 @@ async def jobs_list(kind: str = "", limit: int = 50):
|
||||
return {"jobs": [_public_job(job) for job in background_job_list(kind, limit) if job]}
|
||||
|
||||
|
||||
@router.get("/api/diagnostics")
|
||||
async def diagnostics():
|
||||
usage = shutil.disk_usage(str(CONFIG_DIR))
|
||||
return {
|
||||
"server_rss_mb": round(float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024, 1),
|
||||
"config_disk": {"used_bytes": _directory_bytes(CONFIG_DIR), "free_bytes": usage.free},
|
||||
"temp_disk": {"used_bytes": _directory_bytes(TEMP_DIR)},
|
||||
"background_jobs": len(background_job_list(limit=100)),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/jobs/{job_id}")
|
||||
async def jobs_get(job_id: str):
|
||||
job = background_job_get(job_id)
|
||||
|
||||
2
static/dist/main.min.js
vendored
2
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -3,10 +3,13 @@
|
||||
(function initBackgroundJobs() {
|
||||
const list = document.getElementById('background-jobs-list');
|
||||
const refresh = document.getElementById('background-jobs-refresh');
|
||||
const diagnostics = document.getElementById('resource-diagnostics');
|
||||
const diagnosticsRefresh = document.getElementById('resource-diagnostics-refresh');
|
||||
if (!list || !refresh) return;
|
||||
|
||||
const labels = { transcription: 'Transcription', voice_design: 'Voice design', casting: 'Voice casting', consistent_images: 'Consistent images' };
|
||||
let loading = false;
|
||||
let diagnosticsLoading = false;
|
||||
|
||||
function node(value, fallback = '—') {
|
||||
const item = document.createElement('span');
|
||||
@ -118,9 +121,49 @@
|
||||
refresh.disabled = false;
|
||||
}
|
||||
}
|
||||
function formatBytes(value) {
|
||||
const bytes = Number(value);
|
||||
if (!Number.isFinite(bytes)) return '—';
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
||||
}
|
||||
async function loadDiagnostics() {
|
||||
if (!diagnostics || diagnosticsLoading) return;
|
||||
diagnosticsLoading = true;
|
||||
if (diagnosticsRefresh) diagnosticsRefresh.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/api/diagnostics', { cache: 'no-store' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const server = await response.json();
|
||||
const browser = await new Promise(resolve => requestAnimationFrame(() => resolve({
|
||||
domNodes: document.getElementsByTagName('*').length,
|
||||
audioElements: document.querySelectorAll('audio').length,
|
||||
jsHeapBytes: performance.memory?.usedJSHeapSize || null,
|
||||
})));
|
||||
diagnostics.replaceChildren();
|
||||
[["Server RSS", `${server.server_rss_mb} MiB`], ["Config data", formatBytes(server.config_disk?.used_bytes)],
|
||||
["Config free", formatBytes(server.config_disk?.free_bytes)], ["Temp data", formatBytes(server.temp_disk?.used_bytes)],
|
||||
["Persisted jobs", server.background_jobs], ["DOM nodes", browser.domNodes],
|
||||
["Audio elements", browser.audioElements], ["JS heap", formatBytes(browser.jsHeapBytes)]].forEach(([label, value]) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'resource-diagnostic-item';
|
||||
const name = document.createElement('span'); name.textContent = label;
|
||||
const valueNode = document.createElement('strong'); valueNode.textContent = value == null ? '—' : String(value);
|
||||
item.append(name, valueNode); diagnostics.appendChild(item);
|
||||
});
|
||||
} catch (error) {
|
||||
diagnostics.replaceChildren();
|
||||
const item = node('Diagnostics unavailable: ' + error.message); item.className = 'perf-history-empty'; diagnostics.appendChild(item);
|
||||
} finally {
|
||||
diagnosticsLoading = false;
|
||||
if (diagnosticsRefresh) diagnosticsRefresh.disabled = false;
|
||||
}
|
||||
}
|
||||
refresh.addEventListener('click', load);
|
||||
diagnosticsRefresh?.addEventListener('click', loadDiagnostics);
|
||||
load();
|
||||
loadDiagnostics();
|
||||
setInterval(() => {
|
||||
if (document.getElementById('s-performance')?.classList.contains('is-active')) load();
|
||||
if (document.getElementById('s-performance')?.classList.contains('is-active')) { load(); loadDiagnostics(); }
|
||||
}, 10000);
|
||||
})();
|
||||
|
||||
@ -234,6 +234,17 @@
|
||||
<div class="perf-history-empty">Loading background jobs…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="resource-diagnostics-card">
|
||||
<div class="bench-card-head">
|
||||
<div>
|
||||
<h2>Resource diagnostics</h2>
|
||||
<p class="card-subtitle">Read-only runtime indicators for constrained-device troubleshooting.</p>
|
||||
</div>
|
||||
<button class="btn-secondary btn-sm" id="resource-diagnostics-refresh" type="button"><span class="mdi mdi-refresh"></span> Refresh</button>
|
||||
</div>
|
||||
<div id="resource-diagnostics" class="resource-diagnostics-grid" aria-live="polite"><span class="perf-history-empty">Loading diagnostics…</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bench-pane" id="bench-pane-turn" data-bench-pane="turn">
|
||||
|
||||
@ -3742,6 +3742,10 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
.background-job-main code { color: var(--subtext); font-size: 10px; }
|
||||
.background-job-detail, .background-job-meta { grid-column: 1 / -1; color: var(--subtext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.background-job-artifact { grid-column: 1 / -1; color: var(--accent); font-size: 11px; }
|
||||
.resource-diagnostics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; }
|
||||
.resource-diagnostic-item { display: flex; flex-direction: column; gap: 3px; padding: 9px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); }
|
||||
.resource-diagnostic-item span { color: var(--subtext); font-size: 11px; }
|
||||
.resource-diagnostic-item strong { font-size: 14px; }
|
||||
.background-job-row.is-error .background-job-detail { color: var(--red); }
|
||||
.background-job-row.is-done .background-job-meta { color: var(--green, #22c55e); }
|
||||
.background-job-actions { display: flex; align-items: center; gap: 9px; flex-shrink: 0; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user