use super::*;
use std::collections::BTreeMap;
use std::path::Path;
pub(super) fn parse_psi(raw: &str) -> PsiResource {
let mut out = PsiResource::default();
for line in raw.lines() {
let mut tokens = line.split_whitespace();
let Some(prefix) = tokens.next() else {
continue;
};
let half = match prefix {
"some" => &mut out.some,
"full" => &mut out.full,
_ => continue,
};
for tok in tokens {
let Some((key, value)) = tok.split_once('=') else {
continue;
};
match key {
"avg10" => half.avg10 = parse_centi_percent(value),
"avg60" => half.avg60 = parse_centi_percent(value),
"avg300" => half.avg300 = parse_centi_percent(value),
"total" => half.total_usec = value.parse::<u64>().unwrap_or(0),
_ => {}
}
}
}
out
}
pub(super) fn parse_centi_percent(s: &str) -> u16 {
let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
let Ok(int) = int_part.parse::<u32>() else {
return 0;
};
let frac = if frac_part.is_empty() {
0
} else {
let padded: String = frac_part
.chars()
.chain(std::iter::repeat('0'))
.take(2)
.collect();
padded.parse::<u32>().unwrap_or(0)
};
let combined = int.saturating_mul(100).saturating_add(frac);
combined.try_into().unwrap_or(u16::MAX)
}
pub(super) fn read_host_psi_at(proc_root: &Path) -> Psi {
let pressure_dir = proc_root.join("pressure");
Psi {
cpu: read_psi_file_at(&pressure_dir.join("cpu")),
memory: read_psi_file_at(&pressure_dir.join("memory")),
io: read_psi_file_at(&pressure_dir.join("io")),
irq: read_psi_file_at(&pressure_dir.join("irq")),
}
}
pub(super) fn read_sched_ext_sysfs_at(sys_root: &Path) -> Option<SchedExtSysfs> {
let dir = sys_root.join("kernel").join("sched_ext");
if !dir.exists() {
return None;
}
Some(SchedExtSysfs {
state: fs::read_to_string(dir.join("state"))
.map(|s| s.trim().to_string())
.unwrap_or_default(),
switch_all: read_sysfs_u64(&dir.join("switch_all")),
nr_rejected: read_sysfs_u64(&dir.join("nr_rejected")),
hotplug_seq: read_sysfs_u64(&dir.join("hotplug_seq")),
enable_seq: read_sysfs_u64(&dir.join("enable_seq")),
})
}
pub(super) fn read_sysfs_u64(path: &Path) -> u64 {
fs::read_to_string(path)
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.unwrap_or(0)
}
pub(super) fn read_cgroup_psi_at(cgroup_root: &Path, path: &str) -> Psi {
let relative = path.strip_prefix('/').unwrap_or(path);
let dir = if relative.is_empty() {
cgroup_root.to_path_buf()
} else {
cgroup_root.join(relative)
};
Psi {
cpu: read_psi_file_at(&dir.join("cpu.pressure")),
memory: read_psi_file_at(&dir.join("memory.pressure")),
io: read_psi_file_at(&dir.join("io.pressure")),
irq: read_psi_file_at(&dir.join("irq.pressure")),
}
}
pub(super) fn read_psi_file_at(path: &Path) -> PsiResource {
fs::read_to_string(path)
.ok()
.as_deref()
.map(parse_psi)
.unwrap_or_default()
}
impl CtprofSnapshot {
pub fn load(path: &std::path::Path) -> anyhow::Result<Self> {
use anyhow::Context;
let bytes = std::fs::read(path)
.with_context(|| format!("read ctprof snapshot from {}", path.display()))?;
let json = decompress_capped(&bytes, MAX_DECOMPRESSED_SNAPSHOT_BYTES)
.with_context(|| format!("zstd decompress ctprof snapshot {}", path.display()))?;
let snap: CtprofSnapshot = serde_json::from_slice(&json).with_context(|| {
format!(
"parse ctprof snapshot JSON from {} (did the capture format change?)",
path.display(),
)
})?;
Ok(snap)
}
pub fn write(&self, path: &std::path::Path) -> anyhow::Result<()> {
use anyhow::Context;
let json = serde_json::to_vec(self).context("serialize ctprof snapshot to JSON")?;
let compressed =
zstd::encode_all(json.as_slice(), 3).context("zstd compress ctprof snapshot")?;
std::fs::write(path, compressed)
.with_context(|| format!("write ctprof snapshot to {}", path.display()))?;
Ok(())
}
}
pub(super) fn decompress_capped(bytes: &[u8], max_decompressed: u64) -> anyhow::Result<Vec<u8>> {
use std::io::Read;
let decoder = zstd::stream::read::Decoder::new(bytes)?;
let mut out = Vec::new();
decoder
.take(max_decompressed.saturating_add(1))
.read_to_end(&mut out)?;
if out.len() as u64 > max_decompressed {
anyhow::bail!(
"zstd-decompressed payload exceeds the {}-byte cap (decompression-bomb guard)",
max_decompressed,
);
}
Ok(out)
}
#[allow(dead_code)]
pub const SNAPSHOT_EXTENSION: &str = "ctprof.zst";
pub const MAX_DECOMPRESSED_SNAPSHOT_BYTES: u64 = 256 * 1024 * 1024;
pub const DEFAULT_PROC_ROOT: &str = "/proc";
pub const DEFAULT_CGROUP_ROOT: &str = "/sys/fs/cgroup";
pub const DEFAULT_SYS_ROOT: &str = "/sys";
pub(super) fn task_file(proc_root: &Path, tgid: i32, tid: i32, leaf: &str) -> PathBuf {
proc_root
.join(tgid.to_string())
.join("task")
.join(tid.to_string())
.join(leaf)
}
pub(super) fn proc_file(proc_root: &Path, tgid: i32, leaf: &str) -> PathBuf {
proc_root.join(tgid.to_string()).join(leaf)
}
pub(super) fn policy_name(policy: i32) -> String {
match policy {
libc::SCHED_OTHER => "SCHED_OTHER".to_string(),
libc::SCHED_FIFO => "SCHED_FIFO".to_string(),
libc::SCHED_RR => "SCHED_RR".to_string(),
libc::SCHED_BATCH => "SCHED_BATCH".to_string(),
libc::SCHED_IDLE => "SCHED_IDLE".to_string(),
6 => "SCHED_DEADLINE".to_string(),
7 => "SCHED_EXT".to_string(),
other => format!("SCHED_UNKNOWN({other})"),
}
}
pub(super) fn iter_tgids_at(proc_root: &Path) -> Vec<i32> {
let Ok(entries) = fs::read_dir(proc_root) else {
return Vec::new();
};
let mut tgids: Vec<i32> = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().to_str().and_then(|s| s.parse::<i32>().ok()))
.filter(|&p| p > 0)
.collect();
tgids.sort_unstable();
tgids
}
pub(super) fn iter_task_ids_at(proc_root: &Path, tgid: i32) -> Vec<i32> {
let path = proc_root.join(tgid.to_string()).join("task");
let Ok(entries) = fs::read_dir(&path) else {
return Vec::new();
};
let mut tids: Vec<i32> = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().to_str().and_then(|s| s.parse::<i32>().ok()))
.filter(|&t| t > 0)
.collect();
tids.sort_unstable();
tids
}
pub(super) fn read_process_comm_at(proc_root: &Path, tgid: i32) -> Option<String> {
let raw = fs::read_to_string(proc_file(proc_root, tgid, "comm")).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub(super) fn read_thread_comm_at(proc_root: &Path, tgid: i32, tid: i32) -> Option<String> {
let raw = fs::read_to_string(task_file(proc_root, tgid, tid, "comm")).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct StatFields {
pub(super) minflt: Option<u64>,
pub(super) majflt: Option<u64>,
pub(super) utime_clock_ticks: Option<u64>,
pub(super) stime_clock_ticks: Option<u64>,
pub(super) priority: Option<i32>,
pub(super) nice: Option<i32>,
pub(super) start_time_clock_ticks: Option<u64>,
pub(super) processor: Option<i32>,
pub(super) rt_priority: Option<u32>,
pub(super) policy: Option<i32>,
}
pub(super) fn parse_stat(raw: &str) -> StatFields {
let Some(line) = raw.lines().next() else {
return StatFields::default();
};
let Some(last_close) = line.rfind(')') else {
return StatFields::default();
};
let Some(tail) = line.get(last_close + 1..) else {
return StatFields::default();
};
let parts: Vec<&str> = tail.split_ascii_whitespace().collect();
let get_u64 = |idx: usize| parts.get(idx).and_then(|s| s.parse::<u64>().ok());
let get_u32 = |idx: usize| parts.get(idx).and_then(|s| s.parse::<u32>().ok());
let get_i32 = |idx: usize| parts.get(idx).and_then(|s| s.parse::<i32>().ok());
StatFields {
minflt: get_u64(7),
majflt: get_u64(9),
utime_clock_ticks: get_u64(11),
stime_clock_ticks: get_u64(12),
priority: get_i32(15),
nice: get_i32(16),
start_time_clock_ticks: get_u64(19),
processor: get_i32(36),
rt_priority: get_u32(37),
policy: get_i32(38),
}
}
pub(super) fn read_stat_at_with_tally(
proc_root: &Path,
tgid: i32,
tid: i32,
tally: &mut Option<&mut ParseTally>,
) -> StatFields {
match fs::read_to_string(task_file(proc_root, tgid, tid, "stat")) {
Ok(raw) => parse_stat(&raw),
Err(_) => {
if let Some(t) = tally.as_mut() {
t.record_failure("stat");
}
StatFields::default()
}
}
}
pub(super) fn parse_schedstat(raw: &str) -> (Option<u64>, Option<u64>, Option<u64>) {
let Some(line) = raw.lines().next() else {
return (None, None, None);
};
let mut parts = line.split_ascii_whitespace();
let run = parts.next().and_then(|s| s.parse::<u64>().ok());
let wait = parts.next().and_then(|s| s.parse::<u64>().ok());
let slices = parts.next().and_then(|s| s.parse::<u64>().ok());
(run, wait, slices)
}
pub(super) fn read_schedstat_at_with_tally(
proc_root: &Path,
tgid: i32,
tid: i32,
tally: &mut Option<&mut ParseTally>,
) -> (Option<u64>, Option<u64>, Option<u64>) {
match fs::read_to_string(task_file(proc_root, tgid, tid, "schedstat")) {
Ok(raw) => parse_schedstat(&raw),
Err(_) => {
if let Some(t) = tally.as_mut() {
t.record_failure("schedstat");
}
(None, None, None)
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct IoFields {
pub(super) rchar: Option<u64>,
pub(super) wchar: Option<u64>,
pub(super) syscr: Option<u64>,
pub(super) syscw: Option<u64>,
pub(super) read_bytes: Option<u64>,
pub(super) write_bytes: Option<u64>,
pub(super) cancelled_write_bytes: Option<u64>,
}
pub(super) fn parse_io(raw: &str) -> IoFields {
let mut out = IoFields::default();
for line in raw.lines() {
let Some((key, value)) = line.split_once(':') else {
continue;
};
let parsed = value.trim().parse::<u64>().ok();
match key.trim() {
"rchar" => out.rchar = parsed,
"wchar" => out.wchar = parsed,
"syscr" => out.syscr = parsed,
"syscw" => out.syscw = parsed,
"read_bytes" => out.read_bytes = parsed,
"write_bytes" => out.write_bytes = parsed,
"cancelled_write_bytes" => out.cancelled_write_bytes = parsed,
_ => {}
}
}
out
}
pub(super) fn read_io_at_with_tally(
proc_root: &Path,
tgid: i32,
tid: i32,
tally: &mut Option<&mut ParseTally>,
) -> IoFields {
match fs::read_to_string(task_file(proc_root, tgid, tid, "io")) {
Ok(raw) => parse_io(&raw),
Err(_) => {
if let Some(t) = tally.as_mut() {
t.record_failure("io");
}
IoFields::default()
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct StatusFields {
pub(super) voluntary_csw: Option<u64>,
pub(super) nonvoluntary_csw: Option<u64>,
pub(super) state: Option<char>,
pub(super) cpus_allowed: Option<Vec<u32>>,
pub(super) nr_threads: Option<u64>,
}
pub(super) fn parse_status(raw: &str) -> StatusFields {
let mut out = StatusFields::default();
for line in raw.lines() {
let Some((key, value)) = line.split_once(':') else {
continue;
};
let value = value.trim();
match key.trim() {
"State" => {
out.state = value.chars().next();
}
"voluntary_ctxt_switches" => {
out.voluntary_csw = value.parse::<u64>().ok();
}
"nonvoluntary_ctxt_switches" => {
out.nonvoluntary_csw = value.parse::<u64>().ok();
}
"Cpus_allowed_list" => {
out.cpus_allowed = crate::cpu_util::parse_cpu_list(value);
}
"Threads" => {
out.nr_threads = value.parse::<u64>().ok();
}
_ => {}
}
}
out
}
pub(super) fn read_status_at_with_tally(
proc_root: &Path,
tgid: i32,
tid: i32,
tally: &mut Option<&mut ParseTally>,
) -> StatusFields {
match fs::read_to_string(task_file(proc_root, tgid, tid, "status")) {
Ok(raw) => parse_status(&raw),
Err(_) => {
if let Some(t) = tally.as_mut() {
t.record_failure("status");
}
StatusFields::default()
}
}
}
#[cfg(test)]
pub(super) fn read_cgroup_at(proc_root: &Path, tgid: i32, tid: i32) -> Option<String> {
read_cgroup_at_with_tally(proc_root, tgid, tid, &mut None)
}
pub(super) fn read_cgroup_at_with_tally(
proc_root: &Path,
tgid: i32,
tid: i32,
tally: &mut Option<&mut ParseTally>,
) -> Option<String> {
match fs::read_to_string(task_file(proc_root, tgid, tid, "cgroup")) {
Ok(raw) => parse_cgroup_v2(&raw),
Err(_) => {
if let Some(t) = tally.as_mut() {
t.record_failure("cgroup");
}
None
}
}
}
pub(super) fn parse_cgroup_v2(raw: &str) -> Option<String> {
for line in raw.lines() {
if let Some(rest) = line.strip_prefix("0::") {
let trimmed = rest.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
None
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct SchedFields {
pub(super) nr_wakeups: Option<u64>,
pub(super) nr_wakeups_local: Option<u64>,
pub(super) nr_wakeups_remote: Option<u64>,
pub(super) nr_wakeups_sync: Option<u64>,
pub(super) nr_wakeups_migrate: Option<u64>,
pub(super) nr_wakeups_affine: Option<u64>,
pub(super) nr_wakeups_affine_attempts: Option<u64>,
pub(super) nr_migrations: Option<u64>,
pub(super) nr_forced_migrations: Option<u64>,
pub(super) nr_failed_migrations_affine: Option<u64>,
pub(super) nr_failed_migrations_running: Option<u64>,
pub(super) nr_failed_migrations_hot: Option<u64>,
pub(super) wait_sum: Option<u64>,
pub(super) wait_count: Option<u64>,
pub(super) wait_max: Option<u64>,
pub(super) sleep_sum: Option<u64>,
pub(super) sleep_max: Option<u64>,
pub(super) block_sum: Option<u64>,
pub(super) block_max: Option<u64>,
pub(super) iowait_sum: Option<u64>,
pub(super) iowait_count: Option<u64>,
pub(super) exec_max: Option<u64>,
pub(super) slice_max: Option<u64>,
pub(super) core_forceidle_sum: Option<u64>,
pub(super) fair_slice_ns: Option<u64>,
pub(super) ext_enabled: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ParseDottedNs {
Negative,
Malformed,
}
pub(super) fn parsed_ns_from_dotted(value: &str) -> Result<u64, ParseDottedNs> {
if let Some((ms_str, ns_str)) = value.split_once('.') {
let ms_trimmed = ms_str.trim();
if ms_trimmed.starts_with('-') {
return Err(ParseDottedNs::Negative);
}
if ns_str.trim_start().starts_with('-') {
return Err(ParseDottedNs::Negative);
}
let ms = ms_trimmed
.parse::<u64>()
.map_err(|_| ParseDottedNs::Malformed)?;
let ns_part: String = ns_str.chars().take(6).collect();
let padded = format!("{:0<6}", ns_part);
let ns = padded
.parse::<u64>()
.map_err(|_| ParseDottedNs::Malformed)?;
ms.checked_mul(1_000_000)
.and_then(|x| x.checked_add(ns))
.ok_or(ParseDottedNs::Malformed)
} else {
let trimmed = value.trim();
if trimmed.starts_with('-') {
return Err(ParseDottedNs::Negative);
}
trimmed.parse::<u64>().map_err(|_| ParseDottedNs::Malformed)
}
}
pub(super) fn parse_sched(raw: &str, tally: &mut Option<&mut ParseTally>) -> SchedFields {
let mut out = SchedFields::default();
let mut parse_dotted = |value: &str| -> Option<u64> {
match parsed_ns_from_dotted(value) {
Ok(v) => Some(v),
Err(ParseDottedNs::Negative) => {
if let Some(t) = tally.as_mut() {
t.record_negative_dotted();
}
None
}
Err(ParseDottedNs::Malformed) => None,
}
};
for line in raw.lines() {
let Some((key, value)) = line.split_once(':') else {
continue;
};
let key = key.trim();
let value = value.trim();
let parsed_u64 = || value.parse::<u64>().ok();
if key == "ext.enabled" {
out.ext_enabled = value.parse::<u64>().ok().map(|n| n != 0);
continue;
}
let short = key.rsplit('.').next().unwrap_or(key);
match short {
"nr_wakeups" => out.nr_wakeups = parsed_u64(),
"nr_wakeups_local" => out.nr_wakeups_local = parsed_u64(),
"nr_wakeups_remote" => out.nr_wakeups_remote = parsed_u64(),
"nr_wakeups_sync" => out.nr_wakeups_sync = parsed_u64(),
"nr_wakeups_migrate" => out.nr_wakeups_migrate = parsed_u64(),
"nr_wakeups_affine" => out.nr_wakeups_affine = parsed_u64(),
"nr_wakeups_affine_attempts" => out.nr_wakeups_affine_attempts = parsed_u64(),
"nr_migrations" => out.nr_migrations = parsed_u64(),
"nr_forced_migrations" => out.nr_forced_migrations = parsed_u64(),
"nr_failed_migrations_affine" => out.nr_failed_migrations_affine = parsed_u64(),
"nr_failed_migrations_running" => out.nr_failed_migrations_running = parsed_u64(),
"nr_failed_migrations_hot" => out.nr_failed_migrations_hot = parsed_u64(),
"wait_sum" => out.wait_sum = parse_dotted(value),
"wait_count" => out.wait_count = parsed_u64(),
"wait_max" => out.wait_max = parse_dotted(value),
"sum_sleep_runtime" => out.sleep_sum = parse_dotted(value),
"sleep_max" => out.sleep_max = parse_dotted(value),
"sum_block_runtime" => out.block_sum = parse_dotted(value),
"block_max" => out.block_max = parse_dotted(value),
"iowait_sum" => out.iowait_sum = parse_dotted(value),
"iowait_count" => out.iowait_count = parsed_u64(),
"exec_max" => out.exec_max = parse_dotted(value),
"slice_max" => out.slice_max = parse_dotted(value),
"core_forceidle_sum" => out.core_forceidle_sum = parse_dotted(value),
"slice" => out.fair_slice_ns = parsed_u64(),
_ => {}
}
}
out
}
pub(super) fn read_sched_at_with_tally(
proc_root: &Path,
tgid: i32,
tid: i32,
tally: &mut Option<&mut ParseTally>,
) -> SchedFields {
match fs::read_to_string(task_file(proc_root, tgid, tid, "sched")) {
Ok(raw) => parse_sched(&raw, tally),
Err(_) => {
if let Some(t) = tally.as_mut() {
t.record_failure("sched");
}
SchedFields::default()
}
}
}
pub(super) fn parse_smaps_rollup(raw: &str) -> BTreeMap<String, u64> {
let mut out = BTreeMap::new();
for line in raw.lines() {
let Some((key, value)) = line.split_once(':') else {
continue;
};
let key_trimmed = key.trim();
if key_trimmed.contains(char::is_whitespace) || key_trimmed.contains('-') {
continue;
}
let Some(n_str) = value.split_ascii_whitespace().next() else {
continue;
};
let Ok(n) = n_str.parse::<u64>() else {
continue;
};
out.insert(key_trimmed.to_string(), n);
}
out
}
pub(super) fn read_smaps_rollup_at_with_tally(
proc_root: &Path,
tgid: i32,
tid: i32,
tally: &mut Option<&mut ParseTally>,
) -> BTreeMap<String, u64> {
if tid != tgid {
return BTreeMap::new();
}
match fs::read_to_string(task_file(proc_root, tgid, tid, "smaps_rollup")) {
Ok(raw) => parse_smaps_rollup(&raw),
Err(_) => {
if let Some(t) = tally.as_mut() {
t.record_failure("smaps_rollup");
}
BTreeMap::new()
}
}
}
pub(super) fn parse_cpu_stat(raw: &str) -> (Option<u64>, Option<u64>, Option<u64>) {
let mut usage = None;
let mut throttled = None;
let mut throttled_usec = None;
for line in raw.lines() {
let mut parts = line.split_ascii_whitespace();
let Some(key) = parts.next() else { continue };
let Some(value) = parts.next() else { continue };
let parsed = value.parse::<u64>().ok();
match key {
"usage_usec" => usage = parsed,
"nr_throttled" => throttled = parsed,
"throttled_usec" => throttled_usec = parsed,
_ => {}
}
}
(usage, throttled, throttled_usec)
}
pub(super) fn parse_kv_counters(raw: &str) -> BTreeMap<String, u64> {
let mut out = BTreeMap::new();
for line in raw.lines() {
let mut parts = line.split_ascii_whitespace();
let Some(key) = parts.next() else { continue };
let Some(value) = parts.next() else { continue };
let Ok(parsed) = value.parse::<u64>() else {
continue;
};
out.insert(key.to_string(), parsed);
}
out
}
pub(super) fn parse_max_or_u64(raw: &str) -> Option<u64> {
let trimmed = raw.trim();
if trimmed == "max" {
return None;
}
trimmed.parse::<u64>().ok()
}
pub(super) fn parse_floor_value(raw: &str) -> Option<u64> {
let trimmed = raw.trim();
if trimmed == "max" {
return Some(u64::MAX);
}
trimmed.parse::<u64>().ok()
}
pub(super) fn parse_cpu_max(raw: &str) -> (Option<u64>, u64) {
let mut parts = raw.split_ascii_whitespace();
let quota_token = parts.next();
let period_token = parts.next();
let quota = quota_token.and_then(parse_max_or_u64_str);
let period = period_token
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(CPU_MAX_DEFAULT_PERIOD_US);
(quota, period)
}
pub(super) fn parse_max_or_u64_str(s: &str) -> Option<u64> {
if s == "max" {
return None;
}
s.parse::<u64>().ok()
}
pub(super) const CPU_MAX_DEFAULT_PERIOD_US: u64 = 100_000;
pub(super) fn read_cgroup_stats_at(cgroup_root: &Path, path: &str) -> CgroupStats {
let relative = path.strip_prefix('/').unwrap_or(path);
let dir = if relative.is_empty() {
cgroup_root.to_path_buf()
} else {
cgroup_root.join(relative)
};
let (usage, throttled, throttled_usec) = fs::read_to_string(dir.join("cpu.stat"))
.ok()
.as_deref()
.map(parse_cpu_stat)
.unwrap_or((None, None, None));
let (max_quota_us, max_period_us) = fs::read_to_string(dir.join("cpu.max"))
.ok()
.as_deref()
.map(parse_cpu_max)
.unwrap_or((None, CPU_MAX_DEFAULT_PERIOD_US));
let weight = fs::read_to_string(dir.join("cpu.weight"))
.ok()
.and_then(|s| s.trim().parse::<u64>().ok());
let weight_nice = fs::read_to_string(dir.join("cpu.weight.nice"))
.ok()
.and_then(|s| s.trim().parse::<i32>().ok());
let memory_current = fs::read_to_string(dir.join("memory.current"))
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.unwrap_or(0);
let memory_max = fs::read_to_string(dir.join("memory.max"))
.ok()
.as_deref()
.and_then(parse_max_or_u64);
let memory_high = fs::read_to_string(dir.join("memory.high"))
.ok()
.as_deref()
.and_then(parse_max_or_u64);
let memory_low = fs::read_to_string(dir.join("memory.low"))
.ok()
.as_deref()
.and_then(parse_floor_value);
let memory_min = fs::read_to_string(dir.join("memory.min"))
.ok()
.as_deref()
.and_then(parse_floor_value);
let memory_stat = fs::read_to_string(dir.join("memory.stat"))
.ok()
.as_deref()
.map(parse_kv_counters)
.unwrap_or_default();
let memory_events = fs::read_to_string(dir.join("memory.events"))
.ok()
.as_deref()
.map(parse_kv_counters)
.unwrap_or_default();
let pids_current = fs::read_to_string(dir.join("pids.current"))
.ok()
.and_then(|s| s.trim().parse::<u64>().ok());
let pids_max = fs::read_to_string(dir.join("pids.max"))
.ok()
.as_deref()
.and_then(parse_max_or_u64);
let psi = read_cgroup_psi_at(cgroup_root, path);
CgroupStats {
cpu: CgroupCpuStats {
usage_usec: usage.unwrap_or(0),
nr_throttled: throttled.unwrap_or(0),
throttled_usec: throttled_usec.unwrap_or(0),
max_quota_us,
max_period_us,
weight,
weight_nice,
},
memory: CgroupMemoryStats {
current: memory_current,
max: memory_max,
high: memory_high,
low: memory_low,
min: memory_min,
stat: memory_stat,
events: memory_events,
},
pids: CgroupPidsStats {
current: pids_current,
max: pids_max,
},
psi,
}
}