crates/storage-git-gix/parked-gix-2/bench-gix/have-parity.rs
//! **Wire-byte parity for the have-path split, over the real corpus.**
//!
//! `ObjectStore::contains_without_rescan` is defined to return the same answer
//! as `contains` whenever the store's snapshot is current, which is every freshly
//! opened repository. So over the corpus the served bytes **must** be identical,
//! and this is the check that the split was wired to the right call sites and
//! only to those: a `want`, a delta base or the walk's `Missing::Skip` routed
//! onto the cheap path would show up here as a different pack or a refused
//! fetch, on a real repository rather than on a fixture.
//!
//! Two request shapes per repository, because they exercise different code:
//!
//! * **clone** — no haves at all. The negotiation makes zero membership calls,
//! so this is purely "did anything else move".
//! * **negotiated fetch** — 16 real ancestors of the tip as `have`s, which is
//! the shape `.nornir/bench/have-mix.sh` measured every merely-behind client
//! to send.
//!
//! Each is served twice from the same store, once down each `HavePath`,
//! interleaved. The count of calls that actually reached the negotiation's
//! method is asserted, because equal bytes are what this switch is *supposed*
//! to produce and therefore prove nothing on their own.
//!
//! Run: `cargo run --release --example have-parity -- <dir-of-bare-repos>...`
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context, Result};
use gix_hash::ObjectId;
use gunnar_bench::have_lookup::{HavePath, HaveRouted};
use gunnar_store::fs::FsStore;
use gunnar_wire::upload_pack::FetchRequest;
use gunnar_wire::UploadPack;
fn git(cwd: &Path, args: &[&str]) -> Result<String> {
let out = std::process::Command::new("git")
.current_dir(cwd)
.args(args)
.output()
.with_context(|| format!("running git {args:?}"))?;
ensure!(
out.status.success(),
"git {args:?} in {}: {}",
cwd.display(),
String::from_utf8_lossy(&out.stderr)
);
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
}
fn digest(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in bytes {
h ^= u64::from(*b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
/// Serve one request through a store routed down `path`; return the bytes, the
/// object count and how many calls reached the negotiation's method.
fn serve(store: &FsStore, request: &FetchRequest, path: HavePath) -> Result<(Vec<u8>, u32, u64)> {
let objects = store.objects();
let routed = HaveRouted::new(&objects, path);
let refs = store.refs();
let upload = UploadPack::new(&routed, &refs);
let mut out = Vec::new();
let outcome = upload.fetch(request, &mut out)?;
Ok((out, outcome.objects_sent, routed.as_have()))
}
fn main() -> Result<()> {
let roots: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
ensure!(
!roots.is_empty(),
"usage: have-parity <dir-of-bare-repos>..."
);
// A private, writable copy per repository. `FsStore` takes an exclusive
// writer lock and writes a multi-pack index, so opening the shared corpus
// directly races every other arm that uses it — which it did, twice, while
// `gunnar.have_lookup` was being written.
let scratch = PathBuf::from("/home/rickard/scratch/have-parity");
let _ = std::fs::remove_dir_all(&scratch);
std::fs::create_dir_all(&scratch)?;
let mut repos: Vec<PathBuf> = Vec::new();
for root in &roots {
for entry in std::fs::read_dir(root)?.flatten() {
let p = entry.path();
if p.is_dir() && p.join("HEAD").exists() {
repos.push(p);
}
}
}
repos.sort();
ensure!(!repos.is_empty(), "no bare repositories under {roots:?}");
let mut checked = 0usize;
let mut clone_objects = 0u64;
let mut fetch_objects = 0u64;
let mut skipped: Vec<String> = Vec::new();
for src in &repos {
let name = src.file_name().unwrap().to_string_lossy().into_owned();
let dst = scratch.join(&name);
// Hardlinked: git objects are immutable, so this costs inodes and not
// bytes, and the directories are still this process's own to write into.
let cp = std::process::Command::new("cp")
.args(["-al".as_ref(), src.as_os_str(), dst.as_os_str()])
.status()?;
ensure!(cp.success(), "could not copy {name}");
let kind = gunnar_store::fs::stored_hash_kind(&dst)?;
let tip = git(
&dst,
&[
"for-each-ref",
"--count=1",
"--format=%(objectname)",
"refs/heads/",
],
)?;
if tip.is_empty() {
skipped.push(format!("{name}: no branch"));
continue;
}
let want = ObjectId::from_hex(tip.as_bytes())?;
let ancestors: Vec<ObjectId> = git(&dst, &["rev-list", "--max-count=17", &tip])?
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.skip(1)
.take(16)
.map(|l| ObjectId::from_hex(l.as_bytes()))
.collect::<std::result::Result<Vec<_>, _>>()?;
if ancestors.is_empty() {
skipped.push(format!("{name}: shallower than 2 commits"));
continue;
}
let store = FsStore::open_or_init(&dst, kind)?;
for (shape, haves) in [("clone", Vec::new()), ("fetch", ancestors.clone())] {
let request = FetchRequest {
wants: vec![want],
haves,
done: true,
thin_pack: true,
ofs_delta: true,
..Default::default()
};
// Interleaved so a concurrent repack or a warming page cache lands
// on both columns rather than on whichever ran last.
let (a, na, ha) = serve(&store, &request, HavePath::Rescan)?;
let (b, nb, hb) = serve(&store, &request, HavePath::NoRescan)?;
ensure!(
na > 0 && nb > 0,
"{name}/{shape}: served zero objects, so equal bytes mean nothing"
);
ensure!(
digest(&a) == digest(&b) && a.len() == b.len(),
"{name}/{shape}: the two have paths served DIFFERENT bytes \
({} vs {} bytes, {:016x} vs {:016x}). On a current snapshot they are \
defined to agree, so this is a call site routed onto the cheap path that \
must not be.",
a.len(),
b.len(),
digest(&a),
digest(&b)
);
ensure!(na == nb, "{name}/{shape}: {na} objects against {nb}");
ensure!(
ha == hb,
"{name}/{shape}: the two columns made different numbers of have lookups"
);
if shape == "fetch" {
// One call per DISTINCT have, which is what `HaveMemo` collapses
// a negotiation to. A repository shallower than 17 commits
// offers fewer than 16, so the expectation is the list's own
// length rather than a constant — a constant would have made
// this arm skip the small repositories by failing on them.
ensure!(
hb as usize >= ancestors.len(),
"{name}/fetch: only {hb} of the {} haves reached \
`contains_without_rescan`. The wrapper is not on the path under test, \
and equal bytes would NOT have caught that.",
ancestors.len()
);
fetch_objects += u64::from(nb);
} else {
ensure!(
hb == 0,
"{name}/clone: a clone made {hb} have lookups; it offers no haves"
);
clone_objects += u64::from(nb);
}
}
drop(store);
checked += 1;
println!(" ok {name} ({kind:?})");
let _ = std::fs::remove_dir_all(&dst);
}
println!();
println!("byte-identical on {checked} repositories x 2 shapes");
println!(" clone objects served: {clone_objects}");
println!(" fetch objects served: {fetch_objects}");
for s in &skipped {
println!(" skipped {s}");
}
ensure!(
checked >= 15,
"only {checked} repositories were actually checked"
);
Ok(())
}