mod config; mod report; mod run; mod sensors; mod workload; fn main() { let cfg = config::load(); let args: Vec = 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 = 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) -> String { match v { Some(x) => format!("{:.3} V", x), None => "unreadable".to_string(), } } fn opt_temp(v: Option) -> String { match v { Some(x) => format!("{:.1} C", x), None => "unreadable".to_string(), } } fn opt_watts(v: Option) -> String { match v { Some(x) => format!("{:.1} W", x), None => "unreadable".to_string(), } } fn opt_khz(v: Option) -> String { match v { Some(x) => format!("{:.3} GHz", x as f64 / 1_000_000.0), None => "unreadable".to_string(), } } fn opt_uj(v: Option) -> String { match v { Some(x) => format!("{} uJ", x), None => "unreadable (root-only without adlermon udev rule + adm group)".to_string(), } }