corescout_substrate/observation/
idle.rs1use crate::observation::source;
34use crate::observation::{
35 BindContext, Perturbation, Sensor, SensorDescriptor, SensorId, SensorOutcome, StateWriter,
36 Uncertainty,
37};
38use corescout_core::error::{Error, Result};
39use corescout_mirror::entity::keys;
40use corescout_mirror::state::{ChannelId, Semantics, Unit};
41use std::path::PathBuf;
42
43struct Target {
45 row: u32,
46 state: u32,
47 usage: PathBuf,
48 time: PathBuf,
49}
50
51pub struct IdleSensor {
52 targets: Vec<Target>,
53 usage_channels: Vec<ChannelId>,
56 time_channels: Vec<ChannelId>,
57 depth_channel: Option<ChannelId>,
58}
59
60impl IdleSensor {
61 pub fn new() -> IdleSensor {
62 IdleSensor {
63 targets: Vec::new(),
64 usage_channels: Vec::new(),
65 time_channels: Vec::new(),
66 depth_channel: None,
67 }
68 }
69}
70
71impl Default for IdleSensor {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl Sensor for IdleSensor {
78 fn descriptor(&self) -> SensorDescriptor {
79 SensorDescriptor {
80 id: SensorId(2),
81 key: "idle",
82 physical_fact: "cumulative time and entry count for each hardware idle state of \
83 each core",
84 source: "/sys/devices/system/cpu/cpuN/cpuidle/stateK/",
85 max_rate_hz: 1000.0,
86 perturbation: Perturbation::Negligible,
87 uncertainty: Uncertainty::unknown(
88 "the kernel accounts residency on state exit, so a core's current stay in a \
89 state is not yet included in its total",
90 ),
91 requires_privilege: false,
92 }
93 }
94
95 fn bind(&mut self, ctx: &mut BindContext<'_>) -> Result<()> {
96 let cpu_dir = ctx.substrate().roots.sys.join("devices/system/cpu");
97 let cpus: Vec<u32> = ctx
98 .substrate()
99 .topology
100 .logical_cpus
101 .iter()
102 .filter(|c| c.online)
103 .map(|c| c.id)
104 .collect();
105
106 let mut targets = Vec::new();
107 let mut max_state = 0u32;
108 for cpu in cpus {
109 let Some(row) = ctx.row_of(&keys::logical_cpu(cpu)) else {
110 continue;
111 };
112 let dir = cpu_dir.join(format!("cpu{cpu}/cpuidle"));
113 for (state, path) in source::numbered_children(&dir, "state") {
114 max_state = max_state.max(state);
115 targets.push(Target {
116 row,
117 state,
118 usage: path.join("usage"),
119 time: path.join("time"),
120 });
121 }
122 }
123
124 if targets.is_empty() {
125 return Err(Error::unsupported(
126 "cpuidle is not exposed (no idle driver, or a container without \
127 /sys/devices/system/cpu/*/cpuidle)",
128 ));
129 }
130
131 for state in 0..=max_state {
132 self.usage_channels.push(ctx.declare_channel(
133 format!("cpu.idle.state{state}.entries"),
134 Unit::Count,
135 Semantics::Cumulative,
136 ));
137 self.time_channels.push(ctx.declare_channel(
138 format!("cpu.idle.state{state}.residency"),
139 Unit::Microsecond,
140 Semantics::Cumulative,
141 ));
142 }
143 self.depth_channel = Some(ctx.declare_channel(
144 "cpu.idle.states_available",
145 Unit::Count,
146 Semantics::Configured,
147 ));
148 self.targets = targets;
149 Ok(())
150 }
151
152 fn observe(&mut self, out: &mut StateWriter<'_>) -> SensorOutcome {
153 let mut outcome = SensorOutcome::default();
154 let mut last_row: Option<u32> = None;
155 let mut states_for_row = 0.0;
156
157 for target in &self.targets {
158 let index = target.state as usize;
159 source::sample(
160 out,
161 &mut outcome,
162 target.row,
163 self.usage_channels.get(index).copied(),
164 &target.usage,
165 true,
166 );
167 source::sample(
168 out,
169 &mut outcome,
170 target.row,
171 self.time_channels.get(index).copied(),
172 &target.time,
173 true,
174 );
175
176 match last_row {
178 Some(row) if row == target.row => states_for_row += 1.0,
179 Some(row) => {
180 source::emit(out, &mut outcome, row, self.depth_channel, states_for_row);
181 states_for_row = 1.0;
182 }
183 None => states_for_row = 1.0,
184 }
185 last_row = Some(target.row);
186 }
187 if let Some(row) = last_row {
188 source::emit(out, &mut outcome, row, self.depth_channel, states_for_row);
189 }
190 outcome
191 }
192}