#![allow(clippy::uninlined_format_args)]
#![allow(clippy::unnecessary_wraps)]
use std::fs;
use std::time::Duration;
use super::{Analyzer, AnalyzerError};
#[derive(Debug, Clone, Copy, Default)]
pub struct PsiAverages {
pub avg10: f64,
pub avg60: f64,
pub avg300: f64,
pub total_us: u64,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PsiResource {
pub some: PsiAverages,
pub full: Option<PsiAverages>,
}
#[derive(Debug, Clone, Default)]
pub struct PsiData {
pub cpu: PsiResource,
pub memory: PsiResource,
pub io: PsiResource,
pub available: bool,
}
impl PsiData {
pub fn is_under_pressure(&self) -> bool {
self.cpu.some.avg10 > 5.0 || self.memory.some.avg10 > 5.0 || self.io.some.avg10 > 5.0
}
pub fn highest_pressure(&self) -> (&'static str, f64) {
let cpu = self.cpu.some.avg10;
let mem = self.memory.some.avg10;
let io = self.io.some.avg10;
if cpu >= mem && cpu >= io {
("cpu", cpu)
} else if mem >= io {
("memory", mem)
} else {
("io", io)
}
}
}
pub struct PsiAnalyzer {
data: PsiData,
interval: Duration,
}
impl Default for PsiAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl PsiAnalyzer {
pub fn new() -> Self {
Self {
data: PsiData::default(),
interval: Duration::from_secs(1),
}
}
pub fn data(&self) -> &PsiData {
&self.data
}
fn parse_psi_file(path: &str) -> Result<PsiResource, AnalyzerError> {
let contents = fs::read_to_string(path)
.map_err(|e| AnalyzerError::IoError(format!("Failed to read {}: {}", path, e)))?;
let mut resource = PsiResource::default();
for line in contents.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let metric_type = parts[0];
let avgs = Self::parse_averages(&parts[1..])?;
match metric_type {
"some" => resource.some = avgs,
"full" => resource.full = Some(avgs),
_ => {}
}
}
Ok(resource)
}
fn parse_averages(parts: &[&str]) -> Result<PsiAverages, AnalyzerError> {
let mut avgs = PsiAverages::default();
for part in parts {
if let Some((key, value)) = part.split_once('=') {
match key {
"avg10" => avgs.avg10 = value.parse().unwrap_or(0.0),
"avg60" => avgs.avg60 = value.parse().unwrap_or(0.0),
"avg300" => avgs.avg300 = value.parse().unwrap_or(0.0),
"total" => avgs.total_us = value.parse().unwrap_or(0),
_ => {}
}
}
}
Ok(avgs)
}
}
impl Analyzer for PsiAnalyzer {
fn name(&self) -> &'static str {
"psi"
}
fn collect(&mut self) -> Result<(), AnalyzerError> {
if !std::path::Path::new("/proc/pressure/cpu").exists() {
self.data.available = false;
return Ok(());
}
self.data.available = true;
if let Ok(cpu) = Self::parse_psi_file("/proc/pressure/cpu") {
self.data.cpu = cpu;
self.data.cpu.full = None;
}
if let Ok(memory) = Self::parse_psi_file("/proc/pressure/memory") {
self.data.memory = memory;
}
if let Ok(io) = Self::parse_psi_file("/proc/pressure/io") {
self.data.io = io;
}
Ok(())
}
fn interval(&self) -> Duration {
self.interval
}
fn available(&self) -> bool {
std::path::Path::new("/proc/pressure/cpu").exists()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_averages() {
let parts = [
"avg10=3.89",
"avg60=0.87",
"avg300=0.61",
"total=3052778300",
];
let avgs = PsiAnalyzer::parse_averages(&parts).unwrap();
assert!((avgs.avg10 - 3.89).abs() < 0.01);
assert!((avgs.avg60 - 0.87).abs() < 0.01);
assert!((avgs.avg300 - 0.61).abs() < 0.01);
assert_eq!(avgs.total_us, 3052778300);
}
#[test]
fn test_psi_data_pressure_check() {
let mut data = PsiData::default();
assert!(!data.is_under_pressure());
data.cpu.some.avg10 = 10.0;
assert!(data.is_under_pressure());
let (resource, value) = data.highest_pressure();
assert_eq!(resource, "cpu");
assert!((value - 10.0).abs() < 0.01);
}
#[test]
fn test_analyzer_available() {
let analyzer = PsiAnalyzer::new();
let available = analyzer.available();
let _ = available;
}
#[test]
fn test_analyzer_collect() {
let mut analyzer = PsiAnalyzer::new();
let result = analyzer.collect();
assert!(result.is_ok());
if analyzer.data().available {
assert!(analyzer.data().cpu.full.is_none());
assert!(analyzer.data().memory.full.is_some());
assert!(analyzer.data().io.full.is_some());
}
}
#[test]
fn test_psi_analyzer_default() {
let analyzer = PsiAnalyzer::default();
assert_eq!(analyzer.name(), "psi");
assert_eq!(analyzer.interval(), Duration::from_secs(1));
}
#[test]
fn test_psi_data_default() {
let data = PsiData::default();
assert!(!data.available);
assert!(!data.is_under_pressure());
}
#[test]
fn test_psi_averages_default() {
let avgs = PsiAverages::default();
assert_eq!(avgs.avg10, 0.0);
assert_eq!(avgs.avg60, 0.0);
assert_eq!(avgs.avg300, 0.0);
assert_eq!(avgs.total_us, 0);
}
#[test]
fn test_psi_resource_default() {
let resource = PsiResource::default();
assert_eq!(resource.some.avg10, 0.0);
assert!(resource.full.is_none());
}
#[test]
fn test_highest_pressure_memory() {
let mut data = PsiData::default();
data.memory.some.avg10 = 20.0;
data.cpu.some.avg10 = 5.0;
data.io.some.avg10 = 10.0;
let (resource, value) = data.highest_pressure();
assert_eq!(resource, "memory");
assert!((value - 20.0).abs() < 0.01);
}
#[test]
fn test_highest_pressure_io() {
let mut data = PsiData::default();
data.io.some.avg10 = 30.0;
data.cpu.some.avg10 = 5.0;
data.memory.some.avg10 = 10.0;
let (resource, value) = data.highest_pressure();
assert_eq!(resource, "io");
assert!((value - 30.0).abs() < 0.01);
}
#[test]
fn test_highest_pressure_equal() {
let mut data = PsiData::default();
data.cpu.some.avg10 = 10.0;
data.memory.some.avg10 = 10.0;
data.io.some.avg10 = 10.0;
let (resource, _) = data.highest_pressure();
assert_eq!(resource, "cpu");
}
#[test]
fn test_is_under_pressure_thresholds() {
let mut data = PsiData::default();
data.cpu.some.avg10 = 4.9;
assert!(!data.is_under_pressure());
data.cpu.some.avg10 = 5.0;
assert!(!data.is_under_pressure());
data.cpu.some.avg10 = 5.1;
assert!(data.is_under_pressure());
}
#[test]
fn test_parse_averages_partial() {
let parts = ["avg10=1.5"];
let avgs = PsiAnalyzer::parse_averages(&parts).unwrap();
assert!((avgs.avg10 - 1.5).abs() < 0.01);
assert_eq!(avgs.avg60, 0.0);
assert_eq!(avgs.avg300, 0.0);
}
#[test]
fn test_parse_averages_invalid() {
let parts = ["avg10=invalid", "avg60=2.5"];
let avgs = PsiAnalyzer::parse_averages(&parts).unwrap();
assert_eq!(avgs.avg10, 0.0); assert!((avgs.avg60 - 2.5).abs() < 0.01);
}
#[test]
fn test_parse_averages_unknown_key() {
let parts = ["unknown=123", "avg10=5.0"];
let avgs = PsiAnalyzer::parse_averages(&parts).unwrap();
assert!((avgs.avg10 - 5.0).abs() < 0.01);
}
#[test]
fn test_psi_data_clone() {
let mut data = PsiData::default();
data.cpu.some.avg10 = 15.0;
data.available = true;
let cloned = data.clone();
assert_eq!(cloned.available, data.available);
assert!((cloned.cpu.some.avg10 - 15.0).abs() < 0.01);
}
#[test]
fn test_psi_data_debug() {
let data = PsiData::default();
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("PsiData"));
}
#[test]
fn test_psi_averages_copy() {
let avgs = PsiAverages {
avg10: 1.0,
avg60: 2.0,
avg300: 3.0,
total_us: 1000,
};
let copied = avgs; assert_eq!(copied.avg10, 1.0);
assert_eq!(copied.total_us, 1000);
}
}