use std::io::{self, ErrorKind};
use std::sync::atomic::{AtomicU32, Ordering};
use netlink_packet_core::{
NLM_F_REQUEST, NLMSG_ERROR, NetlinkHeader, NetlinkMessage, NetlinkPayload,
};
use netlink_packet_generic::{
GenlMessage,
ctrl::{GenlCtrl, GenlCtrlCmd, nlas::GenlCtrlAttrs},
};
use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_GENERIC};
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct TaskstatsSummary {
pub ok_count: u64,
pub eperm_count: u64,
pub esrch_count: u64,
pub other_err_count: u64,
}
impl TaskstatsSummary {
pub fn record_result(&mut self, result: &io::Result<DelayStats>) {
const EPERM: i32 = 1;
const ESRCH: i32 = 3;
match result {
Ok(_) => self.ok_count = self.ok_count.saturating_add(1),
Err(e) => {
let errno = classify_errno(e);
match errno {
Some(EPERM) => {
self.eperm_count = self.eperm_count.saturating_add(1);
}
Some(ESRCH) => {
self.esrch_count = self.esrch_count.saturating_add(1);
}
_ => {
self.other_err_count = self.other_err_count.saturating_add(1);
}
}
}
}
}
}
fn classify_errno(e: &io::Error) -> Option<i32> {
if let Some(code) = e.raw_os_error() {
return Some(code);
}
let msg = e.to_string();
let needle = "errno=";
let pos = msg.rfind(needle)?;
let tail = &msg[pos + needle.len()..];
let end = tail
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(tail.len());
tail[..end].parse::<i32>().ok()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DelayStats {
pub cpu_count: u64,
pub cpu_delay_total_ns: u64,
pub cpu_delay_max_ns: u64,
pub cpu_delay_min_ns: u64,
pub blkio_count: u64,
pub blkio_delay_total_ns: u64,
pub blkio_delay_max_ns: u64,
pub blkio_delay_min_ns: u64,
pub swapin_count: u64,
pub swapin_delay_total_ns: u64,
pub swapin_delay_max_ns: u64,
pub swapin_delay_min_ns: u64,
pub freepages_count: u64,
pub freepages_delay_total_ns: u64,
pub freepages_delay_max_ns: u64,
pub freepages_delay_min_ns: u64,
pub thrashing_count: u64,
pub thrashing_delay_total_ns: u64,
pub thrashing_delay_max_ns: u64,
pub thrashing_delay_min_ns: u64,
pub compact_count: u64,
pub compact_delay_total_ns: u64,
pub compact_delay_max_ns: u64,
pub compact_delay_min_ns: u64,
pub wpcopy_count: u64,
pub wpcopy_delay_total_ns: u64,
pub wpcopy_delay_max_ns: u64,
pub wpcopy_delay_min_ns: u64,
pub irq_count: u64,
pub irq_delay_total_ns: u64,
pub irq_delay_max_ns: u64,
pub irq_delay_min_ns: u64,
pub hiwater_rss_bytes: u64,
pub hiwater_vm_bytes: u64,
}
pub struct TaskstatsClient {
socket: Socket,
family_id: u16,
seq: AtomicU32,
}
const TASKSTATS_CMD_GET: u8 = 1;
#[cfg(test)]
const TASKSTATS_CMD_NEW: u8 = 2;
const TASKSTATS_GENL_VERSION: u8 = 1;
const TASKSTATS_CMD_ATTR_PID: u16 = 1;
const TASKSTATS_TYPE_PID: u16 = 1;
const TASKSTATS_TYPE_STATS: u16 = 3;
const TASKSTATS_TYPE_AGGR_PID: u16 = 4;
const TASKSTATS_FAMILY_NAME: &str = "TASKSTATS";
impl TaskstatsClient {
pub fn open() -> io::Result<Self> {
let mut socket = Socket::new(NETLINK_GENERIC)?;
socket.bind_auto()?;
let family_id = resolve_family_id(&socket, TASKSTATS_FAMILY_NAME)?;
Ok(Self {
socket,
family_id,
seq: AtomicU32::new(1),
})
}
pub fn query_tid(&self, tid: u32) -> io::Result<DelayStats> {
let seq = self.seq.fetch_add(1, Ordering::SeqCst);
let request = build_request(self.family_id, seq, tid);
self.socket.send_to(&request, &SocketAddr::new(0, 0), 0)?;
let (reply, _addr) = self.socket.recv_from_full()?;
parse_reply(&reply, tid).map_err(io::Error::other)
}
}
fn resolve_family_id(socket: &Socket, name: &str) -> io::Result<u16> {
let payload = GenlCtrl {
cmd: GenlCtrlCmd::GetFamily,
nlas: vec![GenlCtrlAttrs::FamilyName(name.to_string())],
};
let mut nl_msg: NetlinkMessage<GenlMessage<GenlCtrl>> =
NetlinkMessage::from(GenlMessage::from_payload(payload));
let mut header = NetlinkHeader::default();
header.flags = NLM_F_REQUEST;
nl_msg.header = header;
nl_msg.finalize();
let mut buf = vec![0u8; nl_msg.header.length as usize];
nl_msg.serialize(&mut buf);
socket.send_to(&buf, &SocketAddr::new(0, 0), 0)?;
let (reply_buf, _) = socket.recv_from_full()?;
let reply: NetlinkMessage<GenlMessage<GenlCtrl>> =
NetlinkMessage::deserialize(&reply_buf).map_err(io::Error::other)?;
match reply.payload {
NetlinkPayload::InnerMessage(genl) => {
for attr in &genl.payload.nlas {
if let GenlCtrlAttrs::FamilyId(id) = attr {
return Ok(*id);
}
}
Err(io::Error::new(
ErrorKind::NotFound,
format!("CTRL_ATTR_FAMILY_ID missing in CTRL_CMD_GETFAMILY reply for {name}"),
))
}
NetlinkPayload::Error(err) => Err(io::Error::other(format!(
"CTRL_CMD_GETFAMILY for {name}: {err:?}"
))),
_ => Err(io::Error::new(
ErrorKind::InvalidData,
"unexpected NetlinkPayload variant from CTRL_CMD_GETFAMILY",
)),
}
}
fn build_request(family_id: u16, seq: u32, tid: u32) -> [u8; 28] {
let mut buf = [0u8; 28];
buf[0..4].copy_from_slice(&28u32.to_ne_bytes()); buf[4..6].copy_from_slice(&family_id.to_ne_bytes()); buf[6..8].copy_from_slice(&NLM_F_REQUEST.to_ne_bytes()); buf[8..12].copy_from_slice(&seq.to_ne_bytes()); buf[16] = TASKSTATS_CMD_GET;
buf[17] = TASKSTATS_GENL_VERSION;
buf[20..22].copy_from_slice(&8u16.to_ne_bytes()); buf[22..24].copy_from_slice(&TASKSTATS_CMD_ATTR_PID.to_ne_bytes());
buf[24..28].copy_from_slice(&tid.to_ne_bytes());
buf
}
fn parse_reply(buf: &[u8], expected_tid: u32) -> Result<DelayStats, String> {
let buf_len = buf.len();
if buf_len < 16 {
return Err(format!("reply shorter than nlmsghdr: {buf_len} bytes"));
}
let nlmsg_len = u32::from_ne_bytes(buf[0..4].try_into().unwrap()) as usize;
let nlmsg_type = u16::from_ne_bytes(buf[4..6].try_into().unwrap());
if nlmsg_len > buf_len {
return Err(format!(
"nlmsghdr length {nlmsg_len} exceeds buffer length {buf_len}"
));
}
if nlmsg_type == NLMSG_ERROR {
if buf_len < 20 {
return Err("NLMSG_ERROR shorter than expected".into());
}
let err = i32::from_ne_bytes(buf[16..20].try_into().unwrap());
if err == 0 {
return Err("kernel returned NLMSG_ERROR with errno=0 (ack only)".into());
}
let errno = -err;
return Err(format!("kernel returned NLMSG_ERROR errno={errno}"));
}
if nlmsg_len < 20 {
return Err(format!(
"reply too short for nlmsghdr+genlmsghdr: {nlmsg_len}"
));
}
let payload = &buf[20..nlmsg_len];
let aggr = find_nla(payload, TASKSTATS_TYPE_AGGR_PID)
.ok_or("TASKSTATS_TYPE_AGGR_PID missing in reply")?;
let pid_attr = find_nla(aggr, TASKSTATS_TYPE_PID)
.ok_or("TASKSTATS_TYPE_PID missing in TASKSTATS_TYPE_AGGR_PID")?;
let pid_attr_len = pid_attr.len();
if pid_attr_len < 4 {
return Err(format!(
"TASKSTATS_TYPE_PID payload shorter than u32: {pid_attr_len}"
));
}
let reply_tid = u32::from_ne_bytes(pid_attr[0..4].try_into().unwrap());
if reply_tid != expected_tid {
return Err(format!(
"tid mismatch: requested {expected_tid}, got {reply_tid}"
));
}
let stats = find_nla(aggr, TASKSTATS_TYPE_STATS)
.ok_or("TASKSTATS_TYPE_STATS missing in TASKSTATS_TYPE_AGGR_PID")?;
parse_taskstats_payload(stats)
}
fn find_nla(buf: &[u8], kind: u16) -> Option<&[u8]> {
let mut offset = 0usize;
while offset + 4 <= buf.len() {
let nla_len = u16::from_ne_bytes(buf[offset..offset + 2].try_into().unwrap()) as usize;
let nla_type = u16::from_ne_bytes(buf[offset + 2..offset + 4].try_into().unwrap());
if nla_len < 4 {
return None;
}
let value_start = offset + 4;
let value_end = offset + nla_len;
if value_end > buf.len() {
return None;
}
if nla_type == kind {
return Some(&buf[value_start..value_end]);
}
offset += (nla_len + 3) & !3;
}
None
}
fn parse_taskstats_payload(buf: &[u8]) -> Result<DelayStats, String> {
let r64 = |off: usize| -> u64 {
if off + 8 > buf.len() {
0
} else {
u64::from_ne_bytes(buf[off..off + 8].try_into().unwrap())
}
};
let cpu_count = r64(16);
let cpu_delay_total_ns = r64(24);
let blkio_count = r64(32);
let blkio_delay_total_ns = r64(40);
let swapin_count = r64(48);
let swapin_delay_total_ns = r64(56);
let hiwater_rss_kb = r64(200);
let hiwater_vm_kb = r64(208);
let freepages_count = r64(312);
let freepages_delay_total_ns = r64(320);
let thrashing_count = r64(328);
let thrashing_delay_total_ns = r64(336);
let compact_count = r64(352);
let compact_delay_total_ns = r64(360);
let wpcopy_count = r64(400);
let wpcopy_delay_total_ns = r64(408);
let irq_count = r64(416);
let irq_delay_total_ns = r64(424);
let cpu_delay_max_ns = r64(432);
let cpu_delay_min_ns = r64(440);
let blkio_delay_max_ns = r64(448);
let blkio_delay_min_ns = r64(456);
let swapin_delay_max_ns = r64(464);
let swapin_delay_min_ns = r64(472);
let freepages_delay_max_ns = r64(480);
let freepages_delay_min_ns = r64(488);
let thrashing_delay_max_ns = r64(496);
let thrashing_delay_min_ns = r64(504);
let compact_delay_max_ns = r64(512);
let compact_delay_min_ns = r64(520);
let wpcopy_delay_max_ns = r64(528);
let wpcopy_delay_min_ns = r64(536);
let irq_delay_max_ns = r64(544);
let irq_delay_min_ns = r64(552);
Ok(DelayStats {
cpu_count,
cpu_delay_total_ns,
cpu_delay_max_ns,
cpu_delay_min_ns,
blkio_count,
blkio_delay_total_ns,
blkio_delay_max_ns,
blkio_delay_min_ns,
swapin_count,
swapin_delay_total_ns,
swapin_delay_max_ns,
swapin_delay_min_ns,
freepages_count,
freepages_delay_total_ns,
freepages_delay_max_ns,
freepages_delay_min_ns,
thrashing_count,
thrashing_delay_total_ns,
thrashing_delay_max_ns,
thrashing_delay_min_ns,
compact_count,
compact_delay_total_ns,
compact_delay_max_ns,
compact_delay_min_ns,
wpcopy_count,
wpcopy_delay_total_ns,
wpcopy_delay_max_ns,
wpcopy_delay_min_ns,
irq_count,
irq_delay_total_ns,
irq_delay_max_ns,
irq_delay_min_ns,
hiwater_rss_bytes: hiwater_rss_kb.saturating_mul(1024),
hiwater_vm_bytes: hiwater_vm_kb.saturating_mul(1024),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn build_reply_buf(nlmsg_type: u16, payload: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(20 + payload.len());
buf.extend_from_slice(&0u32.to_ne_bytes());
buf.extend_from_slice(&nlmsg_type.to_ne_bytes());
buf.extend_from_slice(&0u16.to_ne_bytes());
buf.extend_from_slice(&0u32.to_ne_bytes());
buf.extend_from_slice(&0u32.to_ne_bytes());
buf.push(TASKSTATS_CMD_NEW);
buf.push(TASKSTATS_GENL_VERSION);
buf.extend_from_slice(&0u16.to_ne_bytes());
buf.extend_from_slice(payload);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_ne_bytes());
buf
}
#[test]
fn build_request_layout() {
let req = build_request(0x4242, 0x1234, 0xCAFEBABE);
assert_eq!(req.len(), 28);
assert_eq!(u32::from_ne_bytes(req[0..4].try_into().unwrap()), 28);
assert_eq!(u16::from_ne_bytes(req[4..6].try_into().unwrap()), 0x4242);
assert_eq!(
u16::from_ne_bytes(req[6..8].try_into().unwrap()),
NLM_F_REQUEST,
);
assert_eq!(u32::from_ne_bytes(req[8..12].try_into().unwrap()), 0x1234);
assert_eq!(u32::from_ne_bytes(req[12..16].try_into().unwrap()), 0);
assert_eq!(req[16], TASKSTATS_CMD_GET);
assert_eq!(req[17], TASKSTATS_GENL_VERSION);
assert_eq!(u16::from_ne_bytes(req[18..20].try_into().unwrap()), 0);
assert_eq!(u16::from_ne_bytes(req[20..22].try_into().unwrap()), 8);
assert_eq!(
u16::from_ne_bytes(req[22..24].try_into().unwrap()),
TASKSTATS_CMD_ATTR_PID,
);
assert_eq!(
u32::from_ne_bytes(req[24..28].try_into().unwrap()),
0xCAFEBABE
);
}
#[test]
fn find_nla_walks_aligned_attrs() {
let mut buf = Vec::new();
buf.extend_from_slice(&5u16.to_ne_bytes());
buf.extend_from_slice(&10u16.to_ne_bytes());
buf.push(0xAA);
buf.extend_from_slice(&[0, 0, 0]); buf.extend_from_slice(&8u16.to_ne_bytes());
buf.extend_from_slice(&20u16.to_ne_bytes());
buf.extend_from_slice(&0xDEADBEEFu32.to_ne_bytes());
let v1 = find_nla(&buf, 10).expect("attr 10 present");
assert_eq!(v1, &[0xAA]);
let v2 = find_nla(&buf, 20).expect("attr 20 present");
assert_eq!(v2, &0xDEADBEEFu32.to_ne_bytes());
assert!(find_nla(&buf, 99).is_none());
}
#[test]
fn parse_taskstats_payload_handles_truncation() {
let mut buf = vec![0u8; 80];
buf[16..24].copy_from_slice(&123u64.to_ne_bytes()); buf[24..32].copy_from_slice(&456u64.to_ne_bytes()); let stats = parse_taskstats_payload(&buf).expect("short payload OK");
assert_eq!(stats.cpu_count, 123);
assert_eq!(stats.cpu_delay_total_ns, 456);
assert_eq!(stats.cpu_delay_max_ns, 0);
assert_eq!(stats.hiwater_rss_bytes, 0);
assert_eq!(stats.irq_delay_max_ns, 0);
}
#[test]
fn parse_taskstats_payload_kb_to_bytes_conversion() {
let mut buf = vec![0u8; 560];
buf[200..208].copy_from_slice(&512u64.to_ne_bytes()); buf[208..216].copy_from_slice(&u64::MAX.to_ne_bytes()); let stats = parse_taskstats_payload(&buf).expect("full payload OK");
assert_eq!(stats.hiwater_rss_bytes, 512 * 1024);
assert_eq!(stats.hiwater_vm_bytes, u64::MAX);
}
#[test]
fn parse_reply_rejects_tid_mismatch() {
let mut payload = Vec::new();
payload.extend_from_slice(&24u16.to_ne_bytes()); payload.extend_from_slice(&TASKSTATS_TYPE_AGGR_PID.to_ne_bytes());
payload.extend_from_slice(&8u16.to_ne_bytes());
payload.extend_from_slice(&TASKSTATS_TYPE_PID.to_ne_bytes());
payload.extend_from_slice(&42u32.to_ne_bytes());
payload.extend_from_slice(&12u16.to_ne_bytes());
payload.extend_from_slice(&TASKSTATS_TYPE_STATS.to_ne_bytes());
payload.extend_from_slice(&[0u8; 8]); let buf = build_reply_buf(1234, &payload);
let err = parse_reply(&buf, 99).expect_err("tid mismatch should reject");
assert!(err.contains("tid mismatch"), "error: {err}");
assert!(err.contains("99"), "error: {err}");
assert!(err.contains("42"), "error: {err}");
}
#[test]
fn parse_reply_nlmsg_error_surfaces_errno() {
let mut buf = Vec::with_capacity(20);
buf.extend_from_slice(&20u32.to_ne_bytes()); buf.extend_from_slice(&NLMSG_ERROR.to_ne_bytes()); buf.extend_from_slice(&0u16.to_ne_bytes()); buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&(-1i32).to_ne_bytes());
let err = parse_reply(&buf, 1234).expect_err("NLMSG_ERROR must surface as Err");
assert!(
err.contains("errno=1"),
"expected `errno=1` in the rendered string (parse_reply negates the kernel's wire value): {err}",
);
}
#[test]
fn parse_reply_rejects_short_buffer() {
for len in [0usize, 8, 15] {
let buf = vec![0u8; len];
let err = parse_reply(&buf, 1).expect_err("short buffer must reject");
assert!(err.contains("shorter than nlmsghdr"), "len={len}: {err}",);
}
}
#[test]
fn parse_reply_rejects_oversized_nlmsg_len() {
let mut buf = vec![0u8; 16];
buf[0..4].copy_from_slice(&999u32.to_ne_bytes()); let err = parse_reply(&buf, 1).expect_err("oversized nlmsg_len must reject");
assert!(
err.contains("exceeds buffer length"),
"expected `exceeds buffer length` in error: {err}",
);
}
#[test]
fn parse_reply_rejects_short_genlmsghdr() {
let mut buf = vec![0u8; 18];
buf[0..4].copy_from_slice(&18u32.to_ne_bytes()); let err = parse_reply(&buf, 1).expect_err("short nlmsg_len must reject");
assert!(
err.contains("too short for nlmsghdr+genlmsghdr"),
"expected `too short for nlmsghdr+genlmsghdr` in error: {err}",
);
}
#[test]
fn parse_reply_rejects_missing_aggr_pid() {
let mut payload = Vec::new();
payload.extend_from_slice(&8u16.to_ne_bytes()); payload.extend_from_slice(&99u16.to_ne_bytes()); payload.extend_from_slice(&0u32.to_ne_bytes()); let buf = build_reply_buf(1234, &payload);
let err = parse_reply(&buf, 1).expect_err("missing AGGR_PID must reject");
assert!(
err.contains("AGGR_PID missing"),
"expected `AGGR_PID missing` in error: {err}",
);
}
#[test]
fn parse_reply_rejects_missing_pid_in_aggr() {
let mut payload = Vec::new();
payload.extend_from_slice(&16u16.to_ne_bytes()); payload.extend_from_slice(&TASKSTATS_TYPE_AGGR_PID.to_ne_bytes());
payload.extend_from_slice(&12u16.to_ne_bytes());
payload.extend_from_slice(&TASKSTATS_TYPE_STATS.to_ne_bytes());
payload.extend_from_slice(&[0u8; 8]); let buf = build_reply_buf(1234, &payload);
let err = parse_reply(&buf, 1).expect_err("missing PID must reject");
assert!(
err.contains("TASKSTATS_TYPE_PID missing"),
"expected `TASKSTATS_TYPE_PID missing` in error: {err}",
);
}
#[test]
fn parse_reply_rejects_short_pid_payload() {
let mut payload = Vec::new();
payload.extend_from_slice(&12u16.to_ne_bytes()); payload.extend_from_slice(&TASKSTATS_TYPE_AGGR_PID.to_ne_bytes());
payload.extend_from_slice(&6u16.to_ne_bytes()); payload.extend_from_slice(&TASKSTATS_TYPE_PID.to_ne_bytes());
payload.extend_from_slice(&[0u8; 2]); payload.extend_from_slice(&[0u8; 2]); let buf = build_reply_buf(1234, &payload);
let err = parse_reply(&buf, 1).expect_err("short PID payload must reject");
assert!(
err.contains("PID payload shorter than u32"),
"expected `PID payload shorter than u32` in error: {err}",
);
}
#[test]
fn parse_reply_rejects_missing_stats_in_aggr() {
let mut payload = Vec::new();
payload.extend_from_slice(&12u16.to_ne_bytes()); payload.extend_from_slice(&TASKSTATS_TYPE_AGGR_PID.to_ne_bytes());
payload.extend_from_slice(&8u16.to_ne_bytes());
payload.extend_from_slice(&TASKSTATS_TYPE_PID.to_ne_bytes());
payload.extend_from_slice(&7u32.to_ne_bytes());
let buf = build_reply_buf(1234, &payload);
let err = parse_reply(&buf, 7).expect_err("missing STATS must reject");
assert!(
err.contains("TASKSTATS_TYPE_STATS missing"),
"expected `TASKSTATS_TYPE_STATS missing` in error: {err}",
);
}
#[test]
fn find_nla_empty_buffer() {
assert!(find_nla(&[], 1).is_none());
}
#[test]
fn find_nla_short_buffer() {
assert!(find_nla(&[0u8, 0, 0], 1).is_none());
}
#[test]
fn find_nla_corrupt_short_len() {
let mut buf = Vec::new();
buf.extend_from_slice(&2u16.to_ne_bytes()); buf.extend_from_slice(&1u16.to_ne_bytes()); assert!(find_nla(&buf, 1).is_none());
}
#[test]
fn find_nla_truncated_value() {
let mut buf = Vec::new();
buf.extend_from_slice(&20u16.to_ne_bytes()); buf.extend_from_slice(&1u16.to_ne_bytes()); buf.extend_from_slice(&[0u8; 4]);
assert!(find_nla(&buf, 1).is_none());
}
#[test]
fn parse_taskstats_payload_full_v17_roundtrip() {
let mut buf = vec![0u8; 560];
let w = |buf: &mut Vec<u8>, off: usize, v: u64| {
buf[off..off + 8].copy_from_slice(&v.to_ne_bytes());
};
w(&mut buf, 16, 1000); w(&mut buf, 24, 2000); w(&mut buf, 32, 3000); w(&mut buf, 40, 4000); w(&mut buf, 48, 5000); w(&mut buf, 56, 6000); w(&mut buf, 200, 1024); w(&mut buf, 208, 2048); w(&mut buf, 312, 7000); w(&mut buf, 320, 8000); w(&mut buf, 328, 9000); w(&mut buf, 336, 10_000); w(&mut buf, 352, 11_000); w(&mut buf, 360, 12_000); w(&mut buf, 400, 13_000); w(&mut buf, 408, 14_000); w(&mut buf, 416, 15_000); w(&mut buf, 424, 16_000); w(&mut buf, 432, 17_000); w(&mut buf, 440, 18_000); w(&mut buf, 448, 19_000); w(&mut buf, 456, 20_000); w(&mut buf, 464, 21_000); w(&mut buf, 472, 22_000); w(&mut buf, 480, 23_000); w(&mut buf, 488, 24_000); w(&mut buf, 496, 25_000); w(&mut buf, 504, 26_000); w(&mut buf, 512, 27_000); w(&mut buf, 520, 28_000); w(&mut buf, 528, 29_000); w(&mut buf, 536, 30_000); w(&mut buf, 544, 31_000); w(&mut buf, 552, 32_000);
let stats = parse_taskstats_payload(&buf).expect("full v17 payload OK");
let expected = DelayStats {
cpu_count: 1000,
cpu_delay_total_ns: 2000,
cpu_delay_max_ns: 17_000,
cpu_delay_min_ns: 18_000,
blkio_count: 3000,
blkio_delay_total_ns: 4000,
blkio_delay_max_ns: 19_000,
blkio_delay_min_ns: 20_000,
swapin_count: 5000,
swapin_delay_total_ns: 6000,
swapin_delay_max_ns: 21_000,
swapin_delay_min_ns: 22_000,
freepages_count: 7000,
freepages_delay_total_ns: 8000,
freepages_delay_max_ns: 23_000,
freepages_delay_min_ns: 24_000,
thrashing_count: 9000,
thrashing_delay_total_ns: 10_000,
thrashing_delay_max_ns: 25_000,
thrashing_delay_min_ns: 26_000,
compact_count: 11_000,
compact_delay_total_ns: 12_000,
compact_delay_max_ns: 27_000,
compact_delay_min_ns: 28_000,
wpcopy_count: 13_000,
wpcopy_delay_total_ns: 14_000,
wpcopy_delay_max_ns: 29_000,
wpcopy_delay_min_ns: 30_000,
irq_count: 15_000,
irq_delay_total_ns: 16_000,
irq_delay_max_ns: 31_000,
irq_delay_min_ns: 32_000,
hiwater_rss_bytes: 1024 * 1024,
hiwater_vm_bytes: 2048 * 1024,
};
assert_eq!(
stats, expected,
"v17 payload roundtrip mismatch — every field must read \
back the value its offset was written with",
);
}
#[test]
fn parse_reply_full_roundtrip_v17() {
let mut stats_payload = vec![0u8; 560];
let w = |buf: &mut Vec<u8>, off: usize, v: u64| {
buf[off..off + 8].copy_from_slice(&v.to_ne_bytes());
};
w(&mut stats_payload, 16, 1000); w(&mut stats_payload, 24, 2000); w(&mut stats_payload, 32, 3000); w(&mut stats_payload, 40, 4000); w(&mut stats_payload, 48, 5000); w(&mut stats_payload, 56, 6000); w(&mut stats_payload, 200, 1024); w(&mut stats_payload, 208, 2048); w(&mut stats_payload, 312, 7000);
w(&mut stats_payload, 320, 8000);
w(&mut stats_payload, 328, 9000);
w(&mut stats_payload, 336, 10_000);
w(&mut stats_payload, 352, 11_000);
w(&mut stats_payload, 360, 12_000);
w(&mut stats_payload, 400, 13_000);
w(&mut stats_payload, 408, 14_000);
w(&mut stats_payload, 416, 15_000);
w(&mut stats_payload, 424, 16_000);
w(&mut stats_payload, 432, 17_000);
w(&mut stats_payload, 440, 18_000);
w(&mut stats_payload, 448, 19_000);
w(&mut stats_payload, 456, 20_000);
w(&mut stats_payload, 464, 21_000);
w(&mut stats_payload, 472, 22_000);
w(&mut stats_payload, 480, 23_000);
w(&mut stats_payload, 488, 24_000);
w(&mut stats_payload, 496, 25_000);
w(&mut stats_payload, 504, 26_000);
w(&mut stats_payload, 512, 27_000);
w(&mut stats_payload, 520, 28_000);
w(&mut stats_payload, 528, 29_000);
w(&mut stats_payload, 536, 30_000);
w(&mut stats_payload, 544, 31_000);
w(&mut stats_payload, 552, 32_000);
let pid: u32 = 4242;
let stats_nla_len: u16 = 4 + stats_payload.len() as u16; let pid_nla_len: u16 = 4 + 4; let aggr_nla_len: u16 = 4 + pid_nla_len + stats_nla_len;
let mut payload = Vec::new();
payload.extend_from_slice(&aggr_nla_len.to_ne_bytes());
payload.extend_from_slice(&TASKSTATS_TYPE_AGGR_PID.to_ne_bytes());
payload.extend_from_slice(&pid_nla_len.to_ne_bytes());
payload.extend_from_slice(&TASKSTATS_TYPE_PID.to_ne_bytes());
payload.extend_from_slice(&pid.to_ne_bytes());
payload.extend_from_slice(&stats_nla_len.to_ne_bytes());
payload.extend_from_slice(&TASKSTATS_TYPE_STATS.to_ne_bytes());
payload.extend_from_slice(&stats_payload);
let buf = build_reply_buf(0x4242, &payload);
let stats = parse_reply(&buf, pid).expect("full v17 reply OK");
let expected = DelayStats {
cpu_count: 1000,
cpu_delay_total_ns: 2000,
cpu_delay_max_ns: 17_000,
cpu_delay_min_ns: 18_000,
blkio_count: 3000,
blkio_delay_total_ns: 4000,
blkio_delay_max_ns: 19_000,
blkio_delay_min_ns: 20_000,
swapin_count: 5000,
swapin_delay_total_ns: 6000,
swapin_delay_max_ns: 21_000,
swapin_delay_min_ns: 22_000,
freepages_count: 7000,
freepages_delay_total_ns: 8000,
freepages_delay_max_ns: 23_000,
freepages_delay_min_ns: 24_000,
thrashing_count: 9000,
thrashing_delay_total_ns: 10_000,
thrashing_delay_max_ns: 25_000,
thrashing_delay_min_ns: 26_000,
compact_count: 11_000,
compact_delay_total_ns: 12_000,
compact_delay_max_ns: 27_000,
compact_delay_min_ns: 28_000,
wpcopy_count: 13_000,
wpcopy_delay_total_ns: 14_000,
wpcopy_delay_max_ns: 29_000,
wpcopy_delay_min_ns: 30_000,
irq_count: 15_000,
irq_delay_total_ns: 16_000,
irq_delay_max_ns: 31_000,
irq_delay_min_ns: 32_000,
hiwater_rss_bytes: 1024 * 1024,
hiwater_vm_bytes: 2048 * 1024,
};
assert_eq!(
stats, expected,
"full reply roundtrip mismatch — parse_reply must reach \
the same DelayStats as the raw payload parser when given \
a kernel-shaped wrapper",
);
}
#[test]
fn parse_taskstats_payload_version_boundary_truncation() {
struct Boundary {
size: usize,
label: &'static str,
}
for b in [
Boundary {
size: 56,
label: "v1 partial (mid-cpu/blkio/swapin block)",
},
Boundary {
size: 312,
label: "pre-v8 (no freepages)",
},
Boundary {
size: 360,
label: "v11 partial (compact_count without compact_delay_total)",
},
Boundary {
size: 408,
label: "pre-v13 (no wpcopy_delay_total)",
},
Boundary {
size: 424,
label: "pre-v14 (no irq_delay_total)",
},
Boundary {
size: 432,
label: "pre-v15/v16 (no delay_max / delay_min block)",
},
] {
let mut buf = vec![0u8; b.size];
for off in (16..b.size).step_by(8) {
if off + 8 <= b.size {
let marker = (off as u64 / 8) * 1000 + 1;
buf[off..off + 8].copy_from_slice(&marker.to_ne_bytes());
}
}
let stats = parse_taskstats_payload(&buf)
.unwrap_or_else(|e| panic!("{}: parse failed: {e}", b.label));
let m = |off: usize| -> u64 {
if off + 8 <= b.size {
(off as u64 / 8) * 1000 + 1
} else {
0
}
};
assert_eq!(stats.cpu_count, m(16), "{}: cpu_count", b.label);
assert_eq!(
stats.cpu_delay_total_ns,
m(24),
"{}: cpu_delay_total_ns",
b.label
);
assert_eq!(stats.blkio_count, m(32), "{}: blkio_count", b.label);
assert_eq!(
stats.blkio_delay_total_ns,
m(40),
"{}: blkio_delay_total_ns",
b.label
);
assert_eq!(stats.swapin_count, m(48), "{}: swapin_count", b.label);
assert_eq!(
stats.swapin_delay_total_ns,
m(56),
"{}: swapin_delay_total_ns",
b.label
);
assert_eq!(
stats.freepages_count,
m(312),
"{}: freepages_count",
b.label
);
assert_eq!(
stats.freepages_delay_total_ns,
m(320),
"{}: freepages_delay_total_ns",
b.label
);
assert_eq!(
stats.thrashing_count,
m(328),
"{}: thrashing_count",
b.label
);
assert_eq!(
stats.thrashing_delay_total_ns,
m(336),
"{}: thrashing_delay_total_ns",
b.label
);
assert_eq!(stats.compact_count, m(352), "{}: compact_count", b.label);
assert_eq!(
stats.compact_delay_total_ns,
m(360),
"{}: compact_delay_total_ns",
b.label
);
assert_eq!(stats.wpcopy_count, m(400), "{}: wpcopy_count", b.label);
assert_eq!(
stats.wpcopy_delay_total_ns,
m(408),
"{}: wpcopy_delay_total_ns",
b.label
);
assert_eq!(stats.irq_count, m(416), "{}: irq_count", b.label);
assert_eq!(
stats.irq_delay_total_ns,
m(424),
"{}: irq_delay_total_ns",
b.label
);
assert_eq!(
stats.cpu_delay_max_ns,
m(432),
"{}: cpu_delay_max_ns",
b.label
);
assert_eq!(
stats.cpu_delay_min_ns,
m(440),
"{}: cpu_delay_min_ns",
b.label
);
assert_eq!(
stats.blkio_delay_max_ns,
m(448),
"{}: blkio_delay_max_ns",
b.label
);
assert_eq!(
stats.blkio_delay_min_ns,
m(456),
"{}: blkio_delay_min_ns",
b.label
);
assert_eq!(
stats.swapin_delay_max_ns,
m(464),
"{}: swapin_delay_max_ns",
b.label
);
assert_eq!(
stats.swapin_delay_min_ns,
m(472),
"{}: swapin_delay_min_ns",
b.label
);
assert_eq!(
stats.freepages_delay_max_ns,
m(480),
"{}: freepages_delay_max_ns",
b.label
);
assert_eq!(
stats.freepages_delay_min_ns,
m(488),
"{}: freepages_delay_min_ns",
b.label
);
assert_eq!(
stats.thrashing_delay_max_ns,
m(496),
"{}: thrashing_delay_max_ns",
b.label
);
assert_eq!(
stats.thrashing_delay_min_ns,
m(504),
"{}: thrashing_delay_min_ns",
b.label
);
assert_eq!(
stats.compact_delay_max_ns,
m(512),
"{}: compact_delay_max_ns",
b.label
);
assert_eq!(
stats.compact_delay_min_ns,
m(520),
"{}: compact_delay_min_ns",
b.label
);
assert_eq!(
stats.wpcopy_delay_max_ns,
m(528),
"{}: wpcopy_delay_max_ns",
b.label
);
assert_eq!(
stats.wpcopy_delay_min_ns,
m(536),
"{}: wpcopy_delay_min_ns",
b.label
);
assert_eq!(
stats.irq_delay_max_ns,
m(544),
"{}: irq_delay_max_ns",
b.label
);
assert_eq!(
stats.irq_delay_min_ns,
m(552),
"{}: irq_delay_min_ns",
b.label
);
assert_eq!(
stats.hiwater_rss_bytes,
m(200).saturating_mul(1024),
"{}: hiwater_rss_bytes",
b.label
);
assert_eq!(
stats.hiwater_vm_bytes,
m(208).saturating_mul(1024),
"{}: hiwater_vm_bytes",
b.label
);
}
}
#[test]
fn taskstats_summary_records_ok() {
let mut s = TaskstatsSummary::default();
let ok: io::Result<DelayStats> = Ok(DelayStats::default());
s.record_result(&ok);
s.record_result(&ok);
assert_eq!(s.ok_count, 2);
assert_eq!(s.eperm_count, 0);
assert_eq!(s.esrch_count, 0);
assert_eq!(s.other_err_count, 0);
}
#[test]
fn taskstats_summary_records_eperm_from_raw_os_error() {
let mut s = TaskstatsSummary::default();
let err: io::Result<DelayStats> = Err(io::Error::from_raw_os_error(1));
s.record_result(&err);
assert_eq!(s.ok_count, 0);
assert_eq!(s.eperm_count, 1);
assert_eq!(s.esrch_count, 0);
assert_eq!(s.other_err_count, 0);
}
#[test]
fn taskstats_summary_records_esrch_from_raw_os_error() {
let mut s = TaskstatsSummary::default();
let err: io::Result<DelayStats> = Err(io::Error::from_raw_os_error(3));
s.record_result(&err);
assert_eq!(s.ok_count, 0);
assert_eq!(s.eperm_count, 0);
assert_eq!(s.esrch_count, 1);
assert_eq!(s.other_err_count, 0);
}
#[test]
fn taskstats_summary_records_errno_from_text_message() {
let mut s = TaskstatsSummary::default();
let eperm: io::Result<DelayStats> =
Err(io::Error::other("kernel returned NLMSG_ERROR errno=1"));
let esrch: io::Result<DelayStats> =
Err(io::Error::other("kernel returned NLMSG_ERROR errno=3"));
s.record_result(&eperm);
s.record_result(&esrch);
assert_eq!(s.ok_count, 0);
assert_eq!(s.eperm_count, 1);
assert_eq!(s.esrch_count, 1);
assert_eq!(s.other_err_count, 0);
}
#[test]
fn taskstats_summary_records_other_err_for_unrecognized_shapes() {
let mut s = TaskstatsSummary::default();
let einval: io::Result<DelayStats> = Err(io::Error::from_raw_os_error(22));
s.record_result(&einval);
let parse_einval: io::Result<DelayStats> =
Err(io::Error::other("kernel returned NLMSG_ERROR errno=22"));
s.record_result(&parse_einval);
let bare: io::Result<DelayStats> = Err(io::Error::other("AGGR_PID missing in reply"));
s.record_result(&bare);
assert_eq!(s.ok_count, 0);
assert_eq!(s.eperm_count, 0);
assert_eq!(s.esrch_count, 0);
assert_eq!(s.other_err_count, 3);
}
#[test]
fn classify_errno_prefers_raw_os_error_over_text() {
let e = io::Error::other("decoy text saying errno=99 should not win");
let real = io::Error::from_raw_os_error(1);
assert_eq!(classify_errno(&real), Some(1));
assert_eq!(classify_errno(&e), Some(99));
}
}