simplebench_runtime/
cpu_monitor.rs

1//! CPU monitoring for Linux systems
2//!
3//! Provides CPU frequency and thermal monitoring on Linux via sysfs.
4//! Gracefully degrades on non-Linux platforms.
5
6use std::fs;
7use std::time::Instant;
8
9/// Monitor for a specific CPU core
10pub struct CpuMonitor {
11    cpu_core: usize,
12    thermal_zone: Option<usize>,
13}
14
15impl CpuMonitor {
16    /// Create monitor for specific CPU core
17    pub fn new(cpu_core: usize) -> Self {
18        let thermal_zone = Self::discover_thermal_zones().first().copied();
19        Self {
20            cpu_core,
21            thermal_zone,
22        }
23    }
24
25    /// Read current frequency in kHz (returns None if unavailable)
26    pub fn read_frequency(&self) -> Option<u64> {
27        #[cfg(target_os = "linux")]
28        {
29            let path = format!(
30                "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq",
31                self.cpu_core
32            );
33            fs::read_to_string(path)
34                .ok()
35                .and_then(|s| s.trim().parse().ok())
36        }
37        #[cfg(not(target_os = "linux"))]
38        {
39            None
40        }
41    }
42
43    /// Read current governor (returns None if unavailable)
44    pub fn read_governor(&self) -> Option<String> {
45        #[cfg(target_os = "linux")]
46        {
47            let path = format!(
48                "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor",
49                self.cpu_core
50            );
51            fs::read_to_string(path).ok().map(|s| s.trim().to_string())
52        }
53        #[cfg(not(target_os = "linux"))]
54        {
55            None
56        }
57    }
58
59    /// Read frequency range (min, max in kHz)
60    pub fn read_frequency_range(&self) -> Option<(u64, u64)> {
61        #[cfg(target_os = "linux")]
62        {
63            let min_path = format!(
64                "/sys/devices/system/cpu/cpu{}/cpufreq/cpuinfo_min_freq",
65                self.cpu_core
66            );
67            let max_path = format!(
68                "/sys/devices/system/cpu/cpu{}/cpufreq/cpuinfo_max_freq",
69                self.cpu_core
70            );
71
72            let min = fs::read_to_string(min_path)
73                .ok()
74                .and_then(|s| s.trim().parse().ok())?;
75            let max = fs::read_to_string(max_path)
76                .ok()
77                .and_then(|s| s.trim().parse().ok())?;
78
79            Some((min, max))
80        }
81        #[cfg(not(target_os = "linux"))]
82        {
83            None
84        }
85    }
86
87    /// Read current temperature in millidegrees Celsius (returns None if unavailable)
88    pub fn read_temperature(&self) -> Option<i32> {
89        #[cfg(target_os = "linux")]
90        {
91            let zone = self.thermal_zone?;
92            let path = format!("/sys/class/thermal/thermal_zone{}/temp", zone);
93            fs::read_to_string(path)
94                .ok()
95                .and_then(|s| s.trim().parse().ok())
96        }
97        #[cfg(not(target_os = "linux"))]
98        {
99            None
100        }
101    }
102
103    /// Find available thermal zones (returns zone indices)
104    pub fn discover_thermal_zones() -> Vec<usize> {
105        #[cfg(target_os = "linux")]
106        {
107            (0..20)
108                .filter(|&i| {
109                    let path = format!("/sys/class/thermal/thermal_zone{}/temp", i);
110                    fs::metadata(path).is_ok()
111                })
112                .collect()
113        }
114        #[cfg(not(target_os = "linux"))]
115        {
116            Vec::new()
117        }
118    }
119}
120
121/// Snapshot of CPU state at a point in time
122#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
123pub struct CpuSnapshot {
124    #[serde(skip, default = "Instant::now")]
125    pub timestamp: Instant,
126    pub frequency_khz: Option<u64>,
127    pub temperature_millic: Option<i32>,
128}
129
130impl CpuSnapshot {
131    /// Get frequency in MHz
132    pub fn frequency_mhz(&self) -> Option<f64> {
133        self.frequency_khz.map(|khz| khz as f64 / 1000.0)
134    }
135
136    /// Get temperature in Celsius
137    pub fn temperature_celsius(&self) -> Option<f64> {
138        self.temperature_millic.map(|millic| millic as f64 / 1000.0)
139    }
140}
141
142impl Default for CpuSnapshot {
143    fn default() -> Self {
144        Self {
145            timestamp: Instant::now(),
146            frequency_khz: None,
147            temperature_millic: None,
148        }
149    }
150}
151
152/// Verify and report benchmark environment
153pub fn verify_benchmark_environment(cpu_core: usize) {
154    use colored::*;
155
156    println!("{}", "Verifying benchmark environment...".green().bold());
157
158    #[cfg(target_os = "linux")]
159    {
160        println!(
161            "  {} {}",
162            "Platform:".dimmed(),
163            "Linux (full monitoring support)".cyan()
164        );
165
166        let monitor = CpuMonitor::new(cpu_core);
167
168        // Check governor
169        if let Some(governor) = monitor.read_governor() {
170            print!("  {} {} {}", "CPU".dimmed(), cpu_core, "governor:".dimmed());
171            if governor == "performance" {
172                println!(" {}", governor.green());
173            } else {
174                println!(" {}", governor.yellow());
175                println!(
176                    "    {} {}",
177                    "⚠".yellow(),
178                    "Not using 'performance' governor".yellow()
179                );
180                println!(
181                    "    {} {}",
182                    "Consider:".dimmed(),
183                    "sudo cpupower frequency-set -g performance".dimmed()
184                );
185            }
186        }
187
188        // Check frequency range
189        if let Some((min_khz, max_khz)) = monitor.read_frequency_range() {
190            println!(
191                "  {} {} {} {} {} {} {}",
192                "CPU".dimmed(),
193                cpu_core,
194                "frequency range:".dimmed(),
195                (min_khz / 1000).to_string().cyan(),
196                "MHz -".dimmed(),
197                (max_khz / 1000).to_string().cyan(),
198                "MHz".dimmed()
199            );
200        }
201
202        // Check current frequency
203        if let Some(freq_khz) = monitor.read_frequency() {
204            println!(
205                "  {} {} {} {} {}",
206                "CPU".dimmed(),
207                cpu_core,
208                "current frequency:".dimmed(),
209                (freq_khz / 1000).to_string().cyan(),
210                "MHz".dimmed()
211            );
212        }
213
214        // Check thermal zones
215        let zones = CpuMonitor::discover_thermal_zones();
216        if !zones.is_empty() {
217            println!(
218                "  {} {} {}",
219                "Found".dimmed(),
220                zones.len().to_string().cyan(),
221                "thermal zone(s)".dimmed()
222            );
223            for zone in zones.iter().take(3) {
224                let path = format!("/sys/class/thermal/thermal_zone{}/temp", zone);
225                if let Ok(temp_str) = fs::read_to_string(path) {
226                    if let Ok(temp_millic) = temp_str.trim().parse::<i32>() {
227                        println!(
228                            "    {} {} {}°C",
229                            "Zone".dimmed(),
230                            zone,
231                            (temp_millic / 1000).to_string().cyan()
232                        );
233                    }
234                }
235            }
236        }
237    }
238
239    #[cfg(not(target_os = "linux"))]
240    {
241        let os = std::env::consts::OS;
242        println!(
243            "  {} {} {}",
244            "Platform:".dimmed(),
245            os.cyan(),
246            "(limited monitoring support)".dimmed()
247        );
248        println!(
249            "    {} {}",
250            "ℹ".blue(),
251            "CPU frequency/thermal monitoring not available on this platform".dimmed()
252        );
253    }
254
255    println!("{}\n", "Environment check complete.".green());
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_cpu_monitor_creation() {
264        let monitor = CpuMonitor::new(0);
265        // Should not panic on any platform
266        let _ = monitor.read_frequency();
267        let _ = monitor.read_governor();
268        let _ = monitor.read_frequency_range();
269        let _ = monitor.read_temperature();
270    }
271
272    #[test]
273    fn test_thermal_zone_discovery() {
274        let zones = CpuMonitor::discover_thermal_zones();
275        // On Linux, should find at least one zone (usually)
276        // On other platforms, should return empty vec
277        #[cfg(target_os = "linux")]
278        {
279            // May or may not find zones depending on system
280            println!("Found {} thermal zones", zones.len());
281        }
282        #[cfg(not(target_os = "linux"))]
283        {
284            assert!(zones.is_empty());
285        }
286    }
287
288    #[test]
289    fn test_cpu_snapshot() {
290        let snapshot = CpuSnapshot {
291            timestamp: Instant::now(),
292            frequency_khz: Some(4500000),
293            temperature_millic: Some(55000),
294        };
295
296        assert_eq!(snapshot.frequency_mhz(), Some(4500.0));
297        assert_eq!(snapshot.temperature_celsius(), Some(55.0));
298    }
299
300    #[test]
301    fn test_verify_environment() {
302        // Should not panic on any platform
303        verify_benchmark_environment(0);
304    }
305}