josie / alder-tools

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(),
    }
}