Skip to main content

gander_core/linux/
delayacct.rs

1//! Delay accounting via `TASKSTATS` generic netlink.
2//!
3//! The kernel accounts, per task, how long it spent *waiting* rather than
4//! running: waiting for a CPU while runnable, waiting for synchronous block I/O,
5//! and waiting for a page to be swapped back in. Those three numbers are what
6//! separate "this process is working" from "this process is stuck", and nothing
7//! in `/proc` exposes them.
8//!
9//! # Why raw netlink
10//!
11//! htop reaches the same data through `libnl3`, which it `dlopen`s and uses
12//! through function pointers. That is a runtime dependency on a library that may
13//! not be installed, loaded lazily so the failure is a `dlopen` that returns
14//! `NULL`. The protocol is four messages; speaking it directly removes the
15//! dependency and the failure mode, and is the reason `Cargo.toml` still lists
16//! nothing but `libc`.
17//!
18//! # The two ways this fails, and why they are not errors
19//!
20//! * **`EPERM`** — `TASKSTATS_CMD_GET` requires `CAP_NET_ADMIN`. An unprivileged
21//!   `gander` gets `EPERM` on every request, which is the normal case, not a
22//!   fault. `probe()` records it once and no further requests are sent.
23//! * **`ENOENT` on the family** — `CONFIG_TASKSTATS` is not compiled in. The
24//!   generic-netlink family does not exist, and neither does the column.
25//!
26//! Both end up as `None` on every row, which renders `-`: "not measured", which
27//! is the truth. A row must never show `0.0%` for a figure nobody read.
28//!
29//! # Units
30//!
31//! Every `*_delay_total` is **nanoseconds**, and `ac_etime` is **microseconds**.
32//! The percentage htop prints is `Δdelay / Δ(ac_etime × 1000)`, i.e. the share of
33//! the task's wall-clock life spent waiting. It is a lifetime average, not an
34//! instantaneous rate, because the kernel exposes no interval counter — see
35//! `docs/ROADMAP.md`.
36
37#[cfg(target_os = "linux")]
38use std::io;
39#[cfg(target_os = "linux")]
40use std::mem;
41
42/// Generic netlink protocol number (`NETLINK_GENERIC`).
43#[cfg(target_os = "linux")]
44const NETLINK_GENERIC: libc::c_int = 16;
45/// `genl_ctrl` family id, fixed by the kernel.
46#[cfg(target_os = "linux")]
47const GENL_ID_CTRL: u16 = 0x10;
48#[cfg(target_os = "linux")]
49const CTRL_CMD_GETFAMILY: u8 = 3;
50#[cfg(target_os = "linux")]
51const CTRL_ATTR_FAMILY_ID: u16 = 1;
52#[cfg(target_os = "linux")]
53const CTRL_ATTR_FAMILY_NAME: u16 = 2;
54
55#[cfg(target_os = "linux")]
56const TASKSTATS_CMD_GET: u8 = 1;
57#[cfg(target_os = "linux")]
58const TASKSTATS_CMD_ATTR_PID: u16 = 1;
59// The attribute enum, from `include/uapi/linux/taskstats.h`. It starts at
60// `TASKSTATS_TYPE_UNSPEC = 0`, which is what makes these numbers what they are:
61//
62//   UNSPEC=0, PID=1, TGID=2, STATS=3, AGGR_PID=4, AGGR_TGID=5, NULL=6
63//
64// An earlier version of this file had `AGGR_PID = 1`, counting from `PID` instead
65// of from `UNSPEC`. `STATS = 3` happened to be right, which is why the parser
66// looked plausible and returned `None` on every real kernel.
67/// `TASKSTATS_TYPE_STATS`: the `struct taskstats` itself.
68#[cfg(target_os = "linux")]
69const TASKSTATS_TYPE_STATS: u16 = 3;
70/// `TASKSTATS_TYPE_AGGR_PID`: nests a pid and a stats blob (a single task).
71#[cfg(target_os = "linux")]
72const TASKSTATS_TYPE_AGGR_PID: u16 = 4;
73/// `TASKSTATS_TYPE_AGGR_TGID`: nests a tgid and a stats blob (a thread group).
74///
75/// Which one comes back depends on the pid that was asked for: a thread-group
76/// leader is answered with `AGGR_TGID`, one of its threads with `AGGR_PID`.
77#[cfg(target_os = "linux")]
78const TASKSTATS_TYPE_AGGR_TGID: u16 = 5;
79
80#[cfg(target_os = "linux")]
81const NLMSG_ERROR: u16 = 2;
82#[cfg(target_os = "linux")]
83const NLM_F_REQUEST: u16 = 1;
84
85/// Byte offsets into `struct taskstats`, from `include/uapi/linux/taskstats.h`.
86///
87/// The struct is a kernel ABI and is append-only, so these offsets are stable for
88/// every version that has them; `version` is checked before they are read.
89/// `ac_etime` sits at 72 because `ac_comm[32]` is followed by a byte, three bytes
90/// of padding, four `u32`s, and then a `u64` that is 8-aligned.
91mod off {
92    pub const VERSION: usize = 0;
93    pub const CPU_DELAY_TOTAL: usize = 16;
94    pub const BLKIO_DELAY_TOTAL: usize = 24;
95    pub const SWAPIN_DELAY_TOTAL: usize = 32;
96    pub const AC_ETIME: usize = 72;
97}
98
99/// The smallest `struct taskstats` that still contains every field read here.
100///
101/// `ac_etime` ends at byte 80. A reply shorter than that is from a kernel older
102/// than the fields, and is rejected rather than read past.
103const MIN_STATS_LEN: usize = off::AC_ETIME + 8;
104
105/// Cumulative delay counters for one task, all in nanoseconds, plus the task's
106/// elapsed wall-clock life in microseconds.
107///
108/// Cumulative rather than a percentage because the caller owns the previous
109/// sample: turning two samples into a percentage needs the interval, and a
110/// `DelayStats` that stored a percentage would have to be read twice per tick.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub struct DelayStats {
113    /// Time spent runnable but not on a CPU.
114    pub cpu_delay_ns: u64,
115    /// Time spent waiting for synchronous block I/O.
116    pub blkio_delay_ns: u64,
117    /// Time spent waiting for a page to be swapped in.
118    pub swapin_delay_ns: u64,
119    /// The task's elapsed wall-clock life, in microseconds.
120    pub elapsed_us: u64,
121}
122
123impl DelayStats {
124    /// Total time spent waiting, in nanoseconds.
125    pub fn total_delay_ns(self) -> u64 {
126        self.cpu_delay_ns
127            .saturating_add(self.blkio_delay_ns)
128            .saturating_add(self.swapin_delay_ns)
129    }
130
131    /// Share of the task's life spent waiting on a CPU, in percent.
132    ///
133    /// `None` when the elapsed time is zero, which is the state of a task that
134    /// has not been sampled twice yet; a percentage of nothing is not zero.
135    pub fn cpu_percent(self) -> Option<f32> {
136        self.percent_of(self.cpu_delay_ns)
137    }
138
139    pub fn blkio_percent(self) -> Option<f32> {
140        self.percent_of(self.blkio_delay_ns)
141    }
142
143    pub fn swapin_percent(self) -> Option<f32> {
144        self.percent_of(self.swapin_delay_ns)
145    }
146
147    fn percent_of(self, delay_ns: u64) -> Option<f32> {
148        let life_ns = self.elapsed_us.checked_mul(1000)?;
149        if life_ns == 0 {
150            return None;
151        }
152        let pct = delay_ns as f64 / life_ns as f64 * 100.0;
153        // The counters are updated non-atomically with respect to `ac_etime`
154        // (`cpu_delay_total` explicitly so, per the kernel header), so a delay can
155        // momentarily exceed the elapsed time. Clamping is the honest fix: the
156        // true value is "essentially all of it", and a value above 100 would sort
157        // above a process that really is stalled all the time.
158        Some(pct.clamp(0.0, 100.0) as f32)
159    }
160}
161
162/// A connected generic-netlink socket bound to the `TASKSTATS` family.
163///
164/// Linux-only: the generic-netlink ABI has no equivalent on the other unixes this
165/// crate compiles for, so the type is absent there rather than present and
166/// unusable. The parsing half of this module stays portable so its tests run
167/// everywhere.
168#[cfg(target_os = "linux")]
169pub struct DelayAcct {
170    fd: libc::c_int,
171    family: u16,
172    seq: u32,
173    /// Set once the kernel has said `EPERM`, after which no request is sent.
174    denied: bool,
175}
176
177#[cfg(target_os = "linux")]
178impl DelayAcct {
179    /// Open a socket and resolve the `TASKSTATS` family.
180    ///
181    /// Returns `None` when the family does not exist (`CONFIG_TASKSTATS=n`) or the
182    /// socket cannot be created. It does *not* fail when the caller lacks
183    /// `CAP_NET_ADMIN`: that only shows up on the first `read`, and is recorded
184    /// there.
185    pub fn open() -> Option<Self> {
186        // SAFETY: `socket(2)` with three constant arguments; returns a fd or -1.
187        let fd = unsafe {
188            libc::socket(
189                libc::AF_NETLINK,
190                libc::SOCK_RAW | libc::SOCK_CLOEXEC,
191                NETLINK_GENERIC,
192            )
193        };
194        if fd < 0 {
195            return None;
196        }
197        let mut acct = Self {
198            fd,
199            family: 0,
200            seq: 1,
201            denied: false,
202        };
203        // SAFETY: `sockaddr_nl` is plain old data; binding a netlink socket needs
204        // no other precondition.
205        // Zeroed and then written through a pointer, rather than a struct literal:
206        // `libc::sockaddr_nl::nl_pad` is a private `Padding<u16>`, so the value
207        // cannot be built by naming its fields. Zeroing is also exactly what the
208        // kernel expects — `nl_pid = 0` means "let the kernel assign one" and
209        // `nl_groups = 0` subscribes to nothing.
210        let mut addr: libc::sockaddr_nl = unsafe { mem::zeroed() };
211        addr.nl_family = libc::AF_NETLINK as u16;
212        // SAFETY: `addr` is a live, correctly-typed value for the duration of the
213        // call, and its size is passed alongside it.
214        let rc = unsafe {
215            libc::bind(
216                fd,
217                &addr as *const libc::sockaddr_nl as *const libc::sockaddr,
218                mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
219            )
220        };
221        if rc < 0 {
222            acct.close();
223            return None;
224        }
225        // A receive timeout so a kernel that never answers cannot hang a scan.
226        // SAFETY: `setsockopt(2)` on our own fd with a correctly-sized value.
227        let tv = libc::timeval {
228            tv_sec: 0,
229            tv_usec: 200_000,
230        };
231        unsafe {
232            libc::setsockopt(
233                fd,
234                libc::SOL_SOCKET,
235                libc::SO_RCVTIMEO,
236                &tv as *const libc::timeval as *const libc::c_void,
237                mem::size_of::<libc::timeval>() as libc::socklen_t,
238            );
239        }
240        acct.family = acct.resolve_family()?;
241        Some(acct)
242    }
243
244    /// Whether the family resolved but the kernel refuses to answer.
245    pub fn is_denied(&self) -> bool {
246        self.denied
247    }
248
249    fn close(&mut self) {
250        if self.fd >= 0 {
251            // SAFETY: our own fd, closed exactly once (set to -1 immediately).
252            unsafe { libc::close(self.fd) };
253            self.fd = -1;
254        }
255    }
256
257    /// Ask `genl_ctrl` for the numeric id of the `TASKSTATS` family.
258    fn resolve_family(&mut self) -> Option<u16> {
259        let mut msg = Msg::new();
260        msg.put_genl_header(GENL_ID_CTRL, CTRL_CMD_GETFAMILY);
261        msg.put_attr_str(CTRL_ATTR_FAMILY_NAME, b"TASKSTATS");
262        let seq = self.next_seq();
263        let reply = self.transact(&msg.finish(seq, GENL_ID_CTRL), seq)?;
264        // Walk the attributes of the reply looking for the family id.
265        let attrs = Attrs::new(&reply, NLMSG_HDR_LEN + GENL_HDR_LEN, reply.len());
266        for (kind, value) in attrs {
267            if kind == CTRL_ATTR_FAMILY_ID && value.len() >= 2 {
268                return Some(u16::from_ne_bytes([value[0], value[1]]));
269            }
270        }
271        None
272    }
273
274    /// Read the delay counters for `pid`.
275    ///
276    /// `None` means "not measured": the process is gone, the kernel refused, or
277    /// the reply did not contain a stats blob. Callers must render that as `-`,
278    /// never as zero.
279    pub fn read(&mut self, pid: crate::Pid) -> Option<DelayStats> {
280        if self.denied || self.fd < 0 || self.family == 0 {
281            return None;
282        }
283        let mut msg = Msg::new();
284        msg.put_genl_header(self.family, TASKSTATS_CMD_GET);
285        msg.put_attr_u32(TASKSTATS_CMD_ATTR_PID, pid);
286        let seq = self.next_seq();
287        let request = msg.finish(seq, self.family);
288        let reply = self.transact(&request, seq)?;
289
290        let attrs = Attrs::new(&reply, NLMSG_HDR_LEN + GENL_HDR_LEN, reply.len());
291        for (kind, value) in attrs {
292            // Both aggregating shapes nest the stats blob, and which arrives
293            // depends on whether the pid is a thread-group leader. Some kernels
294            // answer with the blob at the top level instead, so all three are
295            // handled.
296            if kind == TASKSTATS_TYPE_AGGR_PID || kind == TASKSTATS_TYPE_AGGR_TGID {
297                let inner = Attrs::new(value, 0, value.len());
298                for (inner_kind, inner_value) in inner {
299                    if inner_kind == TASKSTATS_TYPE_STATS {
300                        return parse_taskstats(inner_value);
301                    }
302                }
303            } else if kind == TASKSTATS_TYPE_STATS {
304                return parse_taskstats(value);
305            }
306        }
307        None
308    }
309
310    fn next_seq(&mut self) -> u32 {
311        self.seq = self.seq.wrapping_add(1);
312        self.seq
313    }
314
315    /// Send one request and return the whole reply datagram.
316    fn transact(&mut self, request: &[u8], seq: u32) -> Option<Vec<u8>> {
317        // SAFETY: `request` is a live slice; the length passed is its length.
318        let sent = unsafe {
319            libc::send(
320                self.fd,
321                request.as_ptr() as *const libc::c_void,
322                request.len(),
323                0,
324            )
325        };
326        if sent < 0 {
327            return None;
328        }
329        // 64 KiB is far above the largest `taskstats` reply (a few hundred bytes)
330        // and far below any kernel limit; one datagram per `recv` is guaranteed by
331        // `SOCK_RAW` on netlink.
332        let mut buf = vec![0u8; 64 * 1024];
333        loop {
334            // SAFETY: `buf` is a live, uniquely-borrowed allocation of its own
335            // stated length.
336            let n = unsafe {
337                libc::recv(
338                    self.fd,
339                    buf.as_mut_ptr() as *mut libc::c_void,
340                    buf.len(),
341                    0,
342                )
343            };
344            if n < 0 {
345                let err = io::Error::last_os_error();
346                // `EAGAIN` here is the receive timeout: the kernel never answered.
347                if err.raw_os_error() == Some(libc::EAGAIN) {
348                    return None;
349                }
350                if err.raw_os_error() == Some(libc::EPERM) {
351                    // No `CAP_NET_ADMIN`. Stop asking: every later request would
352                    // fail identically, and one syscall per process per tick is
353                    // not free.
354                    self.denied = true;
355                }
356                return None;
357            }
358            let n = n as usize;
359            buf.truncate(n);
360            if n < NLMSG_HDR_LEN {
361                return None;
362            }
363            let kind = u16::from_ne_bytes([buf[4], buf[5]]);
364            let reply_seq = u32::from_ne_bytes([buf[8], buf[9], buf[10], buf[11]]);
365            if kind == NLMSG_ERROR {
366                // A zero error code is an ACK for a request that expected none.
367                let code = if n >= NLMSG_HDR_LEN + 4 {
368                    i32::from_ne_bytes([buf[16], buf[17], buf[18], buf[19]])
369                } else {
370                    0
371                };
372                if code == -libc::EPERM {
373                    self.denied = true;
374                }
375                return None;
376            }
377            if reply_seq != seq {
378                // A stale datagram from an earlier request; keep waiting.
379                continue;
380            }
381            return Some(buf);
382        }
383    }
384}
385
386#[cfg(target_os = "linux")]
387impl Drop for DelayAcct {
388    fn drop(&mut self) {
389        self.close();
390    }
391}
392
393/// Parse a `struct taskstats` blob into the four fields that are read.
394///
395/// Split out from the socket code so it can be unit-tested against a synthetic
396/// buffer: the offsets are a kernel ABI and a wrong one would produce a plausible
397/// but wrong percentage, which no live test would catch on an idle machine.
398pub fn parse_taskstats(buf: &[u8]) -> Option<DelayStats> {
399    if buf.len() < MIN_STATS_LEN {
400        return None;
401    }
402    let version = u16::from_ne_bytes([buf[off::VERSION], buf[off::VERSION + 1]]);
403    // Version 1 is the first release of the struct and already contains every
404    // field below; anything lower is not a `taskstats` reply at all.
405    if version == 0 || version > 64 {
406        return None;
407    }
408    let u64_at = |offset: usize| {
409        let mut b = [0u8; 8];
410        b.copy_from_slice(&buf[offset..offset + 8]);
411        u64::from_ne_bytes(b)
412    };
413    Some(DelayStats {
414        cpu_delay_ns: u64_at(off::CPU_DELAY_TOTAL),
415        blkio_delay_ns: u64_at(off::BLKIO_DELAY_TOTAL),
416        swapin_delay_ns: u64_at(off::SWAPIN_DELAY_TOTAL),
417        elapsed_us: u64_at(off::AC_ETIME),
418    })
419}
420
421/// Netlink message header: length, type, flags, sequence, port id.
422#[cfg(target_os = "linux")]
423const NLMSG_HDR_LEN: usize = 16;
424/// Generic netlink header: command, version, reserved.
425#[cfg(target_os = "linux")]
426const GENL_HDR_LEN: usize = 4;
427
428/// A growing netlink message buffer.
429#[cfg(target_os = "linux")]
430struct Msg {
431    buf: Vec<u8>,
432}
433
434#[cfg(target_os = "linux")]
435impl Msg {
436    fn new() -> Self {
437        Self {
438            buf: Vec::with_capacity(128),
439        }
440    }
441
442    /// Append the generic-netlink header. The netlink header is filled in by
443    /// `finish`, which knows the final length.
444    fn put_genl_header(&mut self, _family: u16, cmd: u8) {
445        // Placeholder for the netlink header; `finish` overwrites it.
446        self.buf.extend_from_slice(&[0u8; NLMSG_HDR_LEN]);
447        self.buf.extend_from_slice(&[cmd, 1, 0, 0]);
448    }
449
450    fn put_attr(&mut self, kind: u16, value: &[u8]) {
451        let len = (4 + value.len()) as u16;
452        self.buf.extend_from_slice(&len.to_ne_bytes());
453        self.buf.extend_from_slice(&kind.to_ne_bytes());
454        self.buf.extend_from_slice(value);
455        // Netlink attributes are 4-byte aligned. `is_multiple_of` rather than
456        // `% 4 != 0`: clippy's `manual_is_multiple_of` is in the workspace's lint
457        // set, and CI runs a newer toolchain than the MSRV.
458        while !self.buf.len().is_multiple_of(4) {
459            self.buf.push(0);
460        }
461    }
462
463    fn put_attr_str(&mut self, kind: u16, value: &[u8]) {
464        // String attributes include their NUL terminator in the length.
465        let mut v = value.to_vec();
466        v.push(0);
467        self.put_attr(kind, &v);
468    }
469
470    fn put_attr_u32(&mut self, kind: u16, value: u32) {
471        self.put_attr(kind, &value.to_ne_bytes());
472    }
473
474    /// Fill in the netlink header and return the datagram.
475    fn finish(mut self, seq: u32, kind: u16) -> Vec<u8> {
476        let len = self.buf.len() as u32;
477        self.buf[0..4].copy_from_slice(&len.to_ne_bytes());
478        self.buf[4..6].copy_from_slice(&kind.to_ne_bytes());
479        self.buf[6..8].copy_from_slice(&NLM_F_REQUEST.to_ne_bytes());
480        self.buf[8..12].copy_from_slice(&seq.to_ne_bytes());
481        self.buf[12..16].copy_from_slice(&0u32.to_ne_bytes());
482        self.buf
483    }
484}
485
486/// Iterator over a run of 4-byte-aligned netlink attributes.
487#[cfg(target_os = "linux")]
488struct Attrs<'a> {
489    buf: &'a [u8],
490    pos: usize,
491    end: usize,
492}
493
494#[cfg(target_os = "linux")]
495impl<'a> Attrs<'a> {
496    fn new(buf: &'a [u8], start: usize, end: usize) -> Self {
497        Self {
498            buf,
499            pos: start,
500            end: end.min(buf.len()),
501        }
502    }
503}
504
505#[cfg(target_os = "linux")]
506impl<'a> Iterator for Attrs<'a> {
507    type Item = (u16, &'a [u8]);
508
509    fn next(&mut self) -> Option<Self::Item> {
510        if self.pos + 4 > self.end {
511            return None;
512        }
513        let len = u16::from_ne_bytes([self.buf[self.pos], self.buf[self.pos + 1]]) as usize;
514        let kind = u16::from_ne_bytes([self.buf[self.pos + 2], self.buf[self.pos + 3]]);
515        // A zero or oversized length would loop forever or read out of bounds.
516        if len < 4 || self.pos + len > self.end {
517            return None;
518        }
519        let value = &self.buf[self.pos + 4..self.pos + len];
520        // Advance past the value and its padding.
521        self.pos += (len + 3) & !3;
522        Some((kind, value))
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    /// Build a synthetic `struct taskstats` with the given counters.
531    fn stats_blob(version: u16, cpu: u64, blkio: u64, swapin: u64, etime: u64) -> Vec<u8> {
532        let mut buf = vec![0u8; MIN_STATS_LEN];
533        buf[off::VERSION..off::VERSION + 2].copy_from_slice(&version.to_ne_bytes());
534        buf[off::CPU_DELAY_TOTAL..off::CPU_DELAY_TOTAL + 8].copy_from_slice(&cpu.to_ne_bytes());
535        buf[off::BLKIO_DELAY_TOTAL..off::BLKIO_DELAY_TOTAL + 8]
536            .copy_from_slice(&blkio.to_ne_bytes());
537        buf[off::SWAPIN_DELAY_TOTAL..off::SWAPIN_DELAY_TOTAL + 8]
538            .copy_from_slice(&swapin.to_ne_bytes());
539        buf[off::AC_ETIME..off::AC_ETIME + 8].copy_from_slice(&etime.to_ne_bytes());
540        buf
541    }
542
543    #[test]
544    fn parses_the_four_fields_at_their_kernel_offsets() {
545        let blob = stats_blob(17, 1_000, 2_000, 3_000, 4_000_000);
546        let s = parse_taskstats(&blob).expect("valid blob");
547        assert_eq!(s.cpu_delay_ns, 1_000);
548        assert_eq!(s.blkio_delay_ns, 2_000);
549        assert_eq!(s.swapin_delay_ns, 3_000);
550        assert_eq!(s.elapsed_us, 4_000_000);
551        assert_eq!(s.total_delay_ns(), 6_000);
552    }
553
554    #[test]
555    fn a_short_blob_is_rejected_rather_than_read_past() {
556        for len in [0, 8, 16, MIN_STATS_LEN - 1] {
557            assert!(parse_taskstats(&vec![0u8; len]).is_none(), "len {len}");
558        }
559        // A zeroed blob at exactly the minimum length is long enough; it is
560        // rejected by the version check instead, which the next test covers.
561    }
562
563    #[test]
564    fn a_zero_version_is_not_a_taskstats_reply() {
565        let blob = stats_blob(0, 1, 1, 1, 1);
566        assert!(parse_taskstats(&blob).is_none());
567    }
568
569    /// The percentage is of elapsed *wall clock*, not of delay: 50 ms of a 100 ms
570    /// life is 50 %.
571    #[test]
572    fn percentages_are_delay_over_lifetime() {
573        let s = DelayStats {
574            cpu_delay_ns: 50_000_000,
575            blkio_delay_ns: 10_000_000,
576            swapin_delay_ns: 0,
577            elapsed_us: 100_000, // 100 ms
578        };
579        assert_eq!(s.cpu_percent(), Some(50.0));
580        assert_eq!(s.blkio_percent(), Some(10.0));
581        assert_eq!(s.swapin_percent(), Some(0.0));
582    }
583
584    /// A process that has not been alive long enough to sample has no percentage;
585    /// zero would be a claim about its behaviour.
586    #[test]
587    fn zero_elapsed_time_has_no_percentage() {
588        let s = DelayStats {
589            cpu_delay_ns: 0,
590            blkio_delay_ns: 0,
591            swapin_delay_ns: 0,
592            elapsed_us: 0,
593        };
594        assert_eq!(s.cpu_percent(), None);
595        assert_eq!(s.blkio_percent(), None);
596        assert_eq!(s.swapin_percent(), None);
597    }
598
599    /// `cpu_delay_total` is documented as updated non-atomically with respect to
600    /// `ac_etime`, so a sample can show more delay than elapsed time. Clamping
601    /// keeps a nonsense value from sorting above a genuinely stalled process.
602    #[test]
603    fn delay_above_lifetime_is_clamped_to_100() {
604        let s = DelayStats {
605            cpu_delay_ns: 500_000_000,
606            blkio_delay_ns: 0,
607            swapin_delay_ns: 0,
608            elapsed_us: 1_000, // 1 ms
609        };
610        assert_eq!(s.cpu_percent(), Some(100.0));
611    }
612
613    /// Wire-format tests need the `Msg`/`Attrs` builders, which are Linux-only
614    /// because they exist to feed a Linux socket.
615    #[cfg(target_os = "linux")]
616    #[test]
617    fn attributes_are_walked_with_padding() {
618        // Two attributes: a 4-byte value (already aligned) and a 5-byte string.
619        let mut buf = Vec::new();
620        buf.extend_from_slice(&8u16.to_ne_bytes());
621        buf.extend_from_slice(&1u16.to_ne_bytes());
622        buf.extend_from_slice(&7u32.to_ne_bytes());
623        let mut msg = Msg::new();
624        msg.put_genl_header(1, 1);
625        msg.put_attr(2, b"abcd");
626        msg.put_attr_str(3, b"xy"); // 4 + 3 = 7 bytes, padded to 8
627        let full = msg.finish(1, 1);
628        let mut attrs = Attrs::new(&full, NLMSG_HDR_LEN + GENL_HDR_LEN, full.len());
629        let (kind, value) = attrs.next().expect("first");
630        assert_eq!(kind, 2);
631        assert_eq!(value, b"abcd");
632        let (kind, value) = attrs.next().expect("second");
633        assert_eq!(kind, 3);
634        assert_eq!(value, b"xy\0");
635        assert!(attrs.next().is_none());
636
637        // The hand-built buffer is only used to prove the lengths agree.
638        assert_eq!(buf.len(), 8);
639    }
640
641    #[cfg(target_os = "linux")]
642    #[test]
643    fn a_truncated_attribute_ends_the_walk_instead_of_panicking() {
644        let mut msg = Msg::new();
645        msg.put_genl_header(1, 1);
646        msg.put_attr(2, b"abcd");
647        let mut full = msg.finish(1, 1);
648        full.truncate(full.len() - 2); // cut into the last attribute
649        let mut attrs = Attrs::new(&full, NLMSG_HDR_LEN + GENL_HDR_LEN, full.len());
650        // The header claims 8 bytes but only 6 remain.
651        assert!(attrs.next().is_none());
652    }
653
654    #[cfg(target_os = "linux")]
655    #[test]
656    fn finish_writes_a_consistent_netlink_header() {
657        let mut msg = Msg::new();
658        msg.put_genl_header(0x22, 7);
659        let dgram = msg.finish(99, 0x22);
660        let len = u32::from_ne_bytes([dgram[0], dgram[1], dgram[2], dgram[3]]) as usize;
661        assert_eq!(len, dgram.len());
662        assert_eq!(u16::from_ne_bytes([dgram[4], dgram[5]]), 0x22);
663        assert_eq!(u16::from_ne_bytes([dgram[6], dgram[7]]), NLM_F_REQUEST);
664        assert_eq!(
665            u32::from_ne_bytes([dgram[8], dgram[9], dgram[10], dgram[11]]),
666            99
667        );
668        assert_eq!(dgram[NLMSG_HDR_LEN], 7, "genl command");
669    }
670}