feat: implement hierarchical process tree view and inclusive resource calculation
This commit is contained in:
@@ -26,6 +26,7 @@ struct SystemStats {
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
struct ProcessStats {
|
||||
pid: u32,
|
||||
parent_pid: Option<u32>,
|
||||
name: String,
|
||||
cpu_usage: f32,
|
||||
memory: u64,
|
||||
@@ -80,15 +81,19 @@ struct ProcessHistoryPoint {
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AggregatedProcess {
|
||||
pid: u32,
|
||||
name: String,
|
||||
avg_cpu: f32,
|
||||
peak_cpu: f32,
|
||||
avg_memory_mb: f32,
|
||||
peak_memory_mb: f32,
|
||||
inclusive_avg_cpu: f32,
|
||||
inclusive_avg_memory_mb: f32,
|
||||
instance_count: usize,
|
||||
warnings: Vec<String>,
|
||||
history: Vec<ProcessHistoryPoint>,
|
||||
is_syspulse: bool,
|
||||
children: Vec<AggregatedProcess>,
|
||||
}
|
||||
|
||||
// --- Commands ---
|
||||
@@ -136,6 +141,7 @@ fn get_system_stats(
|
||||
let pid_u32 = pid.as_u32();
|
||||
ProcessStats {
|
||||
pid: pid_u32,
|
||||
parent_pid: process.parent().map(|p| p.as_u32()),
|
||||
name: process.name().to_string_lossy().to_string(),
|
||||
cpu_usage: process.cpu_usage(),
|
||||
memory: process.memory(),
|
||||
@@ -155,6 +161,7 @@ fn get_system_stats(
|
||||
let pid_u32 = pid.as_u32();
|
||||
ProcessStats {
|
||||
pid: pid_u32,
|
||||
parent_pid: process.parent().map(|p| p.as_u32()),
|
||||
name: process.name().to_string_lossy().to_string(),
|
||||
cpu_usage: process.cpu_usage(),
|
||||
memory: process.memory(),
|
||||
@@ -231,95 +238,136 @@ fn stop_profiling(state: State<AppState>) -> Report {
|
||||
}
|
||||
}).collect();
|
||||
|
||||
// 2. Aggregate Processes over Time
|
||||
let mut process_map: HashMap<String, Vec<ProcessHistoryPoint>> = HashMap::new();
|
||||
let mut peak_stats: HashMap<String, (f32, f32)> = HashMap::new(); // (Peak CPU, Peak Mem)
|
||||
let mut unique_pids: HashMap<String, std::collections::HashSet<u32>> = HashMap::new();
|
||||
let mut status_flags: HashMap<String, bool> = HashMap::new(); // Zombie check
|
||||
let mut syspulse_flags: HashMap<String, bool> = HashMap::new();
|
||||
// 2. Aggregate RAW stats per PID
|
||||
struct PidStats {
|
||||
name: String,
|
||||
parent_pid: Option<u32>,
|
||||
history: Vec<ProcessHistoryPoint>,
|
||||
peak_cpu: f32,
|
||||
peak_mem: f32,
|
||||
is_syspulse: bool,
|
||||
is_zombie: bool,
|
||||
}
|
||||
|
||||
let mut pid_map: HashMap<u32, PidStats> = HashMap::new();
|
||||
let num_snapshots = profiling.snapshots.len() as f32;
|
||||
|
||||
for snapshot in &profiling.snapshots {
|
||||
let mut snapshot_procs: HashMap<String, (f32, u64)> = HashMap::new();
|
||||
|
||||
for proc in &snapshot.processes {
|
||||
let entry = snapshot_procs.entry(proc.name.clone()).or_default();
|
||||
entry.0 += proc.cpu_usage;
|
||||
entry.1 += proc.memory;
|
||||
let entry = pid_map.entry(proc.pid).or_insert_with(|| PidStats {
|
||||
name: proc.name.clone(),
|
||||
parent_pid: proc.parent_pid,
|
||||
history: Vec::new(),
|
||||
peak_cpu: 0.0,
|
||||
peak_mem: 0.0,
|
||||
is_syspulse: proc.is_syspulse,
|
||||
is_zombie: false,
|
||||
});
|
||||
|
||||
unique_pids.entry(proc.name.clone()).or_default().insert(proc.pid);
|
||||
if proc.status.contains("Zombie") {
|
||||
status_flags.insert(proc.name.clone(), true);
|
||||
}
|
||||
if proc.is_syspulse {
|
||||
syspulse_flags.insert(proc.name.clone(), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Record history for all processes seen in this snapshot
|
||||
for (name, (cpu, mem)) in snapshot_procs {
|
||||
let hist_entry = process_map.entry(name.clone()).or_default();
|
||||
let mem_mb = mem as f32 / 1024.0 / 1024.0;
|
||||
|
||||
hist_entry.push(ProcessHistoryPoint {
|
||||
let mem_mb = proc.memory as f32 / 1024.0 / 1024.0;
|
||||
entry.history.push(ProcessHistoryPoint {
|
||||
time: snapshot.timestamp.format("%H:%M:%S").to_string(),
|
||||
cpu_usage: cpu,
|
||||
cpu_usage: proc.cpu_usage,
|
||||
memory_mb: mem_mb,
|
||||
});
|
||||
|
||||
let peaks = peak_stats.entry(name).or_insert((0.0, 0.0));
|
||||
if cpu > peaks.0 { peaks.0 = cpu; }
|
||||
if mem_mb > peaks.1 { peaks.1 = mem_mb; }
|
||||
if proc.cpu_usage > entry.peak_cpu { entry.peak_cpu = proc.cpu_usage; }
|
||||
if mem_mb > entry.peak_mem { entry.peak_mem = mem_mb; }
|
||||
if proc.status.contains("Zombie") { entry.is_zombie = true; }
|
||||
}
|
||||
}
|
||||
|
||||
let mut aggregated_processes: Vec<AggregatedProcess> = Vec::new();
|
||||
let num_snapshots = profiling.snapshots.len() as f32;
|
||||
|
||||
for (name, history) in process_map {
|
||||
let (peak_cpu, peak_mem) = peak_stats.get(&name).cloned().unwrap_or((0.0, 0.0));
|
||||
let count = unique_pids.get(&name).map(|s| s.len()).unwrap_or(0);
|
||||
let is_syspulse = syspulse_flags.get(&name).cloned().unwrap_or(false);
|
||||
|
||||
// Average over the whole SESSION (zeros for snapshots where not present)
|
||||
let total_cpu_sum: f32 = history.iter().map(|h| h.cpu_usage).sum();
|
||||
let total_mem_sum: f32 = history.iter().map(|h| h.memory_mb).sum();
|
||||
|
||||
let avg_cpu = if num_snapshots > 0.0 { total_cpu_sum / num_snapshots } else { 0.0 };
|
||||
let avg_mem = if num_snapshots > 0.0 { total_mem_sum / num_snapshots } else { 0.0 };
|
||||
// 3. Convert to nodes and build tree
|
||||
let mut nodes: HashMap<u32, AggregatedProcess> = pid_map.into_iter().map(|(pid, stats)| {
|
||||
let total_cpu: f32 = stats.history.iter().map(|h| h.cpu_usage).sum();
|
||||
let total_mem: f32 = stats.history.iter().map(|h| h.memory_mb).sum();
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
if status_flags.get(&name).cloned().unwrap_or(false) {
|
||||
warnings.push("Zombie Process Detected".to_string());
|
||||
}
|
||||
if peak_cpu > 80.0 {
|
||||
warnings.push("High Peak Load".to_string());
|
||||
}
|
||||
if peak_mem > 2048.0 {
|
||||
warnings.push("Heavy Memory usage".to_string());
|
||||
}
|
||||
if stats.is_zombie { warnings.push("Zombie".to_string()); }
|
||||
if stats.peak_cpu > 80.0 { warnings.push("High Peak".to_string()); }
|
||||
|
||||
aggregated_processes.push(AggregatedProcess {
|
||||
name,
|
||||
avg_cpu,
|
||||
peak_cpu,
|
||||
avg_memory_mb: avg_mem,
|
||||
peak_memory_mb: peak_mem,
|
||||
instance_count: count,
|
||||
(pid, AggregatedProcess {
|
||||
pid,
|
||||
name: stats.name,
|
||||
avg_cpu: total_cpu / num_snapshots,
|
||||
peak_cpu: stats.peak_cpu,
|
||||
avg_memory_mb: total_mem / num_snapshots,
|
||||
peak_memory_mb: stats.peak_mem,
|
||||
inclusive_avg_cpu: 0.0, // Calculated later
|
||||
inclusive_avg_memory_mb: 0.0, // Calculated later
|
||||
instance_count: 1,
|
||||
warnings,
|
||||
history,
|
||||
is_syspulse,
|
||||
});
|
||||
history: stats.history,
|
||||
is_syspulse: stats.is_syspulse,
|
||||
children: Vec::new(),
|
||||
})
|
||||
}).collect();
|
||||
|
||||
// 4. Link children to parents
|
||||
let mut root_pids = Vec::new();
|
||||
let mut child_to_parent = HashMap::new();
|
||||
|
||||
// We need to re-fetch parent info because we moved pid_map
|
||||
for snapshot in &profiling.snapshots {
|
||||
for proc in &snapshot.processes {
|
||||
if let Some(ppid) = proc.parent_pid {
|
||||
if nodes.contains_key(&ppid) {
|
||||
child_to_parent.insert(proc.pid, ppid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by Average CPU descending
|
||||
aggregated_processes.sort_by(|a, b| b.avg_cpu.partial_cmp(&a.avg_cpu).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let pids: Vec<u32> = nodes.keys().cloned().collect();
|
||||
for pid in pids {
|
||||
if let Some(&ppid) = child_to_parent.get(&pid) {
|
||||
// Already handled in recursive aggregation or linked below
|
||||
} else {
|
||||
root_pids.push(pid);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Recursive function to calculate inclusive stats and build tree
|
||||
fn build_node(pid: u32, nodes: &mut HashMap<u32, AggregatedProcess>, child_map: &HashMap<u32, Vec<u32>>) -> AggregatedProcess {
|
||||
let mut node = nodes.remove(&pid).unwrap();
|
||||
let children_pids = child_map.get(&pid).cloned().unwrap_or_default();
|
||||
|
||||
let mut inc_cpu = node.avg_cpu;
|
||||
let mut inc_mem = node.avg_memory_mb;
|
||||
|
||||
for c_pid in children_pids {
|
||||
let child_node = build_node(c_pid, nodes, child_map);
|
||||
inc_cpu += child_node.inclusive_avg_cpu;
|
||||
inc_mem += child_node.inclusive_avg_memory_mb;
|
||||
node.children.push(child_node);
|
||||
}
|
||||
|
||||
node.inclusive_avg_cpu = inc_cpu;
|
||||
node.inclusive_avg_memory_mb = inc_mem;
|
||||
node
|
||||
}
|
||||
|
||||
let mut child_map: HashMap<u32, Vec<u32>> = HashMap::new();
|
||||
for (&child, &parent) in &child_to_parent {
|
||||
child_map.entry(parent).or_default().push(child);
|
||||
}
|
||||
|
||||
let mut final_roots = Vec::new();
|
||||
for pid in root_pids {
|
||||
if nodes.contains_key(&pid) {
|
||||
final_roots.push(build_node(pid, &mut nodes, &child_map));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort roots by inclusive CPU
|
||||
final_roots.sort_by(|a, b| b.inclusive_avg_cpu.partial_cmp(&a.inclusive_avg_cpu).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
Report {
|
||||
start_time: start.to_rfc3339(),
|
||||
end_time: end.to_rfc3339(),
|
||||
duration_seconds: duration,
|
||||
timeline,
|
||||
aggregated_processes,
|
||||
aggregated_processes: final_roots,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user