use crate::observation::source;
use crate::observation::{
BindContext, Perturbation, Sensor, SensorDescriptor, SensorId, SensorOutcome, StateWriter,
Uncertainty,
};
use corescout_core::error::{Error, Result};
use corescout_mirror::entity::{keys, Entity, EntityClass};
use corescout_mirror::relation::{Relation, RelationKind};
use corescout_mirror::state::{ChannelId, Semantics, Unit};
use std::path::PathBuf;
struct Target {
row: u32,
energy: PathBuf,
range: PathBuf,
limit: PathBuf,
}
pub struct PowerSensor {
targets: Vec<Target>,
channel_energy: Option<ChannelId>,
channel_range: Option<ChannelId>,
channel_limit: Option<ChannelId>,
}
impl PowerSensor {
pub fn new() -> PowerSensor {
PowerSensor {
targets: Vec::new(),
channel_energy: None,
channel_range: None,
channel_limit: None,
}
}
}
impl Default for PowerSensor {
fn default() -> Self {
Self::new()
}
}
impl Sensor for PowerSensor {
fn descriptor(&self) -> SensorDescriptor {
SensorDescriptor {
id: SensorId(4),
key: "power",
physical_fact: "cumulative energy consumed by each RAPL domain, and the power \
limits currently enforced on it",
source: "/sys/class/powercap/intel-rapl:*/",
max_rate_hz: 100.0,
perturbation: Perturbation::Negligible,
uncertainty: Uncertainty::relative(
0.05,
"RAPL is a firmware energy model derived from activity counters, not a \
measurement at the power rail; it tracks real consumption closely on CPU \
domains and less well on package and DRAM domains",
),
requires_privilege: true,
}
}
fn bind(&mut self, ctx: &mut BindContext<'_>) -> Result<()> {
let powercap = ctx.substrate().roots.sys.join("class/powercap");
let mut targets = Vec::new();
for (dir_name, path) in source::named_children(&powercap, "intel-rapl:") {
let energy = path.join("energy_uj");
if !energy.exists() {
continue;
}
if source::u64(&energy).is_none() {
continue;
}
let name = source::string(path.join("name")).unwrap_or(dir_name);
let row = ctx.declare_entity(Entity::new(
keys::power_domain(&name),
EntityClass::PowerDomain,
None,
));
if let Some(machine) = ctx.row_of("machine") {
ctx.declare_relation(Relation::new(row, machine, RelationKind::PowerDomain));
}
targets.push(Target {
row,
energy,
range: path.join("max_energy_range_uj"),
limit: path.join("constraint_0_power_limit_uw"),
});
}
if targets.is_empty() {
return Err(Error::unsupported(
"no readable RAPL energy domains (absent on AMD without amd_energy, on most \
VMs, and root-only on distributions that restrict energy_uj)",
));
}
self.targets = targets;
self.channel_energy =
Some(ctx.declare_channel("power.energy", Unit::Microjoule, Semantics::Cumulative));
self.channel_range = Some(ctx.declare_channel(
"power.energy_wrap_at",
Unit::Microjoule,
Semantics::Configured,
));
self.channel_limit =
Some(ctx.declare_channel("power.limit", Unit::Microwatt, Semantics::Configured));
Ok(())
}
fn observe(&mut self, out: &mut StateWriter<'_>) -> SensorOutcome {
let mut outcome = SensorOutcome::default();
for target in &self.targets {
source::sample(
out,
&mut outcome,
target.row,
self.channel_energy,
&target.energy,
true,
);
source::sample(
out,
&mut outcome,
target.row,
self.channel_range,
&target.range,
false,
);
source::sample(
out,
&mut outcome,
target.row,
self.channel_limit,
&target.limit,
false,
);
}
outcome
}
}