Skip to content

test(perf-gate): run benchmarks on the benchmark host, add the engine suite, and publish engine baselines - #273

Merged
jsam merged 18 commits into
mainfrom
fm/bench-machine
Oct 5, 2026
Merged

jsam merged 18 commits into
mainfrom
fm/bench-machine

Conversation

@jsam

@jsam jsam commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Captain's intent: make the dedicated benchmark the engine performance gate; heavy perf runs go there, the dev box runs only mandatory checks. Scope, as merged into this branch: (1) scripts/perf-gate-remote.sh plus make perf-gate-remote / bench-engine-remote / bench-sessions-remote / PRE_PR_PERF: measures a commit on the host over SSH (fetch or push the commit, build release there, run detached so a dropped connection does not stop it, --attach to collect, copy results back to target/perf-gate/remote/), following the host rules: one benchmark at a time via the shared ~/perf-gate-remote/lock (--wait-lock queues, also waits out foreign inputlayer-server processes), nproc+commit+command recorded in bench.txt, never leave servers running; pins gate clients to CPUs 0-7 and servers to 8-31 (new perf-gate.sh --gate-cpus). (2) Engine suite fixtures in perf-gate (rule_query, unbound_query, writes, claims, why, sessions, memory_facts, memory_graphs, recovery, insert_async) the captain asked for after auditing coverage: measured and reported, deliberately NOT in policy.toml's required set, and the default run set stays the gate's eight fixtures; plus perf-gate summary (absolute numbers), --no-verdict, and the environment recording host CPU count instead of the pinned affinity. (3) bench-sessions (#254's harness) now distinguishes late deltas from missing ones (schema sessions/v2): v1 counted a delta slower than the 10 s probe timeout as missing and its arrival as stray, which made a saturated server look like it loses deltas. (4) Publish the numbers already measured on the host: perf-gate/baselines/engine-baseline-2026-10-04.md (main 25058f1), session-scale-2026-10-05.md (main 8b574ef), A/A calibration records (30-round A/A INCONCLUSIVE on 4/22 required metrics, so the approved baseline e732cd5 is deliberately NOT moved), and a dated 'Engine Baseline' section in BENCHMARKS.md added beside the existing published numbers - the existing 6.83 ms / 11.3 s / 1,652x numbers and their table must not be changed, caveated or removed. Engine only, no end-to-end decision numbers. The benchmark machine is now shut down: do not run benchmarks; validate locally (make perf-gate-check, pre-pr selftest).

What Changed

  • Adds scripts/perf-gate-remote.sh and the make perf-gate-remote / bench-engine-remote / bench-sessions-remote targets, plus PRE_PR_PERF for make pre-pr. Together they measure a commit on the benchmark host sam-dev-benchmarks over SSH: the commit is built in release mode on the host and runs detached, so --attach can collect results after a dropped connection. Results are copied back to target/perf-gate/remote/<stamp>. Only one benchmark runs at a time, guarded by a shared lock that --wait-lock queues on. Each run records nproc, commit and command in bench.txt and leaves no server running. A new perf-gate.sh --gate-cpus option pins gate clients to CPUs 0-7 and servers to 8-31.
  • Adds engine-suite fixtures to perf-gate: rule_query, unbound_query, writes, claims, why, sessions, memory_facts, memory_graphs, recovery and insert_async. They are measured and reported but are not in the required policy set, and the default run set is still the gate's eight fixtures. Also adds a perf-gate summary command for absolute numbers and a --no-verdict flag. The environment record now stores the host CPU count instead of the pinned affinity.
  • bench-sessions output moves to schema sessions/v2, which separates late deltas from missing ones and counts late deltas against the latency budget. The branch also publishes numbers already measured on the host: the engine baseline from 2026-10-04, the session-scale results from 2026-10-05, and A/A calibration reports under perf-gate/baselines/. A dated "Engine Baseline" section is added to BENCHMARKS.md next to the existing published numbers, which are unchanged. README, TESTING and Makefile docs are updated to match.

Risk Assessment

⚠️ Medium: The change is benchmark tooling and docs only and meets the stated intent: the default set stays the eight gate fixtures, policy.toml and approved.toml are unchanged, and the existing BENCHMARKS.md numbers are untouched. Two tooling defects should be fixed soon: late session deltas slip past the p99 budget, and a busy host is reported as a gate fail (exit 1 instead of 4). Neither affects production code.

Testing

Ran perf-gate's unit tests and the pre-pr selftest; both pass. The real remote script and make targets were run against a stand-in benchmark host built locally (ssh/scp shims, throwaway clone, stubbed benchmark scripts, host-side pgrep/pkill limited to harness processes, so another session's real inputlayer-server was not touched). The happy path, exit codes for busy (4), setup error (3), gate FAIL (1) and dropped connection (5), lock queueing, waiting out a foreign server, cleanup of a leftover server, --attach and concurrent runs all behaved as documented. perf-gate summary and compare on the published run files show absolute numbers, exit 3 when a fixture run failed, keep engine fixtures out of the gate, and give the A/A record INCONCLUSIVE. A new focused unit test fails without the late-delta budget rule and passes with it, but that is unit-level coverage, not a live run. Real measurements (pinned gate runs, sessions late-delta drain against a server, recorded host CPU count) were not run because the user forbade benchmarks. This is a CLI/script change with no UI, so there are no screenshots; the evidence is CLI transcripts.

  • Live validation: ✅ go - 13 of 16 scenarios driven live against the product
Scenario Result Live Evidence
Developer runs make perf-gate-remote on a commit origin lacks: it is pushed to the host, built/run detached, and run.json/report.md/verdict.json/summary.md/bench.txt/gate.log are copied to target/pe… ✅ pass live remote-R1-happy-path.txt, remote-R1-results/
Gate FAIL status on the host comes back as local exit 1 with results copied ✅ pass live remote-R2-gate-fail-status.txt
Host busy (lock held, no wait): exit 4, gate.log copied explaining the refusal, Results line printed ✅ pass live remote-R3-busy-lock.txt
--wait-lock queues behind another benchmark's lock and then runs ✅ pass live remote-R4-wait-lock-queues.txt
A foreign inputlayer-server on the host gives exit 4; with --wait-lock the run waits until it exits, then measures ✅ pass live remote-R5-foreign-server-busy.txt, remote-R5b-wait-out-foreign-server.txt
Host clone with local changes: refuses to check out, exit 3, gate.log explains ✅ pass live remote-R6-dirty-clone-setup-error.txt
A server the benchmark leaves running is killed when the run ends (no server left on the host) ✅ pass live remote-R7-no-server-left.txt (no harness servers left afterwards; another session's real server untouched)
Dropped SSH connection: local exits 5 with an --attach hint, the host run continues, and --attach STAMP collects the finished results ✅ pass live remote-R8c-dropped-connection-exit.txt, remote-R8a-dropped-connection.txt, remote-R8b-attach.txt
Two concurrent invocations from different script versions each run their own <stamp>.run.sh, run one after the other, and both exit 0 ✅ pass live remote-R9-concurrent-A.txt, remote-R9-concurrent-B.txt, remote-host-state/*.run.sh
make bench-engine-remote runs --aa --baseline-rev &lt;sha&gt; --fixtures engine --no-verdict pinned; make bench-sessions-remote runs bench-sessions.sh with servers pinned to 8-31 ✅ pass live remote-R10-make-bench-engine-remote.txt, remote-R11-make-bench-sessions-remote.txt
perf-gate summary prints absolute-number tables from a published run file, and exits 3 listing the failed run when a fixture run errored ✅ pass live summary-engine-baseline-2026-10-04-engine.md, summary-injected-failure.md
Engine-suite fixtures are measured and reported but not in policy.toml's required set (compare lists them under diagnostic; the 22 required metrics are the gate's eight fixtures) ✅ pass live engine-run-vs-policy.report.md
The published A/A record 25058f1 judges INCONCLUSIVE on 4/22 required metrics, and approved.toml still names e732cd5 ✅ pass live aa-25058f16-compare.txt
BENCHMARKS.md keeps 6.83 ms / 11.3 s / 1,652x unchanged and adds a dated Engine Baseline section ⏸️ untested no The prior payload did not establish a live result: this is a documentation change and was checked only by reading git diff against the base, not by driving the running product
Sessions v2: a run with late deltas fails --max-delta-p99-ms even when the on-time p99 is within budget; late deltas are not missing ones; a delta is matched to its probe by session + ETA state row ⏸️ untested no The prior payload did not establish a live result: it is covered only by new unit tests of verdict()/carries(). Driving late deltas live needs a saturated release server under bench-sessions, which is…
A pinned gate run (--gate-cpus) and the engine suite measured live record the host CPU count, not the pinned affinity ⏸️ untested no Needs a real perf-gate run (release builds and measurement). The user explicitly said not to run benchmarks and the benchmark host is shut down; only the pass-through of --gate-cpus 0-7 to perf-gate.s…
Evidence: perf-gate-remote happy path (make perf-gate-remote)
./scripts/perf-gate-remote.sh --rev HEAD 
=== fakehost: fetch eacdb44ff50644033cf8e5c6006215e53c7d51a6 ===
origin lacks eacdb44ff50644033cf8e5c6006215e53c7d51a6; pushing it to fakehost:bench/inputlayer
=== fakehost: run 20261005T161512Z-eacdb44f (scripts/perf-gate.sh ) ===
host: computeinstance-e00sx5ndp6kwb7pqfv
nproc: 32
commit: eacdb44ff50644033cf8e5c6006215e53c7d51a6
command: scripts/perf-gate.sh  --server-cpus 8-31 --gate-cpus 0-7
started: 2026-10-05T16:15:45Z
load average at start: 4.43 5.22 4.51 2/1680 3574498
stub perf-gate.sh pid 3574500 args: --server-cpus 8-31 --gate-cpus 0-7

Results: /tmp/pgr-rF3f/local/target/perf-gate/remote/20261005T161512Z-eacdb44f (exit status 0)
exit 0
  • Evidence: Files copied back by the happy-path run (bench.txt, gate.log, run.json, ...) (local file: ~/.no-mistakes/evidence/01M46D299DA54364PHDNT2KRZK/remote-R1-results)
Evidence: Busy host (lock held) exits 4 and still copies gate.log
$ scripts/perf-gate-remote.sh 
=== fakehost: fetch eacdb44ff50644033cf8e5c6006215e53c7d51a6 ===
=== fakehost: run 20261005T161610Z-eacdb44f (scripts/perf-gate.sh ) ===
perf-gate-remote: another benchmark holds /tmp/pgr-rF3f/hosthome/perf-gate-remote/lock; waiting up to 0s
perf-gate-remote: lock still held

Results: /tmp/pgr-rF3f/local/target/perf-gate/remote/20261005T161610Z-eacdb44f (exit status 4)
exit 4
Evidence: Dirty host clone exits 3
$ scripts/perf-gate-remote.sh 
=== fakehost: fetch eacdb44ff50644033cf8e5c6006215e53c7d51a6 ===
=== fakehost: run 20261005T161656Z-eacdb44f (scripts/perf-gate.sh ) ===
perf-gate-remote: /tmp/pgr-rF3f/hosthome/bench/inputlayer has local changes; refusing to check out over them

Results: /tmp/pgr-rF3f/local/target/perf-gate/remote/20261005T161656Z-eacdb44f (exit status 3)
exit 3
Evidence: Dropped connection then --attach
$ scripts/perf-gate-remote.sh --attach 20261005T161731Z-eacdb44f
host: computeinstance-e00sx5ndp6kwb7pqfv
nproc: 32
commit: eacdb44ff50644033cf8e5c6006215e53c7d51a6
command: scripts/perf-gate.sh  --server-cpus 8-31 --gate-cpus 0-7
started: 2026-10-05T16:17:32Z
load average at start: 4.50 4.80 4.42 7/1645 3597663
stub perf-gate.sh pid 3597665 args: --server-cpus 8-31 --gate-cpus 0-7

Results: /tmp/pgr-rF3f/local/target/perf-gate/remote/20261005T161731Z-eacdb44f (exit status 0)
exit 0
  • Evidence: Stand-in host state: per-run runners, status files, bench.txt, logs (local file: ~/.no-mistakes/evidence/01M46D299DA54364PHDNT2KRZK/remote-host-state)
Evidence: perf-gate summary of published engine baseline
- server: baseline `25058f160c2360d7c126c1d530db235e165bf792` (binary sha256 cd5103a6c32a); candidate `25058f160c2360d7c126c1d530db235e165bf792` (binary sha256 cd5103a6c32a)
- host `computeinstance-e00nfsdgf9dzq987g3`: AMD EPYC-Genoa Processor (32 CPUs, 125 GB), kernel 6.11.0-1016-nvidia, governor unknown, data on ext4, server CPUs 8-31
- profile `standard`, 10 rounds per arm, load average start `1.78 1.67 1.10 1/464 57942`, end `0.54 0.89 0.95 1/448 66717`
- build: rustc 1.99.0 (b940084d7 2026-09-28); cargo build --release --all-features

| fixture | latency series | p50 | p99 | p50 range over runs | runs | samples |
|---|---|---|---|---|---|---|
| claims | lose_ack_us | 0.432 ms | 0.493 ms | 0.406 ms..0.441 ms | 20 | 3000 |
| claims | race_ack_us | 3.814 ms | 10.043 ms | 3.706 ms..4.081 ms | 20 | 6400 |
| claims | win_ack_us | 1.889 ms | 7.154 ms | 1.794 ms..1.999 ms | 20 | 3000 |
| insert_async | ack_us | 0.143 ms | 0.241 ms | 0.125 ms..0.152 ms | 20 | 4000 |
| insert_async | concurrent_ack_us | 0.235 ms | 0.657 ms | 0.203 ms..0.264 ms | 20 | 4000 |
| recovery | first_query_us | 3.913 ms | 4.147 ms | 2.705 ms..4.209 ms | 20 | 60 |
| recovery | restart_ready_us | 103.771 ms | 153.812 ms | 102.841 ms..105.657 ms | 20 | 60 |
| rule_query | concurrent_latency_us | 1.980 ms | 2.449 ms | 1.944 ms..2.036 ms | 20 | 16000 |
| rule_query | latency_us | 2.002 ms | 2.199 ms | 1.986 ms..2.065 ms | 20 | 6000 |
| sessions | ack_us | 4.502 ms | 10.233 ms | 4.078 ms..5.131 ms | 20 | 2000 |
| sessions | delta_us | 8.302 ms | 14.121 ms | 7.794 ms..8.931 ms | 20 | 2000 |
| sessions | subscribe_us | 3.414 ms | 3.895 ms | 3.317 ms..3.567 ms | 20 | 2000 |
| unbound_query | concurrent_latency_us | 45.855 ms | 53.011 ms | 45.214 ms..46.230 ms | 20 | 800 |
| unbound_query | latency_us | 45.474 ms | 46.009 ms | 45.229 ms..46.038 ms | 20 | 800 |
| why | reach_us | 1.67 s | 1.68 s | 1.65 s..1.70 s | 20 | 400 |
| why | two_hop_us | 3.384 ms | 3.523 ms | 3.305 ms..3.513 ms | 20 | 400 |
| writes | conditional_delete_ack_us | 4.466 ms | 10.981 ms | 4.361 ms..4.706 ms | 20 | 3000 |
| writes | delete_ack_us | 1.946 ms | 7.488 ms | 1.817 ms..2.118 ms | 20 | 3000 |
| writes | update_ack_us | 2.107 ms | 8.194 ms | 1.981 ms..2.416 ms | 20 | 3000 |

| fixture | rate | per second | range over runs | runs |
|---|---|---|---|---|
| insert_async | concurrent_facts_per_sec | 15103 | 14018..17992 | 20 |
| insert_async | facts_per_sec | 5809 | 5155..6647 | 20 |
| rule_query | queries_per_sec | 3860 | 3429..4008 | 20 |
| sessions | subscriptions_per_sec | 56 | 55..58 | 20 |
| unbound_query | queries_per_sec | 81 | 80..82 | 20 |

| fixture | gauge | value | range over runs | runs |
|---|---|---|---|---|
| claims | server_peak_rss_kb | 41314 | 40604..41864 | 20 |
| insert_async | server_peak_rss_kb | 38774 | 38432..39268 | 20 |
| memory | peak_bytes_per_fact | 425 | 375..433 | 20 |
| memory | rss_bytes_per_fact | 425 | 375..433 | 20 |
| memory | rss_first_graph_kb | 12544 | 12288..15104 | 20 |
| memory | rss_idle_kb | 36448 | 36200..36744 | 20 |
| memory | rss_per_graph_kb | 72 | 0..621 | 20 |
| memory | server_peak_rss_kb | 87100 | 86360..87636 | 20 |
| recovery | server_peak_rss_kb | 53104 | 52364..54612 | 20 |
| rule_query | server_peak_rss_kb | 52950 | 52076..54404 | 20 |
| sessions | rss_per_session_kb | 87 | 74..92 | 20 |
| sessions | server_peak_rss_kb | 63332 | 61960..65136 | 20 |
| unbound_query | server_peak_rss_kb | 63058 | 62264..68976 | 20 |
| why | server_peak_rss_kb | 40160 | 39536..40572 | 20 |
| writes | server_peak_rss_kb | 40112 | 39748..40568 | 20 |
Evidence: Engine run judged by policy.toml (engine fixtures under diagnostic)
## Performance gate: INVALID

Policy **provisional**: cost budget p50 +5%, p99 +10%, throughput -5%; 95% interval of the median paired per-round ratio. Budgets are the plan's proposed ceilings, pending captain decision D1; tightening is always allowed.

- baseline: `25058f160c2360d7c126c1d530db235e165bf792` (binary sha256 cd5103a6c32a)
- candidate: `25058f160c2360d7c126c1d530db235e165bf792` (binary sha256 cd5103a6c32a)
- profile `standard`, 10 rounds, fixtures rule_query, unbound_query, writes, claims, why, sessions, memory, recovery, insert_async
- host `computeinstance-e00nfsdgf9dzq987g3`: AMD EPYC-Genoa Processor (32 CPUs, 125 GB), kernel 6.11.0-1016-nvidia, governor unknown, data on ext4 (~/bench/inputlayer/target/perf-gate/runs/20261004T193212Z/servers), server CPUs 8-31
- load average: start `1.78 1.67 1.10 1/464 57942`, end `0.54 0.89 0.95 1/448 66717`
- build: rustc 1.99.0 (b940084d7 2026-09-28); cargo build --release --all-features

\### Problems

- bound_query.concurrent_latency_us.p99: baseline: 0 valid rounds, need 6
- bound_query.latency_us.p50: baseline: 0 valid rounds, need 6
- bound_query.latency_us.p99: baseline: 0 valid rounds, need 6
- bound_query.queries_per_sec: baseline: 0 valid rounds, need 6
- cheap_query.latency_us.p50: baseline: 0 valid rounds, need 6
- cheap_query.latency_us.p99: baseline: 0 valid rounds, need 6
- cheap_query.queries_per_sec: baseline: 0 valid rounds, need 6
- delta_fanout.delta_us.p50: baseline: 0 valid rounds, need 6
- delta_fanout.delta_us.p99: baseline: 0 valid rounds, need 6
- delta_fanout.last_agent_us.p99: baseline: 0 valid rounds, need 6
- delta_first.first_delta_us.p50: baseline: 0 valid rounds, need 6
- delta_first.warm_delta_us.p50: baseline: 0 valid rounds, need 6
- delta_single.delta_us.p50: baseline: 0 valid rounds, need 6
- delta_single.delta_us.p99: baseline: 0 valid rounds, need 6
- insert_batch.ack_us.p50: baseline: 0 valid rounds, need 6
- insert_batch.facts_per_sec: baseline: 0 valid rounds, need 6
- insert_single.ack_us.p50: baseline: 0 valid rounds, need 6
- insert_single.ack_us.p99: baseline: 0 valid rounds, need 6
- insert_single.concurrent_facts_per_sec: baseline: 0 valid rounds, need 6
- insert_single.facts_per_sec: baseline: 0 valid rounds, need 6
- interference.delta_us.p50: baseline: 0 valid rounds, need 6
- interference.delta_us.p99: baseline: 0 valid rounds, need 6

\### Required metrics

| metric | baseline | candidate | cost ratio | interval | budget | baseline spread | status |
|---|---|---|---|---|---|---|---|
| bound_query.concurrent_latency_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| bound_query.latency_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| bound_query.latency_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| bound_query.queries_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| cheap_query.latency_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| cheap_query.latency_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| cheap_query.queries_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_fanout.delta_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_fanout.delta_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_fanout.last_agent_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_first.first_delta_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_first.warm_delta_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_single.delta_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| delta_single.delta_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_batch.ack_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_batch.facts_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_single.ack_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_single.ack_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_single.concurrent_facts_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| insert_single.facts_per_sec | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| interference.delta_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline: 0 valid rounds, need 6 |
| interference.delta_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline: 0 valid rounds, need 6 |

\### Diagnostic metrics (not gated)

| metric | baseline | candidate | cost ratio | interval | budget | baseline spread | status |
|---|---|---|---|---|---|---|---|
| claims.lose_ack_us.p50 | 0.429 ms | 0.433 ms | 0.998 | 0.991..1.028 | 1.05 | 7.7% | PASS |
| claims.lose_ack_us.p99 | 0.497 ms | 0.493 ms | 0.985 | 0.926..1.058 | 1.10 | 11.1% | PASS |
| claims.race_ack_us.p50 | 3.865 ms | 3.797 ms | 1.000 | 0.924..1.012 | 1.05 | 9.7% | PASS |
| claims.race_ack_us.p99 | 9.672 ms | 10.043 ms | 1.051 | 0.673..1.578 | 1.10 | 99.1% | INCONCLUSIVE |
| claims.win_ack_us.p50 | 1.859 ms | 1.895 ms | 1.021 | 0.967..1.033 | 1.05 | 9.7% | PASS |
| claims.win_ack_us.p99 | 7.902 ms | 7.127 ms | 0.984 | 0.626..1.424 | 1.10 | 73.2% | INCONCLUSIVE |
| insert_async.ack_us.p50 | 0.143 ms | 0.145 ms | 1.000 | 0.898..1.036 | 1.05 | 18.9% | PASS |
| insert_async.ack_us.p99 | 0.232 ms | 0.257 ms | 1.129 | 0.895..1.380 | 1.10 | 34.1% | INCONCLUSIVE |
| insert_async.concurrent_ack_us.p50 | 0.235 ms | 0.234 ms | 0.966 | 0.895..1.117 | 1.05 | 12.3% | INCONCLUSIVE |
| insert_async.concurrent_ack_us.p99 | 0.630 ms | 0.664 ms | 0.983 | 0.805..1.184 | 1.10 | 60.3% | INCONCLUSIVE |
| insert_async.concurrent_facts_per_sec | 15059/s | 15435/s | 0.964 | 0.897..1.073 | 1.05 | 11.9% | INCONCLUSIVE |
| insert_async.facts_per_sec | 5809/s | 5797/s | 0.990 | 0.903..1.071 | 1.05 | 25.7% | INCONCLUSIVE |
| recovery.first_query_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline round 1: 3 samples, need 20 |
| recovery.first_query_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 3 samples, need 100 |
| recovery.restart_ready_us.p50 | - | - | - | - | 1.05 | - | INVALID: baseline round 1: 3 samples, need 20 |
| recovery.restart_ready_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 3 samples, need 100 |
| rule_query.concurrent_latency_us.p50 | 1.978 ms | 1.982 ms | 0.993 | 0.988..1.011 | 1.05 | 4.3% | PASS |
| rule_query.concurrent_latency_us.p99 | 2.457 ms | 2.443 ms | 0.996 | 0.865..1.328 | 1.10 | 36.8% | INCONCLUSIVE |
| rule_query.latency_us.p50 | 2.009 ms | 1.998 ms | 0.997 | 0.985..1.014 | 1.05 | 3.7% | PASS |
| rule_query.latency_us.p99 | 2.201 ms | 2.152 ms | 0.991 | 0.965..1.008 | 1.10 | 5.1% | PASS |
| rule_query.queries_per_sec | 3865/s | 3860/s | 0.998 | 0.937..1.033 | 1.05 | 15.0% | PASS |
| sessions.ack_us.p50 | 4.517 ms | 4.485 ms | 0.994 | 0.925..1.124 | 1.05 | 19.9% | INCONCLUSIVE |
| sessions.ack_us.p99 | 10.430 ms | 10.086 ms | 1.118 | 0.839..1.308 | 1.10 | 82.0% | INCONCLUSIVE |
| sessions.delta_us.p50 | 8.352 ms | 8.213 ms | 0.996 | 0.945..1.069 | 1.05 | 10.2% | INCONCLUSIVE |
| sessions.delta_us.p99 | 14.111 ms | 14.232 ms | 0.978 | 0.784..1.154 | 1.10 | 44.3% | INCONCLUSIVE |
| sessions.subscribe_us.p50 | 3.385 ms | 3.469 ms | 1.031 | 0.975..1.059 | 1.05 | 4.4% | INCONCLUSIVE |
| sessions.subscribe_us.p99 | 3.892 ms | 3.895 ms | 0.998 | 0.973..1.028 | 1.10 | 4.4% | PASS |
| sessions.subscriptions_per_sec | 57/s | 56/s | 1.010 | 0.965..1.023 | 1.05 | 4.1% | PASS |
| unbound_query.concurrent_latency_us.p50 | 45.897 ms | 45.827 ms | 0.997 | 0.994..1.007 | 1.05 | 1.6% | PASS |
| unbound_query.concurrent_latency_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 40 samples, need 100 |
| unbound_query.latency_us.p50 | 45.447 ms | 45.493 ms | 1.001 | 0.997..1.003 | 1.05 | 1.8% | PASS |
| unbound_query.latency_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 40 samples, need 100 |
| unbound_query.queries_per_sec | 81/s | 81/s | 0.997 | 0.989..1.005 | 1.05 | 1.5% | PASS |
| why.reach_us.p50 | 1675.395 ms | 1667.321 ms | 0.998 | 0.988..1.010 | 1.05 | 3.4% | PASS |
| why.reach_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 20 samples, need 100 |
| why.two_hop_us.p50 | 3.397 ms | 3.373 ms | 0.987 | 0.976..1.023 | 1.05 | 6.1% | PASS |
| why.two_hop_us.p99 | - | - | - | - | 1.10 | - | INVALID: baseline round 1: 20 samples, need 100 |
| writes.conditional_delete_ack_us.p50 | 4.447 ms | 4.529 ms | 1.011 | 0.998..1.032 | 1.05 | 5.0% | PASS |
| writes.conditional_delete_ack_us.p99 | 11.274 ms | 10.896 ms | 0.938 | 0.803..1.162 | 1.10 | 40.6% | INCONCLUSIVE |
| writes.delete_ack_us.p50 | 1.927 ms | 2.002 ms | 1.033 | 0.979..1.058 | 1.05 | 14.8% | INCONCLUSIVE |
| writes.delete_ack_us.p99 | 7.144 ms | 8.018 ms | 1.350 | 0.939..1.501 | 1.10 | 74.1% | INCONCLUSIVE |
| writes.update_ack_us.p50 | 2.136 ms | 2.091 ms | 0.989 | 0.920..1.054 | 1.05 | 20.4% | INCONCLUSIVE |
| writes.update_ack_us.p99 | 6.152 ms | 9.325 ms | 1.504 | 0.771..2.372 | 1.10 | 122.4% | INCONCLUSIVE |
Evidence: Sessions budget regression test: fails without the fix
test sessions::tests::a_late_delta_is_matched_by_session_and_eta_state_row ... ok
test sessions::tests::late_deltas_fail_a_latency_budget_even_when_the_on_time_p99_holds ... FAILED
test sessions::tests::late_deltas_are_not_missing_without_a_budget_or_beyond_its_sizes ... ok
test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 56 filtered out; finished in 0.00s
Evidence: perf-gate unit tests with the fix
test sessions::tests::a_late_delta_is_matched_by_session_and_eta_state_row ... ok
test sessions::tests::late_deltas_fail_a_latency_budget_even_when_the_on_time_p99_holds ... ok
test sessions::tests::late_deltas_are_not_missing_without_a_budget_or_beyond_its_sizes ... ok
test result: ok. 59 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 4 issues (1 error, 1 warning, 2 infos)
  • ⚠️ perf-gate/src/sessions/mod.rs:324 - In sessions/v2, late probes are kept out of load_delta_ms (lines 518-520). The --max-delta-p99-ms budget check still compares only load_delta_ms.p99, and Run::passed() (line 193) ignores late_deltas. A probe whose delta took more than 10 s is by definition over any sub-10 s budget, yet it no longer counts against the budget. Example: --sessions 1000 --max-delta-p99-ms 500 --budget-sessions 1000 with 1 on-time probe at 300 ms and 5 late probes at 15 s. v1 failed this run (missing deltas). v2 reports p99 = 300 ms and passes with exit 0, so a saturated working tree passes the latency budget. Fix: in the budget loop, fail any candidate run with late_deltas &gt; 0, or fold late_delta_ms into the p99 the budget judges. The late count is not split by idle/loaded phase, so counting any late delta against the budget is the simplest sound rule.
  • ⚠️ scripts/perf-gate-remote.sh:210 - When the host is busy, run.sh calls finish 4 (lines 129 and 139) before $STAMP.bench.txt exists. On a setup failure it calls finish 3 (lines 149, 152, 154), also before bench.txt is written. The local side then runs scp -q &#34;$HOST:$STATE/$STAMP.bench.txt&#34; unguarded under set -e. scp fails and the script exits 1, which the header documents as a gate FAIL. The documented 'exit 4 when the host is busy' therefore never happens, gate.log (which explains the refusal) is never copied back, and no Results: line is printed. This affects make pre-pr PRE_PR_PERF=perf-gate-remote and --wait-lock timeouts. Fix: make both scp calls tolerant (|| true, or [ -s ] before the sed at line 212), or write a minimal bench.txt before the lock is taken. Then exit with the remote $STATUS.
  • ℹ️ scripts/perf-gate-remote.sh:113 - Every invocation rewrites the shared ~/perf-gate-remote/run.sh in place (cat &gt; $STATE/run.sh, which truncates the same inode). With --wait-lock, a second invocation does this while the first run's bash is still executing that file. Bash reads a script incrementally, so after the long &#34;$SCRIPT&#34; call the first run resumes at its old byte offset in the new content. If the two callers have different versions of perf-gate-remote.sh (for example two worktrees), it runs garbled commands and may never write its status file, so its follower waits forever. If bash reads during the truncate-then-write window, the first run ends early and writes no status. The local script was made edit-safe with { ... } (be7ec14), but the host-side script was not. Fix: write a per-run copy ($STATE/$STAMP.run.sh) and run that, or wrap the heredoc body in { ...; exit; }.
  • ⚠️ perf-gate/src/main.rs:69 - Simplification: perf-gate summary --arms (arm filtering) is not used by perf-gate.sh, perf-gate-remote.sh, the Makefile or the published baselines, which all summarize every arm pooled. The intent asks only for 'perf-gate summary (absolute numbers)'. Recommend removing the option and the selected filtering it drives in summary.rs.
  • ℹ️ perf-gate/src/fixtures/mod.rs:133 - Simplification: Fixture::parse_group adds the groups gate and all besides engine. Only engine is needed (make bench-engine-remote). The gate's eight remain the default without gate, and all is used by no script or target. The narrower form that meets the intent is the engine group alone.

🔧 Fix applied.
7 issues (1 error, 3 warnings, 3 infos) still open:

  • ⚠️ perf-gate/src/sessions/mod.rs:324 - In sessions/v2, late probes are kept out of load_delta_ms (lines 518-520). The --max-delta-p99-ms budget check still compares only load_delta_ms.p99, and Run::passed() (line 193) ignores late_deltas. A probe whose delta took more than 10 s is by definition over any sub-10 s budget, yet it no longer counts against the budget. Example: --sessions 1000 --max-delta-p99-ms 500 --budget-sessions 1000 with 1 on-time probe at 300 ms and 5 late probes at 15 s. v1 failed this run (missing deltas). v2 reports p99 = 300 ms and passes with exit 0, so a saturated working tree passes the latency budget. Fix: in the budget loop, fail any candidate run with late_deltas &gt; 0, or fold late_delta_ms into the p99 the budget judges. The late count is not split by idle/loaded phase, so counting any late delta against the budget is the simplest sound rule.
  • ⚠️ scripts/perf-gate-remote.sh:210 - When the host is busy, run.sh calls finish 4 (lines 129 and 139) before $STAMP.bench.txt exists. On a setup failure it calls finish 3 (lines 149, 152, 154), also before bench.txt is written. The local side then runs scp -q &#34;$HOST:$STATE/$STAMP.bench.txt&#34; unguarded under set -e. scp fails and the script exits 1, which the header documents as a gate FAIL. The documented 'exit 4 when the host is busy' therefore never happens, gate.log (which explains the refusal) is never copied back, and no Results: line is printed. This affects make pre-pr PRE_PR_PERF=perf-gate-remote and --wait-lock timeouts. Fix: make both scp calls tolerant (|| true, or [ -s ] before the sed at line 212), or write a minimal bench.txt before the lock is taken. Then exit with the remote $STATUS.
  • ℹ️ scripts/perf-gate-remote.sh:113 - Every invocation rewrites the shared ~/perf-gate-remote/run.sh in place (cat &gt; $STATE/run.sh, which truncates the same inode). With --wait-lock, a second invocation does this while the first run's bash is still executing that file. Bash reads a script incrementally, so after the long &#34;$SCRIPT&#34; call the first run resumes at its old byte offset in the new content. If the two callers have different versions of perf-gate-remote.sh (for example two worktrees), it runs garbled commands and may never write its status file, so its follower waits forever. If bash reads during the truncate-then-write window, the first run ends early and writes no status. The local script was made edit-safe with { ... } (be7ec14), but the host-side script was not. Fix: write a per-run copy ($STATE/$STAMP.run.sh) and run that, or wrap the heredoc body in { ...; exit; }.
  • ⚠️ perf-gate/src/main.rs:69 - Simplification: perf-gate summary --arms (arm filtering) is not used by perf-gate.sh, perf-gate-remote.sh, the Makefile or the published baselines, which all summarize every arm pooled. The intent asks only for 'perf-gate summary (absolute numbers)'. Recommend removing the option and the selected filtering it drives in summary.rs.
  • ℹ️ perf-gate/src/fixtures/mod.rs:133 - Simplification: Fixture::parse_group adds the groups gate and all besides engine. Only engine is needed (make bench-engine-remote). The gate's eight remain the default without gate, and all is used by no script or target. The narrower form that meets the intent is the engine group alone.
  • 🚨 perf-gate/target/debug/perf-gate:1 - The round-1 fix commit 3a03e83 also committed perf-gate's Cargo build directory: 1,138 files under perf-gate/target/, including an 81 MB debug binary (perf-gate/target/debug/perf-gate), .cargo-lock files, fingerprints and .rustc_info.json. The fix round introduced this. ab7ebf7 tracked none of these files. The cause is that the root .gitignore only ignores /target/, which is anchored to the repo root, and perf-gate is a standalone workspace with its own target dir, so building it locally leaves untracked artifacts that git add -A picks up. Merging this would add host-specific binaries and about 80 MB to history. Fix: git rm -r --cached perf-gate/target and amend or follow up so no tracked file remains under perf-gate/target/. To stop it happening again, ignore perf-gate/target/ (or change /target/ to target/) in .gitignore.
  • ℹ️ scripts/perf-gate-remote.sh:113 - Round-1 fix: each run now writes ~/perf-gate-remote/&lt;stamp&gt;.run.sh and never deletes it. The old shared run.sh stays on the host too. These are small files that pile up next to the per-stamp logs and status files, which are already kept, so this is consistent with the existing retention. Noted only.

🔧 Fix applied.
4 issues (1 error, 1 warning, 2 infos) still open:

  • ⚠️ perf-gate/src/main.rs:69 - Simplification: perf-gate summary --arms (arm filtering) is not used by perf-gate.sh, perf-gate-remote.sh, the Makefile or the published baselines, which all summarize every arm pooled. The intent asks only for 'perf-gate summary (absolute numbers)'. Recommend removing the option and the selected filtering it drives in summary.rs.
  • ℹ️ perf-gate/src/fixtures/mod.rs:133 - Simplification: Fixture::parse_group adds the groups gate and all besides engine. Only engine is needed (make bench-engine-remote). The gate's eight remain the default without gate, and all is used by no script or target. The narrower form that meets the intent is the engine group alone.
  • 🚨 perf-gate/target/debug/perf-gate:1 - The round-1 fix commit 3a03e83 also committed perf-gate's Cargo build directory: 1,138 files under perf-gate/target/, including an 81 MB debug binary (perf-gate/target/debug/perf-gate), .cargo-lock files, fingerprints and .rustc_info.json. The fix round introduced this. ab7ebf7 tracked none of these files. The cause is that the root .gitignore only ignores /target/, which is anchored to the repo root, and perf-gate is a standalone workspace with its own target dir, so building it locally leaves untracked artifacts that git add -A picks up. Merging this would add host-specific binaries and about 80 MB to history. Fix: git rm -r --cached perf-gate/target and amend or follow up so no tracked file remains under perf-gate/target/. To stop it happening again, ignore perf-gate/target/ (or change /target/ to target/) in .gitignore.
  • ℹ️ scripts/perf-gate-remote.sh:113 - Round-1 fix: each run now writes ~/perf-gate-remote/&lt;stamp&gt;.run.sh and never deletes it. The old shared run.sh stays on the host too. These are small files that pile up next to the per-stamp logs and status files, which are already kept, so this is consistent with the existing retention. Noted only.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 13 of 16 scenarios driven live against the product
Scenario Result Live Evidence
Developer runs make perf-gate-remote on a commit origin lacks: it is pushed to the host, built/run detached, and run.json/report.md/verdict.json/summary.md/bench.txt/gate.log are copied to target/pe… ✅ pass live remote-R1-happy-path.txt, remote-R1-results/
Gate FAIL status on the host comes back as local exit 1 with results copied ✅ pass live remote-R2-gate-fail-status.txt
Host busy (lock held, no wait): exit 4, gate.log copied explaining the refusal, Results line printed ✅ pass live remote-R3-busy-lock.txt
--wait-lock queues behind another benchmark's lock and then runs ✅ pass live remote-R4-wait-lock-queues.txt
A foreign inputlayer-server on the host gives exit 4; with --wait-lock the run waits until it exits, then measures ✅ pass live remote-R5-foreign-server-busy.txt, remote-R5b-wait-out-foreign-server.txt
Host clone with local changes: refuses to check out, exit 3, gate.log explains ✅ pass live remote-R6-dirty-clone-setup-error.txt
A server the benchmark leaves running is killed when the run ends (no server left on the host) ✅ pass live remote-R7-no-server-left.txt (no harness servers left afterwards; another session's real server untouched)
Dropped SSH connection: local exits 5 with an --attach hint, the host run continues, and --attach STAMP collects the finished results ✅ pass live remote-R8c-dropped-connection-exit.txt, remote-R8a-dropped-connection.txt, remote-R8b-attach.txt
Two concurrent invocations from different script versions each run their own <stamp>.run.sh, run one after the other, and both exit 0 ✅ pass live remote-R9-concurrent-A.txt, remote-R9-concurrent-B.txt, remote-host-state/*.run.sh
make bench-engine-remote runs --aa --baseline-rev &lt;sha&gt; --fixtures engine --no-verdict pinned; make bench-sessions-remote runs bench-sessions.sh with servers pinned to 8-31 ✅ pass live remote-R10-make-bench-engine-remote.txt, remote-R11-make-bench-sessions-remote.txt
perf-gate summary prints absolute-number tables from a published run file, and exits 3 listing the failed run when a fixture run errored ✅ pass live summary-engine-baseline-2026-10-04-engine.md, summary-injected-failure.md
Engine-suite fixtures are measured and reported but not in policy.toml's required set (compare lists them under diagnostic; the 22 required metrics are the gate's eight fixtures) ✅ pass live engine-run-vs-policy.report.md
The published A/A record 25058f1 judges INCONCLUSIVE on 4/22 required metrics, and approved.toml still names e732cd5 ✅ pass live aa-25058f16-compare.txt
BENCHMARKS.md keeps 6.83 ms / 11.3 s / 1,652x unchanged and adds a dated Engine Baseline section ⏸️ untested no The prior payload did not establish a live result: this is a documentation change and was checked only by reading git diff against the base, not by driving the running product
Sessions v2: a run with late deltas fails --max-delta-p99-ms even when the on-time p99 is within budget; late deltas are not missing ones; a delta is matched to its probe by session + ETA state row ⏸️ untested no The prior payload did not establish a live result: it is covered only by new unit tests of verdict()/carries(). Driving late deltas live needs a saturated release server under bench-sessions, which is…
A pinned gate run (--gate-cpus) and the engine suite measured live record the host CPU count, not the pinned affinity ⏸️ untested no Needs a real perf-gate run (release builds and measurement). The user explicitly said not to run benchmarks and the benchmark host is shut down; only the pass-through of --gate-cpus 0-7 to perf-gate.s…
  • cargo test --manifest-path perf-gate/Cargo.toml --target-dir target (the test half of make perf-gate-check; clippy not run per the no-linters rule): 59/59 after adding the sessions tests
  • python3 -m unittest scripts/test_pre_pr.py (the pre-pr selftest): 5 OK
  • perf-gate summary on the published engine and memory run.json.gz files, plus a copy with an injected fixture error (exit 3)
  • perf-gate compare --policy perf-gate/policy.toml on the engine-baseline run (engine fixtures listed under diagnostic, not gated) and on the 25058f16 A/A record (INCONCLUSIVE, 4/22)
  • Stand-in host: ssh/scp shims running commands against a throwaway HOME holding a git clone, with host-side pgrep/pkill limited to processes tagged by the harness. A throwaway local clone with a stub-benchmark commit drove the real scripts/perf-gate-remote.sh and the Makefile
  • make perf-gate-remote PERF_GATE_HOST=fakehost (commit missing from origin gets pushed, detached run, files copied back, bench.txt, CPU pinning)
  • perf-gate-remote: gate exit 1 passed through; lock held gives exit 4; --wait-lock 60 queues behind the lock; foreign server gives exit 4; --wait-lock waits the server out; dirty host clone gives exit 3; a server the benchmark leaves behind is killed
  • Dropped connection (killed the follower over ssh) exits 5 with an --attach hint while the host run continues; --attach STAMP collects the finished run
  • Two concurrent invocations from two different versions of the script, with --wait-lock 120: they run one after the other, each with its own <stamp>.run.sh, both exit 0
  • make bench-engine-remote and make bench-sessions-remote SESSIONS_ARGS=...: commands recorded in bench.txt
  • New tests in perf-gate/src/sessions/mod.rs (sessions::tests::*): the budget test fails with the late check disabled and passes with it
  • git diff 22ceb6b6..HEAD -- BENCHMARKS.md: 0 lines removed; 6.83 ms / 11.3 s / 1,652x still present; 'Engine Baseline (2026-10-04)' section added
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

jsam added 13 commits October 5, 2026 06:23
…pdates, claims, .why, sessions, memory, recovery, async WAL) and a summary command
…finity; make perf-gate-remote.sh safe to edit while it runs
…A calibration records; summary skips empty tables
…chema sessions/v2)

A probe whose delta took longer than the 10 s probe timeout counted as
missing, and its delta, when it came, as stray, so a saturated server
looked like one that loses deltas. Overdue probes now wait for their delta
(and 60 s more after the load stops); one that arrives counts as late with
its latency.
@jsam
jsam force-pushed the fm/bench-machine branch from dc7b02c to 512195c Compare October 5, 2026 06:23
@jsam
jsam marked this pull request as ready for review October 5, 2026 16:06
@jsam jsam changed the title perf-gate: run on the benchmark host, add the engine suite, publish an engine baseline test(perf-gate): run benchmarks on the benchmark host, add the engine suite, and publish engine baselines Oct 5, 2026
@jsam
jsam merged commit 1655d16 into main Oct 5, 2026
20 checks passed
@jsam
jsam deleted the fm/bench-machine branch October 5, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant