1use std::fs;
4
5#[derive(Debug, Clone)]
7pub struct EnvironmentReport {
8 pub thermal_state: ThermalState,
9 pub power_state: PowerState,
10 pub memory_pressure: MemoryPressure,
11 pub cpu_usage: f64,
12 pub warnings: Vec<String>,
13 pub errors: Vec<String>,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum ThermalState {
18 Normal,
19 Warm,
20 Hot,
21 Critical,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum PowerState {
26 AC, Battery, LowBattery, Unknown,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub enum MemoryPressure {
34 Normal,
35 Moderate,
36 High,
37 Critical,
38}
39
40impl EnvironmentReport {
41 pub fn is_suitable_for_benchmarking(&self) -> bool {
43 self.errors.is_empty()
44 && self.thermal_state != ThermalState::Critical
45 && self.power_state != PowerState::LowBattery
46 && self.memory_pressure != MemoryPressure::Critical
47 && self.cpu_usage < 50.0
48 }
49
50 pub fn summary(&self) -> String {
52 let mut parts = vec![
53 format!("Thermal: {:?}", self.thermal_state),
54 format!("Power: {:?}", self.power_state),
55 format!("Memory: {:?}", self.memory_pressure),
56 format!("CPU: {:.1}%", self.cpu_usage),
57 ];
58
59 if !self.warnings.is_empty() {
60 parts.push(format!("Warnings: {}", self.warnings.len()));
61 }
62
63 if !self.errors.is_empty() {
64 parts.push(format!("Errors: {}", self.errors.len()));
65 }
66
67 parts.join(", ")
68 }
69}
70
71pub fn validate_benchmark_environment() -> EnvironmentReport {
73 let mut report = EnvironmentReport {
74 thermal_state: ThermalState::Normal,
75 power_state: PowerState::Unknown,
76 memory_pressure: MemoryPressure::Normal,
77 cpu_usage: 0.0,
78 warnings: Vec::new(),
79 errors: Vec::new(),
80 };
81
82 report.thermal_state = check_thermal_state(&mut report.warnings, &mut report.errors);
84
85 report.power_state = check_power_state(&mut report.warnings, &mut report.errors);
87
88 report.memory_pressure = check_memory_pressure(&mut report.warnings, &mut report.errors);
90
91 report.cpu_usage = check_cpu_usage(&mut report.warnings, &mut report.errors);
93
94 #[cfg(target_os = "macos")]
96 {
97 check_macos_specific(&mut report.warnings, &mut report.errors);
98 }
99
100 report
101}
102
103fn check_thermal_state(warnings: &mut Vec<String>, errors: &mut Vec<String>) -> ThermalState {
105 #[cfg(target_os = "macos")]
106 {
107 if let Ok(output) = std::process::Command::new("pmset")
109 .args(["-g", "thermlog"])
110 .output()
111 {
112 let output_str = String::from_utf8_lossy(&output.stdout);
113
114 if output_str.contains("CPU_Speed_Limit") {
116 let state = if output_str.contains("100") {
117 ThermalState::Normal
118 } else if output_str.contains("75") {
119 warnings.push("CPU thermal throttling detected (75%)".to_string());
120 ThermalState::Warm
121 } else if output_str.contains("50") {
122 warnings.push("Significant CPU thermal throttling (50%)".to_string());
123 ThermalState::Hot
124 } else {
125 errors.push("Critical CPU thermal throttling detected".to_string());
126 ThermalState::Critical
127 };
128
129 return state;
130 }
131 }
132
133 if let Ok(output) = std::process::Command::new("system_profiler")
135 .args(["SPHardwareDataType"])
136 .output()
137 {
138 let output_str = String::from_utf8_lossy(&output.stdout);
139 ThermalState::Normal
141 } else {
142 warnings.push("Could not determine thermal state".to_string());
143 ThermalState::Normal
144 }
145 }
146
147 #[cfg(target_os = "linux")]
148 {
149 let mut max_temp = 0;
151 let mut found_temp = false;
152
153 for entry in fs::read_dir("/sys/class/thermal").unwrap_or_else(|_| {
154 warnings.push("Could not access thermal information".to_string());
155 fs::read_dir("/tmp").unwrap()}) {
157 if let Ok(entry) = entry {
158 let path = entry.path().join("temp");
159 if let Ok(temp_str) = fs::read_to_string(&path) {
160 if let Ok(temp) = temp_str.trim().parse::<u32>() {
161 let temp_c = temp / 1000;
163 max_temp = max_temp.max(temp_c);
164 found_temp = true;
165 }
166 }
167 }
168 }
169
170 if found_temp {
171 if max_temp < 60 {
172 ThermalState::Normal
173 } else if max_temp < 80 {
174 warnings.push(format!("Elevated CPU temperature: {max_temp}°C"));
175 ThermalState::Warm
176 } else if max_temp < 95 {
177 warnings.push(format!("High CPU temperature: {max_temp}°C"));
178 ThermalState::Hot
179 } else {
180 errors.push(format!("Critical CPU temperature: {max_temp}°C"));
181 ThermalState::Critical
182 }
183 } else {
184 warnings.push("Could not determine CPU temperature".to_string());
185 ThermalState::Normal
186 }
187 }
188
189 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
190 {
191 warnings.push("Thermal monitoring not supported on this platform".to_string());
192 ThermalState::Normal
193 }
194}
195
196fn check_power_state(warnings: &mut Vec<String>, _errors: &mut Vec<String>) -> PowerState {
198 #[cfg(target_os = "macos")]
199 {
200 if let Ok(output) = std::process::Command::new("pmset")
201 .args(["-g", "ps"])
202 .output()
203 {
204 let output_str = String::from_utf8_lossy(&output.stdout);
205
206 if output_str.contains("AC Power") {
207 PowerState::AC
208 } else if output_str.contains("Battery Power") {
209 if let Ok(battery_output) = std::process::Command::new("pmset")
211 .args(["-g", "batt"])
212 .output()
213 {
214 let battery_str = String::from_utf8_lossy(&battery_output.stdout);
215
216 if let Some(percent_match) = battery_str.find('%') {
218 let start = battery_str[..percent_match].rfind(' ').unwrap_or(0);
219 if let Ok(percentage) = battery_str[start..percent_match].trim().parse::<u32>() {
220 if percentage < 20 {
221 warnings.push(format!("Low battery: {}%", percentage));
222 return PowerState::LowBattery;
223 } else if percentage < 50 {
224 warnings.push(format!("Battery power: {}%", percentage));
225 }
226 }
227 }
228 }
229 PowerState::Battery
230 } else {
231 PowerState::Unknown
232 }
233 } else {
234 warnings.push("Could not determine power state".to_string());
235 PowerState::Unknown
236 }
237 }
238
239 #[cfg(not(target_os = "macos"))]
240 {
241 PowerState::AC
243 }
244}
245
246fn check_memory_pressure(warnings: &mut Vec<String>, errors: &mut Vec<String>) -> MemoryPressure {
248 #[cfg(target_os = "macos")]
249 {
250 if let Ok(output) = std::process::Command::new("memory_pressure")
251 .output()
252 {
253 let output_str = String::from_utf8_lossy(&output.stdout);
254
255 if output_str.contains("System-wide memory free percentage:") {
256 if let Some(line) = output_str.lines()
258 .find(|line| line.contains("System-wide memory free percentage:"))
259 {
260 if let Some(percent_str) = line.split(':').nth(1) {
261 if let Ok(free_percent) = percent_str.trim().trim_end_matches('%').parse::<f64>() {
262 if free_percent > 50.0 {
263 return MemoryPressure::Normal;
264 } else if free_percent > 25.0 {
265 warnings.push(format!("Moderate memory pressure: {:.1}% free", free_percent));
266 return MemoryPressure::Moderate;
267 } else if free_percent > 10.0 {
268 warnings.push(format!("High memory pressure: {:.1}% free", free_percent));
269 return MemoryPressure::High;
270 } else {
271 errors.push(format!("Critical memory pressure: {:.1}% free", free_percent));
272 return MemoryPressure::Critical;
273 }
274 }
275 }
276 }
277 }
278 }
279
280 if let Ok(output) = std::process::Command::new("vm_stat").output() {
282 let output_str = String::from_utf8_lossy(&output.stdout);
283 MemoryPressure::Normal
286 } else {
287 warnings.push("Could not determine memory pressure".to_string());
288 MemoryPressure::Normal
289 }
290 }
291
292 #[cfg(target_os = "linux")]
293 {
294 if let Ok(meminfo) = fs::read_to_string("/proc/meminfo") {
295 let mut mem_total = 0;
296 let mut mem_available = 0;
297
298 for line in meminfo.lines() {
299 if line.starts_with("MemTotal:") {
300 mem_total = line.split_whitespace().nth(1)
301 .and_then(|s| s.parse().ok())
302 .unwrap_or(0);
303 } else if line.starts_with("MemAvailable:") {
304 mem_available = line.split_whitespace().nth(1)
305 .and_then(|s| s.parse().ok())
306 .unwrap_or(0);
307 }
308 }
309
310 if mem_total > 0 {
311 let available_percent = (mem_available as f64 / mem_total as f64) * 100.0;
312
313 if available_percent > 50.0 {
314 MemoryPressure::Normal
315 } else if available_percent > 25.0 {
316 warnings.push(format!("Moderate memory pressure: {available_percent:.1}% available"));
317 MemoryPressure::Moderate
318 } else if available_percent > 10.0 {
319 warnings.push(format!("High memory pressure: {available_percent:.1}% available"));
320 MemoryPressure::High
321 } else {
322 errors.push(format!("Critical memory pressure: {available_percent:.1}% available"));
323 MemoryPressure::Critical
324 }
325 } else {
326 warnings.push("Could not parse memory information".to_string());
327 MemoryPressure::Normal
328 }
329 } else {
330 warnings.push("Could not read memory information".to_string());
331 MemoryPressure::Normal
332 }
333 }
334
335 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
336 {
337 warnings.push("Memory pressure monitoring not supported on this platform".to_string());
338 MemoryPressure::Normal
339 }
340}
341
342fn check_cpu_usage(warnings: &mut Vec<String>, _errors: &mut Vec<String>) -> f64 {
344 #[cfg(target_os = "macos")]
345 {
346 if let Ok(output) = std::process::Command::new("top")
347 .args(["-l", "1", "-n", "0"])
348 .output()
349 {
350 let output_str = String::from_utf8_lossy(&output.stdout);
351
352 if let Some(line) = output_str.lines()
354 .find(|line| line.contains("CPU usage:"))
355 {
356 if let Some(idle_part) = line.split(',').find(|part| part.contains("idle")) {
357 if let Some(percent_str) = idle_part.split_whitespace().next() {
358 if let Ok(idle_percent) = percent_str.trim_end_matches('%').parse::<f64>() {
359 let usage_percent = 100.0 - idle_percent;
360
361 if usage_percent > 75.0 {
362 warnings.push(format!("High CPU usage: {:.1}%", usage_percent));
363 } else if usage_percent > 50.0 {
364 warnings.push(format!("Moderate CPU usage: {:.1}%", usage_percent));
365 }
366
367 return usage_percent;
368 }
369 }
370 }
371 }
372 }
373 }
374
375 #[cfg(target_os = "linux")]
376 {
377 if let Ok(loadavg) = fs::read_to_string("/proc/loadavg") {
379 if let Some(load_str) = loadavg.split_whitespace().next() {
380 if let Ok(load) = load_str.parse::<f64>() {
381 let usage_percent = load * 100.0;
383
384 if usage_percent > 75.0 {
385 warnings.push(format!("High system load: {load:.2}"));
386 } else if usage_percent > 50.0 {
387 warnings.push(format!("Moderate system load: {load:.2}"));
388 }
389
390 return usage_percent.min(100.0);
391 }
392 }
393 }
394 }
395
396 warnings.push("Could not determine CPU usage".to_string());
397 0.0
398}
399
400#[cfg(target_os = "macos")]
402fn check_macos_specific(warnings: &mut Vec<String>, _errors: &mut Vec<String>) {
403 if let Ok(output) = std::process::Command::new("mdutil")
405 .args(["-s", "/"])
406 .output()
407 {
408 let output_str = String::from_utf8_lossy(&output.stdout);
409 if output_str.contains("Indexing enabled") && !output_str.contains("Indexing disabled") {
410 if let Ok(_) = std::process::Command::new("pgrep")
412 .args(["mds"])
413 .output()
414 {
415 warnings.push("Spotlight indexing may be active".to_string());
416 }
417 }
418 }
419
420 if let Ok(output) = std::process::Command::new("tmutil")
422 .args(["currentphase"])
423 .output()
424 {
425 let output_str = String::from_utf8_lossy(&output.stdout);
426 if !output_str.trim().is_empty() && output_str != "BackupNotRunning" {
427 warnings.push("Time Machine backup may be active".to_string());
428 }
429 }
430
431 if let Ok(output) = std::process::Command::new("softwareupdate")
433 .args(["-l"])
434 .output()
435 {
436 if output.status.success() {
437 let output_str = String::from_utf8_lossy(&output.stdout);
438 if !output_str.contains("No new software available") {
439 warnings.push("Software updates available (may cause background activity)".to_string());
440 }
441 }
442 }
443}
444
445pub fn print_environment_report(report: &EnvironmentReport) {
447 println!("=== Benchmark Environment Report ===");
448 println!("Thermal State: {:?}", report.thermal_state);
449 println!("Power State: {:?}", report.power_state);
450 println!("Memory Pressure: {:?}", report.memory_pressure);
451 println!("CPU Usage: {:.1}%", report.cpu_usage);
452
453 if !report.warnings.is_empty() {
454 println!("\nWarnings:");
455 for warning in &report.warnings {
456 println!(" ⚠️ {warning}");
457 }
458 }
459
460 if !report.errors.is_empty() {
461 println!("\nErrors:");
462 for error in &report.errors {
463 println!(" ❌ {error}");
464 }
465 }
466
467 println!("\nSuitable for benchmarking: {}",
468 if report.is_suitable_for_benchmarking() { "✅ Yes" } else { "❌ No" });
469 println!("=====================================");
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn test_environment_validation() {
478 let report = validate_benchmark_environment();
479
480 assert!(!report.summary().is_empty());
482
483 assert!(report.cpu_usage >= 0.0);
485 assert!(report.cpu_usage <= 200.0); }
487
488 #[test]
489 fn test_environment_report_summary() {
490 let report = EnvironmentReport {
491 thermal_state: ThermalState::Normal,
492 power_state: PowerState::AC,
493 memory_pressure: MemoryPressure::Normal,
494 cpu_usage: 25.5,
495 warnings: vec!["Test warning".to_string()],
496 errors: vec![],
497 };
498
499 let summary = report.summary();
500 assert!(summary.contains("Thermal: Normal"));
501 assert!(summary.contains("Power: AC"));
502 assert!(summary.contains("Memory: Normal"));
503 assert!(summary.contains("CPU: 25.5%"));
504 assert!(summary.contains("Warnings: 1"));
505 }
506
507 #[test]
508 fn test_environment_suitability() {
509 let good_report = EnvironmentReport {
511 thermal_state: ThermalState::Normal,
512 power_state: PowerState::AC,
513 memory_pressure: MemoryPressure::Normal,
514 cpu_usage: 10.0,
515 warnings: vec![],
516 errors: vec![],
517 };
518 assert!(good_report.is_suitable_for_benchmarking());
519
520 let bad_report = EnvironmentReport {
522 thermal_state: ThermalState::Critical,
523 power_state: PowerState::LowBattery,
524 memory_pressure: MemoryPressure::Critical,
525 cpu_usage: 90.0,
526 warnings: vec![],
527 errors: vec!["Critical error".to_string()],
528 };
529 assert!(!bad_report.is_suitable_for_benchmarking());
530 }
531}