Skip to main content

trueno_gpu/monitor/device/
cpu.rs

1//! CPU compute device implementation (TRUENO-SPEC-020)
2//!
3//! CPU monitoring via `/proc` and `/sys` filesystem on Linux.
4
5use super::{ComputeDevice, DeviceId, DeviceType};
6use crate::GpuError;
7
8// ============================================================================
9// CPU Device Implementation
10// ============================================================================
11
12/// CPU compute device using sysinfo
13#[derive(Debug)]
14pub struct CpuDevice {
15    name: String,
16    core_count: u32,
17    total_memory: u64,
18    // Cached metrics (updated on refresh)
19    cpu_usage: f64,
20    memory_used: u64,
21    temperature: Option<f64>,
22}
23
24impl CpuDevice {
25    /// Create a new CPU device monitor
26    #[must_use]
27    pub fn new() -> Self {
28        // Get CPU info from /proc/cpuinfo on Linux
29        let name = Self::read_cpu_name().unwrap_or_else(|| "Unknown CPU".to_string());
30        let core_count = Self::read_core_count();
31        let total_memory = Self::read_total_memory();
32
33        Self {
34            name,
35            core_count,
36            total_memory,
37            cpu_usage: 0.0,
38            memory_used: 0,
39            temperature: None,
40        }
41    }
42
43    fn read_cpu_name() -> Option<String> {
44        #[cfg(target_os = "linux")]
45        {
46            let content = std::fs::read_to_string("/proc/cpuinfo").ok()?;
47            for line in content.lines() {
48                if line.starts_with("model name") {
49                    return line.split(':').nth(1).map(|s| s.trim().to_string());
50                }
51            }
52        }
53        None
54    }
55
56    fn read_core_count() -> u32 {
57        #[cfg(target_os = "linux")]
58        {
59            if let Ok(content) = std::fs::read_to_string("/proc/cpuinfo") {
60                return content
61                    .lines()
62                    .filter(|line| line.starts_with("processor"))
63                    .count() as u32;
64            }
65        }
66        // Fallback
67        std::thread::available_parallelism()
68            .map(|n| n.get() as u32)
69            .unwrap_or(1)
70    }
71
72    fn read_total_memory() -> u64 {
73        #[cfg(target_os = "linux")]
74        {
75            if let Ok(content) = std::fs::read_to_string("/proc/meminfo") {
76                for line in content.lines() {
77                    if line.starts_with("MemTotal:") {
78                        // Parse "MemTotal:       32847868 kB"
79                        let parts: Vec<&str> = line.split_whitespace().collect();
80                        if parts.len() >= 2 {
81                            if let Ok(kb) = parts[1].parse::<u64>() {
82                                return kb * 1024; // Convert to bytes
83                            }
84                        }
85                    }
86                }
87            }
88        }
89        0
90    }
91
92    fn read_memory_used() -> u64 {
93        #[cfg(target_os = "linux")]
94        {
95            if let Ok(content) = std::fs::read_to_string("/proc/meminfo") {
96                let mut total = 0u64;
97                let mut available = 0u64;
98
99                for line in content.lines() {
100                    let parts: Vec<&str> = line.split_whitespace().collect();
101                    if parts.len() >= 2 {
102                        if line.starts_with("MemTotal:") {
103                            total = parts[1].parse().unwrap_or(0) * 1024;
104                        } else if line.starts_with("MemAvailable:") {
105                            available = parts[1].parse().unwrap_or(0) * 1024;
106                        }
107                    }
108                }
109                return total.saturating_sub(available);
110            }
111        }
112        0
113    }
114
115    fn read_cpu_usage() -> f64 {
116        #[cfg(target_os = "linux")]
117        {
118            // Read /proc/stat for CPU usage
119            // This is a simplified version - real implementation would track deltas
120            if let Ok(content) = std::fs::read_to_string("/proc/stat") {
121                for line in content.lines() {
122                    if line.starts_with("cpu ") {
123                        let parts: Vec<&str> = line.split_whitespace().collect();
124                        if parts.len() >= 5 {
125                            let user: u64 = parts[1].parse().unwrap_or(0);
126                            let nice: u64 = parts[2].parse().unwrap_or(0);
127                            let system: u64 = parts[3].parse().unwrap_or(0);
128                            let idle: u64 = parts[4].parse().unwrap_or(0);
129
130                            let total = user + nice + system + idle;
131                            let busy = user + nice + system;
132                            if total > 0 {
133                                return (busy as f64 / total as f64) * 100.0;
134                            }
135                        }
136                    }
137                }
138            }
139        }
140        0.0
141    }
142
143    fn read_temperature() -> Option<f64> {
144        #[cfg(target_os = "linux")]
145        {
146            // Try hwmon thermal zones
147            if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") {
148                for entry in entries.flatten() {
149                    let temp_path = entry.path().join("temp1_input");
150                    if let Ok(content) = std::fs::read_to_string(&temp_path) {
151                        if let Ok(millidegrees) = content.trim().parse::<i64>() {
152                            return Some(millidegrees as f64 / 1000.0);
153                        }
154                    }
155                }
156            }
157            // Fallback to thermal_zone
158            if let Ok(content) = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp") {
159                if let Ok(millidegrees) = content.trim().parse::<i64>() {
160                    return Some(millidegrees as f64 / 1000.0);
161                }
162            }
163        }
164        None
165    }
166}
167
168impl Default for CpuDevice {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl ComputeDevice for CpuDevice {
175    fn device_id(&self) -> DeviceId {
176        DeviceId::cpu()
177    }
178
179    fn device_name(&self) -> &str {
180        &self.name
181    }
182
183    fn device_type(&self) -> DeviceType {
184        DeviceType::Cpu
185    }
186
187    fn compute_utilization(&self) -> Result<f64, GpuError> {
188        Ok(self.cpu_usage)
189    }
190
191    fn compute_clock_mhz(&self) -> Result<u32, GpuError> {
192        // Read current CPU frequency
193        #[cfg(target_os = "linux")]
194        {
195            if let Ok(content) =
196                std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq")
197            {
198                if let Ok(khz) = content.trim().parse::<u64>() {
199                    return Ok((khz / 1000) as u32);
200                }
201            }
202        }
203        Err(GpuError::NotSupported(
204            "CPU frequency not available".to_string(),
205        ))
206    }
207
208    fn compute_temperature_c(&self) -> Result<f64, GpuError> {
209        self.temperature
210            .ok_or_else(|| GpuError::NotSupported("CPU temperature not available".to_string()))
211    }
212
213    fn compute_power_watts(&self) -> Result<f64, GpuError> {
214        // CPU power estimation based on TDP and utilization
215        // This is a rough estimate - RAPL provides better data on supported CPUs
216        Err(GpuError::NotSupported(
217            "CPU power not available".to_string(),
218        ))
219    }
220
221    fn compute_power_limit_watts(&self) -> Result<f64, GpuError> {
222        Err(GpuError::NotSupported(
223            "CPU power limit not available".to_string(),
224        ))
225    }
226
227    fn memory_used_bytes(&self) -> Result<u64, GpuError> {
228        Ok(self.memory_used)
229    }
230
231    fn memory_total_bytes(&self) -> Result<u64, GpuError> {
232        Ok(self.total_memory)
233    }
234
235    fn memory_bandwidth_gbps(&self) -> Result<f64, GpuError> {
236        // Would need memory controller stats - not easily available
237        Err(GpuError::NotSupported(
238            "Memory bandwidth not available".to_string(),
239        ))
240    }
241
242    fn compute_unit_count(&self) -> u32 {
243        self.core_count
244    }
245
246    fn active_compute_units(&self) -> Result<u32, GpuError> {
247        // All cores are typically active
248        Ok(self.core_count)
249    }
250
251    fn pcie_tx_bytes_per_sec(&self) -> Result<u64, GpuError> {
252        Err(GpuError::NotSupported(
253            "CPU has no PCIe metrics".to_string(),
254        ))
255    }
256
257    fn pcie_rx_bytes_per_sec(&self) -> Result<u64, GpuError> {
258        Err(GpuError::NotSupported(
259            "CPU has no PCIe metrics".to_string(),
260        ))
261    }
262
263    fn pcie_generation(&self) -> u8 {
264        0 // N/A for CPU
265    }
266
267    fn pcie_width(&self) -> u8 {
268        0 // N/A for CPU
269    }
270
271    fn refresh(&mut self) -> Result<(), GpuError> {
272        self.cpu_usage = Self::read_cpu_usage();
273        self.memory_used = Self::read_memory_used();
274        self.temperature = Self::read_temperature();
275        Ok(())
276    }
277}