jolt_platform/linux/
power.rs

1use std::collections::VecDeque;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::time::{Duration, Instant};
5
6use color_eyre::eyre::Result;
7
8use crate::power::{PowerInfo, PowerProvider};
9use crate::types::PowerMode;
10
11const RAPL_PATH: &str = "/sys/class/powercap/intel-rapl";
12const HWMON_PATH: &str = "/sys/class/hwmon";
13const SMOOTHING_SAMPLE_COUNT: usize = 5;
14const MIN_WARMUP_SAMPLES: usize = 3;
15
16#[derive(Debug, Clone, Copy)]
17struct PowerSample {
18    cpu_power: f32,
19    gpu_power: f32,
20    system_power: f32,
21}
22
23#[derive(Debug)]
24struct RaplDomain {
25    path: PathBuf,
26    _name: String,
27    last_energy_uj: u64,
28    last_time: Instant,
29}
30
31pub struct LinuxPower {
32    info: PowerInfo,
33    rapl_domains: Vec<RaplDomain>,
34    gpu_hwmon_path: Option<PathBuf>,
35    samples: VecDeque<PowerSample>,
36    cpu_power: f32,
37    gpu_power: f32,
38    system_power: f32,
39}
40
41impl PowerProvider for LinuxPower {
42    fn new() -> Result<Self> {
43        let rapl_domains = discover_rapl_domains();
44        let gpu_hwmon_path = discover_gpu_hwmon();
45
46        let mut provider = Self {
47            info: PowerInfo::default(),
48            rapl_domains,
49            gpu_hwmon_path,
50            samples: VecDeque::with_capacity(SMOOTHING_SAMPLE_COUNT),
51            cpu_power: 0.0,
52            gpu_power: 0.0,
53            system_power: 0.0,
54        };
55
56        std::thread::sleep(Duration::from_millis(100));
57        provider.refresh()?;
58
59        Ok(provider)
60    }
61
62    fn refresh(&mut self) -> Result<()> {
63        self.refresh_rapl_power();
64        self.refresh_gpu_power();
65        self.system_power = self.cpu_power + self.gpu_power;
66        self.record_sample();
67        self.update_info();
68        Ok(())
69    }
70
71    fn info(&self) -> &PowerInfo {
72        &self.info
73    }
74
75    fn is_supported() -> bool {
76        Path::new(RAPL_PATH).exists()
77    }
78}
79
80impl LinuxPower {
81    fn update_info(&mut self) {
82        self.info.cpu_power_watts = self.smoothed_value(|s| s.cpu_power);
83        self.info.gpu_power_watts = self.smoothed_value(|s| s.gpu_power);
84        self.info.system_power_watts = self.smoothed_value(|s| s.system_power);
85        self.info.is_warmed_up = self.samples.len() >= MIN_WARMUP_SAMPLES;
86        self.info.power_mode = PowerMode::Unknown;
87    }
88
89    fn record_sample(&mut self) {
90        let sample = PowerSample {
91            cpu_power: self.cpu_power,
92            gpu_power: self.gpu_power,
93            system_power: self.system_power,
94        };
95
96        if self.samples.len() >= SMOOTHING_SAMPLE_COUNT {
97            self.samples.pop_front();
98        }
99        self.samples.push_back(sample);
100    }
101
102    fn smoothed_value<F>(&self, extractor: F) -> f32
103    where
104        F: Fn(&PowerSample) -> f32,
105    {
106        if self.samples.is_empty() {
107            return 0.0;
108        }
109        let sum: f32 = self.samples.iter().map(extractor).sum();
110        sum / self.samples.len() as f32
111    }
112
113    fn refresh_rapl_power(&mut self) {
114        let mut total_cpu_power = 0.0f32;
115        let now = Instant::now();
116
117        for domain in &mut self.rapl_domains {
118            let energy_path = domain.path.join("energy_uj");
119            if let Ok(content) = fs::read_to_string(&energy_path) {
120                if let Ok(energy_uj) = content.trim().parse::<u64>() {
121                    let elapsed = now.duration_since(domain.last_time);
122                    let elapsed_us = elapsed.as_micros() as u64;
123
124                    if elapsed_us > 0 && domain.last_energy_uj > 0 {
125                        let energy_delta = if energy_uj >= domain.last_energy_uj {
126                            energy_uj - domain.last_energy_uj
127                        } else {
128                            energy_uj
129                        };
130
131                        let watts = energy_delta as f32 / elapsed_us as f32;
132                        total_cpu_power += watts;
133                    }
134
135                    domain.last_energy_uj = energy_uj;
136                    domain.last_time = now;
137                }
138            }
139        }
140
141        self.cpu_power = total_cpu_power;
142    }
143
144    fn refresh_gpu_power(&mut self) {
145        if let Some(ref path) = self.gpu_hwmon_path {
146            if let Ok(content) = fs::read_to_string(path) {
147                if let Ok(microwatts) = content.trim().parse::<u64>() {
148                    self.gpu_power = microwatts as f32 / 1_000_000.0;
149                    return;
150                }
151            }
152        }
153        self.gpu_power = 0.0;
154    }
155}
156
157fn discover_rapl_domains() -> Vec<RaplDomain> {
158    let mut domains = Vec::new();
159    let rapl_path = Path::new(RAPL_PATH);
160
161    if !rapl_path.exists() {
162        return domains;
163    }
164
165    if let Ok(entries) = fs::read_dir(rapl_path) {
166        for entry in entries.flatten() {
167            let path = entry.path();
168            if !path.is_dir() {
169                continue;
170            }
171
172            let name_path = path.join("name");
173            let energy_path = path.join("energy_uj");
174
175            if energy_path.exists() {
176                let name = fs::read_to_string(&name_path)
177                    .map(|s| s.trim().to_string())
178                    .unwrap_or_else(|_| "unknown".to_string());
179
180                if name.contains("package") || name.contains("psys") {
181                    let last_energy_uj = fs::read_to_string(&energy_path)
182                        .ok()
183                        .and_then(|s| s.trim().parse().ok())
184                        .unwrap_or(0);
185
186                    domains.push(RaplDomain {
187                        path,
188                        _name: name,
189                        last_energy_uj,
190                        last_time: Instant::now(),
191                    });
192                }
193            }
194        }
195    }
196
197    domains
198}
199
200fn discover_gpu_hwmon() -> Option<PathBuf> {
201    let hwmon_path = Path::new(HWMON_PATH);
202    if !hwmon_path.exists() {
203        return None;
204    }
205
206    if let Ok(entries) = fs::read_dir(hwmon_path) {
207        for entry in entries.flatten() {
208            let path = entry.path();
209
210            let name_path = path.join("name");
211            if let Ok(name) = fs::read_to_string(&name_path) {
212                let name = name.trim().to_lowercase();
213                if name.contains("amdgpu") || name.contains("i915") || name.contains("nouveau") {
214                    let power_path = path.join("power1_input");
215                    if power_path.exists() {
216                        return Some(power_path);
217                    }
218                }
219            }
220        }
221    }
222
223    None
224}