crates/nornir-saga/src/bench.rs
//! **Bench the hot execution path** (feature `bench`) — replaying (folding) a
//! saga's whole movement stream through the deterministic
//! [`Viewer`](crate::ui::Viewer). This is the crate's inner loop: the CLI replay,
//! the web SSE stream, and [`Player::verify`](crate::ui::Player::verify) all fold
//! the saga this way, so its throughput is the number worth guarding.
//!
//! Registered with the `nornir-bench` harness via [`register_bench!`]. OFF by
//! default; a release build carries no `nornir-bench` dependency.
use nornir_bench::api::Bencher;
use nornir_bench::BenchResult;
use crate::ui::{demo_saga, Player, Saga};
/// The replay-fold bencher: fold a sizable deterministic saga through the viewer
/// many times and report movements-folded per second. The saga is the
/// hand-authored [`demo_saga`] repeated with a time offset so the fold is large
/// enough to time stably and identical every run (no clock, no rng).
pub struct ReplayFold {
saga: Saga,
reps: usize,
}
impl Default for ReplayFold {
fn default() -> Self {
Self::new()
}
}
impl ReplayFold {
/// A bencher over a `reps`-fold of a saga built from `copies` demo sessions.
///
/// PREVIEW (`NORNIR_BENCH_PREVIEW=1`): the same fold through the same `Viewer`,
/// emitting the same metric keys, over a saga 1/10 the size folded 1/10 as often —
/// the fast verification pass. A preview run is stamped `<host>-preview` by the
/// harness, so its number can never be mistaken for the published one.
pub fn new() -> Self {
if nornir_bench::api::preview_run() {
Self::with_size(4, 5)
} else {
Self::with_size(40, 50)
}
}
/// Build the bencher explicitly: `copies` back-to-back demo sessions folded
/// `reps` times per `run()`.
pub fn with_size(copies: u64, reps: usize) -> Self {
let base = demo_saga();
let span = base.duration_ms() + 1;
let mut saga = Saga::new("bench · replay-fold");
for r in 0..copies {
let off = r * span;
for ev in &base.events {
let mut e = ev.clone();
e.t_ms += off;
saga.add(e);
}
}
Self { saga, reps }
}
}
impl Bencher for ReplayFold {
fn id(&self) -> &'static str {
"saga-replay-fold"
}
fn run(&self) -> anyhow::Result<BenchResult> {
let movements = self.saga.len();
let t0 = std::time::Instant::now();
let mut folded = 0usize;
for _ in 0..self.reps {
// `frames()` folds the whole saga from a clean viewer — the hot path.
folded += Player::new(self.saga.clone()).frames().len();
}
let secs = t0.elapsed().as_secs_f64().max(1e-9);
let mut m = serde_json::Map::new();
m.insert("movements".into(), serde_json::json!(movements as f64));
m.insert("reps".into(), serde_json::json!(self.reps as f64));
m.insert("folds_per_sec".into(), serde_json::json!(folded as f64 / secs));
Ok(BenchResult::measured(self.id(), m))
}
}
// Register with the shared harness so `nornir bench` picks up the replay-fold
// path alongside every other nordisk bencher.
nornir_bench::register_bench!(ReplayFold::default());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replay_fold_bencher_runs_and_reports_throughput() {
let b = ReplayFold::with_size(4, 3);
assert_eq!(b.id(), "saga-replay-fold");
let r = b.run().expect("bench runs");
let per_sec = r.metrics.get("folds_per_sec").and_then(|v| v.as_f64()).unwrap();
let moves = r.metrics.get("movements").and_then(|v| v.as_f64()).unwrap();
assert!(per_sec > 0.0, "reports a positive throughput");
assert!(moves > 40.0, "the saga has real movement to fold");
}
}