simplebench_runtime/
cpu_analysis.rs

1//! CPU analysis for detecting thermal throttling, frequency variance, and cold starts
2
3use crate::CpuSnapshot;
4
5#[derive(Debug, Clone)]
6pub struct FrequencyStats {
7    pub min_mhz: f64,
8    pub max_mhz: f64,
9    pub mean_mhz: f64,
10    pub stddev_mhz: f64,
11    pub variance_percent: f64, // (max - min) / mean * 100
12}
13
14#[derive(Debug, Clone)]
15pub struct TemperatureStats {
16    pub min_celsius: f64,
17    pub max_celsius: f64,
18    pub mean_celsius: f64,
19    pub increase_celsius: f64, // max - min
20}
21
22#[derive(Debug, Clone)]
23pub enum CpuWarning {
24    ColdStart {
25        initial_temp_celsius: f64,
26    },
27    ThermalThrottling {
28        temp_increase_celsius: f64,
29        max_temp_celsius: f64,
30    },
31    FrequencyVariance {
32        variance_percent: f64,
33    },
34    LowFrequency {
35        mean_mhz: f64,
36        max_available_mhz: f64,
37        percent_of_max: f64,
38    },
39}
40
41impl CpuWarning {
42    pub fn format(&self) -> String {
43        match self {
44            CpuWarning::ColdStart {
45                initial_temp_celsius,
46            } => {
47                format!(
48                    "⚠ Cold start detected (initial: {:.0}°C)",
49                    initial_temp_celsius
50                )
51            }
52            CpuWarning::ThermalThrottling {
53                temp_increase_celsius,
54                max_temp_celsius,
55            } => {
56                format!(
57                    "⚠ Thermal throttling detected (+{:.0}°C, max: {:.0}°C)",
58                    temp_increase_celsius, max_temp_celsius
59                )
60            }
61            CpuWarning::FrequencyVariance { variance_percent } => {
62                format!(
63                    "⚠ Frequency variance detected ({:.1}% variance)",
64                    variance_percent
65                )
66            }
67            CpuWarning::LowFrequency {
68                mean_mhz,
69                max_available_mhz,
70                percent_of_max,
71            } => {
72                format!(
73                    "⚠ Low frequency detected ({:.0} MHz, {:.0}% of max {:.0} MHz)",
74                    mean_mhz, percent_of_max, max_available_mhz
75                )
76            }
77        }
78    }
79}
80
81#[derive(Debug, Clone)]
82pub struct CpuAnalysis {
83    pub frequency_stats: Option<FrequencyStats>,
84    pub temperature_stats: Option<TemperatureStats>,
85    pub warnings: Vec<CpuWarning>,
86}
87
88impl CpuAnalysis {
89    /// Analyze CPU snapshots and detect anomalies
90    pub fn from_snapshots(snapshots: &[CpuSnapshot], max_freq_khz: Option<u64>) -> Self {
91        let mut warnings = Vec::new();
92
93        // Collect frequency data
94        let frequencies: Vec<f64> = snapshots.iter().filter_map(|s| s.frequency_mhz()).collect();
95
96        // Collect temperature data
97        let temperatures: Vec<f64> = snapshots
98            .iter()
99            .filter_map(|s| s.temperature_celsius())
100            .collect();
101
102        // Calculate frequency statistics
103        let frequency_stats = if !frequencies.is_empty() {
104            let min_mhz = frequencies.iter().copied().fold(f64::INFINITY, f64::min);
105            let max_mhz = frequencies
106                .iter()
107                .copied()
108                .fold(f64::NEG_INFINITY, f64::max);
109            let mean_mhz = frequencies.iter().sum::<f64>() / frequencies.len() as f64;
110
111            // Calculate standard deviation
112            let variance = frequencies
113                .iter()
114                .map(|&f| {
115                    let diff = f - mean_mhz;
116                    diff * diff
117                })
118                .sum::<f64>()
119                / frequencies.len() as f64;
120            let stddev_mhz = variance.sqrt();
121
122            // Variance as percentage of mean
123            let variance_percent = if mean_mhz > 0.0 {
124                ((max_mhz - min_mhz) / mean_mhz) * 100.0
125            } else {
126                0.0
127            };
128
129            // Detect frequency variance (>10%)
130            if variance_percent > 10.0 {
131                warnings.push(CpuWarning::FrequencyVariance { variance_percent });
132            }
133
134            // Detect low frequency (<50% of max)
135            if let Some(max_freq_khz) = max_freq_khz {
136                let max_available_mhz = max_freq_khz as f64 / 1000.0;
137                let percent_of_max = (mean_mhz / max_available_mhz) * 100.0;
138
139                if percent_of_max < 50.0 {
140                    warnings.push(CpuWarning::LowFrequency {
141                        mean_mhz,
142                        max_available_mhz,
143                        percent_of_max,
144                    });
145                }
146            }
147
148            Some(FrequencyStats {
149                min_mhz,
150                max_mhz,
151                mean_mhz,
152                stddev_mhz,
153                variance_percent,
154            })
155        } else {
156            None
157        };
158
159        // Calculate temperature statistics
160        let temperature_stats = if !temperatures.is_empty() {
161            let min_celsius = temperatures.iter().copied().fold(f64::INFINITY, f64::min);
162            let max_celsius = temperatures
163                .iter()
164                .copied()
165                .fold(f64::NEG_INFINITY, f64::max);
166            let mean_celsius = temperatures.iter().sum::<f64>() / temperatures.len() as f64;
167            let increase_celsius = max_celsius - min_celsius;
168
169            // Detect cold start (initial temp <50°C)
170            if let Some(&initial_temp) = temperatures.first() {
171                if initial_temp < 50.0 {
172                    warnings.push(CpuWarning::ColdStart {
173                        initial_temp_celsius: initial_temp,
174                    });
175                }
176            }
177
178            // Detect thermal throttling (>20°C increase or >85°C max)
179            if increase_celsius > 20.0 || max_celsius > 85.0 {
180                warnings.push(CpuWarning::ThermalThrottling {
181                    temp_increase_celsius: increase_celsius,
182                    max_temp_celsius: max_celsius,
183                });
184            }
185
186            Some(TemperatureStats {
187                min_celsius,
188                max_celsius,
189                mean_celsius,
190                increase_celsius,
191            })
192        } else {
193            None
194        };
195
196        CpuAnalysis {
197            frequency_stats,
198            temperature_stats,
199            warnings,
200        }
201    }
202
203    /// Format stats as a single-line string
204    pub fn format_stats_line(&self) -> Option<String> {
205        let mut parts = Vec::new();
206
207        if let Some(ref freq) = self.frequency_stats {
208            parts.push(format!(
209                "{:.0}-{:.0} MHz (mean: {:.0} MHz, variance: {:.1}%)",
210                freq.min_mhz, freq.max_mhz, freq.mean_mhz, freq.variance_percent
211            ));
212        }
213
214        if let Some(ref temp) = self.temperature_stats {
215            parts.push(format!(
216                "{:.0}-{:.0}°C (increase: +{:.0}°C)",
217                temp.min_celsius, temp.max_celsius, temp.increase_celsius
218            ));
219        }
220
221        if parts.is_empty() {
222            None
223        } else {
224            Some(parts.join(", "))
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use std::time::Instant;
233
234    #[test]
235    fn test_frequency_analysis() {
236        let snapshots = vec![
237            CpuSnapshot {
238                timestamp: Instant::now(),
239                frequency_khz: Some(4_000_000), // 4000 MHz
240                temperature_millic: None,
241            },
242            CpuSnapshot {
243                timestamp: Instant::now(),
244                frequency_khz: Some(4_500_000), // 4500 MHz
245                temperature_millic: None,
246            },
247            CpuSnapshot {
248                timestamp: Instant::now(),
249                frequency_khz: Some(4_600_000), // 4600 MHz
250                temperature_millic: None,
251            },
252        ];
253
254        let analysis = CpuAnalysis::from_snapshots(&snapshots, Some(5_000_000));
255
256        assert!(analysis.frequency_stats.is_some());
257        let freq_stats = analysis.frequency_stats.unwrap();
258        assert_eq!(freq_stats.min_mhz, 4000.0);
259        assert_eq!(freq_stats.max_mhz, 4600.0);
260        assert!((freq_stats.mean_mhz - 4366.67).abs() < 1.0);
261    }
262
263    #[test]
264    fn test_cold_start_detection() {
265        let snapshots = vec![
266            CpuSnapshot {
267                timestamp: Instant::now(),
268                frequency_khz: None,
269                temperature_millic: Some(45_000), // 45°C - cold start
270            },
271            CpuSnapshot {
272                timestamp: Instant::now(),
273                frequency_khz: None,
274                temperature_millic: Some(55_000), // 55°C
275            },
276        ];
277
278        let analysis = CpuAnalysis::from_snapshots(&snapshots, None);
279
280        assert!(!analysis.warnings.is_empty());
281        assert!(matches!(analysis.warnings[0], CpuWarning::ColdStart { .. }));
282    }
283
284    #[test]
285    fn test_frequency_variance_detection() {
286        let snapshots = vec![
287            CpuSnapshot {
288                timestamp: Instant::now(),
289                frequency_khz: Some(2_000_000), // 2000 MHz
290                temperature_millic: None,
291            },
292            CpuSnapshot {
293                timestamp: Instant::now(),
294                frequency_khz: Some(4_500_000), // 4500 MHz - large variance
295                temperature_millic: None,
296            },
297        ];
298
299        let analysis = CpuAnalysis::from_snapshots(&snapshots, None);
300
301        assert!(!analysis.warnings.is_empty());
302        let has_variance_warning = analysis
303            .warnings
304            .iter()
305            .any(|w| matches!(w, CpuWarning::FrequencyVariance { .. }));
306        assert!(has_variance_warning);
307    }
308
309    #[test]
310    fn test_thermal_throttling_detection() {
311        let snapshots = vec![
312            CpuSnapshot {
313                timestamp: Instant::now(),
314                frequency_khz: None,
315                temperature_millic: Some(60_000), // 60°C
316            },
317            CpuSnapshot {
318                timestamp: Instant::now(),
319                frequency_khz: None,
320                temperature_millic: Some(90_000), // 90°C - throttling
321            },
322        ];
323
324        let analysis = CpuAnalysis::from_snapshots(&snapshots, None);
325
326        assert!(!analysis.warnings.is_empty());
327        let has_throttling_warning = analysis
328            .warnings
329            .iter()
330            .any(|w| matches!(w, CpuWarning::ThermalThrottling { .. }));
331        assert!(has_throttling_warning);
332    }
333}