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

nordisk/holger

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(())
}