josie / alder-tools

mod config;
mod report;
mod run;
mod workload;

fn main() {
    let cfg = config::load();
    let args: Vec<String> = std::env::args().collect();
    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);
    }
}

/// `--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::<Vec<_>>()
                .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}"),
    }
}