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

nordisk/edda

crates/storage-git-gix/parked-gix-2/bench-gix/fetchbytes-probe.rs

//! **Where a fetch's wire bytes actually go**, side by side with stock git.
//!
//! `gunnar.vs_git_fetch_divergent` reports one number per side — `wire_out` —
//! and gunnar loses it 1.2458×. That number is a sum of two things that have
//! nothing to do with each other:
//!
//!  * the **packfile**, which is `P-001`/`S-003`'s subject, and
//!  * everything else on the wire: the capability advertisement, `ls-refs`, the
//!    negotiation's `ACK`/`NAK` rounds, progress, and five bytes of pkt-line
//!    header per sideband chunk.
//!
//! Nothing in the tree had ever separated them, so "gunnar sends 1.2458× the
//! bytes" was being read as "gunnar's pack is 1.2458× the size" — a claim the
//! measurement does not make. This probe makes it, by scanning the
//! server→client stream as pkt-lines and attributing every byte to a band.
//!
//! # The second thing it exists for: **which selection path answered**
//!
//! `Exclusion::Full` is documented as worth 7 186 bytes on the divergent
//! fixture and as costing `O(client history)`. Both halves are statements about
//! **the walk**. `crates/gunnar-wire/src/bitmap.rs` says in as many words that
//! the bitmap path "ignores the knob and always gives the answer
//! `Exclusion::Full` gives — at `Exclusion::Edges`' cost", and `select_objects`
//! returns from the bitmap before `options.exclusion` is ever read.
//!
//! So the whole question is which path a given measurement was on, and a
//! `.bitmap` on disk is what decides it. This probe reports
//! `UploadSummary::answered_from_bitmap` per run rather than inferring it,
//! because the failure is silent in the direction that matters: a repository
//! with no bitmap serves every request from the walk and reports nothing
//! unusual while doing it.
//!
//! # Roles
//!
//! One binary, three roles, the same shape `nornir-bench.rs`'s head-to-head
//! arms use (`ext::` makes git run a helper; the helper spawns the real server
//! and pumps the pipe):
//!
//! * `FB_SERVE=git|gunnar` — the meter. Spawns the server, proxies, counts.
//! * `FB_INNER=1` — gunnar's `UploadPack` over stdin/stdout.
//! * neither — the driver.
//!
//! # Usage
//!
//! ```text
//! fetchbytes-probe fixture                # the h2h divergent fixture
//! fetchbytes-probe real 10 50 200         # the 19-repo corpus at three depths
//! ```
//!
//! Every arm is the **same binary** under different environment; the only
//! difference between `gunnar-bitmap-edges` and `gunnar-walk-full` is two
//! `getenv`s the server already reads per request.

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

use anyhow::{bail, ensure, Context, Result};
use gix_hash::Kind;
use gunnar_store::fs::FsStore;
use gunnar_store::ObjectStore;
use gunnar_wire::{ProtocolVersion, UploadPack};
use serde_json::{json, Value};

/// The `git --version` every row here is a statement about. Asserted, not
/// assumed: a pack-size ratio belongs to one specific git.
const GIT_VERSION: &str = "git version 2.53.0";

fn fixture_root() -> PathBuf {
    PathBuf::from(
        std::env::var("GUNNAR_BENCH_FIXTURES")
            .unwrap_or_else(|_| "/home/rickard/scratch/gunnar-bench-fixtures".to_string()),
    )
}

/// The scratch tree, **named for this process**.
///
/// The driver clears it at startup, and a fixed name means a second probe
/// started while the first is running deletes the first one's working
/// repositories out from under a live `git fetch`. That produced
/// `fatal: Unable to read current working directory` and
/// `fatal: fetch-pack: invalid index-pack output` — an error that reads exactly
/// like a server bug and is entirely the harness's. Seen 2026-08-04, on a
/// corpus run 30 cases deep.
fn work_root() -> PathBuf {
    fixture_root().join(format!("fetchbytes-work-{}", std::process::id()))
}

fn main() -> Result<()> {
    if let Some(side) = std::env::var_os("FB_SERVE") {
        return meter(&side.to_string_lossy());
    }
    if std::env::var_os("FB_INNER").is_some() {
        return inner();
    }
    driver()
}

// ───────────────────────────────────────────────────────────────────────────
// the meter
// ───────────────────────────────────────────────────────────────────────────

/// Attributes every server→client byte to *pack* or *not pack*.
///
/// # Why a pkt-line scan and not a subtraction
///
/// The obvious decomposition — total wire minus the pack on disk — is wrong in
/// both directions: the on-disk pack has been through `index-pack --fix-thin`
/// (it is *larger* than the wire pack, by every base that was completed) and
/// re-indexed. A byte on the wire is only a pack byte if it arrived inside a
/// band-1 pkt-line, and that is a fact about the stream, so it is read off the
/// stream.
///
/// # The rule, and why it is protocol-agnostic
///
/// A pkt-line's payload belongs to the pack iff its first byte is `0x01`. Every
/// non-pack line either side sends is ASCII text — `version 2`, `agent=…`,
/// `ACK <oid>`, `NAK`, `ready`, `packfile`, `wanted-refs`, `shallow-info` — so
/// no text line can be mistaken for band 1, and the rule holds for protocol v0
/// and v2 alike without needing to know which is being spoken. Bands 2
/// (progress) and 3 (error) are counted as overhead, which is what they are.
///
/// **Seen red** by pointing it at a non-sidebanded v0 exchange: `pack_wire`
/// comes out 0 and the driver's `ensure!` fires, rather than silently
/// attributing a whole packfile to overhead.
#[derive(Default)]
struct PktScan {
    carry: Vec<u8>,
    /// Payload bytes of band-1 lines, band byte excluded: the pack itself.
    pack_wire: u64,
    /// How many band-1 lines carried it. `pack_wire + 5 * pack_pkts` is what
    /// the pack cost on the wire including its framing.
    pack_pkts: u64,
    /// Bytes in every other pkt-line's payload, headers excluded.
    other_payload: u64,
    other_pkts: u64,
    /// Flush/delim/response-end packets, which are four bytes and no payload.
    marker_pkts: u64,
    /// Set if a length header was not valid hex, i.e. the stream stopped being
    /// pkt-lines. Everything after that point is unattributed and the driver
    /// treats the run as unusable rather than reporting a plausible number.
    desynced: bool,
    trailing: u64,
    /// `FB_DUMP=1`: print every non-pack pkt-line to stderr.
    dump: bool,
}

impl PktScan {
    fn feed(&mut self, chunk: &[u8]) {
        if self.desynced {
            self.trailing += chunk.len() as u64;
            return;
        }
        self.carry.extend_from_slice(chunk);
        let mut at = 0usize;
        loop {
            if self.carry.len() - at < 4 {
                break;
            }
            let head = &self.carry[at..at + 4];
            let Some(len) = hex4(head) else {
                self.desynced = true;
                self.trailing += (self.carry.len() - at) as u64;
                break;
            };
            if len < 4 {
                // 0 flush, 1 delim, 2 response-end.
                self.marker_pkts += 1;
                at += 4;
                continue;
            }
            if self.carry.len() - at < len {
                break;
            }
            let payload = &self.carry[at + 4..at + len];
            if payload.first() == Some(&1) {
                self.pack_wire += (payload.len() - 1) as u64;
                self.pack_pkts += 1;
            } else {
                self.other_payload += payload.len() as u64;
                self.other_pkts += 1;
                // Every non-pack line, verbatim, when asked for. A byte count
                // says the protocol is chattier; only the lines say WHICH lines,
                // and that is the difference between a finding and a guess.
                if self.dump {
                    let text = String::from_utf8_lossy(payload);
                    eprintln!("FB_PKT {}", text.trim_end());
                }
            }
            at += len;
        }
        self.carry.drain(..at);
    }
}

fn hex4(b: &[u8]) -> Option<usize> {
    let mut n = 0usize;
    for &c in b {
        let d = match c {
            b'0'..=b'9' => c - b'0',
            b'a'..=b'f' => c - b'a' + 10,
            b'A'..=b'F' => c - b'A' + 10,
            _ => return None,
        };
        n = n * 16 + d as usize;
    }
    Some(n)
}

fn meter(side: &str) -> Result<()> {
    let args: Vec<String> = std::env::args().collect();
    let service = args.get(1).cloned().unwrap_or_default();
    let repo = args.get(2).cloned().unwrap_or_default();
    ensure!(
        service == "git-upload-pack",
        "this probe meters fetches only, not {service:?}"
    );

    let server_stats: Option<PathBuf> = std::env::var_os("FB_STATS")
        .map(|s| PathBuf::from(s).with_extension(format!("srv.{}", std::process::id())));
    if let Some(p) = &server_stats {
        let _ = std::fs::remove_file(p);
    }

    let mut command = match side {
        "git" => {
            let mut c = Command::new("git");
            c.arg("upload-pack").arg(&repo);
            c
        }
        "gunnar" => {
            let mut c = Command::new(std::env::current_exe()?);
            c.env_remove("FB_SERVE");
            c.env("FB_INNER", "1");
            if let Some(p) = &server_stats {
                c.env("FB_SERVER_STATS", p);
            }
            c.arg(&service).arg(&repo);
            c
        }
        other => bail!("unknown side {other:?}"),
    };
    command
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit());

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

    let bytes_in = Arc::new(AtomicU64::new(0));
    let haves = Arc::new(AtomicU64::new(0));
    {
        let bytes_in = Arc::clone(&bytes_in);
        let haves = Arc::clone(&haves);
        std::thread::spawn(move || {
            let stdin = std::io::stdin();
            let mut stdin = stdin.lock();
            let mut buf = vec![0u8; 64 * 1024];
            let mut carry: Vec<u8> = Vec::new();
            loop {
                let n = match stdin.read(&mut buf) {
                    Ok(0) | Err(_) => break,
                    Ok(n) => n,
                };
                if to_server.write_all(&buf[..n]).is_err() || to_server.flush().is_err() {
                    break;
                }
                bytes_in.fetch_add(n as u64, Ordering::Relaxed);
                haves.fetch_add(
                    count_needle(b"have ", &buf[..n], &mut carry),
                    Ordering::Relaxed,
                );
            }
            drop(to_server);
        });
    }

    let mut scan = PktScan {
        dump: std::env::var_os("FB_DUMP").is_some(),
        ..Default::default()
    };
    let mut bytes_out = 0u64;
    {
        let stdout = std::io::stdout();
        let mut stdout = stdout.lock();
        let mut buf = vec![0u8; 64 * 1024];
        loop {
            let n = from_server.read(&mut buf)?;
            if n == 0 {
                break;
            }
            stdout.write_all(&buf[..n])?;
            stdout.flush()?;
            bytes_out += n as u64;
            scan.feed(&buf[..n]);
        }
    }

    let mut status: libc::c_int = 0;
    let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
    let reaped = unsafe { libc::wait4(pid, &mut status, 0, &mut usage) };
    ensure!(reaped == pid, "wait4 returned {reaped}, not {pid}");
    let server_wall_ms = started.elapsed().as_secs_f64() * 1000.0;
    let exit = if libc::WIFEXITED(status) {
        libc::WEXITSTATUS(status)
    } else {
        -1
    };
    let cpu_s = secs(usage.ru_utime) + secs(usage.ru_stime);

    let server: Value = server_stats
        .as_ref()
        .and_then(|p| std::fs::read_to_string(p).ok())
        .and_then(|t| serde_json::from_str(t.trim()).ok())
        .unwrap_or_else(|| json!({}));
    if let Some(p) = &server_stats {
        let _ = std::fs::remove_file(p);
    }

    if let Ok(path) = std::env::var("FB_STATS") {
        let line = json!({
            "side": side,
            "wire_out": bytes_out as f64,
            "wire_in": bytes_in.load(Ordering::Relaxed) as f64,
            "pack_wire": scan.pack_wire as f64,
            "pack_pkts": scan.pack_pkts as f64,
            "other_payload": scan.other_payload as f64,
            "other_pkts": scan.other_pkts as f64,
            "marker_pkts": scan.marker_pkts as f64,
            "desynced": scan.desynced,
            "trailing": scan.trailing as f64,
            "haves": haves.load(Ordering::Relaxed) as f64,
            "cpu_s": cpu_s,
            "rss_kb": usage.ru_maxrss as f64,
            "server_wall_ms": server_wall_ms,
            "exit": exit as f64,
            "server": server,
        });
        let mut f = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)?;
        f.write_all(format!("{line}\n").as_bytes())?;
    }
    ensure!(exit == 0, "the {side} server exited {exit}");
    Ok(())
}

fn count_needle(needle: &[u8], chunk: &[u8], carry: &mut Vec<u8>) -> u64 {
    carry.extend_from_slice(chunk);
    let mut n = 0u64;
    let mut i = 0usize;
    while i + needle.len() <= carry.len() {
        if &carry[i..i + needle.len()] == needle {
            n += 1;
            i += needle.len();
        } else {
            i += 1;
        }
    }
    let keep = needle.len().saturating_sub(1);
    if carry.len() > keep {
        carry.drain(..carry.len() - keep);
    }
    n
}

fn secs(t: libc::timeval) -> f64 {
    t.tv_sec as f64 + t.tv_usec as f64 / 1e6
}

// ───────────────────────────────────────────────────────────────────────────
// gunnar's server
// ───────────────────────────────────────────────────────────────────────────

fn inner() -> Result<()> {
    let args: Vec<String> = std::env::args().collect();
    let repo = PathBuf::from(args.get(2).cloned().unwrap_or_default());
    let hash = hash_kind(&repo);
    let store = FsStore::open_or_init(&repo, hash)
        .with_context(|| format!("opening {repo:?} as a {hash} store"))?;
    let objects = store.objects();
    let refs = store.refs();

    // Whether this repository even HAS a bitmap, read independently of whether
    // one answered. The pair is the guard: `has_bitmap && !answered_from_bitmap`
    // means the request shape declined, `!has_bitmap` means the repository never
    // offered one, and a single flag could not tell them apart.
    let has_bitmap = objects.bitmap().is_some();

    let stdin = std::io::stdin();
    let mut input = stdin.lock();
    let stdout = std::io::stdout();
    let mut out = BufWriter::new(stdout.lock());

    let version = ProtocolVersion::from_git_protocol(std::env::var("GIT_PROTOCOL").ok().as_deref());
    let upload = UploadPack::new(&objects, &refs);
    let summary = upload
        .serve(version, &mut input, &mut out)
        .with_context(|| format!("gunnar upload-pack v{} on {repo:?}", version.number()))?;
    out.flush()?;

    if let Ok(path) = std::env::var("FB_SERVER_STATS") {
        let line = json!({
            "objects_sent": summary.objects_sent as f64,
            "copied": summary.objects_copied_from_pack as f64,
            "recompressed": summary.objects_recompressed as f64,
            "rounds": summary.fetch_rounds as f64,
            "common": summary.common as f64,
            "protocol": summary.protocol.number() as f64,
            "answered_from_bitmap": summary.answered_from_bitmap,
            "thin_offered": summary.thin_bases_offered as f64,
            "thin_vetoed": summary.thin_bases_vetoed as f64,
            "has_bitmap": has_bitmap,
            "exclusion": std::env::var("GUNNAR_EXCLUSION").unwrap_or_else(|_| "edges".into()),
            "bitmap_env": std::env::var("GUNNAR_BITMAP").unwrap_or_else(|_| "read".into()),
        });
        std::fs::write(path, format!("{line}\n"))?;
    }
    Ok(())
}

fn hash_kind(repo: &Path) -> Kind {
    let config = std::fs::read_to_string(repo.join("config")).unwrap_or_default();
    if config.to_ascii_lowercase().contains("sha256") {
        Kind::Sha256
    } else {
        Kind::Sha1
    }
}

// ───────────────────────────────────────────────────────────────────────────
// the driver
// ───────────────────────────────────────────────────────────────────────────

/// One arm: a label and the environment that distinguishes it.
struct Arm {
    label: &'static str,
    side: &'static str,
    env: &'static [(&'static str, &'static str)],
}

const ARMS: &[Arm] = &[
    Arm {
        label: "git",
        side: "git",
        env: &[],
    },
    // The shipping default, both knobs unset.
    Arm {
        label: "gunnar-bitmap-edges",
        side: "gunnar",
        env: &[],
    },
    // The lever, on the path production takes.
    Arm {
        label: "gunnar-bitmap-full",
        side: "gunnar",
        env: &[("GUNNAR_EXCLUSION", "full")],
    },
    // The walk, which is what a repository with no `.bitmap` gets whether it
    // asked for it or not.
    Arm {
        label: "gunnar-walk-edges",
        side: "gunnar",
        env: &[("GUNNAR_BITMAP", "off")],
    },
    Arm {
        label: "gunnar-walk-full",
        side: "gunnar",
        env: &[("GUNNAR_BITMAP", "off"), ("GUNNAR_EXCLUSION", "full")],
    },
];

fn driver() -> Result<()> {
    let version = git(&["--version"], None)?.trim().to_string();
    ensure!(
        version == GIT_VERSION,
        "this probe measured against {version:?}, not the pinned {GIT_VERSION:?}"
    );
    let args: Vec<String> = std::env::args().skip(1).collect();
    let mode = args.first().map(String::as_str).unwrap_or("fixture");
    let work = work_root();
    let _ = std::fs::remove_dir_all(&work);
    std::fs::create_dir_all(&work)?;

    let mut cases: Vec<(String, PathBuf, PathBuf)> = Vec::new();
    match mode {
        "fixture" => {
            let root = fixture_root();
            let server = root.join("h2h-divergent-2000b-500d-server");
            let client = root.join("h2h-divergent-2000b-500d-client");
            ensure!(
                server.exists() && client.exists(),
                "the divergent fixture is not built"
            );
            cases.push((
                "divergent-500d/as-is".into(),
                server.clone(),
                client.clone(),
            ));
            // The same fixture with a bitmap on the server, which is what any
            // repository a gunnar or a git server has repacked actually has.
            // `git clone --bare` writes none, which is the whole reason the
            // shipping arm has never run the shipping path.
            let bm = root.join("fetchbytes-divergent-server-bitmap.git");
            if !bm.exists() {
                copy_repo(&server, &bm)?;
                let _ = std::fs::remove_dir_all(bm.join("gunnar-store"));
                git(&["-C", &s(&bm), "repack", "-adqb"], None)?;
            }
            ensure!(
                has_bitmap_file(&bm),
                "the bitmap variant of the server fixture has no .bitmap, so the arm that is \
                 supposed to exercise the bitmap path would silently measure the walk"
            );
            cases.push((
                "divergent-500d/server-bitmap".into(),
                bm.clone(),
                client.clone(),
            ));
            // ── the isolation ──────────────────────────────────────────────
            //
            // `server-bitmap` differs from `as-is` in two ways at once: it was
            // repacked, and it has a `.bitmap`. Stock git's pack grew 16.8%
            // between them, and attributing that to the bitmap without ruling
            // out the repack would be a guess. This is the same repacked pack
            // with the bitmap file removed and nothing else changed, so the
            // difference between this row and the one above is the bitmap
            // alone.
            let nobm = root.join("fetchbytes-divergent-server-repacked-nobitmap.git");
            if !nobm.exists() {
                copy_repo(&bm, &nobm)?;
                let _ = std::fs::remove_dir_all(nobm.join("gunnar-store"));
                let dir = nobm.join("objects").join("pack");
                for entry in std::fs::read_dir(&dir)? {
                    let path = entry?.path();
                    if path.extension().is_some_and(|x| x == "bitmap") {
                        std::fs::remove_file(&path)?;
                    }
                }
            }
            ensure!(
                !has_bitmap_file(&nobm),
                "the no-bitmap control still has a .bitmap, so it controls for nothing"
            );
            cases.push(("divergent-500d/repacked-nobitmap".into(), nobm, client));
        }
        "real" => {
            let depths: Vec<usize> = args[1..]
                .iter()
                .map(|a| a.parse::<usize>().context("depth must be a number"))
                .collect::<Result<_>>()?;
            ensure!(!depths.is_empty(), "`real` needs at least one depth");
            let corpus = fixture_root().join("real-corpus");
            let mut names: Vec<String> = std::fs::read_dir(&corpus)?
                .filter_map(|e| e.ok())
                .map(|e| e.file_name().to_string_lossy().into_owned())
                .filter(|n| n.ends_with(".git"))
                .collect();
            names.sort();
            ensure!(
                names.len() >= 19,
                "the real corpus holds {} repositories, not the 19 the constellation has; a \
                 smaller corpus is a different measurement, not a shorter one",
                names.len()
            );
            for name in &names {
                let server = corpus.join(name);
                for &d in &depths {
                    match cut_client(&server, name.trim_end_matches(".git"), d)? {
                        Some(client) => cases.push((
                            format!("{}/{}d", name.trim_end_matches(".git"), d),
                            server.clone(),
                            client,
                        )),
                        None => eprintln!("probe: {name} has no HEAD~{d}, skipped"),
                    }
                }
            }
        }
        other => bail!("no mode {other:?} (expected `fixture` or `real`)"),
    }

    let rounds: usize = std::env::var("FB_ROUNDS")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(3);
    let mut rows: Vec<Value> = Vec::new();
    for (name, server, client) in &cases {
        for arm in ARMS {
            let mut runs: Vec<Value> = Vec::new();
            for round in 0..rounds {
                let row = one_fetch(name, arm, server, client, &work, round)?;
                runs.push(row);
            }
            rows.push(fold(name, arm.label, &runs)?);
        }
    }
    println!("{}", json!({ "git_version": version, "rows": rows }));
    let _ = std::fs::remove_dir_all(&work);
    Ok(())
}

fn s(p: &Path) -> String {
    p.to_string_lossy().into_owned()
}

fn has_bitmap_file(repo: &Path) -> bool {
    std::fs::read_dir(repo.join("objects").join("pack"))
        .map(|d| {
            d.filter_map(|e| e.ok())
                .any(|e| e.path().extension().is_some_and(|x| x == "bitmap"))
        })
        .unwrap_or(false)
}

fn copy_repo(from: &Path, to: &Path) -> Result<()> {
    let _ = std::fs::remove_dir_all(to);
    let out = Command::new("cp").arg("-a").arg(from).arg(to).output()?;
    ensure!(out.status.success(), "cp -a {from:?} {to:?} failed");
    Ok(())
}

/// A client repository holding `server`'s history up to `HEAD~depth`.
///
/// Built by a real `file://` clone of a temporary branch rather than by
/// deleting objects out of a copy, so the client's object database is exactly
/// the closure of the cut commit — which is what makes the fetch that follows a
/// real negotiation and not a fetch by a client that secretly already holds the
/// answer.
fn cut_client(server: &Path, name: &str, depth: usize) -> Result<Option<PathBuf>> {
    let dst = fixture_root()
        .join("fetchbytes-clients")
        .join(format!("{name}-{depth}d.git"));
    if dst.exists() {
        return Ok(Some(dst));
    }
    let head = git(&["-C", &s(server), "symbolic-ref", "--short", "HEAD"], None)?
        .trim()
        .to_string();
    let cut = match git(
        &["-C", &s(server), "rev-parse", &format!("{head}~{depth}")],
        None,
    ) {
        Ok(v) => v.trim().to_string(),
        Err(_) => return Ok(None),
    };
    std::fs::create_dir_all(dst.parent().unwrap())?;
    let tmp = format!("refs/heads/__fb_cut_{depth}");
    git(&["-C", &s(server), "update-ref", &tmp, &cut], None)?;
    let r = (|| -> Result<()> {
        git(
            &[
                "clone",
                "-q",
                "--bare",
                "--single-branch",
                "--branch",
                &format!("__fb_cut_{depth}"),
                &format!("file://{}", server.display()),
                &s(&dst),
            ],
            None,
        )?;
        git(
            &[
                "-C",
                &s(&dst),
                "branch",
                "-m",
                &format!("__fb_cut_{depth}"),
                "main",
            ],
            None,
        )?;
        git(
            &["-C", &s(&dst), "symbolic-ref", "HEAD", "refs/heads/main"],
            None,
        )?;
        Ok(())
    })();
    git(&["-C", &s(server), "update-ref", "-d", &tmp], None)?;
    r?;
    Ok(Some(dst))
}

fn one_fetch(
    case: &str,
    arm: &Arm,
    server: &Path,
    client: &Path,
    work: &Path,
    round: usize,
) -> Result<Value> {
    let target = work.join(format!("t-{round}"));
    let stats = work.join(format!("stats-{round}"));
    let veto = work.join(format!("veto-{round}"));
    let _ = std::fs::remove_dir_all(&target);
    let _ = std::fs::remove_file(&stats);
    let _ = std::fs::remove_file(&veto);
    copy_repo(client, &target)?;
    let before = object_count(&target)?;

    let exe = std::env::current_exe()?;
    let head = git(&["-C", &s(server), "symbolic-ref", "--short", "HEAD"], None)?
        .trim()
        .to_string();

    let mut c = Command::new("git");
    c.args(["-c", "protocol.ext.allow=always"])
        .env("FB_SERVE", arm.side)
        .env("FB_STATS", &stats)
        .env("GIT_PROTOCOL", "version=2")
        .env("GIT_TERMINAL_PROMPT", "0")
        // Both knobs cleared first, so an arm that does not set one is measured
        // with the default and not with whatever the shell happened to hold.
        .env_remove("GUNNAR_EXCLUSION")
        .env_remove("GUNNAR_BITMAP")
        .env("GUNNAR_THIN_VETO_LOG", &veto);
    for (k, v) in arm.env {
        c.env(k, v);
    }
    let started = Instant::now();
    let status = c
        .args(["-C", &s(&target), "fetch", "--quiet"])
        .arg(format!("ext::{} %S {}", exe.display(), server.display()))
        .arg(format!("refs/heads/{head}:refs/remotes/fb/main"))
        .stdout(Stdio::null())
        .stderr(Stdio::inherit())
        .status()?;
    let wall_ms = started.elapsed().as_secs_f64() * 1000.0;
    ensure!(
        status.success(),
        "the {} fetch of {case} failed ({status})",
        arm.label
    );

    // ── the bar: stock git is the judge ────────────────────────────────────
    let want = git(
        &["-C", &s(server), "rev-parse", &format!("refs/heads/{head}")],
        None,
    )?;
    let got = git(
        &["-C", &s(&target), "rev-parse", "refs/remotes/fb/main"],
        None,
    )?;
    ensure!(
        got.trim() == want.trim(),
        "{} landed {got:?}, not {want:?}",
        arm.label
    );
    git(
        &[
            "-C",
            &s(&target),
            "fsck",
            "--full",
            "--strict",
            "--no-progress",
        ],
        None,
    )
    .with_context(|| format!("fsck after the {} fetch of {case}", arm.label))?;
    // Set equality of the object graph, which is the only check that can see a
    // pack that is valid, connected, and short.
    let theirs = rev_list(server, &format!("refs/heads/{head}"))?;
    let ours = rev_list(&target, "refs/remotes/fb/main")?;
    ensure!(
        theirs == ours,
        "{} delivered {} objects where the server's own rev-list has {} for {case}",
        arm.label,
        ours.len(),
        theirs.len()
    );
    let after = object_count(&target)?;
    ensure!(
        after > before,
        "the {} fetch of {case} added no objects",
        arm.label
    );

    let text = std::fs::read_to_string(&stats)
        .with_context(|| format!("no meter stats for {case}/{}", arm.label))?;
    let line = text
        .lines()
        .find(|l| !l.trim().is_empty())
        .context("empty stats")?;
    let mut v: Value = serde_json::from_str(line)?;
    let _ = std::fs::remove_dir_all(&target);
    let _ = std::fs::remove_file(&stats);

    ensure!(
        v["desynced"] != json!(true),
        "the pkt-line scan desynced on {case}/{}, so its byte attribution is not a measurement",
        arm.label
    );
    ensure!(
        v["pack_wire"].as_f64().unwrap_or(0.0) > 0.0,
        "{case}/{} put zero band-1 bytes on the wire; a fetch with no sidebanded packfile is not \
         the comparison this probe makes",
        arm.label
    );
    ensure!(
        v["haves"].as_f64().unwrap_or(0.0) > 0.0,
        "{case}/{} offered no `have`, so nothing was negotiated",
        arm.label
    );
    v["wall_ms"] = json!(wall_ms);
    v["objects"] = json!((after - before) as f64);

    // ── was the refusal RIGHT? ─────────────────────────────────────────────
    //
    // A refused base costs its object the delta. That is the correct answer
    // when the receiver really lacks the base and a pure loss when it does not,
    // and the two are indistinguishable from the server's side. So they are
    // told apart from the **client's** side, against `client` — the pristine
    // pre-fetch repository, not `target`, which by now holds everything the
    // fetch just delivered and would call almost every base present.
    let (wrong, right) = judge_vetoes(&veto, client)?;
    v["veto_base_held_by_client"] = json!(wrong as f64);
    v["veto_base_absent_from_client"] = json!(right as f64);
    let _ = std::fs::remove_file(&veto);
    Ok(v)
}

fn fold(case: &str, label: &str, runs: &[Value]) -> Result<Value> {
    let med = |k: &str| -> f64 {
        let mut v: Vec<f64> = runs.iter().filter_map(|r| r[k].as_f64()).collect();
        v.sort_by(|a, b| a.partial_cmp(b).expect("no NaN"));
        if v.is_empty() {
            0.0
        } else {
            v[v.len() / 2]
        }
    };
    // Bytes are deterministic; a spread here means the fixture moved under the
    // run and every ratio taken from it is noise.
    let spread = |k: &str| -> f64 {
        let v: Vec<f64> = runs.iter().filter_map(|r| r[k].as_f64()).collect();
        let (lo, hi) = v
            .iter()
            .fold((f64::MAX, f64::MIN), |(l, h), &x| (l.min(x), h.max(x)));
        if v.is_empty() {
            0.0
        } else {
            hi - lo
        }
    };
    // **Not `== 0`.** Stock `git pack-objects` runs a threaded delta search
    // and its output is not bit-reproducible run to run, so a zero-spread
    // assertion is one this comparison can never hold both sides to. What must
    // hold is that the spread is far below the differences being reported: a
    // 25 400-byte finding read off a column that wanders by more than a
    // fraction of a percent would be noise wearing a decimal point. The spread
    // is emitted as well, so a reader can check that rather than trust it.
    let wire_spread = spread("wire_out");
    ensure!(
        wire_spread <= 0.005 * med("wire_out"),
        "{case}/{label} moved {wire_spread} bytes across {} rounds, which is more than 0.5% of \
         {}; nothing taken from that column is a measurement",
        runs.len(),
        med("wire_out")
    );
    let srv = &runs[0]["server"];
    Ok(json!({
        "case": case,
        "arm": label,
        "wire_out": med("wire_out"),
        "pack_wire": med("pack_wire"),
        "pack_framing": med("pack_pkts") * 5.0,
        "overhead": med("wire_out") - med("pack_wire"),
        "wire_out_spread": wire_spread,
        "pack_wire_spread": spread("pack_wire"),
        "other_payload": med("other_payload"),
        "other_pkts": med("other_pkts"),
        "wire_in": med("wire_in"),
        "haves": med("haves"),
        "objects": med("objects"),
        "cpu_s": med("cpu_s"),
        "rss_kb": med("rss_kb"),
        "server_wall_ms": med("server_wall_ms"),
        "wall_ms": med("wall_ms"),
        "answered_from_bitmap": srv["answered_from_bitmap"],
        "has_bitmap": srv["has_bitmap"],
        "copied": srv["copied"],
        "recompressed": srv["recompressed"],
        "objects_sent": srv["objects_sent"],
        "rounds": srv["rounds"],
        "thin_offered": srv["thin_offered"],
        "thin_vetoed": srv["thin_vetoed"],
        "veto_base_held_by_client": runs[0]["veto_base_held_by_client"].clone(),
        "veto_base_absent_from_client": runs[0]["veto_base_absent_from_client"].clone(),
        "protocol": srv["protocol"],
    }))
}

/// Split a run's refused thin-delta bases into the ones the client **did**
/// hold and the ones it did not.
///
/// `git cat-file --batch-check` over the *pre-fetch* client, in one process for
/// the whole list: one `git` per refused base would make a 1 000-refusal run
/// slower than the fetch it is measuring, and the batch form answers
/// `<oid> missing` for an absent object rather than failing, which is exactly
/// the distinction being drawn.
fn judge_vetoes(log: &Path, client: &Path) -> Result<(usize, usize)> {
    let Ok(text) = std::fs::read_to_string(log) else {
        return Ok((0, 0));
    };
    let bases: BTreeSet<String> = text
        .lines()
        .filter_map(|l| l.split_whitespace().nth(1))
        .map(str::to_owned)
        .collect();
    if bases.is_empty() {
        return Ok((0, 0));
    }
    let mut child = Command::new("git")
        .args(["-C", &s(client), "cat-file", "--batch-check"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()?;
    {
        let mut sink = child.stdin.take().context("no cat-file stdin")?;
        for b in &bases {
            sink.write_all(b.as_bytes())?;
            sink.write_all(b"\n")?;
        }
    }
    let out = child.wait_with_output()?;
    let text = String::from_utf8_lossy(&out.stdout);
    let mut held = 0usize;
    let mut absent = 0usize;
    for line in text.lines().filter(|l| !l.trim().is_empty()) {
        if line.ends_with(" missing") {
            absent += 1;
        } else {
            held += 1;
        }
    }
    ensure!(
        held + absent == bases.len(),
        "cat-file answered for {} of {} refused bases; a silent short answer here would read as \
         \"every refusal was correct\"",
        held + absent,
        bases.len()
    );
    Ok((held, absent))
}

fn object_count(repo: &Path) -> Result<u64> {
    let out = git(&["-C", &s(repo), "rev-list", "--objects", "--all"], None)?;
    Ok(out.lines().filter(|l| !l.trim().is_empty()).count() as u64)
}

fn rev_list(repo: &Path, tip: &str) -> Result<BTreeSet<String>> {
    let out = git(&["-C", &s(repo), "rev-list", "--objects", tip], None)?;
    let set: BTreeSet<String> = out
        .lines()
        .filter_map(|l| l.split_whitespace().next())
        .map(str::to_owned)
        .collect();
    ensure!(
        !set.is_empty(),
        "rev-list of {tip} in {repo:?} is empty, which is not a graph"
    );
    Ok(set)
}

fn git(args: &[&str], cwd: Option<&Path>) -> Result<String> {
    let mut c = Command::new("git");
    c.args(args);
    if let Some(d) = cwd {
        c.current_dir(d);
    }
    let out = c
        .output()
        .with_context(|| format!("running git {args:?}"))?;
    ensure!(
        out.status.success(),
        "git {args:?} exited {:?}: {}",
        out.status.code(),
        String::from_utf8_lossy(&out.stderr).trim()
    );
    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}