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

nordisk/holger

bench-scenarios/examples/secure_update_bench.rs

//! Light, self-contained runner for the Secure Update Gateway bundle-path bench.
//!
//! Emits the SAME `BenchRun` envelope shape the workspace `run-benches.sh` captures
//! (the last stdout line that parses as JSON: `{date, timestamp, version, machine,
//! cores, results:[{name, …metrics}], tests:[{name, passed, duration_ms, message}]}`),
//! but WITHOUT pulling the heavy nexus/artifactory/server-lib graph the full
//! `xtask/examples/nornir-bench.rs` links — it depends only on
//! `holger-bench-scenarios` with the `secure-update` feature. Use it to produce the
//! MVP's first persisted numbers on a disk-tight / contended host without building
//! unrelated arms:
//!
//! ```bash
//! NORNIR_MACHINE=oden NORNIR_BENCH_VERSION=0.1.0 HOLGER_BENCH_SCALE=quick \
//!   cargo run --release -p holger-bench-scenarios \
//!   --example secure_update_bench --features secure-update > run.json
//! ```
//!
//! The canonical `nornir bench run holger` path (which also runs these arms, plus
//! per-arm CPU/mem telemetry) is `xtask/examples/nornir-bench.rs`; this runner is
//! the light twin for a focused, low-contention MVP measurement.

use std::time::Instant;

use holger_bench_scenarios::{secure_update, Scale, Throughput};
use serde_json::json;

/// One arm's outcome: a result row (present on success) + a test row (always).
fn run_arm(
    name: &str,
    id: &str,
    scale: &Scale,
    f: fn(&Scale) -> anyhow::Result<Throughput>,
    extra: &[(&str, serde_json::Value)],
) -> (Option<serde_json::Value>, serde_json::Value) {
    eprintln!("[secure_update] running {id} …");
    let start = Instant::now();
    let outcome = f(scale);
    let dur_ms = start.elapsed().as_secs_f64() * 1000.0;
    match outcome {
        Ok(t) => {
            let mut m = serde_json::Map::new();
            m.insert("name".into(), json!(name));
            m.insert("ops".into(), json!(t.ops));
            m.insert("ops_per_sec".into(), json!(t.ops_sec));
            m.insert("mb_per_sec".into(), json!(t.mb_per_sec));
            m.insert("bytes".into(), json!(t.bytes));
            m.insert("seconds".into(), json!(t.elapsed.as_secs_f64()));
            for (k, v) in extra {
                m.insert((*k).into(), v.clone());
            }
            eprintln!(
                "[secure_update]   {id}: {} ops, {:.2} ops/s, {:.1} MB/s over {:.2}s",
                t.ops,
                t.ops_sec,
                t.mb_per_sec,
                t.elapsed.as_secs_f64()
            );
            let test = json!({ "name": id, "passed": true, "duration_ms": dur_ms });
            (Some(serde_json::Value::Object(m)), test)
        }
        Err(e) => {
            let msg = format!("{e:#}");
            eprintln!("[secure_update]   {id}: FAILED — {msg}");
            let test =
                json!({ "name": id, "passed": false, "duration_ms": dur_ms, "message": msg });
            (None, test)
        }
    }
}

/// Best-effort 1-minute load average from `/proc/loadavg` (pure read, no shelling).
fn loadavg_1min() -> Option<f64> {
    std::fs::read_to_string("/proc/loadavg")
        .ok()?
        .split_whitespace()
        .next()?
        .parse()
        .ok()
}

/// The version of the SUBJECT this bench measures — holger — read from holger's
/// ROOT manifest, never from this harness crate.
///
/// `env!("CARGO_PKG_VERSION")` here is the version of `holger-bench-scenarios`
/// (0.1.1), which is not what any row in this file is about. That fallback is the
/// same defect that stamped `0.1.8` and `0.6.8` onto rows all day: a plausible
/// number belonging to something other than the subject. An unstamped row is
/// visibly unstamped; a mislabelled one is not. So there is no `CARGO_PKG_VERSION`
/// fallback, and with no answer at all the row reads `"unknown"`.
///
/// Order, matching `nornir_bench::runner::subject_version` upstream (which this
/// crate cannot call — `bench-scenarios` is deliberately nornir-free):
/// `[package].version`, then `[workspace.package].version`. A dependency's
/// `version =` requirement is NEVER a source. `NORNIR_BENCH_VERSION` is consulted
/// only when the root manifest is silent, and a disagreement is refused out loud
/// naming both values.
fn subject_version() -> String {
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .map(|p| p.join("Cargo.toml"));
    let manifest = root.and_then(|p| std::fs::read_to_string(p).ok());
    let from_manifest = manifest.as_deref().and_then(root_manifest_version);

    let ambient = std::env::var("NORNIR_BENCH_VERSION")
        .ok()
        .filter(|v| !v.is_empty());

    match (from_manifest, ambient) {
        (Some(m), Some(a)) if m != a => {
            eprintln!(
                "[secure_update] REFUSED: NORNIR_BENCH_VERSION={a:?} disagrees with the root \
                 manifest's {m:?}. The manifest is the authority; unset the env var or fix the \
                 manifest."
            );
            std::process::exit(2);
        }
        (Some(m), _) => m,
        (None, Some(a)) => a,
        (None, None) => "unknown".to_string(),
    }
}

/// `[package].version` then `[workspace.package].version` from a root manifest.
/// Deliberately tiny: a `version = "x"` line inside one of those two tables only,
/// so a dependency's `version =` requirement can never be mistaken for the answer.
fn root_manifest_version(manifest: &str) -> Option<String> {
    let pick = |want: &str| -> Option<String> {
        let mut in_table = false;
        for line in manifest.lines() {
            let t = line.trim();
            if t.starts_with('[') {
                in_table = t == want;
                continue;
            }
            if !in_table {
                continue;
            }
            let Some(rest) = t.strip_prefix("version") else {
                continue;
            };
            let Some(rest) = rest.trim_start().strip_prefix('=') else {
                continue;
            };
            let v = rest.trim().trim_matches('"');
            if !v.is_empty() {
                return Some(v.to_string());
            }
        }
        None
    };
    pick("[package]").or_else(|| pick("[workspace.package]"))
}

fn main() {
    let scale = Scale::from_env();
    let cores = holger_bench_scenarios::cores() as u32;
    let machine = std::env::var("NORNIR_MACHINE").unwrap_or_default();
    let version = subject_version();

    let load_start = loadavg_1min();
    eprintln!(
        "[secure_update] scale: bench_secs={} entries={} target_bytes={:?}  machine={machine:?} cores={cores} load1={:?}",
        scale.bench_secs, scale.entries, scale.target_bytes, load_start
    );

    let arms: &[(
        &str,
        &str,
        fn(&Scale) -> anyhow::Result<Throughput>,
        Vec<(&str, serde_json::Value)>,
    )] = &[
        (
            "secure_update_prepare",
            "holger.secure_update_prepare",
            secure_update::bundle_prepare_throughput,
            // The znippy delta-vs-whole answer, recorded on the row.
            vec![("packs_whole", json!(1)), ("deltas", json!(0))],
        ),
        (
            "secure_update_sign_finalize",
            "holger.secure_update_sign_finalize",
            secure_update::bundle_sign_finalize_throughput,
            vec![],
        ),
        (
            "secure_update_verify",
            "holger.secure_update_verify",
            secure_update::bundle_verify_throughput,
            vec![],
        ),
        (
            "secure_update_import",
            "holger.secure_update_import",
            secure_update::bundle_import_throughput,
            vec![],
        ),
    ];

    let mut results = Vec::new();
    let mut tests = Vec::new();
    for (name, id, f, extra) in arms {
        let (row, test) = run_arm(name, id, &scale, *f, extra);
        if let Some(r) = row {
            results.push(r);
        }
        tests.push(test);
    }

    let load_end = loadavg_1min();
    let now = chrono::Utc::now();
    let run = json!({
        "date": now.format("%Y-%m-%d").to_string(),
        "timestamp": now.to_rfc3339(),
        "version": version,
        "machine": machine,
        "cores": cores,
        // Method context so the persisted row is self-describing (this light runner
        // has no per-arm CPU/mem sampler; the canonical xtask runner adds telem_*).
        "bench_secs": scale.bench_secs,
        "load1_start": load_start,
        "load1_end": load_end,
        "results": results,
        "tests": tests,
    });

    // The envelope is the LAST stdout line that parses as JSON.
    println!(
        "{}",
        serde_json::to_string(&run).expect("serialize BenchRun envelope")
    );
}