use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Instant;
#[global_allocator]
static ALLOC: gunnar_server::ServerAllocator = gunnar_server::SERVER_ALLOCATOR;
use gunnar_server::storage::Engine;
use gunnar_store::git_storage_trait::RefUpdate;
use gunnar_wire::receive_pack::AllowAll;
use gunnar_wire::{ProtocolVersion, ReceivePack, UploadPack};
type Res<T> = Result<T, Box<dyn std::error::Error>>;
fn store_root(repo: &Path, engine: Engine) -> PathBuf {
let mut s = repo.as_os_str().to_os_string();
s.push(format!(".gunnar-{}", engine.name()));
PathBuf::from(s)
}
fn hash_kind(repo: &Path) -> gix_hash::Kind {
let config = std::fs::read_to_string(repo.join("config")).unwrap_or_default();
if config.to_ascii_lowercase().contains("sha256") {
gix_hash::Kind::Sha256
} else {
gix_hash::Kind::Sha1
}
}
fn prepare(repo: &Path, engine: Engine) -> Res<()> {
let root = store_root(repo, engine);
let kind = hash_kind(repo);
let started = Instant::now();
std::fs::create_dir_all(&root)?;
let store = engine.open_repo(&root, kind)?;
let held = store.serve().refs()?.len();
if held > 0 {
eprintln!(
"{engine}-prepare: {} already holds {held} refs",
root.display()
);
return Ok(());
}
let pack_dir = repo.join("objects").join("pack");
let mut packs: Vec<PathBuf> = Vec::new();
if pack_dir.is_dir() {
for entry in std::fs::read_dir(&pack_dir)? {
let path = entry?.path();
if path.extension().is_some_and(|e| e == "pack") {
packs.push(path);
}
}
}
packs.sort();
let loose = loose_object_count(repo);
let mut pack_bytes = 0u64;
let mut announced = 0u64;
let ingested_from;
if !packs.is_empty() && loose == 0 {
ingested_from = format!("{} verbatim pack(s)", packs.len());
for pack in &packs {
let file = std::fs::File::open(pack)?;
pack_bytes += file.metadata()?.len();
let mut reader = BufReader::new(file);
let receipt = engine.ingest_push(
&root,
kind,
&mut reader,
gunnar_server::storage::ObjectSet::NotNeeded,
None,
None,
)?;
announced += u64::from(receipt.num_objects);
}
} else {
if packs.is_empty() && loose == 0 {
return Err(format!("{} holds no objects at all", repo.display()).into());
}
let why = if packs.is_empty() {
format!("{loose} loose object(s)")
} else {
format!("{} pack(s) beside {loose} loose object(s)", packs.len())
};
ingested_from = format!("{why}, re-packed into a stream with --delta-base-offset");
let bytes = pack_objects(repo)?;
pack_bytes += bytes.len() as u64;
let mut reader: &[u8] = &bytes;
let receipt = engine.ingest_push(
&root,
kind,
&mut reader,
gunnar_server::storage::ObjectSet::NotNeeded,
None,
None,
)?;
announced += u64::from(receipt.num_objects);
}
let edits = read_refs(repo)?;
if edits.is_empty() {
return Err(format!("{} has no references under refs/", repo.display()).into());
}
let count = edits.len();
let updates: Vec<RefUpdate> = edits
.iter()
.map(|(name, oid)| RefUpdate::set(name.clone(), oid.clone()))
.collect();
store.serve().put_refs(&updates)?;
store.serve().set_head(&head_target(repo)?)?;
let after = store.serve().refs()?.len();
if after == 0 {
return Err("the store holds no refs after ingest".into());
}
eprintln!(
"{engine}-prepare: {} <- {ingested_from}, {pack_bytes} bytes, {announced} announced, \
{count} refs, {:.1} ms",
root.display(),
started.elapsed().as_secs_f64() * 1000.0
);
Ok(())
}
fn read_refs(repo: &Path) -> Res<Vec<(String, String)>> {
let out = Command::new("git")
.arg("-C")
.arg(repo)
.args(["for-each-ref", "--format=%(refname)%00%(objectname)"])
.stderr(Stdio::inherit())
.output()?;
if !out.status.success() {
return Err(format!("git for-each-ref in {} failed", repo.display()).into());
}
let mut refs = Vec::new();
for line in String::from_utf8_lossy(&out.stdout).lines() {
let mut parts = line.split('\0');
if let (Some(name), Some(oid)) = (parts.next(), parts.next()) {
if !name.is_empty() && !oid.is_empty() {
refs.push((name.to_owned(), oid.to_owned()));
}
}
}
Ok(refs)
}
fn head_target(repo: &Path) -> Res<String> {
let sym = Command::new("git")
.arg("-C")
.arg(repo)
.args(["symbolic-ref", "--quiet", "HEAD"])
.stderr(Stdio::null())
.output()?;
if sym.status.success() {
let name = String::from_utf8_lossy(&sym.stdout).trim().to_string();
if !name.is_empty() {
return Ok(name);
}
}
let direct = Command::new("git")
.arg("-C")
.arg(repo)
.args(["rev-parse", "--verify", "--quiet", "HEAD"])
.stderr(Stdio::null())
.output()?;
if direct.status.success() {
let oid = String::from_utf8_lossy(&direct.stdout).trim().to_string();
if !oid.is_empty() {
return Ok(oid);
}
}
Ok("refs/heads/main".to_owned())
}
fn pack_objects(repo: &Path) -> Res<Vec<u8>> {
let listed = Command::new("git")
.arg("-C")
.arg(repo)
.args(["rev-list", "--objects", "--all"])
.stderr(Stdio::inherit())
.output()?;
if !listed.status.success() {
return Err(format!("git rev-list in {} failed", repo.display()).into());
}
let mut oids = String::new();
for line in String::from_utf8_lossy(&listed.stdout).lines() {
if let Some(oid) = line.split_whitespace().next() {
oids.push_str(oid);
oids.push('\n');
}
}
if oids.is_empty() {
return Err(format!("git rev-list named no objects in {}", repo.display()).into());
}
let mut child = Command::new("git")
.arg("-C")
.arg(repo)
.args(["pack-objects", "--stdout", "--delta-base-offset", "-q"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()?;
child
.stdin
.take()
.ok_or("git pack-objects has no stdin")?
.write_all(oids.as_bytes())?;
let out = child.wait_with_output()?;
if !out.status.success() {
return Err(format!(
"git pack-objects in {} exited {:?}",
repo.display(),
out.status.code()
)
.into());
}
if out.stdout.len() < 32 {
return Err(format!(
"git pack-objects in {} produced {} bytes, which is not a packfile",
repo.display(),
out.stdout.len()
)
.into());
}
Ok(out.stdout)
}
fn loose_object_count(repo: &Path) -> usize {
let dir = repo.join("objects");
let Ok(entries) = std::fs::read_dir(&dir) else {
return 0;
};
let mut n = 0;
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.len() == 2 && name.chars().all(|c| c.is_ascii_hexdigit()) {
if let Ok(inner) = std::fs::read_dir(entry.path()) {
n += inner.flatten().count();
}
}
}
n
}
fn copy_stats(copied: usize, recompressed: usize) -> Res<()> {
if let Ok(path) = std::env::var("GUNNAR_BENCH_COPY_STATS") {
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
writeln!(
f,
"{{\"copied\":{},\"recompressed\":{}}}",
copied as f64, recompressed as f64
)?;
}
Ok(())
}
fn serve(service: &str, repo: &Path, engine: Engine) -> Res<()> {
let root = store_root(repo, engine);
let kind = hash_kind(repo);
let store = engine.open_repo(&root, kind)?;
if store.serve().refs()?.is_empty() {
return Err(format!(
"{} holds no refs; run `{} prepare {}` before measuring",
root.display(),
std::env::args().next().unwrap_or_default(),
repo.display()
)
.into());
}
let stdin = std::io::stdin();
let mut input = BufReader::with_capacity(gunnar_wire::TRANSPORT_BUFFER_BYTES, stdin.lock());
let stdout = std::io::stdout();
let mut out = BufWriter::with_capacity(gunnar_wire::TRANSPORT_BUFFER_BYTES, stdout.lock());
let started = Instant::now();
match service {
"git-upload-pack" => {
let version =
ProtocolVersion::from_git_protocol(std::env::var("GIT_PROTOCOL").ok().as_deref());
let summary =
UploadPack::new(store.serve(), kind).serve(version, &mut input, &mut out)?;
out.flush()?;
eprintln!(
"{engine}: served git-upload-pack v{} rounds={} common={} selected={} \
objects={} copied={} recompressed={} bitmap={} {:.1} ms",
version.number(),
summary.fetch_rounds,
summary.common,
summary.objects_selected,
summary.objects_sent,
summary.objects_copied_from_pack,
summary.objects_recompressed,
summary.answered_from_bitmap,
started.elapsed().as_secs_f64() * 1000.0,
);
copy_stats(
summary.objects_copied_from_pack,
summary.objects_recompressed,
)?;
}
"git-receive-pack" => {
let ingest = ExampleIngest {
git_dir: root.clone(),
hash_kind: kind,
engine,
};
let outcome = ReceivePack::new(store.serve(), kind)
.serve(&mut input, &mut out, &ingest, &AllowAll)?;
out.flush()?;
match outcome {
Some(o) => eprintln!(
"{engine}: served git-receive-pack commands={} statuses={:?} {:.1} ms",
o.commands.len(),
o.statuses,
started.elapsed().as_secs_f64() * 1000.0,
),
None => eprintln!("{engine}: served git-receive-pack, no commands"),
}
}
other => return Err(format!("this store serves no {other:?}").into()),
}
Ok(())
}
struct ExampleIngest {
git_dir: PathBuf,
hash_kind: gix_hash::Kind,
engine: Engine,
}
impl gunnar_wire::PackIngest for ExampleIngest {
fn ingest(&self, pack: &mut dyn BufRead) -> gunnar_wire::Result<gunnar_wire::PackReceipt> {
let received = self
.engine
.ingest_push(
&self.git_dir,
self.hash_kind,
pack,
gunnar_server::storage::ObjectSet::NotNeeded,
None,
None,
)
.map_err(|err| {
gunnar_wire::Error::Store(gunnar_store::Error::backend(format!(
"storing the pushed pack: {err:#}"
)))
})?;
let pack_id = gix_hash::ObjectId::try_from(received.pack_id.as_slice()).map_err(|_| {
gunnar_wire::Error::Store(gunnar_store::Error::backend(format!(
"the stored pack's trailer is {} bytes, which is not a {} object id",
received.pack_id.len(),
self.hash_kind
)))
})?;
Ok(gunnar_wire::PackReceipt::without_object_set(
gunnar_wire::pack::IndexedPack {
num_objects: received.num_objects,
pack_id,
index_id: gix_hash::ObjectId::null(self.hash_kind),
pack_version: gunnar_wire::pack::PackVersion::from_u32(received.version),
hash_kind: self.hash_kind,
index_path: None,
data_path: None,
keep_path: None,
stored_bytes: Some(received.pack_bytes),
},
))
}
}
fn has(repo: &Path, oid: &str, engine: Engine) -> Res<()> {
let root = store_root(repo, engine);
let kind = hash_kind(repo);
let opened = Instant::now();
let store = engine.open_repo(&root, kind)?;
let open_ms = opened.elapsed().as_secs_f64() * 1000.0;
let refs = store.serve().refs()?.len();
if oid.is_empty() {
println!("engine={engine} refs={refs} open={open_ms:.1} ms");
return Ok(());
}
let raw = hex_oid(oid, kind)?;
let selected = store
.serve()
.select(&[&raw], &[])
.map(|r| r.map(|s| (s.objects.len(), s.commits.len())));
println!(
"engine={engine} refs={refs} open={open_ms:.1} ms has({oid})={:?} header={:?} \
extents={:?} select(objects,commits)={selected:?}",
store.serve().has(&raw),
store.serve().header(&raw),
store.serve().extents(&[&raw]).map(|v| v[0].is_some()),
);
Ok(())
}
fn have_list(pool: &[Vec<u8>], n: usize, hit_frac: f64) -> Vec<Vec<u8>> {
let want_hits = (n as f64 * hit_frac).round() as usize;
let mut state: u64 = 0x5EED_1234 | 1;
let mut next = move || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
state ^ (state >> 31)
};
let mut out: Vec<Vec<u8>> = Vec::with_capacity(n);
for i in 0..n {
let mut o = pool[(next() % pool.len() as u64) as usize].clone();
if i >= want_hits {
o[0] ^= 0xff;
o[1] ^= (i & 0xff) as u8;
let last = o.len() - 1;
o[last] ^= 0x5a;
}
out.push(o);
}
out
}
const HAVE_SHAPES: &[(&str, usize, f64)] = &[
("behind16", 16, 1.0),
("diverge16", 16, 0.375),
("diverge32", 32, 0.25),
("diverge64", 64, 0.20),
("diverge128", 128, 0.162),
("cap256_unobs", 256, 0.0),
];
fn have_batch(repo: &Path, engine: Engine) -> Res<()> {
let root = store_root(repo, engine);
let kind = hash_kind(repo);
let opened = Instant::now();
let store = engine.open_repo(&root, kind)?;
let open_ms = opened.elapsed().as_secs_f64() * 1000.0;
let serve = store.serve();
let refs = serve.refs()?;
let tip = refs
.iter()
.filter(|r| r.name.starts_with("refs/heads/"))
.find_map(|r| r.oid.clone())
.or_else(|| refs.iter().find_map(|r| r.oid.clone()))
.ok_or_else(|| format!("{} advertises no oid-valued ref", root.display()))?;
let reach = serve
.select(&[&tip], &[])?
.ok_or("the store declined to select over its own tip")?;
let pool: Vec<Vec<u8>> = if reach.commits.len() >= 256 {
reach.commits.clone()
} else {
reach.objects.clone()
};
if pool.len() < 256 {
return Err(format!(
"the tip's closure is {} object(s); a membership probe that small measures L1",
pool.len()
)
.into());
}
let reps: usize = std::env::var("GUNNAR_HAVEBATCH_REPS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2001);
println!(
"engine={engine} store={} open={open_ms:.1}ms pool={} reps={reps}",
root.display(),
pool.len()
);
println!(
"{:<14} {:>5} {:>10} {:>11} {:>10} {:>8} {:>8} {:>12} {:>12}",
"shape",
"haves",
"ns_scalar",
"ns_extents",
"ns_hybrid",
"ext_x",
"hyb_x",
"cold_scalar",
"cold_hybrid"
);
let mut rate: Vec<(f64, f64)> = Vec::with_capacity(HAVE_SHAPES.len());
for (name, n, hit_frac) in HAVE_SHAPES.iter().copied() {
let want_hits = (n as f64 * hit_frac).round() as usize;
let haves = have_list(&pool, n, hit_frac);
let slices: Vec<&[u8]> = haves.iter().map(|v| v.as_slice()).collect();
let mut present = 0usize;
for (i, o) in slices.iter().enumerate() {
let here = serve.has(o)?;
if here {
present += 1;
}
if (i < want_hits) != here {
return Err(format!(
"{name}: have {i} is {}, the shape says {}",
if here { "present" } else { "absent" },
if i < want_hits { "present" } else { "absent" }
)
.into());
}
}
let cold_store = engine.open_repo(&root, kind)?;
let cold = cold_store.serve();
let t = Instant::now();
let mut acc = 0usize;
for o in &slices {
if cold.has(o)? {
acc += 1;
}
}
let cold_scalar = t.elapsed().as_nanos() as f64 / n as f64;
let cold_store2 = engine.open_repo(&root, kind)?;
let cold2 = cold_store2.serve();
let t = Instant::now();
let ext = cold2.extents(&slices)?;
let mut acc2 = 0usize;
for (i, e) in ext.iter().enumerate() {
if e.is_some() || cold2.has(slices[i])? {
acc2 += 1;
}
}
let cold_hybrid = t.elapsed().as_nanos() as f64 / n as f64;
if acc != present || acc2 != present {
return Err(format!(
"{name}: the cold columns found {acc}/{acc2} present, the checked set says \
{present}"
)
.into());
}
let mut ns: [f64; 3] = [0.0; 3];
let mut found: [usize; 3] = [0; 3];
for (col, slot) in ns.iter_mut().enumerate() {
let mut samples: Vec<f64> = Vec::with_capacity(9);
for rep in 0..9 {
let t = Instant::now();
let mut hits = 0usize;
for _ in 0..reps {
match col {
0 => {
for o in &slices {
if serve.has(o)? {
hits += 1;
}
}
}
1 => {
for e in serve.extents(&slices)? {
if e.is_some() {
hits += 1;
}
}
}
_ => {
for (i, e) in serve.extents(&slices)?.iter().enumerate() {
if e.is_some() || serve.has(slices[i])? {
hits += 1;
}
}
}
}
}
samples.push(t.elapsed().as_secs_f64() * 1e9 / (reps * n) as f64);
if rep == 0 {
found[col] = hits / reps;
}
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
*slot = samples[0];
}
if found[0] != present || found[2] != present {
return Err(format!(
"{name}: scalar found {} and hybrid found {}, the checked set says {present}",
found[0], found[2]
)
.into());
}
let ext_note = if found[1] == present {
""
} else {
" EXTENTS-DISAGREES"
};
rate.push((ns[0], ns[2]));
println!(
"{name:<14} {n:>5} {:>10.1} {:>11.1} {:>10.1} {:>8.3} {:>8.3} {cold_scalar:>12.1} \
{cold_hybrid:>12.1}{ext_note}",
ns[0],
ns[1],
ns[2],
ns[0] / ns[1],
ns[0] / ns[2],
);
}
println!();
println!(
"{:<14} {:>5} {:>10} {:>10} {:>10} {:>9}",
"e2e_shape", "haves", "fetch_ms", "probe_us", "saved_pct", "objects"
);
let want_hex = tip.iter().map(|b| format!("{b:02x}")).collect::<String>();
let format = if kind == gix_hash::Kind::Sha256 {
"sha256"
} else {
"sha1"
};
for (idx, (name, n, hit_frac)) in HAVE_SHAPES.iter().copied().enumerate() {
let haves = have_list(&pool, n, hit_frac);
let pkt = |s: &str| -> Vec<u8> {
let mut v = format!("{:04x}", s.len() + 4).into_bytes();
v.extend_from_slice(s.as_bytes());
v
};
let mut req: Vec<u8> = Vec::new();
req.extend(pkt("command=fetch\n"));
req.extend(pkt(&format!("object-format={format}\n")));
req.extend_from_slice(b"0001");
req.extend(pkt("thin-pack\n"));
req.extend(pkt("no-progress\n"));
req.extend(pkt("ofs-delta\n"));
req.extend(pkt(&format!("want {want_hex}\n")));
for h in &haves {
let hex = h.iter().map(|b| format!("{b:02x}")).collect::<String>();
req.extend(pkt(&format!("have {hex}\n")));
}
req.extend(pkt("done\n"));
req.extend_from_slice(b"0000");
let mut samples: Vec<f64> = Vec::new();
let mut objects = 0u32;
let mut digest: u64 = 0;
for _ in 0..5 {
let mut input = BufReader::new(std::io::Cursor::new(req.clone()));
let mut out: Vec<u8> = Vec::new();
let t = Instant::now();
let summary = UploadPack::new(serve, kind).serve_v2(&mut input, &mut out)?;
samples.push(t.elapsed().as_secs_f64() * 1000.0);
objects = summary.objects_sent;
let mut d: u64 = 0xcbf2_9ce4_8422_2325;
for byte in &out {
d ^= u64::from(*byte);
d = d.wrapping_mul(0x0000_0100_0000_01b3);
}
digest = d;
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let fetch_ms = samples[samples.len() / 2];
let (ns_scalar, ns_hybrid) = rate[idx];
let probe_us = ns_scalar * n as f64 / 1e3;
let saved_us = (ns_scalar - ns_hybrid) * n as f64 / 1e3;
println!(
"{name:<14} {n:>5} {fetch_ms:>10.3} {probe_us:>10.3} {:>10.4} {objects:>9} \
digest={digest:016x}",
100.0 * saved_us / (fetch_ms * 1e3),
);
}
Ok(())
}
fn hex_oid(oid: &str, kind: gix_hash::Kind) -> Res<Vec<u8>> {
let raw = (0..oid.len())
.step_by(2)
.map(|i| u8::from_str_radix(&oid[i..i + 2], 16))
.collect::<Result<Vec<u8>, _>>()?;
if raw.len() != kind.len_in_bytes() {
return Err(format!(
"{oid} is {} bytes, and this repository's hash is {kind}",
raw.len()
)
.into());
}
Ok(raw)
}
fn ops_emit(engine: &str, fixture: &str, op: &str, batch: usize, mut ns: Vec<u128>) {
if ns.is_empty() {
return;
}
ns.sort_unstable();
let us = |x: u128| x as f64 / 1000.0;
let med = us(ns[ns.len() / 2]);
let mn = us(ns[0]);
let mx = us(ns[ns.len() - 1]);
println!(
"{{\"engine\":\"{engine}\",\"fixture\":\"{fixture}\",\"op\":\"{op}\",\"reps\":{},\
\"batch\":{batch},\"median_us\":{med:.3},\"min_us\":{mn:.3},\"max_us\":{mx:.3}}}",
ns.len()
);
}
fn opsprobe(repo: &Path, engine: Engine) -> Res<()> {
let root = store_root(repo, engine);
let kind = hash_kind(repo);
let store = engine.open_repo(&root, kind)?;
let serve = store.serve();
let base = serve.refs()?;
if base.is_empty() {
return Err(format!("{} holds no refs; run `prepare` first", root.display()).into());
}
let reps: usize = std::env::var("GUNNAR_OPS_REPS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5);
let batch: usize = std::env::var("GUNNAR_OPS_BATCH")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(128);
let eng = engine.name();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let fixture = repo
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let tip: Vec<u8> = base
.iter()
.find(|r| r.name == "refs/heads/main")
.and_then(|r| r.oid.clone())
.or_else(|| base.iter().find_map(|r| r.oid.clone()))
.ok_or("no ref carries an oid to probe")?;
let head_target = base
.iter()
.find(|r| r.name == "refs/heads/main")
.map(|_| "refs/heads/main".to_string())
.unwrap_or_else(|| base[0].name.clone());
let mut ns = Vec::with_capacity(reps);
for _ in 0..reps {
let t = Instant::now();
let r = serve.refs()?;
ns.push(t.elapsed().as_nanos());
if r.len() != base.len() {
return Err("refs count drifted mid-probe".into());
}
}
ops_emit(eng, &fixture, "refs", base.len(), ns);
let mut ns = Vec::with_capacity(reps);
for _ in 0..reps {
let t = Instant::now();
let h = serve.head()?;
ns.push(t.elapsed().as_nanos());
if h.is_none() {
return Err("head() returned None on a prepared store".into());
}
}
ops_emit(eng, &fixture, "head", 0, ns);
let mut ns = Vec::with_capacity(reps);
for _ in 0..reps {
let t = Instant::now();
let ok = serve.has(&tip)?;
ns.push(t.elapsed().as_nanos());
if !ok {
return Err("has(tip) was false for a ref this store holds".into());
}
}
ops_emit(eng, &fixture, "has", 0, ns);
let mut ns = Vec::with_capacity(reps);
let mut sz_from_size = None;
for _ in 0..reps {
let t = Instant::now();
let s = serve.size(&tip)?;
ns.push(t.elapsed().as_nanos());
sz_from_size = s;
if s.is_none() {
return Err("size(tip) was None".into());
}
}
ops_emit(eng, &fixture, "size", 0, ns);
let mut ns = Vec::with_capacity(reps);
let mut sz_from_header = None;
for _ in 0..reps {
let t = Instant::now();
let h = serve.header(&tip)?;
ns.push(t.elapsed().as_nanos());
match h {
Some((_, sz)) => sz_from_header = Some(sz),
None => return Err("header(tip) was None".into()),
}
}
ops_emit(eng, &fixture, "header", 0, ns);
let mut ns = Vec::with_capacity(reps);
for _ in 0..reps {
let t = Instant::now();
let r = serve.read(&tip)?;
ns.push(t.elapsed().as_nanos());
match r {
Some((_, bytes)) => {
if Some(bytes.len() as u64) != sz_from_header {
return Err(format!(
"read() gave {} bytes but header() said {:?}",
bytes.len(),
sz_from_header
)
.into());
}
}
None => return Err("read(tip) was None".into()),
}
}
ops_emit(eng, &fixture, "read", 0, ns);
if sz_from_size != sz_from_header {
return Err(
format!("size()={sz_from_size:?} disagrees with header()={sz_from_header:?}").into(),
);
}
let mut ns = Vec::with_capacity(reps);
for _ in 0..reps {
let t = Instant::now();
let _ = serve.extents(&[&tip])?;
ns.push(t.elapsed().as_nanos());
}
ops_emit(eng, &fixture, "extents", 0, ns);
let mut ns = Vec::with_capacity(reps);
let mut reach_err = None;
for _ in 0..reps {
let t = Instant::now();
match serve.reachable(&[&tip], &[]) {
Ok(v) => {
ns.push(t.elapsed().as_nanos());
if v.is_empty() {
reach_err = Some("reachable(tip,[]) returned an empty frontier".to_string());
break;
}
}
Err(e) => {
reach_err = Some(e.to_string());
break;
}
}
}
match reach_err {
None => ops_emit(eng, &fixture, "reachable", 0, ns),
Some(e) => println!(
"{{\"engine\":\"{eng}\",\"fixture\":\"{fixture}\",\"op\":\"reachable\",\"error\":{:?}}}",
e
),
}
let frontier: Vec<Vec<u8>> = serve.reachable(&[&tip], &[]).unwrap_or_default();
if !frontier.is_empty() {
let oids: Vec<&[u8]> = frontier.iter().map(|o| o.as_slice()).collect();
let mut single_total: u64 = 0;
for o in &oids {
single_total += serve.size(o)?.ok_or("size() None for a reachable object")?;
}
let mut ns = Vec::with_capacity(reps);
for _ in 0..reps {
let t = Instant::now();
let szs = serve.sizes(&oids)?;
ns.push(t.elapsed().as_nanos());
if szs.len() != oids.len() {
return Err(format!(
"sizes() returned {} entries for {} oids",
szs.len(),
oids.len()
)
.into());
}
let bulk_total: u64 = szs
.iter()
.map(|s| s.ok_or("sizes() had a None for a reachable object"))
.sum::<std::result::Result<u64, _>>()?;
if bulk_total != single_total {
return Err(format!(
"bulk sizes() total {bulk_total} disagrees with per-object size() total {single_total}"
)
.into());
}
}
ops_emit(eng, &fixture, "batch_check", frontier.len(), ns);
let mut ns = Vec::with_capacity(reps);
for _ in 0..reps {
let mut total: u64 = 0;
let t = Instant::now();
for o in &oids {
match serve.read(o)? {
Some((_, b)) => total += b.len() as u64,
None => return Err("cat_file_batch: read() None for a reachable object".into()),
}
}
ns.push(t.elapsed().as_nanos());
if total == 0 {
return Err("cat_file_batch read 0 bytes over a non-empty frontier".into());
}
if total != single_total {
return Err(format!(
"cat_file_batch byte total {total} disagrees with sizes() total {single_total}"
)
.into());
}
}
ops_emit(eng, &fixture, "cat_file_batch", frontier.len(), ns);
}
let mut ns = Vec::with_capacity(reps);
for _ in 0..reps {
let t = Instant::now();
serve.set_head(&head_target)?;
ns.push(t.elapsed().as_nanos());
let h = serve.head()?.ok_or("head() None after set_head")?;
let ok = h.symref_target.as_deref() == Some(head_target.as_str()) || h.oid.is_some();
if !ok {
return Err("set_head did not read back".into());
}
}
ops_emit(eng, &fixture, "set_head", 0, ns);
let mut ns = Vec::with_capacity(reps);
for i in 0..reps {
let name = format!("refs/heads/opsprobe/single/{eng}/{nonce}/{i}");
let t = Instant::now();
serve.update_ref(&name, None, Some(&tip))?;
ns.push(t.elapsed().as_nanos());
}
ops_emit(eng, &fixture, "update_ref", 0, ns);
let mut wrong = tip.clone();
wrong[0] ^= 0xff;
let neg = serve.update_ref(&head_target, Some(&wrong), Some(&tip));
if neg.is_ok() {
return Err("update_ref accepted a compare-and-swap with a wrong old value".into());
}
let mut ns = Vec::with_capacity(reps);
for i in 0..reps {
let names: Vec<String> = (0..batch)
.map(|j| format!("refs/heads/opsprobe/batch/{eng}/{nonce}/{i}/{j}"))
.collect();
let edits: Vec<gunnar_store::git_storage_trait::RefCas> = names
.iter()
.map(|n| gunnar_store::git_storage_trait::RefCas {
name: n.clone(),
old: None,
new: Some(&tip),
peeled: None,
})
.collect();
let before = serve.refs()?.len();
let t = Instant::now();
serve.put_refs_cas(&edits)?;
ns.push(t.elapsed().as_nanos());
let after = serve.refs()?.len();
if after != before + batch {
return Err(format!(
"put_refs_cas of {batch} refs moved the count by {}",
after as i64 - before as i64
)
.into());
}
}
ops_emit(eng, &fixture, "put_refs_cas", batch, ns);
Ok(())
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let mode = args.get(1).cloned().unwrap_or_default();
let repo = PathBuf::from(args.get(2).cloned().unwrap_or_default());
if std::env::var_os("GUNNAR_BENCH_NO_PACK_COPY").is_some() && mode != "prepare" {
eprintln!(
"git-store-serve: GUNNAR_BENCH_NO_PACK_COPY is set, and this server has no \
NoPackCopy shim to honour it with. Serving anyway would report a switched run's \
label over an unswitched server."
);
std::process::exit(1);
}
let engine = match Engine::selected() {
Ok(e) => e,
Err(err) => {
eprintln!("git-store-serve: {err:#}");
std::process::exit(1);
}
};
let result = match mode.as_str() {
"prepare" => prepare(&repo, engine),
"opsprobe" => opsprobe(&repo, engine),
"havebatch" => have_batch(&repo, engine),
"has" => has(
&repo,
args.get(3).map(String::as_str).unwrap_or_default(),
engine,
),
"git-upload-pack" | "git-receive-pack" => serve(&mode, &repo, engine),
other => Err(format!(
"usage: {} prepare|opsprobe|has|havebatch|git-upload-pack|git-receive-pack <repo> \
(got {other:?}); \
this binary carries {}",
args.first()
.map(String::as_str)
.unwrap_or("git-store-serve"),
Engine::compiled_list(),
)
.into()),
};
gunnar_server::storage::shutdown();
if let Err(err) = result {
eprintln!("git-store-serve: {err}");
std::process::exit(1);
}
}