feat: implement automatic SysPulse detection and Report View overhead toggle
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
src/App.tsx
44
src/App.tsx
@@ -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,20 +337,22 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sortedProcesses = useMemo(() => {
|
const sortedProcesses = useMemo(() => {
|
||||||
return [...report.aggregated_processes].sort((a, b) => {
|
return [...report.aggregated_processes]
|
||||||
const valA = a[sortField as keyof AggregatedProcess];
|
.filter(p => !hideProfiler || !p.is_syspulse)
|
||||||
const valB = b[sortField as keyof AggregatedProcess];
|
.sort((a, b) => {
|
||||||
|
const valA = a[sortField as keyof AggregatedProcess];
|
||||||
|
const valB = b[sortField as keyof AggregatedProcess];
|
||||||
|
|
||||||
if (typeof valA === 'string' && typeof valB === 'string') {
|
if (typeof valA === 'string' && typeof valB === 'string') {
|
||||||
return sortOrder === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
return sortOrder === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
||||||
}
|
}
|
||||||
|
|
||||||
const numA = (valA as number) ?? 0;
|
const numA = (valA as number) ?? 0;
|
||||||
const numB = (valB as number) ?? 0;
|
const numB = (valB as number) ?? 0;
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user