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}