holperf/src/main.rs
//! HOL-PERF-FORMATS: `holperf <out.json> [formats,...]`. With `HOLPERF_REAL=<seeds dir>` it
//! measures the REAL package sets there (`holperf::real`, lane README-BENCH) instead of the
//! synthetic ones; `cargo xtask bench-readme run` drives it and renders the README table.
use holperf::{chain, harness, real, seed};
use std::sync::Arc;
use std::time::Duration;
use harness::*;
#[global_allocator]
static A: Counting = Counting;
fn measure(
sc: &Scenario,
base_shared: Option<&str>,
rt: &tokio::runtime::Runtime,
dur: Duration,
) -> anyhow::Result<serde_json::Value> {
eprintln!("== {}", sc.format);
// the process's peak RSS over this format alone (VmHWM, reset here), after the
// allocator handed back what the seeding freed
unsafe {
libc::malloc_trim(0);
}
let mut ph = Phases::start();
let rss_start = status_kib("VmRSS:") / 1024.0;
let mut row = serde_json::json!({ "format": sc.format, "packages": sc.packages,
"small_size": sc.small_size, "large_size": sc.large_size, "rss_start_mib": rss_start });
// ── cold: a fresh backend, pages evicted, first request each ──
let mut cold = serde_json::Map::new();
for (k, urls) in [
("index", &sc.index),
("meta2", &sc.meta2),
("small", &sc.small),
("large", &sc.large),
] {
if urls.is_empty() {
continue;
}
let (mut t_open, mut t_first, mut cpu, mut status) =
(Vec::new(), Vec::new(), Vec::new(), 0);
for rep in 0..3 {
evict(&sc.dir);
let c0 = cpu_ms();
let t = std::time::Instant::now();
let b = (sc.open)();
t_open.push(t.elapsed().as_secs_f64() * 1e3);
let t = std::time::Instant::now();
let (s, _) = serve(b.as_ref(), &urls[rep % urls.len()]);
t_first.push(t.elapsed().as_secs_f64() * 1e3);
cpu.push(cpu_ms() - c0);
status = s;
}
cold.insert(
k.into(),
serde_json::json!({ "status": status,
"open_ms": pct(&mut t_open, 0.5), "first_ms": pct(&mut t_first, 0.5),
"cpu_ms": pct(&mut cpu, 0.5) }),
);
}
row["cold"] = serde_json::Value::Object(cold);
ph.mark("cold");
// ── warm in-process ──
let b = (sc.open)();
let small_iters = std::env::var("HOLPERF_ITERS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(2000);
let iters = |k: &str| if k == "large" { 20 } else { small_iters };
let mut inp = serde_json::Map::new();
for (k, urls) in [
("index", &sc.index),
("meta2", &sc.meta2),
("small", &sc.small),
("large", &sc.large),
] {
if urls.is_empty() {
continue;
}
let r = inproc(b.as_ref(), urls, iters(k));
inp.insert(
k.into(),
serde_json::json!({ "status": r.status, "bytes": r.bytes,
"p50_us": r.p50_us, "p99_us": r.p99_us, "allocs": r.allocs,
"alloc_bytes": r.alloc_bytes, "peak_bytes": r.peak_bytes, "first_us": r.first_us }),
);
}
row["inproc"] = serde_json::Value::Object(inp);
ph.mark("inproc");
// ── served over the real HTTP door ──
let base = match base_shared {
Some(b) => b.to_string(),
None => {
let backend: Arc<dyn traits::RepositoryBackendTrait> = b.clone();
let addr = rt.block_on(start_gateway(vec![(sc.route.clone(), backend)], None))?;
format!("http://{addr}")
}
};
let full = |v: &Vec<String>| v.iter().map(|u| format!("{base}{u}")).collect::<Vec<_>>();
// one warm-up pass so the door's caches are filled like the in-process tier's
for u in full(&sc.index)
.iter()
.chain(full(&sc.meta2).iter())
.chain(full(&sc.small).iter())
{
if let Ok(mut r) = reqwest::blocking::get(u) {
let _ = drain(&mut r, &mut [0u8; 64 * 1024]);
}
}
ph.mark("served_warmup");
let mut srv = serde_json::Map::new();
let plan: Vec<(&str, Vec<String>, usize)> = vec![
("index_c1", full(&sc.index), 1),
("index_c32", full(&sc.index), 32),
("meta2_c32", full(&sc.meta2), 32),
("small_c1", full(&sc.small), 1),
("small_c32", full(&sc.small), 32),
("large_c1", full(&sc.large), 1),
("large_c8", full(&sc.large), 8),
];
for (k, urls, c) in plan {
if urls.is_empty() {
continue;
}
let r = served(&urls, c, dur);
srv.insert(
k.into(),
serde_json::json!({ "ok": r.ok, "fail": r.fail, "rps": r.rps,
"mbs": r.mbs, "p50_ms": r.p50_ms, "p99_ms": r.p99_ms }),
);
ph.mark(k);
}
row["served"] = serde_json::Value::Object(srv);
row["hwm_mib"] = serde_json::json!(ph.hwm_mib());
row["heap_peak_mib"] = serde_json::json!(ph.heap_peak_mib());
row["phases"] = serde_json::Value::Object(std::mem::take(&mut ph.rows));
row["load1"] = serde_json::json!(load1());
eprintln!("{}", serde_json::to_string(&row)?);
Ok(row)
}
/// This process's CPU time (user + system, every thread), ms, at µs resolution.
fn cpu_ms() -> f64 {
let mut u: libc::rusage = unsafe { std::mem::zeroed() };
unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut u) };
let t = |v: libc::timeval| v.tv_sec as f64 * 1e3 + v.tv_usec as f64 / 1e3;
t(u.ru_utime) + t(u.ru_stime)
}
fn load1() -> f64 {
std::fs::read_to_string("/proc/loadavg")
.unwrap_or_default()
.split_whitespace()
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0.0)
}
/// The real set a row measured (`HOLPERF_REAL`): its name and what it holds.
fn mark_real(row: &mut serde_json::Value, seeds: &std::path::Path, format: &str, engine: &str) {
let set = format.strip_prefix("chain-").unwrap_or(format);
let f = real::facts(seeds, set);
row["real"] = serde_json::json!(true);
row["set"] = serde_json::json!(set);
row["engine"] = serde_json::json!(engine);
row["set_files"] = serde_json::json!(f.files);
row["set_packages"] = serde_json::json!(f.packages);
row["set_bytes"] = serde_json::json!(f.bytes);
}
fn main() -> anyhow::Result<()> {
// the server's allocator settings: the process measured is the server (lane HOL-RSS)
let _ = server_lib::memory::bound_allocator();
let _ = rustls::crypto::ring::default_provider().install_default();
let args: Vec<String> = std::env::args().collect();
let out = args
.get(1)
.cloned()
.unwrap_or_else(|| "/t/data/results.json".into());
let only: Option<Vec<String>> = args
.get(2)
.map(|s| s.split(',').map(str::to_string).collect());
let root = std::path::PathBuf::from(
std::env::var("HOLPERF_DATA").unwrap_or_else(|_| "/t/data".into()),
);
let env = |k: &str, d: usize| {
std::env::var(k)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(d)
};
let n = env("HOLPERF_N", 2000);
let m = env("HOLPERF_CHAIN_N", 300);
let small = env("HOLPERF_SMALL", 16 * 1024);
let large = env("HOLPERF_LARGE", 64 * 1024 * 1024);
let dur = Duration::from_secs(env("HOLPERF_SECS", 4) as u64);
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(16)
.enable_all()
.build()?;
let mut rows = Vec::new();
let seeds = std::env::var_os("HOLPERF_REAL").map(std::path::PathBuf::from);
let plain = match &seeds {
Some(s) => real::plain(s, &root, only.as_deref())?,
None => seed::all(&root, n, small, large, only.as_deref())?,
};
eprintln!("seeded {} plain scenarios", plain.len());
for sc in &plain {
let mut row = measure(sc, None, &rt, dur)?;
if let Some(s) = &seeds {
mark_real(&mut row, s, sc.format, "plain");
}
rows.push(row);
std::fs::write(&out, serde_json::to_string_pretty(&rows)?)?;
}
let cs = match &seeds {
Some(s) => real::chain(s, &root, only.as_deref())?,
None => chain::build(&root, m, small, large, only.as_deref())?,
};
if let Some(cs) = cs {
let addr = rt.block_on(start_gateway(
cs.routes.clone(),
Some(Arc::clone(&cs.chain)),
))?;
let base = format!("http://{addr}");
for sc in &cs.scenarios {
let mut row = measure(sc, Some(&base), &rt, dur)?;
row["chain_available"] = serde_json::json!(cs.available);
row["chain_refused"] = serde_json::json!(cs.refused);
if let Some(s) = &seeds {
mark_real(&mut row, s, sc.format, "chain");
}
rows.push(row);
std::fs::write(&out, serde_json::to_string_pretty(&rows)?)?;
}
}
Ok(())
}