nordisk/nornir
src/bench_mimir.rs
//! Mimir POPULATE + SEARCH bench suite (EPIC — Mimir performance).
//!
//! ~100 registered benchers driving the two north-star numbers for the Mimir
//! knowledge warehouse: **how fast it POPULATES** (ingest / index build / commit)
//! and **how fast it SEARCHES** (the MCP retrieval tools). Every bench:
//!
//! * uses a **deterministic** fixture (seeded synthetic rows, or a real repo's
//! own `src/` for the real-ingest arms) — no network, no model download;
//! * does a **warmup** then `iters` timed iterations;
//! * records **throughput** (`*_per_sec`) + **latency percentiles** (`p50_us` /
//! `p99_us`), and the harness folds in **core-utilisation** telemetry
//! (`cpu_*` / `cores_busy_*`) automatically per row;
//! * is named `nornir.mimir.<populate|search>.<op>_<size>` so the mashup / the
//! dwarves testmatrix-benches view groups them by operation × size.
//!
//! The suite is the driver for the never-ending optimize loop: baseline it, find
//! the slowest populate + slowest search, fix (gatling-only parallelism, better
//! index, fewer allocs), re-bench, prove the delta. See
//! `.nornir/mimir-bench-optimize-loop.md` for the cycle procedure.
//!
//! Registrations live in the `nornir` LIBRARY (like `bench_registry.rs`) so
//! `inventory` links them into every unit — the runner (`examples/nornir-bench.rs`)
//! and the release completeness-gate's bencher-discovery seam.
#![allow(dead_code)]
use anyhow::{Result, ensure};
use crate::bench::BenchResult;
use crate::bench::api::Bencher;
// ─────────────────────────── shared helpers ────────────────────────────────
/// p50 / p99 of a sample of per-iteration latencies (microseconds in, out).
/// Nearest-rank; empty ⇒ (0,0).
fn percentiles_us(mut xs: Vec<f64>) -> (f64, f64) {
if xs.is_empty() {
return (0.0, 0.0);
}
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let at = |q: f64| -> f64 {
let idx = (((xs.len() - 1) as f64) * q).round() as usize;
xs[idx.min(xs.len() - 1)]
};
(at(0.50), at(0.99))
}
/// Fold the standard latency/throughput metric block into a metrics map: the
/// `<verb>_per_sec` throughput, p50/p99 in µs, iteration count and total wall.
fn record(
m: &mut serde_json::Map<String, serde_json::Value>,
throughput_key: &str,
ops: u64,
per_iter_us: Vec<f64>,
total: std::time::Duration,
) {
let (p50, p99) = percentiles_us(per_iter_us.clone());
let per_sec = if total.as_secs_f64() > 0.0 {
ops as f64 / total.as_secs_f64()
} else {
0.0
};
m.insert(throughput_key.into(), serde_json::json!(per_sec));
m.insert("p50_us".into(), serde_json::json!(p50));
m.insert("p99_us".into(), serde_json::json!(p99));
m.insert("iters".into(), serde_json::json!(per_iter_us.len()));
m.insert("ops".into(), serde_json::json!(ops));
m.insert(
"elapsed_ms".into(),
serde_json::json!(total.as_secs_f64() * 1000.0),
);
}
/// Deterministic pseudo-embedding: a smooth, distinct, non-degenerate `dim`-D
/// vector for a seed (no model — we bench the STORE/SEARCH kernels, not the
/// embedder). Same shape the existing `nornir.vector_search` bench uses.
fn mk_vec(seed: u64, dim: usize) -> Vec<f32> {
(0..dim)
.map(|d| {
let x = (seed.wrapping_mul(2_654_435_761).wrapping_add(d as u64)) as f32;
(x * 0.000_1).sin() + 0.001 * (d as f32 + 1.0)
})
.collect()
}
// ── knowledge fixtures ──────────────────────────────────────────────────────
use crate::knowledge::symbols::{CallEdgeRow, SymbolRow};
fn mk_symbol(i: usize) -> SymbolRow {
SymbolRow {
crate_name: "app".into(),
module_path: format!("app::m{}", i % 512),
item_kind: if i % 4 == 0 { "struct" } else { "fn" }.into(),
item_name: format!("f{i}"),
visibility: if i % 3 == 0 { "pub" } else { "pub(crate)" }.into(),
file: format!("src/m{}.rs", i % 512),
line: (i % 4000) as u32 + 1,
doc_lines: (i % 5) as u32,
signature: Some(format!("fn f{i}(x: u32) -> u32")),
}
}
/// A deep call chain `f0→f1→…` plus a shared fan-in callee (`helper`) — real work
/// for both deep `call_path` BFS and wide `callers_of`.
fn mk_call(i: usize, n: usize) -> Vec<CallEdgeRow> {
let mut out = Vec::with_capacity(2);
let caller = format!("app::m{}::f{}", i % 512, i);
if i + 1 < n {
out.push(CallEdgeRow {
crate_name: "app".into(),
caller_path: caller.clone(),
callee_ident: format!("app::m{}::f{}", (i + 1) % 512, i + 1),
call_kind: "call".into(),
file: format!("src/m{}.rs", i % 512),
line: (i % 4000) as u32 + 1,
});
}
out.push(CallEdgeRow {
crate_name: "app".into(),
caller_path: caller,
callee_ident: "util::helper".into(),
call_kind: "call".into(),
file: format!("src/m{}.rs", i % 512),
line: (i % 4000) as u32 + 2,
});
out
}
/// `n_sym` symbols + the call sites of `n_call` chain links — the in-memory
/// knowledge view the context bench joins against.
fn mk_view(n_sym: usize, n_call: usize) -> crate::knowledge::query::KnowledgeView {
let symbols: Vec<SymbolRow> = (0..n_sym).map(mk_symbol).collect();
let mut calls = Vec::with_capacity(n_call * 2);
for i in 0..n_call {
calls.extend(mk_call(i, n_call));
}
crate::knowledge::query::KnowledgeView::new(symbols, calls)
}
/// A `git_heat_facts` scan of `rows` files — the Iceberg commit payload for the
/// commit-throughput and SQL arms (a live table with a live producer; the old
/// `symbol_facts` payload was retired with Mímir v2).
fn mk_heat(repo: &str, rows: usize) -> crate::knowledge::git_heat::GitHeatScan {
let ts = chrono::Utc::now();
crate::knowledge::git_heat::GitHeatScan {
snapshot_id: uuid::Uuid::new_v4(),
ts,
repo: repo.into(),
files: (0..rows)
.map(|i| crate::knowledge::git_heat::GitHeatRow {
file: format!("src/m{}/f{i}.rs", i % 512),
commits_total: (i % 97) as i64 + 1,
commits_30d: (i % 7) as i64,
commits_90d: (i % 23) as i64,
authors_total: (i % 5) as i64 + 1,
last_commit_ts: ts,
})
.collect(),
}
}
// ═══════════════════════════ POPULATE benches ══════════════════════════════
/// POPULATE — `call_edges` index build: time `KnowledgeView::new` over `edges`
/// call rows (the interned CSR / STree adjacency build the query layer stands on).
/// Pure in-memory.
struct PopCallEdgeIndex {
id: &'static str,
edges: usize,
iters: usize,
}
impl Bencher for PopCallEdgeIndex {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::knowledge::query::KnowledgeView;
let mut calls = Vec::with_capacity(self.edges * 2);
for i in 0..self.edges {
calls.extend(mk_call(i, self.edges));
}
let symbols: Vec<SymbolRow> = (0..self.edges).map(mk_symbol).collect();
// Warmup one build (clones inside).
let _ = KnowledgeView::new(symbols.clone(), calls.clone());
let mut per_iter = Vec::with_capacity(self.iters);
let t0 = std::time::Instant::now();
let mut built = 0u64;
for _ in 0..self.iters {
let t = std::time::Instant::now();
let v = KnowledgeView::new(symbols.clone(), calls.clone());
built += v.calls.len() as u64;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert(
"edges".into(),
serde_json::json!(built / self.iters.max(1) as u64),
);
record(&mut m, "edges_per_sec", built, per_iter, total);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// POPULATE — Ragnar/Eytzinger/STree64 index build: time `RagnarLog::build` over
/// `keys` version rows (STree64 exact tree + Eytzinger floor layout). The
/// time-travel index the warehouse commit-log stands on.
struct PopRagnarBuild {
id: &'static str,
keys: usize,
iters: usize,
}
impl Bencher for PopRagnarBuild {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::ragnar::RagnarLog;
let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
let _ = RagnarLog::build(rows.clone()); // warmup
let mut per_iter = Vec::with_capacity(self.iters);
let t0 = std::time::Instant::now();
let mut built = 0u64;
for _ in 0..self.iters {
let t = std::time::Instant::now();
let log = RagnarLog::build(rows.clone());
built += log.len() as u64;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("keys".into(), serde_json::json!(self.keys));
record(&mut m, "keys_per_sec", built, per_iter, total);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// POPULATE — LinearScan baseline build: the naive path `RagnarLog` replaces, so
/// the build-cost delta is on the record too.
struct PopLinearBuild {
id: &'static str,
keys: usize,
iters: usize,
}
impl Bencher for PopLinearBuild {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::ragnar::LinearScan;
let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
let _ = LinearScan::build(rows.clone());
let mut per_iter = Vec::with_capacity(self.iters);
let t0 = std::time::Instant::now();
for _ in 0..self.iters {
let t = std::time::Instant::now();
let _ = LinearScan::build(rows.clone());
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("keys".into(), serde_json::json!(self.keys));
record(
&mut m,
"keys_per_sec",
(self.keys * self.iters) as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// POPULATE — Iceberg commit throughput: append `commits` git-heat scans of
/// `rows_each` rows and report commits/sec (the warehouse write cadence).
struct PopIcebergCommit {
id: &'static str,
commits: usize,
rows_each: usize,
}
impl Bencher for PopIcebergCommit {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::iceberg::IcebergWarehouse;
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
wh.append_git_heat_scan(&mk_heat("warm", 16))?;
let mut per_iter = Vec::with_capacity(self.commits);
let t0 = std::time::Instant::now();
for _ in 0..self.commits {
let scan = mk_heat("repo", self.rows_each);
let t = std::time::Instant::now();
wh.append_git_heat_scan(&scan)?;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("commits".into(), serde_json::json!(self.commits));
m.insert("rows_each".into(), serde_json::json!(self.rows_each));
record(
&mut m,
"commits_per_sec",
self.commits as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// POPULATE — Iceberg commit latency **as a function of table depth**, which is
/// the only shape in which entry P1 of the serial-passage census can be measured
/// at all.
///
/// `iceberg_commit_*` above tops out at 64 commits. The per-commit
/// duplicate-file precheck (`SnapshotProducer::validate_duplicate_files`) loads
/// the manifest list and then `load_manifest()`s **every** entry in it, so its
/// cost is O(live data files) and it is invisible on a shallow table — exactly
/// the trap that produced a false "no win" in mimir-bench cycle 5. This arm
/// therefore **seeds `seed_commits` appends first** (untimed, and always with
/// the check off so the seed itself stays linear), then times `measure_commits`
/// further appends with the check forced to `check_dup`.
///
/// The two `check_dup` variants at the same `seed_commits` are the A/B. The
/// `deep_0_*` pair is the control: at depth 0 the check has nothing to scan, so
/// the pair must come out level — if it does not, the arm is measuring noise.
///
/// `data_files` in the output is the real live parquet count under the
/// warehouse, so the "≥1000 data files" precondition is reported, not assumed.
struct PopIcebergCommitDeep {
id: &'static str,
seed_commits: usize,
measure_commits: usize,
check_dup: bool,
}
impl Bencher for PopIcebergCommitDeep {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::iceberg::{FORCE_CHECK_DUPLICATE, IcebergWarehouse};
use std::sync::atomic::Ordering;
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
// ── Seed (UNTIMED). Always check-off: the seed only has to produce the
// table *shape* (N snapshots ⇒ N manifests ⇒ N data files),
// and seeding with the check on is O(N²) — 1k commits would take the
// better part of an hour and would not change the shape one bit. ──
FORCE_CHECK_DUPLICATE.store(1, Ordering::Relaxed);
wh.append_git_heat_scan(&mk_heat("warm", 16))?;
for _ in 0..self.seed_commits {
wh.append_git_heat_scan(&mk_heat("seed", 64))?;
}
// Count the live data files the measured commits will be scanned against.
let data_files = count_parquet_files(dir.path());
// ── Measure. ──
FORCE_CHECK_DUPLICATE.store(if self.check_dup { 2 } else { 1 }, Ordering::Relaxed);
let mut per_iter = Vec::with_capacity(self.measure_commits);
let t0 = std::time::Instant::now();
for _ in 0..self.measure_commits {
let scan = mk_heat("repo", 200);
let t = std::time::Instant::now();
wh.append_git_heat_scan(&scan)?;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
FORCE_CHECK_DUPLICATE.store(0, Ordering::Relaxed);
let mut m = serde_json::Map::new();
m.insert("seed_commits".into(), serde_json::json!(self.seed_commits));
m.insert(
"measure_commits".into(),
serde_json::json!(self.measure_commits),
);
m.insert("check_duplicate".into(), serde_json::json!(self.check_dup));
m.insert("data_files".into(), serde_json::json!(data_files));
record(
&mut m,
"commits_per_sec",
self.measure_commits as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// Live `.parquet` files anywhere under `root` — the depth figure the P1 bench
/// has to report for its numbers to mean anything.
fn count_parquet_files(root: &std::path::Path) -> usize {
fn walk(p: &std::path::Path, n: &mut usize) {
let Ok(rd) = std::fs::read_dir(p) else { return };
for e in rd.flatten() {
let path = e.path();
if path.is_dir() {
walk(&path, n);
} else if path.extension().is_some_and(|x| x == "parquet") {
*n += 1;
}
}
}
let mut n = 0;
walk(root, &mut n);
n
}
// ── vector / embedding populate (feature-gated) ─────────────────────────────
#[cfg(feature = "vector")]
mod vec_benches {
use super::*;
use crate::vector::chunk::{Chunk, ChunkOptions, hash_text};
use crate::vector::store::{
IndexParams, ModelProfile, chunker_hash, index_snapshot, search, search_batch,
};
use crate::warehouse::iceberg::IcebergWarehouse;
pub(super) fn profile(dim: usize) -> ModelProfile {
ModelProfile {
model_name: "bench-embed".into(),
weights_sha: "w0".into(),
tokenizer_sha: "t0".into(),
pooling: "mean".into(),
normalize: true,
dim,
dtype: "f32".into(),
}
}
/// `n` deterministic, distinct chunks (unique content_hash each).
pub(super) fn mk_chunks(n: usize) -> Vec<Chunk> {
(0..n)
.map(|i| {
let text = format!("chunk_{i}_token_{}", i % 997);
Chunk {
file: format!("src/f{}.rs", i % 512),
start_line: (i % 4000) + 1,
end_line: (i % 4000) + 12,
content_hash: hash_text(&text),
text,
}
})
.collect()
}
/// Build an embedding snapshot (embeddings + manifest) in `wh` for `repo`.
pub(super) fn seed_snapshot(
wh: &IcebergWarehouse,
repo: &str,
chunks: &[Chunk],
dim: usize,
) -> Result<()> {
let m = profile(dim);
let ch = chunker_hash(&ChunkOptions::default());
let params = IndexParams {
workspace: "ws",
repo,
git_sha: "sha1",
branch: "main",
model: &m,
chunker_hash: &ch,
complete: true,
};
index_snapshot(wh, ¶ms, chunks, |missing| {
Ok(missing
.iter()
.enumerate()
.map(|(i, _)| mk_vec(i as u64, dim))
.collect())
})?;
Ok(())
}
/// POPULATE — embeddings + `embedding_manifest` write: chunk `chunks` items and
/// time `index_snapshot` (dedup + embed-missing + Parquet append of embeddings,
/// manifest and snapshot rows). Cheap deterministic embedder isolates the
/// STORE cost.
pub(super) struct PopEmbeddingIndex {
pub id: &'static str,
pub chunks: usize,
pub dim: usize,
}
impl Bencher for PopEmbeddingIndex {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
let chunks = mk_chunks(self.chunks);
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
let t = std::time::Instant::now();
seed_snapshot(&wh, "repo", &chunks, self.dim)?;
let el = t.elapsed();
let mut m = serde_json::Map::new();
m.insert("chunks".into(), serde_json::json!(self.chunks));
m.insert("dim".into(), serde_json::json!(self.dim));
record(
&mut m,
"chunks_per_sec",
self.chunks as u64,
vec![el.as_secs_f64() * 1e6],
el,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// POPULATE — `VectorIndex` (znippy_zoomies vann) build: `new` + `add` of
/// `rows` × `dim` vectors (the SIMD int8/f32 flat index construction).
pub(super) struct PopVannBuild {
pub id: &'static str,
pub rows: usize,
pub dim: usize,
}
impl Bencher for PopVannBuild {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::vector::VectorIndex;
let mut flat = Vec::with_capacity(self.rows * self.dim);
let mut ids = Vec::with_capacity(self.rows);
for r in 0..self.rows as u64 {
flat.extend(mk_vec(r, self.dim));
ids.push(r);
}
let t = std::time::Instant::now();
let mut idx = VectorIndex::new(self.dim)?;
idx.add(&flat, &ids)?;
let el = t.elapsed();
let mut m = serde_json::Map::new();
m.insert("rows".into(), serde_json::json!(self.rows));
m.insert("dim".into(), serde_json::json!(self.dim));
record(
&mut m,
"rows_per_sec",
self.rows as u64,
vec![el.as_secs_f64() * 1e6],
el,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ═══════════════════════ SEARCH: vector kNN ════════════════════════════
/// SEARCH — dense cosine kNN (the `vector_search` MCP tool path): warm a
/// `rows`×`dim` snapshot, then time `queries` single-query top-`k` searches
/// through `store::search` (reconstruct-cached + SIMD int8/f32 scan).
pub(super) struct SearchVectorKnnSingle {
pub id: &'static str,
pub rows: usize,
pub dim: usize,
pub queries: usize,
pub k: usize,
}
impl Bencher for SearchVectorKnnSingle {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
let repo = self.id; // unique per bench
let chunks = mk_chunks(self.rows);
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
seed_snapshot(&wh, repo, &chunks, self.dim)?;
let mp = profile(self.dim).id();
// Warm the reconstruct cache.
let _ = search(&wh, repo, Some("sha1"), &mp, &mk_vec(1, self.dim), self.k)?;
let mut per_iter = Vec::with_capacity(self.queries);
let mut hits = 0u64;
let t0 = std::time::Instant::now();
for q in 0..self.queries as u64 {
let qv = mk_vec(q.wrapping_mul(31).wrapping_add(7), self.dim);
let t = std::time::Instant::now();
let r = search(&wh, repo, Some("sha1"), &mp, &qv, self.k)?;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
hits += r.len() as u64;
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("rows".into(), serde_json::json!(self.rows));
m.insert("dim".into(), serde_json::json!(self.dim));
m.insert("k".into(), serde_json::json!(self.k));
m.insert("hits".into(), serde_json::json!(hits));
record(
&mut m,
"queries_per_sec",
self.queries as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// SEARCH — batched dense kNN: `store::search_batch` amortises the
/// reconstructed-matrix read across `batch` queries (bulk re-rank / dedup).
pub(super) struct SearchVectorKnnBatch {
pub id: &'static str,
pub rows: usize,
pub dim: usize,
pub batch: usize,
pub k: usize,
}
impl Bencher for SearchVectorKnnBatch {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
let repo = self.id;
let chunks = mk_chunks(self.rows);
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
seed_snapshot(&wh, repo, &chunks, self.dim)?;
let mp = profile(self.dim).id();
let queries: Vec<Vec<f32>> = (0..self.batch as u64)
.map(|q| mk_vec(q.wrapping_mul(31).wrapping_add(7), self.dim))
.collect();
let _ = search_batch(&wh, repo, Some("sha1"), &mp, &queries, self.k)?; // warm
let iters = 8usize;
let mut per_iter = Vec::with_capacity(iters);
let mut hits = 0u64;
let t0 = std::time::Instant::now();
for _ in 0..iters {
let t = std::time::Instant::now();
let r = search_batch(&wh, repo, Some("sha1"), &mp, &queries, self.k)?;
per_iter.push(t.elapsed().as_secs_f64() * 1e6 / self.batch as f64);
hits += r.iter().map(|v| v.len() as u64).sum::<u64>();
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("rows".into(), serde_json::json!(self.rows));
m.insert("dim".into(), serde_json::json!(self.dim));
m.insert("batch".into(), serde_json::json!(self.batch));
m.insert("hits".into(), serde_json::json!(hits));
record(
&mut m,
"queries_per_sec",
(self.batch * iters) as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// Which corpus geometry an HNSW recall arm runs against. **This choice, not
/// `ef_search`, is what dominates the recall number** — see
/// [`znippy_zoomies::vann::clustered_row`] and the `hnsw` module docs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum HnswCorpus {
/// Clustered, like a real embedding corpus: mean pairwise cosine within a
/// query's true top-10 ≈ 0.66, matching the measured 0.658 of nornir's own
/// `jina-v2-base-code` warehouse (39,559 × 768). **This is the regime the
/// live ANN path in `vector::store` actually serves**, so it is the one a
/// recall floor is meaningful in.
Realistic,
/// i.i.d. uniform random — an ADVERSARIAL worst case with intrinsic
/// dimensionality = `dim`. A query's true top-10 are mutually
/// near-orthogonal (measured mean pairwise cosine 0.082; only 2.6% of them
/// appear in each other's exact 32-NN lists), so no bounded-degree
/// proximity graph can walk between them and recall has to be bought with
/// `ef_search` — which is why this arm pins an explicit, costed `ef`.
/// Kept on purpose as a stress case; NOT representative of production.
UniformAdversarial,
}
/// Well-spread deterministic pseudo-random vector (NOT `mk_vec`'s smooth
/// near-tie ramp) so **recall@k** is a meaningful metric — a near-duplicate
/// corpus makes the true top-k ill-defined and spuriously tanks recall.
pub(super) fn rand_vec(seed: u64, dim: usize) -> Vec<f32> {
let mut s = seed
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add(0xABCD_1234);
(0..dim)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
((s >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0) as f32
})
.collect()
}
/// SEARCH — **HNSW ANN vs the brute-force flat oracle** (Mimir vector cycle).
/// Builds a `rows`×`dim` flat [`VectorIndex`] (the exact oracle) and a vendored
/// HNSW over the SAME rows, then measures, over `queries`:
/// * HNSW single-query latency (the O(log n) win), plus the flat baseline
/// latency on the same corpus for the speedup;
/// * **recall@`k`** of the HNSW top-k against the flat oracle's top-k.
/// Carries a **red-when-broken** floor: if recall drops below `recall_floor`
/// the bench turns red (break the graph → red).
///
/// ⚠ **A recall floor is only meaningful together with the corpus it is
/// measured on.** These arms used to run on i.i.d. uniform vectors and read
/// 0.7320 @ 10k / 0.2656 @ 100k against a 0.90 floor — which was NOT an index
/// defect but the curse of dimensionality (uniform vectors have intrinsic
/// dimensionality `dim`; the graph itself measured within 6 points of the
/// best *possible* degree-32 proximity graph on that data, and converged to
/// exactly 1.0000 recall as `ef_search` grew, proving the links are sound).
/// The live path serves clustered code embeddings, where the same index+params
/// measure **0.998** recall@10. So the primary arms now carry
/// [`HnswCorpus::Realistic`], and the uniform case is kept as an explicitly
/// labelled adversarial arm with a *costed* `ef_search`. Every arm records
/// `corpus`, `ef_search` and `top10_cohesion` so the regime is legible from
/// the JSON alone and nobody re-litigates this from the recall number.
pub(super) struct SearchHnswKnn {
pub id: &'static str,
pub rows: usize,
pub dim: usize,
pub queries: usize,
pub k: usize,
pub recall_floor: f64,
/// Corpus geometry — see [`HnswCorpus`].
pub corpus: HnswCorpus,
/// Query-time candidate width. `0` = the shipped [`HnswParams`] default,
/// which is what the live path uses; a non-zero value is an explicit,
/// on-the-record purchase of recall with latency.
pub ef_search: usize,
}
impl Bencher for SearchHnswKnn {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::vector::{
HnswParams, REALISTIC_SPREAD, VectorIndex, clustered_row, normalized,
};
let (rows, dim, k) = (self.rows, self.dim, self.k);
// ~50 rows per cluster, so a query's true top-k sits inside one — the
// shape a real embedding corpus has.
let clusters = ((rows / 50) as u64).max(4);
// L2-normalized on the way out: `VectorIndex::add` would normalize
// anyway, and keeping the local copy normalized too is what makes the
// `top10_cohesion` dot product below an actual cosine.
let row_at = |r: u64| match self.corpus {
HnswCorpus::Realistic => {
clustered_row(0xC0DE_1234, r, dim, clusters, REALISTIC_SPREAD)
}
HnswCorpus::UniformAdversarial => normalized(&rand_vec(r, dim)),
};
// Flat oracle over the same rows the HNSW will index.
let mut flat = VectorIndex::new(dim)?;
let mut flatv = Vec::with_capacity(rows * dim);
let mut ids = Vec::with_capacity(rows);
for r in 0..rows as u64 {
flatv.extend(row_at(r));
ids.push(r);
}
flat.add(&flatv, &ids)?;
// HNSW build (single-threaded; timed so the build cost is on record).
let tb = std::time::Instant::now();
let hnsw = flat.build_hnsw(HnswParams::default());
let build_ms = tb.elapsed().as_secs_f64() * 1000.0;
// `ef_search` is a QUERY-time knob (`search_with_ef`) — no rebuild.
let ef = if self.ef_search == 0 {
hnsw.ef_search()
} else {
self.ef_search
};
// Held-out queries: drawn from the same generator beyond the corpus
// range, so they are in-distribution but never index members (no free
// self-match inflating recall).
let qs: Vec<Vec<f32>> = (0..self.queries as u64)
.map(|q| row_at(9_000_000 + q))
.collect();
// Warm both paths.
let _ = hnsw.search_with_ef(&qs[0], k, ef);
let _ = flat.search(&qs[0], k);
// recall@k vs the flat oracle, plus the corpus-geometry diagnostic
// that explains the recall number: how tight the true top-k are to
// EACH OTHER. High (~0.66) = clustered/realistic and walkable by a
// proximity graph; ~0 = uniform noise, where no graph can help.
let mut hit = 0usize;
let mut tot = 0usize;
let mut cohesion = 0.0f64;
let mut cohesion_pairs = 0usize;
for q in &qs {
let exact = flat.search(q, k);
let oracle: std::collections::HashSet<u64> =
exact.iter().map(|(id, _)| *id).collect();
for (id, _) in hnsw.search_with_ef(q, k, ef) {
if oracle.contains(&id) {
hit += 1;
}
}
tot += oracle.len();
for a in 0..exact.len() {
for b in (a + 1)..exact.len() {
let (ia, ib) = (exact[a].0 as usize, exact[b].0 as usize);
let (va, vb) = (
&flatv[ia * dim..(ia + 1) * dim],
&flatv[ib * dim..(ib + 1) * dim],
);
cohesion += va.iter().zip(vb).map(|(x, y)| (x * y) as f64).sum::<f64>();
cohesion_pairs += 1;
}
}
}
let recall = hit as f64 / tot.max(1) as f64;
let cohesion = cohesion / cohesion_pairs.max(1) as f64;
// HNSW query latency.
let mut per_iter = Vec::with_capacity(self.queries);
let t0 = std::time::Instant::now();
for q in &qs {
let t = std::time::Instant::now();
let _ = std::hint::black_box(hnsw.search_with_ef(q, k, ef));
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
// Flat baseline latency on the same corpus (for the speedup number).
let mut flat_us = Vec::with_capacity(self.queries);
for q in &qs {
let t = std::time::Instant::now();
let _ = std::hint::black_box(flat.search(q, k));
flat_us.push(t.elapsed().as_secs_f64() * 1e6);
}
let (flat_p50, _) = percentiles_us(flat_us);
let (hnsw_p50, _) = percentiles_us(per_iter.clone());
// RED-WHEN-BROKEN: the ANN index must preserve recall vs the exact
// oracle. Break the graph/search and this bench turns red.
anyhow::ensure!(
recall + 1e-9 >= self.recall_floor,
"HNSW recall@{k} {recall:.4} fell below floor {:.4} (rows={rows}, dim={dim}, \
corpus={:?}, ef_search={ef}, top10_cohesion={cohesion:.3}). \
Before touching ef_search: a correct graph converges to recall 1.0 as \
ef_search grows on ANY corpus — check that invariant first \
(znippy_zoomies::vann::hnsw tests), because a low recall at a LOW cohesion \
is the corpus, not the index.",
self.recall_floor,
self.corpus,
);
let mut m = serde_json::Map::new();
m.insert("rows".into(), serde_json::json!(rows));
m.insert("dim".into(), serde_json::json!(dim));
m.insert("k".into(), serde_json::json!(k));
m.insert("recall_at_k".into(), serde_json::json!(recall));
m.insert("recall_floor".into(), serde_json::json!(self.recall_floor));
m.insert(
"corpus".into(),
serde_json::json!(match self.corpus {
HnswCorpus::Realistic => "realistic_clustered",
HnswCorpus::UniformAdversarial => "uniform_adversarial",
}),
);
m.insert("ef_search".into(), serde_json::json!(ef));
m.insert("top10_cohesion".into(), serde_json::json!(cohesion));
m.insert("build_ms".into(), serde_json::json!(build_ms));
m.insert("flat_p50_us".into(), serde_json::json!(flat_p50));
m.insert(
"speedup_x".into(),
serde_json::json!(if hnsw_p50 > 0.0 {
flat_p50 / hnsw_p50
} else {
0.0
}),
);
record(
&mut m,
"queries_per_sec",
self.queries as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ═══════════════════════ SEARCH: Graph-RAG context-slicing ═════════════
/// SEARCH — Graph-RAG context-slicing (`knowledge_context` MCP tool):
/// `context_slices` joins embeddings ⋈ manifest ⋈ symbol_facts (optionally
/// trimmed by a `call_edges` change-radius) into ranked prompt-injectable
/// slices. The headline retrieval latency.
pub(super) struct SearchContextSlice {
pub id: &'static str,
pub chunks: usize,
pub k: usize,
pub radius: bool,
}
impl Bencher for SearchContextSlice {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::knowledge::context::{ChangeRadius, ContextQuery, context_slices};
let repo = self.id;
let dim = 32usize;
let chunks = mk_chunks(self.chunks);
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
seed_snapshot(&wh, repo, &chunks, dim)?;
// A matching symbol view so the manifest span joins to symbols (the
// view is an input since Mímir v2 — no `symbol_facts` read).
let n_sym = self.chunks.min(20_000);
let view = mk_view(n_sym, n_sym);
let mp = profile(dim).id();
let radius = if self.radius {
Some(ChangeRadius {
seeds: vec!["f0".into(), "f1".into()],
hops: 3,
include_callers: true,
include_callees: true,
})
} else {
None
};
let mk_q = |_qi: u64| ContextQuery {
repo,
git_sha: Some("sha1"),
model_profile: &mp,
k: self.k,
radius: radius.clone(),
};
let _ = context_slices(&wh, &view, &mk_q(1), &mk_vec(1, dim))?; // warm
let queries = 64usize;
let mut per_iter = Vec::with_capacity(queries);
let mut slices = 0u64;
let t0 = std::time::Instant::now();
for q in 0..queries as u64 {
let qv = mk_vec(q.wrapping_mul(31).wrapping_add(7), dim);
let t = std::time::Instant::now();
let r = context_slices(&wh, &view, &mk_q(q), &qv)?;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
slices += r.len() as u64;
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("chunks".into(), serde_json::json!(self.chunks));
m.insert("k".into(), serde_json::json!(self.k));
m.insert("radius".into(), serde_json::json!(self.radius));
m.insert("slices".into(), serde_json::json!(slices));
record(&mut m, "queries_per_sec", queries as u64, per_iter, total);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ═══════════════════════ SEARCH: hybrid RRF (optimize cycle 1) ══════════
/// SEARCH — **RRF hybrid vs dense-only** (optimize cycle 1 proof). Builds a
/// REAL Tantivy BM25 index AND a REAL dense embedding snapshot over the SAME
/// planted corpus, then measures recall@k for BM25-only, dense-only, and the
/// Reciprocal-Rank-Fusion of the two — plus the fused query latency. Half the
/// relevant docs are semantic-only (paraphrase; dense finds, BM25 misses) and
/// half are lexical-only (rare token in an off-topic doc; BM25 finds, dense
/// misses), so the fusion must beat EITHER engine alone — the before/after
/// number the loop drives.
pub(super) struct SearchHybridRrf {
pub id: &'static str,
pub docs: usize,
}
impl Bencher for SearchHybridRrf {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::index::{Corpus, Index};
use crate::knowledge::fusion::{DEFAULT_RRF_K, fuse_bm25_dense, recall_at_k};
use crate::vector::store::search;
use std::path::PathBuf;
let repo = self.id;
let dim = 16usize;
// The rare lexical token only the "keyword" relevant docs contain.
let kw = "flux_capacitor_z9";
// topic axis 0 = the query's semantic topic.
let n = self.docs;
// Relevant docs: indices [0..R). semantic-only: even; lexical-only: odd.
let r_count = 8usize.min(n / 2);
let mut relevant: Vec<String> = Vec::new();
let corpus_dir = tempfile::tempdir()?;
let ws = corpus_dir.path();
// Build the planted corpus files + chunks in lockstep so BM25 doc
// path == chunk.file (fusion key namespace).
use crate::vector::chunk::{Chunk, hash_text};
let mut chunks: Vec<Chunk> = Vec::with_capacity(n);
let mut paths: Vec<PathBuf> = Vec::with_capacity(n);
for i in 0..n {
let fname = format!("doc_{i:05}.rs");
let relevant_i = i < r_count;
let semantic_only = relevant_i && i % 2 == 0;
let lexical_only = relevant_i && i % 2 == 1;
// Topic: relevant semantic docs and the query share topic 0; lexical
// docs sit in an off topic; noise spreads across topics.
let topic = if semantic_only {
0u64
} else if lexical_only {
100 + i as u64 // far from topic 0 in embedding space
} else {
1 + (i as u64 % 64)
};
// Body: paraphrase text for semantic docs; the rare token for
// lexical docs (and the two keyword-bearing relevant docs).
let mut body = format!("module doc {i}\nfn work() {{ /* topic {topic} */ }}\n");
if lexical_only {
body.push_str(&format!("// {kw} appears here in an off-topic file\n"));
}
if semantic_only {
body.push_str("// warp field manifold energy resonance coupling\n");
}
std::fs::write(ws.join(&fname), &body)?;
paths.push(ws.join(&fname));
chunks.push(Chunk {
file: fname.clone(),
start_line: 1,
end_line: 3,
content_hash: hash_text(&format!("{i}-{topic}-{}", body.len())),
text: body,
});
if relevant_i {
relevant.push(fname);
}
}
// Dense snapshot: embed each chunk near its topic axis.
{
use crate::vector::chunk::ChunkOptions;
use crate::vector::store::{IndexParams, chunker_hash, index_snapshot};
let m = profile(dim);
let ch = chunker_hash(&ChunkOptions::default());
let params = IndexParams {
workspace: "ws",
repo,
git_sha: "sha1",
branch: "main",
model: &m,
chunker_hash: &ch,
complete: true,
};
// Deterministic topic embedding: unit-ish vector on `topic % dim`.
let by_hash: std::collections::HashMap<String, u64> = chunks
.iter()
.enumerate()
.map(|(i, c)| {
let semantic_only = i < r_count && i % 2 == 0;
let lexical_only = i < r_count && i % 2 == 1;
let topic = if semantic_only {
0
} else if lexical_only {
100 + i as u64
} else {
1 + (i as u64 % 64)
};
(c.content_hash.clone(), topic)
})
.collect();
let dim_c = dim;
index_snapshot(&wh_for(&corpus_dir)?, ¶ms, &chunks, |missing| {
Ok(missing
.iter()
.map(|c| {
let topic = *by_hash.get(&c.content_hash).unwrap_or(&999);
let mut v = vec![0.001f32; dim_c];
v[(topic as usize) % dim_c] += 1.0;
v
})
.collect())
})?;
}
// Re-open the same warehouse for search (index_snapshot used a fresh handle).
let wh = wh_for(&corpus_dir)?;
let mp = profile(dim).id();
// BM25 index over the same files.
let idx_dir = tempfile::tempdir()?;
let index = Index::open_at(ws, idx_dir.path())?;
index.index_files(repo, Corpus::Code, &paths)?;
let k = r_count.max(4);
// Dense query = topic-0 axis.
let mut qv = vec![0.001f32; dim];
qv[0] += 1.0;
let dense_hits = search(&wh, repo, Some("sha1"), &mp, &qv, n.min(64))?;
let dense_keys: Vec<String> = dense_hits.iter().map(|(_, o)| o.file.clone()).collect();
// BM25 query = the rare token.
let bm25_hits = index.search(kw, Some(Corpus::Code), Some(repo), n.min(64))?;
let bm25_keys: Vec<String> = bm25_hits.iter().map(|h| h.path.clone()).collect();
// Fuse (timed).
let iters = 200usize;
let mut per_iter = Vec::with_capacity(iters);
let t0 = std::time::Instant::now();
let mut fused = Vec::new();
for _ in 0..iters {
let t = std::time::Instant::now();
fused = fuse_bm25_dense(&bm25_keys, &dense_keys, 1.0, 1.0, DEFAULT_RRF_K, k);
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
let r_bm25 = recall_at_k(&bm25_keys, &relevant, k);
let r_dense = recall_at_k(&dense_keys, &relevant, k);
let r_fused = recall_at_k(&fused, &relevant, k);
// RED-WHEN-BROKEN (runs in release, recorded as a bench TestOutcome):
// the whole point of the fusion is that it must not LOSE recall vs
// either engine, and on this planted corpus (half semantic-only, half
// lexical-only relevant docs) it must STRICTLY beat both. Break
// `reciprocal_rank_fusion` / `fuse_bm25_dense` and this bench turns red.
anyhow::ensure!(
r_fused + 1e-6 >= r_bm25 && r_fused + 1e-6 >= r_dense,
"RRF hybrid recall {r_fused} regressed below bm25 {r_bm25} / dense {r_dense}"
);
anyhow::ensure!(
r_fused > r_dense + 1e-6,
"RRF hybrid must beat the dense-only MCP path (fused {r_fused} vs dense {r_dense})"
);
let mut m = serde_json::Map::new();
m.insert("docs".into(), serde_json::json!(n));
m.insert("relevant".into(), serde_json::json!(relevant.len()));
m.insert("recall_bm25_pct".into(), serde_json::json!(r_bm25 * 100.0));
m.insert(
"recall_dense_pct".into(),
serde_json::json!(r_dense * 100.0),
);
m.insert(
"recall_hybrid_pct".into(),
serde_json::json!(r_fused * 100.0),
);
// The optimize-loop headline: recall points GAINED over the dense-only
// MCP path that ships today.
m.insert(
"recall_gain_pct".into(),
serde_json::json!((r_fused - r_dense) * 100.0),
);
record(&mut m, "fuse_per_sec", iters as u64, per_iter, total);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// Re-open the warehouse living under a corpus tempdir (search + populate use
/// the same on-disk tree, distinct handles).
fn wh_for(dir: &tempfile::TempDir) -> Result<IcebergWarehouse> {
IcebergWarehouse::open(&dir.path().join(".wh"))
}
// ═══════════════════════ SEARCH: real-project ingest ═══════════════════
/// POPULATE (real) — ingest nornir's OWN `src/` tree: chunk every `*.rs` and
/// time `index_snapshot`. The north-star "populate a real codebase" number
/// (runs from the repo root under `cargo run --example`; degrades to 0 files
/// if `src/` is absent).
pub(super) struct RealEmbedPopulate {
pub id: &'static str,
}
impl Bencher for RealEmbedPopulate {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::vector::chunk::{ChunkOptions, chunk_file};
use crate::vector::store::collect_rust_sources;
let srcs = collect_rust_sources(std::path::Path::new("src"));
let opts = ChunkOptions::default();
let mut chunks = Vec::new();
for (path, text) in &srcs {
chunks.extend(chunk_file(path, text, &opts));
}
let dim = 32usize;
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
let t = std::time::Instant::now();
if !chunks.is_empty() {
seed_snapshot(&wh, "nornir", &chunks, dim)?;
}
let el = t.elapsed();
let mut m = serde_json::Map::new();
m.insert("files".into(), serde_json::json!(srcs.len()));
m.insert("chunks".into(), serde_json::json!(chunks.len()));
record(
&mut m,
"chunks_per_sec",
chunks.len() as u64,
vec![el.as_secs_f64() * 1e6],
el,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// SEARCH (real) — dense kNN over nornir's own `src/` snapshot: warm the index,
/// time top-10 queries. The north-star "search a real codebase" number.
pub(super) struct RealVectorSearch {
pub id: &'static str,
}
impl Bencher for RealVectorSearch {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::vector::chunk::{ChunkOptions, chunk_file};
use crate::vector::store::collect_rust_sources;
let srcs = collect_rust_sources(std::path::Path::new("src"));
let opts = ChunkOptions::default();
let mut chunks = Vec::new();
for (path, text) in &srcs {
chunks.extend(chunk_file(path, text, &opts));
}
let dim = 32usize;
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
let mut m = serde_json::Map::new();
m.insert("files".into(), serde_json::json!(srcs.len()));
m.insert("chunks".into(), serde_json::json!(chunks.len()));
if chunks.is_empty() {
record(
&mut m,
"queries_per_sec",
0,
vec![],
std::time::Duration::ZERO,
);
return Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
});
}
seed_snapshot(&wh, "nornir", &chunks, dim)?;
let mp = profile(dim).id();
let _ = search(&wh, "nornir", Some("sha1"), &mp, &mk_vec(1, dim), 10)?;
let queries = 128usize;
let mut per_iter = Vec::with_capacity(queries);
let t0 = std::time::Instant::now();
for q in 0..queries as u64 {
let qv = mk_vec(q.wrapping_mul(31).wrapping_add(7), dim);
let t = std::time::Instant::now();
let _ = search(&wh, "nornir", Some("sha1"), &mp, &qv, 10)?;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
record(&mut m, "queries_per_sec", queries as u64, per_iter, total);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
use crate::vector::store::{Embedder, RepoInput, RepoRef, index_repo, index_repos_batched};
/// A model-free stand-in embedder for the batched-embed arm: a deterministic,
/// non-degenerate, L2-normalized `dim`-vector per text. It measures the STEP
/// STRUCTURE (flattened chunking + one existing-hash read + one embed call +
/// serial appends) NOT a GPU — the real 4090 batching win rides on top of this
/// and cannot be benched without weights, so we bench the part that is
/// deterministic and machine-independent.
struct BenchEmbedder {
dim: usize,
}
impl Embedder for BenchEmbedder {
fn profile(&self) -> ModelProfile {
profile(self.dim)
}
fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>> {
Ok(texts
.iter()
.map(|t| {
let seed = hash_text(t);
let bytes = seed.as_bytes();
let mut v = vec![0f32; self.dim];
for (k, slot) in v.iter_mut().enumerate() {
let b = bytes[k % bytes.len()] as f32;
*slot = (b * 0.01 + k as f32 * 0.001).sin() + 0.001 * (k as f32 + 1.0);
}
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-9);
for x in &mut v {
*x /= norm;
}
v
})
.collect())
}
}
/// Synthetic per-member Rust sources for the embed arm: `files` files of unique
/// content per member so every member's chunks are distinct (real embed work).
fn embed_member_files(member: usize, files: usize) -> Vec<(String, String)> {
(0..files)
.map(|f| {
let mut body = String::new();
for i in 0..24 {
body.push_str(&format!(
"/// doc m{member} f{f} i{i}\npub fn m{member}_f{f}_fn{i}(x: u32) -> u32 {{ x.wrapping_mul({}).wrapping_add({member}) }}\n",
i * 7 + 1
));
}
(format!("src/m{member}_f{f}.rs"), body)
})
.collect()
}
/// Populate step-3b EMBED: `members` repos embedded one-at-a-time via the old
/// per-member `index_repo` loop vs the flattened, GPU-batched
/// [`index_repos_batched`] the fix uses. Times both into separate fresh
/// warehouses, asserts identical occurrence + new-vector totals, records the
/// `speedup`. The win here is the flattened union chunking + ONE existing-hash
/// read + ONE embed call replacing N of each.
pub(super) struct PopEmbedMembersBatched {
pub id: &'static str,
pub members: usize,
pub files: usize,
pub dim: usize,
}
impl Bencher for PopEmbedMembersBatched {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
let opts = ChunkOptions::default();
let emb = BenchEmbedder { dim: self.dim };
let per_member: Vec<Vec<(String, String)>> = (0..self.members)
.map(|m| embed_member_files(m, self.files))
.collect();
// SEQUENTIAL — old one-repo-at-a-time `index_repo` loop, fresh warehouse.
let dir_seq = tempfile::tempdir()?;
let wh_seq = IcebergWarehouse::open(dir_seq.path())?;
let t = std::time::Instant::now();
let mut seq_occ = 0usize;
let mut seq_new = 0usize;
for (m, files) in per_member.iter().enumerate() {
let repo_name = format!("repo{m}");
let r = RepoRef {
workspace: "ws",
repo: &repo_name,
git_sha: "sha1",
branch: "main",
complete: true,
};
let snap = index_repo(&wh_seq, &r, files, &opts, &emb)?;
seq_occ += snap.occurrences;
seq_new += snap.new_vectors;
}
let seq_ms = t.elapsed().as_secs_f64() * 1000.0;
// BATCHED — flattened chunk pool + one embed call, fresh warehouse.
let dir_b = tempfile::tempdir()?;
let wh_b = IcebergWarehouse::open(dir_b.path())?;
let inputs: Vec<RepoInput> = per_member
.iter()
.enumerate()
.map(|(m, files)| RepoInput {
workspace: "ws".into(),
repo: format!("repo{m}"),
git_sha: "sha1".into(),
branch: "main".into(),
complete: true,
files: files.clone(),
})
.collect();
let t = std::time::Instant::now();
let results = index_repos_batched(&wh_b, &inputs, &opts, &emb);
let bat_ms = t.elapsed().as_secs_f64() * 1000.0;
let mut bat_occ = 0usize;
let mut bat_new = 0usize;
for res in results {
let snap = res?;
bat_occ += snap.occurrences;
bat_new += snap.new_vectors;
}
// CORRECTNESS: the batched path indexes the SAME occurrences and embeds
// the SAME number of new vectors as the serial loop.
anyhow::ensure!(
bat_occ == seq_occ,
"batched embed indexed {bat_occ} occurrences, serial {seq_occ} — not identical"
);
anyhow::ensure!(
bat_new == seq_new,
"batched embed produced {bat_new} new vectors, serial {seq_new} — dedup diverged"
);
let speedup = if bat_ms > 0.0 { seq_ms / bat_ms } else { 0.0 };
let mut m = serde_json::Map::new();
m.insert("members".into(), serde_json::json!(self.members));
m.insert("occurrences".into(), serde_json::json!(bat_occ));
m.insert("new_vectors".into(), serde_json::json!(bat_new));
m.insert("seq_ms".into(), serde_json::json!(seq_ms));
m.insert("batched_ms".into(), serde_json::json!(bat_ms));
m.insert("speedup".into(), serde_json::json!(speedup));
m.insert(
"cores".into(),
serde_json::json!(
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
),
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
}
// ═══════════════════════ SEARCH: call-graph (pure) ═════════════════════════
/// SEARCH — call-graph queries (`callers_of` / `call_path` MCP tools): warm a
/// `nodes`-deep chain + fan-in view, then time a battery of deep-BFS + fan-in
/// lookups.
struct SearchCallGraph {
id: &'static str,
nodes: usize,
queries: usize,
}
impl Bencher for SearchCallGraph {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::knowledge::query::KnowledgeView;
let mut calls = Vec::with_capacity(self.nodes * 2);
for i in 0..self.nodes {
calls.extend(mk_call(i, self.nodes));
}
let view = KnowledgeView::new(vec![], calls);
let from = "f0".to_string();
let to = format!("f{}", self.nodes - 1);
let _ = view.call_path(&from, &to);
let _ = view.callers_of("helper");
let mut per_iter = Vec::with_capacity(self.queries);
let mut work = 0u64;
let t0 = std::time::Instant::now();
for _ in 0..self.queries {
let t = std::time::Instant::now();
if let Some(p) = view.call_path(&from, &to) {
work += p.len() as u64;
}
work += view.callers_of("helper").len() as u64;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("nodes".into(), serde_json::json!(self.nodes));
m.insert("work".into(), serde_json::json!(work));
record(
&mut m,
"queries_per_sec",
(self.queries * 2) as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// SEARCH — change-radius traversal: BFS the `call_edges` graph `hops` out from a
/// seed (the "trim to affected set" lever behind Graph-RAG radius + the
/// change-radius MCP query). Pure in-memory over a `nodes` view.
struct SearchCallRadius {
id: &'static str,
nodes: usize,
hops: usize,
}
impl Bencher for SearchCallRadius {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::knowledge::query::KnowledgeView;
let mut calls = Vec::with_capacity(self.nodes * 2);
for i in 0..self.nodes {
calls.extend(mk_call(i, self.nodes));
}
let view = KnowledgeView::new(vec![], calls);
// BFS `hops` from a set of seeds, following callees (forward blast radius).
let bfs = |seed: &str, hops: usize| -> usize {
use std::collections::HashSet;
let mut seen: HashSet<String> = HashSet::new();
let mut frontier = vec![seed.to_string()];
seen.insert(seed.to_string());
for _ in 0..hops {
let mut next = Vec::new();
for nname in &frontier {
for e in view.callees_of(nname) {
let c = e
.callee_ident
.rsplit("::")
.next()
.unwrap_or(&e.callee_ident)
.to_string();
if seen.insert(c.clone()) {
next.push(c);
}
}
}
if next.is_empty() {
break;
}
frontier = next;
}
seen.len()
};
let _ = bfs("f0", self.hops);
let queries = 64usize;
let mut per_iter = Vec::with_capacity(queries);
let mut reached = 0u64;
let t0 = std::time::Instant::now();
for q in 0..queries {
let seed = format!("f{}", (q * 7) % self.nodes);
let t = std::time::Instant::now();
reached += bfs(&seed, self.hops) as u64;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("nodes".into(), serde_json::json!(self.nodes));
m.insert("hops".into(), serde_json::json!(self.hops));
m.insert("reached".into(), serde_json::json!(reached));
record(&mut m, "queries_per_sec", queries as u64, per_iter, total);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ═══════════════════ SEARCH: Ragnar/Eytzinger/STree64 ══════════════════════
/// SEARCH — STree64 exact point lookup vs LinearScan (A/B): `at_exact` over a
/// `keys`-key Ragnar log, timed against the naive linear baseline, with the
/// speedup ratio the keep-or-drop gate reads.
struct SearchRagnarAtExact {
id: &'static str,
keys: usize,
}
impl Bencher for SearchRagnarAtExact {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::ragnar::{LinearScan, RagnarLog};
let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
let log = RagnarLog::build(rows.clone());
let lin = LinearScan::build(rows);
let probes: Vec<i64> = (0..1024u64)
.map(|i| ((i.wrapping_mul(2_654_435_761) as usize % self.keys) as i64) * 2)
.collect();
// Warm.
for &p in probes.iter().take(16) {
let _ = log.at_exact(p);
let _ = lin.at_exact(p);
}
let iters = 32usize;
let mut per_iter = Vec::with_capacity(iters);
let mut acc = 0u64;
let t0 = std::time::Instant::now();
for _ in 0..iters {
let t = std::time::Instant::now();
for &p in &probes {
acc += log.at_exact(p).copied().unwrap_or(0);
}
per_iter.push(t.elapsed().as_secs_f64() * 1e6 / probes.len() as f64);
}
let fast = t0.elapsed();
// Baseline pass.
let tb = std::time::Instant::now();
let mut accb = 0u64;
for _ in 0..iters {
for &p in &probes {
accb += lin.at_exact(p).copied().unwrap_or(0);
}
}
let slow = tb.elapsed();
// A/B correctness gate — NOT a debug-only sanity check. `speedup_x` below is
// only a speedup if both sides answered the same question, and benches run in
// release where `debug_assert!` is compiled out. Fail the arm instead.
ensure!(
acc == accb,
"{}: STree64 `at_exact` and LinearScan disagree over {} keys \
({} probes × {} iters) — checksum {} vs baseline {}; the reported \
speedup would be measuring a wrong answer",
self.id,
self.keys,
probes.len(),
iters,
acc,
accb
);
let n = (iters * probes.len()) as u64;
let mut m = serde_json::Map::new();
m.insert("keys".into(), serde_json::json!(self.keys));
m.insert(
"baseline_lookups_per_sec".into(),
serde_json::json!(n as f64 / slow.as_secs_f64().max(1e-9)),
);
m.insert(
"speedup_x".into(),
serde_json::json!(slow.as_secs_f64() / fast.as_secs_f64().max(1e-9)),
);
m.insert("checksum".into(), serde_json::json!(acc));
m.insert("baseline_checksum".into(), serde_json::json!(accb));
record(&mut m, "lookups_per_sec", n, per_iter, fast);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// SEARCH — Eytzinger floor (`as_of`, point-in-time) lookup vs LinearScan (A/B).
struct SearchRagnarAsOf {
id: &'static str,
keys: usize,
}
impl Bencher for SearchRagnarAsOf {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::ragnar::{LinearScan, RagnarLog};
let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
let log = RagnarLog::build(rows.clone());
let lin = LinearScan::build(rows);
// Odd probes → floor descent (between stored even keys).
let probes: Vec<i64> = (0..1024u64)
.map(|i| ((i.wrapping_mul(40_503) as usize % self.keys) as i64) * 2 + 1)
.collect();
for &p in probes.iter().take(16) {
let _ = log.as_of(p);
let _ = lin.as_of(p);
}
let iters = 32usize;
let mut per_iter = Vec::with_capacity(iters);
let mut acc = 0u64;
let t0 = std::time::Instant::now();
for _ in 0..iters {
let t = std::time::Instant::now();
for &p in &probes {
acc += log.as_of(p).copied().unwrap_or(0);
}
per_iter.push(t.elapsed().as_secs_f64() * 1e6 / probes.len() as f64);
}
let fast = t0.elapsed();
let tb = std::time::Instant::now();
let mut accb = 0u64;
for _ in 0..iters {
for &p in &probes {
accb += lin.as_of(p).copied().unwrap_or(0);
}
}
let slow = tb.elapsed();
// A/B correctness gate (see `SearchRagnarAtExact`): release builds compile
// `debug_assert!` out, so the invariant must be an `ensure!` to be real.
ensure!(
acc == accb,
"{}: Eytzinger `as_of` floor and LinearScan disagree over {} keys \
({} probes × {} iters) — checksum {} vs baseline {}; the reported \
speedup would be measuring a wrong answer",
self.id,
self.keys,
probes.len(),
iters,
acc,
accb
);
let n = (iters * probes.len()) as u64;
let mut m = serde_json::Map::new();
m.insert("keys".into(), serde_json::json!(self.keys));
m.insert(
"baseline_lookups_per_sec".into(),
serde_json::json!(n as f64 / slow.as_secs_f64().max(1e-9)),
);
m.insert(
"speedup_x".into(),
serde_json::json!(slow.as_secs_f64() / fast.as_secs_f64().max(1e-9)),
);
m.insert("checksum".into(), serde_json::json!(acc));
m.insert("baseline_checksum".into(), serde_json::json!(accb));
record(&mut m, "lookups_per_sec", n, per_iter, fast);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// SEARCH — range cut (`boundary_leq`, the as-of replay boundary) vs LinearScan.
struct SearchRagnarBoundary {
id: &'static str,
keys: usize,
}
impl Bencher for SearchRagnarBoundary {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::ragnar::{LinearScan, RagnarLog};
let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
let log = RagnarLog::build(rows.clone());
let lin = LinearScan::build(rows);
let probes: Vec<i64> = (0..1024u64)
.map(|i| ((i.wrapping_mul(2_246_822_519) as usize % self.keys) as i64) * 2 + 1)
.collect();
for &p in probes.iter().take(16) {
let _ = log.boundary_leq(p);
let _ = lin.boundary_leq(p);
}
let iters = 32usize;
let mut per_iter = Vec::with_capacity(iters);
let mut acc = 0usize;
let t0 = std::time::Instant::now();
for _ in 0..iters {
let t = std::time::Instant::now();
for &p in &probes {
acc += log.boundary_leq(p);
}
per_iter.push(t.elapsed().as_secs_f64() * 1e6 / probes.len() as f64);
}
let fast = t0.elapsed();
let tb = std::time::Instant::now();
let mut accb = 0usize;
for _ in 0..iters {
for &p in &probes {
accb += lin.boundary_leq(p);
}
}
let slow = tb.elapsed();
// A/B correctness gate (see `SearchRagnarAtExact`): release builds compile
// `debug_assert!` out, so the invariant must be an `ensure!` to be real.
ensure!(
acc == accb,
"{}: `boundary_leq` range cut and LinearScan disagree over {} keys \
({} probes × {} iters) — checksum {} vs baseline {}; the reported \
speedup would be measuring a wrong answer",
self.id,
self.keys,
probes.len(),
iters,
acc,
accb
);
let n = (iters * probes.len()) as u64;
let mut m = serde_json::Map::new();
m.insert("keys".into(), serde_json::json!(self.keys));
m.insert(
"baseline_lookups_per_sec".into(),
serde_json::json!(n as f64 / slow.as_secs_f64().max(1e-9)),
);
m.insert(
"speedup_x".into(),
serde_json::json!(slow.as_secs_f64() / fast.as_secs_f64().max(1e-9)),
);
m.insert("checksum".into(), serde_json::json!(acc));
m.insert("baseline_checksum".into(), serde_json::json!(accb));
record(&mut m, "lookups_per_sec", n, per_iter, fast);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ═══════════════════════ SEARCH: DataFusion SQL ════════════════════════════
/// SEARCH — raw DataFusion SQL over `git_heat_facts`: a point `WHERE file=` and a
/// `GROUP BY authors_total` aggregate — the shape the `mimir_sql` MCP tool runs.
struct SearchSqlRaw {
id: &'static str,
kind: &'static str, // "point" | "agg"
rows: usize,
}
impl Bencher for SearchSqlRaw {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::iceberg::IcebergWarehouse;
use crate::warehouse::sql::query;
let dir = tempfile::tempdir()?;
let wh = IcebergWarehouse::open(dir.path())?;
wh.append_git_heat_scan(&mk_heat("repo", self.rows))?;
let sql = match self.kind {
"agg" => "SELECT authors_total, COUNT(*) AS n FROM git_heat_facts GROUP BY authors_total"
.to_string(),
_ => "SELECT file, commits_total FROM git_heat_facts WHERE file = 'src/m1/f1.rs'"
.to_string(),
};
let _ = query(&wh, &sql);
let iters = 16usize;
let mut per_iter = Vec::with_capacity(iters);
let mut got = 0u64;
let t0 = std::time::Instant::now();
for _ in 0..iters {
let t = std::time::Instant::now();
if let Ok(b) = query(&wh, &sql) {
got += b.iter().map(|x| x.num_rows() as u64).sum::<u64>();
}
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
}
let total = t0.elapsed();
let mut m = serde_json::Map::new();
m.insert("kind".into(), serde_json::json!(self.kind));
m.insert("rows".into(), serde_json::json!(self.rows));
m.insert("result_rows".into(), serde_json::json!(got / iters as u64));
record(&mut m, "queries_per_sec", iters as u64, per_iter, total);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// Nearest-rank p50 of a latency sample (µs in, µs out); empty ⇒ 0.
fn p50_us(xs: &[f64]) -> f64 {
percentiles_us(xs.to_vec()).0
}
// ═══════════════════════════ REGISTRATIONS ═════════════════════════════════
//
// Grouped by operation × size. `nornir.mimir.populate.*` and
// `nornir.mimir.search.*` so the mashup / dwarves testmatrix-benches view groups
// them cleanly. Sizes chosen so the whole suite runs serially in a few minutes;
// 10M arms are the cheap in-memory (Ragnar) structures only.
// ── POPULATE: call_edges index build ──
crate::register_bench!(PopCallEdgeIndex {
id: "nornir.mimir.populate.call_edge_index_1k",
edges: 1_000,
iters: 20
});
crate::register_bench!(PopCallEdgeIndex {
id: "nornir.mimir.populate.call_edge_index_10k",
edges: 10_000,
iters: 10
});
crate::register_bench!(PopCallEdgeIndex {
id: "nornir.mimir.populate.call_edge_index_100k",
edges: 100_000,
iters: 5
});
crate::register_bench!(PopCallEdgeIndex {
id: "nornir.mimir.populate.call_edge_index_1m",
edges: 1_000_000,
iters: 3
});
// ── POPULATE: Ragnar/Eytzinger/STree64 build ──
crate::register_bench!(PopRagnarBuild {
id: "nornir.mimir.populate.ragnar_build_1k",
keys: 1_000,
iters: 50
});
crate::register_bench!(PopRagnarBuild {
id: "nornir.mimir.populate.ragnar_build_10k",
keys: 10_000,
iters: 20
});
crate::register_bench!(PopRagnarBuild {
id: "nornir.mimir.populate.ragnar_build_100k",
keys: 100_000,
iters: 10
});
crate::register_bench!(PopRagnarBuild {
id: "nornir.mimir.populate.ragnar_build_1m",
keys: 1_000_000,
iters: 5
});
crate::register_bench!(PopRagnarBuild {
id: "nornir.mimir.populate.ragnar_build_10m",
keys: 10_000_000,
iters: 2
});
// ── POPULATE: LinearScan baseline build ──
crate::register_bench!(PopLinearBuild {
id: "nornir.mimir.populate.linear_build_1k",
keys: 1_000,
iters: 50
});
crate::register_bench!(PopLinearBuild {
id: "nornir.mimir.populate.linear_build_100k",
keys: 100_000,
iters: 10
});
crate::register_bench!(PopLinearBuild {
id: "nornir.mimir.populate.linear_build_1m",
keys: 1_000_000,
iters: 5
});
crate::register_bench!(PopLinearBuild {
id: "nornir.mimir.populate.linear_build_10m",
keys: 10_000_000,
iters: 2
});
// ── POPULATE: Iceberg commit throughput ──
crate::register_bench!(PopIcebergCommit {
id: "nornir.mimir.populate.iceberg_commit_8",
commits: 8,
rows_each: 200
});
crate::register_bench!(PopIcebergCommit {
id: "nornir.mimir.populate.iceberg_commit_16",
commits: 16,
rows_each: 200
});
crate::register_bench!(PopIcebergCommit {
id: "nornir.mimir.populate.iceberg_commit_32",
commits: 32,
rows_each: 200
});
crate::register_bench!(PopIcebergCommit {
id: "nornir.mimir.populate.iceberg_commit_64",
commits: 64,
rows_each: 200
});
// ── POPULATE: commit latency vs table depth — the census-P1 A/B (see
// `PopIcebergCommitDeep`). `_dup` keeps the per-commit duplicate precheck on,
// `_fast` turns it off; `deep_0_*` is the level-pair control. ──
crate::register_bench!(PopIcebergCommitDeep {
id: "nornir.mimir.populate.iceberg_commit_deep_0_dup",
seed_commits: 0,
measure_commits: 8,
check_dup: true
});
crate::register_bench!(PopIcebergCommitDeep {
id: "nornir.mimir.populate.iceberg_commit_deep_0_fast",
seed_commits: 0,
measure_commits: 8,
check_dup: false
});
crate::register_bench!(PopIcebergCommitDeep {
id: "nornir.mimir.populate.iceberg_commit_deep_250_dup",
seed_commits: 250,
measure_commits: 8,
check_dup: true
});
crate::register_bench!(PopIcebergCommitDeep {
id: "nornir.mimir.populate.iceberg_commit_deep_250_fast",
seed_commits: 250,
measure_commits: 8,
check_dup: false
});
crate::register_bench!(PopIcebergCommitDeep {
id: "nornir.mimir.populate.iceberg_commit_deep_1k_dup",
seed_commits: 1000,
measure_commits: 8,
check_dup: true
});
crate::register_bench!(PopIcebergCommitDeep {
id: "nornir.mimir.populate.iceberg_commit_deep_1k_fast",
seed_commits: 1000,
measure_commits: 8,
check_dup: false
});
// ── SEARCH: call-graph queries ──
crate::register_bench!(SearchCallGraph {
id: "nornir.mimir.search.callgraph_1k",
nodes: 1_000,
queries: 500
});
crate::register_bench!(SearchCallGraph {
id: "nornir.mimir.search.callgraph_10k",
nodes: 10_000,
queries: 200
});
crate::register_bench!(SearchCallGraph {
id: "nornir.mimir.search.callgraph_100k",
nodes: 100_000,
queries: 50
});
crate::register_bench!(SearchCallGraph {
id: "nornir.mimir.search.callgraph_500k",
nodes: 500_000,
queries: 20
});
// ── SEARCH: change-radius traversal ──
crate::register_bench!(SearchCallRadius {
id: "nornir.mimir.search.call_radius_1k",
nodes: 1_000,
hops: 4
});
crate::register_bench!(SearchCallRadius {
id: "nornir.mimir.search.call_radius_10k",
nodes: 10_000,
hops: 4
});
crate::register_bench!(SearchCallRadius {
id: "nornir.mimir.search.call_radius_100k",
nodes: 100_000,
hops: 4
});
// ── SEARCH: Ragnar STree64 exact point lookup (+A/B) ──
crate::register_bench!(SearchRagnarAtExact {
id: "nornir.mimir.search.ragnar_at_exact_1k",
keys: 1_000
});
crate::register_bench!(SearchRagnarAtExact {
id: "nornir.mimir.search.ragnar_at_exact_10k",
keys: 10_000
});
crate::register_bench!(SearchRagnarAtExact {
id: "nornir.mimir.search.ragnar_at_exact_100k",
keys: 100_000
});
crate::register_bench!(SearchRagnarAtExact {
id: "nornir.mimir.search.ragnar_at_exact_1m",
keys: 1_000_000
});
crate::register_bench!(SearchRagnarAtExact {
id: "nornir.mimir.search.ragnar_at_exact_10m",
keys: 10_000_000
});
// ── SEARCH: Eytzinger floor as_of lookup (+A/B) ──
crate::register_bench!(SearchRagnarAsOf {
id: "nornir.mimir.search.ragnar_as_of_1k",
keys: 1_000
});
crate::register_bench!(SearchRagnarAsOf {
id: "nornir.mimir.search.ragnar_as_of_10k",
keys: 10_000
});
crate::register_bench!(SearchRagnarAsOf {
id: "nornir.mimir.search.ragnar_as_of_100k",
keys: 100_000
});
crate::register_bench!(SearchRagnarAsOf {
id: "nornir.mimir.search.ragnar_as_of_1m",
keys: 1_000_000
});
crate::register_bench!(SearchRagnarAsOf {
id: "nornir.mimir.search.ragnar_as_of_10m",
keys: 10_000_000
});
// ── SEARCH: range cut boundary_leq (+A/B) ──
crate::register_bench!(SearchRagnarBoundary {
id: "nornir.mimir.search.ragnar_boundary_1k",
keys: 1_000
});
crate::register_bench!(SearchRagnarBoundary {
id: "nornir.mimir.search.ragnar_boundary_10k",
keys: 10_000
});
crate::register_bench!(SearchRagnarBoundary {
id: "nornir.mimir.search.ragnar_boundary_100k",
keys: 100_000
});
crate::register_bench!(SearchRagnarBoundary {
id: "nornir.mimir.search.ragnar_boundary_1m",
keys: 1_000_000
});
crate::register_bench!(SearchRagnarBoundary {
id: "nornir.mimir.search.ragnar_boundary_10m",
keys: 10_000_000
});
// ── SEARCH: raw DataFusion SQL ──
crate::register_bench!(SearchSqlRaw {
id: "nornir.mimir.search.sql_point_500",
kind: "point",
rows: 500
});
crate::register_bench!(SearchSqlRaw {
id: "nornir.mimir.search.sql_point_5k",
kind: "point",
rows: 5_000
});
crate::register_bench!(SearchSqlRaw {
id: "nornir.mimir.search.sql_agg_500",
kind: "agg",
rows: 500
});
crate::register_bench!(SearchSqlRaw {
id: "nornir.mimir.search.sql_agg_5k",
kind: "agg",
rows: 5_000
});
// ── vector / embedding arms (feature-gated) ──
#[cfg(feature = "vector")]
mod vec_registrations {
use super::vec_benches::*;
// POPULATE: step-3b embed — per-member `index_repo` loop vs flattened,
// GPU-batched `index_repos_batched` (the 2026-07-25 fix). Records `speedup`.
crate::register_bench!(PopEmbedMembersBatched {
id: "nornir.mimir.populate.embed_members_batched",
members: 16,
files: 6,
dim: 64
});
// POPULATE: embeddings + manifest.
crate::register_bench!(PopEmbeddingIndex {
id: "nornir.mimir.populate.embeddings_1k",
chunks: 1_000,
dim: 32
});
crate::register_bench!(PopEmbeddingIndex {
id: "nornir.mimir.populate.embeddings_5k",
chunks: 5_000,
dim: 32
});
crate::register_bench!(PopEmbeddingIndex {
id: "nornir.mimir.populate.embeddings_20k",
chunks: 20_000,
dim: 32
});
crate::register_bench!(PopEmbeddingIndex {
id: "nornir.mimir.populate.embeddings_50k",
chunks: 50_000,
dim: 32
});
// POPULATE: VectorIndex (vann) build across rows × dim.
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_1k_d64",
rows: 1_000,
dim: 64
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_1k_d128",
rows: 1_000,
dim: 128
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_1k_d256",
rows: 1_000,
dim: 256
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_10k_d64",
rows: 10_000,
dim: 64
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_10k_d128",
rows: 10_000,
dim: 128
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_10k_d256",
rows: 10_000,
dim: 256
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_100k_d64",
rows: 100_000,
dim: 64
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_100k_d128",
rows: 100_000,
dim: 128
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_100k_d256",
rows: 100_000,
dim: 256
});
// SEARCH: dense kNN single-query across corpus sizes.
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_500",
rows: 500,
dim: 128,
queries: 256,
k: 10
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_1k",
rows: 1_000,
dim: 128,
queries: 256,
k: 10
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_10k",
rows: 10_000,
dim: 128,
queries: 128,
k: 10
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_50k",
rows: 50_000,
dim: 128,
queries: 64,
k: 10
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_100k",
rows: 100_000,
dim: 128,
queries: 32,
k: 10
});
// SEARCH: batched dense kNN across corpus sizes.
crate::register_bench!(SearchVectorKnnBatch {
id: "nornir.mimir.search.vec_knn_batch_500",
rows: 500,
dim: 128,
batch: 256,
k: 10
});
crate::register_bench!(SearchVectorKnnBatch {
id: "nornir.mimir.search.vec_knn_batch_1k",
rows: 1_000,
dim: 128,
batch: 256,
k: 10
});
crate::register_bench!(SearchVectorKnnBatch {
id: "nornir.mimir.search.vec_knn_batch_10k",
rows: 10_000,
dim: 128,
batch: 256,
k: 10
});
crate::register_bench!(SearchVectorKnnBatch {
id: "nornir.mimir.search.vec_knn_batch_50k",
rows: 50_000,
dim: 128,
batch: 256,
k: 10
});
crate::register_bench!(SearchVectorKnnBatch {
id: "nornir.mimir.search.vec_knn_batch_100k",
rows: 100_000,
dim: 128,
batch: 256,
k: 10
});
// SEARCH: HNSW ANN vs the brute-force flat oracle (recall@10 + latency).
// The Mimir vector cycle: O(log n) approximate search with a recall floor.
// SEARCH: HNSW recall + latency vs the exact flat oracle, on the corpus
// geometry the LIVE path actually serves (clustered code embeddings). These
// carry the real 0.90 floor at the SHIPPED default ef_search (ef=0 → default),
// so they are red-when-broken without buying recall with latency.
crate::register_bench!(SearchHnswKnn {
id: "nornir.mimir.search.vec_hnsw_1k",
rows: 1_000,
dim: 128,
queries: 256,
k: 10,
recall_floor: 0.90,
corpus: HnswCorpus::Realistic,
ef_search: 0
});
crate::register_bench!(SearchHnswKnn {
id: "nornir.mimir.search.vec_hnsw_10k",
rows: 10_000,
dim: 128,
queries: 128,
k: 10,
recall_floor: 0.90,
corpus: HnswCorpus::Realistic,
ef_search: 0
});
crate::register_bench!(SearchHnswKnn {
id: "nornir.mimir.search.vec_hnsw_100k",
rows: 100_000,
dim: 128,
queries: 64,
k: 10,
recall_floor: 0.90,
corpus: HnswCorpus::Realistic,
ef_search: 0
});
// SEARCH: the ADVERSARIAL i.i.d.-uniform corpus, kept deliberately. Recall
// there is NOT free — at the default ef=64 it measures .733 @10k / .266 @100k,
// which is the curse of dimensionality, not a graph defect (proof: recall
// converges to exactly 1.0000 as ef grows). So these arms pin the ef the
// measured curve says is needed for 0.90 and let the latency columns show what
// it costs (at 100k the HNSW ends up ~10× SLOWER than the flat scan — the
// honest verdict is that an ANN index does not belong on data of this shape).
crate::register_bench!(SearchHnswKnn {
id: "nornir.mimir.search.vec_hnsw_10k_uniform",
rows: 10_000,
dim: 128,
queries: 128,
k: 10,
recall_floor: 0.90,
corpus: HnswCorpus::UniformAdversarial,
ef_search: 512
});
crate::register_bench!(SearchHnswKnn {
id: "nornir.mimir.search.vec_hnsw_100k_uniform",
rows: 100_000,
dim: 128,
queries: 64,
k: 10,
recall_floor: 0.90,
corpus: HnswCorpus::UniformAdversarial,
ef_search: 2_048
});
// SEARCH: dense kNN varying k at fixed corpus.
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_k1",
rows: 20_000,
dim: 128,
queries: 128,
k: 1
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_k10",
rows: 20_000,
dim: 128,
queries: 128,
k: 10
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_k50",
rows: 20_000,
dim: 128,
queries: 128,
k: 50
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_k100",
rows: 20_000,
dim: 128,
queries: 128,
k: 100
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_k200",
rows: 20_000,
dim: 128,
queries: 128,
k: 200
});
// SEARCH: Graph-RAG context-slicing (radius off/on) across corpus sizes.
crate::register_bench!(SearchContextSlice {
id: "nornir.mimir.search.context_slice_1k",
chunks: 1_000,
k: 10,
radius: false
});
crate::register_bench!(SearchContextSlice {
id: "nornir.mimir.search.context_slice_1k_radius",
chunks: 1_000,
k: 10,
radius: true
});
crate::register_bench!(SearchContextSlice {
id: "nornir.mimir.search.context_slice_5k",
chunks: 5_000,
k: 10,
radius: false
});
crate::register_bench!(SearchContextSlice {
id: "nornir.mimir.search.context_slice_5k_radius",
chunks: 5_000,
k: 10,
radius: true
});
crate::register_bench!(SearchContextSlice {
id: "nornir.mimir.search.context_slice_10k",
chunks: 10_000,
k: 10,
radius: false
});
crate::register_bench!(SearchContextSlice {
id: "nornir.mimir.search.context_slice_10k_radius",
chunks: 10_000,
k: 10,
radius: true
});
crate::register_bench!(SearchContextSlice {
id: "nornir.mimir.search.context_slice_20k",
chunks: 20_000,
k: 10,
radius: false
});
// SEARCH: RRF hybrid vs dense-only (optimize cycle 1 proof).
crate::register_bench!(SearchHybridRrf {
id: "nornir.mimir.search.hybrid_rrf_200",
docs: 200
});
crate::register_bench!(SearchHybridRrf {
id: "nornir.mimir.search.hybrid_rrf_1k",
docs: 1_000
});
// REAL-project ingest (nornir's own src/) — the north-star populate+search.
crate::register_bench!(RealEmbedPopulate {
id: "nornir.mimir.populate.real_src_embed"
});
crate::register_bench!(RealVectorSearch {
id: "nornir.mimir.search.real_src_vec_knn"
});
}
#[cfg(test)]
mod search_ab_tests {
use super::*;
use crate::bench::api::Bencher;
/// RED-WHEN-BROKEN: the three SEARCH A/B arms publish a `speedup_x` that only
/// means anything if the fast index and the linear baseline resolve the SAME
/// answers. That equality is enforced inside the arm by `ensure!`, so an `Err`
/// from `run()` IS the failure signal — in every profile. (A `debug_assert!`
/// is compiled out of the release build the benches actually run in, which is
/// what these arms used to rely on.) Tiny `keys` keeps this a unit test; the
/// registered arms sweep 1k…10m.
#[test]
fn ragnar_ab_arms_agree_with_the_linear_baseline() {
for keys in [64usize, 512] {
let exact = SearchRagnarAtExact {
id: "nornir.mimir.search.ragnar_at_exact_test",
keys,
}
.run()
.expect("at_exact must agree with LinearScan");
let as_of = SearchRagnarAsOf {
id: "nornir.mimir.search.ragnar_as_of_test",
keys,
}
.run()
.expect("as_of must agree with LinearScan");
let bound = SearchRagnarBoundary {
id: "nornir.mimir.search.ragnar_boundary_test",
keys,
}
.run()
.expect("boundary_leq must agree with LinearScan");
for out in [&exact, &as_of, &bound] {
let ck = out.metrics.get("checksum").and_then(|v| v.as_u64());
let base = out
.metrics
.get("baseline_checksum")
.and_then(|v| v.as_u64());
assert_eq!(
ck, base,
"{}: checksums must match: {:?}",
out.name, out.metrics
);
assert!(
out.metrics
.get("lookups_per_sec")
.and_then(|v| v.as_f64())
.unwrap_or(0.0)
> 0.0,
"{}: lookups_per_sec must be positive: {:?}",
out.name,
out.metrics
);
}
}
}
}
// ╔══════════════════════════════════════════════════════════════════════════╗
// ║ APPENDED REGION — the measurement gaps (2026-07-23) ║
// ╚══════════════════════════════════════════════════════════════════════════╝
//
// A deliberately SEPARATE, purely ADDITIVE block: nothing above this line is
// reordered, re-wrapped or re-indented, so this merges cleanly beside the two
// other in-flight edits to this file (the `debug_assert` → `ensure!` promotion
// and the served-path `symbol_lookup` / `callers_of` arms).
//
// What it closes, in the order the audit ranked it:
//
// 1. `mimir analyze duplicates` had **zero** arms — the verb that could not
// complete (15 min CPU, SIGTERM, zero output) and was then rewritten to
// route candidate generation through `VectorIndex::pairs_above`, shipped
// with nothing measuring it. Benched at FIVE sizes at the live dim (768) so
// the complexity CLASS is visible, not one point (see `comparisons_per_sec`).
// 2. The **Ragnar batch sweep** — `as_of_batch::<P>` / `boundary_leq_batch::<P>`
// landed with no arm at all. Swept over P at two corpus sizes so P is PICKED
// from evidence rather than guessed.
// 3. `git_heat::scan_repo` — 0 arms, and it is half of `knowledge::scan_all`.
// 4. Tantivy **build** and **search** — 0 arms. (`hybrid_rrf` does not cover
// this: both of its queries run OUTSIDE its timed loop, which contains only
// `fuse_bm25_dense`, so it measures the fusion and not the search.)
// 5. Every embedding arm ran at dim 32 while the live model is 768.
//
// **Every arm below carries a floor that runs in RELEASE** (`anyhow::ensure!`,
// never `debug_assert!` — which is compiled out in release and is exactly why 21
// existing arms check nothing). Where the floor is a THROUGHPUT number it is one
// measured on this machine with a stated margin; where no number was measured the
// arm says so in its doc comment and ships with its correctness floor only.
// ─────────────────── shared fixtures for the appended arms ──────────────────
/// Deterministic, well-spread pseudo-random vector — xorshift, uniform in
/// `[-1, 1)`.
///
/// NOT [`mk_vec`]: that one is a smooth `sin` curve, so two different seeds come
/// out strongly correlated. A duplicate detector's fixture needs the opposite —
/// at `dim = 768` the cosine of two independent draws here is ≈ `N(0, 1/√768)`,
/// so the 0.92 threshold sits ~25σ out and **no unplanted pair can clear it by
/// luck**. That is what makes the exact-count floors below safe to assert.
fn gap_rand_vec(seed: u64, dim: usize) -> Vec<f32> {
let mut s = seed
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add(0x1234_5678)
| 1;
(0..dim)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
((s >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0) as f32
})
.collect()
}
// ══════════════ ANALYZE: `mimir analyze duplicates` (was: 0 arms) ═══════════
/// ANALYZE — the whole `analyze duplicates` verb at the LIVE embedding width.
///
/// Path: [`analyze_duplicates`] →
/// `candidate_pairs` → `VectorIndex::pairs_above` (exact O(n²) SIMD over
/// pre-normalized rows, fanned out on gatling, deliberately **not**
/// ANN-prefiltered because this report's consumer deletes code and a silently
/// dropped pair is worse than a slower scan).
///
/// The arm times the verb from the `SymbolVec` slice inwards — i.e. everything
/// the rewrite touched — on a fixture with `planted` cross-crate duplicate
/// triangles hidden in `symbols - 3·planted` near-orthogonal decoys.
///
/// ## Why several sizes
/// One point cannot show a complexity class. `comparisons_per_sec`
/// (`n(n-1)/2` divided by the wall) is the class-visible column. MEASURED on this
/// box (Threadripper PRO 3975WX, 32 cores, dim 768, contended, best of 3, via a
/// standalone release harness over the same `VectorIndex::pairs_above`):
///
/// | rows | comparisons | comparisons/s |
/// |-------|-------------|---------------|
/// | 1 000 | 0.50 M | 9.2 M |
/// | 2 000 | 2.0 M | 8.4 M |
/// | 4 000 | 8.0 M | 26.9 M |
/// | 8 000 | 32.0 M | 43.0 M |
/// |16 000 | 128.0 M | 51.2 M |
/// |32 000 | 512.0 M | 57.1 M |
///
/// So the column is **not** flat: it RISES ~6× from 1k to 16k and then plateaus.
/// That is the gatling fan-out saturating — below a few thousand rows the band
/// scan does not earn its threads — and it is exactly the shape a single-point
/// arm would have hidden. Above ~16k rows the curve is flat, which is what makes
/// it extrapolable: the real warehouse shape (79k × 768, 3.1 G comparisons) lands
/// at ≈ 55 s of phase-1 scan. The verb that used to SIGTERM after 15 min of CPU
/// with zero output now has a number.
///
/// `symbols_per_sec` falls ~linearly by construction and is the operator-facing
/// column.
///
/// ## Floors (all `ensure!`, all machine-INDEPENDENT)
/// * every planted triangle must be FOUND — `cluster_count == planted` and
/// `pair_count >= 3·planted` (recall);
/// * nothing else may be reported — `cluster_count == planted` is also the
/// precision floor: a decoy pair clearing 0.92 would raise the count;
/// * `pairs_above` and the naive loop must agree, which the existing unit test
/// `both_candidate_paths_agree` pins; here the count IS the oracle.
///
/// There is no throughput floor on this arm. The numbers above were measured
/// through a standalone release harness (a full release build of nornir was not
/// affordable in this worktree), so they are not yet a number this arm has
/// produced about ITSELF — and a floor transplanted from a different binary is a
/// rumour with a decimal point. The correctness floors above are the
/// red-when-broken part; the throughput floor is deliberately left for the first
/// run of the arm in its own release build.
struct AnalyzeDuplicates {
id: &'static str,
symbols: usize,
dim: usize,
/// Cross-crate duplicate triangles planted in the corpus (3 members each).
planted: usize,
iters: usize,
}
impl Bencher for AnalyzeDuplicates {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::mimir_analyze::{DupThresholds, SymbolVec, analyze_duplicates};
use std::collections::BTreeSet;
anyhow::ensure!(
self.symbols >= self.planted * 3,
"fixture is impossible: {} symbols cannot hold {} planted triangles",
self.symbols,
self.planted
);
let dim = self.dim;
let mk = |name: String, krate: String, sig: String, callees: &[&str], vector: Vec<f32>| {
SymbolVec {
id: format!("synthetic::{krate}::{name}"),
repo: "synthetic".into(),
crate_name: krate,
module_path: "m".into(),
item_kind: "fn".into(),
item_name: name,
file: "src/lib.rs".into(),
line: 1,
signature: Some(sig),
callees: callees.iter().map(|s| (*s).to_string()).collect(),
item_end_line: 0,
span_lines: 0,
chunk_hash: String::new(),
impl_header: None,
vector,
}
};
let mut syms: Vec<SymbolVec> = Vec::with_capacity(self.symbols);
// Planted: three copies of one function in three DIFFERENT crates —
// identical signature + identical callee set (structural 1.0) and a
// 0.5%-jittered shared vector (cosine ≈ 0.9999). This is the exact shape
// `cross_scope_only` + the structural gate are built to keep.
for c in 0..self.planted {
let base = gap_rand_vec(0xD00D_0000 + c as u64, dim);
for m in 0..3usize {
let jit = gap_rand_vec(0xBEEF_0000 + (c * 3 + m) as u64, dim);
let v: Vec<f32> = base.iter().zip(&jit).map(|(b, j)| b + 0.005 * j).collect();
syms.push(mk(
format!("dup{c}"),
format!("dupcrate_{m}"),
format!("fn dup{c}(x: u32) -> u32"),
&["helper", "log"],
v,
));
}
}
// Decoys: near-orthogonal, so none of them may pair with anything.
for i in 0..(self.symbols - self.planted * 3) {
syms.push(mk(
format!("noise{i}"),
format!("noisecrate_{}", i % 11),
format!("fn noise{i}(a: u{}) -> ()", 8 + (i % 4) * 8),
&["misc"],
gap_rand_vec(0x5EED_0000 + i as u64, dim),
));
}
let thresholds = DupThresholds::default();
let canonical: BTreeSet<String> = BTreeSet::new();
let n = syms.len();
let comparisons = (n as u64) * (n as u64 - 1) / 2;
let mut per_iter = Vec::with_capacity(self.iters);
let mut total = std::time::Duration::ZERO;
let mut clusters = 0usize;
let mut pairs = 0usize;
for _ in 0..self.iters.max(1) {
let t = std::time::Instant::now();
let rep = analyze_duplicates(&syms, &thresholds, &canonical);
let el = t.elapsed();
total += el;
per_iter.push(el.as_secs_f64() * 1e6);
clusters = rep.cluster_count;
pairs = rep.pair_count;
}
// ── RED-WHEN-BROKEN (release too — `ensure!`, not `debug_assert!`) ──
anyhow::ensure!(
pairs >= 3 * self.planted,
"RECALL: only {pairs} duplicate pairs survived, but {} planted triangles \
carry {} edges — the candidate scan is dropping true duplicates",
self.planted,
3 * self.planted
);
anyhow::ensure!(
clusters == self.planted,
"PRECISION/RECALL: {clusters} clusters reported, {} planted. Decoys here are \
~25σ from the 0.92 cosine bar, so any extra cluster means the scan is \
pairing unrelated symbols, and any missing one means it lost a real copy",
self.planted
);
let secs = total.as_secs_f64().max(1e-9) / self.iters.max(1) as f64;
let mut m = serde_json::Map::new();
m.insert("symbols".into(), serde_json::json!(n));
m.insert("dim".into(), serde_json::json!(dim));
m.insert("planted_clusters".into(), serde_json::json!(self.planted));
m.insert("clusters".into(), serde_json::json!(clusters));
m.insert("pairs".into(), serde_json::json!(pairs));
m.insert("comparisons".into(), serde_json::json!(comparisons));
// The CLASS-visible column: flat across sizes ⇒ still an exact quadratic
// scan with constant per-pair cost. It collapses if the per-pair work grows.
m.insert(
"comparisons_per_sec".into(),
serde_json::json!(comparisons as f64 / secs),
);
record(&mut m, "symbols_per_sec", n as u64, per_iter, total);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ═══════ SEARCH: the Ragnar BATCH sweep (`as_of_batch` / `boundary_leq_batch`) ═══════
/// `P` is a **const generic**, so the sweep needs one arm per width and a match
/// to get from the runtime field to the type-level parameter. Registering the
/// widths is what makes the sweep a bench sweep rather than a one-off script.
macro_rules! ragnar_as_of_batch {
($log:expr, $probes:expr, $p:expr) => {
match $p {
1 => $log.as_of_batch::<1>($probes),
4 => $log.as_of_batch::<4>($probes),
8 => $log.as_of_batch::<8>($probes),
16 => $log.as_of_batch::<16>($probes),
20 => $log.as_of_batch::<20>($probes),
32 => $log.as_of_batch::<32>($probes),
64 => $log.as_of_batch::<64>($probes),
other => anyhow::bail!(
"batch width P={other} is not one of the registered sweep points \
(1, 4, 8, 16, 20, 32, 64) — add it to `ragnar_as_of_batch!`"
),
}
};
}
macro_rules! ragnar_boundary_batch {
($log:expr, $probes:expr, $p:expr) => {
match $p {
1 => $log.boundary_leq_batch::<1>($probes),
4 => $log.boundary_leq_batch::<4>($probes),
8 => $log.boundary_leq_batch::<8>($probes),
16 => $log.boundary_leq_batch::<16>($probes),
20 => $log.boundary_leq_batch::<20>($probes),
32 => $log.boundary_leq_batch::<32>($probes),
64 => $log.boundary_leq_batch::<64>($probes),
other => anyhow::bail!(
"batch width P={other} is not one of the registered sweep points \
(1, 4, 8, 16, 20, 32, 64) — add it to `ragnar_boundary_batch!`"
),
}
};
}
/// The probe set every batch arm uses: 32 768 floor probes (ODD keys, so each one
/// is a genuine predecessor descent between two stored even keys).
///
/// The count is not arbitrary — it is exactly the working set the existing scalar
/// arms (`nornir.mimir.search.ragnar_*`, 1024 probes × 32 iters) issue **one at a
/// time**, so `lookups_per_sec` here is directly comparable to the 8.1 M/s those
/// arms measured at 10M keys.
fn gap_ragnar_probes(keys: usize, queries: usize) -> Vec<i64> {
(0..queries as u64)
.map(|i| ((i.wrapping_mul(2_654_435_761) as usize % keys) as i64) * 2 + 1)
.collect()
}
/// SEARCH — **batched** Eytzinger floor descent (`RagnarLog::as_of_batch::<P>`)
/// against the scalar `as_of` it replaces, swept over the batch width `P`.
///
/// One floor descent serialises on memory: every level of the tree is a dependent
/// load, so the CPU stalls on a miss it cannot overlap with anything. `P`
/// independent descents advanced in lockstep (with a prefetch of every live
/// lane's next line before any of them branch) turn those stalls into
/// parallelism. `P` is a tuning knob, not a constant of nature — this sweep is
/// how it gets PICKED, at both a cache-resident (1M keys) and a
/// memory-bound (10M keys) corpus size.
///
/// ## Floor (`ensure!`, runs in release)
/// The batched answer must be **identical to the scalar descent for every
/// probe**, checked outside the timed region. A faster wrong answer is the only
/// failure mode this optimisation can have, and it is machine-independent — so it
/// is the floor every arm in the sweep carries regardless of what the host does
/// to the throughput numbers.
///
/// `floor_lookups_per_sec` is an additional, MEASURED throughput floor; `0.0`
/// means the arm ships descriptive (no number was measured for that point).
struct SearchRagnarAsOfBatch {
id: &'static str,
keys: usize,
p: usize,
queries: usize,
iters: usize,
floor_lookups_per_sec: f64,
}
impl Bencher for SearchRagnarAsOfBatch {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::ragnar::RagnarLog;
let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
let log = RagnarLog::build(rows);
let probes = gap_ragnar_probes(self.keys, self.queries);
// ── CORRECTNESS FLOOR — outside the timed region, `ensure!` so it is
// still there in the release build the numbers come from. ──
{
let batched = ragnar_as_of_batch!(log, &probes, self.p);
let scalar: Vec<Option<&u64>> = probes.iter().map(|&q| log.as_of(q)).collect();
anyhow::ensure!(
batched.len() == scalar.len(),
"as_of_batch::<{}> returned {} answers for {} queries",
self.p,
batched.len(),
scalar.len()
);
let mismatch = batched
.iter()
.zip(&scalar)
.position(|(a, b)| (*a).copied() != (*b).copied());
anyhow::ensure!(
mismatch.is_none(),
"as_of_batch::<{}> disagrees with the scalar floor descent at probe {} \
(key {}) — a faster WRONG answer",
self.p,
mismatch.unwrap_or(0),
probes[mismatch.unwrap_or(0)]
);
}
// Warm both paths, then time them separately.
let _ = ragnar_as_of_batch!(log, &probes, self.p);
let mut acc = 0u64;
let mut per_iter = Vec::with_capacity(self.iters);
let mut fast = std::time::Duration::ZERO;
for _ in 0..self.iters {
let t = std::time::Instant::now();
let out = ragnar_as_of_batch!(log, &probes, self.p);
let el = t.elapsed();
fast += el;
// Consumed AFTER the timer so the sum is not benched, but consumed at
// all so the call cannot be optimised away.
acc = acc.wrapping_add(out.iter().filter_map(|v| v.copied()).sum::<u64>());
per_iter.push(el.as_secs_f64() * 1e6 / self.queries as f64);
}
let mut accb = 0u64;
let mut slow = std::time::Duration::ZERO;
for _ in 0..self.iters {
let t = std::time::Instant::now();
let mut s = 0u64;
for &q in &probes {
s = s.wrapping_add(log.as_of(q).copied().unwrap_or(0));
}
slow += t.elapsed();
accb = accb.wrapping_add(s);
}
anyhow::ensure!(
acc == accb,
"batched and scalar checksums differ ({acc} vs {accb}) — the two paths \
resolved different rows"
);
let n = (self.iters * self.queries) as u64;
let per_sec = n as f64 / fast.as_secs_f64().max(1e-9);
let base_per_sec = n as f64 / slow.as_secs_f64().max(1e-9);
anyhow::ensure!(
self.floor_lookups_per_sec <= 0.0 || per_sec >= self.floor_lookups_per_sec,
"as_of_batch::<{}> at {} keys did {:.2} M lookups/s, below the measured floor \
of {:.2} M/s",
self.p,
self.keys,
per_sec / 1e6,
self.floor_lookups_per_sec / 1e6
);
let mut m = serde_json::Map::new();
m.insert("keys".into(), serde_json::json!(self.keys));
m.insert("batch_p".into(), serde_json::json!(self.p));
m.insert("queries".into(), serde_json::json!(self.queries));
m.insert(
"baseline_lookups_per_sec".into(),
serde_json::json!(base_per_sec),
);
m.insert(
"speedup_x".into(),
serde_json::json!(per_sec / base_per_sec.max(1e-9)),
);
m.insert("checksum".into(), serde_json::json!(acc));
record(&mut m, "lookups_per_sec", n, per_iter, fast);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// SEARCH — batched replay-boundary cut (`RagnarLog::boundary_leq_batch::<P>`)
/// against the scalar `boundary_leq`. Same descent, different projection (the
/// index+1 cut rather than the value), so it is registered at the un-pipelined
/// control (P=1) and at the plateau the `as_of_batch` sweep PICKED (P=8) rather
/// than at all seven widths — the curve was measured to be the same shape.
///
/// Floor: identical to the scalar cut for every probe (`ensure!`, release).
struct SearchRagnarBoundaryBatch {
id: &'static str,
keys: usize,
p: usize,
queries: usize,
iters: usize,
floor_lookups_per_sec: f64,
}
impl Bencher for SearchRagnarBoundaryBatch {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::warehouse::ragnar::RagnarLog;
let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
let log = RagnarLog::build(rows);
let probes = gap_ragnar_probes(self.keys, self.queries);
{
let batched = ragnar_boundary_batch!(log, &probes, self.p);
let scalar: Vec<usize> = probes.iter().map(|&q| log.boundary_leq(q)).collect();
let mismatch = batched.iter().zip(&scalar).position(|(a, b)| a != b);
anyhow::ensure!(
batched.len() == scalar.len() && mismatch.is_none(),
"boundary_leq_batch::<{}> disagrees with the scalar cut at probe {} — \
a faster WRONG replay boundary",
self.p,
mismatch.unwrap_or(0)
);
}
let _ = ragnar_boundary_batch!(log, &probes, self.p);
let mut acc = 0usize;
let mut per_iter = Vec::with_capacity(self.iters);
let mut fast = std::time::Duration::ZERO;
for _ in 0..self.iters {
let t = std::time::Instant::now();
let out = ragnar_boundary_batch!(log, &probes, self.p);
let el = t.elapsed();
fast += el;
acc = acc.wrapping_add(out.iter().copied().sum::<usize>());
per_iter.push(el.as_secs_f64() * 1e6 / self.queries as f64);
}
let mut accb = 0usize;
let mut slow = std::time::Duration::ZERO;
for _ in 0..self.iters {
let t = std::time::Instant::now();
let mut s = 0usize;
for &q in &probes {
s = s.wrapping_add(log.boundary_leq(q));
}
slow += t.elapsed();
accb = accb.wrapping_add(s);
}
anyhow::ensure!(acc == accb, "batched and scalar boundary checksums differ");
let n = (self.iters * self.queries) as u64;
let per_sec = n as f64 / fast.as_secs_f64().max(1e-9);
let base_per_sec = n as f64 / slow.as_secs_f64().max(1e-9);
anyhow::ensure!(
self.floor_lookups_per_sec <= 0.0 || per_sec >= self.floor_lookups_per_sec,
"boundary_leq_batch::<{}> at {} keys did {:.2} M/s, below the measured floor {:.2} M/s",
self.p,
self.keys,
per_sec / 1e6,
self.floor_lookups_per_sec / 1e6
);
let mut m = serde_json::Map::new();
m.insert("keys".into(), serde_json::json!(self.keys));
m.insert("batch_p".into(), serde_json::json!(self.p));
m.insert("queries".into(), serde_json::json!(self.queries));
m.insert(
"baseline_lookups_per_sec".into(),
serde_json::json!(base_per_sec),
);
m.insert(
"speedup_x".into(),
serde_json::json!(per_sec / base_per_sec.max(1e-9)),
);
m.insert("checksum".into(), serde_json::json!(acc as u64));
record(&mut m, "lookups_per_sec", n, per_iter, fast);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ═════════════ POPULATE: `git_heat::scan_repo` (was: 0 arms) ════════════════
/// Build a deterministic git repo for the git-heat arm: `files` files, one
/// initial commit that adds them all, then `commits` commits that each touch
/// exactly THREE files, with the author rotating over four identities.
///
/// The shape is what makes the arm's floors exact: `scan_repo` diffs every commit
/// against its first parent (root commit ⇒ its whole tree), so the total
/// commit-touches it must aggregate is exactly `files + 3·commits`.
///
/// Uses the `git` CLI rather than `gix` plumbing on purpose — the fixture is
/// UNTIMED setup, and a hand-rolled object writer here would be a second
/// implementation of commit creation with its own bugs. (`tests/support/mcp_seed.rs`
/// shells `git init` for the same reason.) Paths are named explicitly on every
/// `git add`; nothing here ever runs `git add -A`.
fn gap_git_heat_fixture(dir: &std::path::Path, files: usize, commits: usize) -> Result<()> {
use std::process::Command;
const AUTHORS: [(&str, &str); 4] = [
("Aslaug", "aslaug@nornir.test"),
("Bjorn", "bjorn@nornir.test"),
("Cerdic", "cerdic@nornir.test"),
("Dagny", "dagny@nornir.test"),
];
let git = |author: usize, args: &[&str]| -> Result<()> {
let (name, email) = AUTHORS[author % AUTHORS.len()];
let st = Command::new("git")
.current_dir(dir)
.args(["-c", &format!("user.name={name}")])
.args(["-c", &format!("user.email={email}")])
.args(["-c", "commit.gpgsign=false"])
.args(["-c", "init.defaultBranch=main"])
.args(["-c", "gc.auto=0"])
.args(args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map_err(|e| anyhow::anyhow!("spawning `git {}`: {e}", args.join(" ")))?;
anyhow::ensure!(
st.success(),
"git {} failed in {}",
args.join(" "),
dir.display()
);
Ok(())
};
git(0, &["init", "-q"])?;
// FLAT layout on purpose. `scan_repo`'s root-commit branch iterates
// `tree.iter()` and records `entry.filename()` — the TOP-LEVEL entry names,
// without recursing — so a nested `src/f0000.rs` would have its seed commit
// credited to the directory entry `src` instead of to the file. Keeping the
// fixture flat means the arm's exact commit-touch identity is a statement
// about the rev-walk, not about that quirk. (The quirk itself is real and is
// reported separately; an arm should not be permanently red for it.)
let name = |i: usize| format!("f{i:04}.rs");
for i in 0..files {
let p = dir.join(name(i));
std::fs::write(&p, format!("pub fn f{i}() -> u32 {{ {i} }}\n"))?;
}
let all: Vec<String> = (0..files).map(name).collect();
let mut add: Vec<&str> = vec!["add", "--"];
add.extend(all.iter().map(|s| s.as_str()));
git(0, &add)?;
git(0, &["commit", "-q", "-m", "seed"])?;
for c in 0..commits {
let touched: Vec<String> = (0..3).map(|k| name((c * 7 + k * 13) % files)).collect();
for (k, rel) in touched.iter().enumerate() {
let p = dir.join(rel);
let body = std::fs::read_to_string(&p).unwrap_or_default();
std::fs::write(&p, format!("{body}// edit {c}.{k}\n"))?;
}
let mut a: Vec<&str> = vec!["add", "--"];
a.extend(touched.iter().map(|s| s.as_str()));
git(c, &a)?;
git(c, &["commit", "-q", "-m", "edit"])?;
}
Ok(())
}
/// POPULATE — `knowledge::git_heat::scan_repo`, the gix rev-walk + per-commit
/// first-parent tree diff that produces the per-file activity heat.
///
/// It is **half of `knowledge::scan_all`** and had no arm at all, so nothing
/// noticed its cost: the walk is `O(commits × tree)` by construction (the module
/// says so itself and defers batching), which means it is the half of `scan_all`
/// whose cost tracks HISTORY DEPTH rather than source size. Two sizes are
/// registered at the same file count so that dependence is visible.
///
/// ## Floors (`ensure!`, all exact and machine-independent)
/// * one row per file — a rev-walk that stops early loses rows;
/// * `Σ commits_total == files + 3·commits` — the fixture touches exactly three
/// files per commit after an initial commit that adds them all, so this is the
/// arithmetic identity a correct first-parent diff must satisfy. A diff that
/// silently drops renames/modes, or a walk that misses commits, breaks it;
/// * `commits_30d == commits_total` on every row — the fixture's commits are all
/// "now", so the recency buckets must contain all of them.
struct PopGitHeatScan {
id: &'static str,
files: usize,
commits: usize,
}
impl Bencher for PopGitHeatScan {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
let dir = tempfile::tempdir()?;
gap_git_heat_fixture(dir.path(), self.files, self.commits)?;
let ts = chrono::Utc::now();
// Warm the page cache / object store, then time the real scan.
let _ =
crate::knowledge::git_heat::scan_repo(dir.path(), "bench", uuid::Uuid::new_v4(), ts)?;
let iters = 3usize;
let mut per_iter = Vec::with_capacity(iters);
let mut total = std::time::Duration::ZERO;
let mut scan = None;
for _ in 0..iters {
let t = std::time::Instant::now();
let s = crate::knowledge::git_heat::scan_repo(
dir.path(),
"bench",
uuid::Uuid::new_v4(),
ts,
)?;
let el = t.elapsed();
total += el;
per_iter.push(el.as_secs_f64() * 1e6);
scan = Some(s);
}
let scan = scan.expect("at least one timed iteration");
anyhow::ensure!(
scan.files.len() == self.files,
"git-heat produced {} rows for a {}-file repo — the rev-walk lost files",
scan.files.len(),
self.files
);
let touches: i64 = scan.files.iter().map(|f| f.commits_total).sum();
let expected = self.files as i64 + 3 * self.commits as i64;
anyhow::ensure!(
touches == expected,
"git-heat aggregated {touches} commit-touches, expected {expected} \
({} files seeded + 3 per each of {} commits) — the first-parent diff is \
dropping or double-counting changes",
self.files,
self.commits
);
anyhow::ensure!(
scan.files
.iter()
.all(|f| f.commits_30d == f.commits_total && f.authors_total >= 1),
"every fixture commit is 'now' and authored, so 30d buckets must equal the \
totals and every file must name at least one author"
);
let commits_walked = (self.commits + 1) as u64;
let mut m = serde_json::Map::new();
m.insert("files".into(), serde_json::json!(self.files));
m.insert("commits".into(), serde_json::json!(commits_walked));
m.insert("rows".into(), serde_json::json!(scan.files.len()));
m.insert("commit_touches".into(), serde_json::json!(touches));
m.insert(
"commits_per_sec".into(),
serde_json::json!(
(commits_walked * iters as u64) as f64 / total.as_secs_f64().max(1e-9)
),
);
record(
&mut m,
"rows_per_sec",
(scan.files.len() * iters) as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ═══════════ POPULATE + SEARCH: tantivy BM25 (was: 0 arms) ══════════════════
/// The lexical-search fixture: `docs` small Rust files, each carrying the shared
/// token `nornir` plus a **unique, single-token** marker `qzmarkerNNNNN`.
///
/// The marker is deliberately alphanumeric-only: tantivy's default tokenizer
/// splits on non-alphanumerics, so `qz_marker_00042` would become three terms and
/// the "exactly one hit" floor below would be a statement about the query parser
/// rather than about the index.
fn gap_tantivy_corpus(ws: &std::path::Path, docs: usize) -> Result<Vec<std::path::PathBuf>> {
let mut paths = Vec::with_capacity(docs);
for i in 0..docs {
let f = ws.join(format!("doc_{i:05}.rs"));
std::fs::write(
&f,
format!(
"//! module {i} of the nornir lexical bench corpus\n\
// marker qzmarker{i:05}\n\
pub fn work_{i}(input: u32) -> u32 {{\n \
let scaled = input.wrapping_mul({});\n \
scaled ^ {}\n}}\n",
i as u32 + 7,
i as u32 * 31
),
)?;
paths.push(f);
}
Ok(paths)
}
/// POPULATE — tantivy index BUILD (`Index::index_files`): the tokenize + posting
/// + commit path behind `nornir index build` and the docs index.
///
/// Floor (`ensure!`): the build must ADD every document — `added == docs`, with
/// zero errors. A writer that silently skipped documents (oversize guard
/// mis-tuned, upsert keying broken) would still "succeed" and still produce a
/// throughput number; this is what makes that case red.
struct PopTantivyBuild {
id: &'static str,
docs: usize,
}
impl Bencher for PopTantivyBuild {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::index::{Corpus, Index};
let ws = tempfile::tempdir()?;
let paths = gap_tantivy_corpus(ws.path(), self.docs)?;
let idx_dir = tempfile::tempdir()?;
let index = Index::open_at(ws.path(), idx_dir.path())?;
let t = std::time::Instant::now();
let stats = index.index_files("bench", Corpus::Code, &paths)?;
let el = t.elapsed();
anyhow::ensure!(
stats.added == self.docs && stats.errors == 0,
"tantivy build added {} of {} documents ({} errors) — the index is not \
holding the corpus it was handed",
stats.added,
self.docs,
stats.errors
);
let mut m = serde_json::Map::new();
m.insert("docs".into(), serde_json::json!(self.docs));
m.insert("added".into(), serde_json::json!(stats.added));
m.insert(
"skipped_too_large".into(),
serde_json::json!(stats.skipped_too_large),
);
record(
&mut m,
"docs_per_sec",
self.docs as u64,
vec![el.as_secs_f64() * 1e6],
el,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// SEARCH — tantivy BM25 query latency (`Index::search`), the lexical half of the
/// hybrid retrieval the MCP `search` tool serves.
///
/// This is the arm the suite did not have. `nornir.mimir.search.hybrid_rrf_*`
/// looks like it covers it but does not: **both** of its queries (dense and
/// BM25) run OUTSIDE its timed loop, whose body is only `fuse_bm25_dense` — it
/// measures the fusion, and its recall floors are about the fusion too.
///
/// ## Floor (`ensure!`, machine-independent)
/// Every probe queries a marker token planted in exactly ONE document, so the
/// index must return **exactly one hit, and it must be that document**. A
/// tokenizer change, a lost field, or a scoped-filter regression turns this red
/// instead of quietly returning a fast, empty result set — which is the failure a
/// pure latency arm cannot see.
struct SearchTantivyBm25 {
id: &'static str,
docs: usize,
queries: usize,
}
impl Bencher for SearchTantivyBm25 {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
use crate::index::{Corpus, Index};
let ws = tempfile::tempdir()?;
let paths = gap_tantivy_corpus(ws.path(), self.docs)?;
let idx_dir = tempfile::tempdir()?;
let index = Index::open_at(ws.path(), idx_dir.path())?;
index.index_files("bench", Corpus::Code, &paths)?;
// Probe documents spread across the corpus (not the first N).
let probes: Vec<usize> = (0..self.queries)
.map(|q| (q * 2_654_435_761) % self.docs)
.collect();
for &d in probes.iter().take(4) {
let _ = index.search(
&format!("qzmarker{d:05}"),
Some(Corpus::Code),
Some("bench"),
10,
)?;
}
let mut per_iter = Vec::with_capacity(probes.len());
let mut hits_total = 0usize;
let t0 = std::time::Instant::now();
for &d in &probes {
let q = format!("qzmarker{d:05}");
let t = std::time::Instant::now();
let hits = index.search(&q, Some(Corpus::Code), Some("bench"), 10)?;
per_iter.push(t.elapsed().as_secs_f64() * 1e6);
anyhow::ensure!(
hits.len() == 1 && hits[0].path == format!("doc_{d:05}.rs"),
"BM25 probe `{q}` returned {} hit(s) {:?} — the marker is planted in \
exactly one document, so anything else means the index or the query \
path lost it",
hits.len(),
hits.iter().map(|h| h.path.clone()).collect::<Vec<_>>()
);
hits_total += hits.len();
}
let total = t0.elapsed();
// The corpus-wide token must reach the whole corpus — a `limit`-sized
// page proves the postings list is not truncated to the probe documents.
let broad = index.search("nornir", Some(Corpus::Code), Some("bench"), 25)?;
anyhow::ensure!(
broad.len() == 25.min(self.docs),
"the shared token `nornir` returned {} of an expected {} hits",
broad.len(),
25.min(self.docs)
);
let mut m = serde_json::Map::new();
m.insert("docs".into(), serde_json::json!(self.docs));
m.insert("hits".into(), serde_json::json!(hits_total));
record(
&mut m,
"queries_per_sec",
probes.len() as u64,
per_iter,
total,
);
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
// ─────────────── REGISTRATIONS for the appended region ──────────────────────
// ── ANALYZE: `mimir analyze duplicates` at the LIVE embedding width (768) ──
//
// Sizes chosen so the class is readable: `comparisons_per_sec` should be flat
// across the five, while `symbols_per_sec` falls ~linearly. 4k×768 is the size
// the only existing guard (the `analyze_duplicates_completes_at_scale` unit test,
// 60 s ceiling) uses, so the bench brackets it on both sides; the real warehouse
// is 79k×768, which is 100× the pair count of the 8k arm and is left out
// deliberately — a single arm that takes minutes is not a bench, and the flat
// `comparisons_per_sec` column is what extrapolates to it.
crate::register_bench!(AnalyzeDuplicates {
id: "nornir.mimir.analyze.duplicates_1k_d768",
symbols: 1_000,
dim: 768,
planted: 8,
iters: 3
});
crate::register_bench!(AnalyzeDuplicates {
id: "nornir.mimir.analyze.duplicates_2k_d768",
symbols: 2_000,
dim: 768,
planted: 8,
iters: 3
});
crate::register_bench!(AnalyzeDuplicates {
id: "nornir.mimir.analyze.duplicates_4k_d768",
symbols: 4_000,
dim: 768,
planted: 8,
iters: 2
});
crate::register_bench!(AnalyzeDuplicates {
id: "nornir.mimir.analyze.duplicates_8k_d768",
symbols: 8_000,
dim: 768,
planted: 8,
iters: 2
});
crate::register_bench!(AnalyzeDuplicates {
id: "nornir.mimir.analyze.duplicates_16k_d768",
symbols: 16_000,
dim: 768,
planted: 8,
iters: 1
});
// ── SEARCH: the Ragnar batch-width (P) sweep ──
//
// 1M keys ≈ the index still fits in cache-friendly reach; 10M keys is where the
// dependent-load stall the batch API exists to hide actually dominates.
//
// ## MEASURED — the sweep result the floors below come from
//
// Threadripper PRO 3975WX, 32 cores, 499 GB RAM, `--release`, 32 768 DISTINCT
// probes, best of 7 interleaved reps, machine CONTENDED (load average 9–27
// throughout; several agents compiling). Standalone harness linking the same
// `znippy_zoomies::stree_log::RagnarLog` — a full release build of nornir was not
// affordable on this disk, so the arms below carry that harness's numbers as
// their floors with a 2× margin, not as their expected values.
//
// | P | 1M keys M/s (best/worst) | 10M keys M/s (best/worst) | 10M ×scalar |
// |----|--------------------------|---------------------------|-------------|
// | - | scalar 7.12 / 3.94 | scalar 3.20 / 3.09 | 1.00 |
// | 1 | 7.15 / 4.52 | 3.15 / 3.00 | 0.98 |
// | 4 | 14.47 / 11.42 | 6.43 / 6.02 | 2.01 |
// | 8 | **14.59 / 11.87** | **6.63 / 6.43** | **2.07** |
// | 16 | 12.44 / 9.98 | 6.02 / 5.75 | 1.88 |
// | 20 | 11.89 / 9.03 | 5.64 / 5.49 | 1.76 |
// | 32 | 12.55 / 9.42 | 5.84 / 5.35 | 1.82 |
// | 64 | 14.29 / 6.82 | 6.23 / 5.35 | 1.94 |
//
// **The plateau is P = 8, at both sizes** — not ~20. P=4 is within 3 % of it;
// P=16–32 is 9–15 % WORSE than P=8 and P=20 is the worst point of the whole
// sweep above 1. The dip-then-partial-recovery shape (worst at 20, better again
// at 64) is reproducible across all 7 reps at 10M, where best and worst are
// within 3 % of each other, so it is not contention noise. The likely cause is
// register pressure: `floor_index_batch` keeps `[usize; P]` + `[u32; P]` live
// across the descent, which fits the register file at P≤8 and spills beyond it —
// stated as the plausible mechanism, not as a measured one (no PMU counters were
// taken).
//
// P=1 is at or BELOW the scalar descent (0.98–1.00×): the pipeline's prefetch and
// lane bookkeeping cost something and there is nothing to overlap it with. That
// is the control that proves the 2× at P=8 is the overlap and not the rewrite.
//
// ### A caveat about the 8.1 M/s the EXISTING scalar arms report at 10M keys
// It is ~2.2× optimistic. Those arms replay ONE 1024-probe set 32 times, so after
// the first pass the touched tree paths are cache-resident. Measured here on the
// same box: 1024×32 replay → 7.14 M/s at 10M keys; 32 768 distinct probes →
// 3.32 M/s. The batch arms below therefore use distinct probes, and their
// `baseline_lookups_per_sec` column is NOT comparable to the older arms'.
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_1m_p1",
keys: 1_000_000,
p: 1,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 2.2e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_1m_p4",
keys: 1_000_000,
p: 4,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 5.7e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_1m_p8",
keys: 1_000_000,
p: 8,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 5.9e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_1m_p16",
keys: 1_000_000,
p: 16,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 4.9e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_1m_p20",
keys: 1_000_000,
p: 20,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 4.5e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_1m_p32",
keys: 1_000_000,
p: 32,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 4.7e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_1m_p64",
keys: 1_000_000,
p: 64,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 3.4e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_10m_p1",
keys: 10_000_000,
p: 1,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 1.5e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_10m_p4",
keys: 10_000_000,
p: 4,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 3.0e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_10m_p8",
keys: 10_000_000,
p: 8,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 3.2e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_10m_p16",
keys: 10_000_000,
p: 16,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 2.8e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_10m_p20",
keys: 10_000_000,
p: 20,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 2.7e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_10m_p32",
keys: 10_000_000,
p: 32,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 2.6e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_10m_p64",
keys: 10_000_000,
p: 64,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 2.6e6
});
// The other half of the new batch API, at P=1 (the un-pipelined control) and at
// P=8 — the width the sweep above PICKED. Measured the same way and in the same
// run: 1M P=1 7.75/4.60 M/s, P=8 15.85/12.32 M/s; 10M P=1 3.17/3.03 M/s,
// P=8 6.79/6.44 M/s. Note P=1 is 15 % BELOW the scalar `boundary_leq` here, the
// same "pipeline with nothing to overlap" control as above.
crate::register_bench!(SearchRagnarBoundaryBatch {
id: "nornir.mimir.search.ragnar_boundary_batch_1m_p1",
keys: 1_000_000,
p: 1,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 2.3e6
});
crate::register_bench!(SearchRagnarBoundaryBatch {
id: "nornir.mimir.search.ragnar_boundary_batch_1m_p8",
keys: 1_000_000,
p: 8,
queries: 32_768,
iters: 16,
floor_lookups_per_sec: 6.1e6
});
crate::register_bench!(SearchRagnarBoundaryBatch {
id: "nornir.mimir.search.ragnar_boundary_batch_10m_p1",
keys: 10_000_000,
p: 1,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 1.5e6
});
crate::register_bench!(SearchRagnarBoundaryBatch {
id: "nornir.mimir.search.ragnar_boundary_batch_10m_p8",
keys: 10_000_000,
p: 8,
queries: 32_768,
iters: 8,
floor_lookups_per_sec: 3.2e6
});
// ── POPULATE: git-heat (same file count, 4× the history depth) ──
crate::register_bench!(PopGitHeatScan {
id: "nornir.mimir.populate.git_heat_100c",
files: 40,
commits: 100
});
crate::register_bench!(PopGitHeatScan {
id: "nornir.mimir.populate.git_heat_400c",
files: 40,
commits: 400
});
// ═══ POPULATE: per-member step PARALLELIZATION (sequential vs flattened gatling) ═══
//
// These arms measure the 2026-07-25 fix: populate steps that were written as a
// per-member `for` loop (one repo at a time, 31 idle cores) moved into ONE
// flattened znippy-zoomies gatling pool over the members. Each arm times the SAME
// work BOTH ways in one run and records `speedup = seq_ms / par_ms` — the honest
// before/after the fix is about. Correctness is asserted (parallel output MUST
// equal the sequential output); the speedup floor only bites on a genuinely
// multi-core host (`available_parallelism >= 4`) and NEVER during the single-core
// (`_st`) pass (where both passes share one core, so speedup is ~1 by design).
/// Time the step-1c git-heat scan across `members` repos SEQUENTIALLY (the old
/// per-member loop) vs in ONE flattened gatling pool (the fix). git-heat is a real
/// per-repo gix rev-walk + first-parent diff, so `members` independent walks are
/// exactly the kind of work the pool saturates every core with.
struct PopGitHeatMembersParallel {
id: &'static str,
members: usize,
files: usize,
commits: usize,
}
impl Bencher for PopGitHeatMembersParallel {
fn id(&self) -> &'static str {
self.id
}
fn run(&self) -> Result<BenchResult> {
let root = tempfile::tempdir()?;
let mut repos: Vec<(String, std::path::PathBuf)> = Vec::with_capacity(self.members);
for m in 0..self.members {
let d = root.path().join(format!("m{m}"));
std::fs::create_dir_all(&d)?;
gap_git_heat_fixture(&d, self.files, self.commits)?;
repos.push((format!("m{m}"), d));
}
let now = chrono::Utc::now();
let scan = |name: &str, root: &std::path::Path| {
crate::knowledge::git_heat::scan_repo(root, name, uuid::Uuid::new_v4(), now)
};
// Warm the object store / page cache once (parallel) so neither timed pass
// pays the cold-read tax the other avoids.
let _warm: Vec<_> = znippy_zoomies::gatling_forkjoin::gatling_map_balanced(
&repos,
0,
1,
|_| 1u64,
|_, (n, p)| scan(n, p),
);
// SEQUENTIAL — the old one-repo-at-a-time loop.
let t = std::time::Instant::now();
let seq: Vec<_> = repos.iter().map(|(n, p)| scan(n, p)).collect();
let seq_ms = t.elapsed().as_secs_f64() * 1000.0;
// PARALLEL — the flattened gatling pool the fix uses.
let t = std::time::Instant::now();
let par: Vec<_> = znippy_zoomies::gatling_forkjoin::gatling_map_balanced(
&repos,
0,
1,
|_| 1u64,
|_, (n, p)| scan(n, p),
);
let par_ms = t.elapsed().as_secs_f64() * 1000.0;
// CORRECTNESS: the pool must produce byte-identical per-member row counts.
let mut total_rows = 0usize;
for (s, p) in seq.iter().zip(par.iter()) {
let s = s
.as_ref()
.map_err(|e| anyhow::anyhow!("seq git-heat: {e:#}"))?;
let p = p
.as_ref()
.map_err(|e| anyhow::anyhow!("par git-heat: {e:#}"))?;
anyhow::ensure!(
s.files.len() == p.files.len(),
"parallel git-heat produced {} rows, sequential {} — not identical",
p.files.len(),
s.files.len()
);
total_rows += p.files.len();
}
let speedup = if par_ms > 0.0 { seq_ms / par_ms } else { 0.0 };
speedup_ok(speedup, self.members)?;
let mut m = serde_json::Map::new();
m.insert("members".into(), serde_json::json!(self.members));
m.insert("rows".into(), serde_json::json!(total_rows));
m.insert("seq_ms".into(), serde_json::json!(seq_ms));
m.insert("par_ms".into(), serde_json::json!(par_ms));
m.insert("speedup".into(), serde_json::json!(speedup));
m.insert("cores".into(), serde_json::json!(cores_now()));
Ok(BenchResult {
name: self.id.into(),
metrics: m,
..Default::default()
})
}
}
/// Machine core count for the bench envelope's `cores` metric.
fn cores_now() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
/// RED-when-broken speedup floor for a parallelization arm — but ONLY where a
/// speedup is actually expected: skip it during the single-core (`_st`) pass (both
/// timed passes share one pinned core, so speedup is ~1 by design) and on hosts
/// with fewer than 4 cores (a 2-core CI can't show a 4× fan). Where it does apply,
/// a step that silently reverted to a serial loop (speedup ≈ 1) turns the arm RED.
fn speedup_ok(speedup: f64, members: usize) -> Result<()> {
let cores = cores_now();
if crate::bench::api::pinned_to_single_core() || cores < 4 || members < 2 {
return Ok(());
}
anyhow::ensure!(
speedup > 1.2,
"parallelized populate step showed NO speedup (seq/par = {speedup:.2}× on {cores} cores) \
— the step likely reverted to a serial per-member loop"
);
Ok(())
}
// ── POPULATE: per-member step parallelization (seq vs flattened gatling) ──
crate::register_bench!(PopGitHeatMembersParallel {
id: "nornir.mimir.populate.git_heat_members_par",
members: 16,
files: 40,
commits: 60
});
// ── POPULATE + SEARCH: tantivy BM25 ──
crate::register_bench!(PopTantivyBuild {
id: "nornir.mimir.populate.tantivy_build_500",
docs: 500
});
crate::register_bench!(PopTantivyBuild {
id: "nornir.mimir.populate.tantivy_build_2k",
docs: 2_000
});
crate::register_bench!(SearchTantivyBm25 {
id: "nornir.mimir.search.tantivy_bm25_500",
docs: 500,
queries: 200
});
crate::register_bench!(SearchTantivyBm25 {
id: "nornir.mimir.search.tantivy_bm25_2k",
docs: 2_000,
queries: 200
});
// ── The 768-dim embedding arms ──
//
// Every embedding arm in the suite above runs at **dim 32** while the live model
// (`jina-v2-base-code`) emits **768**. A 24× narrower row changes which resource
// the kernel is bound by — 32 floats is one cache line, 768 is twelve — so those
// arms cannot be read as statements about the shipped path. These re-register the
// SAME benchers at the live width; the dim-32 ones stay as the narrow-row
// control. Row counts are trimmed where 768 floats × rows would otherwise make
// the fixture the dominant cost.
#[cfg(feature = "vector")]
mod vec_registrations_dim768 {
use super::vec_benches::*;
crate::register_bench!(PopEmbeddingIndex {
id: "nornir.mimir.populate.embeddings_1k_d768",
chunks: 1_000,
dim: 768
});
crate::register_bench!(PopEmbeddingIndex {
id: "nornir.mimir.populate.embeddings_5k_d768",
chunks: 5_000,
dim: 768
});
crate::register_bench!(PopEmbeddingIndex {
id: "nornir.mimir.populate.embeddings_20k_d768",
chunks: 20_000,
dim: 768
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_1k_d768",
rows: 1_000,
dim: 768
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_10k_d768",
rows: 10_000,
dim: 768
});
crate::register_bench!(PopVannBuild {
id: "nornir.mimir.populate.vann_build_100k_d768",
rows: 100_000,
dim: 768
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_k10_d768",
rows: 20_000,
dim: 768,
queries: 64,
k: 10
});
crate::register_bench!(SearchVectorKnnSingle {
id: "nornir.mimir.search.vec_knn_k50_d768",
rows: 20_000,
dim: 768,
queries: 64,
k: 50
});
}
#[cfg(test)]
mod gap_arm_tests {
use super::*;
use crate::bench::api::Bencher;
/// RED-WHEN-BROKEN: the duplicate-analysis arm must actually FIND the copies
/// it planted. This runs the smallest registered shape, so a regression in
/// `candidate_pairs` / `pairs_above` fails in `cargo test` and not only when
/// somebody runs the bench suite.
#[test]
fn analyze_duplicates_arm_finds_its_planted_clusters() {
let out = AnalyzeDuplicates {
id: "nornir.mimir.analyze.duplicates_test",
symbols: 400,
dim: 768,
planted: 4,
iters: 1,
}
.run()
.expect("the arm's own ensure! floors must hold");
assert_eq!(
out.metrics.get("clusters").and_then(|v| v.as_u64()),
Some(4)
);
assert!(
out.metrics
.get("pairs")
.and_then(|v| v.as_u64())
.unwrap_or(0)
>= 12
);
assert!(
out.metrics
.get("comparisons_per_sec")
.and_then(|v| v.as_f64())
.unwrap_or(0.0)
> 0.0
);
}
/// The batch floor descent must agree with the scalar one at EVERY registered
/// width. This is the arm's own `ensure!` run over the whole sweep at a size
/// small enough for a unit test — so "a faster wrong answer" cannot reach the
/// bench suite unnoticed.
#[test]
fn ragnar_batch_widths_all_agree_with_the_scalar_descent() {
for p in [1usize, 4, 8, 16, 20, 32, 64] {
SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_test",
keys: 50_000,
p,
queries: 4_096,
iters: 1,
floor_lookups_per_sec: 0.0,
}
.run()
.unwrap_or_else(|e| panic!("as_of_batch::<{p}> arm failed: {e}"));
SearchRagnarBoundaryBatch {
id: "nornir.mimir.search.ragnar_boundary_batch_test",
keys: 50_000,
p,
queries: 4_096,
iters: 1,
floor_lookups_per_sec: 0.0,
}
.run()
.unwrap_or_else(|e| panic!("boundary_leq_batch::<{p}> arm failed: {e}"));
}
}
/// An unregistered width must ERROR, never silently fall back to some other
/// `P` — a sweep whose points quietly collapse onto one width measures one
/// thing and reports seven.
#[test]
fn an_unswept_batch_width_is_an_error_not_a_silent_fallback() {
let err = SearchRagnarAsOfBatch {
id: "nornir.mimir.search.ragnar_as_of_batch_test",
keys: 1_000,
p: 7,
queries: 64,
iters: 1,
floor_lookups_per_sec: 0.0,
}
.run()
.expect_err("P=7 is not a registered sweep point");
assert!(
err.to_string()
.contains("not one of the registered sweep points"),
"{err}"
);
}
/// RED-WHEN-BROKEN: the git-heat arm's exact commit-touch identity. The
/// fixture is tiny here; the point is that the arithmetic floor holds against
/// the real `scan_repo`, not that it is fast.
The file goes on; the console shows all of it.