1use 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, }
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, }
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 pub fn from_snapshots(snapshots: &[CpuSnapshot], max_freq_khz: Option<u64>) -> Self {
91 let mut warnings = Vec::new();
92
93 let frequencies: Vec<f64> = snapshots.iter().filter_map(|s| s.frequency_mhz()).collect();
95
96 let temperatures: Vec<f64> = snapshots
98 .iter()
99 .filter_map(|s| s.temperature_celsius())
100 .collect();
101
102 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 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 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 if variance_percent > 10.0 {
131 warnings.push(CpuWarning::FrequencyVariance { variance_percent });
132 }
133
134 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 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 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 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 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), temperature_millic: None,
241 },
242 CpuSnapshot {
243 timestamp: Instant::now(),
244 frequency_khz: Some(4_500_000), temperature_millic: None,
246 },
247 CpuSnapshot {
248 timestamp: Instant::now(),
249 frequency_khz: Some(4_600_000), 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), },
271 CpuSnapshot {
272 timestamp: Instant::now(),
273 frequency_khz: None,
274 temperature_millic: Some(55_000), },
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), temperature_millic: None,
291 },
292 CpuSnapshot {
293 timestamp: Instant::now(),
294 frequency_khz: Some(4_500_000), 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), },
317 CpuSnapshot {
318 timestamp: Instant::now(),
319 frequency_khz: None,
320 temperature_millic: Some(90_000), },
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}