rigtop_core/linux/
rapl.rs1use std::path::{Path, PathBuf};
11use std::time::Instant;
12
13use super::{SysRoots, read_trimmed, read_u64, sorted_entries};
14
15#[derive(Debug, Default)]
17pub struct RaplMeter {
18 zones: Option<Vec<Zone>>,
20 last_sample: Option<Instant>,
22}
23
24#[derive(Debug)]
25struct Zone {
26 energy_path: PathBuf,
28 max_range_uj: u64,
30 last_uj: Option<u64>,
32}
33
34impl RaplMeter {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn sample(&mut self, roots: &SysRoots, now: Instant) -> Option<f32> {
46 let zones = self.zones.get_or_insert_with(|| discover_zones(roots));
47 if zones.is_empty() {
48 return None;
49 }
50 let elapsed = self
51 .last_sample
52 .map(|prev| now.saturating_duration_since(prev).as_secs_f64());
53 self.last_sample = Some(now);
54
55 let mut total_w = 0.0;
56 let mut complete = true;
57 for zone in zones.iter_mut() {
58 let current = read_u64(&zone.energy_path);
59 match (zone.last_uj, current, elapsed) {
60 (Some(prev), Some(cur), Some(secs)) => {
61 match power_from_delta(prev, cur, zone.max_range_uj, secs) {
62 Some(w) => total_w += w,
63 None => complete = false,
64 }
65 }
66 _ => complete = false,
67 }
68 zone.last_uj = current;
69 }
70 complete.then_some(total_w as f32)
71 }
72}
73
74fn discover_zones(roots: &SysRoots) -> Vec<Zone> {
78 let class_dir = roots.sys.join("class/powercap");
79 sorted_entries(&class_dir)
80 .into_iter()
81 .filter(|entry| {
82 entry.matches(':').count() == 1 && entry.ends_with(|c: char| c.is_ascii_digit())
83 })
84 .map(|entry| class_dir.join(entry))
85 .filter(|dir| is_package_zone(dir))
86 .filter_map(|dir| {
87 let energy_path = dir.join("energy_uj");
88 read_u64(&energy_path)?;
90 Some(Zone {
91 max_range_uj: read_u64(&dir.join("max_energy_range_uj")).unwrap_or(0),
92 energy_path,
93 last_uj: None,
94 })
95 })
96 .collect()
97}
98
99fn is_package_zone(dir: &Path) -> bool {
100 read_trimmed(&dir.join("name")).is_some_and(|name| name.starts_with("package"))
101}
102
103pub(crate) fn power_from_delta(
108 prev_uj: u64,
109 cur_uj: u64,
110 max_range_uj: u64,
111 secs: f64,
112) -> Option<f64> {
113 if secs < 0.01 {
115 return None;
116 }
117 let delta = if cur_uj >= prev_uj {
118 cur_uj - prev_uj
119 } else if max_range_uj > prev_uj {
120 (max_range_uj - prev_uj) + cur_uj
122 } else {
123 return None;
124 };
125 Some(delta as f64 / 1_000_000.0 / secs)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::linux::testutil::write;
132 use std::time::Duration;
133
134 #[test]
135 fn power_from_plain_delta() {
136 let w = power_from_delta(1_000_000, 51_000_000, 0, 2.0).unwrap();
138 assert!((w - 25.0).abs() < 1e-9);
139 }
140
141 #[test]
142 fn power_across_wrap_around() {
143 let w = power_from_delta(90_000_000, 5_000_000, 100_000_000, 1.0).unwrap();
145 assert!((w - 15.0).abs() < 1e-9);
146 }
147
148 #[test]
149 fn power_rejects_tiny_interval_and_unknown_wrap() {
150 assert!(power_from_delta(0, 10, 0, 0.001).is_none());
151 assert!(power_from_delta(10, 5, 0, 1.0).is_none());
152 }
153
154 #[test]
155 fn meter_sums_packages_and_skips_subzones() {
156 let tmp = tempfile::tempdir().unwrap();
157 let root = tmp.path();
158 write(root, "sys/class/powercap/intel-rapl:0/name", "package-0\n");
159 write(
160 root,
161 "sys/class/powercap/intel-rapl:0/energy_uj",
162 "1000000\n",
163 );
164 write(
165 root,
166 "sys/class/powercap/intel-rapl:0/max_energy_range_uj",
167 "65532610987\n",
168 );
169 write(root, "sys/class/powercap/intel-rapl:0:0/name", "core\n");
171 write(
172 root,
173 "sys/class/powercap/intel-rapl:0:0/energy_uj",
174 "500000\n",
175 );
176 let roots = SysRoots {
177 sys: root.join("sys"),
178 proc: root.join("proc"),
179 };
180
181 let mut meter = RaplMeter::new();
182 let t0 = Instant::now();
183 assert_eq!(meter.sample(&roots, t0), None);
184
185 write(
186 root,
187 "sys/class/powercap/intel-rapl:0/energy_uj",
188 "31000000\n",
189 );
190 write(
191 root,
192 "sys/class/powercap/intel-rapl:0:0/energy_uj",
193 "99000000\n",
194 );
195 let w = meter.sample(&roots, t0 + Duration::from_secs(1)).unwrap();
196 assert!((w - 30.0).abs() < 1e-3);
197 }
198}