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

nordisk/gunnar

bench/src/stages.rs

//! Serving one request, **broken into named stages**. See [`fetch`] and
//! [`push`].
//!
//! # What this is for
//!
//! Every other arm in this harness measures a whole operation and reports one
//! number. When the number moves, nothing says which stage moved; when gunnar
//! loses, nothing says where. Two results on 2026-08-04 stalled on exactly
//! that, and both are questions about *stages* rather than about symbols:
//!
//! * CPU per request rises 2.54x from 1 to 32 concurrent clients while gitea's
//!   falls — [`concurrent_fetch`] runs the same decomposition at both widths;
//! * a negotiated fetch costs 2.2–2.6x on the znippy backend where a warm clone
//!   costs 1.06–1.41x, narrowed to "object reads, not graph traversal" —
//!   [`fetch`] runs both shapes against the same repository and charges every
//!   `ObjectStore::read` to the stage that issued it.
//!
//! # The instrument
//!
//! [`gunnar_stage`] does the bookkeeping and is compiled into `gunnar-wire`
//! only under its `stages` feature, which `bench/Cargo.toml` is the only thing
//! in the tree that turns on. This module installs the three facts that crate
//! deliberately refuses to gather for itself:
//!
//! | hook | source | why not in `gunnar-stage` |
//! |---|---|---|
//! | per-thread user/sys CPU | `getrusage(RUSAGE_THREAD)` | needs `libc`, outside the dependency budget |
//! | resident set | `/proc/self/statm` | ditto, and it is process-wide |
//! | allocation count/bytes | [`CountingAlloc`] | a `#[global_allocator]` is a binary's to choose |
//!
//! # Every column has been seen to report zero for the right reason
//!
//! `getrusage` failing, no RSS probe installed, no counting allocator linked —
//! each of those produces a **zero column**, and a zero column is shaped
//! exactly like "this stage is free". `pack_stats` shipped a persisted run that
//! way (busybox has no `find -printf`, the pipeline exited 0 through `cat`, and
//! it reported 0 packs). So [`Instruments`] records which hooks are actually
//! installed and every report carries it, and the arms assert non-vacuity
//! rather than trusting a tidy table.

use std::alloc::{GlobalAlloc, Layout};
use std::collections::BTreeMap;

use anyhow::{ensure, Context, Result};
use gix_hash::ObjectId;
use gunnar_server::SERVER_ALLOCATOR;
use gunnar_stage::{Report, Tally};
use gunnar_store::git_storage_trait::{
    Caps, Extent, GcReport, GitOps, GitServe, ObjType, Oid, PackStats, ReachSet, RefCas, RefRow,
    RefUpdate, Stored, TxId,
};
use gunnar_wire::upload_pack::UploadPack;

// ── the hooks ───────────────────────────────────────────────────────────────

/// `getrusage(RUSAGE_THREAD)`, in nanoseconds.
///
/// **`RUSAGE_THREAD` and not `RUSAGE_SELF`.** The concurrency question is a
/// question about 32 threads; `RUSAGE_SELF` sums all of them and would report
/// every worker as costing what the whole process cost. And not
/// `/proc/self/task/<tid>/stat` either: that answers in 10 ms clock ticks,
/// which is coarser than fourteen of the eighteen stages are long, so most of
/// the table would be a confident zero.
pub(crate) fn thread_cpu_ns() -> (u64, u64) {
    // SAFETY: `getrusage` writes a `struct rusage` through the pointer and
    // reads nothing else. The zeroed value is a valid `rusage`.
    let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
    if unsafe { libc::getrusage(libc::RUSAGE_THREAD, &mut usage) } != 0 {
        return (0, 0);
    }
    let ns = |t: libc::timeval| (t.tv_sec as u64) * 1_000_000_000 + (t.tv_usec as u64) * 1_000;
    (ns(usage.ru_utime), ns(usage.ru_stime))
}

/// `getrusage(RUSAGE_SELF)`: the **whole process**, every thread, in
/// nanoseconds.
///
/// The coverage check, and the reason it is not optional. `RUSAGE_THREAD` sees
/// only the thread that entered the stage, so any CPU a stage spends on worker
/// threads it spawned is invisible to the attribution — and it is not
/// hypothetical: `gix-pack` is built with `parallel` here, so
/// `index_thin_pack` fans out and the push's `pack_ingest` stage is charged
/// only its own thread. A table whose columns quietly describe a fraction of
/// the work is exactly what "measure, never assert" is about, so every report carries
/// [`StageRun::process_cpu_ns`] beside the stage sum and
/// [`StageRun::attributed_frac`] between them.
fn process_cpu_ns() -> u64 {
    // SAFETY: as `thread_cpu_ns`.
    let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
    if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } != 0 {
        return 0;
    }
    let ns = |t: libc::timeval| (t.tv_sec as u64) * 1_000_000_000 + (t.tv_usec as u64) * 1_000;
    ns(usage.ru_utime) + ns(usage.ru_stime)
}

/// Resident set in bytes, out of `/proc/self/statm`, **without allocating**.
///
/// Raw `open`/`read`/`close` into a stack buffer rather than
/// `fs::read_to_string`, because this runs at a stage boundary and every byte
/// it allocated would be charged to the stage it is measuring. An instrument
/// that shows up in its own table is not one.
fn rss_bytes() -> u64 {
    const PATH: &[u8] = b"/proc/self/statm\0";
    // SAFETY: `PATH` is NUL-terminated; `buf` is written at most `buf.len()`.
    unsafe {
        let fd = libc::open(PATH.as_ptr() as *const libc::c_char, libc::O_RDONLY);
        if fd < 0 {
            return 0;
        }
        let mut buf = [0u8; 128];
        let n = libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len() - 1);
        libc::close(fd);
        if n <= 0 {
            return 0;
        }
        let text = std::str::from_utf8_unchecked(&buf[..n as usize]);
        // "size resident shared text lib data dt", in pages.
        let Some(resident) = text.split_whitespace().nth(1) else {
            return 0;
        };
        let pages: u64 = resident.parse().unwrap_or(0);
        pages * (libc::sysconf(libc::_SC_PAGESIZE) as u64)
    }
}

/// A `#[global_allocator]` that tells [`gunnar_stage`] the size of every block,
/// **per thread**.
///
/// # Why the binary installs it and not this module
///
/// `#[global_allocator]` is a property of the whole executable. Installing one
/// in `examples/nornir-bench.rs` unconditionally would put a branch in front of
/// every allocation in six other lanes' arms, whose rows land in the same
/// table. It is therefore behind the `count-alloc` cargo feature, default off,
/// and `examples/stage-probe.rs` — which measures nothing else — has it on
/// always.
///
/// # The blindness this shape avoids
///
/// A counting allocator written in this repository before counted per
/// **process** while `cargo test` ran its cases on threads, so every byte
/// landed on whichever case happened to be running.
/// [`gunnar_stage::note_alloc`] touches nothing but thread-local `Cell`s, and
/// `gunnar-stage/tests/attribution.rs` holds two barrier-synchronised threads
/// doing deliberately lopsided work as the proof.
/// # It wraps the allocator the server ships, not `System`
///
/// `System` until 2026-08-11. The server's heap is
/// [`gunnar_server::ServerAllocator`] — mimalloc in every shipping build, worth
/// 2.2x on clone CPU — and a probe that counted `System` bytes instead would be
/// reporting an allocator no gunnar runs on. Same failure family as the zero
/// columns this module's header is about: a plausible number measured off the
/// real path.
///
/// The counts themselves are allocator-independent (they are `layout.size()`,
/// what the *program* asked for), so this changes no historical row. What it
/// fixes is the timing that surrounds them and any future column that reads
/// back what the allocator actually did.
pub struct CountingAlloc;

// SAFETY: every method forwards to `SERVER_ALLOCATOR` unchanged and the
// accounting call allocates nothing, so it cannot re-enter this allocator.
unsafe impl GlobalAlloc for CountingAlloc {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let p = SERVER_ALLOCATOR.alloc(layout);
        if !p.is_null() {
            gunnar_stage::note_alloc(layout.size());
        }
        p
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        gunnar_stage::note_dealloc(layout.size());
        SERVER_ALLOCATOR.dealloc(ptr, layout)
    }
    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
        let p = SERVER_ALLOCATOR.realloc(ptr, layout, new_size);
        if !p.is_null() {
            gunnar_stage::note_dealloc(layout.size());
            gunnar_stage::note_alloc(new_size);
        }
        p
    }
    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
        let p = SERVER_ALLOCATOR.alloc_zeroed(layout);
        if !p.is_null() {
            gunnar_stage::note_alloc(layout.size());
        }
        p
    }
}

/// Which of the four columns are actually being measured on this run.
///
/// Carried into every report so a zero can be read as "measured zero" or as
/// "not measured", which is the distinction `pack_stats` lost.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Instruments {
    /// A per-thread CPU clock is installed and answered non-zero at least once.
    pub cpu: bool,
    /// An RSS probe is installed and answered non-zero.
    pub rss: bool,
    /// The counting allocator is linked.
    pub alloc: bool,
}

/// Install the CPU clock, and the RSS probe unless `GUNNAR_STAGE_NO_RSS` is
/// set.
///
/// The RSS probe is three syscalls per stage boundary. On the eighteen-boundary
/// fetch that is ~54 syscalls, which is nothing beside the fetch — but the
/// concurrency arm runs it 32 ways at once against one `/proc` and the number
/// it reports there is process-wide anyway, so that arm turns it off and says
/// `rss: false` rather than publishing a figure that means nothing.
pub fn install() -> Instruments {
    gunnar_stage::set_cpu_clock(thread_cpu_ns);
    let want_rss = std::env::var_os("GUNNAR_STAGE_NO_RSS").is_none();
    if want_rss {
        gunnar_stage::set_rss_probe(rss_bytes);
    }
    Instruments {
        cpu: thread_cpu_ns() != (0, 0),
        rss: want_rss && rss_bytes() > 0,
        alloc: alloc_counting_linked(),
    }
}

/// Whether a counting allocator is actually in this binary.
///
/// Asked of the accounting rather than of a `cfg!`, because the `cfg!` is true
/// in the crate that *defines* [`CountingAlloc`] whether or not the binary
/// installed it. Allocates a vector under a stage and looks for it.
fn alloc_counting_linked() -> bool {
    gunnar_stage::reset();
    gunnar_stage::enable();
    {
        let _s = gunnar_stage::enter("__alloc_selftest__");
        let probe: Vec<u8> = Vec::with_capacity(4096);
        std::hint::black_box(&probe);
    }
    let seen = gunnar_stage::thread_report()
        .get("__alloc_selftest__")
        .map(|t| t.excl_alloc_bytes >= 4096)
        .unwrap_or(false);
    gunnar_stage::disable();
    gunnar_stage::reset();
    seen
}

// ── the reports ─────────────────────────────────────────────────────────────

/// One stage's share of one request.
#[derive(Debug, Clone)]
pub struct StageRow {
    /// The stage's name.
    pub name: &'static str,
    /// Its share of the request's exclusive wall time, 0.0–1.0.
    pub wall_frac: f64,
    /// Its share of the request's exclusive CPU (user + sys), 0.0–1.0.
    pub cpu_frac: f64,
    /// Its share of the request's peak growth, 0.0–1.0.
    pub peak_frac: f64,
    /// The tally the fractions come from, so an absolute figure is one field
    /// away and never has to be reconstructed from a percentage.
    pub tally: Tally,
    /// `ObjectStore::read` calls issued while this stage was innermost.
    pub store_reads: u64,
    /// Inflated bytes those reads returned.
    pub store_read_bytes: u64,
}

/// Which arm of `select_objects` actually answered this request.
///
/// # Why a decomposition is worthless without it
///
/// `select_objects` consults `graph.store().bitmap()` **first** and returns
/// before the walk when it answers (`gunnar-wire/src/graph.rs:1459`). The two
/// paths select the **same object set**, so no correctness test can tell them
/// apart, and [`wire_digest`] — this instrument's own fidelity check — agreed
/// across nineteen repositories while every measurement was silently taken on
/// the walk, because `StageCounting` forwarded every `ObjectStore` method
/// except `bitmap()`, whose trait default is `None`, and so turned the bitmap
/// OFF for everything measured through it.
///
/// (The *bytes* are not identical, which nobody had measured either: the two
/// paths carry different `Selection::client_has` shapes — an id set against a
/// bitmap `Vouch` — so the thin pack can name a different set of delta bases.
/// MEASURED the desktop 2026-08-04 over six corpus repositories at 200 commits behind,
/// the bitmap path emits **0.863x–0.999x** the wire bytes of the walk for an
/// identical object count. It is the object set that is invariant, not the
/// encoding.)
///
/// The lesson generalises past this bug and is the reason this enum exists
/// rather than a comment: **an instrument's fidelity check must also assert
/// that the path it thinks it measured is the path that ran.** Comparing
/// outputs is blind to a switch that does not change the output. So every run
/// now says which arm answered, [`StageRun::expect_path`] refuses a run that
/// took the other one, and the arms declare what they expect.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelectPath {
    /// The `.bitmap` answered. The walk stages are absent from the table
    /// entirely — the bitmap does not shrink the walk, it replaces it.
    Bitmap,
    /// The commit and tree walk ran.
    Walk,
    /// Neither, which means `select_objects` was never reached — a fetch that
    /// sent no pack, or a table taken from the wrong window.
    Neither,
}

impl std::fmt::Display for SelectPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            SelectPath::Bitmap => "bitmap",
            SelectPath::Walk => "walk",
            SelectPath::Neither => "neither",
        })
    }
}

/// A whole request, decomposed.
#[derive(Debug, Clone)]
pub struct StageRun {
    /// Every stage, largest exclusive wall share first.
    pub rows: Vec<StageRow>,
    /// Total exclusive wall, which is the request.
    pub wall_ns: u64,
    /// Total exclusive CPU.
    pub cpu_ns: u64,
    /// Total exclusive user CPU.
    pub user_ns: u64,
    /// Total exclusive system CPU.
    pub sys_ns: u64,
    /// What the **whole process** burned across the same window, every thread
    /// included. See [`process_cpu_ns`].
    pub process_cpu_ns: u64,
    /// Bytes the server put on the wire.
    pub wire_bytes: u64,
    /// Objects it said it sent.
    pub objects: u64,
    /// What was measurable.
    pub instruments: Instruments,
    /// Which arm of `select_objects` answered. See [`SelectPath`].
    pub path: SelectPath,
    /// Whether a `.bitmap` file was on disk beside the pack when this ran.
    ///
    /// Read from the filesystem, not inferred from the table, so the two can be
    /// checked **against each other**: a repository that has one and did not
    /// use it is the defect this field exists to make impossible to miss again.
    pub bitmap_on_disk: bool,
}

impl StageRun {
    /// Fail unless the request took the arm the caller expected.
    ///
    /// The assertion that was missing. An arm that means to measure production
    /// asks for [`SelectPath::Bitmap`]; one that means to measure the walk
    /// forces `GUNNAR_BITMAP=off` and asks for [`SelectPath::Walk`]. Either one
    /// getting the other is a silent change of subject, and the fractions it
    /// produces are perfectly plausible.
    pub fn expect_path(&self, want: SelectPath) -> Result<()> {
        ensure!(
            self.path == want,
            "this run took the {} arm of `select_objects` and the caller was measuring the {} \
             one. A `.bitmap` was {} on disk. The two arms select the SAME OBJECT SET, so \
             nothing downstream fails and no correctness test notices: the table would simply \
             have described a server nobody runs",
            self.path,
            want,
            if self.bitmap_on_disk {
                "present"
            } else {
                "absent"
            }
        );
        Ok(())
    }

    /// How much of the process's CPU the stage table actually accounts for.
    ///
    /// Near 1.0 means the decomposition is the request. Well below 1.0 means
    /// work happened on threads no stage ever entered — `gix-pack`'s parallel
    /// index resolution is the known case — and every fraction in the table is
    /// then a fraction of the attributed part only, which the arms say out
    /// loud rather than leaving to be discovered.
    ///
    /// Can exceed 1.0 slightly: the two clocks are read at different instants
    /// and `RUSAGE_SELF` has microsecond granularity.
    pub fn attributed_frac(&self) -> f64 {
        if self.process_cpu_ns == 0 {
            return 0.0;
        }
        self.cpu_ns as f64 / self.process_cpu_ns as f64
    }

    /// The share of one named stage, or 0.0 if it did not run.
    pub fn wall_frac(&self, name: &str) -> f64 {
        self.rows
            .iter()
            .find(|r| r.name == name)
            .map_or(0.0, |r| r.wall_frac)
    }

    /// The store reads one named stage issued, or 0 — the unit a probe that
    /// moves *reads* between stages has to assert in, because the price of a
    /// read is the engine's and changes under the table (see the RED probe in
    /// `gunnar.stage_fetch`).
    pub fn store_reads(&self, name: &str) -> u64 {
        self.rows
            .iter()
            .find(|r| r.name == name)
            .map_or(0, |r| r.store_reads)
    }

    /// The CPU share of one named stage, or 0.0.
    pub fn cpu_frac(&self, name: &str) -> f64 {
        self.rows
            .iter()
            .find(|r| r.name == name)
            .map_or(0.0, |r| r.cpu_frac)
    }

    /// The stage with the largest exclusive CPU share.
    pub fn cpu_owner(&self) -> &'static str {
        self.rows
            .iter()
            .max_by(|a, b| a.cpu_frac.total_cmp(&b.cpu_frac))
            .map_or("none", |r| r.name)
    }

    /// The stage that grew the thread's live-bytes high-water mark most.
    pub fn memory_owner(&self) -> &'static str {
        self.rows
            .iter()
            .max_by(|a, b| a.peak_frac.total_cmp(&b.peak_frac))
            .map_or("none", |r| r.name)
    }
}

fn shape(
    report: &Report,
    reads: &BTreeMap<String, (u64, u64)>,
    wire_bytes: u64,
    objects: u64,
    process_cpu_ns: u64,
    instruments: Instruments,
    bitmap_on_disk: bool,
) -> StageRun {
    // Derived from what RAN, not from what was configured. `select.bitmap` is
    // entered whenever bitmaps are enabled — including when the bitmap then
    // DECLINES the request shape and falls through — so its presence proves
    // nothing. The discriminator is whether the walk happened.
    let ran = |name: &str| report.contains_key(name);
    let path = if ran("select.tree_closure") || ran("select.commit_walk") {
        SelectPath::Walk
    } else if ran("select.bitmap") {
        SelectPath::Bitmap
    } else {
        SelectPath::Neither
    };
    let wall_ns = gunnar_stage::total_excl_wall_ns(report);
    let cpu_ns = gunnar_stage::total_excl_cpu_ns(report);
    let user_ns: u64 = report.values().map(|t| t.excl_user_ns).sum();
    let sys_ns: u64 = report.values().map(|t| t.excl_sys_ns).sum();
    let peak_total: u64 = report.values().map(|t| t.peak_growth_bytes).sum();
    let over = |n: u64, d: u64| if d == 0 { 0.0 } else { n as f64 / d as f64 };

    let mut rows: Vec<StageRow> = report
        .iter()
        .map(|(name, tally)| {
            let (store_reads, store_read_bytes) = reads.get(*name).copied().unwrap_or((0, 0));
            StageRow {
                name,
                wall_frac: over(tally.excl_wall_ns, wall_ns),
                cpu_frac: over(tally.excl_user_ns + tally.excl_sys_ns, cpu_ns),
                peak_frac: over(tally.peak_growth_bytes, peak_total),
                tally: *tally,
                store_reads,
                store_read_bytes,
            }
        })
        .collect();
    rows.sort_by(|a, b| b.wall_frac.total_cmp(&a.wall_frac));
    StageRun {
        rows,
        wall_ns,
        cpu_ns,
        user_ns,
        sys_ns,
        process_cpu_ns,
        wire_bytes,
        objects,
        instruments,
        path,
        bitmap_on_disk,
    }
}

/// Whether a `.bitmap` sits beside a pack in this repository.
pub fn has_bitmap(repo: &std::path::Path) -> bool {
    std::fs::read_dir(repo.join("objects").join("pack"))
        .map(|dir| {
            dir.flatten()
                .any(|e| e.path().extension().is_some_and(|x| x == "bitmap"))
        })
        .unwrap_or(false)
}

// ── the counting store ──────────────────────────────────────────────────────

/// A [`GitServe`] that charges every read to the stage that issued it.
///
/// This is the half a stage timer cannot supply and the half the znippy
/// question needs: "the fetch read 12 000 objects" and "the tree walk read
/// 11 400 of them and the pack writer re-read 600" are different findings with
/// different fixes, and only the second one points anywhere.
///
/// Ported off `gunnar_store::ObjectStore` on 2026-08-10 with the rest of the
/// tree. Only [`GitServe::read`] is charged, as before — this type answers
/// *"which stage inflated how much"*, and an existence probe inflates nothing.
struct StageCounting<'a> {
    inner: &'a dyn GitServe,
    reads: std::sync::Mutex<BTreeMap<String, (u64, u64)>>,
}

impl<'a> StageCounting<'a> {
    fn new(inner: &'a dyn GitServe) -> Self {
        StageCounting {
            inner,
            reads: std::sync::Mutex::new(BTreeMap::new()),
        }
    }
    fn note(&self, bytes: u64) {
        let stage = gunnar_stage::current().unwrap_or("__outside__");
        let mut map = self.reads.lock().expect("the stage read map");
        let entry = map.entry(stage.to_owned()).or_insert((0, 0));
        entry.0 += 1;
        entry.1 += bytes;
    }
    fn into_reads(self) -> BTreeMap<String, (u64, u64)> {
        self.reads.into_inner().expect("the stage read map")
    }
}

/// The twelve, forwarded and uncounted: this type attributes *inflation* to a
/// stage, and none of the twelve inflates anything.
impl GitOps for StageCounting<'_> {
    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 has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
        self.inner.has(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 GitServe for StageCounting<'_> {
    fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
        let out = self.inner.read(oid)?;
        if let Some((_, bytes)) = out.as_ref() {
            self.note(bytes.len() as u64);
        }
        Ok(out)
    }
    fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
        self.inner.header(oid)
    }
    fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
        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)
    }
}

// ── the drivers ─────────────────────────────────────────────────────────────

/// A pkt-line carrying `payload`.
pub fn pkt(payload: &str) -> Vec<u8> {
    let mut out = format!("{:04x}", payload.len() + 4).into_bytes();
    out.extend_from_slice(payload.as_bytes());
    out
}

/// A protocol-v2 `command=fetch`, in the shape stock git sends.
///
/// `haves` empty is a **clone**: nothing to negotiate away, every base is in
/// the transfer. A non-empty `haves` with `done` is the final round of an
/// incremental fetch, which is where the 2.2–2.6x lives.
pub fn fetch_request(want: &ObjectId, haves: &[ObjectId], format: &str) -> Vec<u8> {
    let mut req = 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}\n")));
    for have in haves {
        req.extend(pkt(&format!("have {have}\n")));
    }
    req.extend(pkt("done\n"));
    req.extend_from_slice(b"0000");
    req
}

/// Serve one fetch and hash what went on the wire: `(digest, bytes, objects)`.
///
/// **The correctness bar.** Instrumentation that changes what it measures is
/// worthless, and "the fetch still succeeded" is true of a server that emitted
/// different bytes. This takes the response verbatim — advertisement,
/// acknowledgments, sidebands, packfile and all — so a probe that perturbed the
/// pack writer, the selection order or the flush pattern shows up as a
/// different digest rather than as a benchmark that still passes.
///
/// It deliberately does **not** touch [`gunnar_stage::enable`]: the caller sets
/// the recording state, which is what lets one binary compare recording against
/// not-recording, and two binaries compare compiled-in against compiled-out.
///
/// FNV-1a 64 rather than a cryptographic digest, with the byte count and the
/// object count beside it. Nothing adversarial is being defended against; what
/// is being detected is an implementation that changed, and a length plus a
/// 64-bit hash detects that.
pub fn wire_digest(
    repo: &std::path::Path,
    want: &ObjectId,
    haves: &[ObjectId],
) -> Result<(String, u64, u64)> {
    let kind = stored_hash_kind(repo)?;
    let store = crate::engine::open_repo(repo, kind)?;
    wire_digest_on(store.as_ref(), kind, want, haves)
}

/// **Many requests, one open store, one digest each — and the reason it exists
/// is that [`wire_digest`] over a single request is BLIND.**
///
/// A fetch touches only the objects its own want reaches. A server whose
/// reachability index, `have` closure or voucher is wrong for *every other*
/// commit in the repository still answers that one request byte-for-byte
/// correctly, so a fidelity check built on one `(want, have)` pair passes a
/// store that cannot serve anything else. Learned the hard way on this exact
/// code (`select`/`client_bases`), which is why the corpus is a *set* of
/// distinct wants and this function does not take a single one.
///
/// It is deliberately the **warm** shape — [`wire_digest`]'s per-call open would
/// re-derive the index for every pair and hide an answer that depends on which
/// request came first, which is a class of bug this is meant to catch rather
/// than to avoid.
///
/// # Both directions, and they are not symmetric
///
/// The `have` set feeds `union - had`, so a corrupt one enters sign-flipped:
/// **dropping** an entry OVER-sends and **adding** one UNDER-sends. An
/// under-send is silent data loss in a clone; an over-send is bandwidth no
/// connectivity check will ever complain about. The digest catches both because
/// it is over the bytes; the object count returned beside it says *which*.
pub fn wire_digest_set(
    repo: &std::path::Path,
    requests: &[(ObjectId, Vec<ObjectId>)],
) -> Result<Vec<(String, u64, u64)>> {
    ensure!(
        !requests.is_empty(),
        "a digest over an empty request set is a digest of nothing, and it would compare equal \
         to any other digest of nothing"
    );
    let kind = stored_hash_kind(repo)?;
    let store = crate::engine::open_repo(repo, kind)?;
    requests
        .iter()
        .map(|(want, haves)| {
            wire_digest_on(store.as_ref(), kind, want, haves)
                .with_context(|| format!("serving want {want} with {} have(s)", haves.len()))
        })
        .collect()
}

/// The same, served out of a store the caller **already holds open**.
///
/// The production shape, and a different measurement: a server opens a
/// repository once and answers requests out of it for weeks, so everything
/// `engine::open_repo` and `BitmapCache` do at open — mapping the `.idx`,
/// parsing the `.bitmap`, building the pack-order permutation and any derived
/// index beside it — is paid once and not per request. [`wire_digest`] pays all
/// of it every call, which is the right shape for a cold-start question and the
/// wrong one for a throughput question.
///
/// Both spellings run the same state machine over the same store type; only who
/// owns the open differs; there is one implementation, not two.
pub fn wire_digest_on(
    store: &dyn gunnar_store::RepoStore,
    kind: gix_hash::Kind,
    want: &ObjectId,
    haves: &[ObjectId],
) -> Result<(String, u64, u64)> {
    let out = serve_on(store, kind, want, haves)?;
    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
    for byte in &out.0 {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    Ok((format!("{hash:016x}"), out.0.len() as u64, out.1))
}

/// Serve one fetch out of an already-open store and hand back the raw
/// response: `(bytes, objects)`.
///
/// The half of [`wire_digest_on`] that is the server, split out so a profiling
/// loop can run the serve path **without** the FNV pass over the response.
/// That pass is byte-at-a-time over the whole packfile — on a 6.59 MB response
/// it is ~8% of the profile, all of it the harness, and it lands in the
/// denominator of every share the profile reports. `wire_digest_on` is defined
/// in terms of this rather than beside it, so the digest the fidelity check
/// compares is a digest of exactly the bytes the profiled loop produced, out
/// of one implementation rather than two.
pub fn serve_on(
    store: &dyn gunnar_store::RepoStore,
    kind: gix_hash::Kind,
    want: &ObjectId,
    haves: &[ObjectId],
) -> Result<(Vec<u8>, u64)> {
    // One handle. `objects()`/`refs()` were the `ObjectStore`/`RefStore` split;
    // `RepoStore::git()` returns the single `Arc<dyn GitServe>` that owns both.
    let git = store.git();
    let format = match kind {
        gix_hash::Kind::Sha1 => "sha1",
        _ => "sha256",
    };
    let upload = UploadPack::new(git.as_ref(), kind);
    let mut input =
        std::io::BufReader::new(std::io::Cursor::new(fetch_request(want, haves, format)));
    let mut out: Vec<u8> = Vec::new();
    let summary = upload.serve_v2(&mut input, &mut out)?;
    ensure!(
        summary.objects_sent > 0,
        "the served response carried ZERO objects; two empty responses are equal"
    );
    Ok((out, u64::from(summary.objects_sent)))
}

/// Serve **one** fetch out of `repo` and attribute it.
///
/// The store, the state machine and the pack writer are the real ones; only the
/// socket is a `Cursor`, which is what makes this a decomposition of the serve
/// path rather than of a benchmark's imitation of it. What it therefore does
/// **not** include is `accept(2)`, the SSH handshake and the kernel's share of
/// moving bytes down a socket — see `gunnar.stage_fetch`'s doc comment for what
/// that leaves out and how big it is.
pub fn fetch(repo: &std::path::Path, want: &ObjectId, haves: &[ObjectId]) -> Result<StageRun> {
    let instruments = install();
    let kind =
        stored_hash_kind(repo).with_context(|| format!("reading the hash kind of {repo:?}"))?;
    let store =
        crate::engine::open_repo(repo, kind).with_context(|| format!("opening {repo:?}"))?;
    let git = store.git();
    let counting = StageCounting::new(git.as_ref());

    let format = match kind {
        gix_hash::Kind::Sha1 => "sha1",
        _ => "sha256",
    };
    let request = fetch_request(want, haves, format);

    let upload = UploadPack::new(&counting, kind);
    let mut input = std::io::BufReader::new(std::io::Cursor::new(request));
    let mut out: Vec<u8> = Vec::new();

    gunnar_stage::reset();
    gunnar_stage::enable();
    let cpu0 = process_cpu_ns();
    let summary = upload.serve_v2(&mut input, &mut out)?;
    let cpu1 = process_cpu_ns();
    gunnar_stage::disable();
    let report = gunnar_stage::thread_report();

    ensure!(
        summary.objects_sent > 0,
        "the decomposed fetch sent ZERO objects, so every fraction below is a share of nothing"
    );
    ensure!(
        !report.is_empty(),
        "the serve path recorded NO stages. The `stages` feature is off, or `gunnar-wire` was \
         resolved without it — either way the table would be empty rather than wrong, which is \
         the one failure mode that looks like a clean result"
    );
    Ok(shape(
        &report,
        &counting.into_reads(),
        out.len() as u64,
        u64::from(summary.objects_sent),
        cpu1.saturating_sub(cpu0),
        instruments,
        has_bitmap(repo),
    ))
}

/// A protocol-v2 `command=ls-refs`, in the shape `git ls-remote` sends.
///
/// `peel` and `symrefs` are what stock git asks for, and they matter to a
/// census: `peel` is what makes the server read tag objects out of the store,
/// so an `ls-refs` request without it would report the one command that touches
/// the object database as touching none of it.
pub fn ls_refs_request(format: &str) -> Vec<u8> {
    let mut req = Vec::new();
    req.extend(pkt("command=ls-refs\n"));
    req.extend(pkt(&format!("object-format={format}\n")));
    req.extend_from_slice(b"0001");
    req.extend(pkt("peel\n"));
    req.extend(pkt("symrefs\n"));
    req.extend(pkt("unborn\n"));
    req.extend_from_slice(b"0000");
    req
}

/// Serve **one** `ls-remote` out of `repo` and attribute it.
///
/// The fourth server command, and the one no decomposition had covered. It
/// serves no objects at all, so [`StageRun::objects`] is 0 here **by
/// construction** — which is why this driver does not carry `fetch`'s
/// `objects_sent > 0` guard and instead asserts that refs were advertised. A
/// census that dropped `ls-remote` for having no object count would have
/// dropped the one command whose whole cost is the ref store.
pub fn ls_remote(repo: &std::path::Path) -> Result<StageRun> {
    let instruments = install();
    let kind =
        stored_hash_kind(repo).with_context(|| format!("reading the hash kind of {repo:?}"))?;
    let store =
        crate::engine::open_repo(repo, kind).with_context(|| format!("opening {repo:?}"))?;
    let git = store.git();
    let counting = StageCounting::new(git.as_ref());
    let format = match kind {
        gix_hash::Kind::Sha1 => "sha1",
        _ => "sha256",
    };

    let upload = UploadPack::new(&counting, kind);
    let mut input = std::io::BufReader::new(std::io::Cursor::new(ls_refs_request(format)));
    let mut out: Vec<u8> = Vec::new();

    gunnar_stage::reset();
    gunnar_stage::enable();
    let cpu0 = process_cpu_ns();
    let summary = upload.serve_v2(&mut input, &mut out)?;
    let cpu1 = process_cpu_ns();
    gunnar_stage::disable();
    let report = gunnar_stage::thread_report();

    ensure!(
        summary.ls_refs > 0,
        "the decomposed ls-remote served NO ls-refs command at all"
    );
    ensure!(
        out.len() > 4,
        "the ls-remote advertised {} bytes, which is not a ref list",
        out.len()
    );
    ensure!(
        !report.is_empty(),
        "the serve path recorded NO stages; see `fetch`"
    );
    Ok(shape(
        &report,
        &counting.into_reads(),
        out.len() as u64,
        0,
        cpu1.saturating_sub(cpu0),
        instruments,
        has_bitmap(repo),
    ))
}

/// The same fetch served by `width` threads at once, with each worker's stages
/// kept apart and then summed.
///
/// Returns the per-request decomposition — every column divided by `width` —
/// so the 1-client and 32-client tables are directly comparable and a stage
/// whose *share* grows is visible as such. That is the localisation
/// `gunnar.concurrency` could not do: it measured 0.418 s of CPU per request at
/// 1 client and 1.063 s at 32 and had nowhere to put the difference.
pub fn concurrent_fetch(
    repo: &std::path::Path,
    want: &ObjectId,
    haves: &[ObjectId],
    width: usize,
) -> Result<StageRun> {
    ensure!(width >= 1, "a concurrency width of {width} is not a run");
    let instruments = install();
    gunnar_stage::reset_global();

    let mut wire = 0u64;
    let mut objects = 0u64;
    let reads: std::sync::Mutex<BTreeMap<String, (u64, u64)>> =
        std::sync::Mutex::new(BTreeMap::new());
    let start = std::sync::Barrier::new(width);
    // BEFORE the workers, not after. Written the other way round first, and
    // every worker then ran with recording off and flushed an empty table — the
    // non-vacuity guard below is what caught it, which is the whole reason it
    // is an `ensure!` and not a comment.
    gunnar_stage::enable();

    // **One store, shared**, exactly as the server has one. Opened per worker
    // instead, `width = 2` DEADLOCKED: opening the engine repository twice opens the
    // `.gunnar` Arrow log, and a second handle onto the same directory in the
    // same process waits for the first one's lock forever. That is worth
    // recording rather than working around silently, because it is also the
    // faithful shape: sharing the odb, the pack cache and the ref store across
    // connections is what the daemon does, and a per-connection store would
    // have measured a cold cache 32 times and called it concurrency.
    let kind = stored_hash_kind(repo)?;
    let store = crate::engine::open_repo(repo, kind)?;
    let shared_git = store.git();
    let format = match kind {
        gix_hash::Kind::Sha1 => "sha1",
        _ => "sha256",
    };

    let cpu0 = process_cpu_ns();
    std::thread::scope(|scope| -> Result<()> {
        let mut handles = Vec::with_capacity(width);
        for _ in 0..width {
            let start = &start;
            let reads = &reads;
            let git = shared_git.as_ref();
            handles.push(scope.spawn(move || -> Result<(u64, u64)> {
                let counting = StageCounting::new(git);
                let request = fetch_request(want, haves, format);
                let upload = UploadPack::new(&counting, kind);
                let mut input = std::io::BufReader::new(std::io::Cursor::new(request));
                let mut out: Vec<u8> = Vec::new();

                gunnar_stage::reset();
                // Every worker opened its own store and built its own request
                // before this line. The barrier is what makes the measured
                // window genuinely concurrent rather than a staggered ramp, and
                // a staggered ramp is exactly the shape that hides a
                // contention effect.
                start.wait();
                let summary = upload.serve_v2(&mut input, &mut out)?;
                gunnar_stage::flush_thread();

                let mut map = reads.lock().expect("the stage read map");
                for (stage, (n, b)) in counting.into_reads() {
                    let e = map.entry(stage).or_insert((0, 0));
                    e.0 += n;
                    e.1 += b;
                }
                Ok((out.len() as u64, u64::from(summary.objects_sent)))
            }));
        }
        for handle in handles {
            let (bytes, sent) = handle.join().expect("a fetch worker panicked")?;
            wire += bytes;
            objects += sent;
        }
        Ok(())
    })?;
    let cpu1 = process_cpu_ns();

    gunnar_stage::disable();
    let mut report = gunnar_stage::global_report();
    ensure!(
        !report.is_empty(),
        "no worker recorded a stage; see `fetch`"
    );
    ensure!(
        objects > 0,
        "the concurrent workers sent ZERO objects between them"
    );
    // Per request, so the table is the same shape at every width.
    let divide = |v: u64| v / width as u64;
    for tally in report.values_mut() {
        tally.excl_wall_ns = divide(tally.excl_wall_ns);
        tally.incl_wall_ns = divide(tally.incl_wall_ns);
        tally.excl_user_ns = divide(tally.excl_user_ns);
        tally.excl_sys_ns = divide(tally.excl_sys_ns);
        tally.excl_alloc_count = divide(tally.excl_alloc_count);
        tally.excl_alloc_bytes = divide(tally.excl_alloc_bytes);
        tally.peak_growth_bytes = divide(tally.peak_growth_bytes);
        tally.rss_growth_bytes = divide(tally.rss_growth_bytes);
    }
    let mut per_request = reads.into_inner().expect("the stage read map");
    for value in per_request.values_mut() {
        value.0 /= width as u64;
        value.1 /= width as u64;
    }
    Ok(shape(
        &report,
        &per_request,
        wire / width as u64,
        objects / width as u64,
        cpu1.saturating_sub(cpu0) / width as u64,
        Instruments {
            // Process-wide, and therefore not attributable at width > 1. Said
            // out loud rather than published as a number that reads like one.
            rss: instruments.rss && width == 1,
            ..instruments
        },
        has_bitmap(repo),
    ))
}

/// The packfile out of a v2 `packfile` section, demultiplexed off the sidebands.
pub fn demux_packfile(response: &[u8]) -> Result<Vec<u8>> {
    let mut pack = Vec::new();
    let mut cursor = response;
    let mut in_packfile = false;
    while cursor.len() >= 4 {
        let len = usize::from_str_radix(std::str::from_utf8(&cursor[..4])?, 16)?;
        if len < 4 {
            cursor = &cursor[4..];
            continue;
        }
        ensure!(
            len <= cursor.len(),
            "a pkt-line ran past the end of the response"
        );
        let payload = &cursor[4..len];
        if in_packfile {
            ensure!(!payload.is_empty(), "an empty band in the packfile section");
            match payload[0] {
                1 => pack.extend_from_slice(&payload[1..]),
                3 => anyhow::bail!("band 3: {}", String::from_utf8_lossy(&payload[1..])),
                _ => {}
            }
        } else if payload == b"packfile\n" {
            in_packfile = true;
        }
        cursor = &cursor[len..];
    }
    ensure!(
        pack.starts_with(b"PACK"),
        "the demultiplexed bytes are not a packfile"
    );
    Ok(pack)
}

/// Serve **one** push into `into` and attribute it.
///
/// The packfile is the one a real `upload-pack` produced for the same object
/// set, taken off `source` — which is what a push carries, and is a corpus with
/// somebody else's delta chains in it rather than one this implementation just
/// computed for itself.
pub fn push(
    source: &std::path::Path,
    into: &std::path::Path,
    tip: &ObjectId,
    ref_name: &str,
) -> Result<StageRun> {
    use gunnar_wire::receive_pack::{AllowAll, PackIngest, PackReceipt, ReceivePack};

    let instruments = install();
    let kind = stored_hash_kind(source)?;

    // 1. What the client would have sent, produced by the real pack writer.
    let pack = {
        let store = crate::engine::open_repo(source, kind)?;
        let git = store.git();
        let format = match kind {
            gix_hash::Kind::Sha1 => "sha1",
            _ => "sha256",
        };
        let upload = UploadPack::new(git.as_ref(), kind);
        let mut input =
            std::io::BufReader::new(std::io::Cursor::new(fetch_request(tip, &[], format)));
        let mut out: Vec<u8> = Vec::new();
        upload.serve_v2(&mut input, &mut out)?;
        demux_packfile(&out)?
    };
    ensure!(
        pack.len() > 1024,
        "the pack to be pushed is {} bytes; a push of nothing decomposes into nothing",
        pack.len()
    );

    // 2. The receiving side.
    std::fs::create_dir_all(into)?;
    let target = crate::engine::open_repo(into, kind)?;
    let target_git = target.git();
    // No `objects/pack` is created. The engine stores the pushed pack itself;
    // an empty packfile directory is what a later reader uses to decide a
    // repository is packfile-shaped, and this server never writes one.

    /// The push, through the engine — the same `GitOps::put_pack` a real
    /// `receive-pack` lands in, so the stage measures the shipping path rather
    /// than a fixture that resembles it.
    ///
    /// **`Arc<dyn GitOps>`, and not a concrete engine type.** This field held
    /// `Arc<gunnar_znippy_engine::GitStore>` until 2026-08-10, and that one line is
    /// most of why building *any* of the 49 arms — `pack_deflate` included —
    /// required a sibling znippy checkout. The contract is the seam; the engine
    /// arrives through [`crate::engine`].
    ///
    /// ⚠️ **A CONTRACT GAP, recorded rather than papered over.** The deleted
    /// `GitStorePackIngest::ingest` handed back `(pack_id, num_objects,
    /// version)`. `GitOps::put_pack` hands back a `TxId`, whose three fields are
    /// `pack_id: Option<u64>` (a dense *internal* id, not the pack trailer),
    /// `extent` and `push_seq` — **none of the three things `PackReceipt`
    /// needs**. So they are read back off the pack bytes below.
    ///
    /// That is honest as far as it goes: object count and trailer are facts
    /// about the bytes the client sent, not about what the store decided, and
    /// reading them from the wire is the same thing `index_pack` does. What it
    /// cannot do is report the store's own view — so if `receive-pack` ever
    /// needs to answer "which pack, as the engine numbers them", that is a
    /// `GitOps` question and belongs on the contract, not here.
    struct Ingest {
        store: std::sync::Arc<dyn gunnar_store::git_storage_trait::GitOps>,
        hash_kind: gix_hash::Kind,
    }
    impl PackIngest for Ingest {
        fn ingest(&self, pack: &mut dyn std::io::BufRead) -> gunnar_wire::Result<PackReceipt> {
            // `put_pack` takes `&[u8]`: the whole pack, in memory. That is the
            // contract's shape and not a choice made here — see the note above.
            let mut bytes = Vec::new();
            pack.read_to_end(&mut bytes).map_err(|e| {
                gunnar_wire::Error::Store(gunnar_store::Error::backend(format!(
                    "reading the pushed pack: {e}"
                )))
            })?;

            let hash_len = self.hash_kind.len_in_bytes();
            // 12-byte header (`PACK`, version, count) and a trailer of one hash.
            // A pack shorter than both is not a short pack, it is not a pack.
            if bytes.len() < 12 + hash_len || &bytes[..4] != b"PACK" {
                return Err(gunnar_wire::Error::Store(gunnar_store::Error::backend(
                    "the pushed bytes are not a packfile",
                )));
            }
            let pack_version = gunnar_wire::PackVersion::from_u32(u32::from_be_bytes([
                bytes[4], bytes[5], bytes[6], bytes[7],
            ]));
            // `u32`, not `usize`: `gunnar_wire::IndexedPack::num_objects` is a
            // `u32`, which is the width the pack header itself carries.
            let num_objects = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
            let pack_id =
                gix_hash::ObjectId::try_from(&bytes[bytes.len() - hash_len..]).map_err(|_| {
                    gunnar_wire::Error::Store(gunnar_store::Error::backend(
                        "the pushed pack's trailer is not an object id",
                    ))
                })?;

            self.store.put_pack(&bytes).map_err(|e| {
                gunnar_wire::Error::Store(gunnar_store::Error::backend(format!(
                    "the engine refused the pushed pack: {e:#}"
                )))
            })?;

            Ok(PackReceipt::without_object_set(gunnar_wire::IndexedPack {
                num_objects,
                pack_id,
                // No `.idx` exists: the objects table replaced it.
                index_id: gix_hash::ObjectId::null(self.hash_kind),
                pack_version,
                hash_kind: self.hash_kind,
                index_path: None,
                data_path: None,
                keep_path: None,
                // The engine was handed these bytes, so the count is exact and
                // needs no file to stat — which is the point, since this arm
                // writes none.
                stored_bytes: Some(bytes.len() as u64),
            }))
        }
    }

    let null = kind.null();
    let mut request = Vec::new();
    let first = format!(
        "{null} {tip} {ref_name}\0report-status side-band-64k object-format={}\n",
        match kind {
            gix_hash::Kind::Sha1 => "sha1",
            _ => "sha256",
        }
    );
    request.extend(pkt(&first));
    request.extend_from_slice(b"0000");
    request.extend_from_slice(&pack);

    let ingest = Ingest {
        // The store the receiving side already opened, shared rather than
        // reopened: a second handle onto the same directory in the same process
        // waits on the first one's lock forever (see the `width = 2` deadlock
        // note above). `RepoStore::git()` hands out the `Arc` for exactly this.
        store: target_git.clone(),
        hash_kind: kind,
    };
    let receive = ReceivePack::new(target_git.as_ref(), kind);
    let mut input = std::io::BufReader::new(std::io::Cursor::new(request));
    let mut out: Vec<u8> = Vec::new();

    gunnar_stage::reset();
    gunnar_stage::enable();
    let cpu0 = process_cpu_ns();
    let outcome = receive.serve(&mut input, &mut out, &ingest, &AllowAll)?;
    let cpu1 = process_cpu_ns();
    gunnar_stage::disable();
    let report = gunnar_stage::thread_report();

    let outcome = outcome.context("the push produced no outcome at all")?;
    ensure!(
        outcome.all_ok(),
        "the decomposed push was refused: {:?}",
        outcome.statuses
    );
    ensure!(
        !report.is_empty(),
        "the push recorded NO stages; see `fetch`"
    );
    let objects_in = outcome
        .pack
        .as_ref()
        .map_or(0, |p| u64::from(p.num_objects));
    ensure!(
        objects_in > 0,
        "the push indexed ZERO objects, so every fraction is a share of nothing"
    );
    Ok(shape(
        &report,
        &BTreeMap::new(),
        pack.len() as u64,
        objects_in,
        cpu1.saturating_sub(cpu0),
        instruments,
        has_bitmap(into),
    ))
}

/// The repository's hash algorithm, read from its own config.
///
/// `gunnar_store::fs::stored_hash_kind` read it off the packfile engine's
/// on-disk layout and went with that engine. A repository served under the wrong
/// hash is not a degraded repository, it is a different one, so this asks the
/// repository rather than defaulting.
fn stored_hash_kind(repo: &std::path::Path) -> anyhow::Result<gix_hash::Kind> {
    let config = std::fs::read_to_string(repo.join("config")).unwrap_or_default();
    Ok(if config.to_ascii_lowercase().contains("sha256") {
        gix_hash::Kind::Sha256
    } else {
        gix_hash::Kind::Sha1
    })
}