#!/usr/bin/env python3 # Author: cirakg & claude ai # Summary: Compare the app's debug diagnostics (start-up time, memory) with the budgets in tools/budgets.json. """Compare the app's debug diagnostics with the budgets in tools/budgets.json. Two kinds of lines are read from build/diag/.log: DIAG first_frame startup_ms=1 mem_used=6984 mem_total=781888 printed by the debug build (source/util/Diag.mc) after its first frame; HARNESS first_frame wall_ms=2310 added by app-diag.sh: wall-clock time from launching the app with monkeydo until that DIAG line appeared, which includes loading the program (the in-app timer can't see that part). Usage: check_budgets.py {perf,mem} LOG [LOG ...] [--budgets FILE] [--summary OUT.json] Exit: 0 within budget, 1 over budget, 2 missing or unreadable diagnostics. """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path EXIT_OK, EXIT_OVER, EXIT_MISSING = 0, 1, 2 EVENT_RE = re.compile(r"\b(DIAG|HARNESS)\s+(\w+)((?:\s+\w+=-?\d+(?:\.\d+)?)+)") PAIR_RE = re.compile(r"(\w+)=(-?\d+(?:\.\d+)?)") def read_events(text: str) -> dict[str, dict[str, float]]: """The first occurrence of each event, keyed as "DIAG.first_frame" etc.""" events: dict[str, dict[str, float]] = {} for match in EVENT_RE.finditer(text): key = f"{match.group(1)}.{match.group(2)}" if key not in events: events[key] = {name: float(value) for name, value in PAIR_RE.findall(match.group(3))} return events def measure(kind: str, events: dict[str, dict[str, float]]) -> dict[str, float]: """The values one budget kind looks at; empty when the app never drew.""" frame = events.get("DIAG.first_frame") if frame is None: return {} if kind == "perf": values = {"startup_ms": frame["startup_ms"]} harness = events.get("HARNESS.first_frame") if harness is not None: values["first_frame_wall_ms"] = harness["wall_ms"] return values total = frame["mem_total"] return {"mem_used_fraction": frame["mem_used"] / total if total else 1.0} def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Check diagnostics against budgets.") parser.add_argument("kind", choices=("perf", "mem")) parser.add_argument("logs", type=Path, nargs="+") parser.add_argument("--budgets", type=Path, default=Path("tools/budgets.json")) parser.add_argument("--summary", type=Path) args = parser.parse_args(argv) budgets: dict[str, float] = json.loads(args.budgets.read_text(encoding="utf-8"))[args.kind] worst = EXIT_OK summary: dict[str, dict[str, float]] = {} for log in args.logs: device = log.stem try: values = measure(args.kind, read_events(log.read_text(encoding="utf-8", errors="replace"))) except (OSError, KeyError) as err: print(f"[{device}] cannot read diagnostics: {err}") worst = max(worst, EXIT_MISSING) continue if not values: print(f"[{device}] no 'DIAG first_frame' line (did the app start? is this a debug build?)") worst = max(worst, EXIT_MISSING) continue summary[device] = values for name, limit in budgets.items(): if name not in values: print(f"[{device}] {name}: not measured") worst = max(worst, EXIT_MISSING) continue value = values[name] over = value > limit print(f"[{device}] {name} = {value:.4g} (budget {limit:.4g}) {'OVER BUDGET' if over else 'ok'}") if over: worst = max(worst, EXIT_OVER) if args.summary: args.summary.parent.mkdir(parents=True, exist_ok=True) args.summary.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8") return worst if __name__ == "__main__": sys.exit(main())