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

nordisk/edda

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