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

nordisk/gunnar

bench/examples/nornir-bench.rs

//! gunnar's nornir-bench suite: the benchmark harness this repository did not
//! have, built to the spec that came out of the profiling review.
//!
//! One `BenchRun` JSON line on stdout, one `impl Bencher` per arm, ids named
//! `gunnar.<scenario>`. Metric suffixes are the type system the gate reads:
//! `_mbs` / `_per_sec` / `ops_sec` are higher-is-better, `_ms` / `_us` / `p50` /
//! `p99` are lower-is-better, `_x` / `_pct` are neutral.
//!
//! # Every arm carries its red
//!
//! A benchmark that only produces a number cannot tell *slow* from *broken*. So
//! each arm below documents (and, where it can be checked in-process, **asserts**)
//! the applied-output condition that means the thing is broken. Several arms fail
//! outright rather than reporting a fast, meaningless number:
//!
//! | arm | the assertion that makes a fake impossible |
//! |---|---|
//! | `push_large` | `parallel_speedup_x`: identical serial and parallel index throughput means the thread limit did nothing (`gix-pack`'s `parallel` feature is not declared, so the build silently takes the serial path) |
//! | `walk_1m` | commit and tree reads must be ≥ their counts (a stub cannot read a million objects) **and** `commit_reads_per_commit` must be ≤ 1 (the closure walk is the serial bottleneck; it was 2.000 until 2026-08-03) |
//! | `lookup_1m` | hit and miss measured **separately**; a suite that only measures hits cannot see that object lookup is O(packs) on a miss |
//! | `negotiate_divergent` | `rounds` must be > 1, or the fixture is not divergent and the arm is measuring the happy path |
//! | `constellation` | the server must be the **same live process** at the end, checked by `/proc` start time — an OOM otherwise reads as a client fault; and a corpus smaller than the whole membership has to be asked for by name |
//! | `import_export` | the judge is a stock-git mirror clone of the import, not the importer's store: `fast-import` leaves objects no ref reaches, 705 of 5 267 on the largest member |
//!
//! # Isolation
//!
//! Neither container nor VM, by design. The house isolation strategy is
//! same-machine comparison: `machine` is mandatory in the envelope and the gate
//! filters history to the same machine. The runner executes arms **sequentially on
//! the main thread**, so a VM would add variance and remove none. The one thing
//! that genuinely wants a container, a pinned stock `git` for interop, is not
//! built here, and that leaves stock-git interop unmeasured.
//!
//! # Fixtures
//!
//! Generated lazily, deterministically (a fixed LCG seed, so the same fixture is
//! the same bytes on any machine) and cached under `$GUNNAR_BENCH_FIXTURES`
//! (default `~/scratch/gunnar-bench-fixtures`, i.e. md1). Never
//! `/tmp`: that is RAM on this host.
//!
//! `NORNIR_BENCH_PREVIEW=1` shrinks every workload for the development loop. The
//! harness stamps such a run onto `<host>-preview`, so a preview number can never
//! land in or overwrite the real host's row.

/// The counting `#[global_allocator]`, behind `--features count-alloc`.
///
/// # Why it is a feature and not simply installed
///
/// A `#[global_allocator]` belongs to the whole executable, so switching one on
/// here puts a branch in front of every allocation in **every** arm — concurrent
/// runs' included, whose rows land in the same `bench_runs` table. Off, the
/// `#[cfg]` compiles away and this binary is byte-for-byte the one it was.
///
/// On, `gunnar.stage_*` gains its `alloc_mib_*` and `peak_pct_*` columns:
///
/// ```text
/// NORNIR_BENCH_FEATURES=count-alloc nornir bench run gunnar
/// ```
///
/// which is still `nornir bench run` and still persists. `gunnar_bench::stages`
/// detects at runtime whether this is linked and every stage arm emits
/// `alloc_measured` accordingly, so an absent allocator reads as "not measured"
/// rather than as a repository that allocates nothing.
#[cfg(feature = "count-alloc")]
#[global_allocator]
static COUNTING_ALLOC: gunnar_bench::stages::CountingAlloc = gunnar_bench::stages::CountingAlloc;

use std::io::{BufRead, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;

use anyhow::{bail, ensure, Context, Result};
use gix_hash::{Kind, ObjectId};
use gix_object::Data;
use gunnar_bench::packbuild::PackBuilder;
use gunnar_pack::{index_thin_pack, IndexOptions, IndexedPack, WriteOptions};
use gunnar_store::git_storage_trait::{
    Caps, Extent, GcReport, GitOps, GitServe, ObjType, Oid, PackStats, ReachSet, RefCas, RefRow,
    RefUpdate, Stored, TxId,
};
// One in-memory store, not two. `MemObjectStore` + `MemRefStore` were the
// `ObjectStore`/`RefStore` pair; `MemGitServe` is a single store that answers
// both halves of the contract, and a clone of it shares the same state.
use gunnar_store::mem::MemGitServe;
use gunnar_wire::receive_pack::AllowAll;
use gunnar_wire::{FetchRequest, ProtocolVersion, ReceivePack, RefStatus, UploadPack};
use nornir_bench::api::Bencher;
use nornir_bench::BenchResult;
use serde_json::{json, Map, Value};

/// The `gunnar.vs_forge_*` arms: gunnar against gitea and against Forgejo, all
/// three over smart HTTP, each in its own container booted through draupnir.
///
/// A submodule rather than another ten thousand lines in this file, and an
/// explicit `#[path]` because `mod` inside an example resolves relative to the
/// examples directory in a way that is easy to misread. `inventory` collects at
/// link time, so its `register_bench!` arms land in this binary's arm set
/// exactly as if they were written below.
#[path = "forge/mod.rs"]
mod forge;

/// `gunnar.fetkaos_monkey`: three fat repositories through a disposable gunnar
/// container, 100 000 seeded-random operations, and the container's own log
/// held to a list of lines it MUST say.
///
/// A submodule for the same reasons `forge` is one, and with the same explicit
/// `#[path]`.
#[path = "fetkaos/mod.rs"]
mod fetkaos;

/// `gunnar.multiuser_scaling`: the 102-user roster of assignment 1 driving a
/// documented op mix over token-authenticated smart HTTP against a pinned
/// real-repository corpus, asking one question — do operations scale with
/// user count. A submodule for the same reasons `forge` is one.
#[path = "multiuser/mod.rs"]
mod multiuser;

/// `gunnar.opstorm`: the whole-surface Operation Storm — a seeded interleave of
/// git transport AND the gRPC control plane AND single-node replication/
/// abdication against one running `gunnar serve`, a sibling to `fetkaos` (which
/// storms only the pack graph). A submodule for the same reasons `forge` is one,
/// and it reuses fetkaos's chaos harness (`output_within`/`OpOutcome`/`git_env`,
/// made `pub(super)` there) rather than twinning it (one implementation, not two).
#[path = "opstorm/mod.rs"]
mod opstorm;

// ───────────────────────────────────────────────────────────────────────────
// the one place that speaks both pack vocabularies
// ───────────────────────────────────────────────────────────────────────────

/// `gunnar_wire::IndexedPack`, short enough to sit in a closure signature next
/// to the `gunnar_pack::IndexedPack` this file already imports unqualified.
///
/// Both names are in scope here and they are **different types**; the alias
/// exists so the difference stays visible at every use rather than turning into
/// a one-character `IndexedPack` that reads like the other one.
type WireIndexedPack = gunnar_wire::IndexedPack;

/// Restate `gunnar-pack`'s index receipt as the wire vocabulary's.
///
/// `gunnar-wire` grew its own [`gunnar_wire::IndexedPack`] when the pack
/// vocabulary moved off `gix-pack` — one `pub struct` field spelled
/// `gix_pack::data::Version` was enough to link the whole pack pipeline into
/// builds whose point is that it is absent (see `gunnar-wire/src/pack.rs`).
///
/// The bench still indexes through `gunnar-pack` **on purpose**: it is the
/// outside oracle that disagrees when gunnar is wrong, and swapping it for the
/// engine under test would be marking your own homework. So the two shapes meet
/// here, in the single function that names both, rather than at each
/// [`PackIngest`](gunnar_wire::receive_pack::PackIngest) closure.
fn wire_indexed(indexed: IndexedPack) -> WireIndexedPack {
    gunnar_wire::IndexedPack {
        num_objects: indexed.num_objects,
        pack_id: indexed.pack_id,
        index_id: indexed.index_id,
        pack_version: match indexed.pack_version {
            gix_pack::data::Version::V3 => gunnar_wire::PackVersion::V3,
            // `V2` and anything a later `gix-pack` adds. The wire enum is two
            // variants wide by the same argument its own `from_u32` makes.
            _ => gunnar_wire::PackVersion::V2,
        },
        hash_kind: indexed.hash_kind,
        index_path: indexed.index_path,
        data_path: indexed.data_path,
        keep_path: indexed.keep_path,
        // `None`: `gunnar-pack`'s indexer writes a real `.pack`, so the file IS
        // the count and `measured_pack_bytes` falls through to stat'ing it.
        // Filling this in from the receipt would report the same number twice
        // and hide which path produced it.
        stored_bytes: None,
    }
}

/// A `gunnar-pack` indexing failure as the error the ingest contract returns.
///
/// `gunnar_wire::Error` has no `From<gunnar_pack::Error>` and should not grow
/// one: that impl is exactly the dependency edge the vocabulary move removed.
/// The oracle's failure is a storage-side failure from the protocol's point of
/// view, and `{:#}` keeps the source chain that says which part of the pack was
/// bad.
fn wire_ingest_err(e: gunnar_pack::Error) -> gunnar_wire::Error {
    gunnar_wire::Error::Store(gunnar_store::Error::backend(format!(
        "indexing the pushed pack failed: {e:#}"
    )))
}

/// The same shape for an ingest that never went near `gunnar-pack`: the engine
/// took the bytes itself. One spelling for both, so a caller cannot tell which
/// arm failed by the shape of the error (one implementation, not two).
fn wire_store_err(what: &str) -> gunnar_wire::Error {
    gunnar_wire::Error::Store(gunnar_store::Error::backend(what.to_string()))
}

// ───────────────────────────────────────────────────────────────────────────
// main: the filter, and the guard that makes a zero-arm run RED
// ───────────────────────────────────────────────────────────────────────────

/// The prefix every arm in this file carries. Also the filter `main()` installs.
const GUNNAR: &str = "gunnar.";

fn main() -> Result<()> {
    // ── the head-to-head re-execs, before anything else ────────────────────
    //
    // The `vs_git_*` arms compare gunnar against stock `git` by making git's own
    // `ext::` transport run a server on the far end of a pipe. That server is
    // THIS binary, re-executed. Three roles, dispatched here because they must
    // never touch the bench runner:
    //
    //  * `GUNNAR_BENCH_SERVE=<side>` is the meter. Spawns the real server as a
    //    child, pumps the pipe both ways counting bytes, and reaps it with
    //    `wait4` for CPU seconds and peak RSS. Identical for both sides, so the
    //    measurement apparatus cancels out of every ratio.
    //  * `GUNNAR_BENCH_SERVE_INNER` **was** gunnar's server itself: `gunnar-wire`
    //    over stdin/stdout, this binary re-executed, serving out of a store it
    //    constructed in-process. **Gone 2026-08-10.** That in-process store was
    //    the thing that made the seam comment below false: `bench/` claimed to
    //    know "a path, not a crate" while linking the old znippy engine crate and, through
    //    it, four znippy crates at cargo RESOLVE time. The role is kept only to
    //    refuse, by name, so that a driver still setting the variable is told
    //    what to set instead rather than serving nothing.
    //  * `GUNNAR_BENCH_H2H_DRIVER` runs the whole scenario INSIDE the pinned
    //    container, so both sides see the same `git` and the same kernel view.
    // `gunnar.fetkaos_monkey` stands in for `ssh` while it exports, so it can
    // judge the RAW bytes gunnar served rather than git's rewrite of them.
    // Dispatched here, before the runner, exactly like the h2h server roles.
    {
        let argv: Vec<String> = std::env::args().collect();
        if let Some(at) = argv.iter().position(|a| a == "--fetkaos-ssh-tee") {
            return fetkaos::ssh_tee(&argv[at + 1..]);
        }
    }
    if let Some(side) = std::env::var_os("GUNNAR_BENCH_SERVE") {
        return h2h_serve_meter(&side.to_string_lossy());
    }
    if std::env::var_os("GUNNAR_BENCH_SERVE_INNER").is_some() {
        return h2h_serve_gunnar();
    }
    if let Some(socket) = std::env::var_os("GUNNAR_BENCH_SERVE_DAEMON") {
        return h2h_serve_daemon(Path::new(&socket));
    }
    // The scale suite's server: the same shape, plus receive-pack and a live
    // count of sessions alive at once. See `scale_serve_daemon`.
    if let Some(base) = std::env::var_os("GUNNAR_BENCH_SCALE_DAEMON") {
        return scale_serve_daemon(Path::new(&base));
    }
    if let Some(scenario) = std::env::var_os("GUNNAR_BENCH_H2H_DRIVER") {
        return h2h_drive(&scenario.to_string_lossy());
    }
    // `GUNNAR_BENCH_H2H_CORPUS=<repo>` runs [`h2h_assert_production_corpus`] and
    // nothing else, so the guard that decides whether a head-to-head arm may run
    // can be **seen going red** without a container, a fixture build or an hour.
    //
    // It exists because that guard's whole subject is a file's absence, and a
    // guard about an absence is the easiest kind to write blind. Point it at a
    // `git clone --bare` carve and at the same directory after `git repack -adq`
    // and it must disagree with itself; if it does not, it is not measuring
    // anything (measured, not asserted).
    if let Some(repo) = std::env::var_os("GUNNAR_BENCH_H2H_CORPUS") {
        let repo = PathBuf::from(&repo);
        let shape = h2h_assert_production_corpus(&repo, "server", "corpus-check")?;
        println!(
            "server_packs={} server_bitmaps={} server_pack_mb={} server_loose={}",
            shape.packs,
            shape.bitmaps,
            mib(shape.pack_bytes / 1024.0),
            shape.loose
        );
        return Ok(());
    }
    // `gunnar.fetch_readside`'s workload: serve exactly one fetch and print the
    // tally. A child process rather than an in-process call, because the number
    // that matters is instructions retired and `perf stat` counts a process.
    if let Some(repo) = std::env::var_os("GUNNAR_BENCH_READSIDE") {
        println!(
            "{}",
            gunnar_bench::readside::probe(
                Path::new(&repo),
                "refs/heads/yesterday",
                "refs/heads/main",
            )?
        );
        return Ok(());
    }

    // `NORNIR_MACHINE` is what the whole same-machine comparison strategy keys
    // on: the gate filters bench history to one machine, and an empty machine is
    // rejected downstream. Default it to the host so a bare `cargo run` still
    // produces an attributable row.
    if std::env::var_os("NORNIR_MACHINE").is_none() {
        let host = std::fs::read_to_string("/etc/hostname")
            .map(|s| s.trim().to_string())
            .unwrap_or_default();
        if !host.is_empty() {
            // SAFETY: single-threaded, before any bench arm has started.
            unsafe { std::env::set_var("NORNIR_MACHINE", host) };
        }
    }

    // THE DAY-ONE TRAP. `inventory` collects registrations at LINK time,
    // so a bench binary runs every `register_bench!` in everything it links, not
    // just its own repo's. This happened for real on 2026-07-22: `nornir bench run
    // skade` executed 213 `nornir.mimir.*` arms and reached ZERO `skade.*` arms.
    //
    // This crate links `nornir-bench` (the leaf harness) rather than `nornir` (the
    // orchestrator, which registers its own arms), so today the linked set IS
    // gunnar's set. That is a property of the current dependency graph, not a
    // guarantee: one future dependency reintroduces the hazard silently. So the
    // filter is set unconditionally.
    if std::env::var_os("NORNIR_BENCH_ONLY").is_none() {
        // SAFETY: single-threaded, before any bench arm has started.
        unsafe { std::env::set_var("NORNIR_BENCH_ONLY", GUNNAR) };
    }

    // ── THE GUARD ──────────────────────────────────────────────────────────
    // An empty run is RED, never a silently green zero-arm success (suites that
    // self-skip and report green). `run_main_json` has its
    // own bail for "the filter matched nothing", but it only fires when something
    // WAS linked and got filtered away; a binary that linked no gunnar arms at all
    // would sail through it, print a results-free envelope, and exit 0, which is
    // exactly the shape of a clean pass.
    //
    // Proven RED, not assumed: `NORNIR_BENCH_ONLY=gunnar.nonexistent.` exits
    // non-zero with this message.
    let only: Vec<String> = std::env::var("NORNIR_BENCH_ONLY")
        .unwrap_or_default()
        .split(',')
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .collect();
    let linked = nornir_bench::api::registered_bench_ids();
    let selected: Vec<&String> = linked
        .iter()
        .filter(|id| only.is_empty() || only.iter().any(|p| id.starts_with(p.as_str())))
        .collect();
    let gunnar_arms = selected.iter().filter(|id| id.starts_with(GUNNAR)).count();
    if gunnar_arms == 0 {
        bail!(
            "RAGNARÖK: this run would execute {} arm(s) and NOT ONE of them is a `{GUNNAR}` arm. \
             A bench run that measures zero gunnar arms is RED, never a green zero-arm success \
             (the link-time day-one trap). NORNIR_BENCH_ONLY={:?}; linked arms: [{}]",
            selected.len(),
            only.join(","),
            linked.join(", ")
        );
    }
    // ── THE OTHER GUARD: WHICH TREE ────────────────────────────────────────
    //
    // `nornir bench run --config <p>` resolves the repository by NAME out of the
    // config's grandparent, so the tree it builds and benches is not necessarily
    // the tree the operator is standing in. It has taken the wrong one at least
    // three times; the performance record notes run `4b65a303`, where
    // "`nornir bench run gunnar` resolved the repository by name and built
    // `~/scratch/wt/gunnar`, not this branch."
    //
    // `CARGO_MANIFEST_DIR` is baked in at compile time and therefore names the
    // tree this binary really came from, whatever the runner believes. Printed
    // on every run, because a run that says which tree it measured cannot make
    // that mistake silently; and refused outright when the caller states an
    // expectation, because a run scoped to one branch's arms wants an abort, not
    // a footnote.
    //
    // The row carries it too, as `gunnar.capabilities.bench_tree_id`.
    let tree = bench_tree();
    eprintln!(
        "bench: source tree {} (id {})",
        tree.display(),
        bench_tree_id()
    );
    if let Ok(expected) = std::env::var("GUNNAR_BENCH_EXPECT_TREE") {
        let expected = PathBuf::from(&expected);
        let same = std::fs::canonicalize(&expected).ok() == std::fs::canonicalize(tree).ok();
        ensure!(
            same,
            "GUNNAR_BENCH_EXPECT_TREE={} but this binary was built from {}. A bench binary \
             carries its source tree in `CARGO_MANIFEST_DIR`, and `nornir bench run --config` \
             picks the repository by NAME out of the config's grandparent — so a run in one \
             worktree can measure another, and every column of it is a true measurement of \
             the wrong branch.",
            expected.display(),
            tree.display()
        );
    }

    eprintln!(
        "bench: {gunnar_arms} gunnar arm(s) selected out of {} linked{}",
        linked.len(),
        if preview() {
            " (PREVIEW: shrunk workload, machine stamped <host>-preview)"
        } else {
            ""
        }
    );

    nornir_bench::api::run_main_json()
}

// ───────────────────────────────────────────────────────────────────────────
// shared helpers
// ───────────────────────────────────────────────────────────────────────────

/// The repository's hash kind. Decision D11 is sha256, so that is what the suite
/// measures. Benchmarking sha1 would be benchmarking a configuration gunnar does
/// not create.
///
/// Spelled out rather than read from `gunnar_store::DEFAULT_HASH_KIND`, which
/// went with the packfile engine on 2026-08-10. The surviving constant of that
/// name is `gunnar_grpc::seam::DEFAULT_HASH_KIND`, and it is a *different type*
/// (the gRPC seam's own `HashKind` enum) belonging to a different layer —
/// reaching for it because the name matches would tie the hash this suite
/// measures to a wire protocol's default. D11 is the authority, so D11 is what
/// is written here.
const HASH: Kind = Kind::Sha256;

fn preview() -> bool {
    nornir_bench::api::preview_run()
}

/// Pick the full-size value, or the shrunk one under `NORNIR_BENCH_PREVIEW=1`.
fn scaled(full: usize, light: usize) -> usize {
    if preview() {
        light
    } else {
        full
    }
}

fn env_usize(key: &str, default: usize) -> usize {
    std::env::var(key)
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(default)
}

/// Where generated fixtures are cached. **md1**, never `/tmp`. `/tmp` is RAM on
/// the desktop, and a 1 GiB fixture there is a memory leak with a filename.
fn fixture_root() -> PathBuf {
    PathBuf::from(std::env::var("GUNNAR_BENCH_FIXTURES").unwrap_or_else(|_| {
        gunnar_bench::expand_home("~/scratch/gunnar-bench-fixtures")
            .to_string_lossy()
            .into_owned()
    }))
}

/// A deterministic byte source.
///
/// Deterministic so a fixture is the same bytes on every machine and every run;
/// otherwise `ratio_x` drifts with the weather and the deflate guard is worthless.
/// Semi-compressible on purpose: pure random deflates to 1.0× and pure zeros to
/// ~1000×, and neither says anything about a git object corpus.
struct Lcg(u64);

impl Lcg {
    fn new(seed: u64) -> Self {
        Lcg(seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1))
    }
    fn next_u64(&mut self) -> u64 {
        self.0 = self
            .0
            .wrapping_mul(6_364_136_223_846_793_005)
            .wrapping_add(1_442_695_040_888_963_407);
        self.0
    }
    /// `len` bytes of synthetic source text: lines drawn at random from a fixed
    /// pool, which is what makes it compress the way code compresses.
    ///
    /// Getting this right took two wrong answers, and both were wrong in a way
    /// that mattered rather than in a way that was merely untidy. `ratio_x` is
    /// the deflate-backend guard, and a guard on an unrepresentative corpus reports
    /// on the generator instead of on the codec:
    ///
    /// * **runs of up to 64 identical bytes → `ratio_x` 13.2×.** Not a git object
    ///   corpus; a compressibility that flatters every codec equally.
    /// * **runs of 1–2 bytes over a 66-symbol alphabet → `ratio_x` 1.35×.** That
    ///   is the *entropy bound*: Huffman coding of ~6-bit symbols and no LZ
    ///   matches at all. deflate's match finder, which is the half a backend swap
    ///   actually changes, was doing nothing.
    ///
    /// Repeated lines give the match finder real work, at a ratio in the range
    /// source trees actually sit in.
    fn text(&mut self, len: usize) -> Vec<u8> {
        let pool = line_pool();
        let mut out = Vec::with_capacity(len + 128);
        while out.len() < len {
            // HIGH bits. An LCG modulo 2^64 has period 2^k in its low k bits, so
            // `next_u64() % 128` is a *permutation cycle*: every line appears exactly
            // once per 128 draws and the text never repeats a line inside one object.
            // That is how a pool-of-lines generator managed to deflate at the
            // entropy bound (1.33×) while looking, in the source, like it obviously
            // would not.
            let line = &pool[((self.next_u64() >> 33) % pool.len() as u64) as usize];
            out.extend_from_slice(line);
        }
        out.truncate(len);
        out
    }
}

/// The fixed line pool every synthetic blob is assembled from.
///
/// Built once, deterministically, from its own LCG, so the corpus is identical
/// on every machine and every run, and `ratio_x` is comparable across hosts.
/// 128 lines of 16–80 bytes puts a 16 KiB object at **~3× deflate**, which is
/// where a real source tree sits. Measured, not guessed: 256 lines gives 2.2×,
/// 64 gives 5.3× and 16 gives 13×, and the corpus has to sit somewhere a codec
/// swap can move `ratio_x` in either direction.
fn line_pool() -> &'static [Vec<u8>] {
    static POOL: OnceLock<Vec<Vec<u8>>> = OnceLock::new();
    POOL.get_or_init(|| {
        const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        let mut rng = Lcg::new(0x11_4E_5);
        (0..128)
            .map(|_| {
                let len = 16 + (rng.next_u64() % 64) as usize;
                let mut line = Vec::with_capacity(len + 1);
                for _ in 0..len {
                    line.push(ALPHABET[(rng.next_u64() % ALPHABET.len() as u64) as usize]);
                }
                line.push(b'\n');
                line
            })
            .collect()
    })
}

/// A `Write` that counts and discards. Used where the measurement is the
/// producer's throughput, not the consumer's.
#[derive(Default)]
struct Sink(u64);

impl Write for Sink {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.0 += buf.len() as u64;
        Ok(buf.len())
    }
    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

/// An [`ObjectStore`] that reports what was actually pulled through it, by kind.
///
/// This is the applied-output instrument. A wall clock and a byte count are
/// **compatible with a fake**: a server that streamed a pre-made pack from disk
/// would produce a plausible `clone_ms` and a plausible `bytes_out` while reading
/// nothing. Counting the reads that reached the object store cannot be faked by
/// anything except doing the work.
///
/// Kinds are counted separately and deliberately not folded together. During an
/// `upload-pack` the graph walk reads **commits and trees only**. Blobs are
/// discovered from tree entries and never inflated by the walker
/// (`gunnar-wire/src/graph.rs`, `tree_closure`), so a blob read can only have
/// come from the pack writer inflating it in order to re-deflate it. That is what
/// makes `objects_recompressed` mean what the pack-copy path needs it to mean.
struct Counting<S> {
    inner: S,
    blobs: AtomicU64,
    commits: AtomicU64,
    trees: AtomicU64,
    other: AtomicU64,
    headers: AtomicU64,
    contains: AtomicU64,
}

/// The bound is `Deref`, not `GitServe`, and deliberately.
///
/// Neither `GitOps` nor `GitServe` has a blanket impl for `&T`, `Box<T>` or
/// `Arc<T>` (unlike `RepoStore`, which has one for `Arc`). The call sites here
/// hold all three shapes — `&MemGitServe` for the in-memory fixtures, and
/// `Arc<dyn GitServe>` out of `RepoStore::git()` for the on-disk ones — so
/// requiring `S: GitServe` would force a clone at some sites and be impossible
/// at others. Deref covers every shape at once and costs nothing at runtime.
impl<S> Counting<S>
where
    S: std::ops::Deref,
    S::Target: GitServe,
{
    fn new(inner: S) -> Self {
        Counting {
            inner,
            blobs: AtomicU64::new(0),
            commits: AtomicU64::new(0),
            trees: AtomicU64::new(0),
            other: AtomicU64::new(0),
            headers: AtomicU64::new(0),
            contains: AtomicU64::new(0),
        }
    }
    fn commit_reads(&self) -> u64 {
        self.commits.load(Ordering::Relaxed)
    }
    fn tree_reads(&self) -> u64 {
        self.trees.load(Ordering::Relaxed)
    }
    fn reads(&self) -> u64 {
        self.blobs.load(Ordering::Relaxed)
            + self.commits.load(Ordering::Relaxed)
            + self.trees.load(Ordering::Relaxed)
            + self.other.load(Ordering::Relaxed)
    }
    fn touches(&self) -> u64 {
        self.reads() + self.headers.load(Ordering::Relaxed) + self.contains.load(Ordering::Relaxed)
    }
}

/// The twelve, forwarded; only [`GitOps::has`] is counted, into `contains`.
///
/// `contains` keeps its name and its meaning. `ObjectStore` had two membership
/// methods and this decorator counted BOTH into this one counter, precisely so
/// that `touches()` would not move when the negotiation switched between them;
/// [`GitOps`] has one, so the counter now receives from one place instead of
/// two and every total it feeds is unchanged.
impl<S> GitOps for Counting<S>
where
    S: std::ops::Deref + Send + Sync,
    S::Target: GitServe,
{
    fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
        self.contains.fetch_add(1, Ordering::Relaxed);
        self.inner.has(oid)
    }

    fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> anyhow::Result<TxId> {
        self.inner.put(pack, refs)
    }
    fn put_pack(&self, bytes: &[u8]) -> anyhow::Result<TxId> {
        self.inner.put_pack(bytes)
    }
    fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
        self.inner.put_refs(updates)
    }
    fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
        self.inner.get(oid)
    }
    fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
        self.inner.size(oid)
    }
    fn extents(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<Extent>>> {
        self.inner.extents(oids)
    }
    fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
        self.inner.refs()
    }
    fn update_ref(
        &self,
        name: &str,
        old: Option<Oid<'_>>,
        new: Option<Oid<'_>>,
    ) -> anyhow::Result<TxId> {
        self.inner.update_ref(name, old, new)
    }
    fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
        self.inner.put_refs_cas(edits)
    }
    fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
        self.inner.reachable(want, have)
    }
    fn gc(&self) -> anyhow::Result<GcReport> {
        self.inner.gc()
    }
}

impl<S> GitServe for Counting<S>
where
    S: std::ops::Deref + Send + Sync,
    S::Target: GitServe,
{
    fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
        let found = self.inner.read(oid)?;
        if let Some((kind, _)) = &found {
            match kind {
                ObjType::Blob => &self.blobs,
                ObjType::Commit => &self.commits,
                ObjType::Tree => &self.trees,
                // `Tag`, and the two delta codes an inflated read must never
                // return. Folded into `other` exactly as `Tag` alone used to be:
                // the three counters this arm publishes are blobs, commits and
                // trees, and pack-copy's `objects_recompressed` is the blob one.
                _ => &self.other,
            }
            .fetch_add(1, Ordering::Relaxed);
        }
        Ok(found)
    }
    fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
        self.headers.fetch_add(1, Ordering::Relaxed);
        self.inner.header(oid)
    }
    /// Charged once per id, not once per call — `sizes` is the bulk `header`,
    /// and charging it one would let work moved into it read as work removed.
    fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
        self.headers.fetch_add(oids.len() as u64, Ordering::Relaxed);
        self.inner.sizes(oids)
    }
    fn head(&self) -> anyhow::Result<Option<RefRow>> {
        self.inner.head()
    }
    fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
        self.inner.set_head(target)
    }
    fn emit_pack(
        &self,
        objects: &[Oid<'_>],
        have: &[Oid<'_>],
        caps: &Caps,
        out: &mut dyn std::io::Write,
    ) -> anyhow::Result<PackStats> {
        self.inner.emit_pack(objects, have, caps, out)
    }
    fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
        self.inner.select(want, have)
    }
}

/// ── RED SWITCH: `GUNNAR_BENCH_NO_PACK_COPY=1` ───────────────────────────────
///
/// The switch was `gunnar_store::NoPackCopy`, a shim that sat in front of an
/// `ObjectStore` and refused to hand the serve path a verbatim pack, so the
/// writer had to re-deflate every object. It moved into `gunnar-store` on
/// 2026-08-03 (one implementation, shared with `gunnar-wire/tests/pack_copy.rs`) and it was
/// **deleted with the packfile engine on 2026-08-10**.
///
/// There is no replacement, and that is a property of the new contract rather
/// than an omission: [`GitServe::emit_pack`] *is* the engine's pack writer, so
/// there is no longer a seam between "the store" and "the thing that decides
/// whether to copy" at which a shim could stand.
///
/// So every path that reads this now **refuses** when it is set, rather than
/// running unswitched under a switched label — the same answer
/// `crates/gunnar-server/examples/git-store-serve.rs` gives in its `main()`.
///
/// Whether this process has been asked to serve with pack-copy switched off.
fn pack_copy_off() -> bool {
    std::env::var_os("GUNNAR_BENCH_NO_PACK_COPY").is_some()
}

/// One ref, by name, out of the contract's row stream.
///
/// There is no by-name ref read on [`GitOps`]: `refs()` returns every row and the
/// caller picks. That replaces `RefStore::read(&FullName)`, and with it the
/// `FullName` newtype — the contract takes ref names as `&str` and validates
/// them at the engine, so a `full_name()` helper that panicked on a bad name is
/// no longer the thing standing between a typo and a wrong answer.
///
/// `Ok(None)` distinguishes *"no such ref"* from *"a symbolic ref"*: a row with
/// no `oid` is a symref (`HEAD`); the contract settled that `refs()` does not carry
/// `HEAD` at all. Every caller here asserts on a branch tip, so a symref answer
/// would be a bug and is reported rather than unwrapped.
fn read_ref(git: &dyn GitOps, name: &str) -> Result<Option<ObjectId>> {
    let Some(row) = git.refs()?.into_iter().find(|r| r.name == name) else {
        return Ok(None);
    };
    match row.oid {
        None => bail!(
            "{name} resolved to a symbolic ref pointing at {:?}, not to an object",
            row.symref_target
        ),
        Some(raw) => Ok(Some(
            ObjectId::try_from(raw.as_slice())
                .with_context(|| format!("{name} holds an unreadable object id"))?,
        )),
    }
}

/// Local bridge from gunnar's object trait to gix's pack traits.
///
/// The old gunnar-store adapter went with the gix engine; the bench still needs
/// this seam for pack writers and thin-pack indexing over surviving stores.
#[derive(Clone, Copy)]
struct GunnarObjects<S>(S);

/// `anyhow::Error` as a `std::error::Error`, so it can be raised into gix's
/// exception (gix 0.88's `gix_error::Exn`) with its cause chain intact.
#[derive(Debug)]
struct AnyhowCause(anyhow::Error);

impl std::fmt::Display for AnyhowCause {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        std::fmt::Display::fmt(&self.0, f)
    }
}

impl std::error::Error for AnyhowCause {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.0.source()
    }
}

fn to_gix_find_error(err: anyhow::Error) -> gix::Error {
    // `anyhow::Error` is not `std::error::Error`, so it cannot be raised straight
    // into gix's error slot the way `gunnar_store::Error` could. The newtype above
    // keeps the whole cause chain, which is the part worth keeping: these errors
    // surface as "a pack writer could not find an object", and the engine's
    // reason for that is the useful half. (gix 0.88 replaced `find::Error`, a
    // boxed `dyn Error`, with `gix_error::Exn`, 2026-10-02.)
    gix::Exn::new(AnyhowCause(err)).erased().into_error()
}

impl<S> gix_object::Exists for GunnarObjects<S>
where
    S: std::ops::Deref,
    S::Target: GitServe,
{
    fn exists(&self, id: &gix_hash::oid) -> bool {
        // `GitOps::has` is fallible where `ObjectStore::contains` was not, and
        // `gix_object::Exists` is not — so an error has to collapse to a bool
        // here. It collapses to `false`, which is the safe direction for every
        // caller of this trait in this file: a pack writer that believes an
        // object is absent writes it out whole (bigger pack, still correct),
        // where one that believes an absent object is present emits a delta
        // against nothing (a corrupt pack).
        self.0.has(id.as_bytes()).unwrap_or(false)
    }
}

impl<S> gix_object::Find for GunnarObjects<S>
where
    S: std::ops::Deref,
    S::Target: GitServe,
{
    fn try_find<'a>(
        &self,
        id: &gix_hash::oid,
        buffer: &'a mut Vec<u8>,
    ) -> gix::Result<Option<Data<'a>>> {
        // `GitServe::read` returns owned bytes; `gix_object::Find` hands the
        // caller a `Data<'a>` borrowed from `buffer`. So the bytes are moved
        // into the caller's buffer rather than copied a second time.
        match self.0.read(id.as_bytes()).map_err(to_gix_find_error)? {
            None => Ok(None),
            Some((kind, bytes)) => {
                let kind = match kind {
                    ObjType::Commit => gix_object::Kind::Commit,
                    ObjType::Tree => gix_object::Kind::Tree,
                    ObjType::Blob => gix_object::Kind::Blob,
                    ObjType::Tag => gix_object::Kind::Tag,
                    // An inflated read contracts to return a real object. A
                    // delta code here is an engine bug, and it is reported as
                    // one rather than guessed at.
                    other => {
                        return Err(to_gix_find_error(anyhow::anyhow!(
                            "GitServe::read returned pack-encoding type {other:?} for {id}, \
                             which is not an object type"
                        )))
                    }
                };
                buffer.clear();
                buffer.extend_from_slice(&bytes);
                // `Data::new(data, kind, hash_kind)`. The hash kind comes off the
                // id that was asked for, which is the only authority in scope and
                // is by construction the repository's.
                Ok(Some(Data::new(buffer, kind, id.kind())))
            }
        }
    }
}

/// Historical gix-store threshold kept only for surviving multi-pack fixtures;
/// the explicit writer arm that used the store API was retired with that engine.
const MULTI_PACK_INDEX_THRESHOLD: usize = 16;

fn mbs(bytes: u64, secs: f64) -> f64 {
    if secs <= 0.0 {
        0.0
    } else {
        (bytes as f64 / (1024.0 * 1024.0)) / secs
    }
}

fn round(v: f64, places: u32) -> f64 {
    let f = 10f64.powi(places as i32);
    (v * f).round() / f
}

// ── the synthetic repository ───────────────────────────────────────────────

/// One generated object: its kind, its bytes and the id gunnar gave it.
struct Corpus {
    ids: Vec<ObjectId>,
    blobs: usize,
    trees: usize,
    commits: usize,
    /// Total inflated content bytes across every object.
    content_bytes: u64,
    tip: ObjectId,
}

/// Build a real git history into `store`: `commits` commits, each adding
/// `blobs_per_commit` fresh blobs under one tree.
///
/// A real history rather than a bag of blobs, because half the suite walks it:
/// `walk_1m`, `clone_large` and `negotiate_divergent` are all questions about the
/// graph, and a graph with no shape answers none of them.
///
/// # It writes ONE PACK, because the contract has no single-object write
///
/// This used to call `ObjectStore::write(kind, bytes)` once per object. [`GitOps`]
/// has no such method — a store that keeps pushed packs verbatim refuses a
/// single-object write, and `bench/src/packbuild.rs` exists for exactly this
/// reason. So the objects are built in memory, sealed into one packfile by
/// [`PackBuilder`] and landed with a single [`GitOps::put_pack`].
///
/// The ids are unchanged: [`PackBuilder::add`] hashes through
/// `gix_object::compute_hash`, which is the same function `gunnar-store` hashes
/// with, so a fixture that names an object by id and a store that indexes it
/// still agree by construction.
///
/// **The objects land packed rather than loose, and that is closer to real.**
/// It also means the arms that used to see a loose-object store now see a
/// one-pack store; no arm in this file asserts on looseness, but it is recorded
/// because it is the kind of fixture change that moves a number quietly.
fn build_history(
    store: &dyn GitOps,
    hash: Kind,
    seed: u64,
    commits: usize,
    blobs_per_commit: usize,
    blob_bytes: usize,
    parents_from: Option<ObjectId>,
) -> Result<Corpus> {
    let mut pack = PackBuilder::new(hash);
    let mut rng = Lcg::new(seed);
    let mut ids = Vec::with_capacity(commits * (blobs_per_commit + 2));
    let mut head = parents_from;
    let mut content_bytes = 0u64;
    let (mut n_blobs, mut n_trees, mut n_commits) = (0usize, 0usize, 0usize);

    for c in 0..commits {
        let mut entries: Vec<(String, ObjectId)> = Vec::with_capacity(blobs_per_commit);
        for b in 0..blobs_per_commit {
            let bytes = rng.text(blob_bytes);
            content_bytes += bytes.len() as u64;
            let id = pack.add(gix_object::Kind::Blob, bytes)?;
            ids.push(id);
            n_blobs += 1;
            entries.push((format!("f{c:07}_{b:04}.txt"), id));
        }
        // Tree entries must be sorted by name or gix rejects the tree.
        entries.sort_by(|a, b| a.0.cmp(&b.0));
        let mut tree = Vec::new();
        for (name, id) in &entries {
            tree.extend_from_slice(b"100644 ");
            tree.extend_from_slice(name.as_bytes());
            tree.push(0);
            tree.extend_from_slice(id.as_bytes());
        }
        content_bytes += tree.len() as u64;
        let tree_id = pack.add(gix_object::Kind::Tree, tree)?;
        ids.push(tree_id);
        n_trees += 1;

        let at = 1_700_000_000i64 + c as i64;
        let mut text = format!("tree {}\n", tree_id.to_hex());
        if let Some(p) = head {
            text.push_str(&format!("parent {}\n", p.to_hex()));
        }
        text.push_str(&format!(
            "author Gunnar Bench <bench@gunnar.invalid> {at} +0000\n\
             committer Gunnar Bench <bench@gunnar.invalid> {at} +0000\n\ncommit {c}\n"
        ));
        content_bytes += text.len() as u64;
        let commit_id = pack.add(gix_object::Kind::Commit, text.as_bytes().to_vec())?;
        ids.push(commit_id);
        n_commits += 1;
        head = Some(commit_id);
    }

    // One landing, after the whole graph is built. Landing per commit would put
    // `commits` packs in the store and make every arm that reads this fixture a
    // question about multi-pack lookup instead of the one it asks.
    let bytes = pack.finish().context("sealing the fixture pack")?;
    store
        .put_pack(&bytes)
        .context("landing the fixture pack in the store")?;

    Ok(Corpus {
        ids,
        blobs: n_blobs,
        trees: n_trees,
        commits: n_commits,
        content_bytes,
        tip: head.context("a history with zero commits has no tip")?,
    })
}

/// A flat corpus of loose blobs, no graph, for the arms that are about the
/// *store*'s addressing rather than the *walker*'s traversal.
///
/// # Every blob is distinct, and that is a correction (2026-08-03)
///
/// It was not. [`Lcg::text`] assembles an object out of whole lines drawn from a
/// 128-line pool, and a 96-byte object is two or three of them, so the corpus
/// could only ever contain a few thousand distinct objects however many were
/// asked for. Measured after the fact: `packed-1000000o-1p`'s `oids.bin` held
/// **1 000 000 entries and 16 712 distinct ids.** `gunnar.lookup_1m` was
/// resolving oids against a **sixteen-thousand**-object index, small enough to
/// sit in L2, and calling it a million. Every absolute number that arm published
/// before this date is against that corpus and is not comparable with anything
/// after it.
///
/// This is the same disease as the `ratio_x` fixture bug already recorded in
/// the bench spec (a generator whose output silently collapses), and it was caught the
/// same way, by an assertion on an **applied output** rather than on the
/// request: the multi-pack index reported how many objects it actually held.
///
/// The counter goes in as its own line, so the deflate profile of the rest is
/// untouched and only uniqueness changes.
///
/// Lands as one pack, for the same reason [`build_history`] does.
fn build_blobs(
    store: &dyn GitOps,
    hash: Kind,
    seed: u64,
    n: usize,
    bytes: usize,
) -> Result<Vec<ObjectId>> {
    let mut pack = PackBuilder::new(hash);
    let mut rng = Lcg::new(seed);
    let mut ids = Vec::with_capacity(n);
    for i in 0..n {
        let mut content = format!("object {i} of {n}\n").into_bytes();
        content.extend_from_slice(&rng.text(bytes.saturating_sub(content.len().min(bytes))));
        ids.push(pack.add(gix_object::Kind::Blob, content)?);
    }
    let sealed = pack.finish().context("sealing the blob fixture pack")?;
    store
        .put_pack(&sealed)
        .context("landing the blob fixture pack in the store")?;
    Ok(ids)
}

/// Object ids that are **well-formed and certainly absent**: the miss set.
///
/// Derived by flipping the high byte of a real id rather than by hashing random
/// bytes, so they are the same width and the same kind and cannot be rejected
/// early by a cheap shape check. A miss set a store can dismiss without probing
/// its pack indices measures nothing (lookup is O(packs) only on a miss).
fn miss_ids(from: &[ObjectId], n: usize) -> Vec<ObjectId> {
    let mut out = Vec::with_capacity(n);
    for i in 0..n {
        let src = from[i % from.len()];
        let mut bytes = src.as_slice().to_vec();
        bytes[0] ^= 0xff;
        bytes[1] ^= (i & 0xff) as u8;
        out.push(ObjectId::from_bytes_or_panic(&bytes));
    }
    out
}

/// Open (or lazily create) a cached on-disk repository fixture.
///
/// `build` is called only when the marker file is absent, so the expensive
/// generation happens once per machine and every later run measures gunnar rather
/// than the fixture generator. The marker is written **last**, so an interrupted
/// generation is retried rather than silently reused half-built.
fn cached_repo(name: &str, build: impl FnOnce(&Path) -> Result<()>) -> Result<PathBuf> {
    let dir = fixture_root().join(name);
    let marker = dir.join(".gunnar-bench-complete");
    if marker.exists() {
        return Ok(dir);
    }
    if dir.exists() {
        std::fs::remove_dir_all(&dir).with_context(|| format!("clearing half-built {dir:?}"))?;
    }
    std::fs::create_dir_all(&dir).with_context(|| format!("creating {dir:?}"))?;
    build(&dir).with_context(|| format!("building fixture {name}"))?;
    std::fs::write(&marker, b"complete\n")?;
    Ok(dir)
}

/// The directory an in-process znippy store puts itself in, **inside** the
/// repository it is the object storage for. `gunnar_bench::store_bytes`'s
/// `ZNIPPY_DIR`, repeated here because that constant is private to the counter
/// and this is the only other place that has to recognise the shape.
const ENGINE_STORE_DIR: &str = "gunnar-git.znippy";

/// [`cached_repo`] for a fixture that has to stay **git-native**, and the reason
/// three head-to-head arms could not pass on any znippy column.
///
/// # What goes wrong
///
/// The `h2h-*` fixtures are git repositories built by `git` itself and served
/// two ways: by stock `git upload-pack`, and by seam 1, whose `prepare` ingests
/// them into a store **beside** the repository (`<repo>.gunnar-<engine>`). So
/// the repository itself is git's and stays git's, and `h2h_corpus` →
/// `store_bytes::classify` is entitled to assume it.
///
/// An arm that calls [`gunnar_bench::engine::open_repo`] **on the fixture
/// directory itself** breaks that: on the znippy engine `open_repo` creates
/// `gunnar-git.znippy/` inside it. The fixture now carries git's `objects/pack`
/// AND a znippy store, and `classify` refuses it — correctly, and by design:
///
/// ```text
///   <fixture> carries BOTH a gunnar-git.znippy/ store and packs in
///   objects/pack. That is two engines' layouts in one repository, and any
///   single number taken off it is one engine's bytes labelled with both
///   engines' names.
/// ```
///
/// The refusal lands on whichever arm serves that fixture **next**, which is
/// never the arm that wrote the store. **Seen red on the desktop 2026-08-14** by
/// disabling the repair below and running the real pair,
/// `NORNIR_BENCH_ONLY=gunnar.clone_no_ofs_delta,gunnar.vs_git_push` on
/// znippy_fast — both arms went red, and the second one verbatim:
///
/// ```text
///   ✗ gunnar.clone_no_ofs_delta: the churned fixture has no refs/heads/main
///   ✗ gunnar.vs_git_push: …/h2h-linear-sha1-2048c-1024f-16k carries BOTH a
///     gunnar-git.znippy/ store and packs in objects/pack. That is two engines'
///     layouts in one repository, and any single number taken off it is one
///     engine's bytes labelled with both engines' names.
///       what is actually there:
///         gunnar-git.znippy/  2 file(s), 3686400 byte(s)
///         objects/pack/       4 file(s), 7606732 byte(s)
/// ```
///
/// Both halves of the mechanism are in that one run: `clone_no_ofs_delta` reads
/// the **empty** store it has just caused to be created (hence "no
/// refs/heads/main" on a fixture that has three refs), and `vs_git_push` — which
/// hands the very same directory to `h2h_run` as its `GUNNAR_BENCH_H2H_CLIENT`
/// corpus — is refused by the classifier for the wreckage left behind.
///
/// And the control, same box, same day: with
/// `NORNIR_BENCH_ONLY=gunnar.vs_git_clone` on a **fresh** fixture root,
/// `gunnar.vs_git_clone` passes on znippy_fast (`pack_ratio_x` 0.997, 36 172
/// objects, `gunnar_recompressed` 0) — the same arm that is red on all four
/// znippy columns of a full sweep, where `gunnar.clone_no_ofs_delta` opens a
/// store on the shared sha1 fixture long before it runs. Nothing about the arm
/// changed between those two runs except who had touched the fixture first.
///
/// That is also exactly why `vs_git_clone_sha256` and `vs_git_fetch_divergent`
/// were the two that went green when seam 1 was first set: their fixtures
/// (sha256, divergent) have no in-process reader at all. The three that stayed
/// red — `vs_git_clone`, `vs_git_push`, `vs_git_clone_concurrent` — are exactly
/// the three that share the sha1 one.
///
/// # Why removing the store is the fix and not vandalism
///
/// The store is a *derived* artefact sitting inside a *cached* one, and the two
/// were never meant to share a directory. Deleting it restores the fixture
/// byte-for-byte — `open_repo` writes only `{ENGINE_STORE_DIR}/` and never
/// touches `objects/`, `refs/` or `HEAD` — so this costs nothing and rebuilds
/// nothing. `xtask`'s `cleanup_fixtures` already deletes these wholesale
/// between store passes for the same reason; this is the same policy applied
/// between *arms*, which is where the damage actually happens.
///
/// It is loud on purpose: a fixture that had to be repaired is a real defect in
/// whichever arm wrote the store, and a silent repair would retire the evidence.
fn cached_git_repo(name: &str, build: impl FnOnce(&Path) -> Result<()>) -> Result<PathBuf> {
    let dir = cached_repo(name, build)?;
    let store = dir.join(ENGINE_STORE_DIR);
    if store.exists() {
        eprintln!(
            "bench: {} carried a {ENGINE_STORE_DIR}/ store written by an in-process arm; \
             removing it so this git-native fixture can be served. An arm called \
             engine::open_repo on the fixture itself instead of on a copy of it.",
            dir.display()
        );
        std::fs::remove_dir_all(&store)
            .with_context(|| format!("removing the foreign engine store at {store:?}"))?;
    }
    Ok(dir)
}

fn result(name: &str, metrics: Vec<(&str, Value)>) -> BenchResult {
    let mut m = Map::new();
    for (k, v) in metrics {
        m.insert(k.to_string(), v);
    }
    BenchResult::measured(name, m)
}

// ───────────────────────────────────────────────────────────────────────────
// gunnar.capabilities: static machine facts, gate-exempt
// ───────────────────────────────────────────────────────────────────────────

/// Machine facts, so every other row in the run is interpretable.
///
/// **Red condition: never.** This row is tagged `source: static`, which the
/// no-regression gate skips. An L3 cache that "shrank" because the run moved to
/// a different host is not a performance regression, and treating it as one would
/// train everyone to ignore the gate. Its job is to make the *other* rows
/// falsifiable: `hash_mbs` without `sha_ni` is a different number.
struct Capabilities;

impl Bencher for Capabilities {
    fn id(&self) -> &'static str {
        "gunnar.capabilities"
    }
    fn run(&self) -> Result<BenchResult> {
        let mut r = nornir_bench::machine_capabilities(self.id());
        let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
        let flag = |f: &str| {
            let needle = format!(" {f} ");
            if cpuinfo
                .lines()
                .any(|l| l.starts_with("flags") && format!("{l} ").contains(&needle))
            {
                1.0
            } else {
                0.0
            }
        };
        let l3_kb = std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cache/index3/size")
            .ok()
            .and_then(|s| {
                let s = s.trim();
                let (num, unit) = s.split_at(s.len().saturating_sub(1));
                num.parse::<f64>().ok().map(|n| match unit {
                    "M" => n * 1024.0,
                    "G" => n * 1024.0 * 1024.0,
                    _ => n,
                })
            })
            .unwrap_or(0.0);
        r.metrics.insert("sha_ni".into(), json!(flag("sha_ni")));
        r.metrics.insert("avx2".into(), json!(flag("avx2")));
        r.metrics.insert("l3_kb".into(), json!(l3_kb));
        r.metrics.insert(
            "hash_kind_sha256".into(),
            json!(if HASH == Kind::Sha256 { 1.0 } else { 0.0 }),
        );
        r.metrics
            .insert("preview".into(), json!(if preview() { 1.0 } else { 0.0 }));
        // ── WHICH WORKTREE, on the row ────────────────────────────────────
        //
        // `nornir bench run --config <p>` resolves the repository by NAME out
        // of the config's grandparent directory, and has therefore benched a
        // different worktree from the one it was launched in at least three
        // times — caught once only by this file's own RAGNARÖK guard, and once
        // not caught at all (run `4b65a303`).
        // Nothing on any persisted row said which tree a figure came from.
        //
        // `CARGO_MANIFEST_DIR` is fixed at COMPILE time, so it names the tree
        // this binary was built from — which is exactly the thing that can
        // differ from the tree the operator believes they are measuring. A path
        // is a string and strings do not reach a `bench_runs` row (measured
        // 2026-08-04), so it goes on as a hash: two worktrees give two numbers,
        // and a row can be compared against the tree that is supposed to have
        // produced it. `bench_binary_mtime` distinguishes two builds of the
        // same tree.
        r.metrics
            .insert("bench_tree_id".into(), json!(bench_tree_id() as f64));
        r.metrics.insert(
            "bench_binary_mtime".into(),
            json!(std::env::current_exe()
                .and_then(std::fs::metadata)
                .and_then(|m| m.modified())
                .ok()
                .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
                .map(|d| d.as_secs())
                .unwrap_or(0) as f64),
        );
        Ok(r)
    }
}
nornir_bench::register_bench!(Capabilities);

/// The source tree this binary was built from, as a number.
///
/// FNV-1a over `CARGO_MANIFEST_DIR`'s parent, truncated to 2^48 so it survives
/// the `f64` a warehouse metric is stored as without losing a bit. Collision
/// resistance is not the property wanted here — telling two of the project's
/// forty-odd gunnar worktrees apart is.
fn bench_tree_id() -> u64 {
    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
    for b in bench_tree().to_string_lossy().as_bytes() {
        h ^= *b as u64;
        h = h.wrapping_mul(0x1000_0000_01b3);
    }
    h & 0xffff_ffff_ffff
}

/// The gunnar checkout this binary was built from. Compile-time, so it cannot
/// be talked out of it by an environment variable at run time.
fn bench_tree() -> &'static Path {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap_or(Path::new(env!("CARGO_MANIFEST_DIR")))
}

// ───────────────────────────────────────────────────────────────────────────
// gunnar.pack_deflate: the deflate-backend guard
// ───────────────────────────────────────────────────────────────────────────

/// Deflate a fixed object corpus through the real [`gunnar_pack::PackWriter`].
///
/// **Red condition: `ratio_x` must not move.** `deflate_mbs` going up is the
/// point of swapping the zlib backend (miniz_oxide for zlib-rs), but a backend that
/// is faster *because it compresses worse* is not an improvement, it is a
/// bandwidth regression wearing a throughput number. The pair is the guard: a
/// `ratio_x` change of more than a percent or two alongside a `deflate_mbs`
/// improvement means the swap traded quality for speed and the trade was not
/// declared.
///
/// A secondary red: `deflate_mbs` at or near the memcpy rate of the machine
/// (tens of GB/s) means nothing was compressed at all.
struct PackDeflate;

impl Bencher for PackDeflate {
    fn id(&self) -> &'static str {
        "gunnar.pack_deflate"
    }
    fn run(&self) -> Result<BenchResult> {
        // A fixed corpus: 256 MiB in 16 KiB objects, which is roughly the shape of
        // a real repository's blob-size distribution's fat middle.
        let n = env_usize("GUNNAR_BENCH_DEFLATE_OBJECTS", scaled(16_384, 512));
        let size = env_usize("GUNNAR_BENCH_DEFLATE_SIZE", 16 * 1024);
        let mut rng = Lcg::new(0xDEF1A7E);
        let corpus: Vec<Vec<u8>> = (0..n).map(|_| rng.text(size)).collect();
        let raw: u64 = corpus.iter().map(|c| c.len() as u64).sum();

        let opts = WriteOptions::new(HASH);
        let sink = Sink::default();
        let start = Instant::now();
        let mut writer = gunnar_pack::PackWriter::new(sink, n as u32, &opts)?;
        for object in &corpus {
            writer.add_object(gix_object::Kind::Blob, object)?;
        }
        let outcome = writer.finish()?;
        let secs = start.elapsed().as_secs_f64();

        ensure!(
            outcome.num_objects as usize == n,
            "the writer reported {} objects for a {n}-object corpus",
            outcome.num_objects
        );
        ensure!(
            outcome.bytes_written < raw,
            "the pack ({} B) is not smaller than its input ({raw} B). Nothing was compressed.",
            outcome.bytes_written
        );

        Ok(result(
            self.id(),
            vec![
                ("deflate_mbs", json!(round(mbs(raw, secs), 2))),
                (
                    "ratio_x",
                    json!(round(raw as f64 / outcome.bytes_written as f64, 4)),
                ),
                ("objects", json!(n as f64)),
                ("bytes_in", json!(raw as f64)),
                ("bytes_out", json!(outcome.bytes_written as f64)),
                ("compression_level", json!(opts.compression_level as f64)),
            ],
        ))
    }
}
nornir_bench::register_bench!(PackDeflate);

// ───────────────────────────────────────────────────────────────────────────
// gunnar.pack_trailer_hash: the trailer-hash guard
// ───────────────────────────────────────────────────────────────────────────

/// Hash a fixed byte stream with the same hasher every pack's trailer uses.
///
/// **Red condition: `hash_mbs` far below the machine's memory bandwidth while
/// `gunnar.capabilities.sha_ni` is 1.** On a host that advertises `sha_ni`, a
/// sha256 rate in the low hundreds of MB/s means the accelerated path is not
/// being taken and the software fallback is, which costs 4.9x on the hash, and it is
/// invisible without this pair of rows.
///
/// The stream is hashed in 64 KiB chunks because that is `DEFLATE_CHUNK`, the
/// unit `PackWriter` actually feeds the hasher. Hashing one giant slice would
/// measure a call gunnar never makes.
struct PackTrailerHash;

impl Bencher for PackTrailerHash {
    fn id(&self) -> &'static str {
        "gunnar.pack_trailer_hash"
    }
    fn run(&self) -> Result<BenchResult> {
        let chunk_kib = 64usize;
        let total = env_usize("GUNNAR_BENCH_HASH_MB", scaled(2048, 64)) * 1024 * 1024;
        let mut rng = Lcg::new(0x8A5_1E5);
        let chunk = rng.text(chunk_kib * 1024);
        let chunks = total / chunk.len();

        let start = Instant::now();
        let mut hasher = gix_hash::hasher(HASH);
        for _ in 0..chunks {
            hasher.update(&chunk);
        }
        let id = hasher.try_finalize()?;
        let secs = start.elapsed().as_secs_f64();
        let hashed = (chunks * chunk.len()) as u64;

        ensure!(
            id.kind() == HASH,
            "the trailer hash is {:?}, not the repository's {HASH:?}",
            id.kind()
        );

        Ok(result(
            self.id(),
            vec![
                ("hash_mbs", json!(round(mbs(hashed, secs), 2))),
                ("bytes", json!(hashed as f64)),
                ("chunk_kib", json!(chunk_kib as f64)),
            ],
        ))
    }
}
nornir_bench::register_bench!(PackTrailerHash);

// ───────────────────────────────────────────────────────────────────────────
// gunnar.clone_no_ofs_delta: what a client without `ofs-delta` pays
// ───────────────────────────────────────────────────────────────────────────

/// The same clone twice, once with the client's `ofs-delta` capability and once
/// without, over a **churned** history where delta reuse has something to reuse.
///
/// The pack-copy path is gated on `ofs-delta` (`upload_pack.rs`), because a
/// copied delta is re-pointed as an `OBJ_OFS_DELTA` and a client that cannot read
/// one would be handed an unreadable pack. Every modern `git` sends the
/// capability, so the head-to-head arms never exercise the other branch — and the
/// other branch, until this arm was written, was **the entire server as it was
/// before pack-copy**: no delta reuse, no entry copying, a fresh deflate of every object.
///
/// This arm prices that, on the fixture where it costs the most. It runs
/// in-process against `gunnar-wire` directly, because the capability is a
/// property of the request and there is no way to make stock `git` withhold it.
///
/// # The subject is the CLIENT'S REPOSITORY, not the server's encoding
///
/// Raised 2026-08-14. What this arm asks is *"does a client that never
/// advertised `ofs-delta` still end up with the right repository, at a sane
/// cost?"* — and both halves of that are checked against something outside the
/// server:
///
/// * **`objects_delivered_x` — the object sets.** Each of the two packs is
///   handed to **stock `git index-pack --strict`**, and the ids it writes into
///   the resulting `.idx` are compared against `git rev-list --objects` over the
///   fixture itself. The two clones must deliver *the same* set and it must be
///   *the right* set. That is the assertion that fails if the no-`ofs-delta`
///   branch drops, duplicates or corrupts an object, and it is checked with git,
///   not with a number the server reported about itself.
/// * **`no_ofs_bytes_x` — the cost.** At most 1.20.
///
/// Deliberately **not** asserted any more: which entry type the server chose.
/// `OBJ_OFS_DELTA` against `OBJ_REF_DELTA` is *how* an engine answers the
/// capability, and the two engines are entitled to answer it differently — or,
/// for a store that keeps no packfile at all, to have no stored spelling to
/// re-encode in the first place. A client cannot see the difference except in
/// the byte count, which is exactly what `no_ofs_bytes_x` measures. So this arm
/// no longer distinguishes the two engines' internals, and that is the point:
/// the same question is now put to both of them honestly.
///
/// **Red condition: `no_ofs_bytes_x` must be at most 1.20.** A client that cannot
/// read `ofs-delta` can still read `OBJ_REF_DELTA` — it is the older of the two
/// forms and needs no capability at all — so there is no reason for it to receive
/// a materially larger pack. Seen red at `735728c`, where it read **18.336×**.
///
/// The ceiling is 1.20 rather than 1.10 because the residue is not noise, it is
/// arithmetic: one full base id per delta entry in place of a varint distance,
/// which at sha1 is 20 bytes against 1-3. MEASURED 2026-08-03 on this fixture:
/// **1.095×**, i.e. 591 093 bytes over 6 216 589 for ~33 000 delta entries.
/// MEASURED again the desktop 2026-08-14 on the gix column, through the re-heading
/// emitter this arm's old red asked for: **1.098×**, 6 829 475 over 6 222 710
/// for 36 172 objects, `recompressed = 0`. A sha256 repository pays 32 bytes and
/// would sit higher, which is why a 1.10 ceiling would have been a guard that
/// flaps rather than a guard that holds.
/// `crates/gunnar-pack/tests/ofs_delta_off.rs` asserts the derived bound
/// entry-by-entry, where the delta count is known; this arm asserts the shape.
struct CloneNoOfsDelta;

impl Bencher for CloneNoOfsDelta {
    fn id(&self) -> &'static str {
        "gunnar.clone_no_ofs_delta"
    }
    fn run(&self) -> Result<BenchResult> {
        // The churned head-to-head fixture, not the eleven arms' random blobs: a
        // corpus that deltas to nothing cannot show a delta-reuse difference, and
        // would report 1.00x for a completely broken implementation.
        let source = h2h_linear_fixture(false)?;
        // **The truth this arm is checked against, and it is git's.** Read off
        // the fixture before any engine touches it, so it is a property of the
        // corpus rather than of whichever store is under test.
        let expected = h2h_object_set(&source)?;
        // Handed to whichever engine is selected, in that engine's own layout.
        // Without this the arm asked znippy to read a `.git` directory and got
        // "the churned fixture has no refs/heads/main" — a true statement about
        // an empty store, and nothing at all about the capability under test.
        let dir = engine_served(&source, "clone-no-ofs-delta")?;
        let store = gunnar_bench::engine::open_repo(&dir, Kind::Sha1)?;
        let git = store.git();
        let tip = read_ref(git.as_ref(), "refs/heads/main")?
            .context("the churned fixture has no refs/heads/main")?;

        let judged = fixture_root().join("no-ofs-judged");
        let _ = std::fs::remove_dir_all(&judged);
        std::fs::create_dir_all(&judged)?;

        let mut rows = Vec::new();
        for ofs in [true, false] {
            let counting = Counting::new(git.clone());
            let upload = UploadPack::new(&counting, HASH);
            let request = FetchRequest {
                wants: vec![tip],
                done: true,
                ofs_delta: ofs,
                ..Default::default()
            };
            // A `Vec`, where this used to be a counting `Sink`: the byte count
            // is the same number, and keeping the bytes is what lets stock git
            // judge the clone instead of the server grading its own work.
            let mut sink: Vec<u8> = Vec::new();
            let start = Instant::now();
            let outcome = upload.fetch(&request, &mut sink)?;
            let ms = start.elapsed().as_secs_f64() * 1000.0;
            ensure!(
                outcome.sent_pack,
                "the ofs_delta={ofs} fetch produced no pack"
            );
            let bytes = sink.len() as u64;
            let delivered = objects_stock_git_accepts(
                &gunnar_bench::stages::demux_packfile(&sink)?,
                &judged,
                if ofs { "ofs" } else { "no-ofs" },
                // The sha1 head-to-head fixture, not `HASH`.
                Kind::Sha1,
            )?;
            rows.push((ofs, bytes, ms, outcome, delivered));
        }
        let (_, ofs_bytes, ofs_ms, ofs_out, ofs_ids) = rows[0].clone();
        let (_, raw_bytes, raw_ms, raw_out, raw_ids) = rows[1].clone();

        // ── the clone the client actually ends up with ──────────────────────
        //
        // Three statements, and none of them is the server's own count. The
        // first is the one this arm is FOR: withholding a capability must not
        // change which objects arrive.
        ensure!(
            raw_ids == ofs_ids,
            "the two clones delivered different object sets — {} ids with ofs-delta and {} \
             without, {} present in only one of them — so the byte ratio below compares two \
             different repositories and the client that cannot read ofs-delta got the wrong one",
            ofs_ids.len(),
            raw_ids.len(),
            ofs_ids.symmetric_difference(&raw_ids).count()
        );
        ensure!(
            raw_ids == expected,
            "stock git indexed {} objects out of the no-ofs-delta clone where `git rev-list \
             --objects` says the fixture reaches {} ({} missing, {} unexpected)",
            raw_ids.len(),
            expected.len(),
            expected.difference(&raw_ids).count(),
            raw_ids.difference(&expected).count()
        );
        ensure!(
            ofs_out.objects_sent == raw_out.objects_sent && ofs_out.objects_sent > 0,
            "the two requests sent {} and {} objects, so the byte ratio compares two \
             different clones",
            ofs_out.objects_sent,
            raw_out.objects_sent
        );

        let bytes_x = raw_bytes as f64 / ofs_bytes.max(1) as f64;
        ensure!(
            bytes_x <= 1.20,
            "a client without ofs-delta was served {raw_bytes} bytes where one with it got \
             {ofs_bytes} — {bytes_x:.3}x — and {} of {} entries were inflated and deflated \
             again ({raw_ms:.0} ms against {ofs_ms:.0} ms). OBJ_REF_DELTA needs no \
             capability, so a copied delta can be re-pointed by id instead of by distance \
             and this ratio has no reason to be above 1.0.",
            raw_out.objects_recompressed,
            raw_out.objects_sent
        );

        // **The guard that keeps the ratio above from being a beautiful 1.00
        // over two fallbacks**, raised to something outside the server.
        //
        // What stood here was `objects_recompressed == 0 &&
        // objects_copied_from_pack > 0` — the server's own receipt for its own
        // work, and a mechanism: an engine that keeps no packfile has no stored
        // entry to "copy" and would fail a guard about copying while serving a
        // perfectly good clone. The observable form of the same failure is that
        // a server which inflates and re-deflates every object emits a pack
        // enormously larger than the one `git repack -adfq` already wrote for
        // the same objects — **18.4×**, measured. So the fixture's own packs are
        // the yardstick, and 2.0 is far below the failure and far above the
        // spread between two honest packers.
        let git_pack_bytes = pack_bytes_in(&source.join("objects/pack"))?;
        let vs_git_x = ofs_bytes as f64 / git_pack_bytes.max(1) as f64;
        ensure!(
            vs_git_x <= 2.0,
            "the ofs-delta clone was {ofs_bytes} bytes where `git repack -adfq` stored the same \
             history in {git_pack_bytes} — {vs_git_x:.3}x. A server on the delta-reuse path emits \
             about what git stored; one that inflates and re-deflates every object emits about \
             18x, and the ratio below would then compare two fallbacks and report 1.00"
        );

        Ok(result(
            self.id(),
            vec![
                ("no_ofs_bytes_x", json!(round(bytes_x, 3))),
                ("no_ofs_ms_x", json!(round(raw_ms / ofs_ms.max(0.001), 3))),
                ("bytes_out_ofs", json!(ofs_bytes as f64)),
                ("bytes_out_no_ofs", json!(raw_bytes as f64)),
                ("clone_ms_ofs", json!(round(ofs_ms, 2))),
                ("clone_ms_no_ofs", json!(round(raw_ms, 2))),
                ("ofs_vs_git_pack_x", json!(round(vs_git_x, 3))),
                (
                    "copied_no_ofs",
                    json!(raw_out.objects_copied_from_pack as f64),
                ),
                (
                    "recompressed_no_ofs",
                    json!(raw_out.objects_recompressed as f64),
                ),
                ("objects_sent", json!(ofs_out.objects_sent as f64)),
                ("objects_delivered", json!(raw_ids.len() as f64)),
            ],
        ))
    }
}
nornir_bench::register_bench!(CloneNoOfsDelta);

// ───────────────────────────────────────────────────────────────────────────
// gunnar.push_large: the parallel-indexing guard
// ───────────────────────────────────────────────────────────────────────────

/// Receive, index and connectivity-check a large pack through the real
/// [`ReceivePack::serve`] path.
///
/// **Red condition: `parallel_speedup_x` at 1.0.** The defect is that `gix-pack`'s
/// `parallel` feature is never declared, so the appliance build silently indexes
/// packs on the serial code path. The honest in-process probe for that is to index
/// the *same* pack twice, once with `thread_limit(1)` and once with the default,
/// and compare: if the two throughputs are the same, the parallel path is not
/// engaging and the feature, however it is declared, did nothing.
///
/// This is a runtime probe, not a build-configuration comparison. Running gunnar
/// in *two cargo feature configurations* is not possible from one detached bench
/// workspace: this crate links `gunnar-store/fs`, which enables `gix-odb/parallel`,
/// which enables `gix-features/parallel` for the whole graph. Cargo unifies
/// features per graph, so the serial configuration cannot be reached from here at
/// all. What is unmeasured is stated rather than faked.
///
/// Secondary red: `objects` not equal to the number of objects in the pack means
/// the index silently shortened it.
struct PushLarge;

impl Bencher for PushLarge {
    fn id(&self) -> &'static str {
        "gunnar.push_large"
    }
    fn run(&self) -> Result<BenchResult> {
        let commits = env_usize("GUNNAR_BENCH_PUSH_COMMITS", scaled(1024, 64));
        let blob_bytes = env_usize("GUNNAR_BENCH_PUSH_BLOB_KIB", scaled(512, 64)) * 1024;

        // A store the pushed history is already visible in. `receive-pack`'s
        // connectivity check reads the new tip's closure out of the store, and
        // the store's own ingest→visibility step is a storage concern, not
        // part of what this arm times.
        let store = MemGitServe::new(HASH);
        let corpus = build_history(&store, HASH, 0x9457, commits, 1, blob_bytes, None)?;
        let n_objects = corpus.ids.len();

        // The pack the client would send.
        let odb = GunnarObjects(&store);
        let (pack, outcome) =
            gunnar_pack::write_pack_to_vec(&corpus.ids, &odb, &WriteOptions::new(HASH))?;
        ensure!(
            outcome.num_objects as usize == n_objects,
            "the fixture pack is short"
        );

        // ── the two index passes: serial vs default fan-out ────────────────
        let index_once = |threads: Option<usize>| -> Result<(f64, u32)> {
            let mut opts = IndexOptions::new(HASH);
            if let Some(t) = threads {
                opts = opts.thread_limit(t);
            }
            let mut reader = std::io::Cursor::new(&pack[..]);
            let start = Instant::now();
            let indexed = gunnar_pack::index_pack(&mut reader, None, &opts, None)?;
            Ok((start.elapsed().as_secs_f64(), indexed.num_objects))
        };
        let (serial_secs, serial_objects) = index_once(Some(1))?;
        let (par_secs, par_objects) = index_once(None)?;
        ensure!(
            serial_objects as usize == n_objects && par_objects as usize == n_objects,
            "indexing reported {serial_objects}/{par_objects} objects for a {n_objects}-object pack"
        );

        // ── the full receive: advertisement → commands → pack → status ─────
        let mut request = Vec::with_capacity(pack.len() + 256);
        let mut line = format!(
            "{} {} refs/heads/main",
            HASH.null().to_hex(),
            corpus.tip.to_hex()
        )
        .into_bytes();
        line.push(0);
        line.extend_from_slice(
            format!(
                "report-status object-format={} agent=gunnar-bench/1",
                object_format(HASH)
            )
            .as_bytes(),
        );
        gunnar_pktline::write_text(&mut request, &line)?;
        gunnar_pktline::write_flush(&mut request)?;
        request.extend_from_slice(&pack);

        let counting = Counting::new(&store);
        let receive = ReceivePack::new(&counting, HASH);
        let mut input = std::io::Cursor::new(request);
        let mut out = Vec::new();
        let start = Instant::now();
        let push = receive
            .serve(
                &mut input,
                &mut out,
                &|pack: &mut dyn BufRead| {
                    gunnar_pack::index_pack(pack, None, &IndexOptions::new(HASH), None)
                        .map(wire_indexed)
                        .map_err(wire_ingest_err)
                },
                &AllowAll,
            )?
            .context("the push produced no outcome")?;
        let push_secs = start.elapsed().as_secs_f64();

        // A push that was refused is very cheap and completely useless as a
        // measurement (the shape `connectivity_cost.rs` already guards against).
        ensure!(
            push.statuses == vec![RefStatus::Ok],
            "the push was refused ({:?}), so what was timed is the cost of refusing it",
            push.statuses
        );
        ensure!(
            read_ref(&store, "refs/heads/main")? == Some(corpus.tip),
            "the reference did not move, so nothing was really accepted"
        );
        ensure!(
            counting.touches() as usize >= n_objects,
            "the connectivity check touched {} objects for a {n_objects}-object push, so it \
             did not walk the pushed history",
            counting.touches()
        );

        let speedup = if par_secs > 0.0 {
            serial_secs / par_secs
        } else {
            0.0
        };
        Ok(result(
            self.id(),
            vec![
                ("push_ms", json!(round(push_secs * 1000.0, 2))),
                (
                    "index_mbs",
                    json!(round(mbs(pack.len() as u64, par_secs), 2)),
                ),
                (
                    "index_mbs_serial",
                    json!(round(mbs(pack.len() as u64, serial_secs), 2)),
                ),
                ("parallel_speedup_x", json!(round(speedup, 3))),
                ("objects", json!(n_objects as f64)),
                ("pack_bytes", json!(pack.len() as f64)),
                ("connectivity_touches", json!(counting.touches() as f64)),
            ],
        ))
    }
}
nornir_bench::register_bench!(PushLarge);

fn object_format(kind: Kind) -> &'static str {
    match kind {
        Kind::Sha1 => "sha1",
        _ => "sha256",
    }
}

// ───────────────────────────────────────────────────────────────────────────
// gunnar.walk_1m: the serial closure walk
// ───────────────────────────────────────────────────────────────────────────

/// Closure walk over a million-object graph.
///
/// Driven through `receive-pack`'s connectivity check rather than through a
/// clone, because that path walks the closure and **writes no pack**, so the
/// number is the walker's, not the deflater's. It is the same walker either way
/// (`gunnar-wire/src/graph.rs`, one walker for both directions), which is
/// exactly why it can be measured from the cheaper side.
///
/// **Red condition: fewer object reads than there are commits and trees.** A
/// walk that reports a million objects per second because it visited eight of
/// them is the failure mode here, and `objects_per_sec` alone cannot see it. The
/// counting store makes the claim falsifiable: a closure over C commits and T
/// trees cannot be established without reading each of them at least once, and
/// that is asserted per kind, not hoped for.
///
/// **It used to assert `touches >= objects`, and that was wrong — corrected on
/// 2026-08-03.** The floor was met only because the walk decoded every commit
/// **twice**: once in `commit_closure` for its parents, once again afterwards to
/// ask for the root tree it had already read and thrown away. `touches` came to
/// 1 000 639 on a 1 000 002-object closure, and 333 334 of those reads — one per
/// commit, a third of the total — were the redundant ones. Removing them made
/// this arm go RED at 667 305, so the old floor had quietly encoded the
/// inefficiency as the invariant: the only way to satisfy it was to keep doing
/// the extra work. A floor that a fix can violate is not a floor, and blobs
/// never contributed to it at all.
///
/// **The ceiling is the new half, and it is what guards the walk.**
/// `commit_reads <= commits` says each commit is read at most once. The
/// walker as it stood before reads each one exactly twice and fails it: measured, not
/// assumed — `gunnar.walk_1m` at `e4d8a78` reports `commit_reads_per_commit`
/// 2.000, and after the fix 1.000.
///
/// In memory on purpose: this is a question about traversal cost, and running
/// it against a filesystem would measure page-cache behaviour instead.
struct Walk1m;

impl Bencher for Walk1m {
    fn id(&self) -> &'static str {
        "gunnar.walk_1m"
    }
    fn run(&self) -> Result<BenchResult> {
        // 333_334 commits x (1 blob + 1 tree + 1 commit) ≈ 1.0 M objects.
        let commits = env_usize("GUNNAR_BENCH_WALK_COMMITS", scaled(333_334, 6_667));
        let store = MemGitServe::new(HASH);
        let corpus = build_history(&store, HASH, 0x3A1C, commits, 1, 64, None)?;
        let n_objects = corpus.ids.len();

        let counting = Counting::new(&store);
        let receive = ReceivePack::new(&counting, HASH);
        let mut request = Vec::new();
        let mut line = format!(
            "{} {} refs/heads/main",
            HASH.null().to_hex(),
            corpus.tip.to_hex()
        )
        .into_bytes();
        line.push(0);
        line.extend_from_slice(
            format!(
                "report-status object-format={} agent=gunnar-bench/1",
                object_format(HASH)
            )
            .as_bytes(),
        );
        gunnar_pktline::write_text(&mut request, &line)?;
        gunnar_pktline::write_flush(&mut request)?;
        // An empty pack: every object is already in the store, so the push is
        // pure connectivity verification and the walk is all that is timed.
        request.extend_from_slice(&gunnar_pack::empty_pack(HASH)?);

        let mut input = std::io::Cursor::new(request);
        let mut out = Vec::new();
        let start = Instant::now();
        let push = receive
            .serve(
                &mut input,
                &mut out,
                &|pack: &mut dyn BufRead| {
                    gunnar_pack::index_pack(pack, None, &IndexOptions::new(HASH), None)
                        .map(wire_indexed)
                        .map_err(wire_ingest_err)
                },
                &AllowAll,
            )?
            .context("the connectivity push produced no outcome")?;
        let secs = start.elapsed().as_secs_f64();

        ensure!(
            push.statuses == vec![RefStatus::Ok],
            "the connectivity check rejected the tip ({:?}), so the walk did not complete",
            push.statuses
        );
        let touches = counting.touches();
        let commit_reads = counting.commit_reads();
        let tree_reads = counting.tree_reads();
        // THE FLOOR: the walk really did reach the whole graph.
        ensure!(
            commit_reads >= corpus.commits as u64 && tree_reads >= corpus.trees as u64,
            "the walk read {commit_reads} commits and {tree_reads} trees for a closure of \
             {} commits and {} trees; it did not reach the whole graph, so objects_per_sec \
             describes a partial walk (touches {touches}, objects {n_objects})",
            corpus.commits,
            corpus.trees
        );
        // THE CEILING: and it reached it without reading anything twice.
        //
        // `+ TIPS` is not slack, it is the one ref this push advertises:
        // `verify_connectivity` peels every tip before walking, and peeling
        // reads the object. One tip, one extra read, and it must be spelled out
        // rather than absorbed into a tolerance — a ceiling with room in it
        // could not tell 1 read per commit from 1.5.
        const TIPS: u64 = 1;
        ensure!(
            commit_reads <= corpus.commits as u64 + TIPS,
            "the walk read {commit_reads} commits for a closure of {} — {:.3} reads per \
             commit. Every commit past the first read is a store lookup, a zlib inflate and \
             a delta chain spent re-deriving a field the walk already had",
            corpus.commits,
            commit_reads as f64 / corpus.commits as f64
        );

        Ok(result(
            self.id(),
            vec![
                ("objects_per_sec", json!(round(n_objects as f64 / secs, 1))),
                ("walk_ms", json!(round(secs * 1000.0, 2))),
                ("objects", json!(n_objects as f64)),
                ("commits", json!(corpus.commits as f64)),
                ("trees", json!(corpus.trees as f64)),
                ("blobs", json!(corpus.blobs as f64)),
                ("store_touches", json!(touches as f64)),
                ("commit_reads", json!(commit_reads as f64)),
                (
                    "commit_reads_per_commit",
                    json!(round(commit_reads as f64 / corpus.commits as f64, 3)),
                ),
                (
                    "content_mb",
                    json!(round(corpus.content_bytes as f64 / (1024.0 * 1024.0), 2)),
                ),
            ],
        ))
    }
}
nornir_bench::register_bench!(Walk1m);

// ───────────────────────────────────────────────────────────────────────────
// gunnar.lookup_1m / gunnar.lookup_1m_64pack: batch lookup, and O(packs) on a miss
// ───────────────────────────────────────────────────────────────────────────

/// The single-pack miss rate as `gunnar.lookup_1m` measured it, published for the
/// record only.
///
/// **It is deliberately NOT what `lookup_1m_64pack`'s degradation assertion
/// compares against**, and the reason is a hollow guard that was caught here on
/// 2026-08-03 by trying to make it fail:
///
/// Bumped whenever the corpus [`build_blobs`] generates changes, so a cached
/// fixture from before the change is regenerated instead of silently reused.
///
/// `v2` (2026-08-03): every blob is distinct. `v1` asked for a million and
/// produced 16 712. See [`build_blobs`].
///
/// `v3` (2026-08-14): the corpus is landed **through the engine**
/// ([`packed_fixture_shaped`]) instead of being written straight into
/// `objects/pack`. Every `v2` fixture on a znippy root carries a 0-byte
/// `gunnar-git.znippy/objects.pack` — an empty store — and every arm built on
/// one measured a repository with nothing in it. The key is bumped because a
/// cached `v2` directory is not a smaller version of a `v3` one, it is the
/// defect.
const PACKED_CORPUS_VERSION: &str = "v3";

/// How a [`packed_fixture`]'s objects are laid out across its packs, and whether
/// the result can be **served** or only probed.
///
/// # The shape was implicit, and that is how a corruption bug got out
///
/// Until 2026-08-06 [`packed_fixture`] had exactly one shape and never said so:
/// `ids.chunks(per)`, one residency per object, no refs and no commit, so
/// nothing built on it could ever be served to a client. Every OTHER corpus in
/// this file comes from `git clone --bare`, which repacks to a single pack. So
/// the whole suite held two shapes — *one pack* and *N disjoint packs* — and
/// neither of them is the shape a pushed server has.
///
/// `0d10e17`'s defect needs the third: an object living in **two** packs with a
/// multi-pack index over them, which is what `index-pack --fix-thin` produces
/// when it splices a delta base into a later push. No arm and no test could
/// reach it, and a pack stock git refuses (`fatal: pack is corrupted (SHA1
/// mismatch)`) reached a customer build.
///
/// So the shape is now a value, it is written into the fixture's cache key, and
/// it is **measured back off disk** rather than assumed — see
/// [`objects_by_pack_residency`]. A fixture that drifts out of its declared
/// shape now says so instead of quietly changing what its arm is about.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PackShape {
    packs: usize,
    objects: usize,
    /// Objects the previous pack already holds that are written into this one
    /// as well. `0` is the historical disjoint shape.
    ///
    /// **This is the only knob that makes a multi-pack index matter.** The index
    /// keeps one deduplicated oid table, so it has something to choose between
    /// only when an object has two homes; over disjoint packs it costs nothing
    /// and hides nothing, which is why 1139-pack `bench/year` clones clean.
    overlap: usize,
    /// Write a tree and a commit per pack, chained, plus `refs/heads/main`, so
    /// the fixture is a repository a client can fetch from rather than a pile of
    /// packfiles.
    served: bool,
}

impl PackShape {
    /// The historical shape: disjoint packs, blobs only, lookups only. Kept
    /// deliberately — `gunnar.lookup_1m` and `gunnar.lookup_1m_64pack` are
    /// the miss-path series; re-baselining them to a different corpus would throw
    /// away every number ever published against them. Its cache key is
    /// unchanged, so no existing fixture is rebuilt.
    fn probed(packs: usize, objects: usize) -> Self {
        PackShape {
            packs,
            objects,
            overlap: 0,
            served: false,
        }
    }

    /// The production shape: `overlap` objects shared with the previous pack,
    /// and a history over the blobs so the store can be served.
    ///
    /// **No caller since 2026-08-14.** Its one arm was
    /// `gunnar.multi_pack_extent_cost`, deleted with the reason recorded where
    /// it stood. Kept rather than deleted because it is the only shape in this
    /// file that produces a *servable* store with an object in two packs — the
    /// corpus `0d10e17`'s defect needs — and building it directly instead of
    /// driving sixteen `git push`es is the difference between two seconds and
    /// two minutes for whatever asks that question next.
    #[allow(dead_code)]
    fn served(packs: usize, objects: usize, overlap: usize) -> Self {
        PackShape {
            packs,
            objects,
            overlap,
            served: true,
        }
    }

    /// The fixture cache key. The probed shape keeps its historical name so the
    /// expensive million-object corpora on disk stay valid.
    fn cache_name(&self) -> String {
        let PackShape {
            packs,
            objects,
            overlap,
            served,
        } = *self;
        if overlap == 0 && !served {
            format!("packed-{objects}o-{packs}p-{PACKED_CORPUS_VERSION}")
        } else {
            format!("packed-{objects}o-{packs}p-ov{overlap}-served-{PACKED_CORPUS_VERSION}")
        }
    }
}

/// What a built [`packed_fixture_shaped`] turned out to be — every field read
/// back off disk, never restated from the request (measured, not asserted).
struct PackedFixture {
    dir: PathBuf,
    /// The blobs, in generation order.
    ids: Vec<ObjectId>,
    /// The tip of the commit chain, when the shape asked to be servable. This
    /// is what `stages::serve_on` is given, and its absence is what made every
    /// arm built on this fixture unable to look at a served pack at all.
    ///
    /// Unread since 2026-08-14 for the same reason [`PackShape::served`] has no
    /// caller. It is still **written and checked** by the builder, so the
    /// fixture's own guard ("a servable fixture with no tip") is live.
    #[allow(dead_code)]
    head: Option<ObjectId>,
    /// Objects that live in more than one pack, counted from the `.idx` files.
    ///
    /// Unread by any arm today; the builder still asserts it against the shape
    /// that was asked for, which is where it does its work.
    #[allow(dead_code)]
    duplicated: usize,
}

/// Build a repository whose `objects` objects live in exactly `packs` packfiles.
fn packed_fixture(packs: usize, objects: usize) -> Result<(PathBuf, Vec<ObjectId>)> {
    let f = packed_fixture_shaped(PackShape::probed(packs, objects))?;
    Ok((f.dir, f.ids))
}

/// Where a [`packed_fixture`] keeps its packs **in git's own layout**, whatever
/// engine is serving the repository beside them.
///
/// Deliberately NOT `objects/pack`. That directory is the gix engine's *store*,
/// and `gunnar_bench::store_bytes::classify` is right to call a repository
/// carrying both it and a `gunnar-git.znippy/` "two engines' layouts in one
/// repository". Writing the corpus there and nowhere else is what made every
/// znippy column of this file measure an empty store: the packs went to disk in
/// git's shape and the engine — which is what an arm then opens and probes —
/// was never handed them.
///
/// Three readers need a v2 `.idx` and are asking about an index **structure**
/// rather than about a store, so they read this directory on both engines and
/// are engine-neutral by construction instead of by accident:
///
/// * [`objects_by_pack_residency`] — the fixture's own shape census;
/// * [`sole_idx`] → `gunnar.oid_index`, which benchmarks `Fanout16` and
///   `OidStree32` against `idx_bsearch` and opens no store at all;
/// * [`count_packs_in`], for the "did this fixture end with the number of packs
///   its name claims" guard.
///
/// The bytes are duplicated on the gix arm — once here, once in `objects/pack`
/// after the engine indexes them — and that is the price of the split: ~166 MiB
/// on the million-object corpus, paid once per machine per store root. The
/// alternative was a fixture that is one engine's layout labelled as both.
fn pack_corpus_dir(dir: &Path) -> PathBuf {
    dir.join("pack-corpus")
}

/// [`packed_fixture`] with the shape spelled out. See [`PackShape`].
///
/// The old multi-pack A/B arm that rewrote the store index was retired with the
/// gix engine; this fixture builder remains for serving and lookup arms.
///
/// # The corpus is landed through the ENGINE, and the engine is asked to prove it
///
/// Every pack goes to two places: [`pack_corpus_dir`] in git's own layout, for
/// the arms that read a v2 `.idx`, and `GitOps::put_pack` on the store this
/// process's `GUNNAR_ENGINE` selects, which is what every arm built on this
/// fixture actually probes and serves from. Before 2026-08-14 only the first
/// happened, so on all four znippy columns the store was created, left at a
/// 0-byte `objects.pack`, and measured: `gunnar.lookup_1m` reported *"only 0 of
/// 10000 hit probes were found"* and had done for as long as anyone had run it.
fn packed_fixture_shaped(shape: PackShape) -> Result<PackedFixture> {
    let PackShape {
        packs,
        objects,
        overlap,
        served,
    } = shape;
    let name = shape.cache_name();
    let ids_path = fixture_root().join(&name).join("oids.bin");
    let head_path = fixture_root().join(&name).join("head.txt");
    let dir = cached_repo(&name, |dir| {
        let store = gunnar_bench::engine::open_repo(dir, HASH)?;
        // Generate into memory first: the ids have to exist before a pack can be
        // written, and writing a million loose objects only to pack them would
        // cost an hour of inodes for nothing.
        let mem = MemGitServe::new(HASH);
        let ids = build_blobs(&mem, HASH, 0x10_0CD0 + packs as u64, objects, 96)?;
        let odb = GunnarObjects(&mem);
        let pack_dir = pack_corpus_dir(dir);
        std::fs::create_dir_all(&pack_dir)?;
        let per = objects.div_ceil(packs);
        // The history, when the shape asked for one: one tree and one commit per
        // pack, chained, each tree naming that pack's own blobs. A fetch from the
        // last commit therefore walks every blob in the store, which is what
        // makes the fixture SERVABLE — the thing it could not do before
        // 2026-08-06 and the reason no arm built on it had ever looked at bytes
        // a client receives.
        let mut history: Vec<(ObjectId, ObjectId)> = Vec::new(); // (tree, commit)
        if served {
            // A second pack, for the graph over the blobs `build_blobs` already
            // landed. `GitOps` has no single-object write (see `build_history`),
            // so these are sealed and landed together once the loop is done.
            let mut graph = PackBuilder::new(HASH);
            let mut parent: Option<ObjectId> = None;
            for (c, chunk) in ids.chunks(per).enumerate() {
                let mut entries: Vec<(String, ObjectId)> = chunk
                    .iter()
                    .enumerate()
                    .map(|(i, id)| (format!("p{c:05}_{i:07}.bin"), *id))
                    .collect();
                // gix rejects a tree whose entries are not sorted by name.
                entries.sort_by(|a, b| a.0.cmp(&b.0));
                let mut tree = Vec::new();
                for (n, id) in &entries {
                    tree.extend_from_slice(b"100644 ");
                    tree.extend_from_slice(n.as_bytes());
                    tree.push(0);
                    tree.extend_from_slice(id.as_bytes());
                }
                let tree_id = graph.add(gix_object::Kind::Tree, tree)?;
                let at = 1_700_000_000i64 + c as i64;
                let mut text = format!("tree {}\n", tree_id.to_hex());
                if let Some(p) = parent {
                    text.push_str(&format!("parent {}\n", p.to_hex()));
                }
                text.push_str(&format!(
                    "author Gunnar Bench <bench@gunnar.invalid> {at} +0000\n\
                     committer Gunnar Bench <bench@gunnar.invalid> {at} +0000\n\npack {c}\n"
                ));
                let commit_id = graph.add(gix_object::Kind::Commit, text.as_bytes().to_vec())?;
                parent = Some(commit_id);
                history.push((tree_id, commit_id));
            }
            let sealed = graph.finish().context("sealing the fixture's graph pack")?;
            mem.put_pack(&sealed)
                .context("landing the fixture's graph pack")?;
        }
        for (c, chunk) in ids.chunks(per).enumerate() {
            // The production shape. `index-pack --fix-thin` splices a delta base
            // that arrived thin into the pack it completes, so the base ends up
            // in two packs at once — and THAT is the only condition under which a
            // multi-pack index has to attribute an object to one of them and
            // leave the other's entry list short. Reproduce it directly, by
            // writing the tail of the previous chunk into this pack as well,
            // rather than by driving twelve real pushes.
            let mut want: Vec<ObjectId> = chunk.to_vec();
            if overlap > 0 && c > 0 {
                let prev = &ids[(c - 1) * per..c * per];
                want.extend(prev.iter().rev().take(overlap.min(prev.len())).copied());
            }
            if let Some((tree, commit)) = history.get(c) {
                want.push(*tree);
                want.push(*commit);
            }
            let mut buf = Vec::new();
            gunnar_pack::write_pack(&want, &odb, &mut buf, &WriteOptions::new(HASH))?;
            let mut reader = std::io::Cursor::new(&buf[..]);
            gunnar_pack::index_pack(&mut reader, Some(&pack_dir), &IndexOptions::new(HASH), None)?;
            // …and the SAME bytes into the store the arms will open. This line
            // is the whole of root cause #1: without it a znippy-served fixture
            // is a `gunnar-git.znippy/objects.pack` of zero bytes standing
            // beside a corpus the engine has never seen, and `GitOps::has`
            // answers `false` for every object the fixture is named after.
            //
            // The engine decides what "stored" means: gix re-indexes into
            // `objects/pack` (one pack per call, so the declared shape
            // survives), znippy appends the verbatim bytes into its one
            // `objects.pack`.
            store
                .git()
                .put_pack(&buf)
                .with_context(|| format!("landing pack {c} of {packs} in the store"))?;
        }
        // `.keep` files stop a repack from collecting a pack whose refs are not
        // installed yet. Nothing here repacks, and leaving them makes the
        // fixture's shape depend on git's collection policy.
        for entry in std::fs::read_dir(&pack_dir)? {
            let path = entry?.path();
            if path.extension().is_some_and(|e| e == "keep") {
                std::fs::remove_file(path)?;
            }
        }
        let mut raw = Vec::with_capacity(ids.len() * HASH.len_in_bytes());
        for id in &ids {
            raw.extend_from_slice(id.as_slice());
        }
        std::fs::write(dir.join("oids.bin"), &raw)?;
        // A ref, so the store is a repository. `stages::serve_on` names its want
        // directly, but a store whose only branch is absent is not the thing a
        // client talks to, and the advertisement is half of what a fetch is.
        if let Some((_, tip)) = history.last() {
            // `RefEdit`/`FullName`/`Target` went with `RefStore`. The contract's
            // spelling is a `RefUpdate` row applied through `GitOps::put_refs`,
            // and the name is a plain `&str` the engine validates.
            store
                .git()
                .put_refs(&[RefUpdate::set("refs/heads/main", tip.to_hex().to_string())])?;
            std::fs::write(dir.join("head.txt"), tip.to_hex().to_string())?;
        }
        drop(store);
        Ok(())
    })?;
    let raw = std::fs::read(&ids_path).with_context(|| format!("reading {ids_path:?}"))?;
    let width = HASH.len_in_bytes();
    let ids: Vec<ObjectId> = raw
        .chunks_exact(width)
        .map(ObjectId::from_bytes_or_panic)
        .collect();
    // The guard the corpus never had. A fixture that asked for `objects` and
    // produced a fraction of them makes every arm built on it measure a store of
    // a size nobody chose, under a name that says otherwise, and nothing else
    // in the arm can tell, because duplicated ids are still found and still
    // missed exactly as the assertions expect.
    let distinct: std::collections::BTreeSet<&ObjectId> = ids.iter().collect();
    ensure!(
        distinct.len() == objects && ids.len() == objects,
        "the fixture at {dir:?} holds {} ids of which {} are distinct, not the {objects} it is \
         named for. Every rate measured against it would describe a store of the wrong size",
        ids.len(),
        distinct.len()
    );

    // ── the shape, measured off disk rather than restated ───────────────────
    //
    // The whole point of naming the shape is that it can now be WRONG, and say
    // so. `objects_by_pack_residency` reads the `.idx` files, so this is the
    // corpus as it is, not the request that produced it.
    //
    // Read off `pack_corpus_dir` and NOT off the store: on znippy the store has
    // one container whatever the declared shape is, so `count_packs` there
    // cannot tell a 1-pack fixture from a 16-pack one. The corpus keeps git's
    // layout on both engines precisely so this census stays the same census.
    let corpus = pack_corpus_dir(&dir);
    let actual_packs = count_packs_in(&corpus);
    ensure!(
        actual_packs == packs,
        "the fixture at {dir:?} ended with {actual_packs} pack(s) in {corpus:?}, not the \
         {packs} its name claims — every arm built on it is measuring a different store"
    );
    let (_, duplicated) = objects_by_pack_residency(&corpus)?;
    if overlap == 0 {
        ensure!(
            duplicated == 0,
            "a fixture declared DISJOINT holds {duplicated} object(s) in more than one pack. \
             That is the production shape, not this one, and `gunnar.lookup_1m_64pack`'s series \
             is measured against the disjoint one"
        );
    } else {
        ensure!(
            duplicated > 0,
            "a fixture asked for an overlap of {overlap} per pack holds NO object in more than \
             one pack, so a multi-pack index over it has nothing to attribute and every arm \
             built on it is blind to the defect it exists for"
        );
    }
    let head = if served {
        let hex = std::fs::read_to_string(&head_path)
            .with_context(|| format!("reading {head_path:?} — a servable fixture with no tip"))?;
        Some(ObjectId::from_hex(hex.trim().as_bytes())?)
    } else {
        None
    };

    // ── the ENGINE holds the corpus, asked of the engine ────────────────────
    //
    // Outside `cached_repo`, so it runs on the cheap path too: a fixture built
    // by a version of this file that did not land the packs in the store is
    // still on disk with its `.gunnar-bench-complete` marker written, and a
    // guard that only ran during generation would never see it again.
    //
    // Three ids rather than one — first, middle, last — because the packs are
    // landed in chunk order and a store that took the first `put_pack` and
    // dropped the rest answers the first probe exactly like a healthy one.
    // Three rather than all of them because the million-object corpus is the
    // point of the cache.
    //
    // SEEN RED 2026-08-14, by removing the `store.git().put_pack(&buf)` above
    // and letting the corpus go to disk only — which is exactly the state every
    // znippy fixture root was in:
    //
    //   the store at ".../packed-20000o-1p-v3" does not hold the first of its
    //   20000 declared objects (217843ab…). The corpus is on disk in
    //   ".../packed-20000o-1p-v3/pack-corpus" and the engine has never been
    //   handed it — which is a fixture that probes as 0 hits out of every
    //   probe, reads as a store nobody wrote, …
    //
    // Fired on the cached path too (0.01 s, no rebuild), which is the half that
    // matters: a guard that only ran during generation would never see a
    // fixture already on disk with its completion marker written.
    {
        let store = gunnar_bench::engine::open_repo(&dir, HASH)
            .with_context(|| format!("opening the packed fixture at {dir:?} to check it"))?;
        let git = store.git();
        for (label, id) in [
            ("the first", ids[0]),
            ("the middle", ids[ids.len() / 2]),
            ("the last", ids[ids.len() - 1]),
        ] {
            ensure!(
                git.has(id.as_slice())?,
                "the store at {dir:?} does not hold {label} of its {objects} declared objects \
                 ({}). The corpus is on disk in {corpus:?} and the engine has never been \
                 handed it — which is a fixture that probes as 0 hits out of every probe, \
                 reads as a store nobody wrote, and is what every znippy column of this file \
                 measured before 2026-08-14. Delete the fixture directory to rebuild it.",
                id.to_hex()
            );
        }
    }

    Ok(PackedFixture {
        dir,
        ids,
        head,
        duplicated,
    })
}

/// How many `.pack` files sit in **one named directory**.
///
/// The filesystem question, asked only where a filesystem answer is the right
/// one: a [`pack_corpus_dir`] this file wrote itself, or a clone stock `git`
/// produced. It is NOT the answer to "how big is this store" — see
/// [`count_packs`], which is what a repository must be asked.
fn count_packs_in(pack_dir: &Path) -> usize {
    std::fs::read_dir(pack_dir)
        .map(|rd| {
            rd.filter_map(|e| e.ok())
                .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
                .count()
        })
        .unwrap_or(0)
}

/// **How many pack containers a REPOSITORY's object store holds — on either
/// engine.**
///
/// # Root cause #2, and why the old body could only ever be right once
///
/// This was `read_dir(dir/objects/pack).count()` with an `unwrap_or(0)`, and it
/// has thirteen call sites, several of them hard guards spelled
/// `count_packs(&repo) > 0`. A znippy store has no `objects/` directory at all —
/// its bytes are one append-only `gunnar-git.znippy/objects.pack` — so on all
/// four znippy columns it returned **0 forever**, and `unwrap_or(0)` made a
/// directory that does not exist indistinguishable from a repository that is
/// empty. That is the wrapper failure exactly: 0 is not a small measurement, it is
/// the shape of an instrument pointed at the wrong place.
///
/// So the question goes to the store's own layout, through the one counter this
/// workspace already has for it: `gunnar_bench::store_bytes`, which detects the
/// shape rather than being told it, and **refuses** a directory whose shape it
/// does not recognise instead of returning a confident zero.
///
/// # `1` on znippy is the honest answer, not a placeholder
///
/// `StoreBytes::containers` is documented to be read literally: the znippy
/// engine appends every pack it is handed into one blob region, so a store
/// holding twelve pushes reads `containers = 1` with the payload of all twelve.
/// Counting the appended packs would mean parsing `objects.pack.journal`, a
/// znippy-internal format `bench/` has no business knowing. Every guard here
/// that reads this asks *"is there a store at all"* or *"did the store grow"*;
/// the first is answered correctly by 1, and the second is answered by
/// [`store_payload_bytes`], which moves on both engines.
///
/// # It returns `Result`, and that is half the fix
///
/// The old body ended in `.unwrap_or(0)`. Every caller therefore received a
/// number where an error belonged, and the two are not interchangeable in the
/// one direction that matters: `packs_last == packs_first` — `constellation`'s
/// read-only control — is *satisfied* by `0 == 0`, so an instrument that had
/// stopped working reported the control's whole purpose as achieved.
fn count_packs(dir: &Path) -> Result<usize> {
    Ok(gunnar_bench::store_bytes::of(dir)?.containers as usize)
}

/// The verbatim pack payload a repository's store holds, in bytes — the number
/// that MOVES on both engines when objects land.
///
/// [`count_packs`] cannot answer "did this store grow": on znippy it is pinned
/// at 1 by construction. Any guard about growth has to read this instead.
fn store_payload_bytes(dir: &Path) -> Result<u64> {
    Ok(gunnar_bench::store_bytes::of(dir)?.payload)
}

/// **Did the run pile up enough store to be the case it claims to measure?**
///
/// One helper and two callers — `gunnar.year` and `gunnar.rewrite` — because
/// each held its own copy of the same engine-blind floor (one implementation):
///
/// ```text
/// ensure!(packs_peak > 32.0, "… `gix_odb::Store` sizes its slot table ONCE …")
/// ```
///
/// **That floor is a fact about `gix_odb::Store`, not about a year.** It is
/// asking whether the run reached the 32-slot table a fresh gix repository
/// opens with. A znippy store has no slot table and no second pack: it appends
/// every pack it is handed into ONE `objects.pack`, so `packs` reads 1 for the
/// whole run — see [`count_packs`] — and `packs_peak > 32` is a floor that
/// engine can never clear, on any workload, however long. Both arms would have
/// gone red on all four znippy columns for a reason that says nothing about
/// either arm's subject.
///
/// So the floor is asked in the unit the store actually has:
///
/// * **git shape** — pack COUNT, peak above [`STORE_ACCUMULATION_FLOOR`],
///   exactly as before. The gix column keeps the assertion it had, word for
///   word, because the claim is true there.
/// * **any other shape** — the payload BYTES, which do move: the run must have
///   added at least [`STORE_ACCUMULATION_FLOOR`] **typical rounds' worth** of
///   payload beyond where it started.
///
/// # Why the byte floor is not a multiple of the starting size
///
/// It was: `last >= first * 1.5`. That is a ratio against the SEED, and the
/// seed is a real packed corpus this workload did not write — 4.05 MB of `knut`
/// on both callers. `gunnar.rewrite` moved it 4 054 926 -> 4 308 669 bytes in
/// fifty rounds (1.06x) and `gunnar.year` 4 048 611 -> 5 108 408 in fifty-two
/// weeks (1.26x), both measured on the desktop 2026-08-14 — so **both arms were red on
/// every znippy column for a floor that would have needed some four hundred
/// rounds against that corpus**, and nothing about either failure was a
/// statement about the engine, the workload or the arm.
///
/// The git branch never had that problem, because a pack count starts at 1
/// however big the seed is: `peak > 32` asks *did this workload's own pushes
/// pile up*. So the byte branch asks the same seed-independent question in the
/// unit it has — how much did this run ADD, in units of what one of its own
/// rounds adds. On a workload with uniform rounds the two coincide exactly: 33
/// rounds of equal growth reads as 32 steps, which is 33 packs on the other
/// shape. `STORE_ACCUMULATION_FLOOR` is therefore one constant serving both
/// branches rather than two numbers that have to be kept in agreement.
///
/// The step is the **median** round, not the first and not the mean. The mean
/// is degenerate — `added / mean` is the round count by definition and could
/// never fail — and one arbitrary round is the identity-value trap wearing a
/// denominator's clothes. The median also lets an accelerating run (the year's
/// user ramp) clear the floor by having genuinely accelerated, which is the
/// thing being claimed.
///
/// Returns what it measured — the pack peak, or the number of typical rounds'
/// worth the run accumulated — so the caller can put it on the row.
fn store_accumulated(
    shape: gunnar_bench::store_bytes::StoreShape,
    rows: &[Value],
    knob: &str,
    workload: &str,
) -> Result<f64> {
    let n = |row: &Value, k: &str| row[k].as_f64().unwrap_or_default();
    if shape == gunnar_bench::store_bytes::StoreShape::Git {
        let series: Vec<f64> = rows.iter().map(|r| n(r, "packs")).collect();
        let peak = series.iter().cloned().fold(0.0f64, f64::max);
        ensure!(
            peak > STORE_ACCUMULATION_FLOOR,
            "the repository peaked at {peak} pack(s), round by round {series:?}. \
             `gix_odb::Store` sizes its slot table ONCE at open and a fresh repository gets 32 \
             slots, so a run that never passes {STORE_ACCUMULATION_FLOOR} cannot reach the case \
             this arm is the worst for. This is a green that has not been earned; raise {knob}. \
             {workload}"
        );
        return Ok(peak);
    }
    let series: Vec<f64> = rows.iter().map(|r| n(r, "store_payload_b")).collect();
    let first = series.first().copied().unwrap_or(0.0);
    let last = series.last().copied().unwrap_or(0.0);
    ensure!(
        first > 0.0,
        "the first round read a store payload of {first} bytes, so there is nothing for the \
         growth floor to be a multiple of and `the store grew` would be true of any number at \
         all. {workload}"
    );
    let mut steps: Vec<f64> = series.windows(2).map(|w| w[1] - w[0]).collect();
    ensure!(
        !steps.is_empty(),
        "the run produced one row, so there is no round-to-round growth to measure a floor in. \
         {workload}"
    );
    steps.sort_by(|a, b| a.partial_cmp(b).expect("payload bytes are finite"));
    let median = steps[steps.len() / 2];
    ensure!(
        median > 0.0,
        "the median round of this run added {median} byte(s) to the store's payload, over \
         {} round(s) {series:?}. A run whose typical round writes nothing has not accumulated \
         anything, and dividing by it would make the floor below true of any number. {workload}",
        steps.len()
    );
    let accumulated = (last - first) / median;
    ensure!(
        accumulated > STORE_ACCUMULATION_FLOOR,
        "this store appends every pack into one blob region, so its container count is pinned \
         at 1 and the pack-count floor cannot be asked of it. The floor is the payload instead, \
         and NOT as a multiple of the seed corpus this workload did not write: the run added \
         {} byte(s) on top of {first}, which is {accumulated:.1} typical round(s)' worth against \
         a median round of {median} — floor {STORE_ACCUMULATION_FLOOR}. Round by round \
         {series:?}. A run that barely moved the store has not reached the case this arm is the \
         worst for; raise {knob}. {workload}",
        last - first
    );
    Ok(accumulated)
}

/// **How much a run has to pile up before it is the case these arms claim to
/// measure** — 32, in whichever unit the store has one.
///
/// It is `gix_odb::Store`'s slot table: a fresh repository is opened with 32
/// slots sized ONCE, and the defect both callers exist to reach is what happens
/// on the thirty-third. On the git shape that is literally the pack count. On an
/// engine that appends every pack into one blob region it is 32 rounds' worth of
/// payload, which is the same workload measured in the only unit that moves.
/// See [`store_accumulated`] for why it is one constant and not two.
const STORE_ACCUMULATION_FLOOR: f64 = 32.0;

/// Wait until `objects/pack` has been quiet long enough that the store is
/// willing to trust its own scan of it.
///
/// `gunnar-store`'s pack-directory watch (`crate::odb`) will only skip
/// a re-scan when the mtime it memoised was already in the past when it took it.
/// A filesystem timestamp comes from a coarse clock, so a change inside the
/// same tick as the scan is invisible and the memo would be unsound. **A
/// repository that was written to a moment ago is therefore deliberately not in
/// the steady state**, and it is the steady state this arm is about: every
/// `have` and every `want` from every client, against a repository nobody is
/// pushing to at that instant.
///
/// Without this the arm would measure the racy window instead, and on a
/// first-ever run, when the fixture has just been generated, it would measure
/// nothing else. The "just after a write" number is reported separately by
/// [`Lookup1m64Pack`] rather than left implicit.
///
/// **Whichever directory this store keeps its bytes in.** `objects/pack` is
/// absent on the znippy arm, so the old body's `metadata()` failed, `age` was
/// `None`, and the loop ran its full sixty iterations — six seconds of sleeping
/// per call, on every znippy run, waiting for a directory that was never going
/// to appear.
fn settle(dir: &Path) {
    let pack_dir = if dir.join("gunnar-git.znippy").is_dir() {
        dir.join("gunnar-git.znippy")
    } else {
        dir.join("objects/pack")
    };
    for _ in 0..60 {
        let age = std::fs::metadata(&pack_dir)
            .and_then(|m| m.modified())
            .ok()
            .and_then(|m| std::time::SystemTime::now().duration_since(m).ok());
        if age.is_some_and(|a| a.as_secs_f64() > 1.3) {
            return;
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
}

/// The probe set for one fixture: hits scattered across the whole id space, and
/// well-formed ids that are certainly absent.
fn probe_set(ids: &[ObjectId], probes: usize) -> (Vec<ObjectId>, Vec<ObjectId>) {
    // A deterministic scatter, so the probe cannot sit inside one pack (or one
    // page) and report a cache hit rate as a lookup rate.
    let mut rng = Lcg::new(0x9E37_79B9);
    let hits: Vec<ObjectId> = (0..probes)
        .map(|_| ids[(rng.next_u64() % ids.len() as u64) as usize])
        .collect();
    (hits, miss_ids(ids, probes))
}

/// Run the hit and miss probes against an already-open store.
///
/// The store is the caller's because this arm now measures the **same**
/// repository in more than one configuration (with and without a multi-pack
/// index), and reopening between them would put a store-open (and the index
/// write that comes with it) inside the bracket.
fn probe_open(
    store: &dyn gunnar_store::RepoStore,
    hits: &[ObjectId],
    misses: &[ObjectId],
) -> Result<(f64, f64, usize, usize)> {
    let git = store.git();
    // `GitOps::has` is the contract's single membership probe, and it is
    // fallible where `ObjectStore::contains` was not. An error is NOT collapsed
    // to `false` here: this arm's whole subject is the hit and miss rates, and a
    // store that erred on every probe would otherwise publish a spectacular
    // miss rate and a `found` of zero — a broken store reported as a fast one.
    let has = |id: &ObjectId| -> Result<bool> { git.has(id.as_bytes()) };

    // Warm both paths once, so the first probe does not pay for opening every
    // `.idx`, or for the pack-directory scan that latches the staleness watch,
    // and get it charged to the rate.
    has(&hits[0])?;
    has(&misses[0])?;

    let start = Instant::now();
    let mut found = 0usize;
    for id in hits {
        if has(id)? {
            found += 1;
        }
    }
    let hit_secs = start.elapsed().as_secs_f64();

    let start = Instant::now();
    let mut absent = 0usize;
    for id in misses {
        if !has(id)? {
            absent += 1;
        }
    }
    let miss_secs = start.elapsed().as_secs_f64();

    Ok((
        hits.len() as f64 / hit_secs,
        misses.len() as f64 / miss_secs,
        found,
        absent,
    ))
}

/// Open a fixture, let it settle, and run the probes.
fn probe(dir: &Path, ids: &[ObjectId], probes: usize) -> Result<(f64, f64, usize, usize)> {
    let store = gunnar_bench::engine::open_repo(dir, HASH)?;
    settle(dir);
    let (hits, misses) = probe_set(ids, probes);
    probe_open(store.as_ref(), &hits, &misses)
}

/// 10k random oids against a million objects in **one** pack.
///
/// **Red condition: hit and miss are measured separately, and both must be
/// real.** O(packs) is a miss-path complexity, so a suite reporting one blended
/// `lookups_per_sec` cannot see it at all; that is the whole reason for two
/// metrics. `found` must equal the probe count and `absent` must equal the probe
/// count, both asserted. A "miss" probe that accidentally hits, or a store that
/// answers `false` to everything, would otherwise produce a beautiful number
/// describing nothing.
struct Lookup1m;

fn lookup_shape() -> (usize, usize) {
    (
        env_usize("GUNNAR_BENCH_LOOKUP_OBJECTS", scaled(1_000_000, 20_000)),
        env_usize("GUNNAR_BENCH_LOOKUP_PROBES", scaled(10_000, 2_000)),
    )
}

impl Bencher for Lookup1m {
    fn id(&self) -> &'static str {
        "gunnar.lookup_1m"
    }
    fn run(&self) -> Result<BenchResult> {
        let (objects, probes) = lookup_shape();
        let (dir, ids) = packed_fixture(1, objects)?;
        let (hit_rate, miss_rate, found, absent) = probe(&dir, &ids, probes)?;
        ensure!(
            found == probes,
            "only {found} of {probes} hit probes were found"
        );
        ensure!(
            absent == probes,
            "only {absent} of {probes} miss probes were absent"
        );
        Ok(result(
            self.id(),
            vec![
                ("lookups_per_sec_hit", json!(round(hit_rate, 1))),
                ("lookups_per_sec_miss", json!(round(miss_rate, 1))),
                ("objects", json!(objects as f64)),
                ("probes", json!(probes as f64)),
                // The CORPUS's pack count, which is the shape the arm's name
                // claims and is the same number on both engines. The store's
                // own container count is reported beside it, because on znippy
                // it is 1 for any corpus and a reader comparing the two columns
                // has to be able to see that rather than infer it.
                (
                    "packs",
                    json!(count_packs_in(&pack_corpus_dir(&dir)) as f64),
                ),
                ("store_containers", json!(count_packs(&dir)? as f64)),
                ("store_payload_b", json!(store_payload_bytes(&dir)? as f64)),
            ],
        ))
    }
}
nornir_bench::register_bench!(Lookup1m);

// gunnar.multi_pack_serve: a clone served off a store that grew a multi-pack
// index by itself, judged by both receivers
// ───────────────────────────────────────────────────────────────────────────

/// Files in the multi-pack serve fixture, and pack rounds per commit.
///
/// Sixteen files touched three at a time over forty-eight commits, which is
/// `crates/gunnar-wire/tests/pack_copy.rs`'s `DUP_FILES` shape and is chosen for
/// the same measured reason: the corpus needs one blob to be a delta base inside
/// an early pack **and** to be spliced into a later one by `index-pack
/// --fix-thin`, which needs a file revised more than once inside one push round.
/// At sixty files that almost never happens; at sixteen it does, and this
/// fixture ends with 133 of 240 objects living in more than one pack.
const MIDX_FILES: usize = 16;
const MIDX_COMMITS: usize = 48;
const MIDX_LINES: usize = 400;
const MIDX_TOUCHED: usize = 3;

/// `git`'s spelling of [`HASH`], for `init` and for an `index-pack` run outside
/// any repository — where git assumes sha1 and would refuse a sha256 pack for a
/// reason that looks exactly like the defect this arm is about.
fn hash_arg() -> &'static str {
    hash_arg_of(HASH)
}

/// [`hash_arg`] for a kind that is not [`HASH`].
///
/// Split out because `gunnar.clone_no_ofs_delta` runs on the **sha1**
/// head-to-head fixture while [`HASH`] is sha256, and handing `git index-pack`
/// the wrong `--object-format` makes it refuse a perfectly good pack with an
/// error that reads exactly like a corruption.
fn hash_arg_of(kind: Kind) -> &'static str {
    match kind {
        Kind::Sha1 => "--object-format=sha1",
        _ => "--object-format=sha256",
    }
}

/// A bare repository in the **production shape** — one pack per push, no
/// repack, no `.bitmap` — with enough packs that opening it writes a
/// `multi-pack-index`.
///
/// # This may not be built by cloning, and that is the whole point
///
/// `git clone --bare` **repacks**, so a corpus built that way is single-pack and
/// cannot hold one object in two packs. The other multi-pack fixture in this
/// file, [`packed_fixture`], writes `ids.chunks(per)` — **disjoint** packs, one
/// residency per object — and is only ever probed for lookups, never served.
/// Between them those two shapes are why `0d10e17`'s defect reached a customer
/// build: no arm here had ever served a clone off a store where an object lives
/// in two packs with an index over them.
///
/// Returns the bare repository and its tip.
fn midx_pushed_fixture(rounds: usize) -> Result<(PathBuf, ObjectId)> {
    let name = format!(
        "midx-pushed-{MIDX_FILES}f-{rounds}p-{MIDX_COMMITS}c-{hash}-2",
        hash = hash_arg()
    );
    let dir = cached_repo(&name, |dir| {
        let work = dir.join("seed");
        let origin = dir.join("origin.git");
        std::fs::create_dir_all(&work)?;
        h2h_git(
            &[
                "init",
                "-q",
                "-b",
                "main",
                hash_arg(),
                &work.to_string_lossy(),
            ],
            None,
        )?;
        let at = |a: &[&str]| h2h_git(a, Some(&work)).map(|_| ());
        at(&["config", "user.email", "bench@gunnar.invalid"])?;
        at(&["config", "user.name", "gunnar bench"])?;

        let mut content: Vec<Vec<String>> = (0..MIDX_FILES)
            .map(|f| {
                (0..MIDX_LINES)
                    .map(|i| format!("line {i:04} of file {f:03}, original text"))
                    .collect()
            })
            .collect();
        for revision in 0..MIDX_COMMITS {
            for slot in 0..MIDX_TOUCHED.min(MIDX_FILES) {
                let f = (revision * MIDX_TOUCHED + slot) % MIDX_FILES;
                // ~2% of the lines rewritten: a diff, not a rewrite. A whole new
                // file every commit would delta to nothing, and git's search
                // would leave no cross-pack base for the index to deduplicate.
                for step in 0..MIDX_LINES / 50 {
                    let index = (revision * 7 + step * 53) % MIDX_LINES;
                    content[f][index] = format!("line {index:04} rewritten at revision {revision}");
                }
                let file = format!("churn-{f:03}.txt");
                std::fs::write(work.join(&file), content[f].join("\n"))?;
                at(&["add", &file])?;
            }
            at(&["commit", "-q", "-m", &format!("revision {revision}")])?;
        }

        h2h_git(
            &[
                "init",
                "-q",
                "-b",
                "main",
                "--bare",
                hash_arg(),
                &origin.to_string_lossy(),
            ],
            None,
        )?;
        // Without this git unpacks a small push into loose objects and the
        // multi-pack state this arm is about never arises.
        h2h_git(&["config", "transfer.unpackLimit", "1"], Some(&origin))?;
        for r in 1..=rounds {
            let behind = MIDX_COMMITS - MIDX_COMMITS * r / rounds;
            at(&[
                "push",
                "-q",
                &origin.to_string_lossy(),
                &format!("+HEAD~{behind}:refs/heads/main"),
            ])?;
        }
        // Deliberately no repack, no gc, no `--prune`: the packs ARE the fixture.
        Ok(())
    })?;
    let origin = dir.join("origin.git");
    let head = ObjectId::from_hex(
        h2h_git(
            &["-C", &origin.to_string_lossy(), "rev-parse", "HEAD"],
            None,
        )?
        .trim()
        .as_bytes(),
    )?;
    Ok((origin, head))
}

/// `(distinct objects, how many live in more than one pack)`, read out of the
/// v2 `.idx` files themselves.
///
/// First-party on purpose. `git verify-pack -v` and `git index-pack` both refuse
/// a duplicate at the door and print nothing, so a census taken through either
/// is an assertion that can never fail — and the v2 index is the one listing of
/// a pack's entries that survives the defect this arm exists for. The layout is
/// fixed and small: the magic `\xfftOc`, a version, a 256-cell fanout whose last
/// cell is the object count, then that many raw hashes.
///
/// Takes the **pack directory**, not the repository: the two callers keep their
/// indexes in different places — `midx_pushed_fixture`'s is a stock-git
/// repository's `objects/pack`, `packed_fixture_shaped`'s is its engine-neutral
/// [`pack_corpus_dir`] — and joining `objects/pack` in here silently counted
/// zero indexes for the second, which is a census that agrees with everything.
fn objects_by_pack_residency(pack_dir: &Path) -> Result<(usize, usize)> {
    let mut per_id: std::collections::HashMap<Vec<u8>, usize> = std::collections::HashMap::new();
    let mut indices: Vec<PathBuf> = std::fs::read_dir(pack_dir)?
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.extension().is_some_and(|x| x == "idx"))
        .collect();
    indices.sort();
    for idx in &indices {
        for name in idx_object_ids(idx, HASH)? {
            *per_id.entry(name).or_default() += 1;
        }
    }
    Ok((per_id.len(), per_id.values().filter(|c| **c > 1).count()))
}

/// **Every object id a v2 `.idx` lists**, in the order the file stores them.
///
/// That order is ascending by id — it is what the fanout indexes into — so two
/// indexes over the same object set compare equal element by element, and a
/// `Vec` equality is a set equality without a sort or a hash.
///
/// The reader half of [`objects_by_pack_residency`], split out because a second
/// caller needs the ids themselves rather than a census of them (written once), and
/// first-party for the reason that function's doc already gives: `git
/// verify-pack -v` and `git index-pack` both refuse a duplicate at the door and
/// print nothing, so anything read *through* them cannot report one.
fn idx_object_ids(idx: &Path, hash: Kind) -> Result<Vec<Vec<u8>>> {
    let width = hash.len_in_bytes();
    let bytes = std::fs::read(idx).with_context(|| format!("reading pack index {idx:?}"))?;
    ensure!(
        bytes.starts_with(&[0xff, b't', b'O', b'c'])
            && u32::from_be_bytes(bytes[4..8].try_into()?) == 2,
        "{idx:?} is not a v2 pack index, so this census would be reading the wrong format"
    );
    let fanout_end = 8 + 256 * 4;
    let count = u32::from_be_bytes(bytes[fanout_end - 4..fanout_end].try_into()?) as usize;
    let table = bytes
        .get(fanout_end..fanout_end + count * width)
        .with_context(|| {
            format!("{idx:?} claims {count} objects but is too short to hold their ids")
        })?;
    Ok(table.chunks_exact(width).map(<[u8]>::to_vec).collect())
}

/// Total `.pack` bytes in a pack directory — git's own answer for how big this
/// history is, once `repack -adfq` has had its say.
///
/// The yardstick a server's clone is held against: it is produced by `git`, on
/// the same objects, and neither engine under test had any part in it.
fn pack_bytes_in(pack_dir: &Path) -> Result<u64> {
    let mut total = 0u64;
    for entry in std::fs::read_dir(pack_dir)
        .with_context(|| format!("reading the fixture's pack directory {pack_dir:?}"))?
    {
        let path = entry?.path();
        if path.extension().is_some_and(|x| x == "pack") {
            total += std::fs::metadata(&path)?.len();
        }
    }
    ensure!(
        total > 0,
        "{pack_dir:?} holds no .pack files, so there is nothing to size a served clone against"
    );
    Ok(total)
}

/// How a child process ended, in words, distinguishing a **refusal** from a
/// **crash**.
///
/// `ExitStatus::success()` is false for both, and the two mean opposite things
/// about the bytes we produced: a non-zero exit is the receiver's verdict on our
/// pack, a fatal signal is the receiver falling over and no verdict at all.
fn describe_exit(status: &std::process::ExitStatus) -> String {
    use std::os::unix::process::ExitStatusExt as _;
    match (status.code(), status.signal()) {
        (Some(code), _) => format!("it exited {code}"),
        (None, Some(sig)) => format!(
            "IT WAS KILLED BY SIGNAL {sig} — the receiver crashed, which is not a verdict on \
             this pack"
        ),
        (None, None) => "it ended in neither an exit code nor a signal".to_string(),
    }
}

/// **Hand `pack` to stock `git index-pack --strict` and return the object ids it
/// wrote down**, as hex.
///
/// The receiver that matters, and the only judgement in this file that neither
/// engine can influence: `git` is what a real client runs, so "git indexed it
/// and these are the objects" is the outcome, where "the server says it sent
/// 240" is a claim about the server.
///
/// Comparable against [`h2h_object_set`] without conversion — both are hex —
/// which is the whole reason this returns strings rather than the raw ids
/// [`idx_object_ids`] reads.
///
/// `--strict` because the loose check accepts a pack whose objects do not parse.
/// `tag` names the files inside `dir`, so one directory can judge several packs.
///
/// # It runs INSIDE a scratch repository, and that is not a detail
///
/// The obvious spelling is `git index-pack --strict --object-format=sha256`
/// outside any repository, and it is what stood here. MEASURED the desktop 2026-08-14,
/// **git 2.53.0**: that invocation **segfaults** — `Signal 11`, core dumped,
/// **empty stderr** — on a pack that is not connectivity-complete. It does it on
/// packs `git` itself wrote: `pack-03299ebb….pack` out of this file's own
/// `midx_pushed_fixture`, 25 entries, indexes fine without `--strict` and dies
/// with it.
///
/// An empty stderr behind a failed status is shaped exactly like a refusal, so
/// the arm reported *"stock git refused the pack this server produced:"*
/// followed by nothing — the "a malformed command fails silently and its
/// empty output is shaped exactly like a clean result", with the twist that here
/// it was shaped like somebody else's fault. Inside a `git init --bare
/// --object-format=…` scratch repository the same pack gets an honest verdict:
/// `fatal: did not receive expected object 04af66d4…`.
///
/// It is also the more faithful receiver: a real client indexes a fetched pack
/// into its repository, never into thin air.
fn objects_stock_git_accepts(
    pack: &[u8],
    dir: &Path,
    tag: &str,
    hash: Kind,
) -> Result<std::collections::BTreeSet<String>> {
    let pack_path = dir.join(format!("{tag}.pack"));
    let idx_path = dir.join(format!("{tag}.idx"));
    let repo = dir.join(format!("{tag}.git"));
    std::fs::write(&pack_path, pack).with_context(|| format!("writing {pack_path:?}"))?;
    let _ = std::fs::remove_dir_all(&repo);
    h2h_git(
        &[
            "init",
            "-q",
            "--bare",
            hash_arg_of(hash),
            &repo.to_string_lossy(),
        ],
        None,
    )
    .context("creating the scratch repository a served pack is indexed into")?;
    let out = Command::new("git")
        .current_dir(&repo)
        .args(["index-pack", "--strict", "-o"])
        .arg(&idx_path)
        .arg(&pack_path)
        .output()
        .context("running git index-pack over a served pack")?;
    // **`success()` alone reads a CRASH as a refusal.** `git index-pack
    // --strict --object-format=sha256` was seen dying on SIGSEGV here (the desktop,
    // 2026-08-14, git 2.53), and a killed process has an empty stderr — so the
    // refusal printed the server's byte count followed by nothing at all and
    // read exactly like "git looked at your pack and disliked it silently".
    // Naming the signal is the difference between a defect in what we serve and
    // a defect in the receiver.
    ensure!(
        out.status.success(),
        "stock git did not index the `{tag}` pack ({} bytes) this server produced — {}: {}",
        pack.len(),
        describe_exit(&out.status),
        String::from_utf8_lossy(&out.stderr).trim()
    );
    idx_object_ids(&idx_path, hash)?
        .into_iter()
        .map(|raw| {
            Ok(ObjectId::try_from(raw.as_slice())
                .context("an id in a pack index is not this hash's width")?
                .to_string())
        })
        .collect()
}

/// **A whole clone served off a store built by sixteen separate pushes, judged
/// by stock `git` against the object set the fixture actually reaches.**
///
/// This arm exists because its absence shipped a corrupt pack. Between
/// `fc2e0c3` (2026-08-04 20:54, the entry-length table) and `0d10e17`
/// (2026-08-05 11:47) every gunnar build served packs stock git refuses —
/// `fatal: pack is corrupted (SHA1 mismatch)` — out of any repository holding a
/// multi-pack index over packs that share an object, and no bench arm and no
/// test could see it.
///
/// # The mechanism, in one paragraph
///
/// `PackExtents` sizes an entry as the distance to the next offset in the same
/// pack, which is exact only if it holds every offset that pack has. A
/// multi-pack index keeps one **deduplicated** oid table, so an object in two
/// packs is recorded against exactly one of them and `gix-odb` builds the other
/// pack's list by filtering that table by pack — short by precisely the objects
/// the index attributed elsewhere. The entry before each hole is measured across
/// it, the copied entry carries its neighbour's bytes as a tail, and the
/// receiver parses every entry the header promised and then reads its trailer
/// from the wrong offset.
///
/// # RAISED 2026-08-14: the subject is the clone, not the index file
///
/// That paragraph describes **one engine's** storage. A `multi-pack-index` on
/// disk is how the gix arm holds sixteen packs; the znippy arm holds the same
/// history with no packfile at all, and asserting the file exists asked it a
/// question about somebody else's implementation — which is why this arm used to
/// fail on every znippy column with *"opening a 16-pack store wrote no
/// multi-pack-index"*, a true sentence about a store that was working fine.
///
/// What is worth measuring is one level up: **a store handed a history in
/// sixteen separate pushes still serves a correct clone.** So:
///
/// * the fixture is still built by sixteen real `git push`es, and the residency
///   census below is still taken — off the **source corpus**, which is a git
///   repository on every column, so it says the same thing everywhere;
/// * the corpus is then handed to whichever engine is selected, in that engine's
///   own layout, by [`engine_served`];
/// * and the clone that comes back is judged by **stock `git index-pack
///   --strict`** against `git rev-list --objects` over the fixture.
///
/// No longer asserted: that a `multi-pack-index` file exists, and that the
/// served entries arrive in any particular delta spelling. Both are mechanism.
/// The arm therefore no longer distinguishes the two engines' internals — that
/// is the point, and `storage-git-gix`'s own tests keep the entry-length
/// arithmetic honest beside the code that does it.
///
/// # Red conditions, and what each one catches
///
/// * **`objects_delivered` must equal the fixture's reachable set, id for id.**
///   The headline, and the only one that is about the product: stock git
///   indexed the served pack and these are the objects a client ends up with. A
///   server that drops, duplicates or mangles one fails here regardless of how
///   it stores anything.
/// * **stock git accepts it at all.** `fatal: pack is corrupted (SHA1
///   mismatch)` is what the original defect looked like from here.
/// * **`objects_in_two_packs > 0`.** Without a duplicate in the source corpus
///   the fixture is not the shape this arm exists for and every other assertion
///   is trivially true. It is the one that would have caught the absence.
/// * **`duplicate_oids_indexed == 0`, and the second receiver agreeing.**
///   gitoxide is kept as an independent parser — not as the subject — and this
///   is the exact failure the obvious repair to this arm produces; see below.
///
/// # The `GUNNAR_PACK_EXTENTS` A/B was removed 2026-08-14, because its switch
/// is not wired to anything
///
/// What stood here served the same store twice out of one handle, once with
/// `GUNNAR_PACK_EXTENTS=off`, and asserted the two packs were byte-identical.
/// That variable is read in exactly one place — `gunnar-pack/src/copy.rs`, by
/// `write_pack_copy` — and `write_pack_copy` has **no caller on any serve
/// path**; the only reference left in the constellation is one test in
/// `storage-git-gix`. The engine owns the format now and builds each pack's
/// entry-length table from that pack's own `.idx`, never from a multi-pack
/// index's deduplicated oid table, which is why `gunnar.multi_pack_extent_cost`
/// was tombstoned the same day for the same reason.
///
/// So the assertion was **the shipped path compared against itself**: two
/// serves that could not differ, reported as `surplus_bytes: 0` — a guard that
/// cannot fail, which is worse than no guard because it reads as
/// coverage. MEASURED the desktop 2026-08-14: with the fix in place both serves
/// produced 111 581 bytes, and with the emitter deliberately broken they
/// produced the *same wrong* bytes twice. The historical numbers it used to
/// carry are kept because they are real: 217 314 against 201 177 (+16 137, 8.0%
/// of the pack) on this fixture on 2026-08-06, and 46 080 of 8 066 803 (0.57%)
/// on `knut.git`.
///
/// # What replaced it, and why this one can fail
///
/// Both receivers now write an index, and this arm **reads both `.idx` files
/// itself** and holds them to each other and to the served object count. That
/// catches the defect the obvious repair here introduces: swapping `index_pack`
/// for `index_thin_pack(…, store)` makes gitoxide "succeed" and write **309
/// entries for a 240-object pack, 69 of them duplicate oids**, because
/// `LookupRefDeltaObjectsIter` resolves every ref-delta through the lookup with
/// no in-pack check. `IndexedPack::num_objects` alone does not see that — 309 is
/// a number, and the old `objects` assertion would simply have called it wrong
/// rather than saying what was wrong with it.
///
/// # What the row is worth as a measurement
///
/// `wire_bytes` is the cost of serving this store shape at all: 240 objects,
/// 132 of them resident in two of the sixteen packs.
struct MultiPackServe;

impl Bencher for MultiPackServe {
    fn id(&self) -> &'static str {
        "gunnar.multi_pack_serve"
    }
    fn run(&self) -> Result<BenchResult> {
        let rounds = env_usize("GUNNAR_BENCH_MIDX_PACKS", MULTI_PACK_INDEX_THRESHOLD);
        ensure!(
            rounds >= MULTI_PACK_INDEX_THRESHOLD,
            "{rounds} packs is below MULTI_PACK_INDEX_THRESHOLD ({MULTI_PACK_INDEX_THRESHOLD}), \
             so no index would be written and this arm would serve the configuration it is not \
             about"
        );
        let (origin, head) = midx_pushed_fixture(rounds)?;

        // **The census is taken off the SOURCE corpus**, which is a repository
        // stock `git push` built and is therefore the same on every column.
        // Asking the store instead would answer 1 on znippy for any number of
        // rounds, which is a true statement about a different question — and
        // asking it for a `multi-pack-index` file, as this arm used to, is a
        // question only one engine has an answer to at all.
        let pack_dir = origin.join("objects/pack");
        let packs = count_packs_in(&pack_dir);
        ensure!(
            packs == rounds,
            "the fixture holds {packs} pack(s), not the {rounds} it was pushed in"
        );
        let (distinct, duplicated) = objects_by_pack_residency(&pack_dir)?;
        ensure!(
            duplicated > 0,
            "no object of the {distinct} in this fixture lives in more than one pack, so a \
             multi-pack index has nothing to choose between and this arm cannot see the defect \
             it exists for"
        );
        // The truth the clone is judged against, read off the fixture by git
        // before any engine has touched it.
        let expected = h2h_object_set(&origin)?;

        // Handed to whichever engine is selected, in that engine's own layout.
        //
        // **This is where the znippy column used to stop, and the red was a real
        // finding rather than a fixture accident.** `git push` with
        // `transfer.unpackLimit=1` runs `index-pack --fix-thin`, which completes
        // each round's pack by APPENDING the bases it spliced in and leaving the
        // deltas that name them as `OBJ_REF_DELTA` — so these packs carry
        // ref-deltas whose base is right there in the same pack (verified 2026-08-14:
        // base `96099daf…` is entry 1 of 25 in its own `.idx`).
        //
        // `znippy-plugin-git`'s ack path refused them. `pack_walk::closure()`
        // does `DeltaBase::Ref(oid) => c.external_refs.push(oid)` with **no
        // in-pack check at all** — and it cannot do otherwise, because a walk
        // reports what the bytes say and a `REF_DELTA` names its base by oid,
        // which is a fact about resolved content — and `external_bases_exist`
        // then required every one of those to be in the store already. So this
        // engine could not be handed a git repository's own packs.
        //
        // **Fixed 2026-08-14** (znippy `in-pack-ref-delta`): the fast path is
        // unchanged and a base the store holds still settles with no bytes read,
        // but a base that is *not* held now costs one `resolve_walked` over the
        // pack before the push is refused, and the pack answering for itself is
        // accepted. This arm is the guard for it, at 16 packs with
        // `objects_in_two_packs > 0`.
        //
        // The **serving** half of the same shape was the second half of that
        // fix and is what `receivers_accepting` below is really about: znippy's
        // `emit_set` copied a stored `REF_DELTA` straight through when its base
        // was in the request, and `gix_pack`'s `LookupRefDeltaObjectsIter` reads
        // every ref-delta as naming something the *receiver* already holds — so
        // stock git accepted the pack, gitoxide refused the whole of it, and
        // `gunnar.rewrite` round 0 and `gunnar.year` week 0 died on all four
        // znippy columns with `The object <oid> could not be decoded or wasn't
        // found`. Such an entry is now re-headed as an `OFS_DELTA` naming the
        // same base by distance, payload untouched. The third place with the
        // missing in-pack check, `gix_pack`'s own iterator, is upstream
        // (gitoxide#2882) and is why the rule here is the narrow one:
        // never name a base by oid unless the receiver already holds it.
        let served_dir = engine_served(&origin, &format!("midx-serve-{rounds}p")).context(
            "handing this 16-pack corpus to the selected engine. If this failed with \"deltas \
             against … which this repository does not have\", the pack is NOT thin — see the \
             comment above this line",
        )?;
        let store = gunnar_bench::engine::open_repo(&served_dir, HASH)?;

        let (served, objects) = gunnar_bench::stages::serve_on(store.as_ref(), HASH, &head, &[])?;
        let pack_b = gunnar_bench::stages::demux_packfile(&served)?;

        // ── the receiver that matters ───────────────────────────────────────
        let judged = fixture_root().join("midx-serve-judged");
        let _ = std::fs::remove_dir_all(&judged);
        std::fs::create_dir_all(&judged)?;
        let stock_ids =
            objects_stock_git_accepts(&pack_b, &judged, "served", HASH).with_context(|| {
                format!(
                    "a clone served off a {packs}-pack corpus ({duplicated} of {distinct} objects \
                     resident in two packs)"
                )
            })?;
        ensure!(
            stock_ids == expected,
            "stock git indexed {} objects out of the served clone where `git rev-list --objects` \
             says this fixture reaches {} ({} missing, {} unexpected) — the client would end up \
             with the wrong repository",
            stock_ids.len(),
            expected.len(),
            expected.difference(&stock_ids).count(),
            stock_ids.difference(&expected).count()
        );
        let gix_dir = judged.join("gix");
        std::fs::create_dir_all(&gix_dir)?;
        // **`index_pack`, and it is deliberately NOT `index_thin_pack`.**
        //
        // MEASURED the desktop 2026-08-14 on the gix column, both ways, because the
        // obvious repair here is wrong and looks right:
        //
        //  * `index_thin_pack(…, GunnarObjects(store.git()), …)` "succeeds" and
        //    writes a **corrupt index**: 309 entries for a 240-object pack, of
        //    which **69 oids appear twice** (`git show-index | sort | uniq -d`).
        //    `gix_pack::data::input::LookupRefDeltaObjectsIter` resolves every
        //    ref-delta through the lookup and inserts the base as a new entry
        //    with **no in-pack check at all**, so handing it a store duplicates
        //    every base that was already in the pack. The id-list assertion
        //    below is what turns that from a plausible green into a red.
        //  * `index_pack` (no base lookup) is the honest receiver, and until
        //    2026-08-14 it failed here — *"Ref delta objects are not supported
        //    as there is no way to look them up."* `gix_pack::index::write`
        //    refuses **every** `OBJ_REF_DELTA`, base in the pack or not, while
        //    stock git indexed the same 240 objects and exited 0.
        //
        // That red was true and it was not about the fixture: it named a missing
        // capability in `storage-git-gix`'s `emit_oids`, which copied stored
        // entries verbatim and so put `index-pack --fix-thin`'s ref-deltas
        // straight on the wire. `emit_oids` now re-heads a delta into whichever
        // spelling the client can read — ref-to-ofs here, ofs-to-ref in
        // `gunnar.clone_no_ofs_delta`, one branch in `write_emission` — so both
        // receivers see a pack of `OBJ_OFS_DELTA`s over an untouched payload.
        let indexed = gunnar_pack::index_pack(
            &mut std::io::BufReader::new(std::io::Cursor::new(&pack_b)),
            Some(&gix_dir),
            &IndexOptions::new(HASH),
            None,
        )
        .context("gitoxide refused the same clone pack stock git accepted")?;
        ensure!(
            u64::from(indexed.num_objects) == objects,
            "gitoxide indexed {} objects out of a pack the server says carries {objects}",
            indexed.num_objects
        );

        // ── the second receiver's index, read rather than trusted ───────────
        //
        // `num_objects` above is the receiver's own count of its own work; these
        // are the ids it actually wrote down. A v2 index stores them ascending,
        // so a `Vec` equality is a set equality, and a duplicate shows up as a
        // length that disagrees with its own dedup.
        let gix_idx = indexed
            .index_path
            .clone()
            .context("gitoxide indexed the pack but wrote no .idx to read back")?;
        let gix_ids: std::collections::BTreeSet<String> = idx_object_ids(&gix_idx, HASH)?
            .into_iter()
            .map(|raw| {
                Ok(ObjectId::try_from(raw.as_slice())
                    .context("an id in gitoxide's index is not this hash's width")?
                    .to_string())
            })
            .collect::<Result<_>>()?;
        let duplicate_oids = indexed.num_objects as usize - gix_ids.len();
        ensure!(
            duplicate_oids == 0,
            "gitoxide wrote an index of {} entries over this pack in which {duplicate_oids} \
             oid(s) appear more than once — a base already in the pack was resolved through a \
             store and inserted a second time, which is a corrupt index that `num_objects` alone \
             calls merely wrong",
            indexed.num_objects
        );
        ensure!(
            gix_ids == stock_ids,
            "the two receivers indexed the same {} pack bytes into different object sets — stock \
             git listed {} ids and gitoxide {} — so at most one of them read the pack the server \
             meant to send",
            pack_b.len(),
            stock_ids.len(),
            gix_ids.len()
        );

        Ok(result(
            self.id(),
            vec![
                ("packs", json!(packs as f64)),
                ("objects", json!(objects as f64)),
                ("objects_in_two_packs", json!(duplicated as f64)),
                ("distinct_objects_on_disk", json!(distinct as f64)),
                ("objects_delivered", json!(stock_ids.len() as f64)),
                ("wire_bytes", json!(pack_b.len() as f64)),
                ("duplicate_oids_indexed", json!(duplicate_oids as f64)),
                ("receivers_accepting", json!(2.0)),
            ],
        ))
    }
}
nornir_bench::register_bench!(MultiPackServe);

// ───────────────────────────────────────────────────────────────────────────
// gunnar.multi_pack_extent_cost: DELETED 2026-08-14 — its subject was retired
// ───────────────────────────────────────────────────────────────────────────
//
// The arm priced `gunnar_pack::copy`'s extent table: how much of a serve falls
// back to `gix-odb`'s inflate because `PackedObjects::learn_pack` declined a
// table for a pack a multi-pack index had left short. It reported
// `extent_lookups`, `extent_hit_frac`, `extent_declined_lookups` and a
// `GUNNAR_PACK_EXTENTS=off` A/B, and it is deleted rather than left red because
// **none of those five numbers has anything to read any more.**
//
// # What changed, in the tree's own words
//
// `crates/gunnar-wire/src/upload_pack.rs`, at the emission:
//
//   > What stood here was gix: `write_pack_copy_thin` drove `iter_from_counts`
//   > […] Both are gone with the packfile engine […] The engine owns the format
//   > now.
//
// So no serve reaches `gunnar_pack::copy` on either engine. And the defect the
// arm priced cannot arise in the engine that replaced it:
// `storage-git-gix/src/packs.rs` builds each pack's entry-length table from
// **that pack's own `.idx`** ("a packfile's entries are contiguous, so the
// successor of an offset is where its entry ends"), never from the
// deduplicated multi-pack oid table. There is no short list, so there is no
// declined table, so there is no fallback to price.
//
// # Measured before deleting, rather than argued
//
// the desktop 2026-08-14, gix column, fixture `packed-40000o-16p-ov8-served-v2` built
// clean (16 packs, `multi-pack-index` written by the store's own policy, 120
// objects in two packs):
//
//   ✗ gunnar.multi_pack_extent_cost: the served clone performed ZERO extent
//     lookups, so neither the table nor the inflate was exercised and this row
//     prices nothing
//
// That is the arm's own canary firing correctly, and it fires on every column
// and will keep firing: `gunnar_pack::extent_lookups()` cannot move when
// nothing on the serve path calls into `gunnar_pack::copy`. A row published
// from it would have carried `surplus_bytes: 0` and `receivers_accepting: 2`
// off an A/B whose switch is not wired to anything — the shipped path compared
// against itself, which is the failure its own doc comment named.
//
// # What still covers the ground it covered
//
//  * **correctness of a served clone off a duplicated multi-pack store** —
//    `gunnar.multi_pack_serve`, on a fixture built by sixteen real `git push`es
//    rather than by writing the shape directly. It is the arm that judges with
//    both receivers, and it kept its fixture.
//  * **the entry-length arithmetic itself** — `storage-git-gix`'s own tests,
//    beside the code that now does it.
//
// [`PackShape::served`] is kept: it is the only shape that produces a servable
// store with an object in two packs, and it is a fixture, not an arm.

// ───────────────────────────────────────────────────────────────────────────
// gunnar.negotiate_divergent: the classic git-server failure
// ───────────────────────────────────────────────────────────────────────────

/// `have`/`want` against a genuinely divergent merge history.
///
/// **Red condition: `rounds` must be greater than 1.** A `have`/`want` loop that
/// terminates on the happy path but not on a divergent one is the classic
/// git-server failure, and `gunnar-wire/src/negotiate.rs:4-10` says in its own
/// module docs that a three-commit fixture cannot see it: with a linear history
/// the client's very first `have` is common and the loop ends on round one. So a
/// one-round result here does not mean negotiation is fast, it means the fixture
/// is not divergent and the arm measured nothing. Asserted.
///
/// The fixture is a real divergence: a shared base, then `divergence` commits
/// down each of two branches. The client offers only its own branch's commits,
/// oldest last, 32 per round (git's window), so the server spends round after
/// round on `have`s it has never seen before, which is the shape that turns a
/// slow negotiation into an apparent hang.
///
/// `commits_visited` is the counting store's commit reads: the cost the walk
/// actually paid, not the number of rounds it took to pay it.
struct NegotiateDivergent;

impl Bencher for NegotiateDivergent {
    fn id(&self) -> &'static str {
        "gunnar.negotiate_divergent"
    }
    fn run(&self) -> Result<BenchResult> {
        let base_len = env_usize("GUNNAR_BENCH_NEG_BASE", scaled(20_000, 500));
        let divergence = env_usize("GUNNAR_BENCH_NEG_DIVERGENCE", scaled(20_000, 500));
        let window = 32usize;

        let store = MemGitServe::new(HASH);
        let base = build_history(&store, HASH, 0x0BA5E, base_len, 1, 64, None)?;
        // The server's branch.
        let server = build_history(&store, HASH, 0x5E12, divergence, 1, 64, Some(base.tip))?;
        // The client's branch: same base, entirely different commits. These live
        // in the same store only because the bench has to construct them; the
        // client offers them as `have`s and the server has genuinely never seen
        // them as ancestors of anything it holds a reference to.
        let client_store = MemGitServe::new(HASH);
        let client = build_history(
            &client_store,
            HASH,
            0xC11E,
            divergence,
            1,
            64,
            Some(base.tip),
        )?;

        store.put_refs(&[RefUpdate::set(
            "refs/heads/main",
            server.tip.to_hex().to_string(),
        )])?;

        // The client's `have`s, newest first, in the order git sends them: back
        // down its own divergent branch, and then on into the shared history.
        //
        // The shared tail is load-bearing and was missing in the first version of
        // this arm, which is why that version was wrong. Offering only the
        // divergent commits produces a negotiation with NO common commit at all:
        // `negotiate()` returns early on an empty `common` and never builds a
        // `Reachability`, so `commits_visited` came back **0**: sixteen rounds of
        // measuring nothing, reported as a passing arm. A client with a genuinely
        // shared base does eventually offer it, and that is when the server starts
        // paying: every subsequent round walks from the wants looking for a cut
        // point. That is the cost this arm exists to measure.
        let mut haves: Vec<ObjectId> = client
            .ids
            .iter()
            .copied()
            .filter(|id| is_commit(&client_store, id))
            .collect();
        haves.reverse();
        //
        // `GUNNAR_BENCH_NEG_NO_SHARED=1` omits the shared tail. That is not a
        // tuning knob, it is this arm's **red switch**: it reproduces the broken
        // fixture on demand, so `commits_visited = 0` can be seen failing rather
        // than taken on trust from a comment. A guard nobody has watched fail is
        // not known to work.
        if std::env::var("GUNNAR_BENCH_NEG_NO_SHARED")
            .map(|v| v != "0")
            .unwrap_or(false)
        {
            eprintln!("bench: GUNNAR_BENCH_NEG_NO_SHARED=1, offering no common commit (red probe)");
        } else {
            let mut shared: Vec<ObjectId> = base
                .ids
                .iter()
                .copied()
                .filter(|id| is_commit(&store, id))
                .collect();
            shared.reverse();
            haves.extend(shared);
        }

        let counting = Counting::new(&store);
        let upload = UploadPack::new(&counting, HASH);

        let mut rounds = 0usize;
        let mut offered: Vec<ObjectId> = Vec::new();
        let mut ready = false;
        let mut common = 0usize;
        let start = Instant::now();
        for chunk in haves.chunks(window) {
            offered.extend_from_slice(chunk);
            let request = FetchRequest {
                wants: vec![server.tip],
                haves: offered.clone(),
                done: false,
                ..Default::default()
            };
            let mut sink = Sink::default();
            let outcome = upload.fetch(&request, &mut sink)?;
            rounds += 1;
            common = outcome.common.len();
            if outcome.ready {
                ready = true;
                break;
            }
        }
        let secs = start.elapsed().as_secs_f64();

        ensure!(
            rounds > 1,
            "negotiation finished in one round; the fixture is not divergent, so this arm \
             measured the happy path (negotiate.rs:4-10)"
        );
        ensure!(
            !ready || common > 0,
            "the server declared itself ready with no common commit"
        );
        // The assertion that caught this arm's own first version. `rounds > 1` is
        // necessary but NOT sufficient: sixteen rounds against a client with no
        // common commit never enter the reachability walk at all, and report a
        // gorgeous sub-millisecond `negotiate_ms` for work that did not happen.
        ensure!(
            counting.commit_reads() > 0,
            "commits_visited = 0 after {rounds} rounds. No `have` was ever common, so the \
             server never walked for a cut point and negotiate_ms measures the empty case"
        );

        Ok(result(
            self.id(),
            vec![
                ("negotiate_ms", json!(round(secs * 1000.0, 2))),
                ("rounds", json!(rounds as f64)),
                ("commits_visited", json!(counting.commit_reads() as f64)),
                ("haves_offered", json!(offered.len() as f64)),
                ("common", json!(common as f64)),
                ("ready", json!(if ready { 1.0 } else { 0.0 })),
                ("divergence", json!(divergence as f64)),
            ],
        ))
    }
}
nornir_bench::register_bench!(NegotiateDivergent);

fn is_commit(store: &dyn GitServe, id: &ObjectId) -> bool {
    store
        .header(id.as_bytes())
        .ok()
        .flatten()
        .is_some_and(|(kind, _)| kind == ObjType::Commit)
}

// ───────────────────────────────────────────────────────────────────────────
// NOT BUILT: gunnar.clone_cold_tier
// ───────────────────────────────────────────────────────────────────────────
//
// The bench spec lists `gunnar.clone_cold_tier`, the same clone served entirely
// from the znippy cold tier, as the guard on whether that tier forecloses
// pack-copy and re-compresses every git object. **It cannot be written today**:
// there is no znippy cold-tier engine in this tree. `znippy` appears in gunnar
// only in two comments (`Cargo.toml:123` and `crates/gunnar-cli/Cargo.toml:41`,
// the latter saying the fs backend is "the only ObjectStore backend that
// survives a restart today"), and the cold tier is a design sketch, not code:
// there is no `ObjectStore` implementation over a znippy archive to clone from.
//
// Deliberately not stubbed. An arm that self-skips and reports green is the
// silent-green failure exactly, and it would let the cold-tier question be
// closed against a benchmark that never ran. When the znippy engine exists,
// this arm is a copy of `CloneLarge` with the store swapped and the same
// `objects_recompressed` assertion, which is the cold tier's whole question.

// ═══════════════════════════════════════════════════════════════════════════
//
//  HEAD-TO-HEAD: gunnar versus stock `git`, serving the same repository
//
// ═══════════════════════════════════════════════════════════════════════════
//
// The eleven arms above measure gunnar against gunnar. They catch regressions
// and answer nothing about whether the project is worth continuing. Nothing here
// had ever been load-tested or profiled: "clone wall clock and pack size
// **against stock git on the same fixture**" was the missing half. This is it.
//
// # The shape of the comparison
//
// One client, one repository, one machine, both sides warm, and (this is the
// part that makes the ratios mean something) **the same transport**.
//
// ```text
//   git clone "ext::<this binary> %S <repo>"        <- the CLIENT is stock git,
//         │                                            identically, on both sides
//         └─ spawns:  <this binary>  git-upload-pack <repo>      ← THE METER
//                        │   pumps the pipe both ways, counting bytes
//                        │   reaps the server with wait4 → CPU seconds, peak RSS
//                        └─ spawns the SERVER:
//                             side=git    :  git upload-pack <repo>
//                             side=gunnar :  <this binary>, gunnar-wire in-process
// ```
//
// The meter is byte-for-byte the same process on both sides, so its cost (one
// extra process, two pumping threads, one memcpy of the stream) is present in
// both columns and cancels out of every ratio. `wait4` is what makes CPU
// seconds honest: on Linux it returns `RUSAGE_BOTH`, so `git upload-pack`'s
// fan-out into `git pack-objects` is counted. A server that wins wall clock by
// burning four cores loses `cpu_ratio_x`, and that is the number to read.
//
// # Isolation: a container, and both sides are inside it
//
// A container is used for exactly these arms and for exactly one
// reason: to make the stock-`git` toolchain's *presence and version*
// non-optional, because a self-skipping interop suite reports a false
// green. The image is `ubuntu:26.04` + `git=1:2.53.0-1ubuntu1`, which is the
// host's own distribution and the host's own git package, so:
//
//   * the pinned `git` inside is the same build as `/usr/bin/git` outside, and
//   * the glibc inside is the host's, which is what lets this binary, compiled
//     on the host, run unchanged in there.
//
// **Both sides run inside one `podman run`.** Containerising only the stock-git
// arm would have added a namespace-and-cgroup asymmetry far larger than the
// version pinning it bought. The container is entered once per arm, outside the
// timed region, so it contributes nothing to either column.
//
// No KVM.
//
// # What is NOT equalised, stated rather than pretended away
//
//  1. **gunnar's SSH layer is not in the measurement.** `gunnar serve` is an SSH
//     listener; it has no stdin/stdout `upload-pack` mode, so the gunnar side
//     here is `gunnar-wire`'s `UploadPack`/`ReceivePack`, the same code the SSH
//     server calls, hosted over a pipe. Either equalising the transports or
//     stating the difference was acceptable; this equalises them, and the
//     consequence is that **the numbers below are optimistic for gunnar**: they
//     exclude key exchange, publickey auth, channel framing and the sshsig
//     control-plane work a real `gunnar serve` also does. Stock git over `ssh://`
//     would carry the same tax, which is why removing it from both sides is the
//     defensible choice. But it is removed, and no number here should be quoted
//     as "gunnar over SSH".
//  2. **The hash is sha1, and gunnar's default is sha256** (`D11`). The fixture
//     is sha1 because that is what every repository anyone would migrate to
//     gunnar actually is, and because a sha256 fixture would put *stock git* in
//     the unusual configuration instead. `gunnar.vs_git_clone_sha256` measures
//     the same clone in gunnar's native format so the size of that asymmetry is
//     a number rather than a hand-wave.
//  3. **The gunnar server process is this bench binary**, so its resident set
//     includes bench code stock `git-upload-pack` does not carry. Every clone arm
//     therefore also serves a one-commit repository and reports
//     `*_rss_floor_mb`; the workload's own memory is the difference, and the
//     floor is what should be subtracted before the RSS columns are compared.
//  4. **The host is shared.** the desktop runs other concurrent jobs. Every arm interleaves
//     `gunnar, git, gunnar, git, …` and reports the median, and the clone arm
//     emits `gunnar_drift_x` (first gunnar round over last gunnar round), so a
//     reader can see how much the host moved underneath the comparison.

/// The pinned toolchain image. Built from `Containerfile`:
/// `FROM docker.io/library/ubuntu:26.04` + `git=1:2.53.0-1ubuntu1`.
const H2H_IMAGE: &str = "localhost/gunnar-bench-git:2.53.0";

/// The `git --version` every head-to-head arm asserts it measured against.
/// A version that does not match is **red**, not a footnote: a pack-size ratio
/// is a statement about a specific `git`, and a suite that self-skips and reports
/// green is what happens when a toolchain's identity is assumed, not checked.
const H2H_GIT_VERSION: &str = "git version 2.53.0";

fn h2h_image() -> String {
    std::env::var("GUNNAR_BENCH_H2H_IMAGE").unwrap_or_else(|_| H2H_IMAGE.to_string())
}

/// The scratch tree the driver clones into. Under the fixture root, i.e. md1.
fn h2h_work() -> PathBuf {
    fixture_root().join("h2h-work")
}

// ── SEAM 1: which backend serves gunnar's `ext::` column ───────────────────
//
// `GUNNAR_BENCH_GUNNAR_SERVER=<path>` is the *executable* that serves the gunnar
// side, spawned with the identical argv git's `ext::` transport uses
// (`[exe, <service>, <repo>]`) and the identical `GUNNAR_BENCH_COPY_STATS`
// contract. **It is mandatory.** There was a fallback — unset, the meter
// re-executed this binary and served out of a store it built in-process — and
// it is deleted; see `h2h_serve_gunnar`.
//
// **Why a process and not a trait object.** The obvious seam would be a
// `--backend znippy` switch constructing a different store inside this binary.
// It cannot be: an engine crate in `[dependencies]` drags its own world in at
// cargo **resolve** time (znippy needs a C toolchain and cmake), so naming one
// here makes a sibling checkout a hard requirement of running *any* arm —
// including the 40-odd with nothing to do with storage. So the seam goes where
// the store was already chosen: in these arms, a process boundary that exists.
//
// **bench/ therefore stays engine-free: it knows a path, not a crate.**
//
// 🔴 That sentence stood here from the day the seam was written and was **false
// as written until 2026-08-10**. `bench/Cargo.toml` carried a plain,
// non-optional the old znippy engine crate path dependency, and the old znippy engine crate
// path-depends on four znippy crates — so a znippy checkout *was* a hard
// requirement of building all 49 arms, which is the exact cost the paragraph
// above says the design avoids. Two surveys read this comment and neither
// checked the manifest against it. It is true now: `grep -c znippy engine
// bench/Cargo.toml` is 0, and the engine reaches a measurement only as a path.
//
// ⚠️ **THERE ARE TWO SEAMS AND THIS IS ONLY THE FIRST.** The eight `vs_forge_*`
// arms take `GUNNAR_BENCH_FORGE_BINARY` — a whole server binary bind-mounted
// into a podman container and started with `serve --http-bind` (`forge/mod.rs`,
// `gunnar_binary()`) — and **ignore this variable entirely**. A driver that sets
// only this one and runs with a `gunnar.` filter runs both columns of those
// eight arms against the same binary and labels them as two engines. See
// `gunnar_bench::engine` for the pair written down together.
fn h2h_gunnar_server() -> Option<PathBuf> {
    gunnar_bench::engine::server_binary()
}

/// Build the alternate backend's store for `repo`, **outside the timed region**.
///
/// A no-op when no alternate server is configured, so every call site reads the
/// same on both backends. The helper is the one that knows what a store is; this
/// side only knows that setup has to happen before the clock starts, which is
/// the part a benchmark can get wrong.
fn h2h_prepare_gunnar(repo: &Path) -> Result<()> {
    let Some(bin) = h2h_gunnar_server() else {
        return Ok(());
    };
    let out = Command::new(&bin)
        .arg("prepare")
        .arg(repo)
        // PIPED, not inherited, because the engine read-back below is parsed out
        // of it. It is echoed on to stderr immediately afterwards so the line an
        // operator is used to seeing is still in the log.
        .stderr(Stdio::piped())
        .output()
        .with_context(|| format!("running {} prepare {}", bin.display(), repo.display()))?;
    let err = String::from_utf8_lossy(&out.stderr).into_owned();
    eprint!("{err}");
    ensure!(
        out.status.success(),
        "{} prepare {} exited {:?}: {}",
        bin.display(),
        repo.display(),
        out.status.code(),
        err.trim()
    );
    h2h_assert_engine(&err, repo)
}

/// Where seam 1's `prepare` puts the store it builds for `repo`.
///
/// `git-store-serve`'s own `store_root`: `<repo>.gunnar-<engine>`, the same
/// string [`h2h_assert_engine`] rebuilds to corroborate the engine read-back.
/// Named once here because [`h2h_serve_daemon`] has to *open* that directory
/// rather than merely check it exists, and two spellings of one convention is
/// how the seam and the store come to disagree.
///
/// The engine defaults to `znippy` when `GUNNAR_ENGINE` is unset, matching
/// `gunnar_server::storage::Engine::selected()` and [`h2h_assert_engine`]'s
/// `want`.
fn h2h_engine_store_root(repo: &Path) -> PathBuf {
    let engine = std::env::var("GUNNAR_ENGINE")
        .ok()
        .map(|v| v.trim().to_ascii_lowercase())
        .filter(|v| !v.is_empty())
        .unwrap_or_else(|| "znippy".to_string());
    PathBuf::from(format!("{}.gunnar-{engine}", repo.display()))
}

/// **The engine seam-1's server says it used**, read back and refused on
/// mismatch.
///
/// The seam-2 counterpart is `forge/parity.rs`'s `assert_storage_engine`, and
/// this exists for the same reason and after the same failure: what the harness
/// passed is not what the server ran. Here the evidence is `git-store-serve`'s
/// own `prepare` report — it opens with `<engine>-prepare:` on **both** its
/// paths (the already-populated one and the ingest one) — corroborated by the
/// store directory it wrote, which is `<repo>.gunnar-<engine>` and so names the
/// engine in the filesystem as well.
///
/// Two facts, not one, deliberately: the line is a string the server prints and
/// the directory is a side effect it cannot fake, and this arm has already been
/// burned once by an environment variable that was set and never arrived.
///
/// Seen red: before `h2h_run` forwarded `GUNNAR_ENGINE` across the container
/// boundary, a `GUNNAR_ENGINE=gix` run reported `znippy-prepare:` and wrote
/// `<repo>.gunnar-znippy`, and this function is what says so out loud instead of
/// publishing that column as gix.
///
/// # What this does NOT cover, and what would
///
/// Only the `clone` and `fetch` scenarios call `prepare`, so this guard reaches
/// `vs_git_clone`, `vs_git_clone_sha256` and `vs_git_fetch_divergent`. The
/// `push` and `concurrent` scenarios build their store by pushing into it and
/// never call `prepare`, so **`vs_git_push` has no programmatic engine
/// read-back**.
///
/// Checking that `<repo>.gunnar-<want>` merely *exists* would not fix that; it
/// would be a hollow guard. The fixtures are reused across passes, so after one
/// znippy pass and one gix pass BOTH directories are there and an existence
/// check passes whichever engine actually served.
///
/// The evidence for those arms does exist — `git-store-serve`'s serve path
/// prints `<engine>: served git-upload-pack …` / `git-receive-pack` per session,
/// from the process that served — but it goes to inherited stderr, so it lands
/// in the run's log rather than anywhere an assertion can reach. Closing it
/// properly means the meter capturing its child's stderr and writing the engine
/// beside the receipt it already writes for `GUNNAR_BENCH_COPY_STATS`. Not done
/// here; stated so it is not mistaken for covered.
fn h2h_assert_engine(prepare_stderr: &str, repo: &Path) -> Result<()> {
    let want = std::env::var("GUNNAR_ENGINE")
        .ok()
        .map(|v| v.trim().to_ascii_lowercase())
        .filter(|v| !v.is_empty())
        .unwrap_or_else(|| "znippy".to_string());

    let said = prepare_stderr
        .lines()
        .find_map(|l| l.trim().split("-prepare:").next().filter(|n| !n.is_empty()))
        .map(|n| n.trim().to_ascii_lowercase())
        .with_context(|| {
            format!(
                "`git-store-serve prepare {}` printed no `<engine>-prepare:` line, so this run \
                 cannot say which storage engine built the store it is about to measure. \
                 Refusing rather than labelling the column with the engine that was REQUESTED.\n\
                 What it did print:\n{}",
                repo.display(),
                prepare_stderr.trim()
            )
        })?;
    ensure!(
        said == want,
        "seam 1's server prepared {} with the {said:?} storage engine and this run asked for \
         {want:?}. `GUNNAR_ENGINE` is read inside the head-to-head DRIVER's container, and the \
         driver's environment is an explicit allow-list — a variable that is set on the host and \
         never crosses is how this arm served znippy for both columns of a two-engine run.",
        repo.display()
    );

    // The corroborating fact: `store_root` writes `<repo>.gunnar-<engine>`, so
    // the engine is in the filesystem and not only in a string the server chose
    // to print.
    let root = PathBuf::from(format!("{}.gunnar-{said}", repo.display()));
    ensure!(
        root.exists(),
        "seam 1's server reported {said:?} but wrote no store at {}. The report and the \
         directory disagree, so one of them is not describing this run.",
        root.display()
    );
    Ok(())
}

// ───────────────────────────────────────────────────────────────────────────
// THE METER: `GUNNAR_BENCH_SERVE=<side>`, run by git's `ext::` transport
// ───────────────────────────────────────────────────────────────────────────

/// Spawn the real server, pump the pipe, and report what it cost.
///
/// `argv` is what `ext::` hands a helper: `[exe, <service>, <repo>]`, where
/// `<service>` is `git-upload-pack` or `git-receive-pack` (`%S`).
///
/// The pumping is deliberate rather than an accident of implementation. `exec`ing
/// the server would be one process cheaper, but then nothing counts the bytes and
/// nothing holds the `rusage`; and counting them on only one side would be worse
/// than not counting them at all.
fn h2h_serve_meter(side: &str) -> Result<()> {
    // The label this pass is recorded under. Normally the side itself; the
    // `GUNNAR_BENCH_H2H_SAME_SIDE` red switch sets them apart deliberately, so
    // that a run where BOTH columns are gunnar still reaches the arm's
    // comparison guards instead of being caught by the meter's own side check.
    let label = std::env::var("GUNNAR_BENCH_SERVE_LABEL").unwrap_or_else(|_| side.to_string());
    let args: Vec<String> = std::env::args().collect();
    let service = args.get(1).cloned().unwrap_or_default();
    let repo = args.get(2).cloned().unwrap_or_default();
    ensure!(
        service == "git-upload-pack" || service == "git-receive-pack",
        "the ext:: helper was asked for service {service:?}, which is not a git service"
    );

    // The `gunnar-daemon` side is not a child at all: gunnar's `FsStore` takes a
    // cross-process exclusive lock on the repository (`gunnar-store.lock`), so a
    // second gunnar process cannot open a repository the first one has. That is a
    // property of what ships, not of this harness, and it is why the concurrent
    // arm runs each side in ITS OWN native shape. See `VsGitCloneConcurrent`.
    // Here the meter simply proxies its pipe to the one long-lived gunnar
    // process, and that process is measured from `/proc` by the driver.
    if side == "gunnar-daemon" {
        return h2h_meter_daemon(&service);
    }

    // Where the gunnar server will leave its copy-versus-recompress split. One
    // file per session, named off this meter's own pid so sixteen concurrent
    // meters cannot land in each other's.
    let copy_stats: Option<PathBuf> = std::env::var_os("GUNNAR_BENCH_STATS")
        .map(|s| PathBuf::from(s).with_extension(format!("copy.{}", std::process::id())));
    if let Some(path) = &copy_stats {
        let _ = std::fs::remove_file(path);
    }

    let mut command = match side {
        "git" => {
            let verb = service.strip_prefix("git-").unwrap_or(&service).to_string();
            let mut c = Command::new("git");
            c.arg(verb).arg(&repo);
            c
        }
        "gunnar" => {
            // ── THE STORE SEAM, and it is no longer optional ────────────────
            //
            // There used to be a `None` arm here that re-executed this binary
            // with `GUNNAR_BENCH_SERVE_INNER=1` and served out of a store built
            // in-process. It is deleted, and deleting it is what turns the seam
            // comment above from an aspiration into a fact: the *only* way an
            // engine reaches a measurement now is as an executable on a path.
            //
            // Refusing is the safe direction. The alternative — falling back to
            // some other store when the configured one is missing — publishes a
            // full ratio table for an engine that never ran, under that engine's
            // label. That is the same failure `h2h_serve_daemon` asserts against
            // and the same one `two-engine.sh` refuses to start into.
            let mut c = Command::new(gunnar_bench::engine::require_server()?);
            if let Some(path) = &copy_stats {
                c.env("GUNNAR_BENCH_COPY_STATS", path);
            }
            c.arg(&service).arg(&repo);
            c
        }
        other => bail!("unknown head-to-head side {other:?} (expected `git` or `gunnar`)"),
    };
    command
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit());

    let started = Instant::now();
    let mut child = command
        .spawn()
        .with_context(|| format!("spawning the {side} server"))?;
    let pid = child.id() as i32;
    let mut to_server = child.stdin.take().context("no server stdin")?;
    let mut from_server = child.stdout.take().context("no server stdout")?;

    // client → server. Counted, and (for upload-pack only, where the inbound
    // stream is small and plaintext pkt-line) scanned for `have ` so that
    // negotiation volume is measured the SAME way on both sides. gunnar can
    // report its own round count; `git upload-pack` cannot, so a metric derived
    // from one side's self-report would not be a comparison.
    let bytes_in = Arc::new(AtomicU64::new(0));
    let haves = Arc::new(AtomicU64::new(0));
    let count_haves = service == "git-upload-pack";
    {
        let bytes_in = Arc::clone(&bytes_in);
        let haves = Arc::clone(&haves);
        std::thread::spawn(move || {
            let stdin = std::io::stdin();
            let mut stdin = stdin.lock();
            let mut buf = vec![0u8; 64 * 1024];
            let mut carry: Vec<u8> = Vec::new();
            loop {
                let n = match stdin.read(&mut buf) {
                    Ok(0) | Err(_) => break,
                    Ok(n) => n,
                };
                if to_server.write_all(&buf[..n]).is_err() || to_server.flush().is_err() {
                    break;
                }
                bytes_in.fetch_add(n as u64, Ordering::Relaxed);
                if count_haves {
                    haves.fetch_add(
                        h2h_count(b"have ", &buf[..n], &mut carry),

The file goes on; the console shows all of it.