mod config;
mod report;
mod run;
mod sensors;
mod workload;
fn main() {
let cfg = config::load();
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--dump") {
dump_sensors();
return;
}
if let Some(idx) = args.iter().position(|a| a == "--smoke-workload") {
if let Some(name) = args.get(idx + 1) {
smoke_workload(name, &cfg);
return;
}
}
if args.iter().any(|a| a == "--headless") {
headless_run(cfg);
return;
}
println!("adlerbench — {} workloads configured", cfg.workloads.len());
for w in &cfg.workloads {
println!(" {} {}s cores={}", w.name, w.duration_secs, w.cores);
}
}
fn dump_sensors() {
let s = sensors::snapshot(None, 0.0);
println!("vCore : {}", opt_volts(s.vcore));
println!("Pkg temp : {}", opt_temp(s.pkg_temp));
println!("Pkg power : {} (None on first tick — no delta yet)", opt_watts(s.pkg_power));
println!("Peak clock : {}", opt_khz(s.peak_clock_khz));
println!("RAPL energy: {}", opt_uj(s.energy_uj()));
println!("Cores : {} logical", s.core_freqs.len());
}
/// `--headless` mode: drive the run loop without a TUI. Prints one line
/// per workload (the human summary) 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 = {
// Read poll_ms from the run's config via the cfg we still own —
// simpler than exposing it on Run.
// (We moved cfg into run::Run::new, so re-load to read poll_ms.
// A bit awkward; if it matters, expose cfg on Run later.)
config::load().poll_ms
};
let poll = std::time::Duration::from_millis(poll_ms);
let mut summaries: Vec<String> = Vec::new();
loop {
match run.tick() {
Ok(run::TickOutcome::WorkloadStarted { idx, name, duration_secs }) => {
println!("[{idx}] starting {name} ({duration_secs}s)");
}
Ok(run::TickOutcome::SampleTaken { latest }) => {
// In headless mode we don't print every sample — too
// noisy. Print one dot per tick to stderr so the user
// sees progress.
eprint!(".");
let _ = latest;
}
Ok(run::TickOutcome::WorkloadFinished { idx, name, verdict, report_path }) => {
eprintln!();
println!("[{idx}] finished {name}: {:?}", verdict);
println!(" report: {}", report_path.display());
// Re-read the report we just wrote + print its summary.
// (The run owns the Report; we don't have a borrow to it
// here. For the summary we'd need Run to expose it. For
// now, skip the summary in headless mode — the verdict +
// path are the useful parts.)
summaries.push(name);
}
Ok(run::TickOutcome::SweepDone) => {
println!("sweep complete: {} workloads", summaries.len());
break;
}
Ok(run::TickOutcome::Idle) => {}
Err(e) => {
eprintln!("run error: {e}");
break;
}
}
thread::sleep(poll);
}
}
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();
println!("smoke: verdict = {:?}", verdict);
}
Err(e) => println!("smoke: start failed: {e}"),
}
}
fn opt_volts(v: Option<f64>) -> String {
match v {
Some(x) => format!("{:.3} V", x),
None => "unreadable".to_string(),
}
}
fn opt_temp(v: Option<f64>) -> String {
match v {
Some(x) => format!("{:.1} C", x),
None => "unreadable".to_string(),
}
}
fn opt_watts(v: Option<f64>) -> String {
match v {
Some(x) => format!("{:.1} W", x),
None => "unreadable".to_string(),
}
}
fn opt_khz(v: Option<u64>) -> String {
match v {
Some(x) => format!("{:.3} GHz", x as f64 / 1_000_000.0),
None => "unreadable".to_string(),
}
}
fn opt_uj(v: Option<u64>) -> String {
match v {
Some(x) => format!("{} uJ", x),
None => "unreadable (root-only without adlermon udev rule + adm group)".to_string(),
}
}