use core::time::Duration;
use crate::model::{MeasuredValue, Measurement, PressureId, PsiResource, Severity, SystemSnapshot};
use super::super::{
DiagnosticRule, Evidence, Finding, HistoryWindow, Thresholds, TimeWindow, signals,
};
use super::{SUSTAINED_CONFIDENCE, as_percent, escalate, ratio};
pub const PSI_MEMORY_ELEVATED: &str = "psi.memory_elevated";
pub const PSI_IO_ELEVATED: &str = "psi.io_elevated";
const AVG10: Duration = Duration::from_secs(10);
const AVG60: Duration = Duration::from_secs(60);
const AVG300: Duration = Duration::from_secs(300);
fn evaluate_psi(
rule_id: &'static str,
id: PressureId,
title: &'static str,
waiting_for: &'static str,
thresholds: &Thresholds,
current: &SystemSnapshot,
) -> Option<Finding> {
let psi = current.pressure.psi.fresh()?;
let resource: &PsiResource = signals::psi_resource(psi, id);
let some10 = f64::from(resource.some_avg10.value());
let severity = escalate(
some10 >= f64::from(thresholds.psi_watch_percent),
some10 >= f64::from(thresholds.psi_critical_percent),
)?;
let threshold = if severity == Severity::Critical {
thresholds.psi_critical_percent
} else {
thresholds.psi_watch_percent
};
let mut evidence = vec![
Evidence::new(
Measurement::new("some avg10", MeasuredValue::Percent(resource.some_avg10)),
TimeWindow::moving_average(AVG10),
),
Evidence::new(
Measurement::new("some avg60", MeasuredValue::Percent(resource.some_avg60)),
TimeWindow::moving_average(AVG60),
),
Evidence::new(
Measurement::new("some avg300", MeasuredValue::Percent(resource.some_avg300)),
TimeWindow::moving_average(AVG300),
),
Evidence::current(Measurement::new(
"threshold",
MeasuredValue::Percent(as_percent(threshold)),
)),
Evidence::current(Measurement::new(
"total stalled",
MeasuredValue::Duration(resource.total_stalled),
)),
];
if let Some(full) = resource.full_avg10.fresh() {
evidence.push(Evidence::new(
Measurement::new("full avg10", MeasuredValue::Percent(*full)),
TimeWindow::moving_average(AVG10),
));
}
let multiple = ratio(some10, f64::from(threshold))
.map_or_else(String::new, |value| format!(" ({value:.1}x the threshold)"));
let summary = format!(
"The kernel reports at least one task stalled {waiting_for} for {} of the last 10 seconds{multiple}, \
and {} of the last 60. A pressure share is stalled time, not utilization.",
resource.some_avg10, resource.some_avg60,
);
Some(
Finding::new(rule_id, severity, title, summary, SUSTAINED_CONFIDENCE)
.with_evidence(evidence),
)
}
#[derive(Clone, Copy, Debug)]
pub struct MemoryPsiElevatedRule {
thresholds: Thresholds,
}
impl MemoryPsiElevatedRule {
#[must_use]
pub const fn new(thresholds: Thresholds) -> Self {
Self { thresholds }
}
}
impl DiagnosticRule for MemoryPsiElevatedRule {
fn id(&self) -> &'static str {
PSI_MEMORY_ELEVATED
}
fn evaluate(&self, current: &SystemSnapshot, _history: &HistoryWindow<'_>) -> Option<Finding> {
evaluate_psi(
PSI_MEMORY_ELEVATED,
PressureId::PsiMemory,
"Linux memory pressure stalls elevated",
"on memory reclaim",
&self.thresholds,
current,
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct IoPsiElevatedRule {
thresholds: Thresholds,
}
impl IoPsiElevatedRule {
#[must_use]
pub const fn new(thresholds: Thresholds) -> Self {
Self { thresholds }
}
}
impl DiagnosticRule for IoPsiElevatedRule {
fn id(&self) -> &'static str {
PSI_IO_ELEVATED
}
fn evaluate(&self, current: &SystemSnapshot, _history: &HistoryWindow<'_>) -> Option<Finding> {
evaluate_psi(
PSI_IO_ELEVATED,
PressureId::PsiIo,
"Linux I/O pressure stalls elevated",
"on block i/o",
&self.thresholds,
current,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::fixtures::{Timeline, set_psi, snapshot};
use crate::model::{Confidence, MetricState};
fn memory_rule() -> MemoryPsiElevatedRule {
MemoryPsiElevatedRule::new(Thresholds::default().sanitized())
}
fn io_rule() -> IoPsiElevatedRule {
IoPsiElevatedRule::new(Thresholds::default().sanitized())
}
#[test]
fn no_psi_data_produces_no_finding_on_either_rule() {
let timeline = Timeline::new(Duration::from_secs(1));
let window = timeline.window();
let snapshot = snapshot();
assert!(snapshot.pressure.psi.fresh().is_none());
assert!(memory_rule().evaluate(&snapshot, &window).is_none());
assert!(io_rule().evaluate(&snapshot, &window).is_none());
}
#[test]
fn quiet_psi_produces_no_finding() {
let timeline = Timeline::new(Duration::from_secs(1));
let mut snapshot = snapshot();
set_psi(&mut snapshot, 30.0, 0.4, 0.9);
assert!(
memory_rule()
.evaluate(&snapshot, &timeline.window())
.is_none()
);
assert!(io_rule().evaluate(&snapshot, &timeline.window()).is_none());
}
#[test]
fn each_rule_reads_only_its_own_resource() {
let timeline = Timeline::new(Duration::from_secs(1));
let mut snapshot = snapshot();
set_psi(&mut snapshot, 90.0, 12.0, 0.0);
let memory = memory_rule()
.evaluate(&snapshot, &timeline.window())
.expect("memory psi is elevated");
assert_eq!(memory.rule_id, PSI_MEMORY_ELEVATED);
assert_eq!(memory.severity, Severity::Watch);
assert!(
io_rule().evaluate(&snapshot, &timeline.window()).is_none(),
"an idle i/o resource must not inherit the memory reading"
);
}
#[test]
fn elevated_io_psi_escalates_to_critical() {
let timeline = Timeline::new(Duration::from_secs(1));
let mut snapshot = snapshot();
set_psi(&mut snapshot, 0.0, 0.0, 62.0);
let finding = io_rule()
.evaluate(&snapshot, &timeline.window())
.expect("62% stalled is critical");
assert_eq!(finding.severity, Severity::Critical);
assert_eq!(finding.title, "Linux I/O pressure stalls elevated");
}
#[test]
fn the_evidence_window_is_the_span_the_average_covers() {
let timeline = Timeline::new(Duration::from_secs(1));
let mut snapshot = snapshot();
set_psi(&mut snapshot, 0.0, 45.0, 0.0);
let finding = memory_rule()
.evaluate(&snapshot, &timeline.window())
.expect("elevated memory psi");
let avg10 = finding
.evidence
.iter()
.find(|item| item.measurement.label == "some avg10")
.expect("avg10 is evidence");
assert_eq!(avg10.window.span, AVG10);
assert!(
!avg10.window.is_current_sample(),
"one read of avg10 still covers ten seconds"
);
let labels: Vec<&str> = finding
.evidence
.iter()
.map(|item| item.measurement.label)
.collect();
assert!(labels.contains(&"some avg60"), "{labels:?}");
assert!(labels.contains(&"full avg10"), "{labels:?}");
assert!(labels.contains(&"total stalled"), "{labels:?}");
}
#[test]
fn a_kernel_without_the_full_figure_still_produces_a_finding() {
let timeline = Timeline::new(Duration::from_secs(1));
let mut snapshot = snapshot();
set_psi(&mut snapshot, 0.0, 45.0, 0.0);
if let MetricState::Available(psi) = &mut snapshot.pressure.psi {
psi.memory.full_avg10 = MetricState::Unsupported;
}
let finding = memory_rule()
.evaluate(&snapshot, &timeline.window())
.expect("some avg10 is enough to fire");
assert!(
!finding
.evidence
.iter()
.any(|item| item.measurement.label == "full avg10")
);
}
#[test]
fn the_summary_says_a_pressure_share_is_not_a_utilization() {
let timeline = Timeline::new(Duration::from_secs(1));
let mut snapshot = snapshot();
set_psi(&mut snapshot, 0.0, 45.0, 0.0);
let finding = memory_rule()
.evaluate(&snapshot, &timeline.window())
.expect("elevated memory psi");
assert!(
finding.summary.contains("not utilization"),
"{}",
finding.summary
);
assert_eq!(finding.confidence, Confidence::Medium);
}
#[test]
fn stale_psi_does_not_produce_a_finding() {
let timeline = Timeline::new(Duration::from_secs(1));
let mut snapshot = snapshot();
set_psi(&mut snapshot, 0.0, 99.0, 0.0);
snapshot.pressure.psi = snapshot.pressure.psi.into_stale(Duration::from_secs(5));
assert!(
memory_rule()
.evaluate(&snapshot, &timeline.window())
.is_none(),
"a retained value is not a current measurement (§4)"
);
}
}