Skip to main content

monitrs_core/process/
mod.rs

1//! Process list logic: ordering, filtering, and tree construction.
2//!
3//! This module is the *list* half of the process view. The row data itself lives
4//! in [`crate::model::ProcessSnapshot`]; nothing here mutates a snapshot, because
5//! §10.4 requires published snapshots to stay immutable. Every entry point
6//! therefore returns indices into the slice it was given, in display order.
7//!
8//! # What §7.2 demands of this module
9//!
10//! * **Stable sorting with a PID/start-time tie-breaker.** Every comparison ends
11//!   in a [`crate::model::ProcessIdentity`] comparison, so rows with equal keys
12//!   keep the same relative order on every refresh and selection does not jump.
13//! * **Unavailable is not zero** (§26). A metric that was never measured sorts to
14//!   the end of the list in *both* directions instead of pretending to be `0`.
15//! * **Plain text filtering** over name, command, PID, and user, with the
16//!   user-only and hide-kernel-threads toggles expressed as separate composable
17//!   predicates.
18//! * **Tree mode** that preserves parent-child relationships, keeps children
19//!   under their parents when sorted, and survives a malformed parent graph.
20//!
21//! # Rendering a tree row
22//!
23//! [`ProcessTree`] emits a flat pre-order list of [`TreeRow`]s carrying a depth,
24//! a last-sibling flag, and (via [`ProcessTree::continuation_flags`]) whether an
25//! ancestor still has siblings below it. That is everything a renderer needs for
26//! the strict-ASCII shape §5.1 requires:
27//!
28//! ```text
29//! systemd
30//! +- sshd
31//! |  `- bash
32//! `- cron
33//! ```
34//!
35//! # Example
36//!
37//! ```
38//! use monitrs_core::process::{ProcessFilter, ProcessSort, flat_order};
39//!
40//! # fn view(processes: &[monitrs_core::model::ProcessSnapshot]) -> Vec<usize> {
41//! let filter = ProcessFilter::parse("rustc");
42//! flat_order(processes, &filter, ProcessSort::default())
43//! # }
44//! ```
45
46use std::borrow::Borrow;
47
48use crate::model::ProcessSnapshot;
49
50mod filter;
51mod sort;
52mod subtree;
53mod tree;
54
55#[cfg(test)]
56mod fixtures;
57
58pub use filter::{FilterPattern, PlainPattern, ProcessFilter, ProcessPredicate};
59pub use sort::{ProcessSort, ProcessSortKey, SortDirection, UnknownSortKey};
60pub use subtree::{Coverage, SubtreeUsage, Summed};
61pub use tree::{ProcessTree, TreeRow};
62
63/// The visible rows of the flat process view, in display order.
64///
65/// Filters first and sorts second, and returns indices into `processes` rather
66/// than clones: the snapshot is shared behind an `Arc` and must not be copied per
67/// tick (§10.4, §16.1).
68///
69/// Accepts both `&[ProcessSnapshot]` and `&[&ProcessSnapshot]` so a caller that
70/// already narrowed the list does not have to clone rows to sort them.
71#[must_use]
72pub fn flat_order<P: Borrow<ProcessSnapshot>>(
73    processes: &[P],
74    filter: &ProcessFilter,
75    sort: ProcessSort,
76) -> Vec<usize> {
77    let mut rows: Vec<(usize, &ProcessSnapshot)> = processes
78        .iter()
79        .map(Borrow::borrow)
80        .enumerate()
81        .filter(|(_, process)| filter.matches(process))
82        .collect();
83    rows.sort_by(|(_, left), (_, right)| sort.compare(left, right));
84    rows.into_iter().map(|(index, _)| index).collect()
85}
86
87#[cfg(test)]
88mod tests {
89    use super::fixtures::process;
90    use super::*;
91    use crate::model::ProcessState;
92
93    #[test]
94    fn the_flat_view_filters_before_it_sorts() {
95        let processes = vec![
96            process(1, 1).name("systemd").cpu(1.0).build(),
97            process(2, 2).name("rustc").cpu(50.0).build(),
98            process(3, 3).name("rustc").cpu(90.0).build(),
99        ];
100        let order = flat_order(
101            &processes,
102            &ProcessFilter::parse("rustc"),
103            ProcessSort::default(),
104        );
105        assert_eq!(order, vec![2, 1], "only rustc rows, hottest first");
106    }
107
108    #[test]
109    fn an_inactive_filter_keeps_every_row() {
110        let processes = vec![
111            process(1, 1).cpu(1.0).build(),
112            process(2, 2).cpu(2.0).build(),
113        ];
114        let order = flat_order(&processes, &ProcessFilter::new(), ProcessSort::default());
115        assert_eq!(order.len(), processes.len());
116    }
117
118    #[test]
119    fn the_flat_view_accepts_borrowed_rows_without_cloning_them() {
120        let processes = [
121            process(7, 7).cpu(5.0).build(),
122            process(8, 8).cpu(9.0).build(),
123        ];
124        let borrowed: Vec<&ProcessSnapshot> = processes.iter().collect();
125        let order = flat_order(&borrowed, &ProcessFilter::new(), ProcessSort::default());
126        assert_eq!(order, vec![1, 0]);
127    }
128
129    #[test]
130    fn composed_toggles_and_text_all_apply_to_the_flat_view() {
131        let processes = vec![
132            process(1, 1)
133                .name("kworker/0:1")
134                .user(0, Some("root"))
135                .kernel_thread()
136                .build(),
137            process(2, 2).name("cargo").user(501, Some("gabor")).build(),
138            process(3, 3).name("cargo").user(0, Some("root")).build(),
139            process(4, 4)
140                .name("zsh")
141                .user(501, Some("gabor"))
142                .state(ProcessState::Sleeping)
143                .build(),
144        ];
145        let filter = ProcessFilter::parse("cargo")
146            .with_only_user(Some(501))
147            .with_hidden_kernel_threads(true);
148        let order = flat_order(&processes, &filter, ProcessSort::default());
149        assert_eq!(order, vec![1]);
150    }
151}