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")
);
}