jolt_platform/linux/
power.rs1use 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}