Close the plan: screenshots and a measured performance pass

- docs/screenshots: nineteen pages captured with headless Chromium from a
  running instance; README gains a screenshot section
- tests/perf: opt-in load test (NOEMA_PERF=1) seeding 60k events, 6k
  memories and 1.5k beliefs across two minds, timing sixteen hot reads
  against a 400 ms ceiling and rejecting sequential-scan plans
- migration 0016: indexes the test found missing (memories by mind and
  recency, beliefs by mind and confidence, events by mind and category,
  belief versions by mind and time)
- docs/performance.md records the numbers and how to rerun
This commit is contained in:
Andy Dixon
2026-07-06 23:18:31 +01:00
parent 89580c0a79
commit d9ae91e406
25 changed files with 264 additions and 2 deletions
+50
View File
@@ -0,0 +1,50 @@
# Performance pass
`tests/perf` is an opt-in load test. It seeds a fresh schema with a realistic
volume for one mind plus a second "noise" mind (so scoping matters), times
the hot read paths in steady state, and rejects any plan that sequentially
scans the three large tables.
```
NOEMA_PERF=1 NOEMA_TEST_DATABASE_URL=postgres://... go test -count=1 -v -run TestHotPaths ./tests/perf/
```
Seed: 60 000 events, 6 000 memories, 1 500 beliefs with evidence. Ceiling per
read: 400 ms (a regression guard, not a target).
## Results (PostgreSQL 16 in Docker, laptop, 2026-09-14)
| Read | Before 0016 | After 0016 |
|---|---|---|
| events recent 50 | 3.2 ms | 1.2 ms |
| events by type | 1.8 ms | 1.1 ms |
| events after seq (runtime cursor) | 2.2 ms | 1.2 ms |
| events summary search | 3.3 ms | 1.4 ms |
| events stats (group by category) | 13.8 ms | 15.8 ms |
| memory list 50 | 4.1 ms (seq scan) | 0.5 ms |
| memory text search | 1.7 ms | 0.5 ms |
| memory fused retrieve | 31 ms | 27 ms |
| beliefs list | 3.6 ms (seq scan) | 3.6 ms |
| beliefs search / contested / uncertain / stats / recent changes | ≤ 3 ms | ≤ 3 ms |
| snapshot capture (all state tables) | 288 ms | 296 ms |
Migration `0016_hot_path_indexes` adds the indexes the planner needed for the
two list orders that had fallen back to sequential scans, plus
`(mind_id, category, seq)` on events for replay's stimulus scan and
`(mind_id, created_at)` on belief versions for the recent-changes view.
## What was deliberately left alone
- **Fused retrieval (~27 ms)** is the most expensive per-cycle read; it runs a
ranked full-text query capped at 50 candidates plus optional vector search.
It is well inside the cycle budget; tune only if cycle latency becomes visible.
- **Snapshot capture (~300 ms)** serialises every state table; it is an
operator action, not a cycle path.
- **Event stats (~16 ms)** is a group-by over all of a mind's events, used on
the overview page. A materialised counter would be premature at this volume.
## Rerun before touching queries
The plan checks fail loudly when a future query drifts off its index, which is
the cheapest way to catch a regression that would otherwise only show up with
months of history.
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB