use serde::{Deserialize, Serialize};
#[cfg(target_os = "linux")]
const SYSTEM_PSI_PATH: &str = "/proc/pressure/memory";
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq)]
pub struct PsiMem {
pub some_avg10: f32,
pub full_avg10: f32,
}
#[cfg(target_os = "linux")]
fn parse(content: &str) -> Option<PsiMem> {
let mut some_avg10 = None;
let mut full_avg10 = None;
for line in content.lines() {
let mut it = line.split_whitespace();
let kind = it.next()?;
let target = match kind {
"some" => &mut some_avg10,
"full" => &mut full_avg10,
_ => continue,
};
for tok in it {
if let Some(v) = tok.strip_prefix("avg10=") {
*target = v.parse::<f32>().ok();
break;
}
}
}
Some(PsiMem {
some_avg10: some_avg10?,
full_avg10: full_avg10?,
})
}
#[cfg(target_os = "linux")]
pub fn read_system() -> Option<PsiMem> {
let content = std::fs::read_to_string(SYSTEM_PSI_PATH).ok()?;
parse(&content)
}
#[cfg(target_os = "linux")]
pub fn read_process(pid: usize) -> Option<PsiMem> {
let content = std::fs::read_to_string(format!("/proc/{pid}/pressure/memory")).ok()?;
parse(&content)
}
#[cfg(not(target_os = "linux"))]
pub fn read_system() -> Option<PsiMem> {
None
}
#[cfg(not(target_os = "linux"))]
pub fn read_process(_pid: usize) -> Option<PsiMem> {
None
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PsiCapability {
pub system: bool,
pub per_process: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
pub fn detect(pid: usize) -> PsiCapability {
#[cfg(target_os = "linux")]
{
let system = std::path::Path::new(SYSTEM_PSI_PATH).exists();
let per_process = std::path::Path::new(&format!("/proc/{pid}/pressure/memory")).exists();
let reason = if !system {
Some("kernel does not expose /proc/pressure/memory (PSI disabled?)".to_string())
} else if !per_process {
Some("per-process PSI unavailable (kernel < 5.2 or not in cgroup v2)".to_string())
} else {
None
};
PsiCapability {
system,
per_process,
reason,
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = pid;
PsiCapability {
system: false,
per_process: false,
reason: Some("PSI is Linux-only".to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_canonical_format() {
let s = "some avg10=1.23 avg60=4.56 avg300=7.89 total=42\n\
full avg10=0.10 avg60=0.20 avg300=0.30 total=7\n";
#[cfg(target_os = "linux")]
{
let p = parse(s).unwrap();
assert!((p.some_avg10 - 1.23).abs() < 1e-4);
assert!((p.full_avg10 - 0.10).abs() < 1e-4);
}
let _ = s;
}
#[test]
fn rejects_garbage() {
#[cfg(target_os = "linux")]
{
assert!(parse("not a psi file").is_none());
}
}
#[cfg(target_os = "linux")]
#[test]
fn parse_missing_full_line_returns_none() {
let s = "some avg10=1.0 avg60=0.0 avg300=0.0 total=0\n";
assert!(parse(s).is_none());
}
#[cfg(target_os = "linux")]
#[test]
fn parse_missing_avg10_token_returns_none() {
let s = "some avg60=1.0 avg300=0.0 total=0\n\
full avg60=0.5 avg300=0.0 total=0\n";
assert!(parse(s).is_none());
}
#[cfg(target_os = "linux")]
#[test]
fn parse_skips_unknown_kind() {
let s = "weird avg10=9.0\n\
some avg10=0.5 avg60=0.0 avg300=0.0 total=0\n\
full avg10=0.1 avg60=0.0 avg300=0.0 total=0\n";
let p = parse(s).unwrap();
assert!((p.some_avg10 - 0.5).abs() < 1e-4);
assert!((p.full_avg10 - 0.1).abs() < 1e-4);
}
#[test]
fn detect_does_not_panic() {
let cap = detect(std::process::id() as usize);
if !cap.system {
assert!(cap.reason.is_some());
}
}
#[cfg(target_os = "linux")]
#[test]
fn read_process_for_nonexistent_pid_returns_none() {
assert!(read_process(0).is_none());
}
}