1use crate::row::{CommBuf, Row, RowFlags};
8use crate::system::{
9 CpuTimes, DiskCounters, MemInfo, NetCounters, PsiLine, PsiResource, SwapInfo, ZramDevice,
10};
11
12pub fn parse_stat(buf: &[u8]) -> Option<(u32, CommBuf, StatFields)> {
17 let l = memchr::memchr(b'(', buf)?;
18 let r = memchr::memrchr(b')', buf)?;
19 if r <= l {
20 return None;
21 }
22
23 let pid_part = &buf[..l];
24 let pid: u32 = std::str::from_utf8(pid_part).ok()?.trim().parse().ok()?;
25
26 let mut comm = CommBuf::new();
27 let comm_raw = String::from_utf8_lossy(&buf[l + 1..r]);
29 comm.try_push_str(&comm_raw).ok()?;
30
31 let rest = buf.get(r + 1..)?;
32 let mut it = rest
33 .split(|b| *b == b' ' || *b == b'\n')
34 .filter(|t| !t.is_empty());
35
36 let mut f: [&[u8]; 22] = [b""; 22];
37 for slot in f.iter_mut() {
38 *slot = it.next()?;
39 }
40
41 let num = |s: &[u8]| -> Option<u64> {
42 let mut v: u64 = 0;
44 for &c in s {
45 if !c.is_ascii_digit() {
46 return None;
47 }
48 v = v.wrapping_mul(10).wrapping_add((c - b'0') as u64);
49 }
50 Some(v)
51 };
52 let signed = |s: &[u8]| -> Option<i64> {
53 let (neg, s) = match s.first() {
54 Some(b'-') => (true, &s[1..]),
55 Some(b'+') => (false, &s[1..]),
56 _ => (false, s),
57 };
58 let v = num(s)? as i64;
59 Some(if neg { -v } else { v })
60 };
61
62 let fields = StatFields {
63 state: *f[0].first()?,
64 ppid: num(f[1])? as u32,
65 pgrp: num(f[2])? as u32,
66 session: num(f[3])? as u32,
67 tty_nr: signed(f[4])?,
68 tpgid: signed(f[5])?,
69 min_flt: num(f[7])?,
70 maj_flt: num(f[9])?,
71 utime: num(f[11])?,
72 stime: num(f[12])?,
73 priority: signed(f[15])? as i8,
74 nice: signed(f[16])? as i8,
75 nlwp: num(f[17])? as i32,
76 starttime: num(f[19])?,
77 vsize: num(f[20])?,
78 rss_pages: num(f[21])?,
79 };
80
81 Some((pid, comm, fields))
82}
83
84#[derive(Copy, Clone, Debug, PartialEq, Eq)]
85pub struct StatFields {
86 pub state: u8,
87 pub ppid: u32,
88 pub pgrp: u32,
89 pub session: u32,
90 pub tty_nr: i64,
91 pub tpgid: i64,
96 pub min_flt: u64,
97 pub maj_flt: u64,
98 pub utime: u64,
99 pub stime: u64,
100 pub priority: i8,
101 pub nice: i8,
102 pub nlwp: i32,
103 pub starttime: u64,
104 pub vsize: u64,
105 pub rss_pages: u64,
106}
107
108impl StatFields {
109 pub fn cpu_ticks(&self) -> u64 {
110 self.utime.saturating_add(self.stime)
111 }
112}
113
114pub fn fill_row(row: &mut Row, st: &StatFields, page_size: u64) {
116 row.state = st.state;
117 row.ppid = st.ppid;
118 row.pgrp = st.pgrp;
119 row.session = st.session;
120 row.tty_nr = st.tty_nr;
121 row.tpgid = st.tpgid;
122 row.min_flt = st.min_flt;
123 row.maj_flt = st.maj_flt;
124 row.priority = st.priority;
125 row.nice = st.nice;
126 row.nlwp = st.nlwp;
127 row.starttime = st.starttime;
128 row.virt = st.vsize;
129 row.res = st.rss_pages.saturating_mul(page_size);
130 row.time_ticks = st.cpu_ticks();
131}
132
133pub fn parse_status_uid(buf: &[u8]) -> Option<u32> {
138 for line in buf.split(|b| *b == b'\n') {
139 if let Some(rest) = line.strip_prefix(b"Uid:") {
140 let mut it = rest.split(|b| *b == b'\t' || *b == b' ');
141 it.find(|t| !t.is_empty())?; let effective = it.find(|t| !t.is_empty())?;
143 return std::str::from_utf8(effective).ok()?.trim().parse().ok();
144 }
145 }
146 None
147}
148
149pub fn parse_meminfo(buf: &[u8], mem: &mut MemInfo, swap: &mut SwapInfo) {
151 for line in buf.split(|b| *b == b'\n') {
152 let Some(colon) = memchr::memchr(b':', line) else {
153 continue;
154 };
155 let key = &line[..colon];
156 let rest = &line[colon + 1..];
157 let Some(val) = first_number(rest) else {
158 continue;
159 };
160 let bytes = val.saturating_mul(1024);
162 match key {
163 b"MemTotal" => mem.total = bytes,
164 b"MemFree" => mem.free = bytes,
165 b"MemAvailable" => mem.available = bytes,
166 b"Buffers" => mem.buffers = bytes,
167 b"Cached" => mem.cached = bytes,
168 b"SReclaimable" => mem.sreclaimable = bytes,
169 b"Shmem" => mem.shmem = bytes,
170 b"Zswap" => mem.zswap = bytes,
174 b"Zswapped" => mem.zswapped = bytes,
175 b"HugePages_Total" => mem.huge_pages_total = val,
179 b"HugePages_Free" => mem.huge_pages_free = val,
180 b"HugePages_Rsvd" => mem.huge_pages_rsvd = val,
181 b"HugePages_Surp" => mem.huge_pages_surplus = val,
182 b"Hugepagesize" => mem.huge_page_size = bytes,
183 b"SwapTotal" => swap.total = bytes,
184 b"SwapFree" => swap.free = bytes,
185 _ => {}
186 }
187 }
188 mem.finish_linux();
189 swap.used = swap.total.saturating_sub(swap.free);
190}
191
192fn first_number(buf: &[u8]) -> Option<u64> {
193 let start = buf.iter().position(|c| c.is_ascii_digit())?;
194 let mut v: u64 = 0;
195 for &c in &buf[start..] {
196 if !c.is_ascii_digit() {
197 break;
198 }
199 v = v.wrapping_mul(10).wrapping_add((c - b'0') as u64);
200 }
201 Some(v)
202}
203
204pub fn parse_proc_stat(buf: &[u8]) -> Vec<CpuTimes> {
206 let mut out = Vec::new();
207 for line in buf.split(|b| *b == b'\n') {
208 let Some(rest) = line.strip_prefix(b"cpu") else {
209 continue;
210 };
211 let rest = match rest.first() {
215 Some(c) if c.is_ascii_digit() => match memchr::memchr(b' ', rest) {
216 Some(sp) => &rest[sp..],
217 None => continue,
218 },
219 _ => rest,
220 };
221
222 let mut vals = [0u64; 10];
223 let mut n = 0;
224 for tok in rest.split(|b| *b == b' ').filter(|t| !t.is_empty()) {
225 if n == vals.len() {
226 break;
227 }
228 vals[n] = first_number(tok).unwrap_or(0);
229 n += 1;
230 }
231 if n == 0 {
232 continue;
233 }
234 out.push(CpuTimes {
235 user: vals[0],
236 nice: vals[1],
237 system: vals[2],
238 idle: vals[3],
239 iowait: vals[4],
240 irq: vals[5],
241 softirq: vals[6],
242 steal: vals[7],
243 guest: vals[8],
244 guest_nice: vals[9],
245 });
246 }
247 out
248}
249
250pub fn parse_loadavg(buf: &[u8]) -> [f64; 3] {
252 let s = String::from_utf8_lossy(buf);
253 let mut it = s.split_whitespace();
254 let a = it.next().and_then(|v| v.parse().ok()).unwrap_or(0.0);
255 let b = it.next().and_then(|v| v.parse().ok()).unwrap_or(0.0);
256 let c = it.next().and_then(|v| v.parse().ok()).unwrap_or(0.0);
257 [a, b, c]
258}
259
260pub fn classify(row: &mut Row, tgid: u32, cmdline_empty: bool, has_exe: bool) {
265 if tgid != row.pid {
266 row.flags = row.flags.insert(RowFlags::USERLAND_THREAD);
267 } else if cmdline_empty && !has_exe {
268 row.flags = row.flags.insert(RowFlags::KERNEL_THREAD);
269 }
270}
271
272pub fn parse_net_dev(buf: &[u8]) -> NetCounters {
286 let mut out = NetCounters::default();
287 for line in buf.split(|b| *b == b'\n') {
288 let Some(colon) = memchr::memchr(b':', line) else {
290 continue;
291 };
292 let name = trim_ascii(&line[..colon]);
293 if name.is_empty() {
294 continue;
295 }
296 if name == b"lo" {
299 continue;
300 }
301 let mut it = line[colon + 1..]
302 .split(|b| b.is_ascii_whitespace())
303 .filter(|t| !t.is_empty());
304 let Some(rx) = it.next().and_then(parse_u64) else {
308 continue;
309 };
310 let tx = it.nth(7).and_then(parse_u64).unwrap_or(0);
311 out.rx_bytes = out.rx_bytes.saturating_add(rx);
312 out.tx_bytes = out.tx_bytes.saturating_add(tx);
313 }
314 out
315}
316
317pub fn parse_diskstats(buf: &[u8], is_whole_disk: impl Fn(&[u8]) -> bool) -> DiskCounters {
328 const SECTOR: u64 = 512;
329 let mut out = DiskCounters::default();
330 for line in buf.split(|b| *b == b'\n') {
331 let mut it = line
332 .split(|b| b.is_ascii_whitespace())
333 .filter(|t| !t.is_empty());
334 let (_major, _minor) = (it.next(), it.next());
336 let Some(name) = it.next() else { continue };
337 if !is_whole_disk(name) {
338 continue;
339 }
340 let Some(read_sectors) = it.nth(2).and_then(parse_u64) else {
343 continue;
344 };
345 let write_sectors = it.nth(3).and_then(parse_u64).unwrap_or(0);
349 out.read_bytes = out
350 .read_bytes
351 .saturating_add(read_sectors.saturating_mul(SECTOR));
352 out.write_bytes = out
353 .write_bytes
354 .saturating_add(write_sectors.saturating_mul(SECTOR));
355 }
356 out
357}
358
359pub fn looks_like_whole_disk(name: &[u8]) -> bool {
370 for prefix in [
373 b"loop".as_slice(),
374 b"ram".as_slice(),
375 b"zram".as_slice(),
376 b"dm-".as_slice(),
377 b"sr".as_slice(),
378 b"nbd".as_slice(),
379 b"fd".as_slice(),
380 ] {
381 if name.starts_with(prefix) {
382 return false;
383 }
384 }
385
386 for prefix in [b"nvme".as_slice(), b"mmcblk".as_slice()] {
389 if let Some(rest) = name.strip_prefix(prefix) {
390 return !rest.contains(&b'p');
391 }
392 }
393
394 !name.last().is_some_and(u8::is_ascii_digit)
397}
398
399fn trim_ascii(mut b: &[u8]) -> &[u8] {
401 while let Some((first, rest)) = b.split_first() {
402 if first.is_ascii_whitespace() {
403 b = rest;
404 } else {
405 break;
406 }
407 }
408 while let Some((last, rest)) = b.split_last() {
409 if last.is_ascii_whitespace() {
410 b = rest;
411 } else {
412 break;
413 }
414 }
415 b
416}
417
418fn parse_u64(b: &[u8]) -> Option<u64> {
419 if b.is_empty() {
420 return None;
421 }
422 let mut v: u64 = 0;
423 for &c in b {
424 if !c.is_ascii_digit() {
425 return None;
426 }
427 v = v.wrapping_mul(10).wrapping_add((c - b'0') as u64);
428 }
429 Some(v)
430}
431
432#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
439pub struct GpuUsage {
440 pub gpu_ns: u64,
442 pub busy_ns: u64,
446 pub mem_ns: u64,
448}
449
450impl GpuUsage {
451 pub fn percent(&self) -> Option<f32> {
457 if self.busy_ns == 0 {
458 return None;
459 }
460 Some((self.gpu_ns as f64 / self.busy_ns as f64 * 100.0).clamp(0.0, 100.0) as f32)
461 }
462}
463
464pub fn parse_fdinfo_drm(buf: &[u8]) -> Option<GpuUsage> {
487 let mut out = GpuUsage::default();
488 let mut seen_drm = false;
489
490 for line in buf.split(|b| *b == b'\n') {
491 let Some(colon) = memchr::memchr(b':', line) else {
492 continue;
493 };
494 let key = trim_ascii(&line[..colon]);
495 if !key.starts_with(b"drm-") {
496 continue;
497 }
498 seen_drm = true;
499 let rest = &line[colon + 1..];
500 let Some(value) = first_number_in(rest) else {
504 continue;
505 };
506 if key.starts_with(b"drm-engine-") {
507 out.gpu_ns = out.gpu_ns.saturating_add(value);
510 } else if key == b"drm-busy" {
511 out.busy_ns = out.busy_ns.saturating_add(value);
512 } else if key.starts_with(b"drm-memory-") {
513 out.mem_ns = out.mem_ns.saturating_add(value);
515 }
516 }
517
518 if seen_drm {
519 Some(out)
520 } else {
521 None
522 }
523}
524
525fn first_number_in(buf: &[u8]) -> Option<u64> {
532 let start = buf.iter().position(|c| c.is_ascii_digit())?;
533 let mut v: u64 = 0;
534 for &c in &buf[start..] {
535 if !c.is_ascii_digit() {
536 break;
537 }
538 v = v.wrapping_mul(10).wrapping_add((c - b'0') as u64);
539 }
540 Some(v)
541}
542
543pub fn parse_psi_line(buf: &[u8]) -> Option<PsiLine> {
553 let mut line = PsiLine::default();
554 let mut seen = false;
555 for tok in buf.split(|b| *b == b' ').filter(|t| !t.is_empty()) {
556 let Some(eq) = memchr::memchr(b'=', tok) else {
557 continue;
558 };
559 let (key, value) = (&tok[..eq], &tok[eq + 1..]);
560 let value = trim_ascii(value);
561 match key {
562 b"avg10" => {
563 line.avg10 = parse_f32(value)?;
564 seen = true;
565 }
566 b"avg60" => line.avg60 = parse_f32(value)?,
567 b"avg300" => line.avg300 = parse_f32(value)?,
568 b"total" => line.total_us = parse_u64(value)?,
569 _ => {}
570 }
571 }
572 if seen {
573 Some(line)
574 } else {
575 None
576 }
577}
578
579pub fn parse_psi(buf: &[u8]) -> PsiResource {
581 let mut out = PsiResource {
582 present: true,
583 ..Default::default()
584 };
585 for line in buf.split(|b| *b == b'\n') {
586 let line = trim_ascii(line);
587 if let Some(rest) = line.strip_prefix(b"some ") {
588 if let Some(v) = parse_psi_line(rest) {
589 out.some = v;
590 }
591 } else if let Some(rest) = line.strip_prefix(b"full ") {
592 if let Some(v) = parse_psi_line(rest) {
593 out.full = v;
594 out.has_full = true;
595 }
596 }
597 }
598 out
599}
600
601fn parse_f32(b: &[u8]) -> Option<f32> {
606 std::str::from_utf8(b).ok()?.trim().parse().ok()
607}
608
609pub fn parse_zram_mm_stat(buf: &[u8], name: &str) -> Option<ZramDevice> {
622 let mut it = buf.split(|b| *b == b' ' || *b == b'\n').filter(|t| !t.is_empty());
623 let orig = parse_u64(trim_ascii(it.next()?))?;
624 let compr = parse_u64(trim_ascii(it.next()?))?;
625 let mem_used = parse_u64(trim_ascii(it.next()?))?;
626 Some(ZramDevice {
627 name: name.to_string(),
628 orig_data_size: orig,
629 compr_data_size: compr,
630 mem_used_total: mem_used,
631 })
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 const STAT_TRICKY: &[u8] =
639 b"1234 (my ) we(ird comm) S 1 1234 1234 0 -1 4194560 100 0 5 0 12 34 0 0 20 0 3 0 987654 1048576 256 18446744073709551615\n";
640
641 #[test]
642 fn stat_handles_parens_in_comm() {
643 let (pid, comm, f) = parse_stat(STAT_TRICKY).expect("must parse");
644 assert_eq!(pid, 1234);
645 assert_eq!(&*comm, "my ) we(ird comm");
646 assert_eq!(f.state, b'S');
647 assert_eq!(f.ppid, 1);
648 assert_eq!(f.min_flt, 100);
649 assert_eq!(f.maj_flt, 5);
650 assert_eq!(f.utime, 12);
651 assert_eq!(f.stime, 34);
652 assert_eq!(f.cpu_ticks(), 46);
653 assert_eq!(f.nlwp, 3);
654 assert_eq!(f.starttime, 987_654);
655 assert_eq!(f.vsize, 1_048_576);
656 assert_eq!(f.rss_pages, 256);
657 }
658
659 #[test]
660 fn stat_rejects_truncated() {
661 assert!(parse_stat(b"1234 (bash) S 1 2").is_none());
662 assert!(parse_stat(b"garbage").is_none());
663 }
664
665 const MEMINFO: &[u8] = b"MemTotal: 16000000 kB\nMemFree: 1000000 kB\nMemAvailable: 8000000 kB\nBuffers: 100000 kB\nCached: 2000000 kB\nSReclaimable: 500000 kB\nShmem: 200000 kB\nSwapTotal: 2000000 kB\nSwapFree: 1500000 kB\n";
666
667 #[test]
668 fn meminfo_matches_free_accounting() {
669 let mut mem = MemInfo::default();
670 let mut swap = SwapInfo::default();
671 parse_meminfo(MEMINFO, &mut mem, &mut swap);
672 assert_eq!(mem.total, 16_000_000 * 1024);
673 assert_eq!(swap.used, 500_000 * 1024);
674
675 assert_eq!(mem.used, 12_400_000 * 1024);
678 assert_eq!(mem.available, 8_000_000 * 1024);
681 assert!(mem.is_consistent(), "{mem:?}");
682 assert!(!mem.is_exactly_partitioned());
685 assert!(mem.used + mem.available > mem.total);
686 }
687
688 const PROC_STAT: &[u8] = b"cpu 100 10 50 1000 20 5 5 0 0 0\ncpu0 50 5 25 500 10 2 3 0 0 0\ncpu1 50 5 25 500 10 3 2 0 0 0\nintr 12345 0 0\nctxt 999\n";
689
690 #[test]
691 fn proc_stat_parsing() {
692 let v = parse_proc_stat(PROC_STAT);
693 assert_eq!(v.len(), 3, "aggregate plus two cores");
694 assert_eq!(v[0].user, 100);
695 assert_eq!(v[1].softirq, 3);
696 assert_eq!(v[2].irq, 3);
697 assert_eq!(v[0].total(), 100 + 10 + 50 + 1000 + 20 + 5 + 5);
698 }
699
700 #[test]
701 fn loadavg_parsing() {
702 assert_eq!(
703 parse_loadavg(b"0.52 0.58 0.59 1/1234 5678\n"),
704 [0.52, 0.58, 0.59]
705 );
706 }
707
708 #[test]
709 fn status_uid() {
710 let buf = b"Name:\tbash\nUid:\t1000\t1000\t1000\t1000\nGid:\t1000\t1000\t1000\t1000\n";
711 assert_eq!(parse_status_uid(buf), Some(1000));
712 }
713
714 const NET_DEV: &[u8] = b"Inter-| Receive | Transmit\n face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n lo: 100 1 0 0 0 0 0 0 100 1 0 0 0 0 0 0\n eth0: 5678 34 0 0 0 0 0 0 9101 56 0 0 0 0 0 0\n wlan0: 1000 9 0 0 0 0 0 0 2000 10 0 0 0 0 0 0\n";
718
719 #[test]
720 fn net_dev_sums_non_loopback_interfaces() {
721 let n = parse_net_dev(NET_DEV);
722 assert_eq!(n.rx_bytes, 5678 + 1000);
723 assert_eq!(n.tx_bytes, 9101 + 2000);
724 }
725
726 #[test]
727 fn net_dev_without_loopback_still_works() {
728 let buf = b"h1\nh2\n eth0: 10 1 0 0 0 0 0 0 20 1 0 0 0 0 0 0\n";
729 let n = parse_net_dev(buf);
730 assert_eq!(n.rx_bytes, 10);
731 assert_eq!(n.tx_bytes, 20);
732 }
733
734 #[test]
735 fn net_dev_garbage_is_zero_not_a_panic() {
736 assert_eq!(parse_net_dev(b""), NetCounters::default());
737 assert_eq!(parse_net_dev(b"no colons here\n"), NetCounters::default());
738 }
739
740 const DISKSTATS: &[u8] = concat!(
743 " 8 0 sda 1000 0 8000 100 500 0 4000 50 0 100 150\n",
744 " 8 1 sda1 900 0 7000 90 400 0 3000 40 0 90 130\n",
745 " 259 0 nvme0n1 200 0 1600 20 100 0 800 10 0 20 30\n",
746 " 259 1 nvme0n1p1 150 0 1200 15 80 0 600 8 0 15 22\n",
747 " 7 0 loop0 5 0 40 1 0 0 0 0 0 1 1\n",
748 )
749 .as_bytes();
750
751 #[test]
752 fn diskstats_counts_whole_disks_only() {
753 let d = parse_diskstats(DISKSTATS, looks_like_whole_disk);
754 assert_eq!(d.read_bytes, (8000 + 1600) * 512);
756 assert_eq!(d.write_bytes, (4000 + 800) * 512);
757 }
758
759 #[test]
760 fn diskstats_write_field_is_seven_not_six() {
761 let buf = b" 8 0 sda 1 0 1111 0 1 0 2222 0 0 0 0\n";
764 let d = parse_diskstats(buf, |_| true);
765 assert_eq!(d.read_bytes, 1111 * 512);
766 assert_eq!(d.write_bytes, 2222 * 512, "sectors written is field 7");
767 }
768
769 #[test]
770 fn whole_disk_names() {
771 for disk in ["sda", "vda", "hdb", "nvme0n1", "mmcblk0", "xvda"] {
772 assert!(looks_like_whole_disk(disk.as_bytes()), "{disk} is a disk");
773 }
774 for part in ["sda1", "vda12", "nvme0n1p1", "mmcblk0p2"] {
775 assert!(!looks_like_whole_disk(part.as_bytes()), "{part} is a partition");
776 }
777 for pseudo in ["loop0", "ram0", "zram0", "dm-0", "sr0"] {
778 assert!(!looks_like_whole_disk(pseudo.as_bytes()), "{pseudo} is skipped");
779 }
780 }
781
782 const PSI_IO: &[u8] = b"some avg10=12.50 avg60=8.25 avg300=4.10 total=98765432\nfull avg10=40.50 avg60=30.25 avg300=20.10 total=45678901\n";
785
786 #[test]
787 fn psi_parses_both_lines() {
788 let r = parse_psi(PSI_IO);
789 assert!(r.present);
790 assert!(r.has_full);
791 assert_eq!(r.some.avg10, 12.5);
792 assert_eq!(r.some.avg60, 8.25);
793 assert_eq!(r.some.total_us, 98_765_432);
794 assert_eq!(r.full.avg10, 40.5);
795 assert_eq!(r.full.total_us, 45_678_901);
796 }
797
798 #[test]
805 fn psi_without_a_full_line_reports_has_full_false() {
806 let r = parse_psi(b"some avg10=1.25 avg60=0.75 avg300=0.50 total=12345678\n");
807 assert!(r.present);
808 assert!(!r.has_full, "no full line was present in the input");
809 assert_eq!(r.some.avg10, 1.25);
810 assert_eq!(r.full, PsiLine::default());
811 }
812
813 #[test]
815 fn psi_full_line_is_parsed_when_present() {
816 let r = parse_psi(
817 b"some avg10=1.25 avg60=0.75 avg300=0.50 total=10\nfull avg10=0.50 avg60=0.25 avg300=0.10 total=5\n",
818 );
819 assert!(r.has_full);
820 assert_eq!(r.full.avg10, 0.5);
821 assert_eq!(r.full.total_us, 5);
822 }
823
824 #[test]
827 fn psi_keys_are_positional_independent() {
828 let mut shuffled = parse_psi(b"some total=5 avg10=1.00\n");
829 assert_eq!(shuffled.some.avg10, 1.0);
830 assert_eq!(shuffled.some.total_us, 5);
831 assert!(!shuffled.has_full);
832 shuffled = parse_psi(b"");
833 assert!(shuffled.present, "an empty file was still readable");
834 assert_eq!(shuffled.some, PsiLine::default());
835 }
836
837 #[test]
838 fn psi_rejects_a_line_with_no_avg() {
839 assert!(parse_psi_line(b"total=5").is_none());
840 assert!(parse_psi_line(b"").is_none());
841 }
842
843 #[test]
846 fn zram_mm_stat_parses_the_first_three_columns() {
847 let line = b"1073741824 268435456 268435456 0 268435456 1000 5 0 0\n";
848 let d = parse_zram_mm_stat(line, "zram0").expect("must parse");
849 assert_eq!(d.name, "zram0");
850 assert_eq!(d.orig_data_size, 1_073_741_824);
851 assert_eq!(d.compr_data_size, 268_435_456);
852 assert_eq!(d.mem_used_total, 268_435_456);
853 assert!((d.ratio() - 4.0).abs() < 0.001);
855 }
856
857 #[test]
858 fn zram_rejects_a_truncated_line() {
859 assert!(parse_zram_mm_stat(b"", "zram0").is_none());
860 assert!(parse_zram_mm_stat(b"123 456", "zram0").is_none());
861 }
862
863 #[test]
865 fn an_empty_zram_device_reports_ratio_one() {
866 let d = parse_zram_mm_stat(b"0 0 0 0 0 0 0 0 0\n", "zram0").unwrap();
867 assert_eq!(d.ratio(), 1.0);
868 assert!(d.ratio().is_finite());
869 }
870
871 const FDINFO_DRM: &[u8] = b"pos:\t0\nflags:\t02100002\nmnt_id:\t25\nino:\t1234\ndrm-driver:\tamdgpu\ndrm-pdev:\t0000:03:00.0\ndrm-engine-gfx:\t5000000000 ns\ndrm-engine-compute:\t1000000000 ns\ndrm-memory-vram:\t1048576 KiB\ndrm-busy:\t25000000000 ns\n";
874
875 #[test]
876 fn fdinfo_sums_every_engine_class() {
877 let g = parse_fdinfo_drm(FDINFO_DRM).expect("must parse");
878 assert_eq!(g.gpu_ns, 6_000_000_000);
881 assert_eq!(g.busy_ns, 25_000_000_000);
882 assert_eq!(g.mem_ns, 1_048_576, "parsed as a number, whatever its unit");
886 }
887
888 #[test]
889 fn fdinfo_percent_is_engine_over_busy() {
890 let g = parse_fdinfo_drm(FDINFO_DRM).unwrap();
891 let pct = g.percent().expect("busy time is present");
892 assert!((pct - 24.0).abs() < 0.01, "6/25 == 24 %, got {pct}");
893 }
894
895 #[test]
898 fn fdinfo_without_busy_has_no_percent() {
899 let g = parse_fdinfo_drm(b"drm-engine-gfx:\t5000 ns\n").expect("has a drm key");
900 assert_eq!(g.gpu_ns, 5000);
901 assert_eq!(g.busy_ns, 0);
902 assert_eq!(g.percent(), None);
903 }
904
905 #[test]
908 fn fdinfo_without_drm_keys_is_not_a_gpu_descriptor() {
909 assert!(parse_fdinfo_drm(b"pos:\t0\nflags:\t02\nmnt_id:\t25\n").is_none());
910 assert!(parse_fdinfo_drm(b"").is_none());
911 }
912
913 #[test]
914 fn counters_never_report_a_negative_rate() {
915 let before = NetCounters {
917 rx_bytes: 1_000_000,
918 tx_bytes: 500_000,
919 };
920 let after = NetCounters {
921 rx_bytes: 100,
922 tx_bytes: 50,
923 };
924 assert_eq!(after.delta(&before), NetCounters::default());
925 assert_eq!(after.per_sec(&before, 1.0).total(), 0.0);
926
927 let rate = after.per_sec(&NetCounters::default(), 0.0);
929 assert_eq!(rate.total(), 0.0);
930 assert!(rate.inbound.is_finite());
931 }
932}
933
934#[cfg(test)]
935mod tty_and_tpgid_tests {
936 use super::*;
937
938 #[test]
943 fn tty_and_tpgid_come_from_the_right_fields() {
944 let line = b"42 (my ) we(ird comm) R 1 42 42 34816 4242 4194560 512 7 0 0 120 34 0 0 20 0 1 0 999 1048576 256\n";
947 let (pid, comm, st) = parse_stat(line).expect("parse");
948 assert_eq!(pid, 42);
949 assert_eq!(comm.as_str(), "my ) we(ird comm");
952 assert_eq!(st.session, 42);
953 assert_eq!(st.tty_nr, 34816, "136<<8 = pts/0");
954 assert_eq!(st.tpgid, 4242);
955 }
956
957 #[test]
960 fn a_negative_tpgid_is_not_silently_wrapped() {
961 let line = b"7 (bash) S 1 7 7 0 -1 4194560 1 0 0 0 0 0 0 0 20 0 1 0 1 1 1\n";
962 let (_, _, st) = parse_stat(line).expect("parse");
963 assert_eq!(st.tty_nr, 0);
964 assert_eq!(st.tpgid, -1);
965 }
966}