1use crate::metrics::catalog::FullProfile;
5use crate::metrics::export;
6use anyhow::Result;
7use std::path::{Path, PathBuf};
8
9fn baseline_dir() -> PathBuf {
11 PathBuf::from(".cgp-baselines")
12}
13
14pub 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
23pub 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
37pub 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
61pub fn run_baseline(save: Option<&str>, load: Option<&str>) -> Result<()> {
63 match (save, load) {
64 (Some(path), None) => {
65 let profile = export::load_json(Path::new(path))?;
67
68 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 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 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 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 let names = list_baselines();
159 assert!(names.len() < 1000);
161 }
162}