use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
pub const PREFIX: &str = "bench:";
pub const ACTIONLOG_TAG: &str = "BENCH";
static SEQ: AtomicU64 = AtomicU64::new(0);
pub fn mode_label(single_core: bool) -> &'static str {
if single_core {
"st"
} else {
"mt"
}
}
pub fn starting_line(id: &str, single_core: bool) -> String {
format!("{id} ({}) starting…", mode_label(single_core))
}
pub fn done_line(id: &str, single_core: bool, summary: &str, elapsed: Duration) -> String {
let arrow = if summary.is_empty() {
String::new()
} else {
format!(" → {summary}")
};
format!(
"{id} ({}){arrow} ({:.2}s)",
mode_label(single_core),
elapsed.as_secs_f64()
)
}
pub fn telem_suffix(busy: u32, n_cores: u32) -> String {
if n_cores == 0 {
String::new()
} else {
format!(" [cores-busy {busy}/{n_cores}]")
}
}
pub fn done_line_with_telem(
id: &str,
single_core: bool,
summary: &str,
elapsed: Duration,
busy: u32,
n_cores: u32,
) -> String {
format!("{}{}", done_line(id, single_core, summary, elapsed), telem_suffix(busy, n_cores))
}
pub fn failed_line(id: &str, single_core: bool, err: &str, elapsed: Duration) -> String {
format!(
"{id} ({}) ✗ failed: {err} ({:.2}s)",
mode_label(single_core),
elapsed.as_secs_f64()
)
}
pub fn emit(body: &str) {
eprintln!("{PREFIX} {body}");
append_to_actionlog(body);
}
fn append_to_actionlog(body: &str) {
let Some(path) = std::env::var_os("NORNIR_VIZ_ACTIONLOG") else {
return;
};
if path.is_empty() {
return;
}
let mut f = match std::fs::OpenOptions::new().create(true).append(true).open(&path) {
Ok(f) => f,
Err(_) => return,
};
let seq = SEQ.fetch_add(1, Ordering::Relaxed) + 1;
let stamp = now_stamp();
let _ = writeln!(f, "{stamp} {seq:>5} [{ACTIONLOG_TAG}] {PREFIX} {body}");
let _ = f.flush();
}
fn now_stamp() -> String {
chrono::Local::now().format("%H:%M:%S%.3f").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mode_labels() {
assert_eq!(mode_label(false), "mt");
assert_eq!(mode_label(true), "st");
}
#[test]
fn starting_line_names_suite_and_mode() {
assert_eq!(
starting_line("znippy.compress", false),
"znippy.compress (mt) starting…"
);
assert_eq!(
starting_line("znippy.compress", true),
"znippy.compress (st) starting…"
);
}
#[test]
fn done_line_includes_summary_and_elapsed() {
let line = done_line("holger.ops", false, "1234 ops/s", Duration::from_millis(420));
assert_eq!(line, "holger.ops (mt) → 1234 ops/s (0.42s)");
let bare = done_line("x.y", true, "", Duration::from_secs(1));
assert_eq!(bare, "x.y (st) (1.00s)");
}
#[test]
fn telem_suffix_and_done_line() {
assert_eq!(telem_suffix(3, 8), " [cores-busy 3/8]");
assert_eq!(telem_suffix(0, 0), "");
let line =
done_line_with_telem("z.c", false, "100 MB/s", Duration::from_millis(500), 4, 8);
assert_eq!(line, "z.c (mt) → 100 MB/s (0.50s) [cores-busy 4/8]");
}
#[test]
fn failed_line_carries_error() {
let line = failed_line("a.b", false, "boom", Duration::from_millis(50));
assert_eq!(line, "a.b (mt) ✗ failed: boom (0.05s)");
}
#[test]
fn actionlog_append_matches_viz_format() {
let path = std::env::temp_dir().join("nornir_bench_progress_test.log");
let _ = std::fs::remove_file(&path);
unsafe { std::env::set_var("NORNIR_VIZ_ACTIONLOG", &path) };
emit("demo.case (mt) starting…");
unsafe { std::env::remove_var("NORNIR_VIZ_ACTIONLOG") };
let contents = std::fs::read_to_string(&path).unwrap();
let line = contents.lines().last().unwrap();
let mut parts = line.splitn(2, |c: char| c == '[');
let head = parts.next().unwrap(); let tail = parts.next().unwrap(); assert!(head.split_whitespace().count() == 2, "head: {head:?}");
assert!(tail.starts_with("BENCH] bench: demo.case (mt) starting…"), "tail: {tail:?}");
let _ = std::fs::remove_file(&path);
}
}