use core::time::Duration;
use crate::model::{
MetricState, PressureId, PressureSignal, PressureSnapshot, PressureState, SystemSnapshot,
UnavailableReason,
};
use super::{Hysteresis, SignalReading, Thresholds, signals};
const DISABLED_RULE: &str = "diagnostics are disabled in configuration";
const RECENT_INTERVALS: usize = 9;
#[derive(Clone, Debug)]
pub struct PressureEngine {
thresholds: Thresholds,
trackers: Vec<Hysteresis>,
recent_intervals: [Duration; RECENT_INTERVALS],
recent_len: usize,
recent_next: usize,
observations: u64,
discontinuities: u64,
}
impl PressureEngine {
#[must_use]
pub fn new(thresholds: Thresholds) -> Self {
let thresholds = thresholds.sanitized();
Self {
trackers: PressureId::DISPLAY_ORDER
.iter()
.map(|_| Hysteresis::new(&thresholds))
.collect(),
thresholds,
recent_intervals: [Duration::ZERO; RECENT_INTERVALS],
recent_len: 0,
recent_next: 0,
observations: 0,
discontinuities: 0,
}
}
#[must_use]
pub const fn thresholds(&self) -> &Thresholds {
&self.thresholds
}
pub fn set_thresholds(&mut self, thresholds: Thresholds) {
self.thresholds = thresholds.sanitized();
self.trackers = PressureId::DISPLAY_ORDER
.iter()
.map(|_| Hysteresis::new(&self.thresholds))
.collect();
self.observations = 0;
}
pub fn reset(&mut self) {
for tracker in &mut self.trackers {
tracker.reset();
}
self.observations = 0;
}
#[must_use]
pub const fn observations(&self) -> u64 {
self.observations
}
#[must_use]
pub const fn discontinuities(&self) -> u64 {
self.discontinuities
}
#[must_use]
pub fn observe(&mut self, snapshot: &SystemSnapshot) -> PressureSnapshot {
if !self.thresholds.enabled {
return PressureSnapshot {
signals: PressureId::DISPLAY_ORDER
.iter()
.map(|&id| PressureSignal::unsupported(id, DISABLED_RULE))
.collect(),
psi: snapshot.pressure.psi,
};
}
if !snapshot.has_valid_interval() {
return self.warming_up_snapshot(snapshot);
}
if self.is_discontinuity(snapshot.elapsed) {
self.discontinuities = self.discontinuities.saturating_add(1);
self.reset();
}
self.record_interval(snapshot.elapsed);
self.observations = self.observations.saturating_add(1);
let signals = PressureId::DISPLAY_ORDER
.iter()
.map(|&id| self.signal(id, snapshot))
.collect();
PressureSnapshot {
signals,
psi: snapshot.pressure.psi,
}
}
fn is_discontinuity(&self, elapsed: Duration) -> bool {
let Some(reference) = self.reference_interval() else {
return false;
};
let limit =
Thresholds::intervals_as_seconds(reference, self.thresholds.discontinuity_intervals);
elapsed.as_secs_f64() > limit
}
fn reference_interval(&self) -> Option<Duration> {
if self.recent_len < 2 {
return None;
}
let mut window = self.recent_intervals;
let filled = window.get_mut(..self.recent_len)?;
filled.sort_unstable();
filled.get(self.recent_len / 2).copied()
}
fn record_interval(&mut self, elapsed: Duration) {
if let Some(slot) = self.recent_intervals.get_mut(self.recent_next) {
*slot = elapsed;
}
self.recent_next = self.recent_next.saturating_add(1) % RECENT_INTERVALS;
self.recent_len = self.recent_len.saturating_add(1).min(RECENT_INTERVALS);
}
fn signal(&mut self, id: PressureId, snapshot: &SystemSnapshot) -> PressureSignal {
let reading = signals::read(id, snapshot, &self.thresholds);
let slot = Self::slot(id);
let Some(tracker) = self.trackers.get_mut(slot) else {
return Self::signal_from(id, &reading, MetricState::WarmingUp, None);
};
let Some(&candidate) = reading.state.fresh() else {
tracker.reset();
return Self::signal_from(id, &reading, reading.state, None);
};
let state = tracker.observe(candidate, snapshot.elapsed);
let held_for = tracker.held_for();
Self::signal_from(id, &reading, state, held_for)
}
fn signal_from(
id: PressureId,
reading: &SignalReading,
state: MetricState<PressureState>,
held_for: Option<Duration>,
) -> PressureSignal {
let severity = match state {
MetricState::Available(_) => reading.severity,
MetricState::WarmingUp => MetricState::WarmingUp,
MetricState::PermissionDenied => MetricState::PermissionDenied,
MetricState::Unsupported => MetricState::Unsupported,
MetricState::TemporarilyUnavailable(reason) => {
MetricState::TemporarilyUnavailable(reason)
}
MetricState::Stale { .. } => {
MetricState::TemporarilyUnavailable(UnavailableReason::NeedsSecondSample)
}
};
PressureSignal {
id,
state,
severity,
raw: reading.raw,
rule: reading.rule,
held_for,
}
}
fn warming_up_snapshot(&self, snapshot: &SystemSnapshot) -> PressureSnapshot {
PressureSnapshot {
signals: PressureId::DISPLAY_ORDER
.iter()
.map(|&id| {
let reading = signals::read(id, snapshot, &self.thresholds);
let state = match reading.state {
MetricState::Available(_) | MetricState::Stale { .. } => {
MetricState::WarmingUp
}
other => other,
};
Self::signal_from(id, &reading, state, None)
})
.collect(),
psi: snapshot.pressure.psi,
}
}
fn slot(id: PressureId) -> usize {
PressureId::DISPLAY_ORDER
.iter()
.position(|candidate| *candidate == id)
.unwrap_or(0)
}
}
impl Default for PressureEngine {
fn default() -> Self {
Self::new(Thresholds::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::fixtures::{
Timeline, set_cpu, set_disk_busy, set_memory, set_psi, snapshot,
};
use crate::model::UnavailableReason;
const TOTAL: u64 = 32 * 1024 * 1024 * 1024;
fn engine() -> PressureEngine {
PressureEngine::default()
}
fn cpu_state(radar: &PressureSnapshot) -> MetricState<PressureState> {
radar
.signal(PressureId::Cpu)
.map_or(MetricState::Unsupported, |signal| signal.state)
}
fn feed_cpu(engine: &mut PressureEngine, busy: f32, count: usize) -> PressureSnapshot {
let mut timeline = Timeline::new(Duration::from_secs(1));
let mut radar = PressureSnapshot::warming_up();
for _ in 0..=count {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, busy));
radar = engine.observe(&snapshot);
}
radar
}
#[test]
fn a_forced_refresh_does_not_make_every_later_sample_a_discontinuity() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..4 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let _ = engine.observe(&snapshot);
}
let mut forced = timeline.build(|snapshot| set_cpu(snapshot, 99.0));
forced.elapsed = Duration::from_millis(3);
assert!(timeline.record(&forced));
let _ = engine.observe(&forced);
let after_forced = engine.discontinuities();
for _ in 0..20 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let _ = engine.observe(&snapshot);
}
assert_eq!(
engine.discontinuities(),
after_forced,
"a 1s sample after a 3ms one is not a sleep/wake gap"
);
let radar = engine.observe(&timeline.push(|snapshot| set_cpu(snapshot, 99.0)));
let signal = radar.signal(PressureId::Cpu).expect("cpu signal exists");
assert_eq!(
signal.state,
MetricState::Available(PressureState::Critical),
"the radar must still be able to commit a state, got {:?}",
signal.state
);
}
#[test]
fn a_gap_far_larger_than_the_recent_cadence_is_still_a_discontinuity() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..12 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let _ = engine.observe(&snapshot);
}
let before = engine.discontinuities();
let mut resumed = timeline.build(|snapshot| set_cpu(snapshot, 99.0));
resumed.elapsed = Duration::from_secs(60);
assert!(timeline.record(&resumed));
let radar = engine.observe(&resumed);
assert_eq!(engine.discontinuities(), before + 1);
let signal = radar.signal(PressureId::Cpu).expect("cpu signal exists");
assert!(
signal.state.is_warming_up(),
"§11.3: the window either side of a gap must not be counted together, \
got {:?}",
signal.state
);
}
#[test]
fn the_radar_always_contains_every_signal_in_display_order() {
let radar = engine().observe(&snapshot());
assert_eq!(radar.signals.len(), PressureId::DISPLAY_ORDER.len());
for (signal, expected) in radar.signals.iter().zip(PressureId::DISPLAY_ORDER) {
assert_eq!(signal.id, expected);
assert!(!signal.rule.is_empty(), "§2.3 requires the rule text");
}
}
#[test]
fn the_first_snapshot_produces_no_state_at_all() {
let mut engine = engine();
let radar = engine.observe(&snapshot());
assert!(
radar.worst_state().is_warming_up(),
"an unmeasured system must not read as healthy"
);
assert_eq!(engine.observations(), 0, "a zero interval is not a sample");
}
#[test]
fn a_signal_warms_up_until_the_minimum_sample_count_is_reached() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for index in 0..10 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let radar = engine.observe(&snapshot);
let state = cpu_state(&radar);
if index == 0 {
assert!(state.is_warming_up(), "the first sample has no interval");
continue;
}
assert!(
state.is_warming_up(),
"sample {index} must not support a sustained claim"
);
}
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
assert_eq!(
cpu_state(&engine.observe(&snapshot)),
MetricState::Available(PressureState::Critical)
);
}
#[test]
fn a_sustained_condition_escalates_and_reports_how_long_it_has_held() {
let mut engine = engine();
let radar = feed_cpu(&mut engine, 99.0, 20);
let signal = radar.signal(PressureId::Cpu).expect("cpu signal exists");
assert_eq!(
signal.state,
MetricState::Available(PressureState::Critical)
);
assert_eq!(signal.symbol(), 'X', "§2.3's redundant cue");
assert!(signal.severity.fresh().is_some());
assert!(signal.raw.is_some(), "§2.3 requires the raw metric");
assert!(
signal
.held_for
.is_some_and(|held| held >= Duration::from_secs(10)),
"held_for {:?}",
signal.held_for
);
}
#[test]
fn an_alternating_metric_does_not_flap_the_radar() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
let mut states = Vec::new();
for index in 0..60 {
let busy = if index % 2 == 0 { 99.0 } else { 1.0 };
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, busy));
states.push(cpu_state(&engine.observe(&snapshot)));
}
let derived: Vec<PressureState> = states
.iter()
.filter_map(|state| state.fresh().copied())
.collect();
assert!(!derived.is_empty(), "the signal must settle eventually");
assert!(
derived.iter().all(|state| *state == PressureState::Normal),
"the radar flapped: {derived:?}"
);
}
#[test]
fn an_unavailable_input_leaves_the_signal_unavailable_rather_than_normal() {
let mut engine = engine();
feed_cpu(&mut engine, 99.0, 20);
let mut timeline = Timeline::new(Duration::from_secs(1));
let snapshot = timeline.push(|snapshot| {
snapshot.cpu.total = MetricState::PermissionDenied;
});
let radar = engine.observe(&snapshot);
assert_eq!(cpu_state(&radar), MetricState::PermissionDenied);
let signal = radar.signal(PressureId::Cpu).expect("cpu signal exists");
assert_eq!(signal.symbol(), '!');
assert!(signal.held_for.is_none());
}
#[test]
fn a_counter_reset_clears_the_window_instead_of_counting_as_an_event() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..10 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let _ = engine.observe(&snapshot);
}
let reset = timeline.push(|snapshot| {
snapshot.cpu.total =
MetricState::TemporarilyUnavailable(UnavailableReason::CounterReset);
});
assert_eq!(
cpu_state(&engine.observe(&reset)),
MetricState::TemporarilyUnavailable(UnavailableReason::CounterReset)
);
let after = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
assert!(
cpu_state(&engine.observe(&after)).is_warming_up(),
"§11.3: a reset must not be readable as an event"
);
}
#[test]
fn a_sleep_wake_gap_resets_every_signal() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..15 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let _ = engine.observe(&snapshot);
}
assert_eq!(
cpu_state(&engine.observe(&timeline.push(|s| set_cpu(s, 99.0)))),
MetricState::Available(PressureState::Critical)
);
let mut woken = timeline.build(|snapshot| set_cpu(snapshot, 99.0));
woken.elapsed = Duration::from_secs(7_200);
let radar = engine.observe(&woken);
assert!(
cpu_state(&radar).is_warming_up(),
"the gap must not be read as fifteen saturated samples"
);
assert_eq!(engine.discontinuities(), 1);
}
#[test]
fn a_shorter_than_usual_interval_is_not_a_discontinuity() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..12 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let _ = engine.observe(&snapshot);
}
let mut jittered = timeline.build(|snapshot| set_cpu(snapshot, 99.0));
jittered.elapsed = Duration::from_millis(600);
let _ = engine.observe(&jittered);
assert_eq!(engine.discontinuities(), 0);
}
#[test]
fn the_reference_interval_cannot_be_inflated_by_a_stall() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..3 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 10.0));
let _ = engine.observe(&snapshot);
}
let mut stalled = timeline.build(|snapshot| set_cpu(snapshot, 10.0));
stalled.elapsed = Duration::from_secs(5);
let _ = engine.observe(&stalled);
assert_eq!(engine.discontinuities(), 0);
let mut gap = timeline.build(|snapshot| set_cpu(snapshot, 10.0));
gap.elapsed = Duration::from_secs(30);
let _ = engine.observe(&gap);
assert_eq!(engine.discontinuities(), 1);
}
#[test]
fn raw_psi_is_carried_through_untouched() {
let mut engine = engine();
let mut snapshot = snapshot();
set_psi(&mut snapshot, 1.0, 2.0, 3.0);
let radar = engine.observe(&snapshot);
assert_eq!(
radar.psi, snapshot.pressure.psi,
"psi is the collector's measurement, not the engine's"
);
}
#[test]
fn signals_the_platform_cannot_measure_stay_unsupported_forever() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..20 {
let snapshot = timeline.push(|snapshot| {
set_cpu(snapshot, 10.0);
snapshot.pressure.psi = MetricState::Unsupported;
});
let radar = engine.observe(&snapshot);
for id in [PressureId::PsiCpu, PressureId::PsiMemory, PressureId::PsiIo] {
let signal = radar.signal(id).expect("signal exists");
assert!(
signal.state.is_unsupported(),
"{id:?} became {:?} without PSI data",
signal.state
);
assert_eq!(signal.symbol(), '-');
}
}
}
#[test]
fn a_metric_the_collector_has_not_reported_yet_stays_warming_up() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..20 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 10.0));
let radar = engine.observe(&snapshot);
let signal = radar.signal(PressureId::PsiMemory).expect("signal exists");
assert!(signal.state.is_warming_up(), "{:?}", signal.state);
assert!(signal.state.fresh().is_none());
}
}
#[test]
fn independent_signals_do_not_share_hysteresis_state() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..12 {
let snapshot = timeline.push(|snapshot| {
set_cpu(snapshot, 99.0);
set_memory(snapshot, TOTAL, TOTAL / 2);
set_disk_busy(snapshot, "nvme0n1", 5.0);
});
let radar = engine.observe(&snapshot);
let _ = radar;
}
let snapshot = timeline.push(|snapshot| {
set_cpu(snapshot, 99.0);
set_memory(snapshot, TOTAL, TOTAL / 2);
set_disk_busy(snapshot, "nvme0n1", 5.0);
});
let radar = engine.observe(&snapshot);
assert_eq!(
cpu_state(&radar),
MetricState::Available(PressureState::Critical)
);
assert_eq!(
radar.signal(PressureId::Memory).map(|signal| signal.state),
Some(MetricState::Available(PressureState::Normal))
);
assert_eq!(
radar.signal(PressureId::Disk).map(|signal| signal.state),
Some(MetricState::Available(PressureState::Normal))
);
}
#[test]
fn disabling_diagnostics_reports_unsupported_rather_than_healthy() {
let mut engine = PressureEngine::new(Thresholds {
enabled: false,
..Thresholds::default()
});
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..20 {
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let radar = engine.observe(&snapshot);
for signal in &radar.signals {
assert!(signal.state.is_unsupported());
assert_eq!(signal.rule, DISABLED_RULE);
}
assert!(radar.worst_state().is_warming_up());
}
}
#[test]
fn changing_thresholds_restarts_the_evidence_rather_than_reusing_it() {
let mut engine = engine();
feed_cpu(&mut engine, 99.0, 20);
engine.set_thresholds(Thresholds {
cpu_watch_percent: 20.0,
..Thresholds::default()
});
assert_eq!(engine.observations(), 0);
let mut timeline = Timeline::new(Duration::from_secs(1));
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
assert!(
cpu_state(&engine.observe(&snapshot)).is_warming_up(),
"observations made under other thresholds must not be reused"
);
}
#[test]
fn an_explicit_reset_returns_every_signal_to_warming_up() {
let mut engine = engine();
feed_cpu(&mut engine, 99.0, 20);
engine.reset();
let mut timeline = Timeline::new(Duration::from_secs(1));
timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let snapshot = timeline.push(|snapshot| set_cpu(snapshot, 99.0));
let radar = engine.observe(&snapshot);
assert!(cpu_state(&radar).is_warming_up());
assert_eq!(engine.observations(), 1);
}
#[test]
fn the_worst_state_across_the_radar_is_what_the_header_shows() {
let mut engine = engine();
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..12 {
let snapshot = timeline.push(|snapshot| {
set_cpu(snapshot, 10.0);
set_memory(snapshot, TOTAL, TOTAL / 100);
});
let _ = engine.observe(&snapshot);
}
let snapshot = timeline.push(|snapshot| {
set_cpu(snapshot, 10.0);
set_memory(snapshot, TOTAL, TOTAL / 100);
});
let radar = engine.observe(&snapshot);
assert_eq!(
radar.worst_state(),
MetricState::Available(PressureState::Critical),
"one critical signal makes the system critical"
);
}
}