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

nordisk/gunnar

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