Skip to main content

corescout_substrate/observation/
frequency.rs

1//! CPU frequency state.
2//!
3//! | property | value |
4//! |---|---|
5//! | physical fact | the clock rate the core's PLL is running at |
6//! | source | `/sys/devices/system/cpu/cpuN/cpufreq/` |
7//! | sample rate | ~100 Hz useful; the governor re-evaluates on the order of milliseconds |
8//! | cost | one small file read per CPU per channel |
9//! | perturbation | **Low**, and this one deserves a paragraph |
10//! | uncertainty | `scaling_cur_freq` is a request, not a measurement |
11//!
12//! # Why this sensor perturbs the machine
13//!
14//! Reading `scaling_cur_freq` is not free and does not merely cost the reader.
15//! On `acpi-cpufreq` the kernel reads `APERF`/`MPERF` on the *target* CPU, which
16//! it does with `smp_call_function_single`: an inter-processor interrupt that
17//! pulls the observed core out of whatever it was doing. On `intel_pstate` the
18//! same interface returns the driver's cached request instead, which is cheap
19//! and is also not a measurement of anything physical.
20//!
21//! So this sensor sits exactly on the awkward edge the architecture exists to
22//! make visible: the reading is either expensive and true, or cheap and
23//! approximate, and which one you get depends on a driver the mirror does not
24//! choose. Both cases are declared as `Low` because the mirror cannot tell which
25//! it is on, and over-declaring perturbation is the safe direction.
26//!
27//! The original CoreScout read this file in its topology walk without comment.
28//! That was the clearest instance of assuming observation is free.
29//!
30//! # What `current` actually means
31//!
32//! `scaling_cur_freq` is the frequency the governor last asked for.
33//! `cpuinfo_cur_freq` is closer to what the hardware is doing but usually
34//! requires privilege. Neither is the instantaneous clock, which varies over
35//! microseconds and cannot be sampled from software at all. The channel is
36//! published for what it is.
37
38use crate::observation::source;
39use crate::observation::{
40    BindContext, Perturbation, Sensor, SensorDescriptor, SensorId, SensorOutcome, StateWriter,
41    Uncertainty,
42};
43use corescout_core::cpuset::CpuSet;
44use corescout_core::error::{Error, Result};
45use corescout_mirror::entity::keys;
46use corescout_mirror::relation::{Relation, RelationKind};
47use corescout_mirror::state::{ChannelId, Semantics, Unit};
48use std::path::PathBuf;
49
50/// One CPU's resolved cpufreq paths, worked out once at bind time.
51struct Target {
52    row: u32,
53    current: PathBuf,
54    min: PathBuf,
55    max: PathBuf,
56    base: PathBuf,
57}
58
59pub struct FrequencySensor {
60    targets: Vec<Target>,
61    channel_current: Option<ChannelId>,
62    channel_min: Option<ChannelId>,
63    channel_max: Option<ChannelId>,
64    channel_base: Option<ChannelId>,
65}
66
67impl FrequencySensor {
68    pub fn new() -> FrequencySensor {
69        FrequencySensor {
70            targets: Vec::new(),
71            channel_current: None,
72            channel_min: None,
73            channel_max: None,
74            channel_base: None,
75        }
76    }
77}
78
79impl Default for FrequencySensor {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl Sensor for FrequencySensor {
86    fn descriptor(&self) -> SensorDescriptor {
87        SensorDescriptor {
88            id: SensorId(1),
89            key: "frequency",
90            physical_fact: "the clock rate each core is running at, as the cpufreq driver \
91                            reports it",
92            source: "/sys/devices/system/cpu/cpuN/cpufreq/",
93            max_rate_hz: 100.0,
94            // See the module docs: on some drivers this costs the observed core
95            // an IPI.
96            perturbation: Perturbation::Low,
97            uncertainty: Uncertainty::relative(
98                0.05,
99                "scaling_cur_freq is the governor's request, not a measurement; the core \
100                 may be at a different point in its ramp, or held lower by a power limit",
101            ),
102            requires_privilege: false,
103        }
104    }
105
106    fn bind(&mut self, ctx: &mut BindContext<'_>) -> Result<()> {
107        let cpu_dir = ctx.substrate().roots.sys.join("devices/system/cpu");
108        let online: Vec<u32> = ctx
109            .substrate()
110            .topology
111            .logical_cpus
112            .iter()
113            .filter(|c| c.online)
114            .map(|c| c.id)
115            .collect();
116
117        let mut targets = Vec::new();
118        let mut domains: Vec<(String, Vec<u32>)> = Vec::new();
119        for cpu in online {
120            let dir = cpu_dir.join(format!("cpu{cpu}/cpufreq"));
121            if !dir.exists() {
122                continue;
123            }
124            let Some(row) = ctx.row_of(&keys::logical_cpu(cpu)) else {
125                continue;
126            };
127            targets.push(Target {
128                row,
129                current: dir.join("scaling_cur_freq"),
130                min: dir.join("cpuinfo_min_freq"),
131                max: dir.join("cpuinfo_max_freq"),
132                base: dir.join("base_frequency"),
133            });
134
135            // Which CPUs change frequency together is a real relation between
136            // entities, and one that no other part of the topology exposes.
137            if let Some(related) = source::string(dir.join("related_cpus")) {
138                if let Ok(set) = CpuSet::parse_list(&related) {
139                    let members = set.to_vec();
140                    if members.len() > 1 {
141                        let key = corescout_core::cpuset::format_list(&members);
142                        if !domains.iter().any(|(k, _)| *k == key) {
143                            domains.push((key, members));
144                        }
145                    }
146                }
147            }
148        }
149
150        if targets.is_empty() {
151            return Err(Error::unsupported(
152                "cpufreq is not present on this machine (no scaling driver, or a container \
153                 without /sys/devices/system/cpu/*/cpufreq)",
154            ));
155        }
156
157        // Frequency domains, as edges between the CPUs that share them.
158        for (_, members) in domains {
159            for a in &members {
160                for b in &members {
161                    if a == b {
162                        continue;
163                    }
164                    if let (Some(from), Some(to)) = (
165                        ctx.row_of(&keys::logical_cpu(*a)),
166                        ctx.row_of(&keys::logical_cpu(*b)),
167                    ) {
168                        ctx.declare_relation(Relation::new(
169                            from,
170                            to,
171                            RelationKind::FrequencyDomain,
172                        ));
173                    }
174                }
175            }
176        }
177
178        self.targets = targets;
179        self.channel_current =
180            Some(ctx.declare_channel("cpu.frequency.current", Unit::Kilohertz, Semantics::Instant));
181        self.channel_min =
182            Some(ctx.declare_channel("cpu.frequency.min", Unit::Kilohertz, Semantics::Configured));
183        self.channel_max =
184            Some(ctx.declare_channel("cpu.frequency.max", Unit::Kilohertz, Semantics::Configured));
185        self.channel_base =
186            Some(ctx.declare_channel("cpu.frequency.base", Unit::Kilohertz, Semantics::Configured));
187        Ok(())
188    }
189
190    fn observe(&mut self, out: &mut StateWriter<'_>) -> SensorOutcome {
191        let mut outcome = SensorOutcome::default();
192        for target in &self.targets {
193            // A CPU can go offline between binding and reading. That is a hole
194            // in this snapshot, not a failure of the mirror, but it is worth
195            // counting.
196            source::sample(
197                out,
198                &mut outcome,
199                target.row,
200                self.channel_current,
201                &target.current,
202                true,
203            );
204            source::sample(
205                out,
206                &mut outcome,
207                target.row,
208                self.channel_min,
209                &target.min,
210                true,
211            );
212            source::sample(
213                out,
214                &mut outcome,
215                target.row,
216                self.channel_max,
217                &target.max,
218                true,
219            );
220            // base_frequency exists only on intel_pstate and amd_pstate. Its
221            // absence is expected and is not an error.
222            source::sample(
223                out,
224                &mut outcome,
225                target.row,
226                self.channel_base,
227                &target.base,
228                false,
229            );
230        }
231        outcome
232    }
233}