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

nordisk/gunnar

crates/gunnar-server/examples/git-store-serve.rs

//! **gunnar's head-to-head server binary, on whichever engine this build carries.**
//!
//! The twin of `edda/crates/storage-git-gix/examples/git-store-serve.rs`. Same
//! three modes, same argv, same `GUNNAR_BENCH_COPY_STATS` receipt, so `bench/`
//! needs no change at all: its whole seam is
//! `GUNNAR_BENCH_GUNNAR_SERVER=<path>`, and it knows a path, not a crate.
//!
//! ```text
//!   stock git  ──ext:: over a pipe──►  bench meter  ──►  THIS BINARY
//!                                                          │
//!                                            UploadPack / ReceivePack   (gunnar-wire)
//!                                                          │
//!                                             storage::Engine::open_repo
//!                                                          │
//!                                        znippy-plugin-git  ·OR·  storage-git-gix
//! ```
//!
//! # Why it lives in `gunnar-server` and not somewhere else
//!
//! It died with the old znippy engine crate, and znippy engine is not coming back. The three
//! things this binary needs are all here and nowhere else:
//!
//! 1. **the engine seam.** [`gunnar_server::storage::Engine`] is *the only place
//!    in gunnar that names an implementation of `GitOps`*. A bench server that
//!    opened `znippy_plugin_git::GitStore` directly would be a **second** place,
//!    and the two would drift — the exact defect `gunnar-znippy` was deleted for.
//!    Going through `Engine` also means this binary serves **either** engine
//!    (`GUNNAR_ENGINE=gix` on a build carrying both), from one wire.
//! 2. **`gunnar-wire`.** The edda arm cannot reach it (a path
//!    dependency on a sibling gunnar checkout is read at cargo *resolve* time),
//!    so it hand-rolls a v0 driver and says so. Here `UploadPack`/`ReceivePack`
//!    are the same state machines `transport.rs` runs, so this column measures
//!    *the server*, not a bench-shaped imitation of it.
//! 3. **the ingest.** [`Engine::ingest_push`] is the same call a real push lands
//!    in, `PushIngest` and all.
//!
//! A new crate would have had to name `znippy-plugin-git` by path a second time
//! for the sake of an example, which is the build-time cost paid twice for nothing.
//!
//! # The modes
//!
//! | argv | what it does |
//! |---|---|
//! | `prepare <repo>` | build the store for `<repo>`, **outside any timed region**, idempotent |
//! | `git-upload-pack <repo>` | serve a fetch/clone out of that store |
//! | `git-receive-pack <repo>` | receive a push into that store |
//! | `has <repo> <oid>` | a diagnostic: print the **error**, not the `false` it collapses to |
//!
//! `<repo>` is always the *filesystem* repository path the bench hands out, and
//! the store lives **beside** it at `<repo>.gunnar-<engine>` — a sibling, not a
//! child, because the bench deletes and re-copies repository directories between
//! rounds and a store inside one would be copied along as opaque bytes.
//!
//! **`prepare` is a separate mode on purpose.** Building the store is ingest
//! work, and ingest work inside `git-upload-pack` would be charged to the clone
//! it precedes. A serve that finds no store **fails** rather than quietly
//! building one.
//!
//! # The receipt
//!
//! `GUNNAR_BENCH_COPY_STATS` gets one `{"copied":N,"recompressed":M}` line per
//! served fetch, **byte-identical** to the gix arm's — that is a copy of its
//! `writeln!` and not a re-derivation of it, because two columns written by two
//! formatters are two columns that can disagree about what they mean.
//!
//! # What this binary deliberately does NOT implement
//!
//! `GUNNAR_BENCH_NO_PACK_COPY`, the bench's red switch for verbatim pack copy. There is no
//! `NoPackCopy` shim behind `GitServe::emit_pack`, so the binary **refuses to
//! start** when it is set rather than serving an unswitched server under a
//! switched label. A switch that is silently ignored is the hollow-guard shape
//! this tree has been burned by twice.

use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Instant;

/// The heap, the same one the appliance ships.
///
/// This binary is the `GUNNAR_BENCH_GUNNAR_SERVER` seam, so an allocator
/// difference here would show up as an engine difference in the forge columns.
/// See [`gunnar_server::ServerAllocator`].
#[global_allocator]
static ALLOC: gunnar_server::ServerAllocator = gunnar_server::SERVER_ALLOCATOR;

use gunnar_server::storage::Engine;
use gunnar_store::git_storage_trait::RefUpdate;
use gunnar_wire::receive_pack::AllowAll;
use gunnar_wire::{ProtocolVersion, ReceivePack, UploadPack};

type Res<T> = Result<T, Box<dyn std::error::Error>>;

/// The store that backs `repo`. A sibling, not a child — see the module doc.
///
/// The engine's name is in the path because a build carrying both must not have
/// its two arms fight over one directory: a znippy store and a gix store at the
/// same root is two engines' layouts interleaved, and whichever opened second
/// would either refuse or — worse — succeed.
fn store_root(repo: &Path, engine: Engine) -> PathBuf {
    let mut s = repo.as_os_str().to_os_string();
    s.push(format!(".gunnar-{}", engine.name()));
    PathBuf::from(s)
}

/// The repository's hash algorithm, read from its own config.
///
/// A repository served under the wrong hash is not a degraded repository, it is
/// a different one. Same discipline as the gix arm's `hash_kind`, and the same
/// discipline `RepoStore::hash_kind` is held to in the server proper: asked,
/// never defaulted.
fn hash_kind(repo: &Path) -> gix_hash::Kind {
    let config = std::fs::read_to_string(repo.join("config")).unwrap_or_default();
    if config.to_ascii_lowercase().contains("sha256") {
        gix_hash::Kind::Sha256
    } else {
        gix_hash::Kind::Sha1
    }
}

// ─────────────────────────────────────────────────────────────── prepare ────

/// Build `<repo>.gunnar-<engine>` from `<repo>`, or confirm the one that is there.
///
/// Idempotent: the clone scenario prepares one server repository once and the
/// push scenario prepares a fresh copy every round, so a mode that only worked
/// on an empty directory would need the caller to know which case it was in.
///
/// # What is ingested, and why it is the honest input
///
/// The repository's own `objects/pack/*.pack`, byte for byte, through
/// [`Engine::ingest_push`] — the same call a real push lands in. The bench
/// fixtures are `repack -adq && prune --expire=now`-ed, so those bytes are what
/// stock `git pack-objects` wrote at git's own window and depth, and are what a
/// forge would have received.
///
/// A repository that is part packed and part loose takes the **re-pack** route
/// instead, whole: ingesting only its packs would build a store quietly missing
/// objects, and a clone that omits objects still exits 0. Which path was taken
/// is printed, because a re-pack is a different input and a ratio taken across
/// two different inputs is not a ratio. (`git repack -adq` alone is **not** a
/// sufficient fixture — measured, it leaves loose objects behind, and this is
/// the branch that then diverges.)
fn prepare(repo: &Path, engine: Engine) -> Res<()> {
    let root = store_root(repo, engine);
    let kind = hash_kind(repo);
    let started = Instant::now();
    std::fs::create_dir_all(&root)?;

    // Already built? Ask the ref set, not the directory: `open_repo` creates the
    // directory itself, so its existence says nothing about ingest.
    let store = engine.open_repo(&root, kind)?;
    let held = store.serve().refs()?.len();
    if held > 0 {
        eprintln!(
            "{engine}-prepare: {} already holds {held} refs",
            root.display()
        );
        return Ok(());
    }

    // ── objects ─────────────────────────────────────────────────────────────
    let pack_dir = repo.join("objects").join("pack");
    let mut packs: Vec<PathBuf> = Vec::new();
    if pack_dir.is_dir() {
        for entry in std::fs::read_dir(&pack_dir)? {
            let path = entry?.path();
            if path.extension().is_some_and(|e| e == "pack") {
                packs.push(path);
            }
        }
    }
    packs.sort();
    let loose = loose_object_count(repo);

    let mut pack_bytes = 0u64;
    let mut announced = 0u64;
    let ingested_from;
    if !packs.is_empty() && loose == 0 {
        // The whole point of this backend: the bytes `git repack` produced, kept
        // as they are.
        ingested_from = format!("{} verbatim pack(s)", packs.len());
        for pack in &packs {
            let file = std::fs::File::open(pack)?;
            pack_bytes += file.metadata()?.len();
            let mut reader = BufReader::new(file);
            // `want_objects: false` — nothing here gates on the pushed set, and
            // asking would wait for the indexer drain for no reader's benefit.
            // It is also the one thing the gix arm cannot answer at all.
            let receipt = engine.ingest_push(
                &root,
                kind,
                &mut reader,
                gunnar_server::storage::ObjectSet::NotNeeded,
                // No console polls an offline import; the bytes are the figure
                // this tool already prints.
                None,
                // An offline import is no transaction of a serving box: nothing to frame.
                None,
            )?;
            announced += u64::from(receipt.num_objects);
        }
    } else {
        if packs.is_empty() && loose == 0 {
            return Err(format!("{} holds no objects at all", repo.display()).into());
        }
        let why = if packs.is_empty() {
            format!("{loose} loose object(s)")
        } else {
            format!("{} pack(s) beside {loose} loose object(s)", packs.len())
        };
        ingested_from = format!("{why}, re-packed into a stream with --delta-base-offset");
        let bytes = pack_objects(repo)?;
        pack_bytes += bytes.len() as u64;
        let mut reader: &[u8] = &bytes;
        let receipt = engine.ingest_push(
            &root,
            kind,
            &mut reader,
            gunnar_server::storage::ObjectSet::NotNeeded,
            None,
            None,
        )?;
        announced += u64::from(receipt.num_objects);
    }

    // ── refs, through `git for-each-ref` ────────────────────────────────────
    //
    // Not a walk of `refs/`: packed refs are the case a hand-rolled reader
    // misses first, and git's own reader is the definition of what this
    // repository contains. Identical to the gix arm's `read_refs`, deliberately.
    let edits = read_refs(repo)?;
    if edits.is_empty() {
        return Err(format!("{} has no references under refs/", repo.display()).into());
    }
    let count = edits.len();
    let updates: Vec<RefUpdate> = edits
        .iter()
        .map(|(name, oid)| RefUpdate::set(name.clone(), oid.clone()))
        .collect();
    store.serve().put_refs(&updates)?;

    // ── HEAD, through its own accessor ──────────────────────────────────────
    //
    // `HEAD` is **not** a row in `GitOps::refs` (the `GitServe` HEAD ruling, and
    // both arms now agree on that), so it goes through `GitServe::set_head`. A
    // clone needs it: without a symbolic `HEAD` git picks the default branch by
    // guesswork.
    store.serve().set_head(&head_target(repo)?)?;

    let after = store.serve().refs()?.len();
    if after == 0 {
        return Err("the store holds no refs after ingest".into());
    }
    eprintln!(
        "{engine}-prepare: {} <- {ingested_from}, {pack_bytes} bytes, {announced} announced, \
         {count} refs, {:.1} ms",
        root.display(),
        started.elapsed().as_secs_f64() * 1000.0
    );
    Ok(())
}

/// Every reference in `repo` as `(name, hex oid)`, symrefs resolved.
fn read_refs(repo: &Path) -> Res<Vec<(String, String)>> {
    let out = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["for-each-ref", "--format=%(refname)%00%(objectname)"])
        .stderr(Stdio::inherit())
        .output()?;
    if !out.status.success() {
        return Err(format!("git for-each-ref in {} failed", repo.display()).into());
    }
    let mut refs = Vec::new();
    for line in String::from_utf8_lossy(&out.stdout).lines() {
        let mut parts = line.split('\0');
        if let (Some(name), Some(oid)) = (parts.next(), parts.next()) {
            if !name.is_empty() && !oid.is_empty() {
                refs.push((name.to_owned(), oid.to_owned()));
            }
        }
    }
    Ok(refs)
}

/// What `<repo>`'s `HEAD` points at, in the one `&str` `set_head` takes.
///
/// A `refs/…` name for the symbolic case, a hex oid for the detached one — the
/// two shapes `GitServe::set_head` tells apart, and it refuses anything else
/// rather than guessing. An empty repository still gets `refs/heads/main`,
/// because `git init` points `HEAD` at a branch before that branch exists.
fn head_target(repo: &Path) -> Res<String> {
    let sym = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["symbolic-ref", "--quiet", "HEAD"])
        .stderr(Stdio::null())
        .output()?;
    if sym.status.success() {
        let name = String::from_utf8_lossy(&sym.stdout).trim().to_string();
        if !name.is_empty() {
            return Ok(name);
        }
    }
    let direct = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["rev-parse", "--verify", "--quiet", "HEAD"])
        .stderr(Stdio::null())
        .output()?;
    if direct.status.success() {
        let oid = String::from_utf8_lossy(&direct.stdout).trim().to_string();
        if !oid.is_empty() {
            return Ok(oid);
        }
    }
    Ok("refs/heads/main".to_owned())
}

/// Every reachable object in `repo`, as one packfile.
///
/// `rev-list --objects --all` then `pack-objects --stdout --delta-base-offset`,
/// git's own two step. (`pack-objects --revs` with `--all` on stdin is **not**
/// the same thing and is rejected — "fatal: not a rev '--all'" — because
/// `--revs` reads revisions, not rev-list option words. Measured on git 2.53.0.)
fn pack_objects(repo: &Path) -> Res<Vec<u8>> {
    let listed = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["rev-list", "--objects", "--all"])
        .stderr(Stdio::inherit())
        .output()?;
    if !listed.status.success() {
        return Err(format!("git rev-list in {} failed", repo.display()).into());
    }
    let mut oids = String::new();
    for line in String::from_utf8_lossy(&listed.stdout).lines() {
        if let Some(oid) = line.split_whitespace().next() {
            oids.push_str(oid);
            oids.push('\n');
        }
    }
    // An empty list and a repository with no objects produce the same packfile,
    // and only one of them is a thing to store.
    if oids.is_empty() {
        return Err(format!("git rev-list named no objects in {}", repo.display()).into());
    }
    let mut child = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["pack-objects", "--stdout", "--delta-base-offset", "-q"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit())
        .spawn()?;
    child
        .stdin
        .take()
        .ok_or("git pack-objects has no stdin")?
        .write_all(oids.as_bytes())?;
    let out = child.wait_with_output()?;
    if !out.status.success() {
        return Err(format!(
            "git pack-objects in {} exited {:?}",
            repo.display(),
            out.status.code()
        )
        .into());
    }
    if out.stdout.len() < 32 {
        return Err(format!(
            "git pack-objects in {} produced {} bytes, which is not a packfile",
            repo.display(),
            out.stdout.len()
        )
        .into());
    }
    Ok(out.stdout)
}

/// Loose objects under `objects/`, i.e. everything that is not a pack.
fn loose_object_count(repo: &Path) -> usize {
    let dir = repo.join("objects");
    let Ok(entries) = std::fs::read_dir(&dir) else {
        return 0;
    };
    let mut n = 0;
    for entry in entries.flatten() {
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if name.len() == 2 && name.chars().all(|c| c.is_ascii_hexdigit()) {
            if let Ok(inner) = std::fs::read_dir(entry.path()) {
                n += inner.flatten().count();
            }
        }
    }
    n
}

// ────────────────────────────────────────────────────────── the receipt ────

/// Append the `copied`/`recompressed` receipt, in the **byte-for-byte** shape
/// the gix arm writes and the bench meter reads.
///
/// A differing receipt would make the two columns incomparable, which is the
/// whole point of the exercise, so this is a copy of the other arm's `writeln!`
/// and not a re-derivation of it.
fn copy_stats(copied: usize, recompressed: usize) -> Res<()> {
    if let Ok(path) = std::env::var("GUNNAR_BENCH_COPY_STATS") {
        let mut f = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)?;
        writeln!(
            f,
            "{{\"copied\":{},\"recompressed\":{}}}",
            copied as f64, recompressed as f64
        )?;
    }
    Ok(())
}

// ───────────────────────────────────────────────────────────────── serve ────

/// Serve one request. `service` is git's `%S`.
///
/// Deliberately nothing bench-specific: this is the `UploadPack::serve` /
/// `ReceivePack::serve` `transport.rs` calls, over the engine `transport.rs`
/// opens. The only thing missing is the policy gate, and [`AllowAll`] is named
/// rather than implied.
fn serve(service: &str, repo: &Path, engine: Engine) -> Res<()> {
    let root = store_root(repo, engine);
    let kind = hash_kind(repo);
    let store = engine.open_repo(&root, kind)?;

    // A serve against an unprepared store is a red, not a lazy build. See the
    // module doc: setup inside the measured region is the defect, not the fix.
    if store.serve().refs()?.is_empty() {
        return Err(format!(
            "{} holds no refs; run `{} prepare {}` before measuring",
            root.display(),
            std::env::args().next().unwrap_or_default(),
            repo.display()
        )
        .into());
    }

    let stdin = std::io::stdin();
    let mut input = BufReader::with_capacity(gunnar_wire::TRANSPORT_BUFFER_BYTES, stdin.lock());
    let stdout = std::io::stdout();
    let mut out = BufWriter::with_capacity(gunnar_wire::TRANSPORT_BUFFER_BYTES, stdout.lock());
    let started = Instant::now();

    match service {
        "git-upload-pack" => {
            let version =
                ProtocolVersion::from_git_protocol(std::env::var("GIT_PROTOCOL").ok().as_deref());
            let summary =
                UploadPack::new(store.serve(), kind).serve(version, &mut input, &mut out)?;
            out.flush()?;
            eprintln!(
                "{engine}: served git-upload-pack v{} rounds={} common={} selected={} \
                 objects={} copied={} recompressed={} bitmap={} {:.1} ms",
                version.number(),
                summary.fetch_rounds,
                // Printed beside the rest because it is the one number that
                // says whether negotiation *worked*: a fetch that sent the
                // right pack while acknowledging nothing common is a fetch the
                // client warns about ("no common commits") and a round-trip
                // count nobody can compare.
                summary.common,
                // What `select` chose, printed BEFORE what went out, because
                // the pair is the reading and `objects` alone is the half that
                // misleads: measured 18 selected against 157 emitted on a
                // one-commit fetch out of a packed store (the 4-core laptop, 2026-08-10).
                summary.objects_selected,
                summary.objects_sent,
                summary.objects_copied_from_pack,
                summary.objects_recompressed,
                summary.answered_from_bitmap,
                started.elapsed().as_secs_f64() * 1000.0,
            );
            // The copy split, in the file and the JSON shape the bench meter
            // already reads off the gix arm. `copied`/`recompressed` is the one
            // applied output that says *why* a pack came out the size it did.
            copy_stats(
                summary.objects_copied_from_pack,
                summary.objects_recompressed,
            )?;
        }
        "git-receive-pack" => {
            let ingest = ExampleIngest {
                git_dir: root.clone(),
                hash_kind: kind,
                engine,
            };
            let outcome = ReceivePack::new(store.serve(), kind)
                .serve(&mut input, &mut out, &ingest, &AllowAll)?;
            out.flush()?;
            match outcome {
                Some(o) => eprintln!(
                    "{engine}: served git-receive-pack commands={} statuses={:?} {:.1} ms",
                    o.commands.len(),
                    o.statuses,
                    started.elapsed().as_secs_f64() * 1000.0,
                ),
                None => eprintln!("{engine}: served git-receive-pack, no commands"),
            }
        }
        other => return Err(format!("this store serves no {other:?}").into()),
    }
    Ok(())
}

/// The push ingest, which is [`Engine::ingest_push`] and a receipt.
///
/// The same three facts `transport.rs`'s `PushIngest` fills in, and for the same
/// reasons: the object count and version come off the stored pack's own header,
/// `pack_id` is its trailing checksum, and `index_id` is a **null** oid because
/// The storage contract replaced the `.idx` with the `objects` table — there is no index file to
/// have a checksum, and this tree's spelling for that is the null oid.
///
/// `want_objects: false`, and it is [`AllowAll`] above that makes it honest: no
/// gate here reads the pushed set, so `new_objects` is an `Err` naming the
/// reason rather than an empty set a gate would pass.
struct ExampleIngest {
    git_dir: PathBuf,
    hash_kind: gix_hash::Kind,
    engine: Engine,
}

impl gunnar_wire::PackIngest for ExampleIngest {
    fn ingest(&self, pack: &mut dyn BufRead) -> gunnar_wire::Result<gunnar_wire::PackReceipt> {
        let received = self
            .engine
            .ingest_push(
                &self.git_dir,
                self.hash_kind,
                pack,
                gunnar_server::storage::ObjectSet::NotNeeded,
                None,
                None,
            )
            .map_err(|err| {
                gunnar_wire::Error::Store(gunnar_store::Error::backend(format!(
                    "storing the pushed pack: {err:#}"
                )))
            })?;
        let pack_id = gix_hash::ObjectId::try_from(received.pack_id.as_slice()).map_err(|_| {
            gunnar_wire::Error::Store(gunnar_store::Error::backend(format!(
                "the stored pack's trailer is {} bytes, which is not a {} object id",
                received.pack_id.len(),
                self.hash_kind
            )))
        })?;
        Ok(gunnar_wire::PackReceipt::without_object_set(
            gunnar_wire::pack::IndexedPack {
                num_objects: received.num_objects,
                pack_id,
                index_id: gix_hash::ObjectId::null(self.hash_kind),
                pack_version: gunnar_wire::pack::PackVersion::from_u32(received.version),
                hash_kind: self.hash_kind,
                index_path: None,
                data_path: None,
                keep_path: None,
                // No `.pack` to stat, but the store counted what it appended —
                // the same answer the real server gives (`transport.rs`).
                stored_bytes: Some(received.pack_bytes),
            },
        ))
    }
}

/// Ask the store about one oid and print the **error**, not the `false` a
/// boolean is forced to collapse it into.
///
/// A diagnostic, never part of a measurement, and it earned its place: a push
/// refused with "missing necessary objects" and a swallowed lookup error look
/// identical from the client's end.
/// It also **times the open**, which is not decoration. On the znippy arm the
/// open is where the derived tables are rebuilt (`Absorber::refold`, driven from
/// `GitStore::open_with_arms`), and that rebuild is what makes a cleanly
/// restarted store able to answer `select` at all — an empty commit graph makes
/// `select` return `Ok(None)` and `emit_pack` refuse, both correctly. So "what
/// does a cold open cost" is the number that decides whether the rebuild can
/// stay on the open path, and this is where to read it.
fn has(repo: &Path, oid: &str, engine: Engine) -> Res<()> {
    let root = store_root(repo, engine);
    let kind = hash_kind(repo);
    let opened = Instant::now();
    let store = engine.open_repo(&root, kind)?;
    let open_ms = opened.elapsed().as_secs_f64() * 1000.0;
    let refs = store.serve().refs()?.len();
    if oid.is_empty() {
        println!("engine={engine} refs={refs} open={open_ms:.1} ms");
        return Ok(());
    }
    let raw = hex_oid(oid, kind)?;
    // `select` over the one tip, because that is the call a clone makes and the
    // one that answers `Ok(None)` on a store whose projection is empty. Printed
    // as the count or as the decline, never collapsed into a boolean.
    let selected = store
        .serve()
        .select(&[&raw], &[])
        .map(|r| r.map(|s| (s.objects.len(), s.commits.len())));
    // `extents` beside `has`, because the two are not the same question and the
    // proposed `contains_batch` turned on them being. See
    // `have_batch`'s header: the gix backend's `extents` reads its pack set
    // alone while its `has` also asks the loose object database, so a loose
    // object answers `None` here and `true` there. That divergence is the
    // reason the batch seam cannot be built out of `extents` alone, and it is
    // printed rather than described so it can be seen.
    println!(
        "engine={engine} refs={refs} open={open_ms:.1} ms has({oid})={:?} header={:?} \
         extents={:?} select(objects,commits)={selected:?}",
        store.serve().has(&raw),
        store.serve().header(&raw),
        store.serve().extents(&[&raw]).map(|v| v[0].is_some()),
    );
    Ok(())
}

// ────────────────────────────────────────────────────────── havebatch ────

/// One shape's have list: `want_hits` real oids out of `pool`, the rest a
/// client's own commits.
///
/// The absent ones are well-formed oids this server has never seen, with three
/// bytes flipped so that no one-bit near-miss in an index can answer them. The
/// generator is a fixed LCG, so two runs of this binary probe the same set and
/// a before/after is a before/after.
fn have_list(pool: &[Vec<u8>], n: usize, hit_frac: f64) -> Vec<Vec<u8>> {
    let want_hits = (n as f64 * hit_frac).round() as usize;
    let mut state: u64 = 0x5EED_1234 | 1;
    let mut next = move || {
        state = state
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        state ^ (state >> 31)
    };
    let mut out: Vec<Vec<u8>> = Vec::with_capacity(n);
    for i in 0..n {
        let mut o = pool[(next() % pool.len() as u64) as usize].clone();
        if i >= want_hits {
            o[0] ^= 0xff;
            o[1] ^= (i & 0xff) as u8;
            let last = o.len() - 1;
            o[last] ^= 0x5a;
        }
        out.push(o);
    }
    out
}

/// The shapes, straight off `bench/src/have_lookup.rs`'s 18-repository sweep of
/// real `git fetch`es: git's window is **16 and doubles**, and `HaveMemo`
/// collapses a negotiation to one membership call per *distinct* have. 256 is
/// `MAX_IN_VAIN`'s ceiling, was never observed, and says so in its name.
const HAVE_SHAPES: &[(&str, usize, f64)] = &[
    ("behind16", 16, 1.0),
    ("diverge16", 16, 0.375),
    ("diverge32", 32, 0.25),
    ("diverge64", 64, 0.20),
    ("diverge128", 128, 0.162),
    ("cap256_unobs", 256, 0.0),
];

/// **The measurement**: what a batch membership test is worth at gunnar's
/// own seam, against a real repository through the real engine.
///
/// The record shows `GitOps` grew batch primitives — `extents`, `sizes`,
/// `select`, `reachable` — while the one question negotiation asks a thousand
/// times, membership, stayed scalar: `GitOps::has` takes one oid. Every claim
/// about what fixing that is worth has so far been an inference from the *store
/// side* (znippy's own `lookup_batch`, 3.9x at batch 100, the reference desktop 2026-08-07), and
/// and the standing record says in as many words that **the gunnar seam is
/// unmeasured**. This mode is the caller-side number, and it is
/// built to be capable of coming back "worth nothing".
///
/// # Three columns, and the third is the only one that could ship
///
/// | column | what it does | correct on |
/// |---|---|---|
/// | `scalar` | `has()` per oid — **gunnar as it ships** | every backend |
/// | `extents` | one `extents()`, `is_some()` per position | znippy only |
/// | `hybrid` | one `extents()`, then `has()` for the `None`s | every backend |
///
/// The middle column is **not a candidate**, and measuring it anyway is the
/// point: it is the obvious implementation and it is WRONG. The gix backend's
/// `extents` reads its pack set alone
/// (`edda/crates/storage-git-gix/src/lib.rs:809-828`) while its `has` also asks
/// the loose object database (`:766-787`), so a loose object answers `None` to
/// `extents` and `true` to `has`. On a `have` that is only a wider pack; on a
/// `want` it is a refused fetch. `hybrid` is what survives that, because a
/// `Some(extent)` means the object is in a pack and is therefore authoritative
/// in the one direction that matters — only the `None`s have to be re-asked.
/// The disagreement is **printed rather than raised**, because refusing would
/// hide the very thing the middle column exists to expose.
///
/// # Cold and warm are both reported, and neither is the other
///
/// `cold_*` is the first touch on a **freshly opened** store, before this
/// process has faulted a single index page for those oids. It is one sample by
/// definition — there is only ever one first touch — so it is printed as a raw
/// nanosecond figure and never as a ratio of medians. The estimate was 4-8x "on
/// cold pages"; a warm-only number cannot speak to that and does not pretend to.
///
/// # The probe set means what it says
///
/// Every "present" have is checked present and every "absent" have is checked
/// absent, against `has()`, **before** the clock starts, and the columns are
/// asserted to agree on the count afterwards. A fast column that answers a
/// different question is the failure this tree keeps being burned by.
///
/// # The end-to-end table is the one that decides
///
/// `saved_pct` is the **ceiling** on a batch seam here: the whole scalar-minus-
/// hybrid difference, taken against a real negotiated fetch served through the
/// same `UploadPack::serve_v2` `transport.rs` runs. It assumes the saving is
/// free to collect, which it is not. If that ceiling is a rounding error, the
/// finding is closed by measurement rather than by argument.
fn have_batch(repo: &Path, engine: Engine) -> Res<()> {
    let root = store_root(repo, engine);
    let kind = hash_kind(repo);
    let opened = Instant::now();
    let store = engine.open_repo(&root, kind)?;
    let open_ms = opened.elapsed().as_secs_f64() * 1000.0;
    let serve = store.serve();

    let refs = serve.refs()?;
    let tip = refs
        .iter()
        .filter(|r| r.name.starts_with("refs/heads/"))
        .find_map(|r| r.oid.clone())
        .or_else(|| refs.iter().find_map(|r| r.oid.clone()))
        .ok_or_else(|| format!("{} advertises no oid-valued ref", root.display()))?;

    // The present pool is the tip's own closure — real objects the store really
    // holds, not oids invented for the probe. Commits when there are enough of
    // them, because a `have` is a commit.
    let reach = serve
        .select(&[&tip], &[])?
        .ok_or("the store declined to select over its own tip")?;
    let pool: Vec<Vec<u8>> = if reach.commits.len() >= 256 {
        reach.commits.clone()
    } else {
        reach.objects.clone()
    };
    if pool.len() < 256 {
        return Err(format!(
            "the tip's closure is {} object(s); a membership probe that small measures L1",
            pool.len()
        )
        .into());
    }

    let reps: usize = std::env::var("GUNNAR_HAVEBATCH_REPS")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(2001);

    println!(
        "engine={engine} store={} open={open_ms:.1}ms pool={} reps={reps}",
        root.display(),
        pool.len()
    );
    println!(
        "{:<14} {:>5} {:>10} {:>11} {:>10} {:>8} {:>8} {:>12} {:>12}",
        "shape",
        "haves",
        "ns_scalar",
        "ns_extents",
        "ns_hybrid",
        "ext_x",
        "hyb_x",
        "cold_scalar",
        "cold_hybrid"
    );

    // The scalar and hybrid rates each shape measured, carried to the
    // end-to-end table below so the share it prints is this run's own number and
    // not one copied off another box.
    let mut rate: Vec<(f64, f64)> = Vec::with_capacity(HAVE_SHAPES.len());

    for (name, n, hit_frac) in HAVE_SHAPES.iter().copied() {
        let want_hits = (n as f64 * hit_frac).round() as usize;
        let haves = have_list(&pool, n, hit_frac);
        let slices: Vec<&[u8]> = haves.iter().map(|v| v.as_slice()).collect();

        // The probe set, checked rather than assumed.
        let mut present = 0usize;
        for (i, o) in slices.iter().enumerate() {
            let here = serve.has(o)?;
            if here {
                present += 1;
            }
            if (i < want_hits) != here {
                return Err(format!(
                    "{name}: have {i} is {}, the shape says {}",
                    if here { "present" } else { "absent" },
                    if i < want_hits { "present" } else { "absent" }
                )
                .into());
            }
        }

        // ── cold: the first touch, on a store opened moments ago ────────────
        let cold_store = engine.open_repo(&root, kind)?;
        let cold = cold_store.serve();
        let t = Instant::now();
        let mut acc = 0usize;
        for o in &slices {
            if cold.has(o)? {
                acc += 1;
            }
        }
        let cold_scalar = t.elapsed().as_nanos() as f64 / n as f64;

        let cold_store2 = engine.open_repo(&root, kind)?;
        let cold2 = cold_store2.serve();
        let t = Instant::now();
        let ext = cold2.extents(&slices)?;
        let mut acc2 = 0usize;
        for (i, e) in ext.iter().enumerate() {
            if e.is_some() || cold2.has(slices[i])? {
                acc2 += 1;
            }
        }
        let cold_hybrid = t.elapsed().as_nanos() as f64 / n as f64;
        if acc != present || acc2 != present {
            return Err(format!(
                "{name}: the cold columns found {acc}/{acc2} present, the checked set says \
                 {present}"
            )
            .into());
        }

        // ── warm ────────────────────────────────────────────────────────────
        let mut ns: [f64; 3] = [0.0; 3];
        let mut found: [usize; 3] = [0; 3];
        for (col, slot) in ns.iter_mut().enumerate() {
            let mut samples: Vec<f64> = Vec::with_capacity(9);
            for rep in 0..9 {
                let t = Instant::now();
                let mut hits = 0usize;
                for _ in 0..reps {
                    match col {
                        0 => {
                            for o in &slices {
                                if serve.has(o)? {
                                    hits += 1;
                                }
                            }
                        }
                        1 => {
                            for e in serve.extents(&slices)? {
                                if e.is_some() {
                                    hits += 1;
                                }
                            }
                        }
                        _ => {
                            for (i, e) in serve.extents(&slices)?.iter().enumerate() {
                                if e.is_some() || serve.has(slices[i])? {
                                    hits += 1;
                                }
                            }
                        }
                    }
                }
                samples.push(t.elapsed().as_secs_f64() * 1e9 / (reps * n) as f64);
                if rep == 0 {
                    found[col] = hits / reps;
                }
            }
            samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
            *slot = samples[0];
        }
        if found[0] != present || found[2] != present {
            return Err(format!(
                "{name}: scalar found {} and hybrid found {}, the checked set says {present}",
                found[0], found[2]
            )
            .into());
        }
        let ext_note = if found[1] == present {
            ""
        } else {
            " EXTENTS-DISAGREES"
        };

        rate.push((ns[0], ns[2]));
        println!(
            "{name:<14} {n:>5} {:>10.1} {:>11.1} {:>10.1} {:>8.3} {:>8.3} {cold_scalar:>12.1} \
             {cold_hybrid:>12.1}{ext_note}",
            ns[0],
            ns[1],
            ns[2],
            ns[0] / ns[1],
            ns[0] / ns[2],
        );
    }

    // ── end to end ──────────────────────────────────────────────────────────
    println!();
    println!(
        "{:<14} {:>5} {:>10} {:>10} {:>10} {:>9}",
        "e2e_shape", "haves", "fetch_ms", "probe_us", "saved_pct", "objects"
    );
    let want_hex = tip.iter().map(|b| format!("{b:02x}")).collect::<String>();
    let format = if kind == gix_hash::Kind::Sha256 {
        "sha256"
    } else {
        "sha1"
    };
    for (idx, (name, n, hit_frac)) in HAVE_SHAPES.iter().copied().enumerate() {
        let haves = have_list(&pool, n, hit_frac);
        // The pkt-line shape stock git sends, the same one
        // `bench/src/stages.rs::fetch_request` builds. `0001` is the delimiter
        // between the command and its arguments and is a raw four-byte marker,
        // NOT a pkt-line carrying a payload.
        let pkt = |s: &str| -> Vec<u8> {
            let mut v = format!("{:04x}", s.len() + 4).into_bytes();
            v.extend_from_slice(s.as_bytes());
            v
        };
        let mut req: Vec<u8> = Vec::new();
        req.extend(pkt("command=fetch\n"));
        req.extend(pkt(&format!("object-format={format}\n")));
        req.extend_from_slice(b"0001");
        req.extend(pkt("thin-pack\n"));
        req.extend(pkt("no-progress\n"));
        req.extend(pkt("ofs-delta\n"));
        req.extend(pkt(&format!("want {want_hex}\n")));
        for h in &haves {
            let hex = h.iter().map(|b| format!("{b:02x}")).collect::<String>();
            req.extend(pkt(&format!("have {hex}\n")));
        }
        req.extend(pkt("done\n"));
        req.extend_from_slice(b"0000");

        let mut samples: Vec<f64> = Vec::new();
        let mut objects = 0u32;
        let mut digest: u64 = 0;
        for _ in 0..5 {
            let mut input = BufReader::new(std::io::Cursor::new(req.clone()));
            let mut out: Vec<u8> = Vec::new();
            let t = Instant::now();
            let summary = UploadPack::new(serve, kind).serve_v2(&mut input, &mut out)?;
            samples.push(t.elapsed().as_secs_f64() * 1000.0);
            objects = summary.objects_sent;
            let mut d: u64 = 0xcbf2_9ce4_8422_2325;
            for byte in &out {
                d ^= u64::from(*byte);
                d = d.wrapping_mul(0x0000_0100_0000_01b3);
            }
            digest = d;
        }
        samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
        let fetch_ms = samples[samples.len() / 2];
        // The CEILING on a batch seam here: the whole scalar-minus-hybrid
        // difference, taken against the fetch it would be collected inside.
        let (ns_scalar, ns_hybrid) = rate[idx];
        let probe_us = ns_scalar * n as f64 / 1e3;
        let saved_us = (ns_scalar - ns_hybrid) * n as f64 / 1e3;
        println!(
            "{name:<14} {n:>5} {fetch_ms:>10.3} {probe_us:>10.3} {:>10.4} {objects:>9}  \
             digest={digest:016x}",
            100.0 * saved_us / (fetch_ms * 1e3),
        );
    }
    Ok(())
}

fn hex_oid(oid: &str, kind: gix_hash::Kind) -> Res<Vec<u8>> {
    let raw = (0..oid.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&oid[i..i + 2], 16))
        .collect::<Result<Vec<u8>, _>>()?;
    if raw.len() != kind.len_in_bytes() {
        return Err(format!(
            "{oid} is {} bytes, and this repository's hash is {kind}",
            raw.len()
        )
        .into());
    }
    Ok(raw)
}

/// Print one op's timing as a JSON line: median/min/max in microseconds.
fn ops_emit(engine: &str, fixture: &str, op: &str, batch: usize, mut ns: Vec<u128>) {
    if ns.is_empty() {
        return;
    }
    ns.sort_unstable();
    let us = |x: u128| x as f64 / 1000.0;
    let med = us(ns[ns.len() / 2]);
    let mn = us(ns[0]);
    let mx = us(ns[ns.len() - 1]);
    println!(
        "{{\"engine\":\"{engine}\",\"fixture\":\"{fixture}\",\"op\":\"{op}\",\"reps\":{},\
         \"batch\":{batch},\"median_us\":{med:.3},\"min_us\":{mn:.3},\"max_us\":{mx:.3}}}",
        ns.len()
    );
}

/// **Per-storage-op isolation timer, on whichever engine `GUNNAR_ENGINE` selects.**
///
/// Times the `GitOps`/`GitServe` calls the wire is built out of — the batch have
/// path (`has`/`size`/`header`/`extents`), the object read (`read` = full bytes
/// vs `header` = type+size only), the ref namespace ops (`refs`/`head`/`set_head`/
/// `update_ref` single-CAS/`put_refs_cas` batch-CAS) and reachability
/// (`reachable`) — each in its own cold loop of `GUNNAR_OPS_REPS` reps, against
/// the already-`prepare`d `<repo>.gunnar-<engine>` store. One JSON line per op on
/// stdout; the engine seam is the SAME `Engine::open_repo` the server and the
/// clone matrix use, so a number here and a number there are the same engine.
///
/// Measure, never assert: every timed op asserts its result inside the loop — `head` is `Some`,
/// `has(tip)` is true, `read`'s byte length equals `header`'s size, `set_head`
/// reads back what was set, and a `update_ref` with a deliberately wrong `old`
/// is REJECTED (a silent accept is the bug this proves absent).
fn opsprobe(repo: &Path, engine: Engine) -> Res<()> {
    let root = store_root(repo, engine);
    let kind = hash_kind(repo);
    let store = engine.open_repo(&root, kind)?;
    let serve = store.serve();
    let base = serve.refs()?;
    if base.is_empty() {
        return Err(format!("{} holds no refs; run `prepare` first", root.display()).into());
    }
    let reps: usize = std::env::var("GUNNAR_OPS_REPS")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(5);
    let batch: usize = std::env::var("GUNNAR_OPS_BATCH")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(128);
    let eng = engine.name();
    // A per-run nonce so the scratch refs this probe creates never collide with a
    // previous run's (create-CAS demands the name not already exist).
    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    let fixture = repo
        .file_name()
        .map(|s| s.to_string_lossy().into_owned())
        .unwrap_or_default();
    // The tip to read/probe: refs/heads/main, else the first ref carrying an oid.
    let tip: Vec<u8> = base
        .iter()
        .find(|r| r.name == "refs/heads/main")
        .and_then(|r| r.oid.clone())
        .or_else(|| base.iter().find_map(|r| r.oid.clone()))
        .ok_or("no ref carries an oid to probe")?;
    let head_target = base
        .iter()
        .find(|r| r.name == "refs/heads/main")
        .map(|_| "refs/heads/main".to_string())
        .unwrap_or_else(|| base[0].name.clone());

    // ── read ops (idempotent) ────────────────────────────────────────────────
    // refs
    let mut ns = Vec::with_capacity(reps);
    for _ in 0..reps {
        let t = Instant::now();
        let r = serve.refs()?;
        ns.push(t.elapsed().as_nanos());
        if r.len() != base.len() {
            return Err("refs count drifted mid-probe".into());
        }
    }
    ops_emit(eng, &fixture, "refs", base.len(), ns);
    // head
    let mut ns = Vec::with_capacity(reps);
    for _ in 0..reps {
        let t = Instant::now();
        let h = serve.head()?;
        ns.push(t.elapsed().as_nanos());
        if h.is_none() {
            return Err("head() returned None on a prepared store".into());
        }
    }
    ops_emit(eng, &fixture, "head", 0, ns);
    // has
    let mut ns = Vec::with_capacity(reps);
    for _ in 0..reps {
        let t = Instant::now();
        let ok = serve.has(&tip)?;
        ns.push(t.elapsed().as_nanos());
        if !ok {
            return Err("has(tip) was false for a ref this store holds".into());
        }
    }
    ops_emit(eng, &fixture, "has", 0, ns);
    // size
    let mut ns = Vec::with_capacity(reps);
    let mut sz_from_size = None;
    for _ in 0..reps {
        let t = Instant::now();
        let s = serve.size(&tip)?;
        ns.push(t.elapsed().as_nanos());
        sz_from_size = s;
        if s.is_none() {
            return Err("size(tip) was None".into());
        }
    }
    ops_emit(eng, &fixture, "size", 0, ns);
    // header (type + size, no bytes)
    let mut ns = Vec::with_capacity(reps);
    let mut sz_from_header = None;
    for _ in 0..reps {
        let t = Instant::now();
        let h = serve.header(&tip)?;
        ns.push(t.elapsed().as_nanos());
        match h {
            Some((_, sz)) => sz_from_header = Some(sz),
            None => return Err("header(tip) was None".into()),
        }
    }
    ops_emit(eng, &fixture, "header", 0, ns);
    // read (full bytes) — and cross-check its length against header's size (measured, not asserted)
    let mut ns = Vec::with_capacity(reps);
    for _ in 0..reps {
        let t = Instant::now();
        let r = serve.read(&tip)?;
        ns.push(t.elapsed().as_nanos());
        match r {
            Some((_, bytes)) => {
                if Some(bytes.len() as u64) != sz_from_header {
                    return Err(format!(
                        "read() gave {} bytes but header() said {:?}",
                        bytes.len(),
                        sz_from_header
                    )
                    .into());
                }
            }
            None => return Err("read(tip) was None".into()),
        }
    }
    ops_emit(eng, &fixture, "read", 0, ns);
    // sanity: size() and header() must agree
    if sz_from_size != sz_from_header {
        return Err(
            format!("size()={sz_from_size:?} disagrees with header()={sz_from_header:?}").into(),
        );
    }
    // extents (pack-extent lookup; may be None on gix for a loose object — tolerated)
    let mut ns = Vec::with_capacity(reps);
    for _ in 0..reps {
        let t = Instant::now();
        let _ = serve.extents(&[&tip])?;
        ns.push(t.elapsed().as_nanos());
    }
    ops_emit(eng, &fixture, "extents", 0, ns);
    // reachable (the frontier a clone of one tip walks) — tolerate an engine that
    // declines it, recording the decline rather than aborting the whole probe.
    let mut ns = Vec::with_capacity(reps);
    let mut reach_err = None;
    for _ in 0..reps {
        let t = Instant::now();
        match serve.reachable(&[&tip], &[]) {
            Ok(v) => {
                ns.push(t.elapsed().as_nanos());
                if v.is_empty() {
                    reach_err = Some("reachable(tip,[]) returned an empty frontier".to_string());
                    break;
                }
            }
            Err(e) => {
                reach_err = Some(e.to_string());
                break;
            }
        }
    }
    match reach_err {
        None => ops_emit(eng, &fixture, "reachable", 0, ns),
        Some(e) => println!(
            "{{\"engine\":\"{eng}\",\"fixture\":\"{fixture}\",\"op\":\"reachable\",\"error\":{:?}}}",
            e
        ),
    }

    // ── t/perf-style BULK arms (git `cat-file --batch` / `--batch-check`) ──────
    // git's de-facto perf standard (`t/perf/p*.sh`) times bulk object streaming,
    // not one object at a time. The single `read`/`header`/`size` arms above miss
    // that: they touch ONE oid. These two arms stream the whole reachable frontier
    // — the object set a clone of `tip` must serve — the way `cat-file --batch`
    // (full bytes) and `--batch-check` (type+size only) walk a repository. The
    // frontier is resolved ONCE, outside every timed region, so the arms measure
    // bulk access and not the reachability walk (which `reachable` already owns).
    let frontier: Vec<Vec<u8>> = serve.reachable(&[&tip], &[]).unwrap_or_default();
    if !frontier.is_empty() {
        let oids: Vec<&[u8]> = frontier.iter().map(|o| o.as_slice()).collect();
        // batch_check — bulk type+size over the whole frontier via the trait's
        // BULK `sizes()` path, which no single-op arm here exercises (the analog of
        // `git cat-file --batch-check`). Measure, never assert: the result vector must be the same
        // length as the input, carry no `None` (every reachable object must resolve
        // a size), and — the guard that catches a hollow bulk path — its total must
        // equal the sum of the per-object `size()` calls, i.e. bulk agrees with
        // singular. A `sizes()` that returned zeros or a short vector fails here.
        let mut single_total: u64 = 0;
        for o in &oids {
            single_total += serve.size(o)?.ok_or("size() None for a reachable object")?;
        }
        let mut ns = Vec::with_capacity(reps);
        for _ in 0..reps {
            let t = Instant::now();
            let szs = serve.sizes(&oids)?;
            ns.push(t.elapsed().as_nanos());
            if szs.len() != oids.len() {
                return Err(format!(
                    "sizes() returned {} entries for {} oids",
                    szs.len(),
                    oids.len()
                )
                .into());
            }
            let bulk_total: u64 = szs
                .iter()
                .map(|s| s.ok_or("sizes() had a None for a reachable object"))
                .sum::<std::result::Result<u64, _>>()?;
            if bulk_total != single_total {
                return Err(format!(
                    "bulk sizes() total {bulk_total} disagrees with per-object size() total {single_total}"
                )
                .into());
            }
        }
        ops_emit(eng, &fixture, "batch_check", frontier.len(), ns);
        // cat_file_batch — bulk FULL read of every frontier object (the analog of
        // `git cat-file --batch`). Measure, never assert: every read is `Some`, the summed byte
        // total is non-zero, and it is IDENTICAL across reps and equal to the
        // `batch_check` size total — three ways a store that silently dropped or
        // truncated an object would be caught, none of which a single-oid `read`
        // can see.
        let mut ns = Vec::with_capacity(reps);
        for _ in 0..reps {
            let mut total: u64 = 0;
            let t = Instant::now();
            for o in &oids {
                match serve.read(o)? {
                    Some((_, b)) => total += b.len() as u64,
                    None => return Err("cat_file_batch: read() None for a reachable object".into()),
                }
            }
            ns.push(t.elapsed().as_nanos());
            if total == 0 {
                return Err("cat_file_batch read 0 bytes over a non-empty frontier".into());
            }
            if total != single_total {
                return Err(format!(
                    "cat_file_batch byte total {total} disagrees with sizes() total {single_total}"
                )
                .into());
            }
        }
        ops_emit(eng, &fixture, "cat_file_batch", frontier.len(), ns);
    }

    // ── write ops (each rep uses a UNIQUE scratch ref name so CAS never fights) ─
    // set_head: write, then read back and assert (timing is the set_head only).
    let mut ns = Vec::with_capacity(reps);
    for _ in 0..reps {
        let t = Instant::now();
        serve.set_head(&head_target)?;
        ns.push(t.elapsed().as_nanos());
        let h = serve.head()?.ok_or("head() None after set_head")?;
        let ok = h.symref_target.as_deref() == Some(head_target.as_str()) || h.oid.is_some();
        if !ok {
            return Err("set_head did not read back".into());
        }
    }
    ops_emit(eng, &fixture, "set_head", 0, ns);
    // update_ref: single create-CAS (old = None = must-not-exist), unique name/rep.
    let mut ns = Vec::with_capacity(reps);
    for i in 0..reps {
        let name = format!("refs/heads/opsprobe/single/{eng}/{nonce}/{i}");
        let t = Instant::now();
        serve.update_ref(&name, None, Some(&tip))?;
        ns.push(t.elapsed().as_nanos());
    }
    ops_emit(eng, &fixture, "update_ref", 0, ns);
    // Negative CAS (measured, not asserted): a wrong `old` on an existing ref MUST be rejected.
    let mut wrong = tip.clone();
    wrong[0] ^= 0xff;
    let neg = serve.update_ref(&head_target, Some(&wrong), Some(&tip));
    if neg.is_ok() {
        return Err("update_ref accepted a compare-and-swap with a wrong old value".into());
    }
    // put_refs_cas: a batch of `batch` create-CAS in one call, unique names/rep.
    let mut ns = Vec::with_capacity(reps);
    for i in 0..reps {
        let names: Vec<String> = (0..batch)
            .map(|j| format!("refs/heads/opsprobe/batch/{eng}/{nonce}/{i}/{j}"))
            .collect();
        let edits: Vec<gunnar_store::git_storage_trait::RefCas> = names
            .iter()
            .map(|n| gunnar_store::git_storage_trait::RefCas {
                name: n.clone(),
                old: None,
                new: Some(&tip),
                peeled: None,
            })
            .collect();
        let before = serve.refs()?.len();
        let t = Instant::now();
        serve.put_refs_cas(&edits)?;
        ns.push(t.elapsed().as_nanos());
        let after = serve.refs()?.len();
        if after != before + batch {
            return Err(format!(
                "put_refs_cas of {batch} refs moved the count by {}",
                after as i64 - before as i64
            )
            .into());
        }
    }
    ops_emit(eng, &fixture, "put_refs_cas", batch, ns);
    Ok(())
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let mode = args.get(1).cloned().unwrap_or_default();
    let repo = PathBuf::from(args.get(2).cloned().unwrap_or_default());

    // See the module doc: a red switch this binary cannot honour must stop it,
    // not be ignored by it.
    if std::env::var_os("GUNNAR_BENCH_NO_PACK_COPY").is_some() && mode != "prepare" {
        eprintln!(
            "git-store-serve: GUNNAR_BENCH_NO_PACK_COPY is set, and this server has no \
             NoPackCopy shim to honour it with. Serving anyway would report a switched run's \
             label over an unswitched server."
        );
        std::process::exit(1);
    }

    let engine = match Engine::selected() {
        Ok(e) => e,
        Err(err) => {
            eprintln!("git-store-serve: {err:#}");
            std::process::exit(1);
        }
    };

    let result = match mode.as_str() {
        "prepare" => prepare(&repo, engine),
        "opsprobe" => opsprobe(&repo, engine),
        "havebatch" => have_batch(&repo, engine),
        "has" => has(
            &repo,
            args.get(3).map(String::as_str).unwrap_or_default(),
            engine,
        ),
        "git-upload-pack" | "git-receive-pack" => serve(&mode, &repo, engine),
        other => Err(format!(
            "usage: {} prepare|opsprobe|has|havebatch|git-upload-pack|git-receive-pack <repo>  \
             (got {other:?}); \
             this binary carries {}",
            args.first()
                .map(String::as_str)
                .unwrap_or("git-store-serve"),
            Engine::compiled_list(),
        )
        .into()),
    };
    // **The third exit shape, and the one that made this a measurement.**
    //
    // The two shipped binaries reach `gunnar_server::run`, which closes the
    // stores itself; this one calls the engine directly, so it closes them
    // itself too. On the error path as well: `process::exit` runs no destructor,
    // and a bench server that leaves the database dirty poisons the *next* run
    // rather than its own — MEASURED 2026-08-10, +427 MB RSS and +107 k faults
    // on a subsequent clone that emits a byte-identical pack. See
    // `gunnar_server::storage::shutdown`.
    gunnar_server::storage::shutdown();
    if let Err(err) = result {
        eprintln!("git-store-serve: {err}");
        std::process::exit(1);
    }
}