bench/examples/indexdepth-probe.rs
//! What chain depth costs **push verification**, which is the one place a
//! delta chain was expected to be expensive.
//!
//! `index_thin_pack` is 74.6–90.9% of a push's wall clock, and it is the only
//! path in gunnar that resolves an incoming pack's deltas in order to verify
//! it. This times [`gunnar_pack::index_pack`] over a **ladder**: the same
//! 13 431 objects, packed by stock `git pack-objects` at `--depth=1,2,4,8,16,
//! 32,50`, plus one pack of the same objects with **no deltas at all**.
//!
//! ```text
//! indexdepth-probe <pack> [<pack>...]
//! ```
//!
//! Every pack in the ladder holds the identical object set, so the only thing
//! that varies is how those objects are encoded. Bytes differ — that is the
//! point, a shallow chain costs wire bytes — so the table reports both wall
//! clock and MB/s, and the MB/s column is the one that says whether depth
//! itself costs anything once the byte count is divided out.
use std::io::BufReader;
use std::time::Instant;
use anyhow::{ensure, Context, Result};
use gunnar_pack::{index_pack, IndexOptions};
const HASH: gix_hash::Kind = gix_hash::Kind::Sha1;
const RUNS: usize = 5;
fn main() -> Result<()> {
let packs: Vec<std::path::PathBuf> = std::env::args_os().skip(1).map(Into::into).collect();
ensure!(!packs.is_empty(), "usage: indexdepth-probe <pack>...");
println!(
"{:>14} {:>11} {:>7} {:>9} {:>9} {:>9} {:>9} {:>9}",
"pack", "bytes", "objects", "index_s", "iter_s", "resolve_s", "resolve%", "us/obj"
);
let mut first_objects: Option<u32> = None;
for p in &packs {
let bytes = std::fs::metadata(p)?.len();
let mut best = f64::MAX;
let mut objects = 0u32;
for _ in 0..RUNS {
let file = std::fs::File::open(p)?;
let mut r = BufReader::with_capacity(1 << 20, file);
let t = Instant::now();
// `directory: None` — index and verify the stream, keep nothing.
// That is exactly the work `receive-pack` does before it decides
// whether to accept, minus the write of the `.idx`.
let out = index_pack(&mut r, None, &IndexOptions::new(HASH), None)
.with_context(|| format!("indexing {}", p.display()))?;
best = best.min(t.elapsed().as_secs_f64());
objects = out.num_objects;
}
// ── measured, not asserted ─────────────────────────────────────────
// The ladder's whole premise is that every pack holds the same objects.
// If one does not, its row is a comparison of two different workloads
// wearing the same axis label, and nothing here would say so.
match first_objects {
None => first_objects = Some(objects),
Some(n) => ensure!(
n == objects,
"{} indexed {objects} objects, but the first pack in the ladder had {n}. \
These are not the same object set and the rows are not comparable.",
p.display()
),
}
ensure!(objects > 0, "{} indexed zero objects", p.display());
// ── phase 1 on its own ─────────────────────────────────────────────
//
// `BytesToEntriesIter` is the front half of `index_pack` and only the
// front half: it reads every entry header, decompresses every entry's
// own bytes, CRC32s them and hashes the stream — and it applies **no**
// deltas. Subtracting it from the full index leaves the delta tree
// build, the delta application and the hashing of the resolved objects,
// which is the "chain resolution" half of the question.
let mut iter_best = f64::MAX;
for _ in 0..RUNS {
let file = std::fs::File::open(p)?;
let r = BufReader::with_capacity(1 << 20, file);
let t = Instant::now();
let it = gix_pack::data::input::BytesToEntriesIter::new_from_header(
r,
gix_pack::data::input::Mode::Verify,
gix_pack::data::input::EntryDataMode::Crc32,
HASH,
)
.map_err(|e| anyhow::anyhow!("reading the pack header: {e}"))?;
let mut seen = 0u32;
for e in it {
e.map_err(|e| anyhow::anyhow!("reading a pack entry: {e}"))?;
seen += 1;
}
ensure!(
seen == objects,
"the entry iterator saw {seen} entries but the index found {objects} objects in {}",
p.display()
);
iter_best = iter_best.min(t.elapsed().as_secs_f64());
}
let resolve = best - iter_best;
println!(
"{:>14} {:>11} {:>7} {:>9.3} {:>9.3} {:>9.3} {:>8.1}% {:>9.2}",
p.file_stem().unwrap_or_default().to_string_lossy(),
bytes,
objects,
best,
iter_best,
resolve,
100.0 * resolve / best,
best * 1e6 / f64::from(objects)
);
}
Ok(())
}