Skip to main content

monitrs_core/process/
tree.rs

1//! Process tree construction (§7.2 tree mode, §2.4 process context).
2//!
3//! A snapshot gives each process a bare `parent_pid` (§ the model deliberately
4//! does not pretend to know the parent's start key), so the tree is resolved here
5//! against the rest of the table. The result is a flat, pre-order list of
6//! [`TreeRow`]s carrying depth, sibling position, descendant count, and parent
7//! links — everything the renderer and the §2.4 breadcrumb need, with no borrowed
8//! recursion for the UI to walk.
9//!
10//! # The three hostile cases (§17.1, §17.7)
11//!
12//! A process table is a *racing* read: every row was captured at a slightly
13//! different moment, so the parent graph can be malformed in ways a well-behaved
14//! kernel never produces.
15//!
16//! * **Missing parent.** A parent that exited between two reads is simply not in
17//!   the table. Its children become roots. They are never dropped: a process the
18//!   OS reported must appear on screen exactly once, or the table silently lies
19//!   about what is running.
20//! * **Self-parent.** `parent_pid == pid` is a 1-cycle. The link is cut and the
21//!   row becomes a root.
22//! * **Cycles.** A racing read (or a reused PID landing on an ancestor) can
23//!   produce `a -> b -> a`. Nothing here recurses: construction is a bounded
24//!   iteration over explicit stacks, so a cycle cannot overflow the stack. Each
25//!   cycle has exactly one link cut, and the victim is the lowest
26//!   [`ProcessIdentity`] in the cycle — a property of the cycle itself, not of the
27//!   order the table was enumerated in, so two refreshes cut the same link and the
28//!   tree does not reshuffle.
29//!
30//! Every one of those is *expected*, not an error (§14.1), which is why the only
31//! trace they leave is [`TreeRow::parent_link_cut`] and
32//! [`ProcessTree::cycles_broken`].
33
34use core::cmp::Ordering;
35use std::borrow::Borrow;
36use std::collections::HashMap;
37use std::collections::hash_map::Entry;
38
39use crate::model::{AncestorEntry, ProcessIdentity, ProcessSnapshot, SystemSnapshot};
40use crate::process::{ProcessFilter, ProcessSort};
41
42/// One row of a rendered process tree.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub struct TreeRow {
45    /// The stable identity of the process on this row (§26).
46    pub identity: ProcessIdentity,
47    /// Index of the process in the slice the tree was built from.
48    ///
49    /// Rows are indices rather than clones because a published snapshot is shared
50    /// behind an `Arc` and must not be duplicated per tick (§10.4, §16.1).
51    pub process_index: usize,
52    /// Indentation level: `0` for a root.
53    ///
54    /// `u32` rather than `u16` so a pathological chain cannot wrap; it saturates
55    /// instead.
56    pub depth: u32,
57    /// Row index of this row's parent, always smaller than this row's own index.
58    pub parent_row: Option<usize>,
59    /// Total descendants, direct and indirect (§2.4).
60    pub descendants: u32,
61    /// Whether this row is the last of its sibling group.
62    ///
63    /// Selects `` `- `` over `+- ` when rendering, and for a depth-`0` row refers
64    /// to the group of roots.
65    pub is_last_child: bool,
66    /// Whether this row's parent link was cut to break a cycle.
67    ///
68    /// A *missing* parent does not set this: it is ordinary and expected, whereas a
69    /// cut link means the OS reported something impossible and the placement of
70    /// this subtree is our decision rather than the kernel's.
71    pub parent_link_cut: bool,
72}
73
74/// A process tree flattened into display order.
75#[derive(Clone, Debug, Default)]
76pub struct ProcessTree {
77    rows: Vec<TreeRow>,
78    cycles_broken: u32,
79}
80
81impl ProcessTree {
82    /// Builds the tree of every process in `snapshot`.
83    #[must_use]
84    pub fn from_snapshot(snapshot: &SystemSnapshot, sort: ProcessSort) -> Self {
85        Self::build(&snapshot.processes, sort)
86    }
87
88    /// Builds the tree of the processes in `snapshot` that pass `filter`.
89    #[must_use]
90    pub fn from_snapshot_filtered(
91        snapshot: &SystemSnapshot,
92        sort: ProcessSort,
93        filter: &ProcessFilter,
94    ) -> Self {
95        Self::build_filtered(&snapshot.processes, sort, filter)
96    }
97
98    /// Builds the tree of every process in `processes`.
99    #[must_use]
100    pub fn build<P: Borrow<ProcessSnapshot>>(processes: &[P], sort: ProcessSort) -> Self {
101        Self::build_filtered(processes, sort, &ProcessFilter::new())
102    }
103
104    /// Builds the tree of the processes that pass `filter`.
105    ///
106    /// A hidden process does not orphan its children: they re-attach to the
107    /// nearest *surviving* ancestor, so hiding kernel threads or filtering by name
108    /// reshapes the tree without scattering unrelated rows to the root. Rows that
109    /// do not pass the filter simply do not appear.
110    #[must_use]
111    pub fn build_filtered<P: Borrow<ProcessSnapshot>>(
112        processes: &[P],
113        sort: ProcessSort,
114        filter: &ProcessFilter,
115    ) -> Self {
116        let all: Vec<&ProcessSnapshot> = processes.iter().map(Borrow::borrow).collect();
117        let count = all.len();
118        if count == 0 {
119            return Self::default();
120        }
121        let retained: Vec<bool> = all.iter().map(|process| filter.matches(process)).collect();
122
123        let by_pid = index_by_pid(&all);
124        let (mut parent, mut cut) = resolve_parents(&all, &by_pid);
125        break_cycles(&all, &mut parent, &mut cut);
126        let nearest = nearest_retained(&parent, &retained);
127
128        let (roots, children) = group_children(&nearest, &retained, count);
129        let rows = emit_rows(&all, &cut, sort, roots, children);
130        let cycles_broken =
131            u32::try_from(cut.iter().filter(|link| **link).count()).unwrap_or(u32::MAX);
132
133        Self {
134            rows,
135            cycles_broken,
136        }
137    }
138
139    /// Every row, in display order.
140    #[must_use]
141    pub fn rows(&self) -> &[TreeRow] {
142        &self.rows
143    }
144
145    /// How many rows the tree has.
146    #[must_use]
147    pub fn len(&self) -> usize {
148        self.rows.len()
149    }
150
151    /// Whether the tree has no rows.
152    #[must_use]
153    pub fn is_empty(&self) -> bool {
154        self.rows.is_empty()
155    }
156
157    /// The row at `index`, if there is one.
158    #[must_use]
159    pub fn row(&self, index: usize) -> Option<&TreeRow> {
160        self.rows.get(index)
161    }
162
163    /// The row showing `identity`, if it is in the tree.
164    ///
165    /// Keyed on the full identity, so a reused PID resolves to nothing rather than
166    /// to the wrong row (§26).
167    #[must_use]
168    pub fn row_of(&self, identity: ProcessIdentity) -> Option<usize> {
169        self.rows.iter().position(|row| row.identity == identity)
170    }
171
172    /// How many parent links were cut to break a cycle, self-parents included.
173    ///
174    /// Zero on any sane system. A non-zero value is worth surfacing as collector
175    /// health rather than hiding, because it means the process table was read
176    /// while the graph was inconsistent.
177    #[must_use]
178    pub const fn cycles_broken(&self) -> u32 {
179        self.cycles_broken
180    }
181
182    /// The deepest indentation level in the tree.
183    #[must_use]
184    pub fn max_depth(&self) -> u32 {
185        self.rows.iter().map(|row| row.depth).max().unwrap_or(0)
186    }
187
188    /// The number of rows this row occupies together with its subtree.
189    #[must_use]
190    pub fn subtree_len(&self, index: usize) -> usize {
191        self.rows
192            .get(index)
193            .map_or(0, |row| usize::try_from(row.descendants).unwrap_or(0) + 1)
194    }
195
196    /// The direct children of a row, in display order (§2.4 child navigation).
197    #[must_use]
198    pub fn child_rows(&self, index: usize) -> Vec<usize> {
199        let Some(row) = self.rows.get(index) else {
200            return Vec::new();
201        };
202        let child_depth = row.depth.saturating_add(1);
203        let mut children = Vec::new();
204        for (candidate_index, candidate) in self.rows.iter().enumerate().skip(index + 1) {
205            if candidate.depth <= row.depth {
206                break;
207            }
208            if candidate.depth == child_depth {
209                children.push(candidate_index);
210            }
211        }
212        children
213    }
214
215    /// The ancestors of a row, nearest parent first (§2.4 breadcrumb).
216    ///
217    /// Terminates because `parent_row` is always a smaller index than the row it
218    /// belongs to: pre-order emission guarantees it, so the walk strictly
219    /// decreases.
220    #[must_use]
221    pub fn ancestor_rows(&self, index: usize) -> Vec<usize> {
222        let mut ancestors = Vec::new();
223        let mut cursor = self.rows.get(index).and_then(|row| row.parent_row);
224        while let Some(current) = cursor {
225            ancestors.push(current);
226            cursor = self.rows.get(current).and_then(|row| row.parent_row);
227        }
228        ancestors
229    }
230
231    /// Whether a vertical continuation line is needed at each level above a row.
232    ///
233    /// Element `i` covers indentation level `i` (root-most first) and is `true`
234    /// when the ancestor at that level still has siblings below it, so the renderer
235    /// draws `|  ` there and three spaces otherwise. The row's own connector comes
236    /// from [`TreeRow::is_last_child`].
237    #[must_use]
238    pub fn continuation_flags(&self, index: usize) -> Vec<bool> {
239        let mut flags: Vec<bool> = self
240            .ancestor_rows(index)
241            .into_iter()
242            .filter_map(|ancestor| self.rows.get(ancestor))
243            .map(|ancestor| !ancestor.is_last_child)
244            .collect();
245        flags.reverse();
246        flags
247    }
248
249    /// The process a row refers to, revalidated against its identity.
250    ///
251    /// Returns `None` when `processes` is not the slice the tree was built from, so
252    /// a stale tree paired with a fresh snapshot yields nothing instead of the
253    /// wrong process (§26: a PID is not an identity).
254    #[must_use]
255    pub fn process<'a, P: Borrow<ProcessSnapshot>>(
256        &self,
257        processes: &'a [P],
258        index: usize,
259    ) -> Option<&'a ProcessSnapshot> {
260        let row = self.rows.get(index)?;
261        processes
262            .get(row.process_index)
263            .map(Borrow::borrow)
264            .filter(|process| process.identity == row.identity)
265    }
266
267    /// The §2.4 ancestry breadcrumb for a row, nearest parent first.
268    ///
269    /// Matches the ordering of [`crate::model::ProcessDetail::ancestry`] so the
270    /// live tree and an on-demand detail read render identically. Entries that
271    /// cannot be revalidated against `processes` are skipped rather than guessed.
272    #[must_use]
273    pub fn ancestry<P: Borrow<ProcessSnapshot>>(
274        &self,
275        processes: &[P],
276        index: usize,
277    ) -> Vec<AncestorEntry> {
278        self.ancestor_rows(index)
279            .into_iter()
280            .filter_map(|ancestor| self.process(processes, ancestor))
281            .map(|process| AncestorEntry {
282                identity: process.identity,
283                name: process.name.clone(),
284            })
285            .collect()
286    }
287}
288
289/// Maps each PID to the index of the process that owns it.
290///
291/// A snapshot should never contain two entries for one PID, but a racing read can
292/// produce one. The lowest `start_key` wins, which is a property of the processes
293/// rather than of enumeration order, so the choice is the same on every refresh.
294fn index_by_pid(all: &[&ProcessSnapshot]) -> HashMap<u32, usize> {
295    let mut by_pid: HashMap<u32, usize> = HashMap::with_capacity(all.len());
296    for (index, process) in all.iter().enumerate() {
297        match by_pid.entry(process.identity.pid) {
298            Entry::Vacant(slot) => {
299                slot.insert(index);
300            }
301            Entry::Occupied(mut slot) => {
302                let incumbent = all.get(*slot.get()).map(|other| other.identity.start_key);
303                if incumbent.is_some_and(|key| process.identity.start_key < key) {
304                    slot.insert(index);
305                }
306            }
307        }
308    }
309    by_pid
310}
311
312/// Resolves every `parent_pid` to an index, cutting self-parents.
313///
314/// Returns the parent of each process and which parent links were cut.
315fn resolve_parents(
316    all: &[&ProcessSnapshot],
317    by_pid: &HashMap<u32, usize>,
318) -> (Vec<Option<usize>>, Vec<bool>) {
319    let mut parent: Vec<Option<usize>> = Vec::with_capacity(all.len());
320    let mut cut: Vec<bool> = vec![false; all.len()];
321    for (index, process) in all.iter().enumerate() {
322        let resolved = process
323            .parent_pid
324            .and_then(|pid| by_pid.get(&pid).copied())
325            .filter(|&candidate| candidate != index);
326        if resolved.is_none()
327            && process.parent_pid == Some(process.identity.pid)
328            && let Some(link) = cut.get_mut(index)
329        {
330            *link = true;
331        }
332        parent.push(resolved);
333    }
334    (parent, cut)
335}
336
337/// Marks used while walking parent chains to find cycles.
338#[derive(Clone, Copy, Eq, PartialEq)]
339enum Mark {
340    /// Not yet visited by any walk.
341    New,
342    /// On the chain the current walk is following.
343    OnChain,
344    /// Known to lead to a root.
345    Done,
346}
347
348/// Cuts one link in every cycle so every parent chain terminates.
349///
350/// Each process is pushed onto the chain at most once across all walks, so this is
351/// linear in the number of processes and cannot recurse (§17.7).
352fn break_cycles(all: &[&ProcessSnapshot], parent: &mut [Option<usize>], cut: &mut [bool]) {
353    let count = parent.len();
354    let mut marks: Vec<Mark> = vec![Mark::New; count];
355    let mut chain: Vec<usize> = Vec::new();
356
357    for start in 0..count {
358        if marks.get(start).copied() != Some(Mark::New) {
359            continue;
360        }
361        chain.clear();
362        let mut cursor = start;
363        loop {
364            match marks.get(cursor).copied() {
365                Some(Mark::New) => {
366                    if let Some(mark) = marks.get_mut(cursor) {
367                        *mark = Mark::OnChain;
368                    }
369                    chain.push(cursor);
370                    match parent.get(cursor).copied().flatten() {
371                        Some(next) => cursor = next,
372                        None => break,
373                    }
374                }
375                Some(Mark::OnChain) => {
376                    // `cursor` closes a cycle with the suffix of the chain that
377                    // starts at it. Cutting the lowest identity in that suffix is
378                    // deterministic given the cycle, so refreshes agree.
379                    let suffix_start = chain.iter().position(|&node| node == cursor);
380                    let victim = suffix_start
381                        .and_then(|position| chain.get(position..))
382                        .and_then(|cycle| {
383                            cycle
384                                .iter()
385                                .copied()
386                                .min_by_key(|&node| all.get(node).map(|process| process.identity))
387                        });
388                    if let Some(victim) = victim {
389                        if let Some(link) = parent.get_mut(victim) {
390                            *link = None;
391                        }
392                        if let Some(flag) = cut.get_mut(victim) {
393                            *flag = true;
394                        }
395                    }
396                    break;
397                }
398                Some(Mark::Done) | None => break,
399            }
400        }
401        for &node in &chain {
402            if let Some(mark) = marks.get_mut(node) {
403                *mark = Mark::Done;
404            }
405        }
406    }
407}
408
409/// For each process, the nearest strict ancestor that survived the filter.
410///
411/// Memoized and iterative: each process joins the working chain at most once, so
412/// this stays linear even when a long chain is filtered out entirely.
413fn nearest_retained(parent: &[Option<usize>], retained: &[bool]) -> Vec<Option<usize>> {
414    let count = parent.len();
415    let mut nearest: Vec<Option<usize>> = vec![None; count];
416    let mut resolved: Vec<bool> = vec![false; count];
417    let mut chain: Vec<usize> = Vec::new();
418
419    for start in 0..count {
420        if resolved.get(start).copied() == Some(true) {
421            continue;
422        }
423        chain.clear();
424        let mut cursor = start;
425        // Walk up to the first already-resolved ancestor, or to a root. Cycles are
426        // already broken, so this terminates.
427        let inherited = loop {
428            chain.push(cursor);
429            if chain.len() > count {
430                // Unreachable while `break_cycles` holds: a chain longer than the
431                // table must repeat a node. Bounding it anyway means a bug there
432                // degrades to a flatter tree instead of hanging the collector.
433                break None;
434            }
435            match parent.get(cursor).copied().flatten() {
436                None => break None,
437                Some(above) => {
438                    if resolved.get(above).copied() == Some(true) {
439                        break if retained.get(above).copied() == Some(true) {
440                            Some(above)
441                        } else {
442                            nearest.get(above).copied().flatten()
443                        };
444                    }
445                    cursor = above;
446                }
447            }
448        };
449
450        let mut accumulated = inherited;
451        for &node in chain.iter().rev() {
452            if let Some(slot) = nearest.get_mut(node) {
453                *slot = accumulated;
454            }
455            if let Some(flag) = resolved.get_mut(node) {
456                *flag = true;
457            }
458            if retained.get(node).copied() == Some(true) {
459                accumulated = Some(node);
460            }
461        }
462    }
463    nearest
464}
465
466/// Splits the retained processes into roots and per-parent child lists.
467fn group_children(
468    nearest: &[Option<usize>],
469    retained: &[bool],
470    count: usize,
471) -> (Vec<usize>, Vec<Vec<usize>>) {
472    let mut roots: Vec<usize> = Vec::new();
473    let mut children: Vec<Vec<usize>> = vec![Vec::new(); count];
474    for (index, keep) in retained.iter().enumerate() {
475        if !keep {
476            continue;
477        }
478        match nearest.get(index).copied().flatten() {
479            Some(parent) => {
480                if let Some(list) = children.get_mut(parent) {
481                    list.push(index);
482                }
483            }
484            None => roots.push(index),
485        }
486    }
487    (roots, children)
488}
489
490/// Emits the tree as a pre-order row list, sorting each sibling group.
491///
492/// Sorting sibling groups rather than the whole table is what keeps children under
493/// their parents while still honouring the §7.2 sort: the busiest root comes first,
494/// and within it the busiest child.
495fn emit_rows(
496    all: &[&ProcessSnapshot],
497    cut: &[bool],
498    sort: ProcessSort,
499    mut roots: Vec<usize>,
500    mut children: Vec<Vec<usize>>,
501) -> Vec<TreeRow> {
502    let compare = |left: &usize, right: &usize| -> Ordering {
503        match (all.get(*left), all.get(*right)) {
504            (Some(first), Some(second)) => sort.compare(first, second),
505            // Unreachable: every index comes from `all`. Ordering by index keeps
506            // the comparator total anyway.
507            _ => left.cmp(right),
508        }
509    };
510    roots.sort_by(compare);
511    for group in &mut children {
512        group.sort_by(compare);
513    }
514
515    let mut rows: Vec<TreeRow> = Vec::with_capacity(all.len());
516    let mut stack: Vec<Frame> = Vec::new();
517    push_group(&mut stack, &roots, 0, None);
518
519    while let Some(frame) = stack.pop() {
520        let Some(process) = all.get(frame.node) else {
521            continue;
522        };
523        let row_index = rows.len();
524        rows.push(TreeRow {
525            identity: process.identity,
526            process_index: frame.node,
527            depth: frame.depth,
528            parent_row: frame.parent_row,
529            descendants: 0,
530            is_last_child: frame.is_last,
531            parent_link_cut: cut.get(frame.node).copied().unwrap_or(false),
532        });
533        if let Some(group) = children.get(frame.node) {
534            push_group(
535                &mut stack,
536                group,
537                frame.depth.saturating_add(1),
538                Some(row_index),
539            );
540        }
541    }
542
543    fill_descendants(&mut rows);
544    rows
545}
546
547/// One pending row, so emission is an explicit stack rather than recursion.
548struct Frame {
549    /// Index of the process in the table being walked.
550    node: usize,
551    /// Indentation level this row will be emitted at.
552    depth: u32,
553    /// Whether the row closes its sibling group.
554    is_last: bool,
555    /// Row index of the already-emitted parent.
556    parent_row: Option<usize>,
557}
558
559/// Pushes a sibling group so that the first sibling is popped first.
560fn push_group(stack: &mut Vec<Frame>, group: &[usize], depth: u32, parent_row: Option<usize>) {
561    for (position, &node) in group.iter().enumerate().rev() {
562        stack.push(Frame {
563            node,
564            depth,
565            is_last: position + 1 == group.len(),
566            parent_row,
567        });
568    }
569}
570
571/// Fills in [`TreeRow::descendants`] from the pre-order layout.
572///
573/// Walks backwards keeping the subtree sizes that have not yet found their parent.
574/// Everything after a row with a greater depth is inside that row's subtree, which
575/// makes this linear rather than a scan per row.
576fn fill_descendants(rows: &mut [TreeRow]) {
577    let mut pending: Vec<(u32, u32)> = Vec::new();
578    for row in rows.iter_mut().rev() {
579        let mut total: u32 = 0;
580        while let Some(&(depth, size)) = pending.last() {
581            if depth > row.depth {
582                total = total.saturating_add(size);
583                pending.pop();
584            } else {
585                break;
586            }
587        }
588        row.descendants = total;
589        pending.push((row.depth, total.saturating_add(1)));
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use proptest::prelude::*;
596
597    use super::super::fixtures::{process, snapshot};
598    use super::*;
599    use crate::process::{ProcessSortKey, SortDirection};
600
601    fn pids(tree: &ProcessTree, processes: &[ProcessSnapshot]) -> Vec<u32> {
602        tree.rows()
603            .iter()
604            .filter_map(|row| {
605                processes
606                    .get(row.process_index)
607                    .map(|process| process.identity.pid)
608            })
609            .collect()
610    }
611
612    fn shape(tree: &ProcessTree, processes: &[ProcessSnapshot]) -> Vec<String> {
613        tree.rows()
614            .iter()
615            .filter_map(|row| {
616                processes.get(row.process_index).map(|process| {
617                    let indent = "  ".repeat(usize::try_from(row.depth).unwrap_or(0));
618                    format!("{indent}{}", process.name)
619                })
620            })
621            .collect()
622    }
623
624    fn every_process_exactly_once(tree: &ProcessTree, count: usize) {
625        assert_eq!(tree.len(), count, "row count must equal process count");
626        let mut seen: Vec<usize> = tree.rows().iter().map(|row| row.process_index).collect();
627        seen.sort_unstable();
628        seen.dedup();
629        assert_eq!(seen.len(), count, "every process must appear exactly once");
630    }
631
632    #[test]
633    fn a_normal_tree_nests_children_under_their_parents() {
634        let processes = vec![
635            process(1, 1).name("systemd").build(),
636            process(100, 2).name("sshd").parent(1).build(),
637            process(200, 3).name("bash").parent(100).build(),
638            process(300, 4).name("cron").parent(1).build(),
639        ];
640        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
641        assert_eq!(
642            shape(&tree, &processes),
643            vec!["systemd", "  sshd", "    bash", "  cron"]
644        );
645        every_process_exactly_once(&tree, processes.len());
646        assert_eq!(tree.cycles_broken(), 0);
647        assert_eq!(tree.max_depth(), 2);
648    }
649
650    #[test]
651    fn a_process_whose_parent_is_missing_becomes_a_root_and_is_never_dropped() {
652        // PID 4242 exited between the two reads that produced this table.
653        let processes = vec![
654            process(1, 1).name("systemd").build(),
655            process(500, 2).name("orphan").parent(4242).build(),
656        ];
657        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
658        every_process_exactly_once(&tree, 2);
659        assert_eq!(shape(&tree, &processes), vec!["systemd", "orphan"]);
660        let orphan_row = tree.row_of(ProcessIdentity::new(500, 2)).expect("present");
661        let orphan = tree.row(orphan_row).expect("present");
662        assert_eq!(orphan.depth, 0);
663        assert_eq!(orphan.parent_row, None);
664        assert!(
665            !orphan.parent_link_cut,
666            "a missing parent is expected, not a broken graph"
667        );
668        assert_eq!(tree.cycles_broken(), 0);
669    }
670
671    #[test]
672    fn a_self_parent_becomes_a_flagged_root() {
673        let processes = vec![
674            process(1, 1).name("systemd").build(),
675            process(7, 2).name("ouroboros").parent(7).build(),
676        ];
677        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
678        every_process_exactly_once(&tree, 2);
679        let row_index = tree.row_of(ProcessIdentity::new(7, 2)).expect("present");
680        let row = tree.row(row_index).expect("present");
681        assert_eq!(row.depth, 0);
682        assert!(row.parent_link_cut);
683        assert_eq!(tree.cycles_broken(), 1);
684    }
685
686    #[test]
687    fn a_two_cycle_is_broken_deterministically_and_keeps_both_rows() {
688        let processes = vec![
689            process(10, 5).name("a").parent(20).build(),
690            process(20, 6).name("b").parent(10).build(),
691        ];
692        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
693        every_process_exactly_once(&tree, 2);
694        assert_eq!(tree.cycles_broken(), 1);
695        // The lowest identity in the cycle is PID 10, so it is the one promoted.
696        assert_eq!(shape(&tree, &processes), vec!["a", "  b"]);
697
698        // The same table enumerated the other way round must cut the same link.
699        let reversed: Vec<ProcessSnapshot> = processes.iter().rev().cloned().collect();
700        let other = ProcessTree::build(&reversed, ProcessSort::ascending(ProcessSortKey::Pid));
701        assert_eq!(shape(&other, &reversed), vec!["a", "  b"]);
702    }
703
704    #[test]
705    fn a_three_cycle_is_broken_and_keeps_every_row() {
706        let processes = vec![
707            process(30, 1).name("c").parent(20).build(),
708            process(20, 1).name("b").parent(10).build(),
709            process(10, 1).name("a").parent(30).build(),
710        ];
711        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
712        every_process_exactly_once(&tree, 3);
713        assert_eq!(tree.cycles_broken(), 1);
714        assert_eq!(shape(&tree, &processes), vec!["a", "  b", "    c"]);
715        assert_eq!(tree.max_depth(), 2);
716    }
717
718    #[test]
719    fn a_cycle_with_an_outside_subtree_attached_keeps_everything() {
720        let processes = vec![
721            process(10, 1).name("a").parent(20).build(),
722            process(20, 1).name("b").parent(10).build(),
723            process(30, 1).name("child-of-b").parent(20).build(),
724            process(40, 1).name("grandchild").parent(30).build(),
725        ];
726        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
727        every_process_exactly_once(&tree, 4);
728        assert_eq!(
729            shape(&tree, &processes),
730            vec!["a", "  b", "    child-of-b", "      grandchild"]
731        );
732    }
733
734    #[test]
735    fn two_independent_cycles_are_both_broken() {
736        let processes = vec![
737            process(10, 1).name("a").parent(11).build(),
738            process(11, 1).name("b").parent(10).build(),
739            process(20, 1).name("c").parent(21).build(),
740            process(21, 1).name("d").parent(20).build(),
741        ];
742        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
743        every_process_exactly_once(&tree, 4);
744        assert_eq!(tree.cycles_broken(), 2);
745    }
746
747    #[test]
748    fn a_ten_thousand_deep_chain_does_not_overflow_the_stack() {
749        // §17.1/§17.7: construction must be iterative. A recursive builder dies
750        // here, and so does a recursive descendant count.
751        let depth: u32 = 10_000;
752        let processes: Vec<ProcessSnapshot> = (1..=depth)
753            .map(|pid| {
754                let fixture = process(pid, u64::from(pid));
755                if pid == 1 {
756                    fixture.build()
757                } else {
758                    fixture.parent(pid - 1).build()
759                }
760            })
761            .collect();
762        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
763        every_process_exactly_once(&tree, processes.len());
764        assert_eq!(tree.max_depth(), depth - 1);
765        let root = tree.row(0).expect("a root");
766        assert_eq!(root.depth, 0);
767        assert_eq!(root.descendants, depth - 1);
768        assert_eq!(tree.subtree_len(0), processes.len());
769        assert_eq!(tree.ancestor_rows(tree.len() - 1).len(), 9_999);
770    }
771
772    #[test]
773    fn a_ten_thousand_long_cycle_terminates() {
774        let length: u32 = 10_000;
775        let processes: Vec<ProcessSnapshot> = (1..=length)
776            .map(|pid| {
777                let parent = if pid == 1 { length } else { pid - 1 };
778                process(pid, u64::from(pid)).parent(parent).build()
779            })
780            .collect();
781        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
782        every_process_exactly_once(&tree, processes.len());
783        assert_eq!(tree.cycles_broken(), 1);
784    }
785
786    #[test]
787    fn descendants_count_direct_and_indirect_children() {
788        let processes = vec![
789            process(1, 1).name("root").build(),
790            process(2, 1).name("a").parent(1).build(),
791            process(3, 1).name("a1").parent(2).build(),
792            process(4, 1).name("a2").parent(2).build(),
793            process(5, 1).name("b").parent(1).build(),
794            process(6, 1).name("lonely").build(),
795        ];
796        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
797        let descendants = |pid: u32, start_key: u64| {
798            let index = tree
799                .row_of(ProcessIdentity::new(pid, start_key))
800                .expect("present");
801            tree.row(index).expect("present").descendants
802        };
803        assert_eq!(descendants(1, 1), 4);
804        assert_eq!(descendants(2, 1), 2);
805        assert_eq!(descendants(3, 1), 0);
806        assert_eq!(descendants(5, 1), 0);
807        assert_eq!(descendants(6, 1), 0);
808    }
809
810    #[test]
811    fn sorting_reorders_siblings_without_moving_them_out_of_their_parent() {
812        let processes = vec![
813            process(1, 1).name("root").cpu(1.0).build(),
814            process(2, 1).name("quiet").parent(1).cpu(1.0).build(),
815            process(3, 1).name("busy").parent(1).cpu(90.0).build(),
816            process(4, 1).name("busy-child").parent(2).cpu(99.0).build(),
817        ];
818        let tree = ProcessTree::build(&processes, ProcessSort::default());
819        assert_eq!(
820            shape(&tree, &processes),
821            vec!["root", "  busy", "  quiet", "    busy-child"],
822            "the hottest process stays under its parent"
823        );
824    }
825
826    #[test]
827    fn sibling_order_is_stable_when_their_values_are_equal() {
828        let build = |order: [u32; 3]| {
829            let mut processes = vec![process(1, 1).name("root").build()];
830            for pid in order {
831                processes.push(
832                    process(pid, u64::from(pid))
833                        .name(&format!("child{pid}"))
834                        .parent(1)
835                        .cpu(0.0)
836                        .build(),
837                );
838            }
839            let tree = ProcessTree::build(&processes, ProcessSort::default());
840            shape(&tree, &processes)
841        };
842        assert_eq!(build([2, 3, 4]), build([4, 3, 2]));
843        assert_eq!(
844            build([3, 2, 4]),
845            vec!["root", "  child2", "  child3", "  child4"]
846        );
847    }
848
849    #[test]
850    fn roots_are_sorted_too() {
851        let processes = vec![
852            process(1, 1).name("quiet").cpu(1.0).build(),
853            process(2, 2).name("busy").cpu(80.0).build(),
854        ];
855        let tree = ProcessTree::build(&processes, ProcessSort::default());
856        assert_eq!(shape(&tree, &processes), vec!["busy", "quiet"]);
857        assert!(!tree.row(0).expect("present").is_last_child);
858        assert!(
859            tree.row(1).expect("present").is_last_child,
860            "the final root closes the top-level group"
861        );
862    }
863
864    #[test]
865    fn filtered_rows_disappear_and_their_children_join_the_nearest_survivor() {
866        let processes = vec![
867            process(1, 1).name("systemd").build(),
868            process(2, 1)
869                .name("kthreadd")
870                .parent(1)
871                .kernel_thread()
872                .build(),
873            process(3, 1)
874                .name("kworker/0")
875                .parent(2)
876                .kernel_thread()
877                .build(),
878            process(4, 1).name("app").parent(2).build(),
879        ];
880        let filter = ProcessFilter::new().with_hidden_kernel_threads(true);
881        let tree = ProcessTree::build_filtered(
882            &processes,
883            ProcessSort::ascending(ProcessSortKey::Pid),
884            &filter,
885        );
886        assert_eq!(
887            shape(&tree, &processes),
888            vec!["systemd", "  app"],
889            "app re-attaches to systemd rather than becoming a root"
890        );
891        assert_eq!(tree.len(), 2);
892    }
893
894    #[test]
895    fn a_filtered_out_root_leaves_its_children_as_roots() {
896        let processes = vec![
897            process(1, 1).name("systemd").build(),
898            process(2, 1).name("app").parent(1).build(),
899        ];
900        let filter = ProcessFilter::parse("app");
901        let tree = ProcessTree::build_filtered(&processes, ProcessSort::default(), &filter);
902        assert_eq!(shape(&tree, &processes), vec!["app"]);
903        assert_eq!(tree.row(0).expect("present").parent_row, None);
904    }
905
906    #[test]
907    fn a_filter_that_hides_a_long_chain_still_terminates() {
908        let length: u32 = 5_000;
909        let mut processes: Vec<ProcessSnapshot> = (1..=length)
910            .map(|pid| {
911                let fixture = process(pid, u64::from(pid)).name("hidden");
912                if pid == 1 {
913                    fixture.build()
914                } else {
915                    fixture.parent(pid - 1).build()
916                }
917            })
918            .collect();
919        processes.push(process(90_001, 1).name("visible").parent(length).build());
920        let tree = ProcessTree::build_filtered(
921            &processes,
922            ProcessSort::default(),
923            &ProcessFilter::parse("visible"),
924        );
925        assert_eq!(tree.len(), 1);
926        assert_eq!(tree.row(0).expect("present").depth, 0);
927    }
928
929    #[test]
930    fn duplicate_pids_from_a_racing_read_do_not_lose_rows() {
931        let processes = vec![
932            process(1, 1).name("systemd").build(),
933            process(50, 10).name("old").parent(1).build(),
934            process(50, 99).name("new").parent(1).build(),
935            process(60, 1).name("child-of-50").parent(50).build(),
936        ];
937        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
938        every_process_exactly_once(&tree, 4);
939        // The lower start key owns the PID, so the child attaches to `old`.
940        assert_eq!(
941            shape(&tree, &processes),
942            vec!["systemd", "  old", "    child-of-50", "  new"]
943        );
944    }
945
946    #[test]
947    fn child_and_ancestor_navigation_walks_the_structure() {
948        let processes = vec![
949            process(1, 1).name("root").build(),
950            process(2, 1).name("a").parent(1).build(),
951            process(3, 1).name("a1").parent(2).build(),
952            process(4, 1).name("b").parent(1).build(),
953        ];
954        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
955        assert_eq!(tree.child_rows(0), vec![1, 3]);
956        assert_eq!(tree.child_rows(1), vec![2]);
957        assert_eq!(tree.child_rows(2), Vec::<usize>::new());
958        assert_eq!(tree.ancestor_rows(2), vec![1, 0]);
959        assert_eq!(tree.ancestor_rows(0), Vec::<usize>::new());
960        assert_eq!(tree.child_rows(99), Vec::<usize>::new());
961        assert_eq!(tree.ancestor_rows(99), Vec::<usize>::new());
962        assert_eq!(tree.subtree_len(99), 0);
963    }
964
965    #[test]
966    fn continuation_flags_describe_the_vertical_lines_of_the_ascii_shape() {
967        // systemd
968        // +- sshd
969        // |  `- bash
970        // `- cron
971        let processes = vec![
972            process(1, 1).name("systemd").build(),
973            process(2, 1).name("sshd").parent(1).build(),
974            process(3, 1).name("bash").parent(2).build(),
975            process(4, 1).name("cron").parent(1).build(),
976        ];
977        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
978        assert_eq!(tree.continuation_flags(0), Vec::<bool>::new());
979        assert_eq!(tree.continuation_flags(1), vec![false]);
980        assert_eq!(
981            tree.continuation_flags(2),
982            vec![false, true],
983            "sshd has a sibling below it, so bash needs a vertical bar"
984        );
985        assert!(!tree.row(1).expect("present").is_last_child);
986        assert!(tree.row(3).expect("present").is_last_child);
987    }
988
989    #[test]
990    fn the_breadcrumb_lists_ancestors_nearest_first() {
991        let processes = vec![
992            process(1, 1).name("systemd").build(),
993            process(2, 1).name("sshd").parent(1).build(),
994            process(3, 1).name("bash").parent(2).build(),
995        ];
996        let tree = ProcessTree::build(&processes, ProcessSort::ascending(ProcessSortKey::Pid));
997        let row = tree.row_of(ProcessIdentity::new(3, 1)).expect("present");
998        let ancestry = tree.ancestry(&processes, row);
999        assert_eq!(
1000            ancestry
1001                .iter()
1002                .map(|entry| entry.name.as_ref())
1003                .collect::<Vec<_>>(),
1004            vec!["sshd", "systemd"]
1005        );
1006        assert_eq!(
1007            ancestry.first().map(|entry| entry.identity),
1008            Some(ProcessIdentity::new(2, 1))
1009        );
1010    }
1011
1012    #[test]
1013    fn a_row_resolved_against_the_wrong_snapshot_yields_nothing() {
1014        let processes = vec![process(1, 1).name("systemd").build()];
1015        let tree = ProcessTree::build(&processes, ProcessSort::default());
1016        // The next snapshot has PID 1 belonging to a different process.
1017        let recycled = vec![process(1, 99).name("impostor").build()];
1018        assert!(tree.process(&recycled, 0).is_none());
1019        assert!(tree.ancestry(&recycled, 0).is_empty());
1020        assert!(tree.process(&processes, 0).is_some());
1021        assert!(tree.process(&processes, 7).is_none());
1022    }
1023
1024    #[test]
1025    fn an_empty_table_produces_an_empty_tree() {
1026        let processes: Vec<ProcessSnapshot> = Vec::new();
1027        let tree = ProcessTree::build(&processes, ProcessSort::default());
1028        assert!(tree.is_empty());
1029        assert_eq!(tree.len(), 0);
1030        assert_eq!(tree.max_depth(), 0);
1031        assert_eq!(tree.cycles_broken(), 0);
1032        assert!(tree.row(0).is_none());
1033        assert!(tree.row_of(ProcessIdentity::new(1, 1)).is_none());
1034        assert!(tree.continuation_flags(0).is_empty());
1035    }
1036
1037    #[test]
1038    fn a_tree_can_be_built_straight_from_a_snapshot() {
1039        let live = snapshot(vec![
1040            process(1, 1).name("systemd").build(),
1041            process(2, 1).name("app").parent(1).build(),
1042        ]);
1043        let tree = ProcessTree::from_snapshot(&live, ProcessSort::default());
1044        assert_eq!(tree.len(), 2);
1045        assert_eq!(tree.row(1).expect("present").depth, 1);
1046
1047        let filtered = ProcessTree::from_snapshot_filtered(
1048            &live,
1049            ProcessSort::default(),
1050            &ProcessFilter::parse("app"),
1051        );
1052        assert_eq!(filtered.len(), 1);
1053    }
1054
1055    #[test]
1056    fn a_tree_can_be_built_from_borrowed_rows() {
1057        let processes = vec![
1058            process(1, 1).name("systemd").build(),
1059            process(2, 1).name("app").parent(1).build(),
1060        ];
1061        let borrowed: Vec<&ProcessSnapshot> = processes.iter().collect();
1062        let tree = ProcessTree::build(&borrowed, ProcessSort::default());
1063        assert_eq!(pids(&tree, &processes), vec![1, 2]);
1064    }
1065
1066    proptest! {
1067        /// §17.7: "arbitrary process graphs do not cause recursion overflow or
1068        /// cycles". The generator deliberately produces self-parents, cycles,
1069        /// missing parents, and forests.
1070        #[test]
1071        fn arbitrary_parent_graphs_terminate_and_keep_every_process(
1072            parents in prop::collection::vec(0u32..40, 1..40),
1073            descending in any::<bool>(),
1074        ) {
1075            let count = u32::try_from(parents.len()).unwrap_or(u32::MAX);
1076            let processes: Vec<ProcessSnapshot> = parents
1077                .iter()
1078                .enumerate()
1079                .map(|(index, &raw)| {
1080                    let pid = u32::try_from(index).unwrap_or(0) + 1;
1081                    let fixture = process(pid, u64::from(pid));
1082                    // `0` and anything above `count` name a process that is not in
1083                    // the table; everything else is a real row, including this one.
1084                    if raw == 0 {
1085                        fixture.build()
1086                    } else {
1087                        fixture.parent(raw).build()
1088                    }
1089                })
1090                .collect();
1091
1092            let direction = SortDirection::from_descending(descending);
1093            let tree = ProcessTree::build(
1094                &processes,
1095                ProcessSort::new(ProcessSortKey::Pid, direction),
1096            );
1097
1098            prop_assert_eq!(tree.len(), processes.len());
1099            let mut seen: Vec<usize> = tree.rows().iter().map(|row| row.process_index).collect();
1100            seen.sort_unstable();
1101            seen.dedup();
1102            prop_assert_eq!(seen.len(), processes.len(), "a process was dropped or duplicated");
1103
1104            let mut total_roots = 0u32;
1105            for (row_index, row) in tree.rows().iter().enumerate() {
1106                // Pre-order: a parent is always emitted before its children, which
1107                // is what makes ancestor walks terminate.
1108                if let Some(parent_row) = row.parent_row {
1109                    prop_assert!(parent_row < row_index);
1110                    let parent = tree.row(parent_row).expect("parent row exists");
1111                    prop_assert_eq!(parent.depth + 1, row.depth);
1112                } else {
1113                    prop_assert_eq!(row.depth, 0);
1114                    total_roots += 1;
1115                }
1116                prop_assert!(row.depth < count);
1117                prop_assert!(tree.ancestor_rows(row_index).len() == usize::try_from(row.depth).unwrap_or(0));
1118            }
1119            prop_assert!(total_roots >= 1, "a cyclic forest still needs a root");
1120
1121            // Every row is inside exactly one root subtree.
1122            let root_subtrees: usize = tree
1123                .rows()
1124                .iter()
1125                .enumerate()
1126                .filter(|(_, row)| row.parent_row.is_none())
1127                .map(|(index, _)| tree.subtree_len(index))
1128                .sum();
1129            prop_assert_eq!(root_subtrees, processes.len());
1130        }
1131    }
1132}