#!/usr/bin/env python3 """Measure the store's hot paths against a production-shaped SQLite file. Phase 14 exists because `TODO.md:7` says "pages take forever to load and seem to lock up the database". Task 01's job is the **before**: one run of this script is the baseline in `data/logs/NOTES-store-perf.md`, and task 04 runs it again as the "after". Nothing here changes behaviour — it reads, times, `EXPLAIN`s, and cleans up after itself. uv run python scripts/store_perf_probe.py # baseline (builds the store once) uv run python scripts/store_perf_probe.py --rebuild # force a fresh seed uv run python scripts/store_perf_probe.py --dir /tmp/perf # measure on tmpfs (RAM: faster) uv run python scripts/store_perf_probe.py --out data/logs/store-perf-after It writes two files beside the numbers it prints: `.json` (everything, machine-readable) and `.md` (the same tables as markdown, to paste into the NOTES). The seeded store itself lives in `data/perf/` (gitignored, like the dev DB) and is reused while it is fresh — the same physical file has to be the one timed before and after the fixes, or the two numbers describe two databases. The shape, the timings and the machine specs all come from `tests/store_perf.py`, which is also what the pytest budget tests of this phase use: one definition of "production-shaped", because two of those drift. """ from __future__ import annotations import argparse import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] for entry in (ROOT / "tests", ROOT / "src"): # `tests/store_perf.py` is not a package (pytest imports it the same way via conftest), and # a bare checkout has no installed `dashboards` either. Both paths, or nothing works. if str(entry) not in sys.path: sys.path.insert(0, str(entry)) import store_perf # noqa: E402 - after the sys.path fixup above, by design def log(message: str) -> None: print(f"[{time.strftime('%H:%M:%S')}] {message}", flush=True) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--dir", default=None, help="where the seeded store lives (default: $DASH_PERF_STORE_DIR " "or data/perf — a real block device, unlike pytest's tmpfs)") parser.add_argument("--rebuild", action="store_true", help="throw the cached store away and seed a new one") parser.add_argument("--out", default=str(ROOT / "data" / "logs" / "store-perf-baseline"), help="prefix for the report files (default: data/logs/store-perf-baseline)") parser.add_argument("--race-iterations", type=int, default=100, help="how many 30-sample inserts to race against stats()/maintain() " "(task 01 says ~100)") parser.add_argument("--race-seconds", type=float, default=store_perf.MAX_RACE_SECONDS, help=f"wall-clock budget per race pass (default " f"{store_perf.MAX_RACE_SECONDS:.0f} s — one stats() pass costs " "seconds on this store, so the pass is bounded by time as well as " "by the insert count)") parser.add_argument("--dev-db", default=None, help="also time the three hot reads against this *real* database file, " "opened read-only (e.g. data/dashboards.sqlite3). Nothing here " "writes to it and no test ever passes this flag — the hermetic " "suite never opens the operator's store.") parser.add_argument("--no-http", action="store_true", help="skip the TestClient pass (/api/health, /api/metrics, " "/api/system/history) — store-only numbers") args = parser.parse_args(argv) path = store_perf.ensure_seeded(args.dir, force=args.rebuild, log=log) store = store_perf.open_store(path) try: report = store_perf.measure(store, race_iterations=args.race_iterations, race_max_seconds=args.race_seconds, with_http=not args.no_http, log=log) finally: store.close() if args.dev_db: log(f"read-only pass over the real store: {args.dev_db}") report["dev_db"] = store_perf.measure_dev_db(args.dev_db) written = store_perf.write_report(report, Path(args.out)) print() print(store_perf.render_markdown(report)) print() print(f"report: {written['md']}\n {written['json']}") return 0 if __name__ == "__main__": raise SystemExit(main())