"""dashboard.py — local fleet leak dashboard. Serves on http://localhost:8080. / -> HTML page with charts (WS + per-class instance counts) /data.json -> parsed snapshot data from artifacts/snapshots/main.tsv """ import http.server import json import socketserver import threading from pathlib import Path ROOT = Path(__file__).resolve().parent.parent TSV = ROOT / "artifacts" / "snapshots" / "main.tsv" REPORT_MD = ROOT / "REPORT.md" PORT = 8080 # Expected column order written by snapshot_compare.py HEADER_COLS = ["timestamp", "client", "label", "pid", "mb", "uiitem", "uiitem_cleared", "uiitem_active", "palette", "cphysicsobj", "renderSurf", "renderSurfD3D", "renderTexD3D", "csurface", "imgtex", "cgfxobj", "d3dxmesh"] # Metrics we want to chart, in display order METRICS = ["mb", "palette", "renderSurf", "renderSurfD3D", "renderTexD3D", "csurface", "imgtex", "cphysicsobj", "uiitem"] METRIC_LABELS = { "mb": "Memory (MB)", "palette": "Palette instances", "renderSurf": "RenderSurface base instances", "renderSurfD3D": "RenderSurfaceD3D instances", "renderTexD3D": "RenderTextureD3D instances", "csurface": "CSurface instances", "imgtex": "ImgTex instances", "cphysicsobj": "CPhysicsObj instances", "uiitem": "UIElement_UIItem instances", } def load_tsv(): """Parse main.tsv into {client: [(ts, {metric:val,...}), ...]}. Rows where mb == 0 or 'DEAD' are skipped (stale-PID rows). Header row (with literal 'timestamp' in col 1) is skipped. """ out = {} if not TSV.exists(): return out with open(TSV) as f: for line in f: line = line.rstrip("\n") if not line: continue parts = line.split("\t") if len(parts) < 5: continue if parts[0] == "timestamp": continue # header row = {} for i, col in enumerate(HEADER_COLS): if i >= len(parts): break row[col] = parts[i] # Skip dead-PID rows mb_str = row.get("mb", "") if mb_str in ("0", "DEAD", "NOACCESS", ""): continue try: ts = row["timestamp"] client = row["client"] metrics = {} for m in METRICS: v = row.get(m, "") if v == "": continue try: metrics[m] = int(v) except ValueError: pass # also include pid so the dashboard can detect restarts pid_str = row.get("pid", "") if pid_str: try: metrics["pid"] = int(pid_str) except ValueError: pass out.setdefault(client, []).append((ts, metrics)) except KeyError: continue # Sort each client's series by timestamp for c in out: out[c].sort(key=lambda x: x[0]) # Filter to rows matching the CURRENT (latest) PID per character so we # don't conflate pre-restart series with post-restart series under the # same character name. (Old PIDs become DEAD rows but were once alive # and have data we'd otherwise plot as a misleading plateau.) filtered = {} for c, series in out.items(): if not series: continue latest_pid = series[-1][1].get("pid") if latest_pid is None: filtered[c] = series continue kept = [(ts, m) for ts, m in series if m.get("pid") == latest_pid] if kept: filtered[c] = kept return filtered HTML = """ Leak-Hunt Fleet Dashboard

Leak-Hunt Fleet Dashboard [Final Report →]

loading...  |  data: artifacts/snapshots/main.tsv  |   |   | 

Current state  (click headers to sort; growth rate = avg per hour from last 4 snapshots; cap = projected hours until 2048MB at current rate)

Charts  (click legend names to toggle clients)

""" HTML = HTML.replace("__METRICS__", json.dumps(METRICS)) HTML = HTML.replace("__METRIC_LABELS__", json.dumps(METRIC_LABELS)) REPORT_HTML = """ Leak-Hunt — Final Report
Loading…
""" class Handler(http.server.BaseHTTPRequestHandler): def log_message(self, fmt, *args): # silence per-request logs pass def do_GET(self): if self.path == "/" or self.path.startswith("/index"): self._send(200, "text/html", HTML.encode("utf-8")) elif self.path.startswith("/data.json"): data = load_tsv() payload = json.dumps(data).encode("utf-8") self._send(200, "application/json", payload) elif self.path.startswith("/report.md"): try: body = REPORT_MD.read_bytes() self._send(200, "text/markdown; charset=utf-8", body) except FileNotFoundError: self._send(404, "text/plain", b"REPORT.md not found\n") elif self.path == "/report" or self.path.startswith("/report/"): self._send(200, "text/html", REPORT_HTML.encode("utf-8")) else: self._send(404, "text/plain", b"not found\n") def _send(self, code, ctype, body): self.send_response(code) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(body) def main(): with socketserver.ThreadingTCPServer(("127.0.0.1", PORT), Handler) as srv: srv.allow_reuse_address = True print(f"Dashboard listening on http://localhost:{PORT}") srv.serve_forever() if __name__ == "__main__": main()