Skip to main content

corescout_substrate/observation/
idle.rs

1//! Idle state residency.
2//!
3//! | property | value |
4//! |---|---|
5//! | physical fact | how long each core has spent in each hardware idle state, and how often it entered |
6//! | source | `/sys/devices/system/cpu/cpuN/cpuidle/stateK/{usage,time}` |
7//! | sample rate | ~1 kHz; the counters update on every idle entry and exit |
8//! | cost | two small file reads per CPU per idle state |
9//! | perturbation | **Negligible**: the kernel already maintains these counters for its own governor, and reading them touches no other CPU |
10//! | uncertainty | residency is accounted at state entry and exit, so a core currently idle has not yet accrued its present stay |
11//!
12//! # Why the counters and not "is it idle right now"
13//!
14//! "Is this core idle" is not a well-formed question at the resolution the
15//! mirror samples at: a core enters and leaves C1 thousands of times a second,
16//! so a boolean sampled at 100 Hz is a coin flip, not an observation.
17//!
18//! The cumulative counters are exact and complete. A consumer that wants
19//! occupancy differences two snapshots and gets the true residency over that
20//! interval, with no sampling error at all. This is the clearest example of why
21//! the mirror publishes counters and leaves rates to the memory layer: the
22//! counter is both cheaper *and* more accurate than any instantaneous reading
23//! the mirror could synthesise.
24//!
25//! # Deeper states are the interesting ones
26//!
27//! Exit latency rises sharply with C-state depth, tens of microseconds for the
28//! deep states on a server part. A core sitting in a deep state is a core that
29//! will respond late to the next thing it is asked to do, which is exactly the
30//! property a latency-sensitive consumer wants to know about and which no
31//! frequency or temperature reading exposes.
32
33use 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
43/// One (CPU, idle state) pair, resolved at bind time.
44struct Target {
45    row: u32,
46    state: u32,
47    usage: PathBuf,
48    time: PathBuf,
49}
50
51pub struct IdleSensor {
52    targets: Vec<Target>,
53    /// One channel pair per state index, since state 2 on one core is the same
54    /// hardware state as state 2 on another.
55    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            // Count states per CPU as we go; targets are grouped by CPU.
177            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}