From 8a24fa568905b7fcfeefe1130b65918f1ebe7a6f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 23 Feb 2026 17:56:53 +0100 Subject: [PATCH] updated profiling options --- src-tauri/src/main.rs | 147 ++++++++++++++++++++++++---- src/App.tsx | 217 +++++++++++++++++++++++++++++------------- 2 files changed, 279 insertions(+), 85 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 453884d..48dbd6d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,7 +3,7 @@ windows_subsystem = "windows" )] -use sysinfo::System; +use sysinfo::{System, Pid}; use std::sync::Mutex; use std::process::Command; use tauri::State; @@ -14,6 +14,7 @@ use std::fs; use clap::Parser; use std::path::PathBuf; use std::time::Duration; +use rayon::prelude::*; // --- CLI --- @@ -36,6 +37,12 @@ struct Cli { // --- Data Structures --- +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)] +enum ProfilingMode { + Global, + Targeted, +} + #[derive(Serialize, Clone)] struct SystemStats { cpu_usage: Vec, @@ -68,6 +75,8 @@ struct Snapshot { struct ProfilingSession { is_active: bool, + mode: ProfilingMode, + target_pid: Option, start_time: Option>, snapshots: Vec, } @@ -86,6 +95,8 @@ struct Report { start_time: String, end_time: String, duration_seconds: i64, + mode: ProfilingMode, + target_name: Option, timeline: Vec, aggregated_processes: Vec, } @@ -93,8 +104,10 @@ struct Report { #[derive(Serialize, Deserialize, Clone)] struct TimelinePoint { time: String, - avg_cpu: f32, - memory_gb: f32, + cpu_total: f32, + mem_total_gb: f32, + cpu_profiler: f32, + mem_profiler_gb: f32, } #[derive(Serialize, Deserialize, Clone)] @@ -220,19 +233,36 @@ fn collect_snapshot( } } -fn generate_report(start_time: DateTime, snapshots: Vec) -> Report { +fn generate_report(start_time: DateTime, snapshots: Vec, mode: ProfilingMode, target_pid: Option) -> Report { let end_time = Utc::now(); let duration = (end_time - start_time).num_seconds(); let timeline: Vec = snapshots.iter().map(|s| { - let avg_cpu = s.cpu_usage.iter().sum::() / s.cpu_usage.len() as f32; + let cpu_total = s.cpu_usage.iter().sum::() / s.cpu_usage.len() as f32; + 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)); + TimelinePoint { time: s.timestamp.format("%H:%M:%S").to_string(), - avg_cpu, - memory_gb: s.used_memory as f32 / 1024.0 / 1024.0 / 1024.0, + cpu_total, + mem_total_gb, + cpu_profiler: profiler_stats.0 / s.cpu_usage.len() as f32, + mem_profiler_gb: profiler_stats.1 as f32 / 1024.0 / 1024.0 / 1024.0, } }).collect(); + let mut target_name = None; + if let Some(tpid) = target_pid { + if let Some(snapshot) = snapshots.first() { + if let Some(p) = snapshot.processes.iter().find(|p| p.pid == tpid) { + target_name = Some(p.name.clone()); + } + } + } + let mut pid_map: HashMap, Vec, f32, f32, bool, bool)> = HashMap::new(); let num_snapshots = snapshots.len() as f32; @@ -332,14 +362,82 @@ fn generate_report(start_time: DateTime, snapshots: Vec) -> Repor final_roots.push(node); } } - 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_time.to_rfc3339(), - end_time: end_time.to_rfc3339(), - duration_seconds: duration, - timeline, - aggregated_processes: final_roots, + if mode == ProfilingMode::Global { + // Global mode aggregation: Group by NAME, and flattened + let mut name_groups: HashMap = HashMap::new(); + + fn flatten_to_groups(node: AggregatedProcess, groups: &mut HashMap) { + let entry = groups.entry(node.name.clone()).or_insert_with(|| { + let mut base = node.clone(); + 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 + }); + + 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_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 root in final_roots { + flatten_to_groups(root, &mut name_groups); + } + + let mut flattened: Vec = name_groups.into_values().collect(); + 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, + } + } else { + // Targeted mode: Return only the target root(s) and their hierarchy + let mut targeted_roots = Vec::new(); + if let Some(tpid) = target_pid { + // Find target or its closest surviving ancestor/child in the tree + // Actually final_roots already contains the full tree. + // We want only the root that contains our target PID. + fn find_target_node(roots: &mut Vec, tpid: u32) -> Option { + for i in 0..roots.len() { + if roots[i].pid == tpid { + return Some(roots.remove(i)); + } + if let Some(found) = find_target_node(&mut roots[i].children, tpid) { + return Some(found); + } + } + None + } + if let Some(target_tree) = find_target_node(&mut final_roots, tpid) { + targeted_roots.push(target_tree); + } + } + + Report { + start_time: start_time.to_rfc3339(), + end_time: end_time.to_rfc3339(), + duration_seconds: duration, + mode, + target_name, + timeline, + aggregated_processes: targeted_roots, + } } } @@ -354,8 +452,6 @@ fn get_system_stats(state: State, minimal: bool) -> SystemStats { let self_pid = std::process::id(); let syspulse_pids = get_syspulse_pids(self_pid, &sys); - // NO PSS collection during live dashboard. ONLY during profiling or after. - // This is the primary cause of high CPU/I/O. let snapshot = collect_snapshot(&mut sys, &syspulse_pids, &mut pss_cache, profiling.is_active); if profiling.is_active { @@ -400,6 +496,18 @@ fn save_report(report: Report) -> Result { fn start_profiling(state: State) { let mut profiling = state.profiling.lock().unwrap(); profiling.is_active = true; + profiling.mode = ProfilingMode::Global; + profiling.target_pid = None; + profiling.start_time = Some(Utc::now()); + profiling.snapshots.clear(); +} + +#[tauri::command] +fn start_targeted_profiling(state: State, pid: u32) { + let mut profiling = state.profiling.lock().unwrap(); + profiling.is_active = true; + profiling.mode = ProfilingMode::Targeted; + profiling.target_pid = Some(pid); profiling.start_time = Some(Utc::now()); profiling.snapshots.clear(); } @@ -409,7 +517,7 @@ fn stop_profiling(state: State) -> Report { let mut profiling = state.profiling.lock().unwrap(); profiling.is_active = false; let snapshots: Vec = profiling.snapshots.drain(..).collect(); - generate_report(profiling.start_time.unwrap_or(Utc::now()), snapshots) + generate_report(profiling.start_time.unwrap_or(Utc::now()), snapshots, profiling.mode, profiling.target_pid) } #[tauri::command] @@ -456,7 +564,7 @@ fn main() { } } - let report = generate_report(start_time, snapshots); + let report = generate_report(start_time, snapshots, ProfilingMode::Global, None); 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")))); fs::write(&out_path, json).expect("Failed to write report"); @@ -474,6 +582,8 @@ fn main() { 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(), }), @@ -484,6 +594,7 @@ fn main() { get_system_stats, get_initial_report, start_profiling, + start_targeted_profiling, stop_profiling, run_as_admin, save_report diff --git a/src/App.tsx b/src/App.tsx index 51b4310..89e39ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,13 +6,18 @@ import { } from 'recharts'; import { Activity, Cpu, Server, Database, Play, Square, - AlertTriangle, ArrowLeft, Shield, Save, X, Download, CheckSquare + AlertTriangle, ArrowLeft, Shield, Save, X, Download, CheckSquare, Target, Eye, EyeOff } from 'lucide-react'; import { clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; // --- Types --- +enum ProfilingMode { + Global = "Global", + Targeted = "Targeted" +} + interface ProcessStats { pid: number; name: string; @@ -34,8 +39,10 @@ interface SystemStats { interface TimelinePoint { time: string; - avg_cpu: number; - memory_gb: number; + cpu_total: number; + mem_total_gb: number; + cpu_profiler: number; + mem_profiler_gb: number; } interface ProcessHistoryPoint { @@ -64,6 +71,8 @@ interface ProfilingReport { start_time: string; end_time: string; duration_seconds: number; + mode: ProfilingMode; + target_name?: string; timeline: TimelinePoint[]; aggregated_processes: AggregatedProcess[]; } @@ -157,6 +166,10 @@ function App() { } }; + const startTargeted = async (pid: number) => { + await invoke('start_targeted_profiling', { pid }); + }; + const handleImport = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -246,18 +259,18 @@ function App() {

{formatDuration(stats.recording_duration)}

-
+
Global CPU
{avgCpu.toFixed(1)}%
-
+
Global RAM
{memoryPercent.toFixed(1)}%
- Minimal footprint mode enabled + Performance optimized snapshot mode enabled
) : ( @@ -287,13 +300,13 @@ function App() {
Memory
- {(stats.used_memory / 1024 / 1024 / 1024).toFixed(1)} - GB used + {formatBytes(stats.used_memory)} + used
{memoryPercent.toFixed(1)}% Utilized - {(stats.total_memory / 1024 / 1024 / 1024).toFixed(0)} GB Total + {formatBytes(stats.total_memory, 0)} Total
@@ -308,7 +321,7 @@ function App() { Processes
- Live system feed. Profiling session will provide a full hierarchical tree. + Live system feed. Target a process to profile its hierarchy in detail.
@@ -325,17 +338,17 @@ function App() {
-
+
Process
PID
CPU %
Memory
-
Action
+
Actions
{stats.processes.map((proc) => ( -
+
{proc.name}
-
{proc.pid}
+
{proc.pid}
{proc.cpu_usage.toFixed(1)}%
-
{(proc.memory / 1024 / 1024).toFixed(0)} MB
-
- +
{formatBytes(proc.memory, 0)}
+
+ +
))} @@ -366,10 +386,10 @@ function App() { ); } -type SortField = 'name' | 'pid' | 'inclusive_avg_cpu' | 'peak_cpu' | 'inclusive_avg_memory_mb' | 'peak_memory_mb'; +type SortField = 'name' | 'pid' | 'inclusive_avg_cpu' | 'peak_cpu' | 'inclusive_avg_memory_mb' | 'peak_memory_mb' | 'avg_cpu' | 'avg_memory_mb'; function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () => void }) { - const [sortField, setSortField] = useState('inclusive_avg_cpu'); + const [sortField, setSortField] = useState(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_cpu' : 'avg_cpu'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [selectedProcess, setSelectedProcess] = useState(null); const [hideProfiler, setHideProfiler] = useState(true); @@ -391,9 +411,12 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () => } }; + const filteredProcesses = useMemo(() => { + return report.aggregated_processes.filter(p => !hideProfiler || !p.is_syspulse); + }, [report, hideProfiler]); + const sortedProcesses = useMemo(() => { - return [...report.aggregated_processes] - .filter(p => !hideProfiler || !p.is_syspulse) + return [...filteredProcesses] .sort((a, b) => { const valA = a[sortField as keyof AggregatedProcess]; const valB = b[sortField as keyof AggregatedProcess]; @@ -407,7 +430,21 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () => return sortOrder === 'asc' ? numA - numB : numB - numA; }); - }, [report, sortField, sortOrder, hideProfiler]); + }, [filteredProcesses, sortField, sortOrder]); + + const reactiveTimeline = useMemo(() => { + return report.timeline.map(p => ({ + ...p, + cpu: hideProfiler ? (p.cpu_total - p.cpu_profiler) : p.cpu_total, + mem: hideProfiler ? (p.mem_total_gb - p.mem_profiler_gb) : p.mem_total_gb + })); + }, [report, hideProfiler]); + + const summaryStats = useMemo(() => { + const totalCpu = sortedProcesses.reduce((acc, p) => acc + p.avg_cpu, 0); + const totalMem = sortedProcesses.reduce((acc, p) => acc + p.avg_memory_mb, 0); + return { cpu: totalCpu, mem: totalMem }; + }, [sortedProcesses]); const saveReport = async () => { try { @@ -495,7 +532,9 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
- Profiling Report + + {report.mode === ProfilingMode.Targeted ? `Target: ${report.target_name || "Process"}` : "Global Report"} +
-
Session End
-
{new Date(report.end_time).toLocaleTimeString()}
+
Impact Avg CPU
+
{summaryStats.cpu.toFixed(1)}%
-
Root Processes
-
{report.aggregated_processes.length}
+
Impact Avg RAM
+
{formatMB(summaryStats.mem)}
Issue Alerts
- {report.aggregated_processes.filter(p => p.warnings.length > 0).length} + {filteredProcesses.filter(p => p.warnings.length > 0).length}
@@ -549,12 +588,12 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>

- Session Load Profile + {hideProfiler ? "Reactive Analysis Profile (Profiler Hidden)" : "Full System Load Profile"}

- + @@ -568,13 +607,13 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () => contentStyle={{ background: 'var(--mantle)', border: '1px solid var(--surface1)', borderRadius: '12px', fontSize: '11px', fontWeight: 'bold' }} itemStyle={{ color: 'var(--text)' }} formatter={(value: number, name: string) => { - if (name === 'MEM GB') return [formatBytes(value * 1024 * 1024 * 1024), 'RAM']; - if (name === 'CPU %') return [`${value.toFixed(1)}%`, 'CPU']; + if (name === 'mem') return [formatBytes(value * 1024 * 1024 * 1024), 'RAM']; + if (name === 'cpu') return [`${value.toFixed(1)}%`, 'CPU']; return [value, name]; }} /> - - + +
@@ -583,13 +622,17 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>

- Hierarchical Matrix + {report.mode === ProfilingMode.Targeted ? "Hierarchical Analysis" : "Aggregated Resource Matrix"}

-
-
Inclusive Sum -
- Toggle Nodes to Expand + {report.mode === ProfilingMode.Targeted && ( +
+
Inclusive Sum +
+ )} + + {report.mode === ProfilingMode.Targeted ? "Toggle Nodes to Expand" : "Grouped by Application Name"} +
@@ -597,21 +640,51 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
handleSort('name')}> Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
-
handleSort('pid')}>PID
-
handleSort('inclusive_avg_cpu')}>Total CPU
+
handleSort('pid')}> + {report.mode === ProfilingMode.Targeted ? "PID" : "Units"} +
+
handleSort(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_cpu' : 'avg_cpu')}>Avg CPU
handleSort('peak_cpu')}>Peak
-
handleSort('inclusive_avg_memory_mb')}>Total Mem
+
handleSort(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_memory_mb' : 'avg_memory_mb')}>Avg Mem
handleSort('peak_memory_mb')}>Peak
Insights
- {renderTreeRows(sortedProcesses)} + {report.mode === ProfilingMode.Targeted ? renderTreeRows(sortedProcesses) : sortedProcesses.map((proc, i) => ( +
setSelectedProcess(proc)} + className="grid grid-cols-[1fr_80px_100px_100px_100px_100px_200px] gap-4 px-4 py-4 border-b border-surface1/20 hover:bg-surface1/20 cursor-pointer transition-all rounded-xl group" + > +
+
+ {proc.name} +
+
{proc.instance_count}
+
{proc.avg_cpu.toFixed(1)}%
+
{proc.peak_cpu.toFixed(1)}%
+
{formatMB(proc.avg_memory_mb)}
+
{formatMB(proc.peak_memory_mb, 0)}
+
+ {proc.warnings.length > 0 ? proc.warnings.map((w, idx) => ( + + {w} + + )) : ( + Healthy Cluster + )} +
+
+ ))}
- Memory is Proportional Set Size (PSS). It includes private memory plus a proportional share of shared libraries, providing an accurate sum of total system impact. + Real Memory (PSS) used for accuracy. Global mode aggregates processes by application name for performance.
@@ -621,7 +694,9 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
-
Process Inspector (PID: {selectedProcess.pid})
+
+ {report.mode === ProfilingMode.Targeted ? `Process Inspector (PID: ${selectedProcess.pid})` : "Application Cluster Inspector"} +

{selectedProcess.name}