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;
use corescout_mirror::state::{ChannelId, Semantics, Unit};
use std::path::PathBuf;
struct Target {
row: u32,
state: u32,
usage: PathBuf,
time: PathBuf,
}
pub struct IdleSensor {
targets: Vec<Target>,
usage_channels: Vec<ChannelId>,
time_channels: Vec<ChannelId>,
depth_channel: Option<ChannelId>,
}
impl IdleSensor {
pub fn new() -> IdleSensor {
IdleSensor {
targets: Vec::new(),
usage_channels: Vec::new(),
time_channels: Vec::new(),
depth_channel: None,
}
}
}
impl Default for IdleSensor {
fn default() -> Self {
Self::new()
}
}
impl Sensor for IdleSensor {
fn descriptor(&self) -> SensorDescriptor {
SensorDescriptor {
id: SensorId(2),
key: "idle",
physical_fact: "cumulative time and entry count for each hardware idle state of \
each core",
source: "/sys/devices/system/cpu/cpuN/cpuidle/stateK/",
max_rate_hz: 1000.0,
perturbation: Perturbation::Negligible,
uncertainty: Uncertainty::unknown(
"the kernel accounts residency on state exit, so a core's current stay in a \
state is not yet included in its total",
),
requires_privilege: false,
}
}
fn bind(&mut self, ctx: &mut BindContext<'_>) -> Result<()> {
let cpu_dir = ctx.substrate().roots.sys.join("devices/system/cpu");
let cpus: Vec<u32> = ctx
.substrate()
.topology
.logical_cpus
.iter()
.filter(|c| c.online)
.map(|c| c.id)
.collect();
let mut targets = Vec::new();
let mut max_state = 0u32;
for cpu in cpus {
let Some(row) = ctx.row_of(&keys::logical_cpu(cpu)) else {
continue;
};
let dir = cpu_dir.join(format!("cpu{cpu}/cpuidle"));
for (state, path) in source::numbered_children(&dir, "state") {
max_state = max_state.max(state);
targets.push(Target {
row,
state,
usage: path.join("usage"),
time: path.join("time"),
});
}
}
if targets.is_empty() {
return Err(Error::unsupported(
"cpuidle is not exposed (no idle driver, or a container without \
/sys/devices/system/cpu/*/cpuidle)",
));
}
for state in 0..=max_state {
self.usage_channels.push(ctx.declare_channel(
format!("cpu.idle.state{state}.entries"),
Unit::Count,
Semantics::Cumulative,
));
self.time_channels.push(ctx.declare_channel(
format!("cpu.idle.state{state}.residency"),
Unit::Microsecond,
Semantics::Cumulative,
));
}
self.depth_channel = Some(ctx.declare_channel(
"cpu.idle.states_available",
Unit::Count,
Semantics::Configured,
));
self.targets = targets;
Ok(())
}
fn observe(&mut self, out: &mut StateWriter<'_>) -> SensorOutcome {
let mut outcome = SensorOutcome::default();
let mut last_row: Option<u32> = None;
let mut states_for_row = 0.0;
for target in &self.targets {
let index = target.state as usize;
source::sample(
out,
&mut outcome,
target.row,
self.usage_channels.get(index).copied(),
&target.usage,
true,
);
source::sample(
out,
&mut outcome,
target.row,
self.time_channels.get(index).copied(),
&target.time,
true,
);
match last_row {
Some(row) if row == target.row => states_for_row += 1.0,
Some(row) => {
source::emit(out, &mut outcome, row, self.depth_channel, states_for_row);
states_for_row = 1.0;
}
None => states_for_row = 1.0,
}
last_row = Some(target.row);
}
if let Some(row) = last_row {
source::emit(out, &mut outcome, row, self.depth_channel, states_for_row);
}
outcome
}
}