**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`.
96 lines
4.7 KiB
Python
Executable File
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())
|