Skip to main content

corescout_substrate/observation/
mod.rs

1//! Observation: how the machine samples itself, and what that costs.
2//!
3//! # The rule this layer enforces
4//!
5//! > Observation belongs to the mirror. Intentional perturbation belongs to
6//! > experimentation.
7//!
8//! Every sensor here is passive: it reads state the machine is maintaining
9//! anyway, for its own reasons, whether or not anyone is looking. Nothing in
10//! this module runs a workload, moves a thread, changes a frequency or requests
11//! an idle state. Code that wants to do those things lives in
12//! `corescout-experiment` or `corescout-agency`, and is not part of `M(t)`.
13//!
14//! # Observation is not free
15//!
16//! A computational mirror is unusual among mirrors: the act of looking consumes
17//! the thing being looked at. Reading a sensor costs cycles on some CPU, evicts
18//! cache lines, may take a kernel lock, and in several real cases sends an
19//! inter-processor interrupt to the very core whose state is in question.
20//!
21//! So every sensor must declare, and this is enforced by the type system rather
22//! than by convention:
23//!
24//! | property | why it is required |
25//! |---|---|
26//! | `physical_fact` | what real quantity the number approximates |
27//! | `source` | where it comes from, so a reader can go and check |
28//! | `max_rate_hz` | above this, sampling returns no new information |
29//! | `perturbation` | how much observing changes what is observed |
30//! | `uncertainty` | how wrong the number may be even when everything works |
31//! | `requires_privilege` | whether it will be absent for ordinary users |
32//!
33//! # Never fabricate
34//!
35//! A sensor that cannot read something records **why** through
36//! [`StateWriter::missing`], and the reason reaches the consumer in the
37//! snapshot's availability matrix. A machine with no RAPL does not report zero
38//! watts. The types make the honest path the easy one: writing a value and
39//! marking it observed are the same call, and there is no way to write a value
40//! without claiming you observed it.
41
42#[cfg(target_os = "windows")]
43pub mod windows;
44
45pub mod counters;
46pub mod frequency;
47pub mod idle;
48pub mod interrupts;
49pub mod power;
50pub mod scheduler;
51pub mod source;
52pub mod thermal;
53
54use corescout_core::Result;
55use corescout_mirror::entity::Entity;
56use corescout_mirror::relation::Relation;
57use corescout_mirror::schema::{Availability, AvailabilityMatrix};
58use corescout_mirror::state::{ChannelId, ChannelSpec, Semantics, StateMatrix, Unit};
59
60use crate::discovery::Substrate;
61
62// The metadata a sensor declares is part of the *representation*, so it lives
63// in `corescout-mirror` where a consumer that never links this crate can still
64// read it. Re-exported here because this is where sensors are written.
65pub use corescout_mirror::schema::{Perturbation, SensorId, SensorReport, Uncertainty};
66
67/// Everything a sensor must declare about itself.
68#[derive(Debug, Clone, PartialEq)]
69pub struct SensorDescriptor {
70    pub id: SensorId,
71    /// Stable short name, e.g. `frequency`.
72    pub key: &'static str,
73    /// The physical quantity being approximated, in one sentence.
74    pub physical_fact: &'static str,
75    /// The kernel interface or instruction the value comes from.
76    pub source: &'static str,
77    /// Above this rate, sampling returns no new information.
78    pub max_rate_hz: f64,
79    pub perturbation: Perturbation,
80    pub uncertainty: Uncertainty,
81    /// True when the sensor needs privileges an ordinary user may not have, and
82    /// will therefore be absent rather than wrong.
83    pub requires_privilege: bool,
84}
85
86/// The result of one sensor's observation pass.
87#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
88pub struct SensorOutcome {
89    pub samples: u32,
90    pub errors: u32,
91    /// Age of the underlying values at the time they were read, for sensors
92    /// whose source updates more slowly than the mirror ticks.
93    pub sample_age_ns: u64,
94}
95
96impl SensorOutcome {
97    pub fn sample(&mut self) {
98        self.samples += 1;
99    }
100
101    pub fn error(&mut self) {
102        self.errors += 1;
103    }
104
105    pub fn aged(&mut self, age_ns: u64) {
106        self.sample_age_ns = self.sample_age_ns.max(age_ns);
107    }
108}
109
110/// Handed to a sensor during binding so it can declare what it will observe.
111///
112/// Sensors may declare **entities of their own**, not only channels. A thermal
113/// zone and a RAPL power domain are real parts of the machine that the CPU
114/// topology knows nothing about, and forcing them to be attributes of a core
115/// would be exactly the human-ontology imposition this architecture avoids.
116pub struct BindContext<'a> {
117    substrate: &'a Substrate,
118    sensor: SensorId,
119    entities: &'a mut Vec<Entity>,
120    channels: &'a mut Vec<ChannelSpec>,
121    relations: &'a mut Vec<Relation>,
122}
123
124impl<'a> BindContext<'a> {
125    pub fn new(
126        substrate: &'a Substrate,
127        sensor: SensorId,
128        entities: &'a mut Vec<Entity>,
129        channels: &'a mut Vec<ChannelSpec>,
130        relations: &'a mut Vec<Relation>,
131    ) -> BindContext<'a> {
132        BindContext {
133            substrate,
134            sensor,
135            entities,
136            channels,
137            relations,
138        }
139    }
140
141    /// The structural machine description, including filesystem roots.
142    pub fn substrate(&self) -> &Substrate {
143        self.substrate
144    }
145
146    /// Row index of an already-declared entity, by natural key.
147    pub fn row_of(&self, key: &str) -> Option<u32> {
148        self.entities
149            .iter()
150            .position(|e| e.key == key)
151            .map(|i| i as u32)
152    }
153
154    /// Declare an entity, or return the existing row if its key is already
155    /// present. Idempotent, so two sensors observing the same physical thing
156    /// agree about which row it is rather than duplicating it.
157    pub fn declare_entity(&mut self, entity: Entity) -> u32 {
158        if let Some(row) = self.row_of(&entity.key) {
159            return row;
160        }
161        self.entities.push(entity);
162        (self.entities.len() - 1) as u32
163    }
164
165    /// Declare a channel and get its column index.
166    pub fn declare_channel(
167        &mut self,
168        key: impl Into<String>,
169        unit: Unit,
170        semantics: Semantics,
171    ) -> ChannelId {
172        let key = key.into();
173        if let Some(existing) = self.channels.iter().find(|c| c.key == key) {
174            return existing.id;
175        }
176        let id = ChannelId(self.channels.len() as u16);
177        self.channels.push(ChannelSpec {
178            id,
179            key,
180            unit,
181            semantics,
182            sensor: self.sensor,
183        });
184        id
185    }
186
187    /// Declare an edge.
188    pub fn declare_relation(&mut self, relation: Relation) {
189        self.relations.push(relation);
190    }
191}
192
193/// Records *why* cells are empty. Held by [`StateWriter`].
194pub struct AvailabilityWriter<'a> {
195    matrix: &'a mut AvailabilityMatrix,
196}
197
198impl<'a> AvailabilityWriter<'a> {
199    pub fn new(matrix: &'a mut AvailabilityMatrix) -> AvailabilityWriter<'a> {
200        AvailabilityWriter { matrix }
201    }
202
203    #[inline]
204    pub fn set(&mut self, row: u32, channel: ChannelId, availability: Availability) {
205        self.matrix.set(row as usize, channel.index(), availability);
206    }
207}
208
209/// Handed to a sensor during observation. The only things it can do are record
210/// a number, or record why there is not one.
211pub struct StateWriter<'a> {
212    matrix: &'a mut StateMatrix,
213    availability: AvailabilityWriter<'a>,
214}
215
216impl<'a> StateWriter<'a> {
217    pub fn new(
218        matrix: &'a mut StateMatrix,
219        availability: AvailabilityWriter<'a>,
220    ) -> StateWriter<'a> {
221        StateWriter {
222            matrix,
223            availability,
224        }
225    }
226
227    /// Record an observation.
228    ///
229    /// Out-of-range rows and columns are ignored rather than panicking: a
230    /// sensor racing a CPU hotplug event should degrade to a missing cell, not
231    /// take the whole mirror down.
232    #[inline]
233    pub fn set(&mut self, row: u32, channel: ChannelId, value: f64) {
234        let (r, c) = (row as usize, channel.index());
235        if r < self.matrix.rows() && c < self.matrix.cols() {
236            self.matrix.set(r, c, value);
237            self.availability.set(row, channel, Availability::Observed);
238        }
239    }
240
241    /// Record that a cell has no value, and why.
242    ///
243    /// The alternative, leaving it silently `NaN`, loses the distinction
244    /// between "this machine cannot tell you" and "nobody asked".
245    #[inline]
246    pub fn missing(&mut self, row: u32, channel: ChannelId, why: Availability) {
247        let (r, c) = (row as usize, channel.index());
248        if r < self.matrix.rows() && c < self.matrix.cols() {
249            self.availability.set(row, channel, why);
250        }
251    }
252}
253
254/// A passive source of observations.
255///
256/// Implementors must be honest about [`Perturbation`]. The reflector checks
257/// that no registered sensor declares [`Perturbation::Material`], but it cannot
258/// check that a sensor declaring `Negligible` is telling the truth. That is a
259/// review obligation, and it is why each sensor's module documentation states
260/// the physical basis for its claim.
261pub trait Sensor: Send {
262    fn descriptor(&self) -> SensorDescriptor;
263
264    /// Resolve everything resolvable once: which entities exist, which files to
265    /// read, which descriptors to open, which columns to fill.
266    ///
267    /// This is where CoreScout earns its keep as a normalisation layer. Path
268    /// construction, parsing setup and capability probing happen here, once per
269    /// epoch, so that [`Sensor::observe`] is close to pure I/O.
270    ///
271    /// Returning `Err` means the sensor is unavailable on this machine, which
272    /// is a normal outcome. The error's kind decides what the mirror reports:
273    /// permission denied, unsupported, or merely unavailable.
274    fn bind(&mut self, ctx: &mut BindContext<'_>) -> Result<()>;
275
276    /// Sample the machine once.
277    fn observe(&mut self, out: &mut StateWriter<'_>) -> SensorOutcome;
278}
279
280/// The default passive sensor set for this machine.
281///
282/// Ordered cheapest-first, so a mirror running under a tight tick budget
283/// degrades by dropping the expensive sensors at the end rather than by
284/// randomly missing whichever ones ran late.
285pub fn default_sensors() -> Vec<Box<dyn Sensor>> {
286    // Windows exposes a different, smaller set through entirely different
287    // interfaces. See `linux_sensors` for the set that reads sysfs and procfs,
288    // which a test with a synthetic tree wants by name rather than by default.
289    #[cfg(target_os = "windows")]
290    {
291        return windows::sensors();
292    }
293    #[allow(unreachable_code)]
294    linux_sensors()
295}
296
297/// The sysfs and procfs sensor set.
298///
299/// Named separately from [`default_sensors`] because it is meaningful on any
300/// platform: the sensors read files by path, so a test can point them at a
301/// synthetic tree and exercise them anywhere. What varies by platform is which
302/// set is the *default*, not which set can be constructed.
303pub fn linux_sensors() -> Vec<Box<dyn Sensor>> {
304    vec![
305        Box::new(frequency::FrequencySensor::new()),
306        Box::new(idle::IdleSensor::new()),
307        Box::new(thermal::ThermalSensor::new()),
308        Box::new(power::PowerSensor::new()),
309        Box::new(scheduler::SchedulerSensor::new()),
310        Box::new(interrupts::InterruptSensor::new()),
311        Box::new(counters::CounterSensor::new()),
312    ]
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn no_default_sensor_claims_to_be_material() {
321        // The architectural invariant, asserted rather than trusted.
322        for sensor in default_sensors() {
323            let d = sensor.descriptor();
324            assert!(
325                d.perturbation.is_passive(),
326                "sensor `{}` declares Material perturbation and is an experiment",
327                d.key
328            );
329        }
330    }
331
332    #[test]
333    fn every_sensor_documents_its_physical_basis() {
334        for sensor in default_sensors() {
335            let d = sensor.descriptor();
336            assert!(
337                !d.physical_fact.is_empty(),
338                "{} has no physical_fact",
339                d.key
340            );
341            assert!(!d.source.is_empty(), "{} has no source", d.key);
342            assert!(
343                !d.uncertainty.basis.is_empty(),
344                "{} has no uncertainty basis",
345                d.key
346            );
347            assert!(
348                d.max_rate_hz > 0.0,
349                "{} declares no sampling ceiling",
350                d.key
351            );
352        }
353    }
354
355    #[test]
356    fn sensor_ids_and_keys_are_unique() {
357        let sensors = default_sensors();
358        let mut ids: Vec<u16> = sensors.iter().map(|s| s.descriptor().id.0).collect();
359        let mut keys: Vec<&str> = sensors.iter().map(|s| s.descriptor().key).collect();
360        ids.sort_unstable();
361        keys.sort_unstable();
362        let unique_ids = {
363            let mut v = ids.clone();
364            v.dedup();
365            v.len()
366        };
367        let unique_keys = {
368            let mut v = keys.clone();
369            v.dedup();
370            v.len()
371        };
372        assert_eq!(unique_ids, ids.len(), "duplicate sensor id");
373        assert_eq!(unique_keys, keys.len(), "duplicate sensor key");
374    }
375
376    #[test]
377    fn writing_a_value_marks_it_observed_and_missing_records_a_reason() {
378        let mut matrix = StateMatrix::new(2, 2);
379        let mut availability = AvailabilityMatrix::new(2, 2);
380        {
381            let mut writer =
382                StateWriter::new(&mut matrix, AvailabilityWriter::new(&mut availability));
383            writer.set(0, ChannelId(0), 5.0);
384            writer.missing(1, ChannelId(1), Availability::PermissionDenied);
385            // Out of range must not panic.
386            writer.set(99, ChannelId(0), 1.0);
387            writer.missing(0, ChannelId(99), Availability::Unknown);
388        }
389        assert_eq!(matrix.get(0, 0), 5.0);
390        assert_eq!(availability.get(0, 0), Availability::Observed);
391        assert!(matrix.get(1, 1).is_nan());
392        assert_eq!(availability.get(1, 1), Availability::PermissionDenied);
393        assert_eq!(matrix.observed_cells(), 1);
394    }
395}