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