crates/gunnar-loadbench/src/main.rs
//! **gunnar-loadbench** — how the gunnar.rs estate behaves under a DECLARED
//! concurrency and a DECLARED request rate. See `Cargo.toml`'s header for what
//! it measures and `src/safety.rs` for what it will not touch.
//!
//! Exit codes: 0 PASS, 1 FAIL, 2 the bench could not run.
mod args;
mod engine;
mod estate;
mod http;
mod outcome;
mod safety;
mod ssh;
mod stats;
mod summary;
mod upcloud;
mod verdict;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::verdict::Threshold;
fn main() {
let argv: Vec<String> = std::env::args().skip(1).collect();
let a = match args::parse(&argv) {
Ok(a) => a,
Err(e) if e == "HELP" => {
println!("{}", args::help());
return;
}
Err(e) => {
eprintln!("gunnar-loadbench: {e}\n\n{}", args::help());
std::process::exit(2);
}
};
if a.from_upcloud {
match upcloud::run_from_upcloud(&a) {
Ok(code) => std::process::exit(code),
Err(e) => {
eprintln!("\nFROM-UPCLOUD refused {e}");
eprintln!("The remote vantage did not run. Nothing was faked; run without --from-upcloud to measure from here.");
std::process::exit(2);
}
}
}
match run(&a) {
Ok(true) => std::process::exit(0),
Ok(false) => std::process::exit(1),
Err(e) => {
eprintln!("gunnar-loadbench: {e}");
std::process::exit(2);
}
}
}
/// Returns the PASS/FAIL verdict.
pub fn run(a: &args::Args) -> Result<bool, String> {
let started = Instant::now();
let deadline = started + a.deadline;
println!("gunnar-loadbench — the gunnar.rs estate, read paths only");
println!(
"PLAN {:.1} rps offered, {} in flight, {} requests per probe, {} s deadline, {} s per-request budget",
a.rate_per_s,
a.max_in_flight,
a.requests_per_probe,
a.deadline.as_secs(),
a.request_timeout.as_secs()
);
println!("VANTAGE {}", a.vantage);
println!("SAFETY GET only; no checkout, no entitlement write, no repo or data-set write. The front's Stripe key is live.");
// ── preflight: say what the estate looks like before loading it ──────────
println!("\nPREFLIGHT");
for (name, addr) in estate::MACHINES {
print!(" {name:<10} {addr:<17} ");
match ssh::identify(addr, 2222, Duration::from_secs(5)) {
Ok(ident) => println!("ssh:2222 {ident}"),
Err(e) => println!("ssh:2222 unreachable ({})", short(&e)),
}
}
let probes = estate::build(a.only.as_deref(), a.request_timeout, a.max_in_flight)?;
let mut rows: Vec<summary::ProbeSummary> = Vec::with_capacity(probes.len() + 4);
// ── the HTTP probes ──────────────────────────────────────────────────────
println!("\nPROBES");
for (spec, probe) in &probes {
if Instant::now() >= deadline {
println!(" {:<22} SKIPPED: the run deadline passed", spec.name);
continue;
}
let plan = engine::Plan {
n: a.requests_per_probe,
workers: a.max_in_flight,
rate_per_s: a.rate_per_s,
timeout: a.request_timeout,
};
let r = engine::run(plan, deadline, |_, released| probe.once(released));
let st = stats::summarise(&r.samples, r.wall_s);
let why = verdict::judge(
&st,
spec.expect.key(),
r.not_issued,
a.requests_per_probe,
&spec.bar,
);
report(spec.name, spec.about, &st, r.not_issued, &why);
rows.push(summary::row(
spec.name,
spec.url,
spec.expect.key(),
a.requests_per_probe,
r.not_issued,
&st,
why,
));
}
// ── the git SSH doors ────────────────────────────────────────────────────
for (name, host, port) in estate::SSH_DOORS {
if a.only.as_deref().is_some_and(|o| o != *name) {
continue;
}
if Instant::now() >= deadline {
println!(" {name:<22} SKIPPED: the run deadline passed");
continue;
}
let probe = ssh::KexProbe {
name,
host: (*host).to_string(),
port: *port,
timeout: a.request_timeout,
};
let plan = engine::Plan {
n: a.requests_per_probe,
workers: a.max_in_flight,
rate_per_s: a.rate_per_s,
timeout: a.request_timeout,
};
let r = engine::run(plan, deadline, |_, released| probe.once(released));
let st = stats::summarise(&r.samples, r.wall_s);
let bar = Threshold::ssh_bar();
let why = verdict::judge(&st, "ok", r.not_issued, a.requests_per_probe, &bar);
let about = "RFC 4253 identification exchange + the server's KEXINIT. A real round trip; no Diffie-Hellman, so it is safe to point a rate at.";
report(name, about, &st, r.not_issued, &why);
rows.push(summary::row(
name,
&format!("ssh://{host}:{port}"),
"ok",
a.requests_per_probe,
r.not_issued,
&st,
why,
));
}
// ── the real git read verb, when it was compiled in ──────────────────────
#[cfg(feature = "git-ssh")]
{
let name = "git-ls-refs";
let run_it = a.only.as_deref().map(|o| o == name).unwrap_or(true);
if run_it && Instant::now() < deadline {
let key = std::env::var("GUNNAR_LOADBENCH_SSH_KEY").unwrap_or_else(|_| {
format!(
"{}/.ssh/id_ed25519",
std::env::var("HOME").unwrap_or_default()
)
});
let url = std::env::var("GUNNAR_LOADBENCH_GIT_URL")
.unwrap_or_else(|_| "ssh://git@git.gunnar.rs:2222/nordisk/gunnar".to_string());
let probe = ssh::LsRefsProbe {
name,
url: url.clone(),
key: std::path::PathBuf::from(key),
};
let plan = engine::Plan {
n: a.requests_per_probe,
workers: a.max_in_flight,
rate_per_s: a.rate_per_s,
timeout: a.request_timeout,
};
let r = engine::run(plan, deadline, |_, released| probe.once(released));
let st = stats::summarise(&r.samples, r.wall_s);
let bar = Threshold::git_bar();
let why = verdict::judge(&st, "ok", r.not_issued, a.requests_per_probe, &bar);
report(
name,
"protocol-v2 ls-refs over russh (gunnar_client::ls_remote). No pack is generated: a clone is deliberately not benched.",
&st,
r.not_issued,
&why,
);
rows.push(summary::row(
name,
&url,
"ok",
a.requests_per_probe,
r.not_issued,
&st,
why,
));
}
}
#[cfg(not(feature = "git-ssh"))]
println!(
"\n git-ls-refs NOT COMPILED IN. The pure-Rust path exists (gunnar_client::ls_remote over russh);\n rebuild with --features git-ssh to measure a real git read verb."
);
// ── the verdict ──────────────────────────────────────────────────────────
let pass = rows.iter().all(|r| r.pass) && !rows.is_empty();
let sum = summary::Summary {
schema: summary::SCHEMA,
tool: format!("gunnar-loadbench {}", env!("CARGO_PKG_VERSION")),
at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
vantage: a.vantage.clone(),
estate: estate::MACHINES
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
plan: summary::Plan {
rate_per_s: a.rate_per_s,
max_in_flight: a.max_in_flight,
per_probe_requests: a.requests_per_probe,
deadline_s: a.deadline.as_secs(),
request_timeout_s: a.request_timeout.as_secs(),
},
probes: rows,
pass,
};
let text = serde_json::to_string_pretty(&sum).map_err(|e| e.to_string())?;
std::fs::write(&a.out, &text).map_err(|e| format!("writing {}: {e}", a.out))?;
println!("\nSUMMARY {}", a.out);
if let Some(b) = &a.baseline {
println!("\nAGAINST {b}");
match std::fs::read_to_string(b)
.map_err(|e| e.to_string())
.and_then(|t| serde_json::from_str::<summary::Summary>(&t).map_err(|e| e.to_string()))
{
Ok(prev) => {
for line in summary::compare(&prev, &sum) {
println!(" {line}");
}
}
// No baseline is a fact, not a failure of the estate.
Err(e) => println!(" no usable baseline ({})", short(&e)),
}
}
println!(
"\nVERDICT {} ({} probes, {:.0} s wall)",
if pass { "PASS" } else { "FAIL" },
sum.probes.len(),
started.elapsed().as_secs_f64()
);
if !pass {
for p in sum.probes.iter().filter(|p| !p.pass) {
for w in &p.why {
println!(" FAIL {:<22} {w}", p.name);
}
}
}
Ok(pass)
}
fn report(name: &str, about: &str, st: &stats::Stats, not_issued: usize, why: &[String]) {
println!(
"\n {:<22} {} n={} p50={:.0} p90={:.0} p99={:.0} max={:.0} ms {:.2} rps slip(p99)={:.0} ms",
name,
if why.is_empty() { "PASS" } else { "FAIL" },
st.n,
st.p50_ms,
st.p90_ms,
st.p99_ms,
st.max_ms,
st.throughput_rps,
st.p99_late_ms
);
println!(" {:<22} {}", "", about);
let reasons: Vec<String> = st
.nonzero()
.into_iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
println!(
" {:<22} outcomes: {}{}",
"",
reasons.join(" "),
if not_issued > 0 {
format!(" not-issued={not_issued}")
} else {
String::new()
}
);
}
fn short(e: &str) -> String {
e.lines().next().unwrap_or(e).chars().take(110).collect()
}