crates/storage-git-gix/examples/odb-miss-ab.rs
//! **Does the lifted `OdbPool` actually make a miss faster on THIS box?**
//!
//! The quarantined `src/odb.rs` claims a miss got **5.6× faster at one pack**
//! and 17.9× at 64, MEASURED on **oden** on 2026-08-03. `locked_on_target.md`
//! §7 forbids comparing across machines, so those numbers cannot be carried to
//! t14s and cannot be quoted as this crate's. This probe re-takes the A/B here.
//!
//! # The two arms, and what separates them
//!
//! | arm | what it is |
//! |---|---|
//! | `bare` | **one** `gix_odb::Handle`, default `RefreshMode::AfterAllIndicesLoaded` — exactly what `GixStore` held before this session |
//! | `pool` | [`storage_git_gix::odb::OdbPool`] — `refresh_never()`, pooled handles, delta base cache |
//!
//! Both answer identically, which is the trap: nothing in the *answers* can tell
//! them apart, so a pool that was never consulted would look like a null result.
//! Two things are asserted rather than assumed —
//!
//! 1. every arm's answer set is compared, oid by oid, and a disagreement is a
//! hard failure;
//! 2. the miss count is printed, so an arm that "went fast" by finding
//! everything present is visible.
//!
//! # Prefer the count over the clock
//!
//! `locked_on_target.md` §6. The clock here is `misses/sec`, and it moves with
//! the box. The **count** is `syscalls`: run this under
//! `strace -f -e trace=newfstatat,statx,openat -c` and the arms separate by an
//! order of magnitude that no load average can forge. The cause of the whole
//! difference is that `AfterAllIndicesLoaded` re-scans `objects/` from disk,
//! behind the store's global write lock, **on every single miss**.
//!
//! # Usage
//!
//! ```text
//! odb-miss-ab <repo.git> [queries] [reps]
//! ```
//!
//! `<repo.git>` is any bare repository — the probe never writes to it.
use std::path::PathBuf;
use std::time::Instant;
use gix_hash::ObjectId;
use storage_git_gix::odb::OdbPool;
type Res<T> = Result<T, Box<dyn std::error::Error>>;
/// Deterministic absent oids.
///
/// A fixed seed, so two runs of this probe query the same ids: a random query
/// set makes the arms' inputs differ by run and turns a 2% difference into
/// noise nobody can attribute.
fn absent_oids(kind: gix_hash::Kind, n: usize) -> Vec<ObjectId> {
let mut state: u64 = 0x2545_F491_4F6C_DD1D;
let width = kind.len_in_bytes();
(0..n)
.map(|_| {
let mut bytes = vec![0u8; width];
for chunk in bytes.chunks_mut(8) {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let r = state.to_le_bytes();
chunk.copy_from_slice(&r[..chunk.len()]);
}
ObjectId::try_from(bytes.as_slice()).expect("the repository's hash width")
})
.collect()
}
fn hash_kind(repo: &std::path::Path) -> gix_hash::Kind {
let config = std::fs::read_to_string(repo.join("config")).unwrap_or_default();
if config.to_ascii_lowercase().contains("sha256") {
gix_hash::Kind::Sha256
} else {
gix_hash::Kind::Sha1
}
}
fn loadavg() -> String {
std::fs::read_to_string("/proc/loadavg")
.map(|s| s.split_whitespace().take(3).collect::<Vec<_>>().join(" "))
.unwrap_or_else(|_| "?".into())
}
fn packs_in(repo: &std::path::Path) -> usize {
std::fs::read_dir(repo.join("objects").join("pack"))
.map(|d| {
d.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
.count()
})
.unwrap_or(0)
}
fn main() -> Res<()> {
let mut args = std::env::args().skip(1);
let repo = PathBuf::from(args.next().ok_or("usage: odb-miss-ab <repo.git> [queries] [reps]")?);
let queries: usize = args.next().map_or(20_000, |s| s.parse().unwrap_or(20_000));
let reps: usize = args.next().map_or(5, |s| s.parse().unwrap_or(5));
// Which arms to run. `bare` or `pool` alone exists so a syscall count can be
// attributed: `strace -c` totals a process, and two arms in one process give
// one number for both. See the module doc on preferring the count.
let only = args.next().unwrap_or_else(|| "both".into());
let run_bare = only != "pool";
let run_pool = only != "bare";
let kind = hash_kind(&repo);
let objects_dir = repo.join("objects");
let ids = absent_oids(kind, queries);
eprintln!(
"odb-miss-ab: {} — {} pack(s), {kind:?}, {queries} queries, best of {reps}, loadavg {}",
repo.display(),
packs_in(&repo),
loadavg()
);
let open_store = || -> Res<std::sync::Arc<gix_odb::Store>> {
Ok(std::sync::Arc::new(gix_odb::Store::at_opts(
objects_dir.clone(),
kind,
&mut std::iter::empty(),
gix_odb::store::init::Options::default(),
)?))
};
// ── arm `bare`: one default handle, the pre-lift shape ───────────────────
use gix_object::Exists as _;
let bare = gix_odb::Cache::from(open_store()?.to_handle_arc());
let mut bare_found = 0usize;
let mut bare_best = 0.0f64;
// Warmup, then best-of-N. §6: "warmups and best-of-N always."
for rep in (0..=reps).take_while(|_| run_bare) {
let t = Instant::now();
let mut found = 0usize;
for id in &ids {
if bare.exists(id) {
found += 1;
}
}
let rate = queries as f64 / t.elapsed().as_secs_f64();
if rep > 0 {
bare_best = bare_best.max(rate);
bare_found = found;
}
}
// ── arm `pool`: the lifted OdbPool ───────────────────────────────────────
let pool = OdbPool::new(open_store()?, &repo, kind);
let mut pool_found = 0usize;
let mut pool_best = 0.0f64;
for rep in (0..=reps).take_while(|_| run_pool) {
let t = Instant::now();
let mut found = 0usize;
for id in &ids {
if pool.lookup_without_rescan(|h| h.exists(id), |b: &bool| *b) {
found += 1;
}
}
let rate = queries as f64 / t.elapsed().as_secs_f64();
if rep > 0 {
pool_best = pool_best.max(rate);
pool_found = found;
}
}
// The identity trap, closed. Both arms are *defined* to answer the same, so
// the only way a difference is meaningful is if the answers really did match.
if run_bare && run_pool && bare_found != pool_found {
return Err(format!(
"the arms disagreed: bare found {bare_found}, pool found {pool_found}. \
A speed difference between two arms that answer differently is not a speed \
difference."
)
.into());
}
if bare_found != 0 || pool_found != 0 {
return Err(format!(
"{bare_found} of {queries} 'absent' oids were present; this is not a miss benchmark"
)
.into());
}
let (probes, rescans) = pool.rescan_counts();
if run_bare {
println!("arm=bare misses_per_sec={bare_best:.0}");
}
if run_pool {
println!("arm=pool misses_per_sec={pool_best:.0}");
}
if run_bare && run_pool {
println!("pool_over_bare_x={:.2}", pool_best / bare_best);
}
// The count, which does not move with load: `lookup_without_rescan` reaches
// no `statx` at all, so this must be 0. If it is not, the arm is not the arm
// it says it is.
println!("pool_rescan_probes={probes} pool_rescans={rescans}");
// **The `P-012` count.** How many separately-searched index structures a
// miss walks: one per `.idx`, or exactly 1 when a `multi-pack-index` folds
// them (see `storage_git_gix::midx`). A clock says how fast this run was;
// this says which shape was measured, and it does not move with load. A
// `midx=1` row and a `midx=0` row at the same pack count are the A/B.
println!(
"pool_index_slots={} packs={} midx={}",
pool.index_slots(),
packs_in(&repo),
u8::from(
repo.join("objects")
.join("pack")
.join("multi-pack-index")
.is_file()
)
);
println!("loadavg={}", loadavg());
Ok(())
}