Skip to content

feat(perf-gate): views benchmark and view work counters on /metrics/prometheus - #369

Closed
jsam wants to merge 4 commits into
mainfrom
fm/m9-v1-benches
Closed

jsam wants to merge 4 commits into
mainfrom
fm/m9-v1-benches

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

  • The server now exports three counters on /metrics/prometheus: inputlayer_rule_evaluations_total, inputlayer_view_reads_total and inputlayer_view_maintenance_us_total. They live in the new src/execution/view_counters.rs. When a query, standing-query refresh, proof or guarded write depends on at least one persistent rule, the snapshot plan path counts one rule evaluation for that read (cached plans and dirty-session reads included). The other two counters stay at 0 until views exist. Unit, plan-cache and tests/e2e_reactive/views.rs tests cover this, and the REST API and query-profiling guides document the counters.
  • Adds the perf-gate views scenario (perf-gate/src/views/mod.rs). It reproduces the Section 4 matrix of the incremental-views review: 10k/100k/1M edges; recursive and non-recursive rules; no subscribers, unbound subscribers and keyed subscribers (1–1,000); and catalogs grown by 50 and 200 unrelated rules. For each write it reports p50/p99 ack, write→delta and ad-hoc read latency, plus server CPU, RSS and the counter deltas. A delta that never arrives fails the run; latency is reported but never fails it.
  • Wires the scenario up through scripts/bench-views.sh, make bench-views / make bench-views-remote (which runs on the benchmark host under the shared lock) and a report-only views-bench job in full-suite.yml that also runs with perf_gate_only. Also adds the Criterion bench benches/view_maintenance_benchmarks.rs, which measures the maintained-view micro-benchmark on differential dataflow as the headroom reference.

Risk Assessment

✅ Low: Most of the change is new benchmark tooling (perf-gate views, a criterion bench, scripts and a report-only CI job), and the engine change only adds process-wide atomic counters. Every read path counts exactly one rule evaluation: the query path, the plan-cached path, a session with session facts (the previous round's uncounted provenance baseline holds, and an e2e test that would have seen 2 before the fix covers it), .why, subscriptions and guarded writes.

Testing

I drove a real release server over WebSocket and scraped /metrics/prometheus around each read. The three counters are exported and start at 0. Each deployed-rule read adds exactly 1 to rule_evaluations, including dirty-session reads. Base and session-only reads add 0. With the F1 fix reverted, dirty-session reads add 2, so the check catches the regression. perf-gate views (10k and 100k edges) and scripts/bench-views.sh produced the Section 4 tables (ack, write->delta, read during writes, CPU per write, RSS, counter columns) with no late deltas. The criterion headroom bench and the e2e views tests passed. The full-suite views-bench job was only checked by parsing the workflow YAML, not run live. make bench-views-remote got as far as the benchmark host and was correctly refused by the shared lock because another session's benchmark held it, so the tables were not reproduced on that host. Local latencies were measured on a busy dev box and are not comparable to the review's numbers.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An operator scrapes /metrics/prometheus and sees view_reads, rule_evaluations and view_maintenance_us as counters ✅ pass live counters_transcript.txt: HELP/TYPE counter lines for all three counters, each starting at 0
Each read of a deployed rule (bound, unbound, recursive, or two deployed rules in one read) adds exactly 1 to rule_evaluations; base and ad-hoc reads add 0 ✅ pass live counters_transcript.txt 'reads on a clean session' block
Adversarial F1: a session with a session fact (and a session rule) reads a deployed rule that has results and counts exactly 1, not 2 ✅ pass live counters_transcript.txt dirty-session rows show Δrule_evaluations=1; with the fix reverted, counters_transcript_fix_reverted.txt shows 2
perf-gate views runs the Section 4 matrix (sizes, unbound and keyed subscribers, recursive and non-recursive views, 50/200 filler rules) and reports ack, write->delta p50/p99, read during writes, CPU… ✅ pass live views_local_summary.md, views_local_run.log (exit=0, 0 late in every phase)
The CI entry point scripts/bench-views.sh builds, runs the benchmark with pinned server CPUs and writes summary.md and result.json under target/bench-views/latest ✅ pass live bench_views_script.log (exit 0), bench_views_script_summary.md
The criterion headroom bench (report A.7) runs and reports insert/delete/insert_cold/lookup per size with load time, RSS and row counts ✅ pass live criterion_view_maintenance.txt
The full-suite views-bench job runs on perf_gate_only and on full runs (no !perf_gate_only guard, needs the gate) ⏸️ untested no The prior payload did not establish a live result: the job was never triggered, only its YAML was parsed. It is a GitHub Actions workflow_dispatch definition, and triggering it would start a billed sh…
make bench-views-remote runs under the shared lock and reproduces the Section 4 tables on the benchmark host ⏸️ untested no The prior payload did not establish a live result. make bench-views-remote REV=HEAD reached sam-dev-benchmarks, but the shared lock refused the run (exit 4) because another session's perf-gate run h…
Evidence: Live counter transcript at HEAD (one evaluation per deployed-rule read, including dirty session)
.kg create views -> result
--- /metrics/prometheus view counter lines at start ---
# 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
+edge[(0,1),(1,2),(2,3),(3,4),(4,5)] -> result
+hop2(X, Z) <- edge(X, Y), edge(Y, Z) -> result
+reach(X,Y) <- edge(X,Y) -> result
+reach(X,Z) <- edge(X,Y), reach(Y,Z) -> result
--- reads on a clean session ---
base relation                        ?edge(0, X)                  rows=1   Δrule_evaluations=0 Δview_reads=0 Δview_maintenance_us=0
deployed non-recursive, bound        ?hop2(0, Z)                  rows=1   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
deployed non-recursive, unbound      ?hop2(X, Z)                  rows=4   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
deployed recursive, bound            ?reach(0, X)                 rows=5   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
two deployed rules in one read       ?hop2(0, Z), reach(Z, W)     rows=3   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
ad-hoc over base facts               ?edge(X, Y), X > 2           rows=2   Δrule_evaluations=0 Δview_reads=0 Δview_maintenance_us=0
--- dirty session (session fact asserted) ---
session fact edge(10, 11) -> result
dirty session, deployed rule         ?hop2(0, Z)                  rows=1   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
dirty session, recursive rule        ?reach(0, X)                 rows=5   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
dirty session, base relation         ?edge(10, X)                 rows=1   Δrule_evaluations=0 Δview_reads=0 Δview_maintenance_us=0
session rule tmp -> result
dirty session, session rule only     ?tmp(X)                      rows=6   Δrule_evaluations=0 Δview_reads=0 Δview_maintenance_us=0
dirty session + session rule, deployed ?hop2(X, Z)                  rows=4   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
Evidence: Live counter transcript with the F1 fix reverted (dirty-session reads count 2)
.kg create views -> result
--- /metrics/prometheus view counter lines at start ---
# 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
+edge[(0,1),(1,2),(2,3),(3,4),(4,5)] -> result
+hop2(X, Z) <- edge(X, Y), edge(Y, Z) -> result
+reach(X,Y) <- edge(X,Y) -> result
+reach(X,Z) <- edge(X,Y), reach(Y,Z) -> result
--- reads on a clean session ---
base relation                        ?edge(0, X)                  rows=1   Δrule_evaluations=0 Δview_reads=0 Δview_maintenance_us=0
deployed non-recursive, bound        ?hop2(0, Z)                  rows=1   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
deployed non-recursive, unbound      ?hop2(X, Z)                  rows=4   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
deployed recursive, bound            ?reach(0, X)                 rows=5   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
two deployed rules in one read       ?hop2(0, Z), reach(Z, W)     rows=3   Δrule_evaluations=1 Δview_reads=0 Δview_maintenance_us=0
ad-hoc over base facts               ?edge(X, Y), X > 2           rows=2   Δrule_evaluations=0 Δview_reads=0 Δview_maintenance_us=0
--- dirty session (session fact asserted) ---
session fact edge(10, 11) -> result
dirty session, deployed rule         ?hop2(0, Z)                  rows=1   Δrule_evaluations=2 Δview_reads=0 Δview_maintenance_us=0
dirty session, recursive rule        ?reach(0, X)                 rows=5   Δrule_evaluations=2 Δview_reads=0 Δview_maintenance_us=0
dirty session, base relation         ?edge(10, X)                 rows=1   Δrule_evaluations=0 Δview_reads=0 Δview_maintenance_us=0
session rule tmp -> result
dirty session, session rule only     ?tmp(X)                      rows=6   Δrule_evaluations=0 Δview_reads=0 Δview_maintenance_us=0
dirty session + session rule, deployed ?hop2(X, Z)                  rows=4   Δrule_evaluations=2 Δview_reads=0 Δview_maintenance_us=0
Evidence: Counter driver script
// Drives a real inputlayer-server over /ws and scrapes /metrics/prometheus around each read.
const port = process.argv[2];
const base = `127.0.0.1:${port}`;
function conn(kg) {
  return new Promise((res, rej) => {
    const ws = new WebSocket(`ws://${base}/ws?kg=${kg}`);
    const queue = []; const waiters = [];
    ws.onmessage = (m) => { const f = JSON.parse(m.data); if (waiters.length) waiters.shift()(f); else queue.push(f); };
    ws.onerror = rej;
    const next = () => new Promise(r => queue.length ? r(queue.shift()) : waiters.push(r));
    ws.onopen = () => res({ ws, next, async exec(p) { ws.send(JSON.stringify({type:'execute', program:p})); for(;;){ const f = await next(); if (f.type==='result'||f.type==='error') return f; } } });
  });
}
async function login(kg) { const c = await conn(kg); c.ws.send(JSON.stringify({type:'login',username:'admin',password:'pw-test-long-enough'})); const f = await c.next(); if (f.type!=='authenticated') throw new Error(JSON.stringify(f)); return c; }
let key;
async function scrape() {
  const r = await fetch(`http://${base}/metrics/prometheus`, {headers:{Authorization:`Bearer ${key}`}});
  const t = await r.text(); const g = n => Number((t.match(new RegExp(`^${n} (\\d+)`, 'm'))||[])[1]);
  return {text:t, rule_evaluations:g('inputlayer_rule_evaluations_total'), view_reads:g('inputlayer_view_reads_total'), view_maintenance_us:g('inputlayer_view_maintenance_us_total')};
}
const out = [];
const log = s => { console.log(s); out.push(s); };
const d = await login('default');
log(`.kg create views -> ${(await d.exec('.kg create views')).type}`);
const a = await login('views');
const k = await a.exec('.apikey create drive'); key = k.rows[0][1];
const m0 = await scrape();
log('--- /metrics/prometheus view counter lines at start ---');
for (const l of m0.text.split('\n')) if (/view_reads|rule_evaluations|view_maintenance/.test(l)) log(l);
for (const p of ['+edge[(0,1),(1,2),(2,3),(3,4),(4,5)]', '+hop2(X, Z) <- edge(X, Y), edge(Y, Z)', '+reach(X,Y) <- edge(X,Y)', '+reach(X,Z) <- edge(X,Y), reach(Y,Z)']) {
  const r = await a.exec(p); log(`${p} -> ${r.type}${r.type==='error'?' '+JSON.stringify(r):''}`);
}
const reader = await login('views');
async function read(c, q, label) {
  const b = await scrape(); const r = await c.exec(q); const e = await scrape();
  const rows = r.rows ? r.rows.length : 0;
  log(`${label.padEnd(36)} ${q.padEnd(28)} rows=${String(rows).padEnd(3)} Δrule_evaluations=${e.rule_evaluations-b.rule_evaluations} Δview_reads=${e.view_reads-b.view_reads} Δview_maintenance_us=${e.view_maintenance_us-b.view_maintenance_us}${r.type==='error'?' ERR '+JSON.stringify(r):''}`);
  return e.rule_evaluations-b.rule_evaluations;
}
log('--- reads on a clean session ---');
await read(reader, '?edge(0, X)', 'base relation');
await read(reader, '?hop2(0, Z)', 'deployed non-recursive, bound');
await read(reader, '?hop2(X, Z)', 'deployed non-recursive, unbound');
await read(reader, '?reach(0, X)', 'deployed recursive, bound');
await read(reader, '?hop2(0, Z), reach(Z, W)', 'two deployed rules in one read');
await read(reader, '?edge(X, Y), X > 2', 'ad-hoc over base facts');
log('--- dirty session (session fact asserted) ---');
const s = await login('views');
const sf = await s.exec('edge(10, 11)'); log(`session fact edge(10, 11) -> ${sf.type}${sf.type==='error'?' '+JSON.stringify(sf):''}`);
await read(s, '?hop2(0, Z)', 'dirty session, deployed rule');
await read(s, '?reach(0, X)', 'dirty session, recursive rule');
await read(s, '?edge(10, X)', 'dirty session, base relation');
const sr = await s.exec('tmp(X) <- edge(X, _)'); log(`session rule tmp -> ${sr.type}${sr.type==='error'?' '+JSON.stringify(sr):''}`);
await read(s, '?tmp(X)', 'dirty session, session rule only');
await read(s, '?hop2(X, Z)', 'dirty session + session rule, deployed');
const fs = await import('node:fs');
fs.writeFileSync(process.argv[3], out.join('\n') + '\n');
process.exit(0);
Evidence: perf-gate views summary (10k/100k edges)
# Views: cost of reads and writes against deployed rules

- candidate: f64b7d0a local (/tmp/il-views-bench/inputlayer-server)
- host computeinstance-e00sx5ndp6kwb7pqfv (32 CPUs, server CPUs any), load average 24.22 64.86 47.91 12/1967 3511468 -> 115.16 82.11 55.31 193/4295 3537056

## 10000 edges (candidate)

Load: 10002 edges, 34 hot heads in 55 ms (server CPU 10 ms), RSS 43 MB. Peak RSS 147 MB.

Registration: fillers_to_200 12942 ms, fillers_to_50 980 ms, hot_reach 9 ms, path 6 ms, path_step 8 ms, r1 10 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 | 1.1 / 1.4 | 0.0 | 1.0 | 0.0 |
| `?path(400,Y)` | 4 | 3 | 20.0 / 20.3 | 20.0 | 1.0 | 0.0 |
| `?hot_reach(X,Y)` | 4 | 102 | 21.7 / 22.1 | 23.3 | 1.0 | 0.0 |
| `?hot_reach(400,Y)` | 4 | 3 | 21.7 / 21.8 | 20.0 | 1.0 | 0.0 |
| `?r1(X,Y)` | 54 | 114 | 1.9 / 1.9 | 0.0 | 1.0 | 0.0 |
| `?r1(400,Y)` | 54 | 7 | 1.0 / 1.6 | 3.3 | 1.0 | 0.0 |
| `?r1(X,Y)` | 204 | 124 | 2.2 / 3.2 | 3.3 | 1.0 | 0.0 |
| `?r1(400,Y)` | 204 | 8 | 1.3 / 1.4 | 0.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 / 5.6 | - | 0.8 | 2.0 | 0.0 | 1.0 | 0.0 | 46 | 0 |
| P1_r1_unbound_x1 | 1 | 1.6 / 2.6 | 2.8 / 3.6 | 0.9 | 3.0 | 2.0 | 2.0 | 0.0 | 47 | 0 |
| P2_r1_unbound_x100 | 100 | 1.9 / 3.8 | 3.6 / 5.9 | 0.9 | 7.0 | 200.0 | 2.0 | 0.0 | 50 | 0 |
| P3_hot_reach_unbound_x1 | 1 | 2.0 / 3.3 | 24.1 / 31.5 | 1.1 | 25.0 | 2.0 | 2.0 | 0.0 | 59 | 0 |
| P4_r1_keyed_x1 | 1 | 1.9 / 4.5 | 2.4 / 7.3 | 0.9 | 2.0 | 2.0 | 2.0 | 0.0 | 59 | 0 |
| P4_r1_keyed_x10 | 10 | 1.9 / 10.9 | 2.8 / 11.6 | 1.3 | 9.0 | 11.0 | 11.0 | 0.0 | 63 | 0 |
| P5_hot_reach_keyed_x1 | 1 | 2.1 / 2.7 | 24.1 / 31.9 | 1.2 | 27.0 | 2.0 | 2.0 | 0.0 | 99 | 0 |
| P5_hot_reach_keyed_x10 | 10 | 2.1 / 5.3 | 31.7 / 53.1 | 1.3 | 269.0 | 11.0 | 11.0 | 0.0 | 147 | 0 |
| P6_r1_unbound_x1_rules54 | 1 | 1.9 / 2.3 | 3.1 / 4.1 | 1.0 | 3.0 | 2.0 | 2.0 | 0.0 | 147 | 0 |
| P6_r1_unbound_x1_rules204 | 1 | 2.6 / 2.9 | 3.9 / 4.7 | 1.1 | 4.0 | 2.0 | 2.0 | 0.0 | 147 | 0 |

Subscribe: 1 x `?r1(X,Y)` 3 ms, 100 x `?r1(X,Y)` 62 ms, 1 x `?hot_reach(X,Y)` 24 ms, 1 x `?r1(0,Y)` 3 ms, 10 x `?r1(0,Y)` 5 ms, 1 x `?hot_reach(0,Y)` 30 ms, 10 x `?hot_reach(0,Y)` 54 ms, 1 x `?r1(X,Y)` 3 ms, 1 x `?r1(X,Y)` 3 ms.

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

## 100000 edges (candidate)

Load: 100002 edges, 334 hot heads in 300 ms (server CPU 220 ms), RSS 53 MB. Peak RSS 1066 MB.

Registration: fillers_to_200 15878 ms, fillers_to_50 2149 ms, hot_reach 7 ms, path 6 ms, path_step 7 ms, r1 8 ms.

| read | rules | rows | p50 / p99 ms | server CPU ms/read | rule evaluations/read | view reads/read |
|---|---|---|---|---|---|---|
| `?edge(400,Y)` | 4 | 1 | 0.8 / 1.1 | 0.0 | 0.0 | 0.0 |
| `?r1(X,Y)` | 4 | 334 | 6.7 / 6.8 | 6.7 | 1.0 | 0.0 |
| `?r1(400,Y)` | 4 | 1 | 1.2 / 1.3 | 3.3 | 1.0 | 0.0 |
| `?path(400,Y)` | 4 | 3 | 275.3 / 299.5 | 280.0 | 1.0 | 0.0 |
| `?hot_reach(X,Y)` | 4 | 1002 | 288.9 / 289.8 | 290.0 | 1.0 | 0.0 |
| `?hot_reach(400,Y)` | 4 | 3 | 284.0 / 284.1 | 283.3 | 1.0 | 0.0 |
| `?r1(X,Y)` | 54 | 424 | 7.6 / 7.8 | 6.7 | 1.0 | 0.0 |
| `?r1(400,Y)` | 54 | 8 | 1.8 / 2.5 | 3.3 | 1.0 | 0.0 |
| `?r1(X,Y)` | 204 | 434 | 7.8 / 8.0 | 6.7 | 1.0 | 0.0 |
| `?r1(400,Y)` | 204 | 9 | 1.8 / 2.0 | 3.3 | 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 / 6.2 | - | 1.3 | 2.0 | 0.0 | 1.0 | 0.0 | 39 | 0 |
| P1_r1_unbound_x1 | 1 | 1.9 / 6.3 | 8.5 / 13.0 | 1.6 | 9.0 | 2.0 | 2.0 | 0.0 | 39 | 0 |
| P2_r1_unbound_x100 | 100 | 2.2 / 10.4 | 9.2 / 17.6 | 1.7 | 14.0 | 200.0 | 2.0 | 0.0 | 43 | 0 |
| P3_hot_reach_unbound_x1 | 1 | 2.3 / 3.3 | 296.8 / 305.9 | 1.8 | 301.0 | 2.0 | 2.0 | 0.0 | 43 | 0 |
| P4_r1_keyed_x1 | 1 | 1.9 / 2.4 | 3.5 / 4.2 | 1.5 | 4.0 | 2.0 | 2.0 | 0.0 | 43 | 0 |
| P4_r1_keyed_x10 | 10 | 1.6 / 3.9 | 3.0 / 5.7 | 1.7 | 17.0 | 11.0 | 11.0 | 0.0 | 47 | 0 |
| P4_r1_keyed_x100 | 100 | 2.3 / 5.7 | 6.6 / 10.6 | 10.0 | 173.0 | 101.0 | 101.0 | 0.0 | 58 | 0 |
| P5_hot_reach_keyed_x1 | 1 | 2.3 / 2.6 | 319.6 / 331.3 | 1.9 | 316.0 | 2.0 | 2.0 | 0.0 | 59 | 0 |
| P5_hot_reach_keyed_x10 | 10 | 2.5 / 520.0 | 786.5 / 1698.1 | 3.4 | 3997.0 | 11.0 | 11.0 | 0.0 | 104 | 0 |
| P6_r1_unbound_x1_rules54 | 1 | 2.3 / 8.3 | 9.4 / 15.3 | 2.0 | 11.0 | 2.0 | 2.0 | 0.0 | 107 | 0 |
| P6_r1_unbound_x1_rules204 | 1 | 3.1 / 5.1 | 10.3 / 12.1 | 1.7 | 11.0 | 2.0 | 2.0 | 0.0 | 122 | 0 |

Subscribe: 1 x `?r1(X,Y)` 9 ms, 100 x `?r1(X,Y)` 55 ms, 1 x `?hot_reach(X,Y)` 328 ms, 1 x `?r1(0,Y)` 5 ms, 10 x `?r1(0,Y)` 5 ms, 100 x `?r1(0,Y)` 31 ms, 1 x `?hot_reach(0,Y)` 346 ms, 10 x `?hot_reach(0,Y)` 770 ms, 1 x `?r1(X,Y)` 8 ms, 1 x `?r1(X,Y)` 9 ms.
Evidence: perf-gate views result.json
{"schema":"inputlayer-perf-gate/views/v1","created_unix":1791227541,"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":"24.22 64.86 47.91 12/1967 3511468","loadavg_end":"115.16 82.11 55.31 193/4295 3537056","data_fs":"ext4","data_root":"/tmp/il-views-bench/servers","server_cpus":null,"build":""},"options":{"writes":10,"large_writes":20,"reads":3,"keyed":[1,10,100],"recursive_keyed":[1,10],"large_recursive_keyed":[1,10],"filler_rules":[50,200]},"arms":[["candidate","f64b7d0a local (/tmp/il-views-bench/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.878293,"server_cpu_ms":9.999999999999995,"rss_mb":43},"registration_ms":{"fillers_to_200":12942.332006999999,"fillers_to_50":980.2182,"hot_reach":9.239577,"path":6.461907999999999,"path_step":8.242829,"r1":9.788764},"reads":[{"name":"edge_point","query":"?edge(400,Y)","rules":4,"rows":1,"truncated":false,"latency_ms":{"n":3,"p50":0.532991,"p95":0.769702,"p99":0.769702,"max":0.769702},"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":3,"p50":1.3323180000000001,"p95":1.4203620000000001,"p99":1.4203620000000001,"max":1.4203620000000001},"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":3,"p50":1.138443,"p95":1.39904,"p99":1.39904,"max":1.39904},"server_cpu_ms":0.0,"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":3,"p50":19.953629,"p95":20.349712,"p99":20.349712,"max":20.349712},"server_cpu_ms":20.0,"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":3,"p50":21.732578,"p95":22.091575,"p99":22.091575,"max":22.091575},"server_cpu_ms":23.333333333333332,"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":3,"p50":21.662501,"p95":21.844147,"p99":21.844147,"max":21.844147},"server_cpu_ms":20.0,"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":114,"truncated":false,"latency_ms":{"n":3,"p50":1.925392,"p95":1.934606,"p99":1.934606,"max":1.934606},"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":7,"truncated":false,"latency_ms":{"n":3,"p50":1.027354,"p95":1.583731,"p99":1.583731,"max":1.583731},"server_cpu_ms":3.3333333333332624,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"r1_unbound_rules204","query":"?r1(X,Y)","rules":204,"rows":124,"truncated":false,"latency_ms":{"n":3,"p50":2.186048,"p95":3.151078,"p99":3.151078,"max":3.151078},"server_cpu_ms":3.3333333333332624,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"r1_bound_rules204","query":"?r1(400,Y)","rules":204,"rows":8,"truncated":false,"latency_ms":{"n":3,"p50":1.262332,"p95":1.428514,"p99":1.428514,"max":1.428514},"server_cpu_ms":0.0,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}}],"phases":[{"name":"P0_no_subs","subscribers":0,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":1.522088,"p95":5.611663999999999,"p99":5.611663999999999,"max":5.611663999999999},"write_to_delta_ms":{"n":0,"p50":null,"p95":null,"p99":null,"max":null},"read_during_ms":{"n":10,"p50":0.7958919999999999,"p95":1.0887769999999999,"p99":1.0887769999999999,"max":1.0887769999999999},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":1.999999999999996,"frames_per_write":0.0,"work_per_write":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":46},{"name":"P1_r1_unbound_x1","subscribers":1,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":1.6219400000000002,"p95":2.575852,"p99":2.575852,"max":2.575852},"write_to_delta_ms":{"n":10,"p50":2.80465,"p95":3.647413,"p99":3.647413,"max":3.647413},"read_during_ms":{"n":10,"p50":0.861071,"p95":1.236413,"p99":1.236413,"max":1.236413},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":3.0000000000000027,"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":10,"ack_ms":{"n":10,"p50":1.929137,"p95":3.8455049999999997,"p99":3.8455049999999997,"max":3.8455049999999997},"write_to_delta_ms":{"n":10,"p50":3.62526,"p95":5.918721,"p99":5.918721,"max":5.918721},"read_during_ms":{"n":10,"p50":0.948454,"p95":1.6999579999999999,"p99":1.6999579999999999,"max":1.6999579999999999},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":7.0,"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":10,"ack_ms":{"n":10,"p50":2.041538,"p95":3.318483,"p99":3.318483,"max":3.318483},"write_to_delta_ms":{"n":10,"p50":24.070588,"p95":31.528733000000003,"p99":31.528733000000003,"max":31.528733000000003},"read_during_ms":{"n":10,"p50":1.058001,"p95":1.3776979999999999,"p99":1.3776979999999999,"max":1.3776979999999999},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":25.0,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":59},{"name":"P4_r1_keyed_x1","subscribers":1,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":1.925692,"p95":4.537539,"p99":4.537539,"max":4.537539},"write_to_delta_ms":{"n":10,"p50":2.3911000000000002,"p95":7.306464,"p99":7.306464,"max":7.306464},"read_during_ms":{"n":10,"p50":0.945831,"p95":3.9471899999999995,"p99":3.9471899999999995,"max":3.9471899999999995},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":2.0000000000000018,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":59},{"name":"P4_r1_keyed_x10","subscribers":10,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":1.856066,"p95":10.914067,"p99":10.914067,"max":10.914067},"write_to_delta_ms":{"n":10,"p50":2.7662519999999997,"p95":11.59938,"p99":11.59938,"max":11.59938},"read_during_ms":{"n":10,"p50":1.291407,"p95":1.633297,"p99":1.633297,"max":1.633297},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":9.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":10,"ack_ms":{"n":10,"p50":2.09506,"p95":2.668072,"p99":2.668072,"max":2.668072},"write_to_delta_ms":{"n":10,"p50":24.145132,"p95":31.944356,"p99":31.944356,"max":31.944356},"read_during_ms":{"n":10,"p50":1.156121,"p95":1.3067,"p99":1.3067,"max":1.3067},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":27.0,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":99},{"name":"P5_hot_reach_keyed_x10","subscribers":10,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":2.060257,"p95":5.347201999999999,"p99":5.347201999999999,"max":5.

... [3611 bytes truncated] ...

":289.81548699999996},"server_cpu_ms":289.99999999999994,"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":3,"p50":283.976517,"p95":284.07683000000003,"p99":284.07683000000003,"max":284.07683000000003},"server_cpu_ms":283.33333333333337,"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":424,"truncated":false,"latency_ms":{"n":3,"p50":7.591568,"p95":7.7878870000000004,"p99":7.7878870000000004,"max":7.7878870000000004},"server_cpu_ms":6.666666666667709,"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":8,"truncated":false,"latency_ms":{"n":3,"p50":1.844078,"p95":2.458863,"p99":2.458863,"max":2.458863},"server_cpu_ms":3.33333333333267,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"r1_unbound_rules204","query":"?r1(X,Y)","rules":204,"rows":434,"truncated":false,"latency_ms":{"n":3,"p50":7.837112,"p95":7.989573999999999,"p99":7.989573999999999,"max":7.989573999999999},"server_cpu_ms":6.66666666666534,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}},{"name":"r1_bound_rules204","query":"?r1(400,Y)","rules":204,"rows":9,"truncated":false,"latency_ms":{"n":3,"p50":1.823726,"p95":2.01659,"p99":2.01659,"max":2.01659},"server_cpu_ms":3.333333333335039,"work":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0}}],"phases":[{"name":"P0_no_subs","subscribers":0,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":1.522979,"p95":6.208613,"p99":6.208613,"max":6.208613},"write_to_delta_ms":{"n":0,"p50":null,"p95":null,"p99":null,"max":null},"read_during_ms":{"n":10,"p50":1.309223,"p95":1.733309,"p99":1.733309,"max":1.733309},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":2.0000000000000018,"frames_per_write":0.0,"work_per_write":{"rule_evaluations":1.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":39},{"name":"P1_r1_unbound_x1","subscribers":1,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":1.851339,"p95":6.3011029999999995,"p99":6.3011029999999995,"max":6.3011029999999995},"write_to_delta_ms":{"n":10,"p50":8.504949,"p95":12.979551,"p99":12.979551,"max":12.979551},"read_during_ms":{"n":10,"p50":1.6088090000000002,"p95":2.144355,"p99":2.144355,"max":2.144355},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":8.999999999999986,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":39},{"name":"P2_r1_unbound_x100","subscribers":100,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":2.180359,"p95":10.412964,"p99":10.412964,"max":10.412964},"write_to_delta_ms":{"n":10,"p50":9.216272,"p95":17.649411,"p99":17.649411,"max":17.649411},"read_during_ms":{"n":10,"p50":1.67486,"p95":1.965032,"p99":1.965032,"max":1.965032},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":14.00000000000001,"frames_per_write":200.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":43},{"name":"P3_hot_reach_unbound_x1","subscribers":1,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":2.287432,"p95":3.285803,"p99":3.285803,"max":3.285803},"write_to_delta_ms":{"n":10,"p50":296.77788,"p95":305.93037499999997,"p99":305.93037499999997,"max":305.93037499999997},"read_during_ms":{"n":10,"p50":1.765388,"p95":1.8623260000000001,"p99":1.8623260000000001,"max":1.8623260000000001},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":301.0,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":43},{"name":"P4_r1_keyed_x1","subscribers":1,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":1.947745,"p95":2.413494,"p99":2.413494,"max":2.413494},"write_to_delta_ms":{"n":10,"p50":3.496463,"p95":4.169137,"p99":4.169137,"max":4.169137},"read_during_ms":{"n":10,"p50":1.5195040000000002,"p95":2.122391,"p99":2.122391,"max":2.122391},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":4.0000000000000036,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":43},{"name":"P4_r1_keyed_x10","subscribers":10,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":1.621328,"p95":3.8946699999999996,"p99":3.8946699999999996,"max":3.8946699999999996},"write_to_delta_ms":{"n":10,"p50":2.996993,"p95":5.701961,"p99":5.701961,"max":5.701961},"read_during_ms":{"n":10,"p50":1.68815,"p95":2.050051,"p99":2.050051,"max":2.050051},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":16.999999999999993,"frames_per_write":11.0,"work_per_write":{"rule_evaluations":11.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":47},{"name":"P4_r1_keyed_x100","subscribers":100,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":2.342657,"p95":5.730585,"p99":5.730585,"max":5.730585},"write_to_delta_ms":{"n":10,"p50":6.598997,"p95":10.566668,"p99":10.566668,"max":10.566668},"read_during_ms":{"n":10,"p50":10.01616,"p95":11.585829,"p99":11.585829,"max":11.585829},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":173.00000000000006,"frames_per_write":101.0,"work_per_write":{"rule_evaluations":101.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":58},{"name":"P5_hot_reach_keyed_x1","subscribers":1,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":2.258388,"p95":2.625317,"p99":2.625317,"max":2.625317},"write_to_delta_ms":{"n":10,"p50":319.61424500000004,"p95":331.315821,"p99":331.315821,"max":331.315821},"read_during_ms":{"n":10,"p50":1.9454019999999999,"p95":2.615702,"p99":2.615702,"max":2.615702},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":316.0,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":59},{"name":"P5_hot_reach_keyed_x10","subscribers":10,"rules":4,"writes":10,"ack_ms":{"n":10,"p50":2.483982,"p95":519.996885,"p99":519.996885,"max":519.996885},"write_to_delta_ms":{"n":10,"p50":786.502826,"p95":1698.0553619999998,"p99":1698.0553619999998,"max":1698.0553619999998},"read_during_ms":{"n":10,"p50":3.3693500000000003,"p95":245.087981,"p99":245.087981,"max":245.087981},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":3997.0,"frames_per_write":11.0,"work_per_write":{"rule_evaluations":11.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":104},{"name":"P6_r1_unbound_x1_rules54","subscribers":1,"rules":54,"writes":10,"ack_ms":{"n":10,"p50":2.2532,"p95":8.323331999999999,"p99":8.323331999999999,"max":8.323331999999999},"write_to_delta_ms":{"n":10,"p50":9.447994,"p95":15.29043,"p99":15.29043,"max":15.29043},"read_during_ms":{"n":10,"p50":1.98375,"p95":2.488288,"p99":2.488288,"max":2.488288},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":10.999999999999943,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":107},{"name":"P6_r1_unbound_x1_rules204","subscribers":1,"rules":204,"writes":10,"ack_ms":{"n":10,"p50":3.1050980000000004,"p95":5.110380999999999,"p99":5.110380999999999,"max":5.110380999999999},"write_to_delta_ms":{"n":10,"p50":10.323186999999999,"p95":12.088405,"p99":12.088405,"max":12.088405},"read_during_ms":{"n":10,"p50":1.7106039999999998,"p95":2.634892,"p99":2.634892,"max":2.634892},"late":0,"subscription_errors":0,"server_cpu_ms_per_write":10.999999999999943,"frames_per_write":2.0,"work_per_write":{"rule_evaluations":2.0,"view_reads":0.0,"view_maintenance_us":0.0},"rss_mb":122}],"subscribes":[{"query":"?r1(X,Y)","count":1,"ms":8.629098},{"query":"?r1(X,Y)","count":100,"ms":55.441781000000006},{"query":"?hot_reach(X,Y)","count":1,"ms":328.226701},{"query":"?r1(0,Y)","count":1,"ms":4.523928},{"query":"?r1(0,Y)","count":10,"ms":5.403948000000001},{"query":"?r1(0,Y)","count":100,"ms":31.459778},{"query":"?hot_reach(0,Y)","count":1,"ms":346.367168},{"query":"?hot_reach(0,Y)","count":10,"ms":770.079451},{"query":"?r1(X,Y)","count":1,"ms":8.166855},{"query":"?r1(X,Y)","count":1,"ms":9.335083}],"notes":[],"peak_rss_kb":1092212,"error":null}]}
  • Evidence: scripts/bench-views.sh summary (CI entry point) (local file: ~/.no-mistakes/evidence/01M46NN53TNDNEBVR0VME6S3Y3/bench_views_script_summary.md)
Evidence: Criterion view_maintenance bench output
Benchmarking view_maintenance/lookup/10000: Analyzing
view_maintenance/lookup/10000
                        time:   [141.45 ns 144.81 ns 148.29 ns]
Found 3 outliers among 100 measurements (3.00%)
  3 (3.00%) high mild

view_maintenance 100000 edges: load 148 ms, RSS 69 MB holding the views; rows r1=334 path=200004 hot_reach=1002
Benchmarking view_maintenance/insert/100000
Benchmarking view_maintenance/insert/100000: Warming up for 1.0000 s
Benchmarking view_maintenance/insert/100000: Collecting 100 samples in estimated 3.6000 s (15k iterations)
Benchmarking view_maintenance/insert/100000: Analyzing
view_maintenance/insert/100000
                        time:   [118.34 µs 120.05 µs 122.02 µs]
Found 5 outliers among 100 measurements (5.00%)
  2 (2.00%) high mild
  3 (3.00%) high severe
Benchmarking view_maintenance/delete/100000
Benchmarking view_maintenance/delete/100000: Warming up for 1.0000 s
Benchmarking view_maintenance/delete/100000: Collecting 100 samples in estimated 4.3312 s (15k iterations)
Benchmarking view_maintenance/delete/100000: Analyzing
view_maintenance/delete/100000
                        time:   [142.19 µs 142.48 µs 142.78 µs]
Found 10 outliers among 100 measurements (10.00%)
  3 (3.00%) low severe
  2 (2.00%) low mild
  4 (4.00%) high mild
  1 (1.00%) high severe
Benchmarking view_maintenance/insert_cold/100000
Benchmarking view_maintenance/insert_cold/100000: Warming up for 1.0000 s
Benchmarking view_maintenance/insert_cold/100000: Collecting 100 samples in estimated 3.7610 s (15k iterations)
Benchmarking view_maintenance/insert_cold/100000: Analyzing
view_maintenance/insert_cold/100000
                        time:   [117.69 µs 119.92 µs 122.54 µs]
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high severe
Benchmarking view_maintenance/lookup/100000
Benchmarking view_maintenance/lookup/100000: Warming up for 1.0000 s
Benchmarking view_maintenance/lookup/100000: Collecting 100 samples in estimated 3.0006 s (20M iterations)
Benchmarking view_maintenance/lookup/100000: Analyzing
view_maintenance/lookup/100000
                        time:   [153.16 ns 156.29 ns 159.89 ns]
Found 4 outliers among 100 measurements (4.00%)
  1 (1.00%) high mild
  3 (3.00%) high severe
  • Evidence: e2e_reactive views tests (local file: ~/.no-mistakes/evidence/01M46NN53TNDNEBVR0VME6S3Y3/e2e_views.txt)
  • Evidence: make bench-views-remote log (lock refusal) (local file: ~/.no-mistakes/evidence/01M46NN53TNDNEBVR0VME6S3Y3/bench_views_remote.log)
  • Evidence: full-suite.yml job conditions (local file: ~/.no-mistakes/evidence/01M46NN53TNDNEBVR0VME6S3Y3/full_suite_jobs.txt)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 1 issue found → auto-fixed ✅
  • ⚠️ gui/lib/docs-bundle.ts - merge conflict rebasing onto origin/main

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

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ src/storage_engine/snapshot.rs:431 - rule_evaluations counts some reads twice. Every run_program call records an evaluation. A query in a session that holds session facts or rules (the slow path in src/protocol/handler.rs:3858-3893) runs the session program through execute_program_with_session_facts_profiled, which records one. When there are results, it then runs the provenance baseline snapshot.execute_program_with_rules(query), which records a second. Example: a session asserts a session fact, then reads ?hop2(0,Z) with a deployed hop2 and gets non-empty results; the counter goes up by 2. This breaks the intent's "rule_evaluations increments once per deployed-rule read today" and the docs' "A read counts once" (docs/content/docs/guides/query-profiling.mdx:176, src/execution/view_counters.rs:575-578). The same class appears at src/protocol/handler.rs:3862 and :3891 (one read, two counted runs). The e2e case labelled "session rule over base facts" (tests/e2e_reactive/views.rs:983, ?edge(X, Y), X &gt; 2) uses no session state, so it never reaches the slow path and doesn't cover this. Fix: run the provenance baseline through a non-counting variant, or record the evaluation once at the read boundary instead of per run_program. Then add an e2e case that uses a dirty session.
  • ⚠️ scripts/bench-views.sh:410 - Simplification: the before/after comparison mode (--baseline-rev in scripts/bench-views.sh:410-427, plus --compare/--compare-label and the baseline/candidate arms in perf-gate/src/views/mod.rs:97-101,316-328) builds and measures a second server from git archive. Issue Baseline benches and counters for the view work (V1) #308 asks for the Section 4 matrix as a perf-gate scenario, make bench-views-remote under the lock, the criterion bench and the counters. Nothing in it requires comparing two binaries in one run. Recommend removing the comparison mode unless you want to keep it, for parity with bench-sessions.

🔧 Fix applied.
1 warning still open:

  • ⚠️ scripts/bench-views.sh:410 - Simplification: the before/after comparison mode (--baseline-rev in scripts/bench-views.sh:410-427, plus --compare/--compare-label and the baseline/candidate arms in perf-gate/src/views/mod.rs:97-101,316-328) builds and measures a second server from git archive. Issue Baseline benches and counters for the view work (V1) #308 asks for the Section 4 matrix as a perf-gate scenario, make bench-views-remote under the lock, the criterion bench and the counters. Nothing in it requires comparing two binaries in one run. Recommend removing the comparison mode unless you want to keep it, for parity with bench-sessions.

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Operator scrapes /metrics/prometheus and sees view_reads, rule_evaluations, view_maintenance_us counters ✅ pass live live-counters-transcript.txt: initial scrape shows HELP/TYPE counter lines for inputlayer_view_reads_total, inputlayer_rule_evaluations_total, inputlayer_view_maintenance_us_total
Each read of a deployed rule (non-recursive, recursive, bound/unbound, two rules in one query) increments rule_evaluations by exactly 1; a base-relation read adds 0; 3 reads add 3 ✅ pass live live-counters-transcript.txt STEP lines (+0 base, +1 hop2, +1 reach unbound, +1 two rules, +3 three reads); e2e-views.log
Adversarial (F1): a read in a dirty session (session fact) of a deployed rule with results counts one evaluation, not two ✅ pass live live-counters-transcript.txt: 'session fact only' +0, 'session fact + ?hop2(0,Z)' +1, 'session fact + 3 reads' +3; e2e test a_read_in_a_session_with_session_facts_evaluates_its_deployed_rule_once ok
Developer runs scripts/bench-views.sh (perf-gate views) and gets the Section 4 matrix tables: p50/p99 ack, write->delta, ad-hoc read during writes, CPU per write, RSS and counters per phase, with no l… ✅ pass live bench-views-summary.md / bench-views-local.log: P0–P6 phases at 10k and 100k edges, 0 late, exit 0
Developer runs the kept --baseline-rev compare mode and gets baseline vs candidate tables; a baseline server without the counters shows empty work columns rather than failing ✅ pass live bench-views-compare-summary.md / bench-views-compare.log: baseline section notes 'the server exports no view work counters'; candidate shows 1.0 evaluations/read; exit 0
Developer runs the criterion maintained-view micro-benchmark (headroom reference) ✅ pass live criterion-view-maintenance.log: insert/delete/insert_cold ~95–103 µs and lookup ~124–150 ns at 10k/100k edges, with load/RSS/row counts printed
full-suite.yml runs the views bench as a perf-gate-only job (runs with perf_gate_only, report-only) ⏸️ untested no The prior payload did not establish a live result. This is a GitHub Actions workflow and needs a CI run on GitHub, which this local test phase cannot trigger. It was only checked statically by parsing…
make bench-views-remote runs under the benchmark host's shared lock and reproduces the Section 4 tables (1M edges, 1,000 keyed subscribers) within noise ⏸️ untested no This target ssh'es to the shared benchmark host (sam-dev-benchmarks) and takes its global lock. That host is a shared service outside the disposable workspace, so I did not occupy it. The full 1M-edge…
  • scripts/bench-views.sh --edges 10000,100000 --writes 10 --reads 5 --keyed 1,10,100 --recursive-keyed 1,10 --filler-rules 50,200 (real release server, exit 0)
  • scripts/bench-views.sh --baseline-rev bcbd9d3c --edges 10000 --writes 5 --reads 3 --keyed 1,10 --recursive-keyed 1 --filler-rules 50 (compare mode, exit 0)
  • Manual: started target/release/inputlayer-server on a disposable config/data dir (port 18731), drove IQL steps with inputlayer-client and scraped /metrics/prometheus with curl before and after each step
  • cargo test --test e2e_reactive views:: (3 counter e2e tests, incl. dirty-session read counts once)
  • VIEW_BENCH_EDGES=10000,100000 cargo bench --bench view_maintenance_benchmarks -- --warm-up-time 1 --measurement-time 3 --noplot
  • Parsed .github/workflows/full-suite.yml with PyYAML: checked the views-bench job's needs/if relative to the gate and the other jobs

✅ No issues found.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An operator scrapes /metrics/prometheus and sees view_reads, rule_evaluations and view_maintenance_us as counters ✅ pass live counters_transcript.txt: HELP/TYPE counter lines for all three counters, each starting at 0
Each read of a deployed rule (bound, unbound, recursive, or two deployed rules in one read) adds exactly 1 to rule_evaluations; base and ad-hoc reads add 0 ✅ pass live counters_transcript.txt 'reads on a clean session' block
Adversarial F1: a session with a session fact (and a session rule) reads a deployed rule that has results and counts exactly 1, not 2 ✅ pass live counters_transcript.txt dirty-session rows show Δrule_evaluations=1; with the fix reverted, counters_transcript_fix_reverted.txt shows 2
perf-gate views runs the Section 4 matrix (sizes, unbound and keyed subscribers, recursive and non-recursive views, 50/200 filler rules) and reports ack, write->delta p50/p99, read during writes, CPU… ✅ pass live views_local_summary.md, views_local_run.log (exit=0, 0 late in every phase)
The CI entry point scripts/bench-views.sh builds, runs the benchmark with pinned server CPUs and writes summary.md and result.json under target/bench-views/latest ✅ pass live bench_views_script.log (exit 0), bench_views_script_summary.md
The criterion headroom bench (report A.7) runs and reports insert/delete/insert_cold/lookup per size with load time, RSS and row counts ✅ pass live criterion_view_maintenance.txt
The full-suite views-bench job runs on perf_gate_only and on full runs (no !perf_gate_only guard, needs the gate) ⏸️ untested no The prior payload did not establish a live result: the job was never triggered, only its YAML was parsed. It is a GitHub Actions workflow_dispatch definition, and triggering it would start a billed sh…
make bench-views-remote runs under the shared lock and reproduces the Section 4 tables on the benchmark host ⏸️ untested no The prior payload did not establish a live result. make bench-views-remote REV=HEAD reached sam-dev-benchmarks, but the shared lock refused the run (exit 4) because another session's perf-gate run h…
  • Live driver drive_counters.mjs (Node WebSocket client) against a disposable release inputlayer-server (temporary data dir, port 18731): scraped /metrics/prometheus before and after reads of base relations, deployed non-recursive and recursive rules, two deployed rules in one read, ad-hoc queries over base facts, and session-fact and session-rule reads
  • Adversarial F1: rebuilt the server with execute_program_with_rules_uncounted reverted to execute_program_with_rules in src/protocol/handler.rs (restored afterwards), then drove the same reads on port 18732
  • perf-gate views --edges 10000,100000 --writes 10 --reads 3 --keyed 1,10,100 --recursive-keyed 1,10 --filler-rules 50,200 (exit 0, 0 late)
  • scripts/bench-views.sh --edges 10000 --writes 5 --reads 2 --keyed 1,10 --recursive-keyed 1 --filler-rules 50 --server-cpus 8-31 (CI entry point, exit 0)
  • VIEW_BENCH_EDGES=10000,100000 cargo bench --bench view_maintenance_benchmarks -- --warm-up-time 1 --measurement-time 3 --noplot
  • cargo test --release --all-features --test e2e_reactive views:: (3 tests drive the real server binary)
  • make bench-views-remote REV=HEAD (reached sam-dev-benchmarks; refused at the shared lock, exit 4)
  • Parsed .github/workflows/full-suite.yml with PyYAML to check the views-bench job's needs/if and its run step
✅ **Document** - passed

✅ No issues found.

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

✅ No issues found.

@jsam
jsam force-pushed the fm/m9-v1-benches branch from 88b68ab to b87e93b Compare October 5, 2026 19:03
@jsam jsam changed the title feat(perf): views bench and rule-evaluation counters (V1) feat(perf-gate): views benchmark and rule-evaluation counters on /metrics/prometheus Oct 5, 2026
jsam added 4 commits October 5, 2026 19:04
…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 branch from b87e93b to f64b7d0 Compare October 5, 2026 19:28
@jsam jsam changed the title feat(perf-gate): views benchmark and rule-evaluation counters on /metrics/prometheus feat(perf-gate): views benchmark and view work counters on /metrics/prometheus Oct 5, 2026
@jsam

jsam commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #381: the same #308 work, rebased onto #365 on a fresh branch after this PR's pipeline run wedged on a repeated merge conflict.

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