corescout_substrate/observation/
power.rs1use crate::observation::source;
39use crate::observation::{
40 BindContext, Perturbation, Sensor, SensorDescriptor, SensorId, SensorOutcome, StateWriter,
41 Uncertainty,
42};
43use corescout_core::error::{Error, Result};
44use corescout_mirror::entity::{keys, Entity, EntityClass};
45use corescout_mirror::relation::{Relation, RelationKind};
46use corescout_mirror::state::{ChannelId, Semantics, Unit};
47use std::path::PathBuf;
48
49struct Target {
50 row: u32,
51 energy: PathBuf,
52 range: PathBuf,
53 limit: PathBuf,
54}
55
56pub struct PowerSensor {
57 targets: Vec<Target>,
58 channel_energy: Option<ChannelId>,
59 channel_range: Option<ChannelId>,
60 channel_limit: Option<ChannelId>,
61}
62
63impl PowerSensor {
64 pub fn new() -> PowerSensor {
65 PowerSensor {
66 targets: Vec::new(),
67 channel_energy: None,
68 channel_range: None,
69 channel_limit: None,
70 }
71 }
72}
73
74impl Default for PowerSensor {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl Sensor for PowerSensor {
81 fn descriptor(&self) -> SensorDescriptor {
82 SensorDescriptor {
83 id: SensorId(4),
84 key: "power",
85 physical_fact: "cumulative energy consumed by each RAPL domain, and the power \
86 limits currently enforced on it",
87 source: "/sys/class/powercap/intel-rapl:*/",
88 max_rate_hz: 100.0,
89 perturbation: Perturbation::Negligible,
90 uncertainty: Uncertainty::relative(
91 0.05,
92 "RAPL is a firmware energy model derived from activity counters, not a \
93 measurement at the power rail; it tracks real consumption closely on CPU \
94 domains and less well on package and DRAM domains",
95 ),
96 requires_privilege: true,
97 }
98 }
99
100 fn bind(&mut self, ctx: &mut BindContext<'_>) -> Result<()> {
101 let powercap = ctx.substrate().roots.sys.join("class/powercap");
102 let mut targets = Vec::new();
103
104 for (dir_name, path) in source::named_children(&powercap, "intel-rapl:") {
105 let energy = path.join("energy_uj");
106 if !energy.exists() {
107 continue;
108 }
109 if source::u64(&energy).is_none() {
113 continue;
114 }
115 let name = source::string(path.join("name")).unwrap_or(dir_name);
116 let row = ctx.declare_entity(Entity::new(
117 keys::power_domain(&name),
118 EntityClass::PowerDomain,
119 None,
120 ));
121 if let Some(machine) = ctx.row_of("machine") {
122 ctx.declare_relation(Relation::new(row, machine, RelationKind::PowerDomain));
123 }
124 targets.push(Target {
125 row,
126 energy,
127 range: path.join("max_energy_range_uj"),
128 limit: path.join("constraint_0_power_limit_uw"),
129 });
130 }
131
132 if targets.is_empty() {
133 return Err(Error::unsupported(
134 "no readable RAPL energy domains (absent on AMD without amd_energy, on most \
135 VMs, and root-only on distributions that restrict energy_uj)",
136 ));
137 }
138
139 self.targets = targets;
140 self.channel_energy =
141 Some(ctx.declare_channel("power.energy", Unit::Microjoule, Semantics::Cumulative));
142 self.channel_range = Some(ctx.declare_channel(
143 "power.energy_wrap_at",
144 Unit::Microjoule,
145 Semantics::Configured,
146 ));
147 self.channel_limit =
148 Some(ctx.declare_channel("power.limit", Unit::Microwatt, Semantics::Configured));
149 Ok(())
150 }
151
152 fn observe(&mut self, out: &mut StateWriter<'_>) -> SensorOutcome {
153 let mut outcome = SensorOutcome::default();
154 for target in &self.targets {
155 source::sample(
156 out,
157 &mut outcome,
158 target.row,
159 self.channel_energy,
160 &target.energy,
161 true,
162 );
163 source::sample(
164 out,
165 &mut outcome,
166 target.row,
167 self.channel_range,
168 &target.range,
169 false,
170 );
171 source::sample(
172 out,
173 &mut outcome,
174 target.row,
175 self.channel_limit,
176 &target.limit,
177 false,
178 );
179 }
180 outcome
181 }
182}