Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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(())
}