use core::time::Duration;
use crate::model::{Measurement, MetricState, Severity};
use crate::units::Percent;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum PressureId {
Cpu,
Memory,
Disk,
Network,
Swap,
Load,
PsiCpu,
PsiMemory,
PsiIo,
}
impl PressureId {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Cpu => "CPU",
Self::Memory => "MEM",
Self::Disk => "DISK",
Self::Network => "NET",
Self::Swap => "SWAP",
Self::Load => "LOAD",
Self::PsiCpu => "PSI-CPU",
Self::PsiMemory => "PSI-MEM",
Self::PsiIo => "PSI-IO",
}
}
pub const DISPLAY_ORDER: [Self; 9] = [
Self::Cpu,
Self::Memory,
Self::Disk,
Self::Network,
Self::Swap,
Self::Load,
Self::PsiCpu,
Self::PsiMemory,
Self::PsiIo,
];
}
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum PressureState {
#[default]
Normal,
Watch,
Critical,
}
impl PressureState {
#[must_use]
pub const fn symbol(self) -> char {
match self {
Self::Normal => '.',
Self::Watch => '!',
Self::Critical => 'X',
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Watch => "watch",
Self::Critical => "critical",
}
}
#[must_use]
pub const fn severity(self) -> Severity {
match self {
Self::Normal => Severity::Info,
Self::Watch => Severity::Watch,
Self::Critical => Severity::Critical,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PressureSignal {
pub id: PressureId,
pub state: MetricState<PressureState>,
pub severity: MetricState<Percent>,
pub raw: Option<Measurement>,
pub rule: &'static str,
pub held_for: Option<Duration>,
}
impl PressureSignal {
#[must_use]
pub const fn unsupported(id: PressureId, rule: &'static str) -> Self {
Self {
id,
state: MetricState::Unsupported,
severity: MetricState::Unsupported,
raw: None,
rule,
held_for: None,
}
}
#[must_use]
pub const fn warming_up(id: PressureId, rule: &'static str) -> Self {
Self {
id,
state: MetricState::WarmingUp,
severity: MetricState::WarmingUp,
raw: None,
rule,
held_for: None,
}
}
#[must_use]
pub fn symbol(&self) -> char {
match self.state.displayable() {
Some((state, _)) => state.symbol(),
None => self.state.symbol(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PsiResource {
pub some_avg10: Percent,
pub some_avg60: Percent,
pub some_avg300: Percent,
pub full_avg10: MetricState<Percent>,
pub full_avg60: MetricState<Percent>,
pub full_avg300: MetricState<Percent>,
pub total_stalled: Duration,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PsiSnapshot {
pub cpu: PsiResource,
pub memory: PsiResource,
pub io: PsiResource,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PressureSnapshot {
pub signals: Vec<PressureSignal>,
pub psi: MetricState<PsiSnapshot>,
}
impl PressureSnapshot {
#[must_use]
pub fn signal(&self, id: PressureId) -> Option<&PressureSignal> {
self.signals.iter().find(|signal| signal.id == id)
}
#[must_use]
pub fn worst_state(&self) -> MetricState<PressureState> {
let worst = self
.signals
.iter()
.filter_map(|signal| signal.state.fresh().copied())
.max();
match worst {
Some(state) => MetricState::Available(state),
None => MetricState::WarmingUp,
}
}
#[must_use]
pub fn warming_up() -> Self {
Self {
signals: PressureId::DISPLAY_ORDER
.iter()
.map(|&id| PressureSignal::warming_up(id, "awaiting samples"))
.collect(),
psi: MetricState::WarmingUp,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{MeasuredValue, UnavailableReason};
#[test]
fn state_symbols_are_exactly_the_specified_characters() {
assert_eq!(PressureState::Normal.symbol(), '.');
assert_eq!(PressureState::Watch.symbol(), '!');
assert_eq!(PressureState::Critical.symbol(), 'X');
}
#[test]
fn an_unavailable_signal_shows_a_question_mark_not_normal() {
let signal = PressureSignal {
id: PressureId::Network,
state: MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown),
severity: MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown),
raw: Some(Measurement::new(
"throughput",
MeasuredValue::Count(18_000_000),
)),
rule: "utilization requires a known link speed",
held_for: None,
};
assert_eq!(signal.symbol(), '?');
assert_ne!(signal.symbol(), PressureState::Normal.symbol());
}
#[test]
fn an_unsupported_signal_is_distinguishable_from_a_normal_one() {
let signal = PressureSignal::unsupported(PressureId::PsiIo, "Linux only");
assert_eq!(signal.symbol(), '-');
assert!(signal.state.is_unsupported());
}
#[test]
fn worst_state_ignores_unavailable_signals_rather_than_treating_them_as_healthy() {
let mut snapshot = PressureSnapshot::warming_up();
assert!(snapshot.worst_state().is_warming_up());
if let Some(signal) = snapshot.signals.first_mut() {
signal.state = MetricState::Available(PressureState::Watch);
}
assert_eq!(
snapshot.worst_state(),
MetricState::Available(PressureState::Watch)
);
if let Some(signal) = snapshot.signals.get_mut(1) {
signal.state = MetricState::Available(PressureState::Critical);
}
assert_eq!(
snapshot.worst_state(),
MetricState::Available(PressureState::Critical)
);
}
#[test]
fn warming_up_radar_contains_every_signal_in_display_order() {
let snapshot = PressureSnapshot::warming_up();
assert_eq!(snapshot.signals.len(), PressureId::DISPLAY_ORDER.len());
for (signal, expected) in snapshot.signals.iter().zip(PressureId::DISPLAY_ORDER) {
assert_eq!(signal.id, expected);
}
assert!(snapshot.signal(PressureId::Memory).is_some());
}
#[test]
fn every_signal_carries_the_rule_that_derived_it() {
let snapshot = PressureSnapshot::warming_up();
for signal in &snapshot.signals {
assert!(!signal.rule.is_empty(), "{:?} has no rule text", signal.id);
}
}
#[test]
fn labels_are_short_enough_for_the_radar_column() {
for id in PressureId::DISPLAY_ORDER {
assert!(id.label().len() <= 8, "{id:?} label is too wide");
assert!(id.label().is_ascii());
}
}
}