Skip to main content

auto_cpufreq/modules/
system_info.rs

1// src/modules/system_info.rs - OPTIMIZED VERSION
2use std::collections::HashMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant};
7
8use sysinfo::System;
9
10use crate::AVAILABLE_GOVERNORS_SORTED;
11use crate::CONFIG;
12use crate::POWER_SUPPLY_DIR;
13
14#[derive(Debug, Clone)]
15pub struct CoreInfo {
16    pub id: usize,
17    pub usage: f32,
18    pub temperature: f32,
19    pub frequency: f32,
20}
21
22#[derive(Debug, Clone)]
23pub struct BatteryInfo {
24    pub is_charging: Option<bool>,
25    pub is_ac_plugged: Option<bool>,
26    pub charging_start_threshold: Option<i32>,
27    pub charging_stop_threshold: Option<i32>,
28    pub battery_level: Option<u8>,
29    pub power_consumption: Option<f32>,
30}
31
32#[derive(Debug, Clone)]
33pub struct SystemReport {
34    pub distro_name: String,
35    pub distro_ver: String,
36    pub arch: String,
37    pub processor_model: String,
38    pub total_core: Option<usize>,
39    pub kernel_version: String,
40    pub current_gov: Option<String>,
41    pub current_epp: Option<String>,
42    pub current_epb: Option<String>,
43    pub cpu_driver: Option<String>,
44    pub cpu_fan_speed: Option<i32>,
45    pub cpu_usage: f32,
46    pub cpu_max_freq: Option<f32>,
47    pub cpu_min_freq: Option<f32>,
48    pub load: f32,
49    pub avg_load: Option<(f32, f32, f32)>,
50    pub cores_info: Vec<CoreInfo>,
51    pub battery_info: BatteryInfo,
52    pub is_turbo_on: (Option<bool>, Option<bool>),
53}
54
55// ============================================================================
56// OPTIMIZATION: Temperature Sensor Cache
57// ============================================================================
58struct TempSensorCache {
59    sensor_paths: HashMap<usize, PathBuf>,
60    package_temp_path: Option<PathBuf>,
61    fan_speed_path: Option<PathBuf>,
62    last_scan: Instant,
63}
64
65impl TempSensorCache {
66    fn new() -> Self {
67        let mut cache = Self {
68            sensor_paths: HashMap::new(),
69            package_temp_path: None,
70            fan_speed_path: None,
71            last_scan: Instant::now(),
72        };
73        cache.scan_sensors();
74        cache
75    }
76
77    fn scan_sensors(&mut self) {
78        let sensor_priority = ["coretemp", "k10temp", "zenpower", "acpitz"];
79        let hwmon_path = "/sys/class/hwmon";
80
81        if let Ok(entries) = fs::read_dir(hwmon_path) {
82            for entry in entries.flatten() {
83                let path = entry.path();
84                let name_file = path.join("name");
85
86                if let Ok(sensor_name) = fs::read_to_string(&name_file) {
87                    let sensor_name = sensor_name.trim();
88
89                    if sensor_priority.contains(&sensor_name) {
90                        let pkg_temp = path.join("temp1_input");
91                        if pkg_temp.exists() {
92                            self.package_temp_path = Some(pkg_temp);
93                        }
94
95                        for temp_id in 2..20 {
96                            let temp_file = path.join(format!("temp{}_input", temp_id));
97                            if temp_file.exists() {
98                                let core_id = temp_id - 2;
99                                self.sensor_paths.insert(core_id, temp_file);
100                            }
101                        }
102                    }
103
104                    if self.fan_speed_path.is_none() {
105                        let fan_input = path.join("fan1_input");
106                        if fan_input.exists() {
107                            self.fan_speed_path = Some(fan_input);
108                        }
109                    }
110                }
111            }
112        }
113
114        self.last_scan = Instant::now();
115    }
116
117    fn read_core_temp(&self, core_id: usize) -> f32 {
118        if let Some(path) = self.sensor_paths.get(&core_id) {
119            if let Ok(temp_str) = fs::read_to_string(path) {
120                if let Ok(temp) = temp_str.trim().parse::<f32>() {
121                    return temp / 1000.0;
122                }
123            }
124        }
125
126        if let Some(ref path) = self.package_temp_path {
127            if let Ok(temp_str) = fs::read_to_string(path) {
128                if let Ok(temp) = temp_str.trim().parse::<f32>() {
129                    return temp / 1000.0;
130                }
131            }
132        }
133
134        0.0
135    }
136
137    fn read_fan_speed(&self) -> Option<i32> {
138        if let Some(ref path) = self.fan_speed_path {
139            if let Ok(fan_str) = fs::read_to_string(path) {
140                if let Ok(rpm) = fan_str.trim().parse::<i32>() {
141                    if rpm > 0 {
142                        return Some(rpm);
143                    }
144                }
145            }
146        }
147        None
148    }
149}
150
151lazy_static::lazy_static! {
152    static ref TEMP_CACHE: Arc<Mutex<TempSensorCache>> = Arc::new(Mutex::new(TempSensorCache::new()));
153}
154
155// ============================================================================
156// OPTIMIZATION: Static Info Cache
157// ============================================================================
158struct StaticInfoCache {
159    processor_model: String,
160    cpu_driver: Option<String>,
161    cpu_min_freq: Option<f32>,
162    cpu_max_freq: Option<f32>,
163}
164
165impl StaticInfoCache {
166    fn new() -> Self {
167        Self {
168            processor_model: Self::read_processor_model(),
169            cpu_driver: Self::read_cpu_driver(),
170            cpu_min_freq: Self::read_cpu_min_freq(),
171            cpu_max_freq: Self::read_cpu_max_freq(),
172        }
173    }
174
175    fn read_processor_model() -> String {
176        fs::read_to_string("/proc/cpuinfo")
177            .ok()
178            .and_then(|s| {
179                s.lines()
180                    .find(|l| l.contains("model name"))
181                    .and_then(|l| l.split(':').nth(1))
182                    .map(|s| s.trim().to_string())
183            })
184            .unwrap_or_default()
185    }
186
187    fn read_cpu_driver() -> Option<String> {
188        fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_driver")
189            .ok()
190            .map(|s| s.trim().to_string())
191    }
192
193    fn read_cpu_min_freq() -> Option<f32> {
194        fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq")
195            .ok()
196            .and_then(|s| s.trim().parse::<f32>().ok())
197            .map(|khz| khz / 1000.0)
198    }
199
200    fn read_cpu_max_freq() -> Option<f32> {
201        fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq")
202            .ok()
203            .and_then(|s| s.trim().parse::<f32>().ok())
204            .map(|khz| khz / 1000.0)
205    }
206}
207
208lazy_static::lazy_static! {
209    static ref STATIC_INFO: StaticInfoCache = StaticInfoCache::new();
210}
211
212// ============================================================================
213// OPTIMIZATION: Battery Path Cache
214// ============================================================================
215struct BatteryPathCache {
216    battery_path: Option<PathBuf>,
217    mains_path: Option<PathBuf>,
218    cached_at: Instant,
219}
220
221impl BatteryPathCache {
222    fn new() -> Self {
223        let (battery_path, mains_path) = Self::scan_power_supply();
224        Self {
225            battery_path,
226            mains_path,
227            cached_at: Instant::now(),
228        }
229    }
230
231    fn scan_power_supply() -> (Option<PathBuf>, Option<PathBuf>) {
232        let mut battery = None;
233        let mut mains = None;
234
235        if CONFIG.has_option("battery", "battery_device") {
236            let battery_device = CONFIG.get("battery", "battery_device", "");
237            if !battery_device.is_empty() {
238                let custom = Path::new(POWER_SUPPLY_DIR).join(&battery_device);
239                let type_path = custom.join("type");
240                if type_path.is_file() {
241                    if let Ok(content) = fs::read_to_string(type_path) {
242                        if content.trim().to_lowercase() == "battery" {
243                            battery = Some(custom);
244                        }
245                    }
246                }
247            }
248        }
249
250        if let Ok(entries) = fs::read_dir(POWER_SUPPLY_DIR) {
251            for entry in entries.flatten() {
252                let path = entry.path();
253                let type_path = path.join("type");
254
255                if let Ok(content) = fs::read_to_string(&type_path) {
256                    match content.trim() {
257                        "Battery" if battery.is_none() => battery = Some(path),
258                        "Mains" if mains.is_none() => mains = Some(path),
259                        _ => {}
260                    }
261                }
262            }
263        }
264
265        (battery, mains)
266    }
267
268    fn maybe_rescan(&mut self) {
269        if self.cached_at.elapsed() > Duration::from_secs(60) {
270            let (battery, mains) = Self::scan_power_supply();
271            self.battery_path = battery;
272            self.mains_path = mains;
273            self.cached_at = Instant::now();
274        }
275    }
276}
277
278lazy_static::lazy_static! {
279    static ref BATTERY_PATH_CACHE: Arc<Mutex<BatteryPathCache>> =
280        Arc::new(Mutex::new(BatteryPathCache::new()));
281}
282
283// ============================================================================
284// SystemInfo
285// ============================================================================
286pub struct SystemInfo {
287    pub distro_name: String,
288    pub distro_version: String,
289    pub architecture: String,
290    pub processor_model: String,
291    pub total_cores: Option<usize>,
292    pub cpu_driver: Option<String>,
293    pub kernel_version: String,
294}
295
296impl Default for SystemInfo {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302impl SystemInfo {
303    pub fn new() -> Self {
304        let distro_name = Self::read_os_release_name().unwrap_or_else(|| "UNKNOWN".into());
305        let distro_version = Self::read_os_release_version().unwrap_or_else(|| "UNKNOWN".into());
306        let architecture = std::env::consts::ARCH.to_string();
307        let total_cores = Some(num_cpus::get());
308        let kernel_version = Self::uname_release().unwrap_or_default();
309
310        Self {
311            distro_name,
312            distro_version,
313            architecture,
314            processor_model: STATIC_INFO.processor_model.clone(),
315            total_cores,
316            cpu_driver: STATIC_INFO.cpu_driver.clone(),
317            kernel_version,
318        }
319    }
320
321    fn read_os_release_name() -> Option<String> {
322        if let Ok(content) = fs::read_to_string("/etc/os-release") {
323            for line in content.lines() {
324                if line.starts_with("PRETTY_NAME=") {
325                    return Some(
326                        line.split_once('=')
327                            .map(|x| x.1)
328                            .unwrap_or("")
329                            .trim_matches('"')
330                            .to_string(),
331                    );
332                }
333            }
334        }
335        None
336    }
337
338    fn read_os_release_version() -> Option<String> {
339        if let Ok(content) = fs::read_to_string("/etc/os-release") {
340            for line in content.lines() {
341                if line.starts_with("VERSION=") {
342                    return Some(
343                        line.split_once('=')
344                            .map(|x| x.1)
345                            .unwrap_or("")
346                            .trim_matches('"')
347                            .to_string(),
348                    );
349                }
350            }
351        }
352        None
353    }
354
355    fn uname_release() -> Option<String> {
356        std::process::Command::new("uname")
357            .arg("-r")
358            .output()
359            .ok()
360            .and_then(|o| String::from_utf8(o.stdout).ok())
361            .map(|s| s.trim().to_string())
362    }
363
364    pub fn cpu_min_freq() -> Option<f32> {
365        STATIC_INFO.cpu_min_freq
366    }
367
368    pub fn cpu_max_freq() -> Option<f32> {
369        STATIC_INFO.cpu_max_freq
370    }
371
372    pub fn get_cpu_info(sys: &System) -> Vec<CoreInfo> {
373        let cpus = sys.cpus();
374        let mut cores = Vec::with_capacity(cpus.len());
375
376        let temp_cache = TEMP_CACHE.lock().unwrap();
377
378        for (i, cpu) in cpus.iter().enumerate() {
379            cores.push(CoreInfo {
380                id: i,
381                usage: cpu.cpu_usage(),
382                frequency: cpu.frequency() as f32,
383                temperature: temp_cache.read_core_temp(i),
384            });
385        }
386
387        cores
388    }
389
390    pub fn cpu_fan_speed() -> Option<i32> {
391        TEMP_CACHE.lock().unwrap().read_fan_speed()
392    }
393
394    pub fn current_gov() -> Option<String> {
395        fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
396            .ok()
397            .map(|s| s.trim().to_string())
398    }
399
400    pub fn current_epp(is_ac_plugged: bool) -> Option<String> {
401        let epp_path =
402            Path::new("/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference");
403        if !epp_path.exists() {
404            return None;
405        }
406
407        let section = if is_ac_plugged { "charger" } else { "battery" };
408        Some(CONFIG.get(section, "energy_performance_preference", "balance_power"))
409    }
410
411    pub fn current_epb(is_ac_plugged: bool) -> Option<String> {
412        let epb_path = Path::new("/sys/devices/system/cpu/intel_pstate");
413        if !epb_path.exists() {
414            return None;
415        }
416        let section = if is_ac_plugged { "charger" } else { "battery" };
417        Some(CONFIG.get(section, "energy_perf_bias", "balance_power"))
418    }
419
420    pub fn cpu_usage(sys: &System) -> f32 {
421        let cpus = sys.cpus();
422        if cpus.is_empty() {
423            return 0.0;
424        }
425        let sum: f32 = cpus.iter().map(|c| c.cpu_usage()).sum();
426        sum / (cpus.len() as f32)
427    }
428
429    pub fn system_load() -> f32 {
430        if let Ok(s) = fs::read_to_string("/proc/loadavg") {
431            if let Some(first) = s.split_whitespace().next() {
432                return first.parse::<f32>().unwrap_or(0.0);
433            }
434        }
435        0.0
436    }
437
438    pub fn avg_load() -> Option<(f32, f32, f32)> {
439        if let Ok(s) = fs::read_to_string("/proc/loadavg") {
440            let mut parts = s.split_whitespace();
441            let a = parts.next().and_then(|p| p.parse::<f32>().ok());
442            let b = parts.next().and_then(|p| p.parse::<f32>().ok());
443            let c = parts.next().and_then(|p| p.parse::<f32>().ok());
444            if let (Some(a), Some(b), Some(c)) = (a, b, c) {
445                return Some((a, b, c));
446            }
447        }
448        None
449    }
450
451    pub fn avg_temp(sys: &System) -> i32 {
452        let temps: Vec<f32> = Self::get_cpu_info(sys)
453            .iter()
454            .map(|c| c.temperature)
455            .filter(|&t| t > 0.0)
456            .collect();
457
458        if temps.is_empty() {
459            0
460        } else {
461            (temps.iter().sum::<f32>() / temps.len() as f32) as i32
462        }
463    }
464
465    pub fn turbo_on() -> (Option<bool>, Option<bool>) {
466        let intel_pstate = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo");
467        let cpu_freq = Path::new("/sys/devices/system/cpu/cpufreq/boost");
468        let amd_pstate = Path::new("/sys/devices/system/cpu/amd_pstate/status");
469
470        if intel_pstate.exists() {
471            if let Ok(v) = fs::read_to_string(intel_pstate) {
472                if let Ok(n) = v.trim().parse::<i32>() {
473                    return (Some(n == 0), Some(false));
474                }
475            }
476            return (None, None);
477        }
478
479        if cpu_freq.exists() {
480            if let Ok(v) = fs::read_to_string(cpu_freq) {
481                if let Ok(n) = v.trim().parse::<i32>() {
482                    return (Some(n != 0), Some(false));
483                }
484            }
485            return (None, None);
486        }
487
488        if amd_pstate.exists() {
489            if let Ok(s) = fs::read_to_string(amd_pstate) {
490                if s.trim() == "active" {
491                    return (None, Some(true));
492                }
493                return (None, Some(false));
494            }
495            return (None, None);
496        }
497
498        (None, None)
499    }
500
501    pub fn get_battery_path() -> Option<PathBuf> {
502        let mut cache = BATTERY_PATH_CACHE.lock().unwrap();
503        cache.maybe_rescan();
504        cache.battery_path.clone()
505    }
506
507    pub fn battery_info() -> BatteryInfo {
508        let mut cache = BATTERY_PATH_CACHE.lock().unwrap();
509        cache.maybe_rescan();
510
511        let mut is_ac_plugged = Some(true);
512
513        if let Some(ref mains_path) = cache.mains_path {
514            if let Ok(online) = fs::read_to_string(mains_path.join("online")) {
515                is_ac_plugged = Some(online.trim() == "1");
516            }
517        }
518
519        let battery_path = match &cache.battery_path {
520            Some(p) => p,
521            None => {
522                return BatteryInfo {
523                    is_charging: None,
524                    is_ac_plugged: Some(true),
525                    charging_start_threshold: None,
526                    charging_stop_threshold: None,
527                    battery_level: None,
528                    power_consumption: None,
529                };
530            }
531        };
532
533        let status = fs::read_to_string(battery_path.join("status")).ok();
534        let capacity = fs::read_to_string(battery_path.join("capacity")).ok();
535        let energy_rate = fs::read_to_string(battery_path.join("power_now"))
536            .or_else(|_| fs::read_to_string(battery_path.join("current_now")))
537            .ok();
538        let charge_start = fs::read_to_string(battery_path.join("charge_start_threshold"))
539            .or_else(|_| fs::read_to_string(battery_path.join("charge_control_start_threshold")))
540            .ok();
541        let charge_stop = fs::read_to_string(battery_path.join("charge_stop_threshold"))
542            .or_else(|_| fs::read_to_string(battery_path.join("charge_control_end_threshold")))
543            .ok();
544
545        let is_charging = status
546            .as_ref()
547            .map(|s| s.trim().to_lowercase() == "charging");
548        let battery_level = capacity.and_then(|c| c.trim().parse::<u8>().ok());
549        let power_consumption = energy_rate
550            .and_then(|e| e.trim().parse::<f32>().ok())
551            .map(|v| v / 1_000_000.0);
552        let charging_start_threshold = charge_start.and_then(|s| s.trim().parse::<i32>().ok());
553        let charging_stop_threshold = charge_stop.and_then(|s| s.trim().parse::<i32>().ok());
554
555        BatteryInfo {
556            is_charging,
557            is_ac_plugged,
558            charging_start_threshold,
559            charging_stop_threshold,
560            battery_level,
561            power_consumption,
562        }
563    }
564
565    pub fn turbo_on_suggestion(sys: &System) -> bool {
566        let usage = Self::cpu_usage(sys);
567        if usage >= 20.0 {
568            return true;
569        }
570        if usage <= 25.0 && Self::avg_temp(sys) as f32 >= 70.0 {
571            return false;
572        }
573        false
574    }
575
576    pub fn governor_suggestion() -> Option<String> {
577        let batt = Self::battery_info();
578        if batt.is_ac_plugged.unwrap_or(true) {
579            AVAILABLE_GOVERNORS_SORTED.first().cloned()
580        } else {
581            AVAILABLE_GOVERNORS_SORTED.last().cloned()
582        }
583    }
584
585    pub fn generate_system_report(&self, sys: &System) -> SystemReport {
586        let battery = Self::battery_info();
587        let cores = Self::get_cpu_info(sys);
588
589        SystemReport {
590            distro_name: self.distro_name.clone(),
591            distro_ver: self.distro_version.clone(),
592            arch: self.architecture.clone(),
593            processor_model: self.processor_model.clone(),
594            total_core: self.total_cores,
595            kernel_version: self.kernel_version.clone(),
596            current_gov: Self::current_gov(),
597            current_epp: battery.is_ac_plugged.and_then(Self::current_epp),
598            current_epb: battery.is_ac_plugged.and_then(Self::current_epb),
599            cpu_driver: self.cpu_driver.clone(),
600            cpu_fan_speed: Self::cpu_fan_speed(),
601            cpu_usage: Self::cpu_usage(sys),
602            cpu_max_freq: Self::cpu_max_freq(),
603            cpu_min_freq: Self::cpu_min_freq(),
604            load: Self::system_load(),
605            avg_load: Self::avg_load(),
606            cores_info: cores,
607            battery_info: battery,
608            is_turbo_on: Self::turbo_on(),
609        }
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn smoke() {
619        let s = SystemInfo::new();
620        let mut sys = System::new_all();
621        sys.refresh_cpu_all();
622        std::thread::sleep(std::time::Duration::from_millis(200));
623        sys.refresh_cpu_all();
624        let _ = s.generate_system_report(&sys);
625    }
626
627    #[test]
628    fn test_temp_cache() {
629        let cache = TEMP_CACHE.lock().unwrap();
630        let temp = cache.read_core_temp(0);
631        assert!(temp >= 0.0);
632    }
633
634    #[test]
635    fn test_battery_cache() {
636        let cache = BATTERY_PATH_CACHE.lock().unwrap();
637        let _ = cache.battery_path.is_some();
638    }
639}