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/transportbuf-probe.rs

//! **What the transport's buffer costs, both arms, one process, one store.**
//!
//! ```text
//! transportbuf-probe <repo.git> [repo.git ...]
//! ```
//!
//! `gunnar-cli`'s `WireTransport::serve_blocking` wraps the stream it is given
//! in a `BufWriter` before handing it to `gunnar-wire`. Until 2026-08-05 that
//! was `BufWriter::new`, whose capacity is **8 KiB** — and a sideband packet is
//! **65 520**. So the buffer did not buffer: `write_sideband` writes a 4-byte
//! length, a 1-byte band and then the payload, and on the payload `BufWriter`
//! finds 65 515 bytes that do not fit, dumps the 5 bytes it is holding as a
//! write of its own, and passes the payload straight through. Two writes per
//! packet, **one of them five bytes**.
//!
//! That is not a syscall count on this path. `WireTransport` runs the state
//! machine on a `spawn_blocking` thread writing into a `SyncIoBridge` over a
//! `tokio::io::duplex`, so **one write is one hand-off**: a `block_on`, a waker,
//! and, whenever the reader is parked, a futex pair. Under `gunnar-http` the
//! reader is hyper's body poll; under `gunnar-ssh` it is the russh channel.
//!
//! # Why this is a probe and not a timing
//!
//! The quantity is **deterministic**. The two arms serve the same objects and
//! produce byte-identical output; the only thing that differs is how that
//! output was handed over. A wall clock on oden has run at load 3–270 and could
//! not resolve this at all — the live 16-client A/B on 2026-08-05 measured
//! −26% voluntary context switches per clone and could not separate the wall
//! times, which is exactly the shape of a real change hiding under a loaded
//! box. So this counts the hand-offs directly, off one store, with no socket
//! and no scheduler in the answer.
//!
//! # The counter that decides it, and the red arm
//!
//! `writes` is the applied output. Two guards make it impossible to report a
//! win having measured nothing:
//!
//!  * the 8 KiB arm **must** exhibit a write of 8 bytes or fewer, or the item
//!    this probe exists to measure is not present in that repository and no
//!    ratio off it means anything;
//!  * the two arms **must** produce the same digest over the same object count,
//!    or they are not two framings of one answer but two different answers.

use anyhow::{bail, ensure, Context, Result};
use gix_hash::ObjectId;
use gunnar_bench::stages;
use gunnar_store::fs::FsStore;
use gunnar_wire::upload_pack::UploadPack;
use gunnar_wire::TRANSPORT_BUFFER_BYTES;
use std::io::Write;

/// The stream underneath the `BufWriter`: it remembers how it was called, not
/// what it was given.
///
/// The bytes are identical in both arms by construction, so a sink that
/// compared them would pass at every capacity. What differs is the **sequence
/// of writes**, and on this transport each one is a thread hand-off.
#[derive(Default)]
struct Handoffs {
    writes: u64,
    bytes: u64,
    smallest: Option<usize>,
    /// Writes of exactly five bytes: a 4-byte pkt-line length plus the band
    /// byte, handed over **alone**. That is the defect itself — nothing in the
    /// protocol asks for a 5-byte write, it is what `BufWriter` dumps when the
    /// payload behind the header does not fit it — so counting them is the
    /// difference between measuring this change and measuring a buffer size.
    five_byte: u64,
    /// FNV-1a over everything written, so the two arms can be proved to have
    /// produced the same answer.
    digest: u64,
}

impl Handoffs {
    fn new() -> Handoffs {
        Handoffs {
            digest: 0xcbf2_9ce4_8422_2325,
            ..Handoffs::default()
        }
    }
}

impl Write for Handoffs {
    fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
        self.writes += 1;
        self.bytes += data.len() as u64;
        self.smallest = Some(self.smallest.map_or(data.len(), |m| m.min(data.len())));
        if data.len() == 5 {
            self.five_byte += 1;
        }
        for byte in data {
            self.digest ^= u64::from(*byte);
            self.digest = self.digest.wrapping_mul(0x0000_0100_0000_01b3);
        }
        Ok(data.len())
    }
    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

/// `getrusage(RUSAGE_SELF)`: the whole process, every thread, in nanoseconds.
/// Reported beside the write count and **not** used to decide anything — a
/// scheduler figure off a loaded box is a statement about the box.
fn process_cpu_ns() -> u64 {
    // SAFETY: `getrusage` writes a `struct rusage` through the pointer and
    // reads nothing else. The zeroed value is a valid `rusage`.
    let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
    if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } != 0 {
        return 0;
    }
    let ns = |t: libc::timeval| (t.tv_sec as u64) * 1_000_000_000 + (t.tv_usec as u64) * 1_000;
    ns(usage.ru_utime) + ns(usage.ru_stime)
}

struct Arm {
    writes: u64,
    bytes: u64,
    smallest: usize,
    five_byte: u64,
    digest: u64,
    objects: u64,
    cpu_ns: u64,
}

/// One clone served through a `BufWriter` of `capacity`, exactly as
/// `WireTransport::serve_blocking` builds one.
fn serve(repo: &std::path::Path, want: &ObjectId, capacity: usize) -> Result<Arm> {
    let kind = gunnar_store::fs::stored_hash_kind(repo)?;
    let store = FsStore::open_or_init(repo, kind)?;
    let objects = store.objects();
    let refs = store.refs();
    let format = match kind {
        gix_hash::Kind::Sha1 => "sha1",
        _ => "sha256",
    };
    let upload = UploadPack::new(&objects, &refs);
    let mut input = std::io::BufReader::new(std::io::Cursor::new(stages::fetch_request(
        want,
        &[],
        format,
    )));

    let mut sink = Handoffs::new();
    let cpu0 = process_cpu_ns();
    let summary = {
        // The same two lines `serve_blocking` runs, and the only difference
        // between the arms is the number on this one.
        let mut out = std::io::BufWriter::with_capacity(capacity, &mut sink);
        let summary = upload.serve_v2(&mut input, &mut out)?;
        out.flush()?;
        summary
    };
    let cpu_ns = process_cpu_ns().saturating_sub(cpu0);
    ensure!(
        summary.objects_sent > 0,
        "{repo:?}: the probe served ZERO objects, and two empty answers are framed identically"
    );
    Ok(Arm {
        writes: sink.writes,
        bytes: sink.bytes,
        smallest: sink.smallest.unwrap_or(0),
        five_byte: sink.five_byte,
        digest: sink.digest,
        objects: u64::from(summary.objects_sent),
        cpu_ns,
    })
}

/// The capacity the transport used to have. Named here rather than imported,
/// because it is `std`'s `DEFAULT_BUF_SIZE` and not gunnar's to change — it is
/// the value this probe exists to measure against.
const OLD_CAPACITY: usize = 8 * 1024;

fn compare(repo: &std::path::Path) -> Result<()> {
    let want = tip(repo)?;
    let old = serve(repo, &want, OLD_CAPACITY)?;
    let new = serve(repo, &want, TRANSPORT_BUFFER_BYTES)?;

    // RED: no 5-byte write means `BufWriter` was never defeated here, so
    // there is no item and the ratio below would be a ratio of nothing. A
    // 4-byte write is NOT enough to establish it — a flush-pkt is four bytes
    // and both arms emit those, so an arm counting "small writes" would find
    // the item in a repository that has none.
    ensure!(
        old.five_byte > 0,
        "{repo:?}: the {OLD_CAPACITY}-byte arm never handed over a bare 5-byte pkt-line header, \
         so `BufWriter` was not defeated here and this repository has none of the item. Any \
         ratio taken off it is a ratio of protocol chatter."
    );
    ensure!(
        new.five_byte == 0,
        "{repo:?}: the {TRANSPORT_BUFFER_BYTES}-byte arm still handed over {} bare pkt-line \
         header(s) — the payload behind them is not fitting the buffer, which is the whole \
         defect",
        new.five_byte
    );
    // The two arms are two framings of ONE answer, or they are not comparable.
    ensure!(
        old.digest == new.digest && old.bytes == new.bytes && old.objects == new.objects,
        "{repo:?}: the arms served different bytes — {} objects/{} bytes/{:016x} against {} \
         objects/{} bytes/{:016x}. A buffer capacity cannot change the answer, so one of these \
         arms is not serving what it says.",
        old.objects,
        old.bytes,
        old.digest,
        new.objects,
        new.bytes,
        new.digest
    );

    println!(
        "{{\"repo\":{:?},\"objects\":{},\"bytes\":{},\
         \"writes_8k\":{},\"writes_{}\":{},\"ratio\":{:.3},\
         \"five_byte_8k\":{},\"five_byte_new\":{},\
         \"smallest_8k\":{},\"smallest_new\":{},\
         \"cpu_ns_8k\":{},\"cpu_ns_new\":{}}}",
        repo.file_name().unwrap_or_default().to_string_lossy(),
        old.objects,
        old.bytes,
        old.writes,
        TRANSPORT_BUFFER_BYTES,
        new.writes,
        old.writes as f64 / new.writes.max(1) as f64,
        old.five_byte,
        new.five_byte,
        old.smallest,
        new.smallest,
        old.cpu_ns,
        new.cpu_ns,
    );
    eprintln!(
        "{:<28} obj {:>7}  wire {:>10}  hand-offs {:>7} → {:>6} ({:.2}x)  bare 5-byte headers \
         {:>6} → {:>3}",
        repo.file_name().unwrap_or_default().to_string_lossy(),
        old.objects,
        old.bytes,
        old.writes,
        new.writes,
        old.writes as f64 / new.writes.max(1) as f64,
        old.five_byte,
        new.five_byte,
    );
    Ok(())
}

/// The tip to clone. A clone and not a fetch: the buffer is about the packfile,
/// and a negotiated fetch off a 40-commit `have` sends too little pack for the
/// framing to be the thing being measured.
fn tip(repo: &std::path::Path) -> Result<ObjectId> {
    let out = std::process::Command::new("git")
        .args(["-C", &repo.to_string_lossy(), "rev-list", "-n", "1", "HEAD"])
        .output()
        .context("running git rev-list")?;
    ensure!(
        out.status.success(),
        "git rev-list failed in {repo:?}: {}",
        String::from_utf8_lossy(&out.stderr).trim()
    );
    let listing = String::from_utf8_lossy(&out.stdout);
    let id = listing
        .lines()
        .find_map(|l| ObjectId::from_hex(l.trim().as_bytes()).ok())
        .with_context(|| format!("{repo:?} has no commits"))?;
    Ok(id)
}

fn main() -> Result<()> {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.is_empty() {
        bail!("transportbuf-probe <repo.git> [repo.git ...]");
    }
    let mut old_total = 0u64;
    let mut new_total = 0u64;
    for path in &args {
        let repo = std::path::PathBuf::from(path);
        compare(&repo)?;
        let want = tip(&repo)?;
        old_total += serve(&repo, &want, OLD_CAPACITY)?.writes;
        new_total += serve(&repo, &want, TRANSPORT_BUFFER_BYTES)?.writes;
    }
    eprintln!(
        "── {} repositories: {} hand-offs → {} ({:.2}x)",
        args.len(),
        old_total,
        new_total,
        old_total as f64 / new_total.max(1) as f64
    );
    Ok(())
}