bench-scenarios/src/bin/holger_ds.rs
//! `holger-ds` - the artifact-delivery benchmark, as a BINARY of the bench crate.
//!
//! It used to be a subcommand of `holger-server`, compiled in through the `bench`
//! cargo feature, which pulled `holger-bench-scenarios` (and its reqwest
//! HTTP-bombardment client) into the SHIPPED binary. Cargo unifies features per
//! GRAPH, so that one edge wrote `front`, then `auth-passkey`, then native OpenSSL
//! + `auth-oidc` + `auth-oauth2` into production - three times, unnoticed. Owner
//! ruling 2026-09-20: the bench crate is a DEV DEPENDENCY and never reaches the
//! final binary. So the driver moved here, beside the scenarios it drives, where
//! its `reqwest` and its `server-lib` pin can decide nothing but themselves.
//!
//! The verb, its flags and its output are unchanged:
//!
//! ```text
//! cargo run -p holger-bench-scenarios --features served --bin holger-ds -- \
//! --preset small --eco crate
//! ```
//!
//! `--target <url>` still benchmarks an ALREADY-RUNNING Holger / Nexus /
//! Artifactory over the real package protocol instead of standing one up.
use std::path::PathBuf;
use std::time::Duration;
use anyhow::{Context, Result};
use clap::Parser;
use holger_bench_scenarios::{
build_crate_corpus, build_pip_corpus, cores, free_disk_bytes, propose_dataset_bytes, served,
};
/// Named dataset-scale presets for `holger-ds` (the demo-plan micro/small/large
/// slider). When set, `--preset` overrides the raw `--scale` factor.
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
enum DsScale {
/// Smallest corpus — smoke test / CI (≈1 GiB intent, clamped to free disk).
Micro,
/// A few GiB of crate data.
Small,
/// A broad multi-GiB corpus.
Large,
}
impl DsScale {
/// The `propose_dataset_bytes` scale factor (≈ N GiB intent) for this preset.
fn factor(self) -> u64 {
match self {
DsScale::Micro => 1,
DsScale::Small => 4,
DsScale::Large => 16,
}
}
}
/// Knobs for the `holger-ds` artifact-delivery benchmark.
#[derive(Parser)]
#[command(name = "holger-ds")]
struct HolgerDsArgs {
/// Dataset scale factor: ≈ N GiB of crate data, clamped to ≤ 25 % of the free
/// disk on `--data`.
#[arg(long, default_value_t = 1)]
scale: u64,
/// Named scale preset (micro|small|large). Overrides `--scale` when set —
/// the demo-plan scale slider.
#[arg(long, value_enum)]
preset: Option<DsScale>,
/// Directory to build the synthetic corpus in. Default: a temp dir.
#[arg(long)]
data: Option<PathBuf>,
/// Benchmark an already-running Holger / Nexus / Artifactory at this base URL
/// (e.g. `http://127.0.0.1:18444`, serving the matching `/rust` corpus) over
/// the real package protocol, instead of standing one up. Point this at an
/// EXTERNAL existing Nexus/Artifactory to compare without spawning containers.
#[arg(long)]
target: Option<String>,
/// Ecosystem journey to drive: `crate` (Rust sparse download, the default) or
/// `pip` (the holger-ds Python journey — a PEP 503 / PEP 691 wheel index the
/// way `uv`/`pip` install from it). Both bombard Holger's native HTTP door.
#[arg(long, value_enum, default_value = "crate")]
eco: DsEco,
}
/// Which ecosystem the `holger-ds` journey drives.
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
enum DsEco {
/// Rust crates over the cargo sparse download path.
Crate,
/// Python wheels over the PEP 503 / PEP 691 simple index (the uv/Airflow journey).
Pip,
}
/// 1 GiB in bytes, as `f64`, for the `holger-ds` human-readable sizing report.
const GIB_F: f64 = 1024.0 * 1024.0 * 1024.0;
/// How long the `holger-ds` GET bombardment runs.
const DS_BENCH_SECS: u64 = 5;
/// Drive the `holger-ds` artifact-delivery benchmark: autodetect free disk, propose
/// a corpus size, build the synthetic crate corpus, then fetch it over Holger's
/// package protocol and print throughput. Pure reuse of the bench-scenarios lib.
async fn run_holger_ds(a: HolgerDsArgs) -> Result<()> {
let data_dir = a
.data
.unwrap_or_else(|| std::env::temp_dir().join("holger-ds"));
std::fs::create_dir_all(&data_dir)
.with_context(|| format!("create data dir {}", data_dir.display()))?;
// A named preset overrides the raw scale factor (the demo-plan slider).
let scale = a.preset.map(DsScale::factor).unwrap_or(a.scale);
let scale_label = match a.preset {
Some(p) => format!("{:?}(={scale})", p).to_lowercase(),
None => scale.to_string(),
};
let free = free_disk_bytes(&data_dir);
let budget = propose_dataset_bytes(scale, free);
let free_report = free
.map(|f| format!("{:.1} GiB", f as f64 / GIB_F))
.unwrap_or_else(|| "unknown".to_string());
println!(
"holger-ds: scale={} free-disk={} -> corpus budget {:.2} GiB in {}",
scale_label,
free_report,
budget as f64 / GIB_F,
data_dir.display(),
);
let concurrency = cores();
let dur = Duration::from_secs(DS_BENCH_SECS);
let door = a
.target
.as_deref()
.map(|t| format!("running Holger at {t}"))
.unwrap_or_else(|| "stood-up Holger HTTP gateway".to_string());
let outcome = match a.eco {
DsEco::Crate => {
let (archive, crates) = build_crate_corpus(&data_dir, budget)?;
println!(" corpus: {crates} crates archive {}", archive.display());
let (files, o) =
served::run_crate_fetch(&archive, "rust", a.target.as_deref(), concurrency, dur)
.await?;
println!(
" served {files} crate files over the package protocol [{door}] @ concurrency {concurrency}:"
);
o
}
DsEco::Pip => {
let (archive, packages) = build_pip_corpus(&data_dir, budget)?;
println!(" corpus: {packages} wheels archive {}", archive.display());
let o =
served::run_pip_index_fetch(&archive, "pip", a.target.as_deref(), concurrency, dur)
.await?;
println!(
" served {} wheels over the PEP 503 index [{door}] @ concurrency {concurrency}:",
o.files
);
println!(
" index resolve : {:.0} ops/sec ({} packages, /simple/<name>/)",
o.index.ops_per_sec, o.packages,
);
// Report the wheel-download leg through the same fields below.
o.wheel
}
};
println!(" throughput : {:.1} MB/s", outcome.mbs);
println!(" ops/sec : {:.0}", outcome.ops_per_sec);
println!(
" total : {} ops, {:.2} GiB off the wire in {DS_BENCH_SECS}s",
outcome.ops,
outcome.bytes as f64 / GIB_F,
);
Ok(())
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
run_holger_ds(HolgerDsArgs::parse()).await
}