Skip to content

feat(perf-gate): add views benchmark and view/rule-evaluation counters on /metrics/prometheus - #381

Merged
jsam merged 10 commits into
mainfrom
fm/m9-v1-benches-v2
Oct 5, 2026
Merged

jsam merged 10 commits into
mainfrom
fm/m9-v1-benches-v2

Conversation

@jsam

@jsam jsam commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Intent

please leave this query batching functionliaty just refactor things so its clear and optimized for this ... auto materialize needs to be default mode of operation where rules get deployed as views and everything else is a query .. make the docs true with the correct systems design ... this is not accceptable state for production readiness (Context: implement #308 exactly as written.)

Issue #308 as written, "Baseline benches and counters for the view work (V1)", part of epic #305 (persistent rules are incrementally maintained views; everything else is a query):

Problem. No bench measures the cost of a write vs graph size, rule count and subscriber count, and no counter tells whether a read of a deployed rule evaluated it. The review's numbers (report Section 4) came from an ad-hoc harness.

Target. (a) perf-gate views: the Section 4 matrix (10k/100k/1M edges; unbound/keyed subscribers 1/10/100/1,000; recursive and non-recursive views; 200-rule catalog) as a perf-gate scenario with p50/p99 write->delta, ack, ad-hoc read during writes, server CPU per write and RSS; (b) a criterion bench of the maintained-view micro-benchmark (report A.7) as the headroom reference; (c) counters view_reads, rule_evaluations, view_maintenance_us exported on /metrics/prometheus.

Acceptance. make bench-views-remote runs under the shared lock and reproduces the Section 4 tables within noise; rule_evaluations increments once per deployed-rule read today (so V9 can assert 0); the bench is wired into full-suite.yml as perf-gate-only.

Size M. Dependencies none. Order slot 1.

What Changed

  • Adds process-wide view counters (src/execution/view_counters.rs) and exports them on /metrics/prometheus as inputlayer_rule_evaluations_total, inputlayer_view_reads_total and inputlayer_view_maintenance_us_total. The snapshot's combined-program and plan-cache path tracks whether a program's dependency closure includes a persistent rule. Each such read counts one rule evaluation. A shared standing query or family round also counts once, as does a family cost probe. The provenance baseline re-run uses an uncounted path so it is not counted twice. The REST API and query-profiling guides document the new counters.
  • Adds a perf-gate views scenario (perf-gate/src/views/mod.rs). It runs a matrix over edge counts, unbound and keyed subscriber counts, recursive and non-recursive views and a 200-rule catalog. It reports p50/p99 write->delta, ack latency, ad-hoc read latency during writes, server CPU per write, RSS and the view counters. You run it with scripts/bench-views.sh, make bench-views, or make bench-views-remote (on the benchmark host under the shared lock). In full-suite.yml, a views-bench job runs it only on perf_gate_only dispatches.
  • Adds a criterion bench (benches/view_maintenance_benchmarks.rs) for the maintained-view micro-benchmark, as the headroom reference. Also adds a views scenario test asserting that rule_evaluations goes up for each read of a deployed rule. The perf-gate README and TESTING.md are updated to match.

Risk Assessment

✅ Low: The production change is small: one relaxed atomic increment per evaluation that depends on persistent rules, plus three exported counters. The provenance-baseline double count is fixed through an uncounted variant. Everything else is benchmark tooling, docs and tests, and the views CI job is gated on perf_gate_only as the user decided.

Testing

I built the release server and perf-gate and ran a reduced scripts/bench-views.sh matrix end to end. It exited 0 and wrote summary.md and result.json with ack, write->delta, read-during-writes, CPU per write, RSS, frames and counter columns, and noted the keyed x100 skips (34 hot heads). Against a disposable live server, the metrics endpoint returned 401 without a key and exported the three counters with the admin key. Per-read scrapes showed exactly 1 rule evaluation per deployed-rule read (bound or unbound, recursive or not, and a two-rule join), 0 for a base-relation read, and view_reads and view_maintenance_us stayed at 0. The views scenario tests passed against the real server binary: subscribe and each refresh count 1, and a dirty-session read counts 1. The harness counters test, which used to be a known defect, now passes. The criterion view_maintenance bench ran at 10k–1M edges. Parsing full-suite.yml showed views-bench is gated on inputs.perf_gate_only, which is never set on push, PR or publish (workflow_call) runs. That was a static check only, since GitHub Actions can't be dispatched from this phase, so the scenario is untested. As the user decided, the benchmark-host make target was only dry-run here; its evidence is the earlier remote runs 20261005T185930Z and 20261005T192452Z. I removed the transient bench and criterion outputs from target/. No UI surface changed, so there are no screenshots.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An admin scrapes /metrics/prometheus and sees inputlayer_view_reads_total, inputlayer_rule_evaluations_total and inputlayer_view_maintenance_us_total; a request without a key gets 401 ✅ pass live prometheus-scrape.txt
Each read of a deployed rule (bound or unbound, recursive or not, and a join of two deployed rules) raises rule_evaluations by exactly 1; a base-relation read raises it by 0; view_reads and view_maint… ✅ pass live prometheus-per-read-deltas.txt; views::every_read_of_a_deployed_rule_evaluates_it_once
Subscribing to a deployed rule counts 1 evaluation, and each refresh after a relevant commit counts 1 more ✅ pass live scenarios-views-test.txt (views::a_subscription_evaluates_its_deployed_rule_on_every_relevant_commit, against the real server binary)
Adversarial: a read of a deployed rule (with results) on a session holding session facts counts exactly 1 evaluation, not 2 ✅ pass live scenarios-views-test.txt (views::a_read_on_a_session_with_session_facts_evaluates_once)
scripts/bench-views.sh with a reduced matrix runs perf-gate views end to end and writes summary.md and result.json with ack, write->delta, read-during and CPU per write, RSS and counter columns; the k… ✅ pass live bench-views-smoke.log, bench-views-summary.md, bench-views-result.json
The view_maintenance_benchmarks criterion bench runs as the headroom reference (insert, delete, insert_cold, lookup at 10k–1M edges) ✅ pass live criterion-view-maintenance.txt
views-bench runs only on perf_gate_only workflow_dispatch and never on push, release PR or publish (workflow_call) runs ⏸️ untested no The prior payload did not establish a live result. GitHub Actions can't be dispatched from this phase, so the workflow was only checked statically by parsing full-suite.yml (views-bench if='${{ inputs…
make bench-views-remote runs under the benchmark host's shared lock and reproduces the Section 4 tables within noise ⏸️ untested no The user decided against a new benchmark-host run in this round and asked to cite the earlier remote runs on sam-dev-benchmarks instead: 20261005T185930Z (full matrix) and 20261005T192452Z (baseline 0…
Evidence: bench-views smoke run log (build + run, exit=0)
=== Build perf-gate and server (release, all features) ===
   Compiling inputlayer v0.1.0 (~/.no-mistakes/worktrees/9eea230c3aae/01M46T2XXWYMS7VYMDPA54SXRY)
    Finished `release` profile [optimized] target(s) in 1m 46s
=== Benchmark (c103244c96ad2af51e81aeb222c75489b242240d) ===
  P0_no_subs: ack p50 1.5 ms, write->delta p50/p99 NaN/NaN ms, read p50 0.8 ms, cpu 3.3 ms/write, 0 late
  P1_r1_unbound_x1: ack p50 1.6 ms, write->delta p50/p99 2.5/2.6 ms, read p50 0.9 ms, cpu 0.0 ms/write, 0 late
  P2_r1_unbound_x100: ack p50 2.0 ms, write->delta p50/p99 3.3/3.8 ms, read p50 0.9 ms, cpu 6.7 ms/write, 0 late
  P3_hot_reach_unbound_x1: ack p50 2.6 ms, write->delta p50/p99 25.8/28.9 ms, read p50 1.0 ms, cpu 23.3 ms/write, 0 late
  P4_r1_keyed_x1: ack p50 1.6 ms, write->delta p50/p99 2.2/4.0 ms, read p50 1.0 ms, cpu 0.0 ms/write, 0 late
  P4_r1_keyed_x10: ack p50 1.8 ms, write->delta p50/p99 2.7/4.0 ms, read p50 1.1 ms, cpu 10.0 ms/write, 0 late
  P5_hot_reach_keyed_x1: ack p50 2.3 ms, write->delta p50/p99 31.5/33.7 ms, read p50 1.0 ms, cpu 30.0 ms/write, 0 late
  P6_r1_unbound_x1_rules54: ack p50 1.8 ms, write->delta p50/p99 3.1/3.4 ms, read p50 1.0 ms, cpu 3.3 ms/write, 0 late
candidate 10000 edges: loaded in 55 ms, 8 reads, 8 phases, peak RSS 98 MB
# Views: cost of reads and writes against deployed rules

- candidate: c103244c96ad2af51e81aeb222c75489b242240d (~/.no-mistakes/worktrees/9eea230c3aae/01M46T2XXWYMS7VYMDPA54SXRY/target/bench-views/runs/20261005T214758Z/inputlayer-server)
- host computeinstance-e00sx5ndp6kwb7pqfv (32 CPUs, server CPUs any), load average 4.30 4.85 9.03 3/2113 2928493 -> 4.11 4.79 8.96 3/2242 2940439

## 10000 edges (candidate)

Load: 10002 edges, 34 hot heads in 55 ms (server CPU 20 ms), RSS 44 MB. Peak RSS 98 MB.

Registration: fillers_to_50 957 ms, hot_reach 8 ms, path 7 ms, path_step 7 ms, r1 11 ms.

| read | rules | rows | p50 / p99 ms | server CPU ms/read | rule evaluations/read | view reads/read |
|---|---|---|---|---|---|---|
| `?edge(400,Y)` | 4 | 1 | 0.5 / 0.8 | 0.0 | 0.0 | 0.0 |
| `?r1(X,Y)` | 4 | 34 | 1.3 / 1.4 | 0.0 | 1.0 | 0.0 |
| `?r1(400,Y)` | 4 | 1 | 0.7 / 0.7 | 5.0 | 1.0 | 0.0 |
| `?path(400,Y)` | 4 | 3 | 20.8 / 21.5 | 20.0 | 1.0 | 0.0 |
| `?hot_reach(X,Y)` | 4 | 102 | 21.9 / 21.9 | 20.0 | 1.0 | 0.0 |
| `?hot_reach(400,Y)` | 4 | 3 | 21.8 / 21.8 | 20.0 | 1.0 | 0.0 |
| `?r1(X,Y)` | 54 | 55 | 1.7 / 1.8 | 0.0 | 1.0 | 0.0 |
| `?r1(400,Y)` | 54 | 6 | 1.1 / 1.2 | 5.0 | 1.0 | 0.0 |

| phase | subs | ack p50 / p99 ms | write->delta p50 / p99 ms | read during p50 ms | server CPU ms/write | frames/write | rule evaluations/write | maintenance us/write | RSS MB | late |
|---|---|---|---|---|---|---|---|---|---|---|
| P0_no_subs | 0 | 1.5 / 2.0 | - | 0.8 | 3.3 | 0.0 | 1.0 | 0.0 | 47 | 0 |
| P1_r1_unbound_x1 | 1 | 1.6 / 1.6 | 2.5 / 2.6 | 0.9 | 0.0 | 2.0 | 2.0 | 0.0 | 47 | 0 |
| P2_r1_unbound_x100 | 100 | 2.0 / 2.3 | 3.3 / 3.8 | 0.9 | 6.7 | 200.0 | 2.0 | 0.0 | 50 | 0 |
| P3_hot_reach_unbound_x1 | 1 | 2.6 / 3.0 | 25.8 / 28.9 | 1.0 | 23.3 | 2.0 | 2.0 | 0.0 | 60 | 0 |
| P4_r1_keyed_x1 | 1 | 1.6 / 3.3 | 2.2 / 4.0 | 1.0 | 0.0 | 2.0 | 2.0 | 0.0 | 60 | 0 |
| P4_r1_keyed_x10 | 10 | 1.8 / 3.1 | 2.7 / 4.0 | 1.1 | 10.0 | 11.0 | 11.0 | 0.0 | 63 | 0 |
| P5_hot_reach_keyed_x1 | 1 | 2.3 / 3.2 | 31.5 / 33.7 | 1.0 | 30.0 | 2.0 | 2.0 | 0.0 | 98 | 0 |
| P6_r1_unbound_x1_rules54 | 1 | 1.8 / 2.3 | 3.1 / 3.4 | 1.0 | 3.3 | 2.0 | 2.0 | 0.0 | 98 | 0 |

Subscribe: 1 x `?r1(X,Y)` 3 ms, 100 x `?r1(X,Y)` 27 ms, 1 x `?hot_reach(X,Y)` 32 ms, 1 x `?r1(0,Y)` 3 ms, 10 x `?r1(0,Y)` 5 ms, 1 x `?hot_reach(0,Y)` 34 ms, 1 x `?r1(X,Y)` 3 ms.

- r1 keyed x100 skipped: the graph has 34 hot heads
- hot_reach keyed x100 skipped: the graph has 34 hot heads

result file: ~/.no-mistakes/worktrees/9eea230c3aae/01M46T2XXWYMS7VYMDPA54SXRY/target/bench-views/runs/20261005T214758Z/result.json

Summary: ~/.no-mistakes/worktrees/9eea230c3aae/01M46T2XXWYMS7VYMDPA54SXRY/target/bench-views/runs/20261005T214758Z/summary.md
exit=0
Evidence: perf-gate views summary.md
# Views: cost of reads and writes against deployed rules

- candidate: c103244c96ad2af51e81aeb222c75489b242240d (~/.no-mistakes/worktrees/9eea230c3aae/01M46T2XXWYMS7VYMDPA54SXRY/target/bench-views/runs/20261005T214758Z/inputlayer-server)
- host computeinstance-e00sx5ndp6kwb7pqfv (32 CPUs, server CPUs any), load average 4.30 4.85 9.03 3/2113 2928493 -> 4.11 4.79 8.96 3/2242 2940439

## 10000 edges (candidate)

Load: 10002 edges, 34 hot heads in 55 ms (server CPU 20 ms), RSS 44 MB. Peak RSS 98 MB.

Registration: fillers_to_50 957 ms, hot_reach 8 ms, path 7 ms, path_step 7 ms, r1 11 ms.

| read | rules | rows | p50 / p99 ms | server CPU ms/read | rule evaluations/read | view reads/read |
|---|---|---|---|---|---|---|
| `?edge(400,Y)` | 4 | 1 | 0.5 / 0.8 | 0.0 | 0.0 | 0.0 |
| `?r1(X,Y)` | 4 | 34 | 1.3 / 1.4 | 0.0 | 1.0 | 0.0 |
| `?r1(400,Y)` | 4 | 1 | 0.7 / 0.7 | 5.0 | 1.0 | 0.0 |
| `?path(400,Y)` | 4 | 3 | 20.8 / 21.5 | 20.0 | 1.0 | 0.0 |
| `?hot_reach(X,Y)` | 4 | 102 | 21.9 / 21.9 | 20.0 | 1.0 | 0.0 |
| `?hot_reach(400,Y)` | 4 | 3 | 21.8 / 21.8 | 20.0 | 1.0 | 0.0 |
| `?r1(X,Y)` | 54 | 55 | 1.7 / 1.8 | 0.0 | 1.0 | 0.0 |
| `?r1(400,Y)` | 54 | 6 | 1.1 / 1.2 | 5.0 | 1.0 | 0.0 |

| phase | subs | ack p50 / p99 ms | write->delta p50 / p99 ms | read during p50 ms | server CPU ms/write | frames/write | rule evaluations/write | maintenance us/write | RSS MB | late |
|---|---|---|---|---|---|---|---|---|---|---|
| P0_no_subs | 0 | 1.5 / 2.0 | - | 0.8 | 3.3 | 0.0 | 1.0 | 0.0 | 47 | 0 |
| P1_r1_unbound_x1 | 1 | 1.6 / 1.6 | 2.5 / 2.6 | 0.9 | 0.0 | 2.0 | 2.0 | 0.0 | 47 | 0 |
| P2_r1_unbound_x100 | 100 | 2.0 / 2.3 | 3.3 / 3.8 | 0.9 | 6.7 | 200.0 | 2.0 | 0.0 | 50 | 0 |
| P3_hot_reach_unbound_x1 | 1 | 2.6 / 3.0 | 25.8 / 28.9 | 1.0 | 23.3 | 2.0 | 2.0 | 0.0 | 60 | 0 |
| P4_r1_keyed_x1 | 1 | 1.6 / 3.3 | 2.2 / 4.0 | 1.0 | 0.0 | 2.0 | 2.0 | 0.0 | 60 | 0 |
| P4_r1_keyed_x10 | 10 | 1.8 / 3.1 | 2.7 / 4.0 | 1.1 | 10.0 | 11.0 | 11.0 | 0.0 | 63 | 0 |
| P5_hot_reach_keyed_x1 | 1 | 2.3 / 3.2 | 31.5 / 33.7 | 1.0 | 30.0 | 2.0 | 2.0 | 0.0 | 98 | 0 |
| P6_r1_unbound_x1_rules54 | 1 | 1.8 / 2.3 | 3.1 / 3.4 | 1.0 | 3.3 | 2.0 | 2.0 | 0.0 | 98 | 0 |

Subscribe: 1 x `?r1(X,Y)` 3 ms, 100 x `?r1(X,Y)` 27 ms, 1 x `?hot_reach(X,Y)` 32 ms, 1 x `?r1(0,Y)` 3 ms, 10 x `?r1(0,Y)` 5 ms, 1 x `?hot_reach(0,Y)` 34 ms, 1 x `?r1(X,Y)` 3 ms.

- r1 keyed x100 skipped: the graph has 34 hot heads
- hot_reach keyed x100 skipped: the graph has 34 hot heads
Evidence: perf-gate views result.json
{"schema":"inputlayer-perf-gate/views/v1","created_unix":1791236890,"environment":{"hostname":"computeinstance-e00sx5ndp6kwb7pqfv","kernel":"6.11.0-1016-nvidia","cpu_model":"AMD EPYC-Genoa Processor","logical_cpus":32,"mem_total_kb":131897740,"cpu_governor":"unknown","loadavg_start":"4.30 4.85 9.03 3/2113 2928493","loadavg_end":"4.11 4.79 8.96 3/2242 2940439","data_fs":"ext4","data_root":"~/.no-mistakes/worktrees/9eea230c3aae/01M46T2XXWYMS7VYMDPA54SXRY/target/bench-views/runs/20261005T214758Z/servers","server_cpus":null,"build":"rustc 1.99.0 (b940084d7 2026-09-28); cargo build --release --all-features"},"options":{"writes":3,"large_writes":20,"reads":2,"keyed":[1,10,100],"recursive_keyed":[1,100],"large_recursive_keyed":[1,10],"filler_rules":[50]},"arms":[["candidate","c103244c96ad2af51e81aeb222c75489b242240d (~/.no-mistakes/worktrees/9eea230c3aae/01M46T2XXWYMS7VYMDPA54SXRY/target/bench-views/runs/20261005T214758Z/inputlayer-server)"]],"server_overrides":{"INPUTLAYER_HTTP__GUI__ENABLED":"false (the GUI is not under test)","INPUTLAYER_HTTP__RATE_LIMIT__PER_IP_MAX_RPS":"0 (every gate client shares one loopback IP)","INPUTLAYER_HTTP__RATE_LIMIT__WS_MAX_MESSAGES_PER_SEC":"0 (per-connection message cap would bound the measured throughput)"},"server_env":["INPUTLAYER_HTTP__RATE_LIMIT__WS_MAX_PREAUTH_PER_IP=0","INPUTLAYER_HTTP__RATE_LIMIT__MAX_WS_CONNECTIONS=5000","INPUTLAYER_HTTP__RATE_LIMIT__MAX_CONNECTIONS=5000"],"runs":[{"arm":"candidate","edges":10000,"load":{"edges":10002,"hot_heads":34,"ms":54.626467,"server_cpu_ms":19.99999999999999,"rss_mb":44},"registration_ms":{"fillers_to_50":956.9748179999999,"hot_reach":8.110228000000001,"path":6.591786,"path_step":7.202145,"r1":11.289728},"reads":[{"name":"edge_point","query":"?edge(400,Y)","rules":4,"rows":1,"truncated":false,"latency_ms":{"n":2,"p50":0.461553,"p95":0.789382,"p99":0.789382,"max":0.789382},"server_cpu_ms":0.0,"work":{"rule_evaluations":0.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"r1_unbound","query":"?r1(X,Y)","rules":4,"rows":34,"truncated":false,"latency_ms":{"n":2,"p50":1.289042,"p95":1.355494,"p99":1.355494,"max":1.355494},"server_cpu_ms":0.0,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"r1_bound","query":"?r1(400,Y)","rules":4,"rows":1,"truncated":false,"latency_ms":{"n":2,"p50":0.713566,"p95":0.726616,"p99":0.726616,"max":0.726616},"server_cpu_ms":5.000000000000004,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"path_bound","query":"?path(400,Y)","rules":4,"rows":3,"truncated":false,"latency_ms":{"n":2,"p50":20.837725000000002,"p95":21.515567,"p99":21.515567,"max":21.515567},"server_cpu_ms":19.99999999999999,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"hot_reach_unbound","query":"?hot_reach(X,Y)","rules":4,"rows":102,"truncated":false,"latency_ms":{"n":2,"p50":21.924690000000002,"p95":21.927304000000003,"p99":21.927304000000003,"max":21.927304000000003},"server_cpu_ms":20.000000000000004,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"hot_reach_bound","query":"?hot_reach(400,Y)","rules":4,"rows":3,"truncated":false,"latency_ms":{"n":2,"p50":21.783495,"p95":21.842455,"p99":21.842455,"max":21.842455},"server_cpu_ms":20.000000000000004,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"r1_unbound_rules54","query":"?r1(X,Y)","rules":54,"rows":55,"truncated":false,"latency_ms":{"n":2,"p50":1.688991,"p95":1.790416,"p99":1.790416,"max":1.790416},"server_cpu_ms":0.0,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"r1_bound_rules54","query":"?r1(400,Y)","rules":54,"rows":6,"truncated":false,"latency_ms":{"n":2,"p50":1.138042,"p95":1.157813,"p99":1.157813,"max":1.157813},"server_cpu_ms":5.000000000000004,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}}],"phases":[{"name":"P0_no_subs","subscribers":0,"rules":4,"writes":3,"ack_ms":{"n":3,"p50":1.450148,"p95":1.9543149999999998,"p99":1.9543149999999998,"max":1.9543149999999998},"write_to_delta_ms":{"n":0,"p50":null,"p95":null,"p99":null,"max":null},"read_during_ms":{"n":3,"p50":0.7680290000000001,"p95":0.842243,"p99":0.842243,"max":0.842243},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":3.333333333333336,"frames_per_write":0.0,"work_per_write":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":47},{"name":"P1_r1_unbound_x1","subscribers":1,"rules":4,"writes":3,"ack_ms":{"n":3,"p50":1.562719,"p95":1.608239,"p99":1.608239,"max":1.608239},"write_to_delta_ms":{"n":3,"p50":2.523682,"p95":2.555911,"p99":2.555911,"max":2.555911},"read_during_ms":{"n":3,"p50":0.8730289999999999,"p95":1.385629,"p99":1.385629,"max":1.385629},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":0.0,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":47},{"name":"P2_r1_unbound_x100","subscribers":100,"rules":4,"writes":3,"ack_ms":{"n":3,"p50":1.987226,"p95":2.261713,"p99":2.261713,"max":2.261713},"write_to_delta_ms":{"n":3,"p50":3.2654720000000004,"p95":3.8322339999999997,"p99":3.8322339999999997,"max":3.8322339999999997},"read_during_ms":{"n":3,"p50":0.948434,"p95":1.020934,"p99":1.020934,"max":1.020934},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":6.666666666666654,"frames_per_write":200.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":50},{"name":"P3_hot_reach_unbound_x1","subscribers":1,"rules":4,"writes":3,"ack_ms":{"n":3,"p50":2.579017,"p95":2.999287,"p99":2.999287,"max":2.999287},"write_to_delta_ms":{"n":3,"p50":25.784457,"p95":28.930235,"p99":28.930235,"max":28.930235},"read_during_ms":{"n":3,"p50":0.9771580000000001,"p95":1.176131,"p99":1.176131,"max":1.176131},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":23.333333333333332,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":60},{"name":"P4_r1_keyed_x1","subscribers":1,"rules":4,"writes":3,"ack_ms":{"n":3,"p50":1.637763,"p95":3.292212,"p99":3.292212,"max":3.292212},"write_to_delta_ms":{"n":3,"p50":2.193509,"p95":3.9922279999999994,"p99":3.9922279999999994,"max":3.9922279999999994},"read_during_ms":{"n":3,"p50":1.030289,"p95":1.181318,"p99":1.181318,"max":1.181318},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":0.0,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":60},{"name":"P4_r1_keyed_x10","subscribers":10,"rules":4,"writes":3,"ack_ms":{"n":3,"p50":1.833992,"p95":3.099198,"p99":3.099198,"max":3.099198},"write_to_delta_ms":{"n":3,"p50":2.6735499999999996,"p95":4.032479,"p99":4.032479,"max":4.032479},"read_during_ms":{"n":3,"p50":1.141678,"p95":1.230854,"p99":1.230854,"max":1.230854},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":10.000000000000009,"frames_per_write":11.0,"work_per_write":{"rule_evaluations":11.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":63},{"name":"P5_hot_reach_keyed_x1","subscribers":1,"rules":4,"writes":3,"ack_ms":{"n":3,"p50":2.290016,"p95":3.215365,"p99":3.215365,"max":3.215365},"write_to_delta_ms":{"n":3,"p50":31.524805000000004,"p95":33.661148000000004,"p99":33.661148000000004,"max":33.661148000000004},"read_during_ms":{"n":3,"p50":1.0117310000000002,"p95":1.083039,"p99":1.083039,"max":1.083039},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":29.99999999999999,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":98},{"name":"P6_r1_unbound_x1_rules54","subscribers":1,"rules":54,"writes":3,"ack_ms":{"n":3,"p50":1.840172,"p95":2.335326,"p99":2.335326,"max":2.335326},"write_to_delta_ms":{"n":3,"p50":3.068092,"p95":3.396201,"p99":3.396201,"max":3.396201},"read_during_ms":{"n":3,"p50":0.9954950000000001,"p95":1.1059539999999999,"p99":1.1059539999999999,"max":1.1059539999999999},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":3.333333333333336,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":98}],"subscribes":[{"query":"?r1(X,Y)","count":1,"ms":2.8263030000000002},{"query":"?r1(X,Y)","count":100,"ms":26.646238},{"query":"?hot_reach(X,Y)","count":1,"ms":31.909702},{"query":"?r1(0,Y)","count":1,"ms":3.1617529999999996},{"query":"?r1(0,Y)","count":10,"ms":4.7585939999999995},{"query":"?hot_reach(0,Y)","count":1,"ms":34.452032},{"query":"?r1(X,Y)","count":1,"ms":3.225931}],"notes":["r1 keyed x100 skipped: the graph has 34 hot heads","hot_reach keyed x100 skipped: the graph has 34 hot heads"],"peak_rss_kb":100696,"error":null}]}
Evidence: Prometheus scrape (401 without key; counters with admin key)
no-key status: 401
# HELP inputlayer_view_reads_total Reads of persistent rules answered from a view, without evaluating a rule.
# TYPE inputlayer_view_reads_total counter
inputlayer_view_reads_total 0
# HELP inputlayer_rule_evaluations_total Reads of persistent rules answered by evaluating the rules from base facts.
# TYPE inputlayer_rule_evaluations_total counter
inputlayer_rule_evaluations_total 0
# HELP inputlayer_view_maintenance_us_total Microseconds spent keeping views current with commits.
# TYPE inputlayer_view_maintenance_us_total counter
inputlayer_view_maintenance_us_total 0
Evidence: Per-read counter deltas against live server

?edge(0, X) rows=1 rule_evaluations=0 view_reads=0 ?hop2(0, Z) rows=1 rule_evaluations=1 view_reads=0 ?hop2(X, Z) rows=3 rule_evaluations=1 view_reads=0 ?reach(0, X) rows=4 rule_evaluations=1 view_reads=0 ?reach(X, Y) rows=10 rule_evaluations=1 view_reads=0 ?hop2(0, Z), reach(Z, W) rows=2 rule_evaluations=1 view_reads=0

auth -> {"type":"authenticated","session_id":"1ff8dd75-0833-4630-b4be-e82d2385971e","knowledge_graph":"default","version":"0.1.0","role":"admin","protocol_version":5,"stream_epoch":"cc0c897469bf2b7b"}
?edge(0, X)                reply=result rows=1  delta rule_evaluations=0 view_reads=0 view_maintenance_us=0
?hop2(0, Z)                reply=result rows=1  delta rule_evaluations=1 view_reads=0 view_maintenance_us=0
?hop2(X, Z)                reply=result rows=3  delta rule_evaluations=1 view_reads=0 view_maintenance_us=0
?reach(0, X)               reply=result rows=4  delta rule_evaluations=1 view_reads=0 view_maintenance_us=0
?reach(X, Y)               reply=result rows=10  delta rule_evaluations=1 view_reads=0 view_maintenance_us=0
?hop2(0, Z), reach(Z, W)   reply=result rows=2  delta rule_evaluations=1 view_reads=0 view_maintenance_us=0
Evidence: views scenario tests output
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running tests/scenarios/main.rs (target/debug/deps/scenarios-e36d7e30c71358d5)

running 4 tests
test views::a_read_on_a_session_with_session_facts_evaluates_once ... ok
test harness::counters_scrape_the_running_engine ... ok
test views::a_subscription_evaluates_its_deployed_rule_on_every_relevant_commit ... ok
test views::every_read_of_a_deployed_rule_evaluates_it_once ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 33 filtered out; finished in 0.67s
Evidence: criterion view_maintenance bench output
Benchmarking view_maintenance/insert_cold/100000: Warming up for 1.0000 s
Benchmarking view_maintenance/insert_cold/100000: Collecting 10 samples in estimated 2.0053 s (9900 iterations)
Benchmarking view_maintenance/insert_cold/100000: Analyzing
view_maintenance/insert_cold/100000
                        time:   [101.08 µs 101.99 µs 103.83 µs]
Benchmarking view_maintenance/lookup/100000
Benchmarking view_maintenance/lookup/100000: Warming up for 1.0000 s
Benchmarking view_maintenance/lookup/100000: Collecting 10 samples in estimated 2.0000 s (15M iterations)
Benchmarking view_maintenance/lookup/100000: Analyzing
view_maintenance/lookup/100000
                        time:   [131.06 ns 131.82 ns 132.37 ns]

view_maintenance 1000000 edges: load 1524 ms, RSS 401 MB holding the views; rows r1=3334 path=2000004 hot_reach=10002
Benchmarking view_maintenance/insert/1000000
Benchmarking view_maintenance/insert/1000000: Warming up for 1.0000 s
Benchmarking view_maintenance/insert/1000000: Collecting 10 samples in estimated 2.0030 s (10k iterations)
Benchmarking view_maintenance/insert/1000000: Analyzing
view_maintenance/insert/1000000
                        time:   [96.121 µs 96.293 µs 96.472 µs]
Benchmarking view_maintenance/delete/1000000
Benchmarking view_maintenance/delete/1000000: Warming up for 1.0000 s
Benchmarking view_maintenance/delete/1000000: Collecting 10 samples in estimated 2.0057 s (10k iterations)
Benchmarking view_maintenance/delete/1000000: Analyzing
view_maintenance/delete/1000000
                        time:   [96.135 µs 96.361 µs 96.546 µs]
Found 1 outliers among 10 measurements (10.00%)
  1 (10.00%) high severe
Benchmarking view_maintenance/insert_cold/1000000
Benchmarking view_maintenance/insert_cold/1000000: Warming up for 1.0000 s
Benchmarking view_maintenance/insert_cold/1000000: Collecting 10 samples in estimated 2.0102 s (10k iterations)
Benchmarking view_maintenance/insert_cold/1000000: Analyzing
view_maintenance/insert_cold/1000000
                        time:   [97.108 µs 97.292 µs 97.434 µs]
Benchmarking view_maintenance/lookup/1000000
Benchmarking view_maintenance/lookup/1000000: Warming up for 1.0000 s
Benchmarking view_maintenance/lookup/1000000: Collecting 10 samples in estimated 2.0000 s (13M iterations)
Benchmarking view_maintenance/lookup/1000000: Analyzing
view_maintenance/lookup/1000000
                        time:   [149.95 ns 150.39 ns 151.05 ns]
Evidence: full-suite.yml parsed job gating
triggers: ['push', 'pull_request', 'workflow_dispatch', 'workflow_call']
workflow_dispatch inputs.perf_gate_only: {'description': 'Run only the performance gate and the views benchmark, which runs on no other trigger (allowed on any ref)', 'type': 'boolean', 'default': False}
gate: if="(github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main' || inputs.perf_gate_only) && (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'release') &&\n (github.event.action != 'labeled' || github.event.label.name == 'release') &&\n !(startsWith(github.head_ref, 'release/') &&\n   github.event.pull_request.head.repo.full_name == github.repository)))"
test: if='${{ !inputs.perf_gate_only }}'
docs: if='${{ !inputs.perf_gate_only }}'
website: if='${{ !inputs.perf_gate_only }}'
coverage: if='${{ !inputs.perf_gate_only }}'
python-langchain-live: if='${{ !inputs.perf_gate_only }}'
js-sdk-live: if='${{ !inputs.perf_gate_only }}'
python-sdk-live: if='${{ !inputs.perf_gate_only }}'
perf-gate-tool: if='${{ !inputs.perf_gate_only }}'
perf-gate: if=None
views-bench: if='${{ inputs.perf_gate_only }}'
verified-completions: if='${{ !inputs.perf_gate_only }}'
msrv: if='${{ !inputs.perf_gate_only }}'
views-bench steps: ['actions/checkout@v4', 'dtolnay/rust-toolchain@stable', 'scripts/bench-views.sh --server-cpus 8-31', 'cat target/bench-views/latest/summary.md >> "$GITHUB_STEP_SUMMARY" 2> /dev/null || true', 'actions/upload-artifact@v4']
workflow_call: None
- Outcome: 🔧 2 issues found → auto-fixed ✅ across 5 runs (56m10s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 1 issue found → auto-fixed ✅
  • ⚠️ src/protocol/rest/handlers/admin.rs - merge conflict rebasing onto origin/main

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Review** - 3 issues found → auto-fixed (3) ✅
  • ⚠️ .github/workflows/full-suite.yml:426 - The new views-bench job has no if: condition, so it runs on every full-suite run, not only on perf_gate_only dispatches. That includes release/** pushes and v* tags called from publish.yml, where every job must pass before publish. The run fails with exit 1 on any late delta (over 20 s), any failed subscription or any failed step. The 1M-edge phases are already slow (Regression: keyed rule-view sharing, a write costs 11.9 s CPU and a 643 ms ack at 1M edges / 1,000 keys (was 0.18 s / 3.8 ms) #378: a write costs about 11.9 s CPU with 1,000 keyed subscribers), and they run on a shared CI runner with a 90-minute timeout. So a benchmark described as 'report-only' can block a release. The intent says the bench is 'wired into full-suite.yml as perf-gate-only'. That could mean 'runs only when perf_gate_only is set' or 'report-only like the perf gate'. Please confirm which is meant. If the first: add if: ${{ inputs.perf_gate_only }} (or keep it off the release path) and fix the matching comments at full-suite.yml:8-10, 25 and perf-gate/README.md ('Full-suite runs include it as the Views benchmark job'). If the second: consider continue-on-error: true so latency or flakiness cannot block publish.
  • ⚠️ perf-gate/src/views/mod.rs:711 - Intent (Baseline benches and counters for the view work (V1) #308) says: 'the Section 4 matrix (10k/100k/1M edges; unbound/keyed subscribers 1/10/100/1,000; ...)'. The scenario does not cover that full grid. (1) Unbound subscribers run only at 1 and 100: P1_r1_unbound_x1, P2_r1_unbound_x100 and P3 at x1 (views/mod.rs:711-714). There is no unbound 10 or 1,000. (2) Keyed counts above the number of hot heads are skipped with a note (views/mod.rs:731-736). With 1% hot heads, 10k edges has only 34 heads, so keyed 100 and 1,000 are skipped there, and 100k edges has 334, so keyed 1,000 is skipped there. The full keyed 1/10/100/1,000 set runs only at 1M edges. (3) Recursive keyed subscribers run only at 1/100, or 1/10 from 1M edges. This may match the review report's actual Section 4 tables, which are not in the repo, but it does not match the literal grid in the issue. Please confirm the reduced grid is intended, or add unbound x10/x1000 phases.
  • ⚠️ docs/content/docs/guides/query-profiling.mdx:176 - The docs say each subscription refresh counts as one rule evaluation: 'Reads include ... a subscription's initial result and each refresh'. perf-gate/README.md adds 'P4 with 10 keyed subscribers shows 11 per write'. But keyed subscriptions that differ only in a lifted constant form a parameterized Family (src/protocol/subscription/parameterized.rs). Once the views' own evaluations no longer fit on the compute permits, a family runs one shared round per revision. The counter is recorded per snapshot evaluation (snapshot.rs:431 / 609), so N shared refreshes count as one evaluation, plus occasional probes, not N. P4 at 100 and 1,000 keyed subscribers, and P4 at 10 on a host with fewer than 10 permits, will therefore show far fewer evaluations per write than the docs imply. The counter itself is right (it counts evaluations); the docs are wrong. Fix the docs: say that standing queries sharing a parameterized family count one evaluation per shared round, and qualify the README's '11 per write' example as holding only while the family does not share.

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • ℹ️ docs/content/docs/guides/query-profiling.mdx:176 - The docs (and the metric HELP text in src/execution/view_counters.rs:383, 'Reads of persistent rules answered by evaluating…') say rule_evaluations counts reads: queries, a subscription's initial result and each refresh, .why, and conditional writes. A parameterized family's probe also goes through run_query → query_snapshot → execute_cached, so it counts as one evaluation (src/protocol/subscription/parameterized.rs:620-677, 815). No subscriber waits for a probe. So P4 at 100 or 1,000 keyed subscribers can show occasional extra evaluations per write that no read explains. The round-1 finding noted 'plus occasional probes', but that round's doc fix covered only shared rounds. Suggested fix: add one sentence to query-profiling.mdx (and to the perf-gate/README.md 'View work' bullet) saying that a family's probe evaluation also counts once. Keep the counter code as is, per the earlier decision.

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • ℹ️ perf-gate/src/views/mod.rs:839 - The write->delta time can never be shorter than the ack. phase seeds last = Some(reply.at) and then takes the max with each waiter's arrival. So when a subscriber's delta arrives before the writer's reply, the reported write->delta is the ack time, not the time the delta actually arrived. The field is documented as 'Write sent to the last delta it must cause'. Today a refresh re-evaluates the rule after commit, so deltas almost always arrive after the ack and the numbers are right. But the bench exists to compare the later view work (V2/V9), where a maintained view can push its delta before the ack. There, p50/p99 write->delta would be inflated up to the ack without any warning. Fix: start last from the waiters' arrivals only, e.g. fold arrivals into an Option and mark the write late if any waiter returns None, and keep reply.at out of the delta sample.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ No issues found.

🔧 **Test** - 2 issues found → auto-fixed ✅
  • ⚠️ No live evidence was gathered. The release builds were still compiling when the result was due, so neither the counters on /metrics/prometheus nor perf-gate views was exercised. To validate, re-run this test phase and let the build finish: scripts/bench-views.sh --edges 10000 --writes 3 --reads 2 --keyed 1,10,100 --filler-rules 50 and cargo test --test e2e_reactive views.
  • ⚠️ live validation verdict: inconclusive (0 of 7 scenarios were driven live against the product); untested: An admin scrapes /metrics/prometheus and sees inputlayer_rule_evaluations_total, inputlayer_view_reads_total and inputlayer_view_maintenance_us_total, Each read of a deployed rule (bound or unbound, recursive or not) increments rule_evaluations by exactly 1; a base-relation read increments it by 0; view_reads stays 0, A subscription refresh after a relevant commit counts one rule evaluation, scripts/bench-views.sh with a reduced matrix runs perf-gate views end-to-end and writes summary.md and result.json with p50/p99 delta, ack, ad-hoc read, CPU per write, RSS and counters; keyed counts above the hot heads are skipped with a note, The view_maintenance_benchmarks criterion bench runs as the headroom reference, views-bench runs only on perf_gate_only dispatches and never on release or tag runs, make bench-views-remote runs under the benchmark host's shared lock
  • Live validation: ⚠️ inconclusive - 0 of 7 scenarios driven live against the product
Scenario Result Live Evidence
An admin scrapes /metrics/prometheus and sees inputlayer_rule_evaluations_total, inputlayer_view_reads_total and inputlayer_view_maintenance_us_total ⏸️ untested no The server release build had not finished when the structured result was due. There was no binary to start, so nothing could be scraped.
Each read of a deployed rule (bound or unbound, recursive or not) increments rule_evaluations by exactly 1; a base-relation read increments it by 0; view_reads stays 0 ⏸️ untested no The build had not finished, so neither the server nor tests/e2e_reactive/views.rs could be run before the result was due.
A subscription refresh after a relevant commit counts one rule evaluation ⏸️ untested no The build had not finished, so the server could not be run before the result was due.
scripts/bench-views.sh with a reduced matrix runs perf-gate views end-to-end and writes summary.md and result.json with p50/p99 delta, ack, ad-hoc read, CPU per write, RSS and counters; keyed counts… ⏸️ untested no The perf-gate and server release builds were still compiling when the result was due.
The view_maintenance_benchmarks criterion bench runs as the headroom reference ⏸️ untested no Not reached before the result was due.
views-bench runs only on perf_gate_only dispatches and never on release or tag runs ⏸️ untested no GitHub Actions job selection can only be driven live by dispatching the workflow on GitHub. That is an outward-facing action owned by the CI phase, not this one.
make bench-views-remote runs under the benchmark host's shared lock ⏸️ untested no This needs ssh access to the dedicated benchmark host (sam-dev-benchmarks), and a heavy run there would contend with that shared host. It is outside this disposable workspace.
  • cargo build --release --manifest-path perf-gate/Cargo.toml --target-dir target && cargo build --release --all-features --bin inputlayer-server (started in the background; had not finished when the result was due)
  • python3 yaml.safe_load(.github/workflows/full-suite.yml): views-bench has if: ${{ inputs.perf_gate_only }} and needs: gate; every non-perf job has !inputs.perf_gate_only; workflow_call declares no inputs and publish.yml calls full-suite with none

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An admin scrapes /metrics/prometheus and sees inputlayer_rule_evaluations_total, inputlayer_view_reads_total and inputlayer_view_maintenance_us_total; a request without a key gets 401 ✅ pass live prometheus-scrape.log (live release server, admin API key from .apikey create)
Each read of a deployed rule (bound or unbound, recursive or not) adds exactly 1 to rule_evaluations; a base-relation read adds 0; view_reads stays 0 ✅ pass live prometheus-scrape.log per-read deltas; cargo test --test e2e_reactive views::every_read_of_a_deployed_rule_evaluates_it_once
A subscription's initial result and each refresh after a relevant commit count one rule evaluation; an unrelated commit counts none ✅ pass live prometheus-subscription.log; e2e test a_subscription_evaluates_its_deployed_rule_on_every_relevant_commit
Adversarial: a read on a session holding a session fact counts once, not twice (provenance baseline re-run) ✅ pass live e2e-views.log: a_read_on_a_session_with_session_facts_evaluates_once passes; with the handler.rs fix reverted it fails with rule_evaluations 2 vs 1
scripts/bench-views.sh with a reduced matrix runs perf-gate views end to end and writes summary.md and result.json with p50/p99 write->delta, ack, read during writes, CPU per write, RSS and counter… ✅ pass live bench-views-summary.md, bench-views-result.json, bench-views-smoke.log (exit 0; notes 'r1 keyed x100 skipped: the graph has 34 hot heads')
The view_maintenance_benchmarks criterion bench runs as the headroom reference ✅ pass live criterion-view-maintenance.log (insert/delete about 100 us, lookups about 130-150 ns, up to 1M edges)
The views-bench job runs only on perf_gate_only dispatches, never on push, PR or publish.yml (release/tag) runs ⏸️ untested no The prior payload did not establish a live result: GitHub Actions cannot run locally, and no workflow was dispatched. The only check was static: parsing full-suite.yml and publish.yml with PyYAML (ful…
make bench-views-remote runs under the benchmark host's shared lock and reproduces the Section 4 tables within noise ⏸️ untested no Per the user's decision, I did not re-run on the benchmark host (sam-dev-benchmarks). The evidence is the earlier remote runs of this work before the rebase (20261005T185930Z and 20261005T192452Z, com…
  • systemd-run --user --scope -p MemoryMax=16G with CARGO_BUILD_JOBS=4: built perf-gate and inputlayer-server in release in the foreground (both already current)
  • scripts/bench-views.sh --edges 10000 --writes 3 --reads 2 --keyed 1,10,100 --filler-rules 50: exit 0; checked summary.md and result.json
  • cargo test --test e2e_reactive views: 3 passed
  • Reverted the handler.rs fix temporarily and ran cargo test --test e2e_reactive views::a_read_on_a_session: it FAILED with rule_evaluations 2 vs 1; restored the fix
  • Live release server in a temp data dir: logged in as admin, created a key with .apikey create, scraped /metrics/prometheus with and without the key, and took per-read counter deltas for edge/hop2/reach reads, bound and unbound
  • Live .subscribe s ?hop2(X,Z): counter deltas for the initial result, a relevant commit and an irrelevant commit
  • Parsed full-suite.yml and publish.yml with PyYAML: views-bench if, perf_gate_only input, how publish.yml calls full-suite
  • Read the Makefile bench-views-remote target and the lock handling in perf-gate-remote.sh
  • cargo bench --bench view_maintenance_benchmarks -- --warm-up-time 1 --measurement-time 2 --sample-size 10

✅ No issues found.

  • Live validation: ✅ go - 5 of 7 scenarios driven live against the product
Scenario Result Live Evidence
An admin scrapes /metrics/prometheus with an API key and sees inputlayer_rule_evaluations_total, inputlayer_view_reads_total and inputlayer_view_maintenance_us_total; without a key the scrape is refus… ✅ pass live metrics-scrape.log (HTTP 200 with HELP/TYPE/value lines for all three; HTTP 401 without a key)
Each read of a deployed rule (bound or unbound, recursive or not) adds exactly 1 to rule_evaluations; a base-relation read adds 0; view_reads stays 0 ✅ pass live metrics-scrape.log: ?edge +0; ?r1(X,Y), ?r1(1,Y), ?path(X,Y), ?path(1,Y) +1 each; view_reads +0
A read on a session holding session facts counts once (dirty-session double-count fix), and a subscription counts one evaluation per relevant commit ✅ pass live cargo test --test scenarios views -> scenarios-views-tests.log (3 passed); smoke result shows P1 rule evaluations/write 2.0 = 1 read during the write + 1 refresh
scripts/bench-views.sh with a reduced matrix runs perf-gate views end to end and writes summary.md and result.json with p50/p99 write->delta, ack, read during writes, CPU per write, RSS and counters;… ✅ pass live bench-views-summary.md, bench-views-result.json, bench-views-smoke.log (exit 0; 'r1 keyed x100 skipped: the graph has 34 hot heads')
The view_maintenance_benchmarks criterion bench runs as the headroom reference at 10k/100k/1M edges ✅ pass live criterion-view-maintenance.log (insert/delete/insert_cold/lookup at 1M edges, ~100-160 µs per write, RSS 399 MB)
views-bench runs only on perf_gate_only dispatches and never on release pushes or tag (publish.yml) runs ⏸️ untested no The prior payload did not establish a live result. GitHub Actions cannot be triggered from this test phase, which does not own CI, so the workflow was only parsed statically (full-suite-gating.txt sho…
make bench-views-remote runs under the benchmark host's shared lock and reproduces the review's Section 4 tables ⏸️ untested no The user decided against a new run on sam-dev-benchmarks, so this was not driven live. Statically, make -n bench-views-remote expands to ./scripts/perf-gate-remote.sh --rev HEAD --bench views, and t…
  • CARGO_BUILD_JOBS=4 systemd-run --user --scope -p MemoryMax=16G cargo build --release for perf-gate and inputlayer-server (all features), run in the foreground
  • scripts/bench-views.sh --edges 10000 --writes 3 --reads 2 --keyed 1,10,100 --filler-rules 50 (exit 0; summary.md and result.json inspected)
  • cargo test --test scenarios views (3 e2e tests: reads of deployed rules count once, a session with session facts counts once, a subscription counts once per relevant commit)
  • Manual: started the release inputlayer-server in a temp data dir, logged in as admin, created a KG and an API key, deployed r1/path rules, then scraped /metrics/prometheus around each read (node script metrics-scrape.mjs)
  • cargo bench --bench view_maintenance_benchmarks -- --warm-up-time 1 --measurement-time 2 --sample-size 10
  • Parsed .github/workflows/full-suite.yml with PyYAML to check the views-bench if: and the perf_gate_only input; make -n bench-views-remote; read the flock logic in scripts/perf-gate-remote.sh

✅ No issues found.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An admin scrapes /metrics/prometheus with an admin API key and sees inputlayer_rule_evaluations_total, inputlayer_view_reads_total and inputlayer_view_maintenance_us_total; without a key the request i… ✅ pass live prometheus-scrape.txt: no key -> 401, admin key -> 200 with all three counters and their HELP/TYPE lines
Each read of a deployed rule (recursive or not, bound or unbound) adds exactly 1 to rule_evaluations; a base-relation read adds 0; view_reads and view_maintenance_us stay 0 ✅ pass live prometheus-scrape.txt per-read deltas (+0 for ?edge, +1 for ?hop and ?reach in both bound and unbound forms); scenarios-views-test.log views::every_read_of_a_deployed_rule_evaluates_it_once
A subscription refresh after a relevant commit counts one rule evaluation, and a read on a session that holds session facts counts once, not twice ✅ pass live scenarios-views-test.log: a_subscription_evaluates_its_deployed_rule_on_every_relevant_commit, a_read_on_a_session_with_session_facts_evaluates_once (testkit starts a real engine per scenario)
scripts/bench-views.sh with a reduced matrix runs perf-gate views end-to-end and writes summary.md and result.json with p50/p99 ack, write->delta, read during writes, server CPU per write, RSS and cou… ✅ pass live bench-views-summary.md, bench-views-result.json, bench-views-smoke.log (exit 0, 0 late, notes 'r1 keyed x100 skipped: the graph has 34 hot heads')
write->delta is measured from the last delta arrival and is no longer floored at the ack time; a missing delta still marks the write late ✅ pass live bench-views-summary.md: P4_r1_keyed_x1 write->delta p50 2.5 ms against ack p50 2.2 ms, with every phase late=0; perf-gate unit test write_to_delta_is_the_last_arrival_and_not_floored_at_the_ack passes
The view_maintenance_benchmarks criterion bench runs as the headroom reference at 10k, 100k and 1M edges ✅ pass live criterion-view-maintenance.log: insert, delete, insert_cold and lookup each report Success at every size
views-bench runs only on perf_gate_only dispatches and never on release pushes, release PRs or the publish.yml workflow_call ⏸️ untested no The prior payload did not establish a live result. The gating was only checked by parsing full-suite.yml and publish.yml with PyYAML. A live check would mean dispatching GitHub Actions on the shared r…
make bench-views-remote runs under the benchmark host's shared lock and reproduces the review's Section 4 tables within noise ⏸️ untested no The recorded user decision says not to re-run on the benchmark host and to cite the earlier remote runs of this work (88b68ab) instead: 20261005T185930Z (full matrix) and 20261005T192452Z (--baseline…
  • cargo test --test scenarios -- views:: harness::counters_scrape_the_running_engine (4 passed: one evaluation per deployed-rule read, per subscription refresh, per dirty-session read; required counter scrape)
  • cargo test --manifest-path perf-gate/Cargo.toml --target-dir target views:: (incl. write_to_delta_is_the_last_arrival_and_not_floored_at_the_ack)
  • CARGO_BUILD_JOBS=8 systemd-run --user --scope -p MemoryMax=24G cargo build --release ... (perf-gate + inputlayer-server --all-features)
  • scripts/bench-views.sh --edges 10000 --writes 3 --reads 2 --keyed 1,10,100 --filler-rules 50 (exit 0)
  • Manual: release inputlayer-server on port 18731 with a temp data dir; Node WS client logs in as admin, creates an API key and a KG, deploys hop/reach rules, scrapes /metrics/prometheus with no key, then with the admin key, before and after each read
  • cargo bench --bench view_maintenance_benchmarks -- --test
  • Parsed full-suite.yml and publish.yml with PyYAML to check the views-bench if: inputs.perf_gate_only gating and that publish.yml calls full-suite with no inputs
  • make -n bench-views-remote REV=HEAD (resolves to scripts/perf-gate-remote.sh --bench views, which takes the shared flock)

✅ No issues found.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An admin scrapes /metrics/prometheus and sees inputlayer_view_reads_total, inputlayer_rule_evaluations_total and inputlayer_view_maintenance_us_total; a request without a key gets 401 ✅ pass live prometheus-scrape.txt
Each read of a deployed rule (bound or unbound, recursive or not, and a join of two deployed rules) raises rule_evaluations by exactly 1; a base-relation read raises it by 0; view_reads and view_maint… ✅ pass live prometheus-per-read-deltas.txt; views::every_read_of_a_deployed_rule_evaluates_it_once
Subscribing to a deployed rule counts 1 evaluation, and each refresh after a relevant commit counts 1 more ✅ pass live scenarios-views-test.txt (views::a_subscription_evaluates_its_deployed_rule_on_every_relevant_commit, against the real server binary)
Adversarial: a read of a deployed rule (with results) on a session holding session facts counts exactly 1 evaluation, not 2 ✅ pass live scenarios-views-test.txt (views::a_read_on_a_session_with_session_facts_evaluates_once)
scripts/bench-views.sh with a reduced matrix runs perf-gate views end to end and writes summary.md and result.json with ack, write->delta, read-during and CPU per write, RSS and counter columns; the k… ✅ pass live bench-views-smoke.log, bench-views-summary.md, bench-views-result.json
The view_maintenance_benchmarks criterion bench runs as the headroom reference (insert, delete, insert_cold, lookup at 10k–1M edges) ✅ pass live criterion-view-maintenance.txt
views-bench runs only on perf_gate_only workflow_dispatch and never on push, release PR or publish (workflow_call) runs ⏸️ untested no The prior payload did not establish a live result. GitHub Actions can't be dispatched from this phase, so the workflow was only checked statically by parsing full-suite.yml (views-bench if='${{ inputs…
make bench-views-remote runs under the benchmark host's shared lock and reproduces the Section 4 tables within noise ⏸️ untested no The user decided against a new benchmark-host run in this round and asked to cite the earlier remote runs on sam-dev-benchmarks instead: 20261005T185930Z (full matrix) and 20261005T192452Z (baseline 0…
  • CARGO_BUILD_JOBS=4 systemd-run --user --scope -p MemoryMax=16G scripts/bench-views.sh --edges 10000 --writes 3 --reads 2 --keyed 1,10,100 --filler-rules 50 (release build + live perf-gate views run, exit 0)
  • Manual: started the release inputlayer-server on a throwaway data dir (port 18931). Scraped /metrics/prometheus without a key (401) and with the generated admin key, then drove reads over WebSocket with a Node script and took a counter scrape before and after each read
  • cargo test --test scenarios -- views:: counters_scrape_the_running_engine (4 passed; each starts the real server binary)
  • cargo bench --bench view_maintenance_benchmarks -- --warm-up-time 1 --measurement-time 2 --sample-size 10
  • Parsed .github/workflows/full-suite.yml with PyYAML: the jobs' if: conditions, the perf_gate_only input, and that workflow_call has no inputs (publish.yml calls it without perf_gate_only)
  • make -n bench-views-remote REV=HEAD (dry run only: expands to scripts/perf-gate-remote.sh --rev HEAD --bench views --)
✅ **Document** - passed

✅ No issues found.

✅ No issues found.

✅ No issues found.

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ No issues found.

✅ No issues found.

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

✅ No issues found.

✅ No issues found.

✅ No issues found.

@jsam
jsam force-pushed the fm/m9-v1-benches-v2 branch from 48998e4 to 41d343d Compare October 5, 2026 20:46
@jsam jsam changed the title feat(perf-gate): views benchmark and view/rule-evaluation counters on /metrics/prometheus feat(perf-gate): add views benchmark and view/rule-evaluation counters on /metrics/prometheus Oct 5, 2026
@jsam
jsam force-pushed the fm/m9-v1-benches-v2 branch from 41d343d to 4e30aa7 Compare October 5, 2026 21:41
jsam added 10 commits October 5, 2026 21:42
…e time (#308)

Every read of a persistent rule (a query, a standing query's refresh, a
proof, a guarded write) whose dependency closure holds a deployed rule now
counts one rule evaluation; reads served from a view and view maintenance
time get their counters too, at zero until maintained views land. All
three are exported on /metrics/prometheus as
inputlayer_rule_evaluations_total, inputlayer_view_reads_total and
inputlayer_view_maintenance_us_total.
…yed rules (#308)

perf-gate views runs the incremental-views review's Section 4 matrix:
10K/100K/1M edges, idle reads of a non-recursive and a recursive view
bound and unbound, and closed-loop write phases with 0, 1 and 100 unbound
subscribers, 1/10/100/1,000 keyed subscribers, keyed subscribers of the
recursive view and a 200-rule catalog. Each phase reports ack,
write->delta and the ad-hoc read during writes (p50/p99), server CPU per
write, frames per write, RSS, and the engine's rule evaluations per read
and per write from /metrics/prometheus.

make bench-views and make bench-views-remote (perf-gate-remote.sh
--bench views, under the benchmark host's shared lock) run it; the full
suite runs it report-only as the Views benchmark job alongside the perf
gate.
#308)

The incremental-views review's differential-dataflow program (report
Appendix A.7) as a Criterion bench: the views benchmark's graph and three
rules kept as arrangements on one worker; one insert, retraction and
non-hot insert propagated through every view, and a key lookup, at 10K,
100K and 1M edges (VIEW_BENCH_EDGES).
@jsam
jsam force-pushed the fm/m9-v1-benches-v2 branch from 4e30aa7 to c103244 Compare October 5, 2026 21:59
@jsam
jsam merged commit 306df02 into main Oct 5, 2026
25 checks passed
@jsam
jsam deleted the fm/m9-v1-benches-v2 branch October 5, 2026 22:11
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