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}