1use std::collections::HashMap;
7
8use crate::row::{Row, RowFlags};
9use crate::system::SystemSnapshot;
10use crate::tree;
11use crate::Pid;
12
13#[derive(Default)]
18pub struct Cache {
19 hits: HashMap<(Pid, u64), Stable>,
20}
21
22#[derive(Clone, Default, Debug)]
24pub struct Stable {
25 pub uid: u32,
26 pub cmdline: Option<Box<str>>,
27 pub exe: Option<Box<str>>,
28 pub cwd: Option<Box<str>>,
34 pub session: u32,
45 pub pgrp: u32,
50 pub tpgid: i64,
51 pub tty_dev: i64,
57 pub exe_deleted: bool,
69 pub cgroup: Option<Box<str>>,
71 #[cfg(target_os = "linux")]
77 pub container: Option<Box<str>>,
78 #[cfg(target_os = "linux")]
79 pub container_runtime: Option<Box<str>>,
80 pub is_kernel_thread: bool,
81}
82
83impl Cache {
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn get(&self, pid: Pid, starttime: u64) -> Option<&Stable> {
89 self.hits.get(&(pid, starttime))
90 }
91
92 pub fn insert(&mut self, pid: Pid, starttime: u64, stable: Stable) {
93 self.hits.insert((pid, starttime), stable);
94 }
95
96 pub fn retain_live(&mut self, live: &[(Pid, u64)]) {
99 if self.hits.len() <= live.len() {
100 return;
101 }
102 let keep: std::collections::HashSet<_> = live.iter().copied().collect();
103 self.hits.retain(|k, _| keep.contains(k));
104 }
105
106 pub fn clear(&mut self) {
108 self.hits.clear();
109 }
110
111 pub fn len(&self) -> usize {
112 self.hits.len()
113 }
114
115 pub fn is_empty(&self) -> bool {
116 self.hits.is_empty()
117 }
118}
119
120#[derive(Copy, Clone, PartialEq, Eq, Debug)]
122pub enum Dir {
123 Asc,
124 Desc,
125}
126
127#[derive(Copy, Clone, Debug)]
129pub struct Sort {
130 pub field: crate::row::Field,
131 pub dir: Dir,
132}
133
134impl Default for Sort {
135 fn default() -> Self {
142 Sort {
143 field: crate::row::Field::Pid,
144 dir: Dir::Asc,
145 }
146 }
147}
148
149#[derive(Default)]
151pub struct View {
152 pub order: Vec<usize>,
154}
155
156pub fn sort_order(rows: &[Row], order: &mut [usize], sort: Sort) {
158 let descending = sort.dir == Dir::Desc;
175 order.sort_unstable_by(|a, b| {
176 let primary = compare_by(rows, sort.field, *a, *b);
177 let primary = if descending { primary.reverse() } else { primary };
178 primary.then_with(|| rows[*a].pid.cmp(&rows[*b].pid))
179 });
180}
181
182fn compare_by(rows: &[Row], field: crate::row::Field, a: usize, b: usize) -> std::cmp::Ordering {
185 use crate::row::Field as F;
186 match field {
187 F::Pid => rows[a].pid.cmp(&rows[b].pid),
188 F::Ppid => rows[a].ppid.cmp(&rows[b].ppid),
189 F::Pgrp => rows[a].pgrp.cmp(&rows[b].pgrp),
190 F::Comm => rows[a].comm.cmp(&rows[b].comm),
191 F::User => rows[a].uid.cmp(&rows[b].uid),
192 F::Priority => rows[a].priority.cmp(&rows[b].priority),
193 F::Nice => rows[a].nice.cmp(&rows[b].nice),
194 F::PercentCpu | F::PercentNormCpu => {
195 rows[a].percent_cpu.total_cmp(&rows[b].percent_cpu)
196 }
197 F::PercentMem => rows[a].percent_mem.total_cmp(&rows[b].percent_mem),
198 F::ResMem => rows[a].res.cmp(&rows[b].res),
199 F::VirtMem => rows[a].virt.cmp(&rows[b].virt),
200 F::Time => rows[a].time_ticks.cmp(&rows[b].time_ticks),
201 F::Elapsed => rows[a].elapsed.cmp(&rows[b].elapsed),
202 F::Nlwp => rows[a].nlwp.cmp(&rows[b].nlwp),
203 F::MinFlt => rows[a].min_flt.cmp(&rows[b].min_flt),
204 F::MajFlt => rows[a].maj_flt.cmp(&rows[b].maj_flt),
205 F::Session => rows[a].session.cmp(&rows[b].session),
209 F::Tty => rows[a].tty_nr.cmp(&rows[b].tty_nr),
210 F::Tpgid => rows[a].tpgid.cmp(&rows[b].tpgid),
211 F::StartTime => rows[a].starttime.cmp(&rows[b].starttime),
212 F::Tgid => rows[a].tgid.cmp(&rows[b].tgid),
213 F::SchedPolicy => rows[a].sched_policy.cmp(&rows[b].sched_policy),
214 F::ProcComm => rows[a].comm.cmp(&rows[b].comm),
215 F::ProcExe => rows[a]
219 .exe
220 .as_deref()
221 .unwrap_or("")
222 .cmp(rows[b].exe.as_deref().unwrap_or("")),
223 F::Cwd => rows[a]
224 .cwd
225 .as_deref()
226 .unwrap_or("")
227 .cmp(rows[b].cwd.as_deref().unwrap_or("")),
228 F::Processor => rows[a].processor.cmp(&rows[b].processor),
229 #[cfg(target_os = "linux")]
230 F::Cgroup => rows[a]
231 .cgroup
232 .as_deref()
233 .unwrap_or("")
234 .cmp(rows[b].cgroup.as_deref().unwrap_or("")),
235 #[cfg(target_os = "linux")]
236 F::Container => {
237 let key = |i: usize| {
238 (
239 rows[i].container_runtime.as_deref().unwrap_or(""),
240 rows[i].container.as_deref().unwrap_or(""),
241 )
242 };
243 key(a).cmp(&key(b))
244 }
245 #[cfg(target_os = "linux")]
253 F::Pss => rows[a]
254 .pss
255 .unwrap_or(0)
256 .cmp(&rows[b].pss.unwrap_or(0)),
257 #[cfg(target_os = "linux")]
258 F::Epss => rows[a]
259 .epss
260 .unwrap_or(0)
261 .cmp(&rows[b].epss.unwrap_or(0)),
262 #[cfg(target_os = "linux")]
263 F::Private => rows[a]
264 .private
265 .unwrap_or(0)
266 .cmp(&rows[b].private.unwrap_or(0)),
267 #[cfg(target_os = "linux")]
268 F::Swap => rows[a]
269 .swap_bytes
270 .unwrap_or(0)
271 .cmp(&rows[b].swap_bytes.unwrap_or(0)),
272 #[cfg(target_os = "linux")]
273 F::GpuPercent => rows[a]
274 .gpu_percent
275 .unwrap_or(0.0)
276 .total_cmp(&rows[b].gpu_percent.unwrap_or(0.0)),
277 #[cfg(target_os = "linux")]
278 F::GpuTime => rows[a]
279 .gpu_ns
280 .unwrap_or(0)
281 .cmp(&rows[b].gpu_ns.unwrap_or(0)),
282 #[cfg(target_os = "linux")]
286 F::CpuDelayPercent => rows[a]
287 .cpu_delay_percent
288 .unwrap_or(0.0)
289 .total_cmp(&rows[b].cpu_delay_percent.unwrap_or(0.0)),
290 #[cfg(target_os = "linux")]
291 F::IoDelayPercent => rows[a]
292 .io_delay_percent
293 .unwrap_or(0.0)
294 .total_cmp(&rows[b].io_delay_percent.unwrap_or(0.0)),
295 #[cfg(target_os = "linux")]
296 F::SwapDelayPercent => rows[a]
297 .swap_delay_percent
298 .unwrap_or(0.0)
299 .total_cmp(&rows[b].swap_delay_percent.unwrap_or(0.0)),
300 _ => rows[a].pid.cmp(&rows[b].pid),
301 }
302}
303
304#[derive(Clone, Debug)]
306pub struct Filter {
307 terms: Vec<String>,
308}
309
310impl Filter {
311 pub fn parse(pattern: &str) -> Option<Self> {
312 let terms: Vec<String> = pattern
313 .split('|')
314 .map(|t| t.trim().to_ascii_lowercase())
315 .filter(|t| !t.is_empty())
316 .collect();
317 if terms.is_empty() {
318 None
319 } else {
320 Some(Filter { terms })
321 }
322 }
323
324 pub fn matches(&self, row: &Row) -> bool {
326 let haystack = row.display_command().to_ascii_lowercase();
327 self.terms.iter().any(|t| haystack.contains(t.as_str()))
328 }
329}
330
331pub fn find_next(rows: &[Row], order: &[usize], from: usize, needle: &str) -> Option<usize> {
334 if needle.is_empty() || order.is_empty() {
335 return None;
336 }
337 let needle = needle.to_ascii_lowercase();
338 let n = order.len();
339 for step in 1..=n {
340 let pos = (from + step) % n;
341 if rows[order[pos]]
342 .display_command()
343 .to_ascii_lowercase()
344 .contains(&needle)
345 {
346 return Some(pos);
347 }
348 }
349 None
350}
351
352#[derive(Clone, Debug)]
357pub struct Visibility {
358 pub text: Option<Filter>,
360 pub uid: Option<u32>,
362 pub show_kernel_threads: bool,
364 pub show_userland_threads: bool,
366 pub show_containerised: bool,
371}
372
373impl Default for Visibility {
374 fn default() -> Self {
381 Self {
382 text: None,
383 uid: None,
384 show_kernel_threads: true,
385 show_userland_threads: true,
386 show_containerised: true,
387 }
388 }
389}
390
391impl Visibility {
392 pub fn allows(&self, row: &Row) -> bool {
393 if !self.show_kernel_threads && row.flags.contains(RowFlags::KERNEL_THREAD) {
394 return false;
395 }
396 if !self.show_userland_threads && row.is_thread {
397 return false;
398 }
399 if !self.show_containerised {
400 #[cfg(target_os = "linux")]
404 if row.container.is_some() {
405 return false;
406 }
407 }
408 if let Some(uid) = self.uid {
409 if row.uid != uid {
410 return false;
411 }
412 }
413 match &self.text {
414 Some(f) => f.matches(row),
415 None => true,
416 }
417 }
418}
419
420pub fn apply_filter(rows: &[Row], filter: Option<&Filter>) -> Vec<usize> {
422 match filter {
423 None => (0..rows.len()).collect(),
424 Some(f) => (0..rows.len()).filter(|&i| f.matches(&rows[i])).collect(),
425 }
426}
427
428pub struct ProcTable {
430 pub rows: Vec<Row>,
431 pub view: View,
432 pub sort: Sort,
433 pub tree_mode: bool,
434 pub show_kernel_threads: bool,
435 pub show_userland_threads: bool,
436 pub show_containerised: bool,
438}
439
440impl Default for ProcTable {
441 fn default() -> Self {
445 Self {
446 rows: Vec::new(),
447 view: View::default(),
448 sort: Sort::default(),
449 tree_mode: false,
450 show_kernel_threads: true,
451 show_userland_threads: false,
452 show_containerised: true,
453 }
454 }
455}
456
457impl ProcTable {
458 pub fn reorder(&mut self, visibility: &Visibility) {
460 let mut order: Vec<usize> = (0..self.rows.len())
461 .filter(|&i| visibility.allows(&self.rows[i]))
462 .collect();
463
464 if self.tree_mode {
465 let full = tree::build(&mut self.rows);
468 let visible: std::collections::HashSet<usize> = order.into_iter().collect();
469 self.view.order = full.into_iter().filter(|i| visible.contains(i)).collect();
470 return;
471 }
472
473 sort_order(&self.rows, &mut order, self.sort);
474 self.view.order = order;
475 }
476
477 pub fn visible(&self) -> usize {
479 self.view.order.len()
480 }
481
482 pub fn row_at(&self, pos: usize) -> Option<&Row> {
483 self.view.order.get(pos).map(|&i| &self.rows[i])
484 }
485
486 pub fn total_cpu(&self) -> f32 {
488 self.view
489 .order
490 .iter()
491 .map(|&i| self.rows[i].percent_cpu)
492 .sum()
493 }
494}
495
496pub fn derive_cpu(rows: &mut [Row], snapshot: &SystemSnapshot) {
500 let d_total = snapshot.cpu_delta.first().map(|d| d.total()).unwrap_or(0) as f32;
501
502 for row in rows.iter_mut() {
503 let d_proc = row.ticks.proc.saturating_sub(row.prev_ticks.proc) as f32;
504 row.percent_cpu = if d_total <= 0.0 {
505 0.0
506 } else {
507 d_proc * 100.0 / d_total
509 };
510 row.percent_mem = if snapshot.mem.total == 0 {
511 0.0
512 } else {
513 row.res as f32 * 100.0 / snapshot.mem.total as f32
514 };
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::row::Field;
522
523 fn rows() -> Vec<Row> {
524 let mut a = Row::default_for_test(1);
525 a.percent_cpu = 5.0;
526 a.percent_mem = 1.0;
527 a.comm.try_push_str("alpha").unwrap();
528 let mut b = Row::default_for_test(2);
529 b.percent_cpu = 50.0;
530 b.percent_mem = 10.0;
531 b.comm.try_push_str("beta").unwrap();
532 vec![a, b]
533 }
534
535 #[test]
536 fn sort_is_a_permutation() {
537 let rows = rows();
538 let mut order: Vec<usize> = (0..rows.len()).collect();
539 sort_order(
540 &rows,
541 &mut order,
542 Sort {
543 field: Field::PercentCpu,
544 dir: Dir::Desc,
545 },
546 );
547 assert_eq!(order, vec![1, 0]);
548 let mut sorted = order.clone();
549 sorted.sort_unstable();
550 assert_eq!(sorted, vec![0, 1], "no duplicates, no omissions");
551 }
552
553 #[test]
554 fn filter_any_term_wins() {
555 let rows = rows();
556 let f = Filter::parse("ALPHA|nope").unwrap();
557 assert!(f.matches(&rows[0]));
558 assert!(!f.matches(&rows[1]));
559 }
560
561 #[test]
562 fn empty_filter_is_none() {
563 assert!(Filter::parse("").is_none());
564 assert!(Filter::parse(" | ").is_none());
565 }
566
567 #[test]
568 fn search_wraps() {
569 let rows = rows();
570 let order = vec![0, 1];
571 assert_eq!(find_next(&rows, &order, 1, "alpha"), Some(0));
573 }
574
575 #[test]
585 fn ties_are_broken_by_pid_in_both_directions() {
586 let rows: Vec<Row> = (1..=10u32)
587 .map(|pid| {
588 let mut r = Row::default_for_test(pid);
589 r.percent_cpu = 0.0;
590 r
591 })
592 .collect();
593
594 for dir in [Dir::Asc, Dir::Desc] {
595 for _ in 0..5 {
596 let mut order: Vec<usize> = (0..rows.len()).collect();
598 sort_order(&rows, &mut order, Sort { field: Field::PercentCpu, dir });
599 let pids: Vec<u32> = order.iter().map(|&i| rows[i].pid).collect();
600 assert_eq!(
601 pids,
602 vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
603 "all-equal rows must read as ascending pid even when sorting {dir:?}"
604 );
605 }
606 }
607 }
608
609 #[test]
611 fn direction_applies_to_the_primary_key() {
612 let rows: Vec<Row> = (1..=10u32)
613 .map(|pid| {
614 let mut r = Row::default_for_test(pid);
615 r.percent_cpu = if pid == 3 || pid == 7 { 5.0 } else { 0.0 };
616 r
617 })
618 .collect();
619
620 let mut order: Vec<usize> = (0..rows.len()).collect();
621 sort_order(&rows, &mut order, Sort { field: Field::PercentCpu, dir: Dir::Desc });
622 let pids: Vec<u32> = order.iter().map(|&i| rows[i].pid).collect();
623 assert_eq!(&pids[..2], &[3, 7], "highest CPU first, pid-ascending among ties");
624 assert_eq!(&pids[2..], &[1, 2, 4, 5, 6, 8, 9, 10], "then the zeroes");
625
626 let mut order: Vec<usize> = (0..rows.len()).collect();
627 sort_order(&rows, &mut order, Sort { field: Field::PercentCpu, dir: Dir::Asc });
628 let pids: Vec<u32> = order.iter().map(|&i| rows[i].pid).collect();
629 assert_eq!(&pids[..8], &[1, 2, 4, 5, 6, 8, 9, 10], "zeroes first");
630 assert_eq!(&pids[8..], &[3, 7], "then the busy ones");
631 }
632
633 #[test]
635 fn pid_ascending_is_a_stable_identity() {
636 let rows: Vec<Row> = (1..=20u32)
637 .rev()
638 .map(|pid| {
639 let mut r = Row::default_for_test(pid);
640 r.percent_cpu = (pid % 7) as f32;
641 r
642 })
643 .collect();
644 let mut order: Vec<usize> = (0..rows.len()).collect();
645 sort_order(&rows, &mut order, Sort { field: Field::Pid, dir: Dir::Asc });
646 let pids: Vec<u32> = order.iter().map(|&i| rows[i].pid).collect();
647 assert_eq!(pids, (1..=20u32).collect::<Vec<_>>());
648 }
649}
650
651#[cfg(test)]
652mod visibility_tests {
653 use super::*;
654 use crate::row::{Row, RowFlags};
655
656 #[test]
657 fn default_visibility_shows_everything() {
658 let v = Visibility::default();
661 assert!(v.show_kernel_threads);
662 assert!(v.show_userland_threads);
663 assert!(v.show_containerised);
664
665 let mut kernel = Row::default_for_test(2);
666 kernel.flags = kernel.flags.insert(RowFlags::KERNEL_THREAD);
667 assert!(v.allows(&kernel), "kernel threads visible by default");
668
669 let mut thread = Row::default_for_test(3);
670 thread.is_thread = true;
671 assert!(v.allows(&thread), "thread rows visible by default");
672 }
673
674 #[test]
675 fn hiding_kernel_threads_only_hides_kernel_threads() {
676 let v = Visibility {
677 show_kernel_threads: false,
678 ..Visibility::default()
679 };
680 let mut kernel = Row::default_for_test(2);
681 kernel.flags = kernel.flags.insert(RowFlags::KERNEL_THREAD);
682 assert!(!v.allows(&kernel));
683 assert!(v.allows(&Row::default_for_test(3)));
684 }
685
686 #[test]
687 fn hiding_threads_only_hides_threads() {
688 let v = Visibility {
689 show_userland_threads: false,
690 ..Visibility::default()
691 };
692 let mut thread = Row::default_for_test(3);
693 thread.is_thread = true;
694 assert!(!v.allows(&thread));
695 assert!(v.allows(&Row::default_for_test(4)));
696 }
697
698 #[cfg(target_os = "linux")]
699 #[test]
700 fn hiding_containerised_processes_spares_host_processes() {
701 let visible = Visibility::default();
702 let mut containerised = Row::default_for_test(10);
703 containerised.container = Some("abc123".into());
704 containerised.container_runtime = Some("docker".into());
705 assert!(visible.allows(&containerised), "shown by default");
706
707 let hidden = Visibility {
708 show_containerised: false,
709 ..Visibility::default()
710 };
711 assert!(!hidden.allows(&containerised));
712
713 assert!(hidden.allows(&Row::default_for_test(11)));
716 }
717
718 #[cfg(target_os = "linux")]
719 #[test]
720 fn a_thread_of_a_containerised_process_is_also_hidden() {
721 let mut thread = Row::default_for_test(12);
724 thread.is_thread = true;
725 thread.container = Some("abc123".into());
726 thread.container_runtime = Some("docker".into());
727
728 let hidden = Visibility {
729 show_containerised: false,
730 ..Visibility::default()
731 };
732 assert!(!hidden.allows(&thread));
733 }
734}