f8175c8c69f80dd21609ffe92b417e942f074d49 / alderbench/src/main.rs · 10860 bytes · raw
//! alderbench — CLI entry point + arg dispatch.
//!
//! Modes: no args = TUI picker (default); `--tui` = TUI with the loaded
//! config; `--preset NAME` = TUI straight into a preset sweep;
//! `--headless` = scripted sweep, stdout only; `--smoke-workload NAME` =
//! run one workload 3s + print the verdict; `--report PATH` = print a
//! past run's summary; `--list` = enumerate workloads.
mod config;
mod inproc;
mod presets;
mod report;
mod run;
mod ui;
mod workload;
use std::io::IsTerminal;
const USAGE: &str = "\
usage: alderbench [OPTION]...
(no option) pick a preset or edit a custom sweep (TUI)
--tui TUI picker (loaded config = Custom fallback)
--preset NAME TUI straight into a preset: stability | bench |
core-cycle
--headless run the configured sweep without a TUI
--smoke-workload NAME run one workload for 3s + print verdict/score
--report PATH print the summary of a past run's JSON report
--list enumerate available workload names
--version print version and exit
--help show this help";
const OPTION_FLAGS: &[&str] = &[
"--tui",
"--preset",
"--headless",
"--smoke-workload",
"--report",
"--list",
"--version",
"--help",
];
fn main() {
let args: Vec<String> = std::env::args().collect();
for a in &args[1..] {
if a.starts_with('-') && !OPTION_FLAGS.contains(&a.as_str()) {
// A leading-dash token that is a *value* (e.g. `--preset -x`)
// is caught here too — acceptable: our values never start
// with '-'.
eprintln!("alderbench: unknown option '{a}'\n{USAGE}");
std::process::exit(2);
}
}
if args.iter().any(|a| a == "--help") {
println!("{USAGE}");
return;
}
if args.iter().any(|a| a == "--version") {
println!("alderbench {}", env!("CARGO_PKG_VERSION"));
return;
}
if args.iter().any(|a| a == "--list") {
for name in presets::AVAILABLE_WORKLOADS {
println!("{name}");
}
return;
}
if let Some(idx) = args.iter().position(|a| a == "--report") {
match args.get(idx + 1) {
Some(path) => report_print(path),
None => {
eprintln!("alderbench: --report needs a path\n{USAGE}");
std::process::exit(2);
}
}
return;
}
if let Some(idx) = args.iter().position(|a| a == "--smoke-workload") {
match args.get(idx + 1) {
Some(name) => std::process::exit(smoke_workload(name, &config::load())),
None => {
eprintln!("alderbench: --smoke-workload needs a name\n{USAGE}");
std::process::exit(2);
}
}
}
if args.iter().any(|a| a == "--headless") {
headless_run(config::load());
return;
}
if let Some(idx) = args.iter().position(|a| a == "--preset") {
match args.get(idx + 1).map(|s| s.as_str()) {
Some("stability") => require_tty_then(|| ui::run_tui(presets::stability())),
Some("bench") => require_tty_then(|| ui::run_tui(presets::bench())),
Some("core-cycle") => {
require_tty_then(|| ui::run_tui(presets::stability_core_cycle(60)));
}
Some(other) => {
eprintln!("unknown preset: {other} (available: stability, bench, core-cycle)");
std::process::exit(1);
}
None => {
eprintln!("alderbench: --preset needs a name\n{USAGE}");
std::process::exit(2);
}
}
return;
}
if args.iter().any(|a| a == "--tui") {
require_tty_then(|| ui::run_tui(config::load()));
return;
}
// Default mode = TUI picker (lets the user choose a preset or custom).
require_tty_then(|| ui::run_tui(config::load()));
}
/// ratatui panics without a real pty — fail with a clear message instead.
fn require_tty_then(run: impl FnOnce()) {
if !std::io::stdout().is_terminal() {
eprintln!(
"alderbench: the TUI needs a real terminal (stdout is not a tty) — try --headless"
);
std::process::exit(1);
}
run();
}
/// `--report PATH`: print the human summary of a past run's JSON report.
fn report_print(path: &str) {
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
eprintln!("alderbench: can't read {path}: {e}");
std::process::exit(1);
}
};
match report::summary_from_json(&text) {
Some(s) => println!("{s}"),
None => {
eprintln!("alderbench: {path} is not an alderbench report (schema v2)");
std::process::exit(1);
}
}
}
/// `--headless` mode: drive the run loop without a TUI. Prints one line
/// per workload (verdict + score) at the end. Useful for CI / scripts
/// and for verifying run.rs end-to-end before the TUI is wired.
fn headless_run(cfg: config::Config) {
use std::thread;
let mut run = run::Run::new(cfg);
for (name, why) in run.skipped() {
eprintln!("skipped: {name} ({why})");
}
if run.total_workloads() == 0 {
eprintln!("no workloads to run");
return;
}
let poll_ms = config::load().poll_ms;
let poll = std::time::Duration::from_millis(poll_ms);
let mut count = 0usize;
loop {
match run.tick() {
Ok(run::TickOutcome::WorkloadStarted {
idx,
name,
duration_secs,
}) => {
println!("[{idx}] starting {name} ({duration_secs}s)");
}
Ok(run::TickOutcome::WorkloadFinished {
idx,
name,
verdict,
score,
report_path,
}) => {
println!("[{idx}] finished {name}: {:?}", verdict);
println!(" score: {}", format_score(&score));
println!(" report: {}", report_path.display());
count += 1;
}
Ok(run::TickOutcome::SweepDone) => {
println!("sweep complete: {count} workloads");
break;
}
Ok(run::TickOutcome::Idle) => {}
Err(e) => {
eprintln!("run error: {e}");
break;
}
}
thread::sleep(poll);
}
}
/// One-line score formatter for the headless + smoke modes. Mirrors what
/// `report::human_summary` does for the score, but standalone so the
/// headless loop can print it without reconstructing a Report.
fn format_score(s: &workload::Score) -> String {
use workload::Score;
match s {
Score::None => "(no score)".to_string(),
Score::SevenZip {
rating_mips,
r_u_mips,
usage_pct,
compress_mips,
decompress_mips,
} => {
format!(
"rating={rating_mips} MIPS r/u={r_u_mips} usage={usage_pct}% \
compress={compress_mips} MIPS decompress={decompress_mips} MIPS"
)
}
Score::YCruncher { tests } => {
if tests.is_empty() {
return "(no tests recorded)".to_string();
}
tests
.iter()
.map(|t| {
let gbps = t.bits_per_sec / 1e9;
format!(
"{}={} {:.2} Gb/s",
t.tag,
if t.passed { "pass" } else { "FAIL" },
gbps
)
})
.collect::<Vec<_>>()
.join(" ")
}
Score::C2c { pairs } => {
if pairs.is_empty() {
return "(no pairs measured)".to_string();
}
let min = pairs
.iter()
.min_by(|x, y| x.ns_per_hop.total_cmp(&y.ns_per_hop))
.unwrap();
let max = pairs
.iter()
.max_by(|x, y| x.ns_per_hop.total_cmp(&y.ns_per_hop))
.unwrap();
format!(
"{} pairs min {:.1} ns (cpu {}<->{}), max {:.1} ns (cpu {}<->{})",
pairs.len(),
min.ns_per_hop,
min.cpu_a,
min.cpu_b,
max.ns_per_hop,
max.cpu_a,
max.cpu_b
)
}
Score::Dram { levels } => levels
.iter()
.map(|l| format!("{} KiB={:.1} ns", l.size_kb, l.ns_per_access))
.collect::<Vec<_>>()
.join(" "),
}
}
fn smoke_workload(name: &str, cfg: &config::Config) -> i32 {
use std::thread;
use std::time::Duration;
// Build a 3-second spec for the requested workload, regardless of the
// configured duration — this is a smoke test, not a real run.
let spec = config::WorkloadSpec {
name: name.to_string(),
duration_secs: 3,
cores: "0-15".to_string(),
};
let Some(mut w) = workload::from_spec(&spec, &cfg.bin_dir) else {
eprintln!("unknown or not-yet-wired workload: {name}");
return 1;
};
println!("smoke: {} params={}", w.name(), w.params());
let verdict = match w.start() {
Ok(()) => {
// Poll is_running every 250ms; stop after the spec's duration
// plus a 1s grace (some workloads self-exit right at the
// boundary; the grace avoids a Stopped verdict when the
// workload actually finished on its own).
let ticks = spec.duration_secs * 4 + 4;
let mut elapsed = 0u64;
while elapsed < ticks {
thread::sleep(Duration::from_millis(250));
elapsed += 1;
if !w.is_running() {
println!("smoke: exited after {:.1}s", elapsed as f64 / 4.0);
break;
}
}
if w.is_running() {
println!("smoke: still running — calling stop()");
let _ = w.stop();
}
w.wait()
}
Err(e) => {
println!("smoke: start failed: {e}");
return 1;
}
};
let score = w.score();
println!("smoke: verdict = {verdict:?}");
println!("smoke: score = {}", format_score(&score));
// Stopped is expected when a longer workload (7z) is cut at the smoke
// window — only real failures fail the exit code.
match verdict {
workload::Verdict::Clean | workload::Verdict::Stopped => 0,
_ => 1,
}
}