crates/gunnar-pack/src/oidprobe.rs
//! Counting every oid → pack-offset resolution a request makes, at the seams
//! gunnar owns — **and nothing at all** without the `oidprobe` feature.
//!
//! It answers one question: *how often does a real fetch resolve an oid to a
//! pack offset, and where*. That is a diagnostic, not a feature, so the counters
//! are behind a cargo feature that no gunnar crate turns on and only `bench/`
//! asks for — the same shape, and for the same reason, as `gunnar-wire/stages`.
//! With it off [`bump`] is an empty inline function and the statics are not
//! there to be loaded: the default build has **no probe**, rather than a probe
//! that is switched off.
//!
//! A relaxed `fetch_add` is on the order of a nanosecond against a lookup of
//! fifty, so leaving it in unconditionally would be a 2% tax on the exact path
//! `.nornir/fanout16-reasoning.md` was written to measure.
//!
//! Every counter names a **call site**, not an implementation. `gix_odb_*` are
//! calls that enter `gix-odb`, each of which does its own `.idx` fanout+bsearch
//! inside gix and which gunnar therefore cannot front without forking gix.
//! `bitmap_*` are the two that go through gunnar's own
//! `gunnar_store::PackBitmap`, holding a `gix_pack::index::File` gunnar opened
//! itself — the only ones a 16-bit fanout could actually sit in front of.
#[cfg(feature = "oidprobe")]
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
/// The counters, when there are any.
#[cfg(feature = "oidprobe")]
macro_rules! counters {
($($(#[$m:meta])* $name:ident),* $(,)?) => {
$($(#[$m])* pub static $name: AtomicU64 = AtomicU64::new(0);)*
/// Every counter, by name, as it stands.
pub fn snapshot() -> Vec<(&'static str, u64)> {
vec![$((stringify!($name), $name.load(Relaxed))),*]
}
/// Back to zero.
pub fn reset() {
$($name.store(0, Relaxed);)*
}
};
}
#[cfg(feature = "oidprobe")]
counters! {
/// `ObjectStore::contains` → `gix_object::Exists::exists`.
GIX_ODB_CONTAINS,
/// `ObjectStore::contains_without_rescan` → `gix_object::Exists::exists`,
/// i.e. the negotiation's membership test, which answers a miss instead of
/// re-scanning `objects/pack` to disbelieve it.
///
/// Counted **separately from `GIX_ODB_CONTAINS`, not as well as it**, so the
/// split between the two call sites is visible rather than inferred. That
/// matters here more than usual: the intervention is defined to leave every
/// answer, every wire byte and every call *total* unchanged, so the only
/// thing that can witness which of the two paths a request took is which of
/// these two counters moved.
GIX_ODB_CONTAINS_NO_RESCAN,
/// `ObjectStore::read` → `gix_object::Find::try_find`.
GIX_ODB_READ,
/// `ObjectStore::header` → `gix_object::FindHeader::try_header`.
GIX_ODB_HEADER,
/// `gix_pack::Find::contains` on `PackedObjects`.
GIX_PACK_CONTAINS,
/// `gix_pack::Find::try_find_cached` on `PackedObjects`.
GIX_PACK_TRY_FIND_CACHED,
/// `gix_pack::Find::location_by_oid` on `PackedObjects` — the one
/// `iter_from_counts` calls once per selected object.
GIX_PACK_LOCATION_BY_OID,
/// `gix_pack::Find::entry_by_location` — an OFFSET lookup, no oid search.
/// Counted so the ratio of oid-keyed to offset-keyed work is visible.
GIX_PACK_ENTRY_BY_LOCATION,
/// `PackBitmap::pack_position` → `index::File::lookup`.
BITMAP_PACK_POSITION,
/// `PackBitmap::BITMAP_FOR_COMMIT` → `index::File::lookup`.
BITMAP_FOR_COMMIT,
// ── the census counters ─────────────────────────────────────────────────
//
// The nine above answer "how often does a request resolve an oid". These
// answer "what else does a request ask gix for", which is the other half of
// `.nornir/gix-census-reasoning.md` and the half that turned out to hold
// the cycles. They are counted at the same seams and by the same macro, so
// one `snapshot()` is the whole census (LAW 5).
/// `gix_pack::output::entry::iter_from_counts`, once per pack written.
/// The entry point that owns 75–98% of a fetch's CPU.
GIX_PACK_ITER_FROM_COUNTS,
/// Objects `iter_from_counts` copied out of a pack **verbatim** — no
/// inflate, no deflate. `P-001`'s applied output.
GIX_PACK_COPIED,
/// Objects it inflated and deflated again instead, plus the ones gunnar's
/// thin veto took back off the copied side. The zlib bill in one number.
GIX_PACK_RECOMPRESSED,
/// The sum of `output::Entry::decompressed_size` over every entry the
/// generator emitted: the inflated volume of the pack gunnar just served.
///
/// A count alone cannot tell one 40 MB blob from four hundred 100-byte
/// trees, and both the inflate and the deflate bills are in the bytes. gix
/// does not report a byte figure of its own — `iter_from_counts::Outcome`
/// carries four object counts and nothing else — so this is summed at
/// gunnar's own loop over the entries.
GIX_PACK_ENTRY_BYTES,
/// Entries `iter_from_counts` emitted as a whole `Kind::Base`. On a
/// **copy** path this is not "an object with no delta": it is every object
/// whose pack entry was itself a base, plus every object gix expanded
/// because it could not name the base it had.
GIX_PACK_KIND_BASE,
/// Entries emitted as `Kind::DeltaRef` — a delta whose base travels in the
/// same pack. The cheap case: header rewritten, payload copied.
GIX_PACK_KIND_DELTA_REF,
/// Thin deltas gunnar's own veto took back: gix produced a `DeltaOid` and
/// `client_has` would not vouch for the base, so the object was written
/// whole instead.
///
/// Counted separately because the veto's fallback read goes through
/// `PackedObjects::handle` **directly** (`copy.rs:451`) rather than through
/// the `gix_pack::Find` impl, so `GIX_PACK_TRY_FIND_CACHED` does not see
/// it. Without this counter a fetch reports `TRY_FIND_CACHED 0` beside
/// `RECOMPRESSED 182` and the obvious reading — "the veto never fired" —
/// is exactly backwards.
GIX_PACK_THIN_VETOED,
/// Entries emitted as `Kind::DeltaOid` — a delta gix named its base for by
/// **object id** rather than by a position inside the pack. Non-zero only
/// when the caller vouched for the receiver's contents, so it is also the
/// check that `allow_thin_pack` really was on: a fetch that reports zero
/// here served a non-thin pack whatever the request asked for.
///
/// **It does not mean the base is outside the transfer**, and reading it
/// that way cost a fetch on 2026-08-05. gix emits this whenever it could
/// not re-point the delta inside the pack it is writing, which on a
/// multi-pack store includes bases that *are* being sent — the base
/// resolved to a copy in one pack while the delta was copied out of
/// another. Whether the receiver holds the base is `client_has`'s question
/// and nothing here answers it.
GIX_PACK_KIND_DELTA_OID,
/// `gix_pack::Bundle::write_to_directory` — the whole of `index_thin_pack`,
/// once per push. Stream decode, CRC, delta-tree resolution, thin-base
/// completion and `.idx` writing are all inside this one call.
GIX_PACK_BUNDLE_WRITE,
/// `gix_pack::index::File::at` — opening an `.idx` to read it back, which
/// the push path does once more after `Bundle::write_to_directory` to list
/// what arrived.
GIX_PACK_INDEX_OPEN,
/// `gix_pack::index::File::iter` — a full pass over every entry of an
/// `.idx`. Cheap per entry and O(pack) per call, which is the shape worth
/// counting.
GIX_PACK_INDEX_ITER,
/// `gix_object::Find::try_find` reaching gunnar from **inside** gix: the
/// thin-base lookups `Bundle::write_to_directory` makes against the
/// receiving store while completing a pushed pack.
GIX_OBJECT_FIND_FROM_GIX,
/// `gix_ref` reads: `file::Store::iter`, `try_find` and `find`. Being
/// replaced by `ArchiveRefStore` in another lane; counted so the census can
/// say what that lane is worth rather than assert it.
GIX_REF_READ,
/// `gix_ref` writes: one `file::Transaction::prepare` + `commit`.
GIX_REF_TXN,
}
/// Add one to `c`.
#[cfg(feature = "oidprobe")]
#[inline(always)]
pub fn bump(c: &AtomicU64) {
c.fetch_add(1, Relaxed);
}
/// Add `n` to `c`, for the counters that tally a batch rather than a call.
///
/// `GIX_PACK_COPIED` and `GIX_PACK_RECOMPRESSED` are gix's own per-pack totals,
/// corrected by gunnar's thin veto. Bumping them one at a time from inside the
/// entry loop would need a probe on the hot path to report a number that is
/// already sitting in the outcome.
#[cfg(feature = "oidprobe")]
#[inline(always)]
pub fn bump_by(c: &AtomicU64, n: u64) {
c.fetch_add(n, Relaxed);
}
/// The uninstrumented [`bump`]: takes nothing, does nothing, compiles to
/// nothing. Call sites read identically in both builds, so the instrumented and
/// uninstrumented serve paths cannot come to differ.
#[cfg(not(feature = "oidprobe"))]
#[inline(always)]
pub fn bump(_c: &()) {}
/// The uninstrumented [`bump_by`].
#[cfg(not(feature = "oidprobe"))]
#[inline(always)]
pub fn bump_by(_c: &(), _n: u64) {}
/// Every counter, by name, as it stands. Empty without the feature.
#[cfg(not(feature = "oidprobe"))]
pub fn snapshot() -> Vec<(&'static str, u64)> {
Vec::new()
}
/// Back to zero. Nothing to zero without the feature.
#[cfg(not(feature = "oidprobe"))]
pub fn reset() {}
/// The call-site names, when the counters are not compiled in.
///
/// A call site reads `bump(&GIX_ODB_READ)` in both builds; without the feature
/// these are the units it passes and `bump` discards.
#[cfg(not(feature = "oidprobe"))]
mod off {
macro_rules! placeholders {
($($name:ident),* $(,)?) => {
$(
/// A call site's name with no counter behind it.
pub static $name: () = ();
)*
};
}
placeholders! {
GIX_ODB_CONTAINS,
GIX_ODB_CONTAINS_NO_RESCAN,
GIX_ODB_READ,
GIX_ODB_HEADER,
GIX_PACK_CONTAINS,
GIX_PACK_TRY_FIND_CACHED,
GIX_PACK_LOCATION_BY_OID,
GIX_PACK_ENTRY_BY_LOCATION,
BITMAP_PACK_POSITION,
BITMAP_FOR_COMMIT,
GIX_PACK_ITER_FROM_COUNTS,
GIX_PACK_COPIED,
GIX_PACK_RECOMPRESSED,
GIX_PACK_ENTRY_BYTES,
GIX_PACK_KIND_BASE,
GIX_PACK_KIND_DELTA_REF,
GIX_PACK_KIND_DELTA_OID,
GIX_PACK_THIN_VETOED,
GIX_PACK_BUNDLE_WRITE,
GIX_PACK_INDEX_OPEN,
GIX_PACK_INDEX_ITER,
GIX_OBJECT_FIND_FROM_GIX,
GIX_REF_READ,
GIX_REF_TXN,
}
}
#[cfg(not(feature = "oidprobe"))]
pub use off::*;