1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub struct TreeRow {
45 pub identity: ProcessIdentity,
47 pub process_index: usize,
52 pub depth: u32,
57 pub parent_row: Option<usize>,
59 pub descendants: u32,
61 pub is_last_child: bool,
66 pub parent_link_cut: bool,
72}
73
74#[derive(Clone, Debug, Default)]
76pub struct ProcessTree {
77 rows: Vec<TreeRow>,
78 cycles_broken: u32,
79}
80
81impl ProcessTree {
82 #[must_use]
84 pub fn from_snapshot(snapshot: &SystemSnapshot, sort: ProcessSort) -> Self {
85 Self::build(&snapshot.processes, sort)
86 }
87
88 #[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 #[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 #[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 #[must_use]
141 pub fn rows(&self) -> &[TreeRow] {
142 &self.rows
143 }
144
145 #[must_use]
147 pub fn len(&self) -> usize {
148 self.rows.len()
149 }
150
151 #[must_use]
153 pub fn is_empty(&self) -> bool {
154 self.rows.is_empty()
155 }
156
157 #[must_use]
159 pub fn row(&self, index: usize) -> Option<&TreeRow> {
160 self.rows.get(index)
161 }
162
163 #[must_use]
168 pub fn row_of(&self, identity: ProcessIdentity) -> Option<usize> {
169 self.rows.iter().position(|row| row.identity == identity)
170 }
171
172 #[must_use]
178 pub const fn cycles_broken(&self) -> u32 {
179 self.cycles_broken
180 }
181
182 #[must_use]
184 pub fn max_depth(&self) -> u32 {
185 self.rows.iter().map(|row| row.depth).max().unwrap_or(0)
186 }
187
188 #[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 #[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 #[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 #[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 #[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 #[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
289fn 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
312fn 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#[derive(Clone, Copy, Eq, PartialEq)]
339enum Mark {
340 New,
342 OnChain,
344 Done,
346}
347
348fn 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 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
409fn 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 let inherited = loop {
428 chain.push(cursor);
429 if chain.len() > count {
430 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
466fn 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
490fn 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 _ => 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
547struct Frame {
549 node: usize,
551 depth: u32,
553 is_last: bool,
555 parent_row: Option<usize>,
557}
558
559fn 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
571fn 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 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 assert_eq!(shape(&tree, &processes), vec!["a", " b"]);
697
698 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 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 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 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 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 #[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 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 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 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}