fix: resolve UI freezes by parallelizing PSS collection and optimizing system refreshing

This commit is contained in:
2026-02-23 01:12:26 +01:00
parent a5d226503d
commit 191aa494db
2 changed files with 96 additions and 114 deletions

View File

@@ -17,3 +17,4 @@ serde_json = "1.0.149"
sysinfo = "0.38.2" sysinfo = "0.38.2"
tauri = "2.10.2" tauri = "2.10.2"
tokio = { version = "1.49.0", features = ["full"] } tokio = { version = "1.49.0", features = ["full"] }
rayon = "1.10"

View File

@@ -3,36 +3,15 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use sysinfo::{System, Pid}; use sysinfo::System;
use std::sync::Mutex; use std::sync::Mutex;
use std::process::Command; use std::process::Command;
use tauri::State; use tauri::State;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::fs; use std::fs;
use rayon::prelude::*;
// --- Helper for Real Memory (PSS) on Linux ---
fn get_pss(pid: u32) -> Option<u64> {
// PSS (Proportional Set Size) is the most accurate "real" memory metric.
// It counts private memory + proportional share of shared libraries.
// smaps_rollup is a fast way to get this on modern Linux kernels.
let path = format!("/proc/{}/smaps_rollup", pid);
if let Ok(contents) = fs::read_to_string(path) {
for line in contents.lines() {
if line.starts_with("Pss:") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(kb) = parts[1].parse::<u64>() {
return Some(kb * 1024);
}
}
}
}
}
None
}
// --- Data Structures --- // --- Data Structures ---
@@ -119,21 +98,48 @@ struct AggregatedProcess {
children: Vec<AggregatedProcess>, children: Vec<AggregatedProcess>,
} }
// --- Helper for Real Memory (PSS) on Linux ---
fn get_pss(pid: u32) -> Option<u64> {
let path = format!("/proc/{}/smaps_rollup", pid);
if let Ok(contents) = fs::read_to_string(path) {
for line in contents.lines() {
if line.starts_with("Pss:") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(kb) = parts[1].parse::<u64>() {
return Some(kb * 1024);
}
}
}
}
}
None
}
// --- Commands --- // --- Commands ---
fn is_descendant_of(pid: u32, target_pid: u32, sys: &System) -> bool { fn get_all_descendants(target_pid: u32, sys: &System) -> HashSet<u32> {
let mut current_pid = Some(Pid::from_u32(pid)); let mut descendants = HashSet::new();
while let Some(p) = current_pid { let mut stack = vec![target_pid];
if p.as_u32() == target_pid {
return true; let mut child_map: HashMap<u32, Vec<u32>> = HashMap::new();
} for (pid, process) in sys.processes() {
if let Some(process) = sys.process(p) { if let Some(parent) = process.parent() {
current_pid = process.parent(); child_map.entry(parent.as_u32()).or_default().push(pid.as_u32());
} else {
break;
} }
} }
false
while let Some(pid) = stack.pop() {
if let Some(children) = child_map.get(&pid) {
for &child in children {
if descendants.insert(child) {
stack.push(child);
}
}
}
}
descendants
} }
#[tauri::command] #[tauri::command]
@@ -144,59 +150,46 @@ fn get_system_stats(
let mut sys = state.sys.lock().unwrap(); let mut sys = state.sys.lock().unwrap();
let mut profiling = state.profiling.lock().unwrap(); let mut profiling = state.profiling.lock().unwrap();
if minimal {
sys.refresh_cpu_all(); sys.refresh_cpu_all();
sys.refresh_memory(); sys.refresh_memory();
} else { sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
sys.refresh_all();
}
let self_pid = std::process::id(); let self_pid = std::process::id();
let cpu_usage: Vec<f32> = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(); let cpu_usage: Vec<f32> = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).collect();
let total_memory = sys.total_memory(); let total_memory = sys.total_memory();
let used_memory = sys.used_memory(); let used_memory = sys.used_memory();
let mut processes: Vec<ProcessStats> = if minimal && !profiling.is_active { let syspulse_set = get_all_descendants(self_pid, &sys);
Vec::new()
} else {
sys.processes().iter()
.map(|(pid, process)| {
let pid_u32 = pid.as_u32();
let memory = get_pss(pid_u32).unwrap_or_else(|| process.memory());
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,
status: format!("{:?}", process.status()),
user_id: process.user_id().map(|uid| uid.to_string()),
is_syspulse: is_descendant_of(pid_u32, self_pid, &sys),
}
}).collect()
};
if profiling.is_active { let raw_processes: Vec<_> = sys.processes().iter()
// Even in minimal mode, if recording we need the processes for the report .map(|(pid, p)| (
if minimal { pid.as_u32(),
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true); p.parent().map(|pp| pp.as_u32()),
processes = sys.processes().iter() p.name().to_string_lossy().to_string(),
.map(|(pid, process)| { p.cpu_usage(),
let pid_u32 = pid.as_u32(); p.memory(),
let memory = get_pss(pid_u32).unwrap_or_else(|| process.memory()); format!("{:?}", p.status()),
p.user_id().map(|u| u.to_string())
))
.collect();
let processes: Vec<ProcessStats> = raw_processes.into_par_iter()
.map(|(pid, parent_pid, name, cpu, rss, status, uid)| {
let is_syspulse = pid == self_pid || syspulse_set.contains(&pid);
let memory = get_pss(pid).unwrap_or(rss);
ProcessStats { ProcessStats {
pid: pid_u32, pid,
parent_pid: process.parent().map(|p| p.as_u32()), parent_pid,
name: process.name().to_string_lossy().to_string(), name,
cpu_usage: process.cpu_usage(), cpu_usage: cpu,
memory, memory,
status: format!("{:?}", process.status()), status,
user_id: process.user_id().map(|uid| uid.to_string()), user_id: uid,
is_syspulse: is_descendant_of(pid_u32, self_pid, &sys), is_syspulse,
} }
}).collect(); }).collect();
}
if profiling.is_active {
profiling.snapshots.push(Snapshot { profiling.snapshots.push(Snapshot {
timestamp: Utc::now(), timestamp: Utc::now(),
cpu_usage: cpu_usage.clone(), cpu_usage: cpu_usage.clone(),
@@ -211,18 +204,22 @@ fn get_system_stats(
0 0
}; };
let mut display_processes = if minimal && !profiling.is_active {
Vec::new()
} else {
processes.clone()
};
if !minimal { if !minimal {
processes.sort_by(|a, b| b.cpu_usage.partial_cmp(&a.cpu_usage).unwrap_or(std::cmp::Ordering::Equal)); display_processes.sort_by(|a, b| b.cpu_usage.partial_cmp(&a.cpu_usage).unwrap_or(std::cmp::Ordering::Equal));
processes.truncate(50); display_processes.truncate(50);
} else if !profiling.is_active {
processes.clear();
} }
SystemStats { SystemStats {
cpu_usage, cpu_usage,
total_memory, total_memory,
used_memory, used_memory,
processes, processes: display_processes,
is_recording: profiling.is_active, is_recording: profiling.is_active,
recording_duration, recording_duration,
} }
@@ -300,7 +297,7 @@ fn stop_profiling(state: State<AppState>) -> Report {
} }
} }
// 3. Convert to nodes and build tree // 3. Convert to nodes
let mut nodes: HashMap<u32, AggregatedProcess> = pid_map.into_iter().map(|(pid, stats)| { 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_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 total_mem: f32 = stats.history.iter().map(|h| h.memory_mb).sum();
@@ -316,8 +313,8 @@ fn stop_profiling(state: State<AppState>) -> Report {
peak_cpu: stats.peak_cpu, peak_cpu: stats.peak_cpu,
avg_memory_mb: total_mem / num_snapshots, avg_memory_mb: total_mem / num_snapshots,
peak_memory_mb: stats.peak_mem, peak_memory_mb: stats.peak_mem,
inclusive_avg_cpu: 0.0, // Calculated later inclusive_avg_cpu: 0.0,
inclusive_avg_memory_mb: 0.0, // Calculated later inclusive_avg_memory_mb: 0.0,
instance_count: 1, instance_count: 1,
warnings, warnings,
history: stats.history, history: stats.history,
@@ -326,11 +323,8 @@ fn stop_profiling(state: State<AppState>) -> Report {
}) })
}).collect(); }).collect();
// 4. Link children to parents // 4. Build Tree
let mut root_pids = Vec::new();
let mut child_to_parent = HashMap::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 snapshot in &profiling.snapshots {
for proc in &snapshot.processes { for proc in &snapshot.processes {
if let Some(ppid) = proc.parent_pid { if let Some(ppid) = proc.parent_pid {
@@ -341,16 +335,16 @@ fn stop_profiling(state: State<AppState>) -> Report {
} }
} }
let pids: Vec<u32> = nodes.keys().cloned().collect(); let mut child_map: HashMap<u32, Vec<u32>> = HashMap::new();
for pid in pids { for (&child, &parent) in &child_to_parent {
if let Some(&_ppid) = child_to_parent.get(&pid) { child_map.entry(parent).or_default().push(child);
// Already handled in recursive aggregation or linked below
} else {
root_pids.push(pid);
}
} }
// 5. Recursive function to calculate inclusive stats and build tree let root_pids: Vec<u32> = nodes.keys()
.filter(|pid| !child_to_parent.contains_key(pid))
.cloned()
.collect();
fn build_node(pid: u32, nodes: &mut HashMap<u32, AggregatedProcess>, child_map: &HashMap<u32, Vec<u32>>) -> Option<AggregatedProcess> { fn build_node(pid: u32, nodes: &mut HashMap<u32, AggregatedProcess>, child_map: &HashMap<u32, Vec<u32>>) -> Option<AggregatedProcess> {
let mut node = nodes.remove(&pid)?; let mut node = nodes.remove(&pid)?;
let children_pids = child_map.get(&pid).cloned().unwrap_or_default(); let children_pids = child_map.get(&pid).cloned().unwrap_or_default();
@@ -371,11 +365,6 @@ fn stop_profiling(state: State<AppState>) -> Report {
Some(node) Some(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(); let mut final_roots = Vec::new();
for pid in root_pids { for pid in root_pids {
if let Some(root_node) = build_node(pid, &mut nodes, &child_map) { if let Some(root_node) = build_node(pid, &mut nodes, &child_map) {
@@ -383,7 +372,6 @@ fn stop_profiling(state: State<AppState>) -> Report {
} }
} }
// Include any remaining orphan nodes as roots (e.g. if parent info was missing in snapshots)
let remaining_pids: Vec<u32> = nodes.keys().cloned().collect(); let remaining_pids: Vec<u32> = nodes.keys().cloned().collect();
for pid in remaining_pids { for pid in remaining_pids {
if let Some(node) = build_node(pid, &mut nodes, &child_map) { if let Some(node) = build_node(pid, &mut nodes, &child_map) {
@@ -391,7 +379,6 @@ fn stop_profiling(state: State<AppState>) -> Report {
} }
} }
// 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)); final_roots.sort_by(|a, b| b.inclusive_avg_cpu.partial_cmp(&a.inclusive_avg_cpu).unwrap_or(std::cmp::Ordering::Equal));
Report { Report {
@@ -405,12 +392,6 @@ fn stop_profiling(state: State<AppState>) -> Report {
#[tauri::command] #[tauri::command]
fn run_as_admin(command: String) -> Result<String, String> { fn run_as_admin(command: String) -> Result<String, String> {
// Uses pkexec to run a command as root.
// CAUTION: This is a simple implementation. In production, validate inputs carefully.
// Splitting command for safety is hard, so we assume 'command' is a simple executable name or safe string.
// Example usage from frontend: "kill -9 1234" -> pkexec kill -9 1234
let output = Command::new("pkexec") let output = Command::new("pkexec")
.arg("sh") .arg("sh")
.arg("-c") .arg("-c")