use std::collections::HashMap;
use super::filter::{is_target_process, process_kind, ActivityState, ProcessKind};
use super::info::ProcessInfo;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct SubtreeStats {
pub total_cpu: f32,
pub total_memory: u64,
pub process_count: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProcessNode {
pub info: ProcessInfo,
pub children: Vec<ProcessNode>,
pub depth: usize,
pub expanded: bool,
pub is_root: bool,
pub subtree_stats: SubtreeStats,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FlatEntry {
pub info: ProcessInfo,
pub depth: usize,
pub is_root: bool,
pub expanded: bool,
pub has_children: bool,
pub is_last_sibling: bool,
pub kind: Option<ProcessKind>,
pub subtree_stats: SubtreeStats,
pub activity: Option<ActivityState>,
}
pub fn build_forest(processes: &[ProcessInfo]) -> Vec<ProcessNode> {
let mut children_map: HashMap<u32, Vec<&ProcessInfo>> = HashMap::new();
for p in processes {
if let Some(ppid) = p.parent_pid {
children_map.entry(ppid).or_default().push(p);
}
}
let mut roots: Vec<ProcessNode> = processes
.iter()
.filter(|p| is_target_process(p))
.map(|p| build_node(p, &children_map, 0, true))
.collect();
for root in roots.iter_mut() {
compute_subtree_stats(root);
}
roots
}
fn build_node<'a>(
info: &'a ProcessInfo,
children_map: &HashMap<u32, Vec<&'a ProcessInfo>>,
depth: usize,
is_root: bool,
) -> ProcessNode {
let children = children_map
.get(&info.pid)
.map(|kids| {
kids.iter()
.map(|child| build_node(child, children_map, depth + 1, false))
.collect()
})
.unwrap_or_default();
ProcessNode {
info: info.clone(),
children,
depth,
expanded: true,
is_root,
subtree_stats: SubtreeStats::default(),
}
}
pub fn compute_subtree_stats(node: &mut ProcessNode) {
for child in node.children.iter_mut() {
compute_subtree_stats(child);
}
let mut stats = SubtreeStats {
total_cpu: node.info.cpu_usage,
total_memory: node.info.memory_bytes,
process_count: 1,
};
for child in &node.children {
stats.total_cpu += child.subtree_stats.total_cpu;
stats.total_memory += child.subtree_stats.total_memory;
stats.process_count += child.subtree_stats.process_count;
}
node.subtree_stats = stats;
}
pub fn flatten_visible(forest: &[ProcessNode]) -> Vec<FlatEntry> {
let mut out = Vec::new();
let last_idx = forest.len().saturating_sub(1);
for (i, node) in forest.iter().enumerate() {
flatten_node(node, &mut out, i == last_idx);
}
out
}
fn flatten_node(node: &ProcessNode, out: &mut Vec<FlatEntry>, is_last_sibling: bool) {
out.push(FlatEntry {
info: node.info.clone(),
depth: node.depth,
is_root: node.is_root,
expanded: node.expanded,
has_children: !node.children.is_empty(),
is_last_sibling,
kind: process_kind(&node.info),
subtree_stats: node.subtree_stats,
activity: None,
});
if node.expanded {
let last_child = node.children.len().saturating_sub(1);
for (i, child) in node.children.iter().enumerate() {
flatten_node(child, out, i == last_child);
}
}
}
pub fn toggle_expand(forest: &mut [ProcessNode], target_pid: u32) {
for node in forest.iter_mut() {
if node.info.pid == target_pid {
node.expanded = !node.expanded;
return;
}
toggle_expand(&mut node.children, target_pid);
}
}
pub fn collect_expansion(forest: &[ProcessNode]) -> HashMap<u32, bool> {
let mut map = HashMap::new();
for node in forest {
map.insert(node.info.pid, node.expanded);
map.extend(collect_expansion(&node.children));
}
map
}
pub fn preserve_expansion(forest: &mut [ProcessNode], old_states: &HashMap<u32, bool>) {
for node in forest.iter_mut() {
if let Some(&was_expanded) = old_states.get(&node.info.pid) {
node.expanded = was_expanded;
}
preserve_expansion(&mut node.children, old_states);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::process::ProcessInfo;
fn proc(pid: u32, parent: Option<u32>, name: &str) -> ProcessInfo {
ProcessInfo {
pid,
parent_pid: parent,
name: name.to_string(),
cmd: vec![name.to_string()],
exe_path: None,
cwd: None,
cpu_usage: 0.0,
memory_bytes: 0,
status: "Run".to_string(),
environ_count: 0,
start_time: 0,
run_time: 0,
}
}
fn proc_with_resources(
pid: u32,
parent: Option<u32>,
name: &str,
cpu: f32,
mem: u64,
) -> ProcessInfo {
ProcessInfo {
pid,
parent_pid: parent,
name: name.to_string(),
cmd: vec![name.to_string()],
exe_path: None,
cwd: None,
cpu_usage: cpu,
memory_bytes: mem,
status: "Run".to_string(),
environ_count: 0,
start_time: 0,
run_time: 0,
}
}
#[test]
fn build_forest_finds_roots() {
let procs = vec![
proc(1, None, "claude"),
proc(2, Some(1), "node"),
proc(3, None, "bash"),
];
let forest = build_forest(&procs);
assert_eq!(forest.len(), 1);
assert_eq!(forest[0].info.pid, 1);
assert_eq!(forest[0].children.len(), 1);
assert_eq!(forest[0].children[0].info.pid, 2);
}
#[test]
fn flatten_respects_expansion() {
let procs = vec![proc(1, None, "claude"), proc(2, Some(1), "node")];
let mut forest = build_forest(&procs);
let flat = flatten_visible(&forest);
assert_eq!(flat.len(), 2);
toggle_expand(&mut forest, 1);
let flat = flatten_visible(&forest);
assert_eq!(flat.len(), 1);
}
#[test]
fn collect_and_preserve_expansion() {
let procs = vec![proc(1, None, "claude"), proc(2, Some(1), "node")];
let mut forest = build_forest(&procs);
toggle_expand(&mut forest, 1);
let states = collect_expansion(&forest);
assert_eq!(states.get(&1), Some(&false));
let mut new_forest = build_forest(&procs);
preserve_expansion(&mut new_forest, &states);
assert!(!new_forest[0].expanded);
}
#[test]
fn empty_process_list() {
let forest = build_forest(&[]);
assert!(forest.is_empty());
let flat = flatten_visible(&forest);
assert!(flat.is_empty());
}
#[test]
fn test_subtree_stats_leaf_node() {
let procs = vec![proc_with_resources(1, None, "claude", 2.5, 1024)];
let forest = build_forest(&procs);
let stats = forest[0].subtree_stats;
assert_eq!(stats.process_count, 1);
assert!((stats.total_cpu - 2.5).abs() < 1e-4, "cpu mismatch");
assert_eq!(stats.total_memory, 1024);
}
#[test]
fn test_subtree_stats_with_children() {
let procs = vec![
proc_with_resources(1, None, "claude", 1.0, 100),
proc_with_resources(2, Some(1), "node", 2.0, 200),
proc_with_resources(3, Some(1), "node", 3.0, 300),
];
let forest = build_forest(&procs);
let stats = forest[0].subtree_stats;
assert_eq!(stats.process_count, 3);
assert!((stats.total_cpu - 6.0).abs() < 1e-4, "cpu mismatch");
assert_eq!(stats.total_memory, 600);
}
#[test]
fn test_subtree_stats_deep_tree() {
let procs = vec![
proc_with_resources(1, None, "claude", 1.0, 10),
proc_with_resources(2, Some(1), "node", 1.0, 20),
proc_with_resources(3, Some(2), "node", 1.0, 30),
];
let forest = build_forest(&procs);
let root_stats = forest[0].subtree_stats;
assert_eq!(root_stats.process_count, 3);
assert!((root_stats.total_cpu - 3.0).abs() < 1e-4, "root cpu");
assert_eq!(root_stats.total_memory, 60);
let child_stats = forest[0].children[0].subtree_stats;
assert_eq!(child_stats.process_count, 2);
assert!((child_stats.total_cpu - 2.0).abs() < 1e-4, "child cpu");
assert_eq!(child_stats.total_memory, 50);
}
#[test]
fn test_subtree_stats_in_flat_entry() {
let procs = vec![
proc_with_resources(1, None, "claude", 4.0, 400),
proc_with_resources(2, Some(1), "node", 1.0, 100),
];
let forest = build_forest(&procs);
let flat = flatten_visible(&forest);
let root_flat = flat
.iter()
.find(|e| e.info.pid == 1)
.expect("root not found");
assert_eq!(root_flat.subtree_stats.process_count, 2);
assert!((root_flat.subtree_stats.total_cpu - 5.0).abs() < 1e-4);
assert_eq!(root_flat.subtree_stats.total_memory, 500);
let child_flat = flat
.iter()
.find(|e| e.info.pid == 2)
.expect("child not found");
assert_eq!(child_flat.subtree_stats.process_count, 1);
assert!((child_flat.subtree_stats.total_cpu - 1.0).abs() < 1e-4);
assert_eq!(child_flat.subtree_stats.total_memory, 100);
}
}