Skip to main content

cgp/analysis/
compete.rs

1//! `cgp compete` — Head-to-head competitor comparison.
2//! Spec section 2.5: run two or more commands, measure wall time,
3//! compute TFLOP/s, and produce a comparison table.
4
5use anyhow::Result;
6use serde::Serialize;
7use std::process::Command;
8use std::time::Instant;
9
10/// Result of running one competitor.
11#[derive(Debug, Clone, Serialize)]
12pub struct CompetitorResult {
13    pub label: String,
14    pub command: String,
15    pub wall_time_ms: f64,
16    pub exit_code: i32,
17}
18
19/// Run a command and measure wall time.
20fn run_timed(command: &str) -> Result<CompetitorResult> {
21    // Split command into program + args (basic shell-like splitting)
22    let parts: Vec<&str> = command.split_whitespace().collect();
23    if parts.is_empty() {
24        anyhow::bail!("Empty command");
25    }
26
27    let start = Instant::now();
28    let status = Command::new(parts[0])
29        .args(&parts[1..])
30        .stdout(std::process::Stdio::null())
31        .stderr(std::process::Stdio::null())
32        .status()
33        .map_err(|e| anyhow::anyhow!("Failed to run '{}': {}", parts[0], e))?;
34    let elapsed = start.elapsed();
35
36    Ok(CompetitorResult {
37        label: String::new(),
38        command: command.to_string(),
39        wall_time_ms: elapsed.as_secs_f64() * 1000.0,
40        exit_code: status.code().unwrap_or(-1),
41    })
42}
43
44/// Run the `cgp compete` command.
45pub fn run_compete(
46    workload: &str,
47    ours: &str,
48    theirs: &[String],
49    label: Option<&str>,
50    json: bool,
51) -> Result<()> {
52    let labels = build_labels(label, theirs.len());
53
54    println!("\n=== CGP Head-to-Head: {workload} ===\n");
55
56    let mut results: Vec<CompetitorResult> = Vec::new();
57    let default_ours = "ours".to_string();
58    let ours_label = labels.first().unwrap_or(&default_ours).clone();
59    results.push(run_and_record(&ours_label, ours));
60
61    for (i, cmd) in theirs.iter().enumerate() {
62        let default_theirs = format!("theirs-{}", i + 1);
63        let lbl = labels.get(i + 1).unwrap_or(&default_theirs).clone();
64        results.push(run_and_record(&lbl, cmd));
65    }
66
67    if json {
68        println!("{}", serde_json::to_string_pretty(&results)?);
69        return Ok(());
70    }
71
72    print_compete_table(&results);
73    print_compete_winner(&results);
74    println!();
75    Ok(())
76}
77
78/// Build label vector: parse comma-separated overrides, or synthesize defaults.
79fn build_labels(label: Option<&str>, theirs_len: usize) -> Vec<String> {
80    match label {
81        Some(l) => l.split(',').map(String::from).collect(),
82        None => {
83            let mut v = vec!["ours".to_string()];
84            v.extend((0..theirs_len).map(|i| format!("theirs-{}", i + 1)));
85            v
86        }
87    }
88}
89
90/// Run a single competitor and push a `CompetitorResult` with failure handling.
91fn run_and_record(label: &str, command: &str) -> CompetitorResult {
92    eprint!("  Running: {label} ...");
93    match run_timed(command) {
94        Ok(mut r) => {
95            r.label = label.to_string();
96            eprintln!(" {:.1}ms", r.wall_time_ms);
97            r
98        }
99        Err(e) => {
100            eprintln!(" FAILED: {e}");
101            CompetitorResult {
102                label: label.to_string(),
103                command: command.to_string(),
104                wall_time_ms: f64::INFINITY,
105                exit_code: -1,
106            }
107        }
108    }
109}
110
111fn best_finite_time(results: &[CompetitorResult]) -> f64 {
112    results
113        .iter()
114        .filter(|r| r.wall_time_ms.is_finite())
115        .map(|r| r.wall_time_ms)
116        .fold(f64::INFINITY, f64::min)
117}
118
119fn print_compete_table(results: &[CompetitorResult]) {
120    let best_time = best_finite_time(results);
121    println!();
122    println!(
123        "  {:20} {:>12} {:>8} {:>10}",
124        "Competitor", "Time (ms)", "Exit", "vs Best"
125    );
126    println!("  {}", "-".repeat(54));
127    for r in results {
128        let ratio = if best_time > 0.0 && r.wall_time_ms.is_finite() {
129            format!("{:.2}x", r.wall_time_ms / best_time)
130        } else {
131            "N/A".to_string()
132        };
133        let time_str = if r.wall_time_ms.is_finite() {
134            format!("{:.1}", r.wall_time_ms)
135        } else {
136            "FAILED".to_string()
137        };
138        println!(
139            "  {:20} {:>12} {:>8} {:>10}",
140            r.label, time_str, r.exit_code, ratio
141        );
142    }
143}
144
145fn print_compete_winner(results: &[CompetitorResult]) {
146    let Some(winner) = results
147        .iter()
148        .filter(|r| r.wall_time_ms.is_finite())
149        .min_by(|a, b| {
150            a.wall_time_ms
151                .partial_cmp(&b.wall_time_ms)
152                .unwrap_or(std::cmp::Ordering::Equal)
153        })
154    else {
155        return;
156    };
157    println!(
158        "\n  Winner: {} ({:.1}ms)",
159        winner.label, winner.wall_time_ms
160    );
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_run_timed_true() {
169        let result = run_timed("true").unwrap();
170        assert_eq!(result.exit_code, 0);
171        assert!(result.wall_time_ms < 1000.0);
172    }
173
174    #[test]
175    fn test_run_timed_false() {
176        let result = run_timed("false").unwrap();
177        assert_ne!(result.exit_code, 0);
178    }
179
180    #[test]
181    fn test_run_timed_nonexistent() {
182        let result = run_timed("nonexistent_binary_xyz_123");
183        assert!(result.is_err());
184    }
185}