Skip to main content

entrenar/efficiency/device/
cpu.rs

1//! CPU information and detection.
2
3use serde::{Deserialize, Serialize};
4
5use super::simd::SimdCapability;
6
7/// CPU information
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct CpuInfo {
10    /// Number of physical cores
11    pub cores: u32,
12    /// Number of logical threads (with hyperthreading)
13    pub threads: u32,
14    /// SIMD capability
15    pub simd: SimdCapability,
16    /// CPU model name
17    pub model: String,
18    /// Cache size in bytes (L3 or total)
19    pub cache_bytes: u64,
20}
21
22impl CpuInfo {
23    /// Create new CPU info
24    pub fn new(cores: u32, threads: u32, simd: SimdCapability, model: impl Into<String>) -> Self {
25        Self { cores, threads, simd, model: model.into(), cache_bytes: 0 }
26    }
27
28    /// Set cache size
29    pub fn with_cache(mut self, cache_bytes: u64) -> Self {
30        self.cache_bytes = cache_bytes;
31        self
32    }
33
34    /// Reconcile a machine-wide physical core count against the parallelism this
35    /// process is actually allowed to use.
36    ///
37    /// `available_parallelism` is cgroup- and affinity-aware: it reports what THIS
38    /// process may run on. `/proc/cpuinfo` is not — it describes the whole machine
39    /// regardless of any restriction. Comparing the two directly mixes denominators,
40    /// and under any CPU restriction the machine-wide figure is both larger than the
41    /// usable one and useless for sizing work.
42    ///
43    /// Clamping keeps `cores <= threads` true by construction and makes `cores` mean
44    /// "physical cores this process can actually use", which is what every consumer
45    /// of the field (thread-pool sizing, efficiency estimates) needs.
46    pub(super) fn usable_cores(detected_physical: Option<u32>, threads: u32) -> u32 {
47        let threads = threads.max(1);
48        detected_physical.map_or(threads, |physical| physical.clamp(1, threads))
49    }
50
51    /// Detect current CPU information
52    pub fn detect() -> Self {
53        // Get logical CPU count using standard library. This is cgroup/affinity aware.
54        let threads = std::thread::available_parallelism().map(|n| n.get() as u32).unwrap_or(1);
55
56        // Physical cores come from /proc/cpuinfo, which is NOT restriction-aware, so
57        // the result is reconciled against `threads` — see `usable_cores`.
58        let cores = Self::usable_cores(Self::detect_physical_cores(), threads);
59        let simd = SimdCapability::detect();
60
61        // Try to get CPU model name
62        let model = Self::detect_model();
63
64        Self {
65            cores,
66            threads,
67            simd,
68            model,
69            cache_bytes: 0, // Would need platform-specific APIs
70        }
71    }
72
73    /// Detect physical core count (Linux-specific)
74    #[cfg(target_os = "linux")]
75    fn detect_physical_cores() -> Option<u32> {
76        std::fs::read_to_string("/proc/cpuinfo").ok().map(|info| {
77            // Count unique core IDs
78            let mut core_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
79            let mut current_physical_id = String::new();
80
81            for line in info.lines() {
82                if line.starts_with("physical id") {
83                    current_physical_id =
84                        line.split(':').nth(1).map(|s| s.trim().to_string()).unwrap_or_default();
85                } else if line.starts_with("core id") {
86                    let core_id =
87                        line.split(':').nth(1).map(|s| s.trim().to_string()).unwrap_or_default();
88                    core_ids.insert(format!("{current_physical_id}-{core_id}"));
89                }
90            }
91
92            if core_ids.is_empty() {
93                // Fallback: count processor entries
94                info.lines().filter(|line| line.starts_with("processor")).count() as u32
95            } else {
96                core_ids.len() as u32
97            }
98        })
99    }
100
101    /// Detect physical core count (macOS-specific)
102    #[cfg(target_os = "macos")]
103    fn detect_physical_cores() -> Option<u32> {
104        std::process::Command::new("sysctl")
105            .args(["-n", "hw.physicalcpu"])
106            .output()
107            .ok()
108            .and_then(|output| String::from_utf8(output.stdout).ok())
109            .and_then(|s| s.trim().parse().ok())
110    }
111
112    /// Detect physical core count (fallback)
113    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
114    fn detect_physical_cores() -> Option<u32> {
115        None
116    }
117
118    /// Detect CPU model name
119    #[cfg(target_os = "linux")]
120    fn detect_model() -> String {
121        std::fs::read_to_string("/proc/cpuinfo")
122            .ok()
123            .and_then(|info| {
124                info.lines()
125                    .find(|line| line.starts_with("model name"))
126                    .and_then(|line| line.split(':').nth(1))
127                    .map(|s| s.trim().to_string())
128            })
129            .unwrap_or_else(|| "Unknown CPU".to_string())
130    }
131
132    #[cfg(target_os = "macos")]
133    fn detect_model() -> String {
134        std::process::Command::new("sysctl")
135            .args(["-n", "machdep.cpu.brand_string"])
136            .output()
137            .ok()
138            .and_then(|output| String::from_utf8(output.stdout).ok())
139            .map(|s| s.trim().to_string())
140            .unwrap_or_else(|| "Unknown CPU".to_string())
141    }
142
143    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
144    fn detect_model() -> String {
145        "Unknown CPU".to_string()
146    }
147
148    /// Estimate memory bandwidth based on core count (rough approximation)
149    pub fn estimated_memory_bandwidth_gbps(&self) -> f64 {
150        // Rough estimate: ~20 GB/s per channel, assume 2 channels for desktop
151        40.0 * (f64::from(self.cores) / 8.0).min(2.0)
152    }
153}