Skip to main content

gander_core/
table.rs

1//! The process table: scan, cache, derive, sort, filter.
2//!
3//! The central design point is the index indirection: sorting and filtering move
4//! only `Vec<usize>`, never the `Row` storage. See `docs/DESIGN.md` §6.
5
6use std::collections::HashMap;
7
8use crate::row::{Row, RowFlags};
9use crate::system::SystemSnapshot;
10use crate::tree;
11use crate::Pid;
12
13/// Data that never changes for a live process, keyed by `(pid, starttime)`.
14///
15/// Caching it is the single largest syscall reduction available;
16/// `docs/DESIGN.md` §3.2.
17#[derive(Default)]
18pub struct Cache {
19    hits: HashMap<(Pid, u64), Stable>,
20}
21
22/// The cached identity of one process.
23#[derive(Clone, Default, Debug)]
24pub struct Stable {
25    pub uid: u32,
26    pub cmdline: Option<Box<str>>,
27    pub exe: Option<Box<str>>,
28    /// The process's working directory.
29    ///
30    /// Cached alongside `exe` rather than read per sample: it costs a `readlink`
31    /// per process, and it is display-only — nothing sorts or filters on it by
32    /// default. The cost is one syscall per process per *lifetime*, not per tick.
33    pub cwd: Option<Box<str>>,
34    /// Login session id.
35    ///
36    /// Cached because it needs a syscall of its own (`getsid`) on macOS, where
37    /// `proc_bsdinfo` does not carry it. Linux reads it from field 4 of
38    /// `/proc/<pid>/stat`, which the scan already has, so its value does not come
39    /// from here — but keeping one field for both platforms means the two backends
40    /// cannot drift in what `SESSION` means.
41    ///
42    /// `0` is "unknown", which is what `getsid` yields for another user's process
43    /// (`EPERM`). It renders `-`.
44    pub session: u32,
45    /// Process group and the terminal's foreground group, so a thread row can
46    /// inherit them instead of re-asking the kernel per thread.
47    ///
48    /// `tpgid` is `-1` when the terminal has no foreground job.
49    pub pgrp: u32,
50    pub tpgid: i64,
51    /// The controlling terminal's device, in the platform's own encoding.
52    ///
53    /// Linux leaves this `0` because `tty_nr` arrives in the `stat` the scan
54    /// already parses; macOS fills it from `e_tdev`, since `proc_bsdinfo` is a
55    /// separate call per process and the thread pass should not repeat it.
56    pub tty_dev: i64,
57    /// Whether the executable has been replaced or deleted on disk.
58    ///
59    /// Linux exposes this in the `/proc/<pid>/exe` symlink target itself: once the
60    /// file backing a running process is unlinked or replaced, the kernel appends
61    /// ` (deleted)` to the target. That is the only signal available — comparing
62    /// mtimes would need the path to still exist, which is exactly what deleting
63    /// it removes.
64    ///
65    /// Cached with the identity because the flag cannot change without the
66    /// process's exe being replaced, and re-reading the link per frame would be
67    /// thousands of syscalls a second for a value that almost never changes.
68    pub exe_deleted: bool,
69    /// The cgroup *path*, not the raw `/proc/<pid>/cgroup` line.
70    pub cgroup: Option<Box<str>>,
71    /// The container's id, and the runtime that owns it.
72    ///
73    /// Cached here rather than recomputed per frame: classification is a few
74    /// `find`s over a path, which is cheap once per process and wasteful
75    /// thousands of times per second.
76    #[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    /// Forget entries whose process is gone. Called once per tick with the set
97    /// of live `(pid, starttime)` keys.
98    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    /// Drop everything. Called when the cache grows past a sane bound.
107    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/// Sort direction.
121#[derive(Copy, Clone, PartialEq, Eq, Debug)]
122pub enum Dir {
123    Asc,
124    Desc,
125}
126
127/// A sort specification.
128#[derive(Copy, Clone, Debug)]
129pub struct Sort {
130    pub field: crate::row::Field,
131    pub dir: Dir,
132}
133
134impl Default for Sort {
135    /// PID ascending, matching the shipped config default.
136    ///
137    /// Kept in step with `Config::default()` deliberately: a `ProcTable` built
138    /// without an explicit sort (tests, and any future front end) should present
139    /// the same stable order the TUI does, not silently fall back to CPU% and
140    /// reintroduce the reshuffling this default exists to avoid.
141    fn default() -> Self {
142        Sort {
143            field: crate::row::Field::Pid,
144            dir: Dir::Asc,
145        }
146    }
147}
148
149/// The derived, displayable view over the row storage.
150#[derive(Default)]
151pub struct View {
152    /// Indices into `rows`, already sorted and filtered.
153    pub order: Vec<usize>,
154}
155
156/// Sort `order` in place using `field`, without touching `rows`.
157pub fn sort_order(rows: &[Row], order: &mut [usize], sort: Sort) {
158    // One comparison pass, so direction and tie-breaking are decided in one place.
159    //
160    // This used to be a `match` that sorted each field its own way, then a
161    // `reverse()` of the whole vector for descending. Two problems came from that:
162    //
163    // 1. `reverse()` reversed every *tie* as well. On a quiet machine where dozens
164    //    of processes sit at 0.0% CPU, "sort by CPU%, descending" produced
165    //    10, 9, 8, … — a pid list running backwards, which reads as a bug rather
166    //    than as "all of these are equal".
167    //
168    // 2. `sort_unstable_by` gives ties no guaranteed order, so equal rows could
169    //    swap places between samples even without the reversal.
170    //
171    // Reversing the *comparison* rather than the vector, and keeping the pid
172    // tie-break outside the reversal, fixes both: an equal group reads in
173    // ascending pid order in either direction, and never shuffles.
174    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
182/// Compare two rows by `field`, ascending. Used by the descending sort so the
183/// reversal can be applied to the comparison instead of to the finished vector.
184fn 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        // Identity and session fields. Each needs its own comparator: `_` is the
206        // pid comparator, so a column without one sorts by pid while claiming to
207        // sort by something else.
208        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        // The path fields are `Option<Box<str>>`: `None` (another user's process)
216        // sorts below any real path, the same rule the PSS family follows for a
217        // figure that was never read.
218        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        // The PSS family. `None` (not read, or not readable) sorts below any real
246        // figure in both directions, because an unknown cost is not a small one —
247        // but the tie-break on pid keeps the placeholder rows in a stable order.
248        //
249        // Sorting by these is exactly when the sampler is told to read
250        // `smaps_rollup`, so the column that orders the list is also the column
251        // that pays for it.
252        #[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        // Delay percentages. Unread (`None`) sorts as zero, which puts it with
283        // processes that measured zero delay rather than above them: a row that
284        // was never read must not outrank one that was.
285        #[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/// Case-insensitive fixed-string filter, `|`-separated, matching htop's F4.
305#[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    /// A row matches if **any** term is a substring of its command line.
325    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
331/// Case-insensitive incremental search (F3). Unlike the filter, this only
332/// *selects* the next match.
333pub 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/// The complete visibility predicate: command filter, owner, kernel threads.
353///
354/// Kept in one place so the table, the tree view and any future export all agree
355/// on what "visible" means.
356#[derive(Clone, Debug)]
357pub struct Visibility {
358    /// Command-line terms from `F4`.
359    pub text: Option<Filter>,
360    /// Only this uid, from `u`.
361    pub uid: Option<u32>,
362    /// Whether kernel threads are shown (from `K` / `H`).
363    pub show_kernel_threads: bool,
364    /// Whether thread rows are shown (from `H`).
365    pub show_userland_threads: bool,
366    /// Whether processes inside a container are shown (htop's `O`).
367    ///
368    /// Linux-only in effect: `Row::container` is only ever set there, so on macOS
369    /// this is a no-op rather than a key that silently hides everything.
370    pub show_containerised: bool,
371}
372
373impl Default for Visibility {
374    /// Everything visible.
375    ///
376    /// Written out rather than derived: `derive(Default)` sets every `bool` to
377    /// `false`, which for `show_kernel_threads` and `show_containerised` means
378    /// *hidden*. A default that quietly hides rows is a trap — the first
379    /// `Visibility::default()` would have made an empty-looking table.
380    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            // A thread has no cgroup of its own; it is in whatever its process is
401            // in, and the sampler copies that down, so this works for thread rows
402            // without a special case.
403            #[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
420/// Apply the current filter to produce the display order.
421pub 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
428/// State of the table across ticks.
429pub 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    /// Whether processes inside a container are listed (htop's `O`).
437    pub show_containerised: bool,
438}
439
440impl Default for ProcTable {
441    /// Toggles mean "hidden" when `false`, so they are spelled out rather than
442    /// derived — `derive(Default)` would hide kernel threads and container
443    /// processes by default.
444    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    /// Recompute the display order from the current rows.
459    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            // The tree needs every row present to reconstruct parentage, so it is
466            // built over the full set and the visible subset is taken afterwards.
467            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    /// Number of rows currently displayed.
478    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    /// Sum of `percent_cpu` over displayed rows, used by the USED meter label.
487    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
496/// Recompute per-process CPU% and MEM% from two samples.
497///
498/// `docs/DESIGN.md` §3.5.
499pub 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            // May exceed 100% on multicore: that is correct and matches htop.
508            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        // searching from the last position wraps to the first match
572        assert_eq!(find_next(&rows, &order, 1, "alpha"), Some(0));
573    }
574
575    /// Rows with equal keys must keep a fixed, ascending-pid order in *both*
576    /// directions.
577    ///
578    /// Two bugs hid here. `reverse()` applied to the finished vector also reversed
579    /// every tie, so descending CPU% on a quiet machine listed pids backwards
580    /// (10, 9, 8, …) — which reads as a broken sort. And `sort_unstable_by` leaves
581    /// ties unordered, so equal rows could swap between samples even without the
582    /// reversal. Both show up as "the list jumps around", which is what made this
583    /// worth fixing rather than tolerating.
584    #[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                // Repeated so an unstable sort has a chance to show itself.
597                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    /// Direction must still apply to the primary key when it is not tied.
610    #[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    /// PID ascending must be exactly that: the default the app now ships.
634    #[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        // The toggles mean "hidden" when false, so a derived Default would hide
659        // kernel threads and containers and make a fresh table look empty.
660        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        // A host process must survive the filter: the whole point of `O` is to
714        // leave the host's own processes readable.
715        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        // A thread has no cgroup of its own; the sampler copies the process's, so
722        // the filter has to work on thread rows without a special case.
723        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}