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

nordisk/gunnar

bench/src/engine.rs

//! **The engine seam, and why `bench/` no longer contains one.**
//!
//! # The claim this module makes true
//!
//! `examples/nornir-bench.rs`'s store-seam comment has said the same thing since
//! it was written, and it is right:
//!
//! > `GUNNAR_BENCH_GUNNAR_SERVER=<path>` swaps the **executable** that serves the
//! > gunnar side […] **bench/ therefore stays znippy-free: it knows a path, not
//! > a crate.**
//!
//! **It was false as written.** `bench/Cargo.toml` carried a plain,
//! non-optional a path dependency on the deleted engine crate, and
//! the old znippy engine crate path-depends on four znippy crates — `znippy-common`,
//! `znippy-compress`, `znippy-plugin-git`, `znippy-zoomies`. Cargo reads a path
//! dependency's manifest at **resolve** time, so a sibling znippy checkout was a
//! hard requirement of building **any** of the 49 arms, including
//! `pack_deflate`, `pack_trailer_hash`, `capabilities` and `oid_index`, none of
//! which touch a store at all. That is precisely the cost the seam exists to
//! avoid, spent anyway, and it took two independent surveys before anyone said so
//! out loud.
//!
//! The declaration is gone and so are the call sites. What is left is this
//! module: `bench/` names **no engine crate**, and the only way an engine gets
//! into a measurement is as a path on `$PATH`-shaped environment variables.
//!
//! # There are TWO seams, and they are not the same seam
//!
//! Confusing them produced a wrong number that looked exactly like a right one,
//! so they are named here once and referred to by number everywhere else.
//!
//! | # | variable | mechanism | serves |
//! |---|---|---|---|
//! | **1** | `GUNNAR_BENCH_GUNNAR_SERVER` | an executable spawned with git's `ext::` argv (`[exe, <service>, <repo>]`) over a pipe, plus an `<exe> prepare <repo>` before the clock starts | `vs_git_clone`, `vs_git_clone_sha256`, `vs_git_push`, `vs_git_fetch_divergent`, `vs_git_clone_concurrent` |
//! | **2** | `GUNNAR_BENCH_FORGE_BINARY` | a whole server binary bind-mounted into a podman container and started with `serve --http-bind` (`examples/forge/mod.rs`, `gunnar_binary()`) | the eight `vs_forge_*` arms |
//!
//! **Seam 2 ignores seam 1 entirely.** A driver that sets only seam 1 and runs
//! with a `gunnar.` filter runs both columns of the eight forge arms against the
//! *same* binary and publishes them under two engine labels.
//! `bench/two-engine.sh` refuses to start in that configuration; this note is
//! the second copy of the reason, kept because the script can be replaced and
//! this module cannot.
//!
//! # What is NOT reachable through either seam
//!
//! Three groups, and being explicit about them is the point of this module —
//! each used to open an engine in-process and now cannot.
//!
//! * **The concurrent arms' daemon.** `h2h_serve_daemon` and
//!   `scale_serve_daemon` are in-process servers holding **one** store across
//!   many connections, because gunnar cannot be run process-per-connection at
//!   all: the store takes a cross-process exclusive lock. An alternate binary
//!   cannot **replace** them.
//!
//!   ⚠ It does not follow that seam 1 is unwelcome there, and until 2026-08-14
//!   this list said the opposite: `h2h_serve_daemon` *asserted seam 1 was unset*
//!   "rather than quietly measuring the wrong backend under the right label".
//!   That argument died with `FsStore`. [`open_repo`] routes through
//!   `Engine::selected()`, so the daemon and seam 1 read the same
//!   `GUNNAR_ENGINE` and there is no label left to get wrong — while the
//!   fixture the daemon opened is **git-native**, which only the gix engine can
//!   read. On znippy `open_repo` did not fail; it created an empty store inside
//!   the carve and served zero objects, which is why
//!   `gunnar.vs_git_clone_concurrent` was green on gix and red on all four
//!   znippy columns of every sweep.
//!
//!   So the daemon now **requires** seam 1 and uses it for the one thing a
//!   subprocess genuinely can do here: `prepare` the git-native fixture into
//!   `<repo>.gunnar-<engine>`, which the daemon then opens and serves in
//!   process. The binary does not replace the daemon; it builds the store the
//!   daemon holds.
//! * **The fixture builders and in-process probes** — the multi-pack arms, the
//!   `.idx` lookup probes, `stages`, `have_lookup`, `readside`. They reach past
//!   any wire protocol and ask the store directly, which is the whole question
//!   they exist to answer, and no subprocess can be asked it.
//! * **Everything downstream of both**, which is currently also blocked on the
//!   reading contract (`GitServe`) that
//!   `gunnar-wire` is being retargeted onto.
//!
//! For all of them the honest answer today is **a refusal with a reason**, which
//! is what [`open_repo`] is. A benchmark that quietly substitutes a different
//! store is worse than one that does not run: it produces a full, plausible
//! table over the wrong subject.

use std::path::Path;
use std::sync::Arc;

use anyhow::{bail, Context, Result};
use gunnar_store::RepoStore;

/// Seam 1: the executable that serves gunnar's column over `ext::`.
///
/// `None` is a legitimate answer for `git`-side work and a fatal one for
/// anything that has to serve — see [`require_server`].
pub fn server_binary() -> Option<std::path::PathBuf> {
    std::env::var_os("GUNNAR_BENCH_GUNNAR_SERVER").map(std::path::PathBuf::from)
}

/// Seam 2: the whole server binary the `vs_forge_*` arms bind-mount into a
/// container. Named here only so the two seams sit in one place; the forge
/// module reads the variable itself.
pub const FORGE_BINARY_VAR: &str = "GUNNAR_BENCH_FORGE_BINARY";

/// Seam 1, or a refusal that says how to satisfy it.
pub fn require_server() -> Result<std::path::PathBuf> {
    match server_binary() {
        Some(bin) => Ok(bin),
        None => bail!(
            "GUNNAR_BENCH_GUNNAR_SERVER is unset and there is no in-process fallback any more.\n\
             \n\
             This arm serves gunnar's column, and `bench/` no longer links a storage engine:\n\
             the old znippy engine crate path dependency was the last one and it is\n\
             deleted. The seam is an executable, not a crate.\n\
             \n\
             Point it at an engine that speaks git's `ext::` argv and answers\n\
             `<exe> prepare <repo>`:\n\
             \n\
               znippy arm:  cargo build --release -p znippy-plugin-git --example git-store-serve\n\
               gix arm:     cargo build --release -p storage-git-gix   --example git-store-serve\n\
                            (in edda; `--locked` and a CARGO_TARGET_DIR of your own)\n\
             \n\
             Then GUNNAR_BENCH_GUNNAR_SERVER=<that binary>. `bench/two-engine.sh` does\n\
             this for both arms and refuses to publish a table if either side is missing."
        ),
    }
}

/// **Open a repository through an in-process engine.**
///
/// One function, ten call sites, and until 2026-08-11 an unconditional `bail!`.
/// Its own doc promised: *"the day an in-process `dyn RepoStore` is legitimately
/// available to `bench/` again — behind `GitServe` and whatever repo-resolution
/// seam lands with it — there is exactly one place to wire it, and every arm
/// picks it up at once."* This is that wiring, and nothing else changed: every
/// caller took `RepoStore` before and takes it now.
///
/// # What made it possible, and why it is not the thing the manifest forbids
///
/// `bench/Cargo.toml` still names **no engine crate**. It names
/// `gunnar-server`, whose `storage` module is the ONE place in gunnar that names
/// either implementation of the eleven, holds both behind `engine-znippy` /
/// `engine-gix`, and hands back an `Arc<OpenRepo>` — the only implementor of
/// [`RepoStore`] in gunnar. So `bench/` reaches **both** engines through one
/// trait rather than picking one, which is the property the old comment was
/// protecting and the old `bail!` achieved only by measuring nothing.
///
/// # The engine is `GUNNAR_ENGINE`, the same as the other two seams
///
/// [`Engine::selected`] reads it. That is the whole point of routing through
/// `gunnar-server` rather than constructing a store directly: before this, that
/// variable reached seam 1 and seam 2 and **nothing in-process**, so the fifteen
/// arms that ask a store directly were single-engine by construction and could
/// not have told the two apart. They are two-engine arms now, for free, and an
/// unknown name is refused by the server's own resolver instead of silently
/// defaulting.
pub fn open_repo(git_dir: &Path, hash_kind: gix_hash::Kind) -> Result<Box<dyn RepoStore>> {
    let engine = gunnar_server::storage::Engine::selected()
        .context("choosing the storage engine for an in-process bench arm")?;
    let repo = engine.open_repo(git_dir, hash_kind).with_context(|| {
        format!(
            "opening {} in-process on the {engine} engine",
            git_dir.display()
        )
    })?;
    // `Arc<OpenRepo>` -> `Box<dyn RepoStore>`: the callers want an owned handle
    // and do not share it, and boxing the `Arc` keeps the store's own internal
    // sharing (the per-root handle cache) intact.
    Ok(Box::new(ArcRepo(repo)))
}

/// `Arc<OpenRepo>` wearing the `RepoStore` the callers ask for.
///
/// A newtype rather than changing ten signatures to `Arc<dyn RepoStore>`: the
/// engine's handle is refcounted on purpose — the store cache hands the same one
/// to the reading and the ingest path so a push refreshes the reader — and
/// unwrapping it here would be undoing that.
struct ArcRepo(Arc<gunnar_server::storage::OpenRepo>);

impl RepoStore for ArcRepo {
    fn git_dir(&self) -> &Path {
        self.0.git_dir()
    }

    fn hash_kind(&self) -> gix_hash::Kind {
        self.0.hash_kind()
    }

    fn git(&self) -> Arc<dyn gunnar_store::git_storage_trait::GitServe> {
        self.0.git()
    }
}