//! 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 with the config as loaded (no picker) --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 = 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) => smoke_workload(name, &config::load()), None => { eprintln!("alderbench: --smoke-workload needs a name\n{USAGE}"); std::process::exit(2); } } return; } 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::>() .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::>() .join(" "), } } fn smoke_workload(name: &str, cfg: &config::Config) { 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; }; println!("smoke: {} params={}", w.name(), w.params()); 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(); } let verdict = w.wait(); let score = w.score(); println!("smoke: verdict = {:?}", verdict); println!("smoke: score = {}", format_score(&score)); } Err(e) => println!("smoke: start failed: {e}"), } }