feat: implement automatic SysPulse detection and Report View overhead toggle

This commit is contained in:
2026-02-22 22:20:04 +01:00
parent 214a24be9d
commit ab4b2af800
2 changed files with 64 additions and 22 deletions

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;
@@ -31,6 +31,7 @@ struct ProcessStats {
memory: u64, memory: u64,
status: String, status: String,
user_id: Option<String>, user_id: Option<String>,
is_syspulse: bool,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -87,14 +88,29 @@ struct AggregatedProcess {
instance_count: usize, instance_count: usize,
warnings: Vec<String>, warnings: Vec<String>,
history: Vec<ProcessHistoryPoint>, history: Vec<ProcessHistoryPoint>,
is_syspulse: bool,
} }
// --- Commands --- // --- Commands ---
fn is_descendant_of(pid: u32, target_pid: u32, sys: &System) -> bool {
let mut current_pid = Some(Pid::from_u32(pid));
while let Some(p) = current_pid {
if p.as_u32() == target_pid {
return true;
}
if let Some(process) = sys.process(p) {
current_pid = process.parent();
} else {
break;
}
}
false
}
#[tauri::command] #[tauri::command]
fn get_system_stats( fn get_system_stats(
state: State<AppState>, state: State<AppState>,
exclude_self: bool,
minimal: bool minimal: bool
) -> SystemStats { ) -> SystemStats {
let mut sys = state.sys.lock().unwrap(); let mut sys = state.sys.lock().unwrap();
@@ -116,15 +132,16 @@ fn get_system_stats(
Vec::new() Vec::new()
} else { } else {
sys.processes().iter() sys.processes().iter()
.filter(|(pid, _)| !exclude_self || pid.as_u32() != self_pid)
.map(|(pid, process)| { .map(|(pid, process)| {
let pid_u32 = pid.as_u32();
ProcessStats { ProcessStats {
pid: pid.as_u32(), pid: pid_u32,
name: process.name().to_string_lossy().to_string(), name: process.name().to_string_lossy().to_string(),
cpu_usage: process.cpu_usage(), cpu_usage: process.cpu_usage(),
memory: process.memory(), memory: process.memory(),
status: format!("{:?}", process.status()), status: format!("{:?}", process.status()),
user_id: process.user_id().map(|uid| uid.to_string()), user_id: process.user_id().map(|uid| uid.to_string()),
is_syspulse: is_descendant_of(pid_u32, self_pid, &sys),
} }
}).collect() }).collect()
}; };
@@ -134,15 +151,16 @@ fn get_system_stats(
if minimal { if minimal {
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true); sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
processes = sys.processes().iter() processes = sys.processes().iter()
.filter(|(pid, _)| !exclude_self || pid.as_u32() != self_pid)
.map(|(pid, process)| { .map(|(pid, process)| {
let pid_u32 = pid.as_u32();
ProcessStats { ProcessStats {
pid: pid.as_u32(), pid: pid_u32,
name: process.name().to_string_lossy().to_string(), name: process.name().to_string_lossy().to_string(),
cpu_usage: process.cpu_usage(), cpu_usage: process.cpu_usage(),
memory: process.memory(), memory: process.memory(),
status: format!("{:?}", process.status()), status: format!("{:?}", process.status()),
user_id: process.user_id().map(|uid| uid.to_string()), user_id: process.user_id().map(|uid| uid.to_string()),
is_syspulse: is_descendant_of(pid_u32, self_pid, &sys),
} }
}).collect(); }).collect();
} }
@@ -218,6 +236,7 @@ fn stop_profiling(state: State<AppState>) -> Report {
let mut peak_stats: HashMap<String, (f32, f32)> = HashMap::new(); // (Peak CPU, Peak Mem) 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 unique_pids: HashMap<String, std::collections::HashSet<u32>> = HashMap::new();
let mut status_flags: HashMap<String, bool> = HashMap::new(); // Zombie check let mut status_flags: HashMap<String, bool> = HashMap::new(); // Zombie check
let mut syspulse_flags: HashMap<String, bool> = HashMap::new();
for snapshot in &profiling.snapshots { for snapshot in &profiling.snapshots {
let mut snapshot_procs: HashMap<String, (f32, u64)> = HashMap::new(); let mut snapshot_procs: HashMap<String, (f32, u64)> = HashMap::new();
@@ -231,6 +250,9 @@ fn stop_profiling(state: State<AppState>) -> Report {
if proc.status.contains("Zombie") { if proc.status.contains("Zombie") {
status_flags.insert(proc.name.clone(), true); 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 // Record history for all processes seen in this snapshot
@@ -256,6 +278,7 @@ fn stop_profiling(state: State<AppState>) -> Report {
for (name, history) in process_map { for (name, history) in process_map {
let (peak_cpu, peak_mem) = peak_stats.get(&name).cloned().unwrap_or((0.0, 0.0)); 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 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) // 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_cpu_sum: f32 = history.iter().map(|h| h.cpu_usage).sum();
@@ -284,6 +307,7 @@ fn stop_profiling(state: State<AppState>) -> Report {
instance_count: count, instance_count: count,
warnings, warnings,
history, history,
is_syspulse,
}); });
} }

View File

@@ -20,6 +20,7 @@ interface ProcessStats {
memory: number; memory: number;
status: string; status: string;
user_id?: string; user_id?: string;
is_syspulse: boolean;
} }
interface SystemStats { interface SystemStats {
@@ -52,6 +53,7 @@ interface AggregatedProcess {
instance_count: number; instance_count: number;
warnings: string[]; warnings: string[];
history: ProcessHistoryPoint[]; history: ProcessHistoryPoint[];
is_syspulse: boolean;
} }
interface ProfilingReport { interface ProfilingReport {
@@ -77,7 +79,6 @@ function App() {
try { try {
const isRecording = stats?.is_recording ?? false; const isRecording = stats?.is_recording ?? false;
const data = await invoke<SystemStats>('get_system_stats', { const data = await invoke<SystemStats>('get_system_stats', {
excludeSelf: true,
minimal: isRecording || view === 'report' minimal: isRecording || view === 'report'
}); });
setStats(data); setStats(data);
@@ -289,7 +290,10 @@ function App() {
{stats.processes.map((proc) => ( {stats.processes.map((proc) => (
<div key={proc.pid} className="grid grid-cols-[1fr_100px_100px_120px_80px] gap-4 px-4 py-4 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl"> <div key={proc.pid} className="grid grid-cols-[1fr_100px_100px_120px_80px] gap-4 px-4 py-4 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl">
<div className="font-black text-text truncate text-sm flex items-center gap-3" title={proc.name}> <div className="font-black text-text truncate text-sm flex items-center gap-3" title={proc.name}>
<div className="w-2 h-2 rounded-full bg-surface2 group-hover:bg-blue transition-colors" /> <div className={cn(
"w-2 h-2 rounded-full transition-colors",
proc.is_syspulse ? "bg-mauve" : "bg-surface2 group-hover:bg-blue"
)} />
{proc.name} {proc.name}
</div> </div>
<div className="font-mono text-overlay2 text-xs text-right self-center">{proc.pid}</div> <div className="font-mono text-overlay2 text-xs text-right self-center">{proc.pid}</div>
@@ -321,6 +325,7 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
const [sortField, setSortField] = useState<SortField>('avg_cpu'); const [sortField, setSortField] = useState<SortField>('avg_cpu');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [selectedProcess, setSelectedProcess] = useState<AggregatedProcess | null>(null); const [selectedProcess, setSelectedProcess] = useState<AggregatedProcess | null>(null);
const [hideProfiler, setHideProfiler] = useState(true);
const handleSort = (field: SortField) => { const handleSort = (field: SortField) => {
if (sortField === field) { if (sortField === field) {
@@ -332,7 +337,9 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
}; };
const sortedProcesses = useMemo(() => { const sortedProcesses = useMemo(() => {
return [...report.aggregated_processes].sort((a, b) => { return [...report.aggregated_processes]
.filter(p => !hideProfiler || !p.is_syspulse)
.sort((a, b) => {
const valA = a[sortField as keyof AggregatedProcess]; const valA = a[sortField as keyof AggregatedProcess];
const valB = b[sortField as keyof AggregatedProcess]; const valB = b[sortField as keyof AggregatedProcess];
@@ -345,7 +352,7 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
return sortOrder === 'asc' ? numA - numB : numB - numA; return sortOrder === 'asc' ? numA - numB : numB - numA;
}); });
}, [report, sortField, sortOrder]); }, [report, sortField, sortOrder, hideProfiler]);
const saveReport = async () => { const saveReport = async () => {
try { try {
@@ -366,6 +373,17 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<span className="font-black tracking-tighter uppercase italic text-xl">Profiling Report</span> <span className="font-black tracking-tighter uppercase italic text-xl">Profiling Report</span>
</div> </div>
<div className="flex gap-4 px-4"> <div className="flex gap-4 px-4">
<button
onClick={() => setHideProfiler(!hideProfiler)}
className={cn(
"flex items-center gap-2 px-4 py-1.5 border rounded-xl text-xs font-black transition-all uppercase tracking-widest",
hideProfiler
? "bg-blue/10 border-blue/30 text-blue hover:bg-blue/20"
: "bg-surface1/50 border-surface2 text-subtext0 hover:text-text"
)}
>
<Shield size={14} /> {hideProfiler ? "SHOW PROFILER" : "HIDE PROFILER"}
</button>
<button <button
onClick={saveReport} onClick={saveReport}
className="flex items-center gap-2 px-4 py-1.5 bg-surface1/50 hover:bg-surface1 border border-surface2 rounded-xl text-xs font-black transition-all text-text uppercase tracking-widest" className="flex items-center gap-2 px-4 py-1.5 bg-surface1/50 hover:bg-surface1 border border-surface2 rounded-xl text-xs font-black transition-all text-text uppercase tracking-widest"