feat: stabilize targeted profiling extraction and improve process selection UI

This commit is contained in:
2026-02-23 18:31:16 +01:00
parent c7adb2af17
commit 1663c3702e

View File

@@ -3,7 +3,7 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use sysinfo::System; use sysinfo::{System, Pid};
use std::sync::Mutex; use std::sync::Mutex;
use std::process::Command; use std::process::Command;
use tauri::State; use tauri::State;
@@ -195,7 +195,6 @@ fn collect_snapshot(
let is_syspulse = syspulse_pids.contains(&pid_u32); let is_syspulse = syspulse_pids.contains(&pid_u32);
let mut memory = rss; let mut memory = rss;
// PSS collection is expensive. Throttle it heavily.
if collect_pss && (rss > 20 * 1024 * 1024 || is_syspulse) { if collect_pss && (rss > 20 * 1024 * 1024 || is_syspulse) {
let needs_refresh = match pss_cache.get(&pid_u32) { let needs_refresh = match pss_cache.get(&pid_u32) {
Some((_, last)) => (now - *last).num_seconds() > 5, Some((_, last)) => (now - *last).num_seconds() > 5,
@@ -239,11 +238,7 @@ fn generate_report(start_time: DateTime<Utc>, snapshots: Vec<Snapshot>, mode: Pr
let timeline: Vec<TimelinePoint> = snapshots.iter().map(|s| { let timeline: Vec<TimelinePoint> = snapshots.iter().map(|s| {
let cpu_total = s.cpu_usage.iter().sum::<f32>() / s.cpu_usage.len() as f32; let cpu_total = s.cpu_usage.iter().sum::<f32>() / s.cpu_usage.len() as f32;
let mem_total_gb = s.used_memory as f32 / 1024.0 / 1024.0 / 1024.0; let mem_total_gb = s.used_memory as f32 / 1024.0 / 1024.0 / 1024.0;
let profiler_stats = s.processes.iter().filter(|p| p.is_syspulse).fold((0.0, 0), |acc, p| (acc.0 + p.cpu_usage, acc.1 + p.memory));
let profiler_stats = s.processes.iter()
.filter(|p| p.is_syspulse)
.fold((0.0, 0), |acc, p| (acc.0 + p.cpu_usage, acc.1 + p.memory));
TimelinePoint { TimelinePoint {
time: s.timestamp.format("%H:%M:%S").to_string(), time: s.timestamp.format("%H:%M:%S").to_string(),
cpu_total, cpu_total,
@@ -256,9 +251,7 @@ fn generate_report(start_time: DateTime<Utc>, snapshots: Vec<Snapshot>, mode: Pr
let mut target_name = None; let mut target_name = None;
if let Some(tpid) = target_pid { if let Some(tpid) = target_pid {
if let Some(snapshot) = snapshots.first() { if let Some(snapshot) = snapshots.first() {
if let Some(p) = snapshot.processes.iter().find(|p| p.pid == tpid) { if let Some(p) = snapshot.processes.iter().find(|p| p.pid == tpid) { target_name = Some(p.name.clone()); }
target_name = Some(p.name.clone());
}
} }
} }
@@ -267,21 +260,9 @@ fn generate_report(start_time: DateTime<Utc>, snapshots: Vec<Snapshot>, mode: Pr
for snapshot in &snapshots { for snapshot in &snapshots {
for proc in &snapshot.processes { for proc in &snapshot.processes {
let entry = pid_map.entry(proc.pid).or_insert_with(|| ( let entry = pid_map.entry(proc.pid).or_insert_with(|| (proc.name.clone(), proc.parent_pid, Vec::new(), 0.0, 0.0, proc.is_syspulse, false));
proc.name.clone(),
proc.parent_pid,
Vec::new(),
0.0,
0.0,
proc.is_syspulse,
false
));
let mem_mb = proc.memory as f32 / 1024.0 / 1024.0; let mem_mb = proc.memory as f32 / 1024.0 / 1024.0;
entry.2.push(ProcessHistoryPoint { entry.2.push(ProcessHistoryPoint { time: snapshot.timestamp.format("%H:%M:%S").to_string(), cpu_usage: proc.cpu_usage, memory_mb: mem_mb });
time: snapshot.timestamp.format("%H:%M:%S").to_string(),
cpu_usage: proc.cpu_usage,
memory_mb: mem_mb,
});
if proc.cpu_usage > entry.3 { entry.3 = proc.cpu_usage; } if proc.cpu_usage > entry.3 { entry.3 = proc.cpu_usage; }
if mem_mb > entry.4 { entry.4 = mem_mb; } if mem_mb > entry.4 { entry.4 = mem_mb; }
if proc.status.contains("Zombie") { entry.6 = true; } if proc.status.contains("Zombie") { entry.6 = true; }
@@ -294,43 +275,20 @@ fn generate_report(start_time: DateTime<Utc>, snapshots: Vec<Snapshot>, mode: Pr
let mut warnings = Vec::new(); let mut warnings = Vec::new();
if is_zombie { warnings.push("Zombie".to_string()); } if is_zombie { warnings.push("Zombie".to_string()); }
if peak_cpu > 80.0 { warnings.push("High Peak".to_string()); } if peak_cpu > 80.0 { warnings.push("High Peak".to_string()); }
(pid, AggregatedProcess { (pid, AggregatedProcess { pid, name, avg_cpu: total_cpu / num_snapshots, peak_cpu, avg_memory_mb: total_mem / num_snapshots, peak_memory_mb: peak_mem, inclusive_avg_cpu: 0.0, inclusive_avg_memory_mb: 0.0, instance_count: 1, warnings, history, is_syspulse, children: Vec::new() })
pid,
name,
avg_cpu: total_cpu / num_snapshots,
peak_cpu,
avg_memory_mb: total_mem / num_snapshots,
peak_memory_mb: peak_mem,
inclusive_avg_cpu: 0.0,
inclusive_avg_memory_mb: 0.0,
instance_count: 1,
warnings,
history,
is_syspulse,
children: Vec::new(),
})
}).collect(); }).collect();
let mut child_to_parent = HashMap::new(); let mut child_to_parent = HashMap::new();
for snapshot in &snapshots { for snapshot in &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 { if nodes.contains_key(&ppid) { child_to_parent.insert(proc.pid, ppid); } }
if nodes.contains_key(&ppid) {
child_to_parent.insert(proc.pid, ppid);
}
}
} }
} }
let mut child_map: HashMap<u32, Vec<u32>> = HashMap::new(); let mut child_map: HashMap<u32, Vec<u32>> = HashMap::new();
for (&child, &parent) in &child_to_parent { for (&child, &parent) in &child_to_parent { child_map.entry(parent).or_default().push(child); }
child_map.entry(parent).or_default().push(child);
}
let root_pids: Vec<u32> = nodes.keys() let root_pids: Vec<u32> = nodes.keys().filter(|pid| !child_to_parent.contains_key(pid)).cloned().collect();
.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)?;
@@ -350,97 +308,47 @@ fn generate_report(start_time: DateTime<Utc>, snapshots: Vec<Snapshot>, mode: Pr
} }
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) { final_roots.push(root_node); } }
if let Some(root_node) = build_node(pid, &mut nodes, &child_map) {
final_roots.push(root_node);
}
}
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) { final_roots.push(node); } }
if let Some(node) = build_node(pid, &mut nodes, &child_map) {
final_roots.push(node);
}
}
if mode == ProfilingMode::Global { if mode == ProfilingMode::Global {
// Global mode aggregation: Group by NAME, and flattened
let mut name_groups: HashMap<String, AggregatedProcess> = HashMap::new(); let mut name_groups: HashMap<String, AggregatedProcess> = HashMap::new();
fn flatten_to_groups(node: AggregatedProcess, groups: &mut HashMap<String, AggregatedProcess>) { fn flatten_to_groups(node: AggregatedProcess, groups: &mut HashMap<String, AggregatedProcess>) {
let entry = groups.entry(node.name.clone()).or_insert_with(|| { let entry = groups.entry(node.name.clone()).or_insert_with(|| {
let mut base = node.clone(); let mut base = node.clone();
base.children = Vec::new(); base.children = Vec::new(); base.instance_count = 0; base.inclusive_avg_cpu = 0.0; base.inclusive_avg_memory_mb = 0.0; base.avg_cpu = 0.0; base.avg_memory_mb = 0.0;
base.instance_count = 0;
base.inclusive_avg_cpu = 0.0;
base.inclusive_avg_memory_mb = 0.0;
base.avg_cpu = 0.0;
base.avg_memory_mb = 0.0;
base base
}); });
entry.avg_cpu += node.avg_cpu; entry.avg_memory_mb += node.avg_memory_mb; entry.instance_count += 1;
entry.avg_cpu += node.avg_cpu;
entry.avg_memory_mb += node.avg_memory_mb;
entry.instance_count += 1;
if node.peak_cpu > entry.peak_cpu { entry.peak_cpu = node.peak_cpu; } if node.peak_cpu > entry.peak_cpu { entry.peak_cpu = node.peak_cpu; }
if node.peak_memory_mb > entry.peak_memory_mb { entry.peak_memory_mb = node.peak_memory_mb; } if node.peak_memory_mb > entry.peak_memory_mb { entry.peak_memory_mb = node.peak_memory_mb; }
for child in node.children { flatten_to_groups(child, groups); }
for child in node.children {
flatten_to_groups(child, groups);
}
} }
for root in final_roots { flatten_to_groups(root, &mut name_groups); }
for root in final_roots {
flatten_to_groups(root, &mut name_groups);
}
let mut flattened: Vec<AggregatedProcess> = name_groups.into_values().collect(); let mut flattened: Vec<AggregatedProcess> = name_groups.into_values().collect();
flattened.sort_by(|a, b| b.avg_cpu.partial_cmp(&a.avg_cpu).unwrap_or(std::cmp::Ordering::Equal)); flattened.sort_by(|a, b| b.avg_cpu.partial_cmp(&a.avg_cpu).unwrap_or(std::cmp::Ordering::Equal));
Report { start_time: start_time.to_rfc3339(), end_time: end_time.to_rfc3339(), duration_seconds: duration, mode, target_name, timeline, aggregated_processes: flattened }
Report {
start_time: start_time.to_rfc3339(),
end_time: end_time.to_rfc3339(),
duration_seconds: duration,
mode,
target_name,
timeline,
aggregated_processes: flattened,
}
} else { } else {
// Targeted mode: Return only the target root(s) and their hierarchy
let mut targeted_roots = Vec::new(); let mut targeted_roots = Vec::new();
if let Some(tpid) = target_pid { if let Some(tpid) = target_pid {
fn extract_target(nodes: Vec<AggregatedProcess>, tpid: u32) -> (Vec<AggregatedProcess>, Option<AggregatedProcess>) { fn extract_target(nodes: Vec<AggregatedProcess>, tpid: u32) -> (Vec<AggregatedProcess>, Option<AggregatedProcess>) {
let mut remaining = Vec::new(); let mut remaining = Vec::new(); let mut found = None;
let mut found = None;
for mut node in nodes { for mut node in nodes {
if node.pid == tpid { if node.pid == tpid { found = Some(node); }
found = Some(node); else {
} else {
let (new_children, sub_found) = extract_target(node.children, tpid); let (new_children, sub_found) = extract_target(node.children, tpid);
node.children = new_children; node.children = new_children;
if let Some(f) = sub_found { if let Some(f) = sub_found { found = Some(f); }
found = Some(f);
}
remaining.push(node); remaining.push(node);
} }
} }
(remaining, found) (remaining, found)
} }
let (_, target_node) = extract_target(final_roots, tpid); let (_, target_node) = extract_target(final_roots, tpid);
if let Some(t) = target_node { if let Some(t) = target_node { targeted_roots.push(t); }
targeted_roots.push(t);
}
}
Report {
start_time: start_time.to_rfc3339(),
end_time: end_time.to_rfc3339(),
duration_seconds: duration,
mode,
target_name,
timeline,
aggregated_processes: targeted_roots,
} }
Report { start_time: start_time.to_rfc3339(), end_time: end_time.to_rfc3339(), duration_seconds: duration, mode, target_name, timeline, aggregated_processes: targeted_roots }
} }
} }
@@ -451,41 +359,22 @@ fn get_system_stats(state: State<AppState>, minimal: bool) -> SystemStats {
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();
let mut pss_cache = state.pss_cache.lock().unwrap(); let mut pss_cache = state.pss_cache.lock().unwrap();
let self_pid = std::process::id(); let self_pid = std::process::id();
let syspulse_pids = get_syspulse_pids(self_pid, &sys); let syspulse_pids = get_syspulse_pids(self_pid, &sys);
let snapshot = collect_snapshot(&mut sys, &syspulse_pids, &mut pss_cache, profiling.is_active); let snapshot = collect_snapshot(&mut sys, &syspulse_pids, &mut pss_cache, profiling.is_active);
if profiling.is_active { profiling.snapshots.push(snapshot.clone()); }
if profiling.is_active {
profiling.snapshots.push(snapshot.clone());
}
let recording_duration = profiling.start_time.map(|s| (Utc::now() - s).num_seconds() as u64).unwrap_or(0); let recording_duration = profiling.start_time.map(|s| (Utc::now() - s).num_seconds() as u64).unwrap_or(0);
let display_processes = if minimal && !profiling.is_active { Vec::new() } else {
let display_processes = if minimal && !profiling.is_active {
Vec::new()
} else {
let mut p = snapshot.processes.clone(); let mut p = snapshot.processes.clone();
p.sort_by(|a, b| b.cpu_usage.partial_cmp(&a.cpu_usage).unwrap_or(std::cmp::Ordering::Equal)); p.sort_by(|a, b| b.cpu_usage.partial_cmp(&a.cpu_usage).unwrap_or(std::cmp::Ordering::Equal));
p.truncate(50); p.truncate(50);
p p
}; };
SystemStats { cpu_usage: snapshot.cpu_usage, total_memory: sys.total_memory(), used_memory: sys.used_memory(), processes: display_processes, is_recording: profiling.is_active, recording_duration }
SystemStats {
cpu_usage: snapshot.cpu_usage,
total_memory: sys.total_memory(),
used_memory: sys.used_memory(),
processes: display_processes,
is_recording: profiling.is_active,
recording_duration,
}
} }
#[tauri::command] #[tauri::command]
fn get_initial_report(state: State<AppState>) -> Option<Report> { fn get_initial_report(state: State<AppState>) -> Option<Report> { state.initial_report.lock().unwrap().clone() }
state.initial_report.lock().unwrap().clone()
}
#[tauri::command] #[tauri::command]
fn save_report(report: Report) -> Result<String, String> { fn save_report(report: Report) -> Result<String, String> {
@@ -498,21 +387,13 @@ fn save_report(report: Report) -> Result<String, String> {
#[tauri::command] #[tauri::command]
fn start_profiling(state: State<AppState>) { fn start_profiling(state: State<AppState>) {
let mut profiling = state.profiling.lock().unwrap(); let mut profiling = state.profiling.lock().unwrap();
profiling.is_active = true; profiling.is_active = true; profiling.mode = ProfilingMode::Global; profiling.target_pid = None; profiling.start_time = Some(Utc::now()); profiling.snapshots.clear();
profiling.mode = ProfilingMode::Global;
profiling.target_pid = None;
profiling.start_time = Some(Utc::now());
profiling.snapshots.clear();
} }
#[tauri::command] #[tauri::command]
fn start_targeted_profiling(state: State<AppState>, pid: u32) { fn start_targeted_profiling(state: State<AppState>, pid: u32) {
let mut profiling = state.profiling.lock().unwrap(); let mut profiling = state.profiling.lock().unwrap();
profiling.is_active = true; profiling.is_active = true; profiling.mode = ProfilingMode::Targeted; profiling.target_pid = Some(pid); profiling.start_time = Some(Utc::now()); profiling.snapshots.clear();
profiling.mode = ProfilingMode::Targeted;
profiling.target_pid = Some(pid);
profiling.start_time = Some(Utc::now());
profiling.snapshots.clear();
} }
#[tauri::command] #[tauri::command]
@@ -525,83 +406,33 @@ 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> {
let output = Command::new("pkexec") let output = Command::new("pkexec").arg("sh").arg("-c").arg(&command).output().map_err(|e| e.to_string())?;
.arg("sh") if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(String::from_utf8_lossy(&output.stderr).to_string()) }
.arg("-c")
.arg(&command)
.output()
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
} }
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let mut initial_report: Option<Report> = None; let mut initial_report: Option<Report> = None;
if let Some(file_path) = cli.file { if let Ok(content) = fs::read_to_string(file_path) { if let Ok(report) = serde_json::from_str(&content) { initial_report = Some(report); } } }
if let Some(file_path) = cli.file {
if let Ok(content) = fs::read_to_string(file_path) {
if let Ok(report) = serde_json::from_str(&content) {
initial_report = Some(report);
}
}
}
if cli.headless { if cli.headless {
println!("⚡ SysPulse: Starting headless profiling for {}s (interval: {}ms)...", cli.duration, cli.interval); println!("⚡ SysPulse: Starting headless profiling for {}s (interval: {}ms)...", cli.duration, cli.interval);
let mut sys = System::new_all(); let mut sys = System::new_all(); let mut pss_cache = HashMap::new(); let start_time = Utc::now(); let mut snapshots = Vec::new(); let self_pid = std::process::id();
let mut pss_cache = HashMap::new();
let start_time = Utc::now();
let mut snapshots = Vec::new();
let self_pid = std::process::id();
for i in 0..(cli.duration * 1000 / cli.interval) { for i in 0..(cli.duration * 1000 / cli.interval) {
let syspulse_pids = get_syspulse_pids(self_pid, &sys); let syspulse_pids = get_syspulse_pids(self_pid, &sys);
snapshots.push(collect_snapshot(&mut sys, &syspulse_pids, &mut pss_cache, true)); snapshots.push(collect_snapshot(&mut sys, &syspulse_pids, &mut pss_cache, true));
std::thread::sleep(Duration::from_millis(cli.interval)); std::thread::sleep(Duration::from_millis(cli.interval));
if (i + 1) % (1000 / cli.interval) == 0 { if (i + 1) % (1000 / cli.interval) == 0 { println!(" Progress: {}/{}s", (i + 1) * cli.interval / 1000, cli.duration); }
println!(" Progress: {}/{}s", (i + 1) * cli.interval / 1000, cli.duration);
}
} }
let report = generate_report(start_time, snapshots, ProfilingMode::Global, None); let report = generate_report(start_time, snapshots, ProfilingMode::Global, None);
let json = serde_json::to_string_pretty(&report).unwrap(); let json = serde_json::to_string_pretty(&report).unwrap();
let out_path = cli.output.unwrap_or_else(|| PathBuf::from(format!("syspulse_report_{}.json", Utc::now().format("%Y%m%d_%H%M%S")))); let out_path = cli.output.unwrap_or_else(|| PathBuf::from(format!("syspulse_report_{}.json", Utc::now().format("%Y%m%d_%H%M%S"))));
fs::write(&out_path, json).expect("Failed to write report"); fs::write(&out_path, json).expect("Failed to write report");
println!("✅ Report saved to: {:?}", out_path); println!("✅ Report saved to: {:?}", out_path);
if cli.gui { initial_report = Some(report); } else { return; }
if cli.gui {
initial_report = Some(report);
} else {
return;
}
} }
tauri::Builder::default() tauri::Builder::default()
.manage(AppState { .manage(AppState { sys: Mutex::new(System::new_all()), profiling: Mutex::new(ProfilingSession { is_active: false, mode: ProfilingMode::Global, target_pid: None, start_time: None, snapshots: Vec::new() }), initial_report: Mutex::new(initial_report), pss_cache: Mutex::new(HashMap::new()) })
sys: Mutex::new(System::new_all()), .invoke_handler(tauri::generate_handler![get_system_stats, get_initial_report, start_profiling, start_targeted_profiling, stop_profiling, run_as_admin, save_report])
profiling: Mutex::new(ProfilingSession {
is_active: false,
mode: ProfilingMode::Global,
target_pid: None,
start_time: None,
snapshots: Vec::new(),
}),
initial_report: Mutex::new(initial_report),
pss_cache: Mutex::new(HashMap::new()),
})
.invoke_handler(tauri::generate_handler![
get_system_stats,
get_initial_report,
start_profiling,
start_targeted_profiling,
stop_profiling,
run_as_admin,
save_report
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }