hft_benchmarks/
environment.rs

1//! Environment validation for consistent benchmarking
2
3use std::fs;
4
5/// Environment validation result
6#[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,           // On AC power
27    Battery,      // On battery
28    LowBattery,   // Low battery
29    Unknown,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub enum MemoryPressure {
34    Normal,
35    Moderate,
36    High,
37    Critical,
38}
39
40impl EnvironmentReport {
41    /// Check if environment is suitable for reliable benchmarking
42    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    /// Get a summary message about the environment
51    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
71/// Validate the current environment for benchmarking
72pub 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    // Check thermal state
83    report.thermal_state = check_thermal_state(&mut report.warnings, &mut report.errors);
84    
85    // Check power state  
86    report.power_state = check_power_state(&mut report.warnings, &mut report.errors);
87    
88    // Check memory pressure
89    report.memory_pressure = check_memory_pressure(&mut report.warnings, &mut report.errors);
90    
91    // Check CPU usage
92    report.cpu_usage = check_cpu_usage(&mut report.warnings, &mut report.errors);
93    
94    // macOS specific checks
95    #[cfg(target_os = "macos")]
96    {
97        check_macos_specific(&mut report.warnings, &mut report.errors);
98    }
99    
100    report
101}
102
103/// Check thermal state
104fn check_thermal_state(warnings: &mut Vec<String>, errors: &mut Vec<String>) -> ThermalState {
105    #[cfg(target_os = "macos")]
106    {
107        // Use powermetrics to check thermal state
108        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            // Parse thermal state from output
115            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        // Fallback: Check temperature via system sensors
134        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            // This is a simplified check - real thermal monitoring needs more sophistication
140            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        // Check /sys/class/thermal/thermal_zone*/temp
150        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()// Empty fallback
156        }) {
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                        // Temperatures in millidegrees Celsius
162                        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
196/// Check power state
197fn 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                // Try to get battery percentage
210                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                    // Parse battery percentage
217                    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        // For non-macOS systems, assume AC power
242        PowerState::AC
243    }
244}
245
246/// Check memory pressure
247fn 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                // Parse the percentage
257                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        // Fallback: use vm_stat
281        if let Ok(output) = std::process::Command::new("vm_stat").output() {
282            let output_str = String::from_utf8_lossy(&output.stdout);
283            // This would need proper parsing of vm_stat output
284            // For now, assume normal
285            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
342/// Check CPU usage
343fn 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            // Look for CPU usage line: "CPU usage: 12.34% user, 5.67% sys, 81.99% idle"
353            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        // Read /proc/loadavg
378        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                    // Convert load average to rough CPU percentage
382                    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/// macOS-specific environment checks
401#[cfg(target_os = "macos")]
402fn check_macos_specific(warnings: &mut Vec<String>, _errors: &mut Vec<String>) {
403    // Check if Spotlight is indexing
404    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            // Check if indexing is actively running
411            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    // Check for active Time Machine backups
421    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    // Check for Software Update activity
432    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
445/// Print a detailed environment report
446pub 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        // Should not crash and should produce a report
481        assert!(!report.summary().is_empty());
482        
483        // Should have some reasonable values
484        assert!(report.cpu_usage >= 0.0);
485        assert!(report.cpu_usage <= 200.0); // Allow for multi-core
486    }
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        // Good environment
510        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        // Bad environment
521        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}