diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2b3ece1..a5f79cc 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -26,6 +26,7 @@ struct SystemStats { #[derive(Serialize, Clone, Debug)] struct ProcessStats { pid: u32, + parent_pid: Option, 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, history: Vec, is_syspulse: bool, + children: Vec, } // --- 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) -> Report { } }).collect(); - // 2. Aggregate Processes over Time - let mut process_map: HashMap> = HashMap::new(); - let mut peak_stats: HashMap = HashMap::new(); // (Peak CPU, Peak Mem) - let mut unique_pids: HashMap> = HashMap::new(); - let mut status_flags: HashMap = HashMap::new(); // Zombie check - let mut syspulse_flags: HashMap = HashMap::new(); + // 2. Aggregate RAW stats per PID + struct PidStats { + name: String, + parent_pid: Option, + history: Vec, + peak_cpu: f32, + peak_mem: f32, + is_syspulse: bool, + is_zombie: bool, + } + + let mut pid_map: HashMap = HashMap::new(); + let num_snapshots = profiling.snapshots.len() as f32; for snapshot in &profiling.snapshots { - let mut snapshot_procs: HashMap = 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; - - 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); - } - } + 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, + }); - // 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 = 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 = 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 = 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, child_map: &HashMap>) -> 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> = 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, } }