Skip to main content

cgp/analysis/
baseline.rs

1//! `cgp baseline` — Save and load performance baselines.
2//! Baselines stored in `.cgp-baselines/` directory.
3
4use crate::metrics::catalog::FullProfile;
5use crate::metrics::export;
6use anyhow::Result;
7use std::path::{Path, PathBuf};
8
9/// Default baseline directory.
10fn baseline_dir() -> PathBuf {
11    PathBuf::from(".cgp-baselines")
12}
13
14/// Save a profile as a named baseline.
15pub fn save_baseline(name: &str, profile: &FullProfile) -> Result<PathBuf> {
16    let dir = baseline_dir();
17    std::fs::create_dir_all(&dir)?;
18    let path = dir.join(format!("{name}.json"));
19    export::export_json(profile, &path)?;
20    Ok(path)
21}
22
23/// Load a named baseline.
24pub fn load_baseline(name: &str) -> Result<FullProfile> {
25    let path = baseline_dir().join(format!("{name}.json"));
26    if !path.exists() {
27        anyhow::bail!(
28            "Baseline '{name}' not found at {}.\n  \
29             Available baselines: {}",
30            path.display(),
31            list_baselines().join(", ")
32        );
33    }
34    export::load_json(&path)
35}
36
37/// List all saved baselines.
38pub fn list_baselines() -> Vec<String> {
39    let dir = baseline_dir();
40    if !dir.exists() {
41        return vec![];
42    }
43    std::fs::read_dir(dir)
44        .ok()
45        .map(|entries| {
46            entries
47                .filter_map(|e| e.ok())
48                .filter_map(|e| {
49                    let path = e.path();
50                    if path.extension().is_some_and(|ext| ext == "json") {
51                        path.file_stem().and_then(|s| s.to_str()).map(String::from)
52                    } else {
53                        None
54                    }
55                })
56                .collect()
57        })
58        .unwrap_or_default()
59}
60
61/// Run the `cgp baseline` command.
62pub fn run_baseline(save: Option<&str>, load: Option<&str>) -> Result<()> {
63    match (save, load) {
64        (Some(path), None) => {
65            // Save: the path should be a JSON profile to save as baseline
66            let profile = export::load_json(Path::new(path))?;
67
68            // Use filename stem as baseline name
69            let name = Path::new(path)
70                .file_stem()
71                .and_then(|s| s.to_str())
72                .unwrap_or("default");
73
74            let saved_path = save_baseline(name, &profile)?;
75            println!("Baseline '{name}' saved to {}", saved_path.display());
76        }
77        (None, Some(name)) => {
78            // Load and display
79            let profile = load_baseline(name)?;
80            println!("Baseline '{name}':");
81            println!("  Time: {:.1} us", profile.timing.wall_clock_time_us);
82            println!("  TFLOP/s: {:.1}", profile.throughput.tflops);
83            if let Some(k) = &profile.kernel {
84                println!("  Kernel: {}", k.name);
85            }
86            if let Some(gpu) = &profile.hardware.gpu {
87                println!("  GPU: {gpu}");
88            }
89            if let Some(health) = &profile.system_health {
90                println!(
91                    "  System: {:.0}°C, {:.0}W, {:.0} MHz",
92                    health.gpu_temperature_celsius, health.gpu_power_watts, health.gpu_clock_mhz
93                );
94            }
95            if let Some(vram) = &profile.vram {
96                println!(
97                    "  VRAM: {:.0}/{:.0} MB ({:.1}%)",
98                    vram.vram_used_mb, vram.vram_total_mb, vram.vram_utilization_pct
99                );
100            }
101            println!("  Timestamp: {}", profile.timestamp);
102        }
103        (None, None) => {
104            // List baselines
105            let baselines = list_baselines();
106            if baselines.is_empty() {
107                println!("No baselines saved yet.");
108                println!("  Save one with: cgp baseline --save <profile.json>");
109            } else {
110                println!("Saved baselines:");
111                for name in &baselines {
112                    let path = baseline_dir().join(format!("{name}.json"));
113                    let size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
114                    println!("  {name:20} ({:.1} KB)", size as f64 / 1024.0);
115                }
116            }
117        }
118        (Some(_), Some(_)) => {
119            anyhow::bail!("Specify either --save or --load, not both");
120        }
121    }
122    Ok(())
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::metrics::catalog::*;
129
130    #[test]
131    fn test_save_and_load_baseline() {
132        let profile = FullProfile {
133            version: "2.0".to_string(),
134            timing: TimingMetrics {
135                wall_clock_time_us: 23.2,
136                samples: 50,
137                ..Default::default()
138            },
139            throughput: ThroughputMetrics {
140                tflops: 11.6,
141                ..Default::default()
142            },
143            ..Default::default()
144        };
145
146        // Use a temp directory
147        let dir = tempfile::tempdir().unwrap();
148        let path = dir.path().join("test.json");
149        export::export_json(&profile, &path).unwrap();
150
151        let loaded = export::load_json(&path).unwrap();
152        assert!((loaded.timing.wall_clock_time_us - 23.2).abs() < 0.01);
153    }
154
155    #[test]
156    fn test_list_baselines_empty() {
157        // When no .cgp-baselines dir exists, should return empty
158        let names = list_baselines();
159        // May or may not be empty depending on test environment
160        assert!(names.len() < 1000);
161    }
162}