Files
dashboards/scripts/store_perf_probe.py
T
ducoterra 569ca25fdb phase: 14_store_performance
**Phase 14 `store_performance` — final verification pass (all 4 tasks already in `complete/`)**

- Re-verified every task's work in the tree: read caches (`series_names`/`stats`), capped compound `pick_res_many`, `history_many` + `api._history_batch`, windowed `maintain()` + per-resolution prune + `PRAGMA optimize`/`automatic_analyze=0`/WAL pragmas, `store_perf.BUDGETS` (14 ceilings), probe, live watch.
- **Completion criteria:** baseline+after side by side in `data/logs/NOTES-store-perf.md` ✅ · budget tests green (`uv run pytest -m store_perf --store-perf` → **4 passed**, 14/14 inside) ✅ · 25-min dev-server watch re-run **independently** (74 passes, `scripts/watch-store-perf.py`): **0 `store-error`** (total still 190, newest 2026‑10‑18 06:33:30), WAL 7.77 MB ≤ 67.1 MB, medians 1.7–3.2 ms, p95 720/613 ms = the documented cold-cache window; a 3rd 27-pass watch on a freshly restarted server reproduced it (3.1/708, 1.8/572) ✅ · `maintain()` 5,368 ms → 2.7–6.8 ms and WAL bounded across two cycles (test + notes) ✅ · no behaviour change (all four history routes pinned against `query()`) ✅
- **Full gates:** `uv run pytest` → **1267 passed, 15 skipped**; `bash .agents/validate.sh` → **exit 0**, coverage **98.75%** (floor 94), `ruff check src tests agent` clean, `pyright` 0 errors, `node --check` ok, repo hygiene ok; `uv run pytest -m live --live` → **11 passed**; dev server up, 9/9 sources `ok`, pages 200 in 1.5–3.3 ms.
- **Docs fixed:** README/AGENTS still claimed a flat "20 s per pass" while `host_agent.py` (in this same uncommitted change) scales the budget to 5 s × disk count — corrected in both plus the two illustrative "20 s of wedged disk" sentences; broken bold (`` `driveripper** ``) repaired; stale gap-4 title + "tuning is its own phase" wording updated to say phase 14 ran; README's absolute "nothing … is answered by a whole-table scan" qualified to *per call* with the cold-window cost named; "third opt-in marker" → second; conftest `--store-perf` help named the real marker; **`data/logs/watch-store-perf.py` moved to `scripts/watch-store-perf.py`** (both docs told a clone to run a gitignored path) with root-anchored paths, `DASH_WATCH_OUT`, and dead code removed.
- **New finding recorded:** gap 9's "~10 s process-wide stall is still gap 4's / `14_store_performance`'s problem" is now closed — coincident >8 s `llm` + >12 s `ups` raw-stream gaps went 393 (00:00–12:00) and 198 (12:00–21:36) to **0** across 110+ min of the phase-14 store; written into AGENTS.md gaps 4 and 9 and the notes table.
- **Deviations:** did *not* pull either deferred cost knob (`STATS_CACHE_TTL_SECONDS`, warming caches from the maintenance tick) — AGENTS.md records that decision and its reasoning; `uv pip list --outdated` still reports certifi/fastapi/starlette/websockets (pre-existing advisory, no dependency touched this phase, gate treats it as advisory). Overwrote nothing: task 04's watch preserved as `store-perf-watch.task04.*`, my two runs as `.verify25.*` / `.current-code.*`.
- **Next pending phase:** `15_agent_mtls`, then `16_agent_smart_pass_budget` (owns the scaled-budget tests + its doc criterion, now partly pre-satisfied by this pass), `17_installer_device_filter`.
2026-10-19 23:44:35 -04:00

96 lines
4.7 KiB
Python
Executable File

#!/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: `<out>.json` (everything, machine-readable)
and `<out>.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())