josie / alder-tools

mod config;
mod sensors;
mod workload;

fn main() {
    let cfg = config::load();
    let args: Vec<String> = std::env::args().collect();
    // `adlerbench --dump` = one-shot sensor snapshot (spike/verification
    // tool, mirrors adlermon's dump mode). Default = config summary.
    if args.iter().any(|a| a == "--dump") {
        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());
        return;
    }
    // `adlerbench --smoke-workload <name>` = run one workload for 3s and
    // print its verdict. Verifies the Workload trait end-to-end without
    // needing the run loop / TUI.
    if let Some(idx) = args.iter().position(|a| a == "--smoke-workload") {
        if let Some(name) = args.get(idx + 1) {
            smoke_workload(name, &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 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(),
    }
}