use std::collections::HashSet;
use super::btf_offsets::{CgroupWalkOffsets, PsiGroupOffsets};
use super::reader::GuestMem;
use super::symbols::kva_to_pa;
const MAX_CGROUPS_WALKED: u32 = 1024;
const MAX_CGROUP_NAME_LEN: usize = 256;
const CGROUP2_MOUNT_PREFIX: &str = "/sys/fs/cgroup";
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CgroupPsiStat {
pub cgroup_kva: u64,
pub total_ns: u64,
pub avg10_raw: u64,
pub serial_nr: u64,
}
fn workload_root_segments(path: &str) -> Vec<&str> {
let rel = path.strip_prefix(CGROUP2_MOUNT_PREFIX).unwrap_or(path);
rel.split('/').filter(|s| !s.is_empty()).collect()
}
fn children_of(
budget: &mut u32,
mem: &GuestMem,
off: &CgroupWalkOffsets,
page_offset: u64,
parent_kva: u64,
parent_pa: u64,
) -> Vec<u64> {
let children_field = off.cgroup_self + off.css_children;
let anchor_kva = parent_kva.wrapping_add(children_field as u64);
let mut out = Vec::new();
let mut seen: HashSet<u64> = HashSet::new();
let mut node = mem.read_u64(parent_pa, children_field);
while node != anchor_kva {
if node == 0 || *budget == 0 || !seen.insert(node) {
break;
}
*budget -= 1;
let child_cgroup = node
.wrapping_sub(off.css_sibling as u64)
.wrapping_sub(off.cgroup_self as u64);
out.push(child_cgroup);
let node_pa = kva_to_pa(node, page_offset);
node = mem.read_u64(node_pa, 0);
}
out
}
fn is_leaf(off: &CgroupWalkOffsets, cgroup_kva: u64, cgroup_pa: u64, mem: &GuestMem) -> bool {
let children_field = off.cgroup_self + off.css_children;
let anchor_kva = cgroup_kva.wrapping_add(children_field as u64);
mem.read_u64(cgroup_pa, children_field) == anchor_kva
}
fn read_cgroup_name(
mem: &GuestMem,
off: &CgroupWalkOffsets,
page_offset: u64,
cgroup_pa: u64,
) -> Option<String> {
let kn_kva = mem.read_u64(cgroup_pa, off.cgroup_kn);
if kn_kva == 0 {
return None;
}
let kn_pa = kva_to_pa(kn_kva, page_offset);
let name_kva = mem.read_u64(kn_pa, off.kernfs_node_name);
if name_kva == 0 {
return None;
}
let name_pa = kva_to_pa(name_kva, page_offset);
let mut buf = [0u8; MAX_CGROUP_NAME_LEN];
let n = mem.read_bytes(name_pa, &mut buf);
if n == 0 {
return None;
}
let end = buf[..n].iter().position(|&b| b == 0).unwrap_or(n);
std::str::from_utf8(&buf[..end]).ok().map(str::to_owned)
}
fn read_cgroup_psi(
mem: &GuestMem,
off: &CgroupWalkOffsets,
psi_off: &PsiGroupOffsets,
page_offset: u64,
cgroup_kva: u64,
cgroup_pa: u64,
) -> Option<CgroupPsiStat> {
let psi_kva = mem.read_u64(cgroup_pa, off.cgroup_psi);
if psi_kva == 0 {
return None;
}
let total_off = psi_off.total_irq_full_off()?;
let avg10_off = psi_off.avg10_irq_full_off()?;
let psi_pa = kva_to_pa(psi_kva, page_offset);
Some(CgroupPsiStat {
cgroup_kva,
total_ns: mem.read_u64(psi_pa, total_off),
avg10_raw: mem.read_u64(psi_pa, avg10_off),
serial_nr: mem.read_u64(cgroup_pa, off.cgroup_self + off.css_serial_nr),
})
}
#[allow(clippy::too_many_arguments)]
pub fn collect_workload_cgroup_psi(
mem: &GuestMem,
off: &CgroupWalkOffsets,
psi_off: &PsiGroupOffsets,
root_cgroup_kva: u64,
root_cgroup_pa: u64,
workload_root_path: &str,
page_offset: u64,
) -> Vec<CgroupPsiStat> {
let mut budget: u32 = MAX_CGROUPS_WALKED;
let mut cur_kva = root_cgroup_kva;
let mut cur_pa = root_cgroup_pa;
for segment in workload_root_segments(workload_root_path) {
let Some((child_kva, child_pa)) =
children_of(&mut budget, mem, off, page_offset, cur_kva, cur_pa)
.into_iter()
.map(|c| (c, kva_to_pa(c, page_offset)))
.find(|&(_, pa)| {
read_cgroup_name(mem, off, page_offset, pa).as_deref() == Some(segment)
})
else {
return Vec::new();
};
cur_kva = child_kva;
cur_pa = child_pa;
}
let mut out = Vec::new();
let mut visited: HashSet<u64> = HashSet::new();
let mut stack: Vec<(u64, u64)> =
children_of(&mut budget, mem, off, page_offset, cur_kva, cur_pa)
.into_iter()
.map(|c| (c, kva_to_pa(c, page_offset)))
.collect();
while let Some((kva, pa)) = stack.pop() {
if !visited.insert(kva) {
continue; }
if is_leaf(off, kva, pa, mem) {
if let Some(stat) = read_cgroup_psi(mem, off, psi_off, page_offset, kva, pa) {
out.push(stat);
}
} else {
for (child_kva, child_pa) in children_of(&mut budget, mem, off, page_offset, kva, pa)
.into_iter()
.map(|c| (c, kva_to_pa(c, page_offset)))
{
stack.push((child_kva, child_pa));
}
}
}
if budget == 0 {
tracing::warn!(
cap = MAX_CGROUPS_WALKED,
"collect_workload_cgroup_psi: node-visit budget cap reached — if the \
workload cgroup subtree exceeds the cap, its tail (some leaves' \
per-cgroup PSI) was not captured"
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::symbols::DEFAULT_PAGE_OFFSET;
const SELF_OFF: usize = 0;
const CSS_SIBLING: usize = 16;
const CSS_CHILDREN: usize = 32;
const CSS_SERIAL_NR: usize = 48; const CGROUP_KN: usize = 64;
const CGROUP_PSI: usize = 72;
const ROOT_CGRP: usize = 256; const KN_NAME: usize = 8;
fn offs() -> CgroupWalkOffsets {
CgroupWalkOffsets {
cgroup_self: SELF_OFF,
cgroup_kn: CGROUP_KN,
cgroup_psi: CGROUP_PSI,
css_sibling: CSS_SIBLING,
css_children: CSS_CHILDREN,
css_serial_nr: CSS_SERIAL_NR,
cgroup_root_cgrp: ROOT_CGRP,
kernfs_node_name: KN_NAME,
}
}
fn psi_offs() -> PsiGroupOffsets {
PsiGroupOffsets {
psi_group_total: 0,
psi_group_avg: 512,
psi_irq_full_idx: Some(6),
}
}
struct Image {
buf: Vec<u8>,
}
impl Image {
fn new(size: usize) -> Self {
Self {
buf: vec![0u8; size],
}
}
fn kva(pa: usize) -> u64 {
DEFAULT_PAGE_OFFSET + pa as u64
}
fn w64(&mut self, pa: usize, v: u64) {
self.buf[pa..pa + 8].copy_from_slice(&v.to_le_bytes());
}
fn wstr(&mut self, pa: usize, s: &str) {
self.buf[pa..pa + s.len()].copy_from_slice(s.as_bytes());
self.buf[pa + s.len()] = 0;
}
fn init_cgroup(&mut self, pa: usize, kn_pa: usize, psi_kva: u64) {
let children_kva = Self::kva(pa + SELF_OFF + CSS_CHILDREN);
self.w64(pa + SELF_OFF + CSS_CHILDREN, children_kva);
self.w64(pa + CGROUP_KN, Self::kva(kn_pa));
self.w64(pa + CGROUP_PSI, psi_kva);
}
fn link_children(&mut self, parent_pa: usize, children: &[usize]) {
let anchor = Self::kva(parent_pa + SELF_OFF + CSS_CHILDREN);
let mut prev_next_field = parent_pa + SELF_OFF + CSS_CHILDREN; for &c in children {
let sibling_node = Self::kva(c + SELF_OFF + CSS_SIBLING);
self.w64(prev_next_field, sibling_node);
prev_next_field = c + SELF_OFF + CSS_SIBLING; }
self.w64(prev_next_field, anchor);
}
}
#[test]
fn walk_collects_workload_leaves_excludes_scheduler() {
let mut img = Image::new(0x8000);
let psi_cg0 = 0x4000usize;
let psi_cg1 = 0x4800usize;
img.w64(psi_cg0 + 6 * 8, 111_000); img.w64(psi_cg0 + 512 + 6 * 3 * 8, 2048); img.w64(psi_cg1 + 6 * 8, 222_000); img.w64(psi_cg1 + 512 + 6 * 3 * 8, 4096);
let kn_root = 0x100usize;
let name_ktstr = 0x140usize;
let kn_ktstr = 0x180usize;
let name_sched = 0x1c0usize;
let kn_sched = 0x200usize;
let name_cg0 = 0x240usize;
let kn_cg0 = 0x280usize;
let name_cg1 = 0x2c0usize;
let kn_cg1 = 0x300usize;
img.wstr(name_ktstr, "ktstr");
img.wstr(name_sched, "sched");
img.wstr(name_cg0, "cg_0");
img.wstr(name_cg1, "cg_1");
img.w64(kn_root + KN_NAME, Image::kva(0x130)); img.wstr(0x130, "/");
img.w64(kn_ktstr + KN_NAME, Image::kva(name_ktstr));
img.w64(kn_sched + KN_NAME, Image::kva(name_sched));
img.w64(kn_cg0 + KN_NAME, Image::kva(name_cg0));
img.w64(kn_cg1 + KN_NAME, Image::kva(name_cg1));
let cgrp_dfl_root = 0x1000usize; let root = cgrp_dfl_root + ROOT_CGRP; let ktstr = 0x2000usize;
let sched = 0x2400usize;
let cg0 = 0x2800usize;
let cg1 = 0x2c00usize;
for (cg, kn) in [(root, kn_root), (ktstr, kn_ktstr), (sched, kn_sched)] {
img.init_cgroup(cg, kn, 0); }
img.init_cgroup(cg0, kn_cg0, Image::kva(psi_cg0));
img.init_cgroup(cg1, kn_cg1, Image::kva(psi_cg1));
img.w64(cg0 + SELF_OFF + CSS_SERIAL_NR, 7);
img.w64(cg1 + SELF_OFF + CSS_SERIAL_NR, 9);
img.link_children(root, &[ktstr, sched]);
img.link_children(ktstr, &[cg0, cg1]);
let sched_leaf = 0x3000usize;
let kn_sl = 0x340usize;
let name_sl = 0x380usize;
img.wstr(name_sl, "scx");
img.w64(kn_sl + KN_NAME, Image::kva(name_sl));
let psi_sl = 0x5000usize;
img.w64(psi_sl + 6 * 8, 999_000);
img.init_cgroup(sched_leaf, kn_sl, Image::kva(psi_sl));
img.link_children(sched, &[sched_leaf]);
let mem = unsafe { GuestMem::new(img.buf.as_mut_ptr(), img.buf.len() as u64) };
let mut got = collect_workload_cgroup_psi(
&mem,
&offs(),
&psi_offs(),
Image::kva(root),
root as u64, "/sys/fs/cgroup/ktstr",
DEFAULT_PAGE_OFFSET,
);
got.sort_by_key(|s| s.total_ns);
assert_eq!(
got.len(),
2,
"exactly the two workload leaves (sched excluded)"
);
assert_eq!(got[0].total_ns, 111_000);
assert_eq!(got[0].avg10_raw, 2048);
assert_eq!(got[0].cgroup_kva, Image::kva(cg0));
assert_eq!(
got[0].serial_nr, 7,
"cg0 serial_nr read from the embedded css"
);
assert_eq!(got[1].total_ns, 222_000);
assert_eq!(got[1].cgroup_kva, Image::kva(cg1));
assert_eq!(
got[1].serial_nr, 9,
"cg1 serial_nr read from the embedded css"
);
}
#[test]
fn walk_skips_null_psi_leaf() {
let mut img = Image::new(0x4000);
let kn_root = 0x100usize;
let kn_ktstr = 0x180usize;
let kn_cg0 = 0x280usize;
img.wstr(0x140, "ktstr");
img.wstr(0x240, "cg_0");
img.w64(kn_root + KN_NAME, Image::kva(0x130));
img.wstr(0x130, "/");
img.w64(kn_ktstr + KN_NAME, Image::kva(0x140));
img.w64(kn_cg0 + KN_NAME, Image::kva(0x240));
let cgrp_dfl_root = 0x1000usize;
let root = cgrp_dfl_root + ROOT_CGRP;
let ktstr = 0x2000usize;
let cg0 = 0x2800usize;
img.init_cgroup(root, kn_root, 0);
img.init_cgroup(ktstr, kn_ktstr, 0);
img.init_cgroup(cg0, kn_cg0, 0); img.link_children(root, &[ktstr]);
img.link_children(ktstr, &[cg0]);
let mem = unsafe { GuestMem::new(img.buf.as_mut_ptr(), img.buf.len() as u64) };
let got = collect_workload_cgroup_psi(
&mem,
&offs(),
&psi_offs(),
Image::kva(root),
root as u64,
"/sys/fs/cgroup/ktstr",
DEFAULT_PAGE_OFFSET,
);
assert!(
got.is_empty(),
"NULL-psi leaf is skipped (loud-absent), not a 0"
);
}
#[test]
fn walk_absent_workload_root_is_empty() {
let mut img = Image::new(0x4000);
let kn_root = 0x100usize;
img.w64(kn_root + KN_NAME, Image::kva(0x130));
img.wstr(0x130, "/");
let cgrp_dfl_root = 0x1000usize;
let root = cgrp_dfl_root + ROOT_CGRP;
img.init_cgroup(root, kn_root, 0); let mem = unsafe { GuestMem::new(img.buf.as_mut_ptr(), img.buf.len() as u64) };
let got = collect_workload_cgroup_psi(
&mem,
&offs(),
&psi_offs(),
Image::kva(root),
root as u64,
"/sys/fs/cgroup/ktstr",
DEFAULT_PAGE_OFFSET,
);
assert!(got.is_empty(), "absent workload root → loud-absent");
}
#[test]
fn walk_tolerates_cyclic_children_list() {
let mut img = Image::new(0x4000);
let kn_root = 0x100usize;
let kn_ktstr = 0x180usize;
img.wstr(0x140, "ktstr");
img.w64(kn_root + KN_NAME, Image::kva(0x130));
img.wstr(0x130, "/");
img.w64(kn_ktstr + KN_NAME, Image::kva(0x140));
let cgrp_dfl_root = 0x1000usize;
let root = cgrp_dfl_root + ROOT_CGRP;
let ktstr = 0x2000usize;
img.init_cgroup(root, kn_root, 0);
img.init_cgroup(ktstr, kn_ktstr, 0);
img.link_children(root, &[ktstr]);
let bogus = 0x2800usize;
img.w64(
ktstr + SELF_OFF + CSS_CHILDREN,
Image::kva(bogus + CSS_SIBLING),
);
img.w64(bogus + CSS_SIBLING, Image::kva(bogus + CSS_SIBLING)); let mem = unsafe { GuestMem::new(img.buf.as_mut_ptr(), img.buf.len() as u64) };
let got = collect_workload_cgroup_psi(
&mem,
&offs(),
&psi_offs(),
Image::kva(root),
root as u64,
"/sys/fs/cgroup/ktstr",
DEFAULT_PAGE_OFFSET,
);
assert!(got.len() <= 1, "cyclic list bounded, no hang");
}
#[test]
fn walk_descends_deep_tree() {
let mut img = Image::new(0x6000);
let kn_root = 0x100usize;
let kn_ktstr = 0x180usize;
let kn_mid = 0x200usize;
let kn_leaf = 0x280usize;
img.wstr(0x140, "ktstr");
img.wstr(0x1c0, "mid");
img.wstr(0x240, "leaf");
img.w64(kn_root + KN_NAME, Image::kva(0x130));
img.wstr(0x130, "/");
img.w64(kn_ktstr + KN_NAME, Image::kva(0x140));
img.w64(kn_mid + KN_NAME, Image::kva(0x1c0));
img.w64(kn_leaf + KN_NAME, Image::kva(0x240));
let cgrp_dfl_root = 0x1000usize;
let root = cgrp_dfl_root + ROOT_CGRP;
let ktstr = 0x2000usize;
let mid = 0x2400usize;
let leaf = 0x2800usize;
let psi_leaf = 0x4000usize;
img.w64(psi_leaf + 6 * 8, 333_000); img.init_cgroup(root, kn_root, 0);
img.init_cgroup(ktstr, kn_ktstr, 0);
img.init_cgroup(mid, kn_mid, 0);
img.init_cgroup(leaf, kn_leaf, Image::kva(psi_leaf));
img.link_children(root, &[ktstr]);
img.link_children(ktstr, &[mid]);
img.link_children(mid, &[leaf]);
let mem = unsafe { GuestMem::new(img.buf.as_mut_ptr(), img.buf.len() as u64) };
let got = collect_workload_cgroup_psi(
&mem,
&offs(),
&psi_offs(),
Image::kva(root),
root as u64,
"/sys/fs/cgroup/ktstr",
DEFAULT_PAGE_OFFSET,
);
assert_eq!(
got.len(),
1,
"the single deep leaf under ktstr/mid is captured"
);
assert_eq!(got[0].total_ns, 333_000);
assert_eq!(got[0].cgroup_kva, Image::kva(leaf));
}
#[test]
fn children_of_respects_budget() {
let mut img = Image::new(0x4000);
let parent = 0x1000usize;
let kids = [0x1400usize, 0x1800, 0x1c00, 0x2000, 0x2400];
img.init_cgroup(parent, 0x100, 0);
for &k in &kids {
img.init_cgroup(k, 0x100, 0);
}
img.link_children(parent, &kids);
let mem = unsafe { GuestMem::new(img.buf.as_mut_ptr(), img.buf.len() as u64) };
let mut budget: u32 = 3;
let children = children_of(
&mut budget,
&mem,
&offs(),
DEFAULT_PAGE_OFFSET,
Image::kva(parent),
parent as u64,
);
assert_eq!(
children.len(),
3,
"budget caps the per-list walk numerically"
);
assert_eq!(budget, 0, "budget fully consumed");
}
#[test]
fn walk_multi_segment_workload_root() {
let mut img = Image::new(0x6000);
let kn_root = 0x100usize;
let kn_a = 0x180usize;
let kn_b = 0x200usize;
let kn_leaf = 0x280usize;
img.wstr(0x140, "a");
img.wstr(0x1c0, "b");
img.wstr(0x240, "cg_0");
img.w64(kn_root + KN_NAME, Image::kva(0x130));
img.wstr(0x130, "/");
img.w64(kn_a + KN_NAME, Image::kva(0x140));
img.w64(kn_b + KN_NAME, Image::kva(0x1c0));
img.w64(kn_leaf + KN_NAME, Image::kva(0x240));
let cgrp_dfl_root = 0x1000usize;
let root = cgrp_dfl_root + ROOT_CGRP;
let a = 0x2000usize;
let b = 0x2400usize;
let leaf = 0x2800usize;
let psi_leaf = 0x4000usize;
img.w64(psi_leaf + 6 * 8, 444_000);
img.init_cgroup(root, kn_root, 0);
img.init_cgroup(a, kn_a, 0);
img.init_cgroup(b, kn_b, 0);
img.init_cgroup(leaf, kn_leaf, Image::kva(psi_leaf));
img.link_children(root, &[a]);
img.link_children(a, &[b]);
img.link_children(b, &[leaf]);
let mem = unsafe { GuestMem::new(img.buf.as_mut_ptr(), img.buf.len() as u64) };
let got = collect_workload_cgroup_psi(
&mem,
&offs(),
&psi_offs(),
Image::kva(root),
root as u64,
"/sys/fs/cgroup/a/b",
DEFAULT_PAGE_OFFSET,
);
assert_eq!(
got.len(),
1,
"leaf under the a/b multi-segment root is captured"
);
assert_eq!(got[0].total_ns, 444_000);
}
#[test]
fn segments_strip_mount_prefix() {
assert_eq!(
workload_root_segments("/sys/fs/cgroup/ktstr"),
vec!["ktstr"]
);
assert_eq!(workload_root_segments("/sys/fs/cgroup/a/b"), vec!["a", "b"]);
assert_eq!(workload_root_segments("ktstr"), vec!["ktstr"]);
assert_eq!(workload_root_segments("/sys/fs/cgroup"), Vec::<&str>::new());
}
}