From 6b0466a526c4ac1f543acf37e631374d2a0e0907 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 23 Feb 2026 18:51:53 +0100 Subject: [PATCH] refactor: modularize backend and frontend for better maintainability --- src/App.tsx | 775 +--------------------------- src/components/Dashboard.tsx | 256 +++++++++ src/components/ProcessInspector.tsx | 124 +++++ src/components/ReportView.tsx | 336 ++++++++++++ src/types/index.ts | 63 +++ src/utils/index.ts | 25 + 6 files changed, 818 insertions(+), 761 deletions(-) create mode 100644 src/components/Dashboard.tsx create mode 100644 src/components/ProcessInspector.tsx create mode 100644 src/components/ReportView.tsx create mode 100644 src/types/index.ts create mode 100644 src/utils/index.ts diff --git a/src/App.tsx b/src/App.tsx index 3fdac9d..112545b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,106 +1,14 @@ -import { useState, useEffect, useMemo, Fragment } from 'react'; +import { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; -import { - AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, - CartesianGrid -} from 'recharts'; -import { - Activity, Cpu, Server, Database, Play, Square, - 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; - cpu_usage: number; - memory: number; - status: string; - user_id?: string; - is_syspulse: boolean; -} - -interface SystemStats { - cpu_usage: number[]; - total_memory: number; - used_memory: number; - processes: ProcessStats[]; - is_recording: boolean; - recording_duration: number; -} - -interface TimelinePoint { - time: string; - cpu_total: number; - mem_total_gb: number; - cpu_profiler: number; - mem_profiler_gb: number; -} - -interface ProcessHistoryPoint { - time: string; - cpu_usage: number; - memory_mb: number; -} - -interface AggregatedProcess { - pid: number; - name: string; - avg_cpu: number; - peak_cpu: number; - avg_memory_mb: number; - peak_memory_mb: number; - inclusive_avg_cpu: number; - inclusive_avg_memory_mb: number; - instance_count: number; - warnings: string[]; - history: ProcessHistoryPoint[]; - is_syspulse: boolean; - children: AggregatedProcess[]; -} - -interface ProfilingReport { - start_time: string; - end_time: string; - duration_seconds: number; - mode: ProfilingMode; - target_name?: string; - timeline: TimelinePoint[]; - aggregated_processes: AggregatedProcess[]; -} - -function cn(...inputs: (string | undefined | null | false)[]) { - return twMerge(clsx(inputs)); -} - -function formatBytes(bytes: number, decimals = 1) { - if (!bytes || bytes === 0) return '0 B'; - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; -} - -function formatMB(mb: number, decimals = 1) { - return formatBytes(mb * 1024 * 1024, decimals); -} +import { SystemStats, ProfilingReport } from './types'; +import { Dashboard } from './components/Dashboard'; +import { ReportView } from './components/ReportView'; function App() { const [view, setView] = useState<'dashboard' | 'report'>('dashboard'); const [stats, setStats] = useState(null); const [history, setHistory] = useState<{ time: string; cpu: number }[]>([]); const [report, setReport] = useState(null); - const [searchTerm, setSearchBar] = useState(""); - const [pinnedPid, setPinnedPid] = useState(null); // Initial report check useEffect(() => { @@ -164,44 +72,7 @@ function App() { setReport(reportData); setView('report'); } else { - if (pinnedPid) { - await invoke('start_targeted_profiling', { pid: pinnedPid }); - } else { - await invoke('start_profiling'); - } - } - }; - - 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; - - const reader = new FileReader(); - reader.onload = (event) => { - try { - const data = JSON.parse(event.target?.result as string) as ProfilingReport; - if (data.aggregated_processes && data.timeline) { - setReport(data); - setView('report'); - } else { - alert('Invalid report format'); - } - } catch (err) { - alert('Failed to parse JSON'); - } - }; - reader.readAsText(file); - }; - - const killProcess = async (pid: number) => { - try { - await invoke('run_as_admin', { command: `kill -9 ${pid}` }); - } catch (e) { - alert(`Failed to kill process: ${e}`); + await invoke('start_profiling'); } }; @@ -216,635 +87,17 @@ function App() { if (!stats) return
Initializing SysPulse...
; - const avgCpu = stats.cpu_usage.reduce((a, b) => a + b, 0) / stats.cpu_usage.length; - const memoryPercent = (stats.used_memory / stats.total_memory) * 100; - - const filteredLiveProcs = stats.processes.filter(p => - p.name.toLowerCase().includes(searchTerm.toLowerCase()) || - p.pid.toString().includes(searchTerm) - ); - return ( -
-
-
-
- -
- SysPulse -
-
- -
- -
-
- -
- {stats.is_recording ? ( -
-
-
-
-
- -
-
-
-

Profiling Active

-

{formatDuration(stats.recording_duration)}

-
-
-
-
Global CPU
-
{avgCpu.toFixed(1)}%
-
-
-
Global RAM
-
{memoryPercent.toFixed(1)}%
-
-
-
- - Performance optimized snapshot mode enabled -
-
- ) : ( - <> -
-
-
CPU Load
-
- {avgCpu.toFixed(1)} - % -
-
- - - - - - - - - - - -
-
- -
-
Memory
-
- {formatBytes(stats.used_memory)} - used -
-
-
- {memoryPercent.toFixed(1)}% Utilized - {formatBytes(stats.total_memory, 0)} Total -
-
-
-
-
-
- -
-
Tasks
-
- {stats.processes.length} - Processes -
-
- Live system feed. Target a process to profile its hierarchy in detail. -
-
-
- -
-
-

- - Live Feed -

-
-
- setSearchBar(e.target.value)} - className="bg-surface1/50 border border-surface2 rounded-xl px-4 py-2 text-xs font-bold text-text placeholder:text-overlay1 focus:outline-none focus:ring-2 focus:ring-blue/30 w-64 transition-all" - /> -
-
-
- Real-time -
-
-
- -
-
Process
-
PID
-
CPU %
-
Memory
-
Actions
-
- -
- {filteredLiveProcs.map((proc) => ( -
setPinnedPid(pinnedPid === proc.pid ? null : proc.pid)} - > -
-
- {proc.name} -
-
{proc.pid}
-
{proc.cpu_usage.toFixed(1)}%
-
{formatBytes(proc.memory, 0)}
-
- - -
-
- ))} -
-
- - )} -
-
+ { + setReport(importedReport); + setView('report'); + }} + /> ); } -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(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); - const [expandedNodes, setExpandedNodes] = useState>(new Set()); - - const toggleExpand = (pid: number) => { - const newExpanded = new Set(expandedNodes); - if (newExpanded.has(pid)) newExpanded.delete(pid); - else newExpanded.add(pid); - setExpandedNodes(newExpanded); - }; - - const handleSort = (field: SortField) => { - if (sortField === field) { - setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); - } else { - setSortField(field); - setSortOrder('desc'); - } - }; - - const filteredProcesses = useMemo(() => { - return report.aggregated_processes.filter(p => !hideProfiler || !p.is_syspulse); - }, [report, hideProfiler]); - - const sortedProcesses = useMemo(() => { - return [...filteredProcesses] - .sort((a, b) => { - const valA = a[sortField as keyof AggregatedProcess]; - const valB = b[sortField as keyof AggregatedProcess]; - - if (typeof valA === 'string' && typeof valB === 'string') { - return sortOrder === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA); - } - - const numA = (valA as number) ?? 0; - const numB = (valB as number) ?? 0; - - return sortOrder === 'asc' ? numA - numB : numB - numA; - }); - }, [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(() => { - if (report.mode === ProfilingMode.Targeted) { - // In targeted mode, the root of sortedProcesses is our target - const root = sortedProcesses[0]; - if (root) { - return { cpu: root.inclusive_avg_cpu, mem: root.inclusive_avg_memory_mb }; - } - } - 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, report.mode]); - - const saveReport = async () => { - try { - const path = await invoke('save_report', { report }); - alert(`Report saved to: ${path}`); - } catch (e) { - alert(`Failed to save: ${e}`); - } - }; - - const renderTreeRows = (nodes: AggregatedProcess[], depth = 0): React.ReactNode => { - return nodes.map((proc) => { - const isExpanded = expandedNodes.has(proc.pid); - const hasChildren = proc.children && proc.children.length > 0; - - return ( - -
{ - e.stopPropagation(); - setSelectedProcess(proc); - }} - > -
- {hasChildren ? ( - - ) : ( -
- )} -
- {proc.name} -
- -
{proc.pid}
- -
- {proc.inclusive_avg_cpu.toFixed(1)}% - {proc.children.length > 0 && ({proc.avg_cpu.toFixed(1)}% self)} -
- -
{proc.peak_cpu.toFixed(1)}%
- -
- {formatMB(proc.inclusive_avg_memory_mb)} - {proc.children.length > 0 && ({formatMB(proc.avg_memory_mb)} self)} -
- -
{formatMB(proc.peak_memory_mb, 0)}
- -
- {proc.warnings.length > 0 ? proc.warnings.map((w, idx) => ( - - {w} - - )) : ( - proc.children.length > 0 ? {proc.children.length} units : null - )} -
-
- {hasChildren && isExpanded && renderTreeRows( - proc.children.filter(c => !hideProfiler || !c.is_syspulse), - depth + 1 - )} - - ); - }); - }; - - return ( -
-
-
-
- -
- - {report.mode === ProfilingMode.Targeted ? `Target: ${report.target_name || "Process"}` : "Global Report"} - -
-
- - - -
-
- -
-
-
-
Session Duration
-
{formatDuration(report.duration_seconds)}
-
-
-
Impact Avg CPU
-
{summaryStats.cpu.toFixed(1)}%
-
-
-
Impact Avg RAM
-
{formatMB(summaryStats.mem)}
-
-
-
Issue Alerts
-
- {filteredProcesses.filter(p => p.warnings.length > 0).length} -
-
-
- -
-
-

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

-
-
- - - - - - - - - - - - { - if (name === 'mem') return [formatBytes(value * 1024 * 1024 * 1024), 'RAM']; - if (name === 'cpu') return [`${value.toFixed(1)}%`, 'CPU']; - return [value, name]; - }} - /> - - - - -
-
- -
-
-

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

-
- {report.mode === ProfilingMode.Targeted && ( -
-
Inclusive Sum -
- )} - - {report.mode === ProfilingMode.Targeted ? "Toggle Nodes to Expand" : "Grouped by Application Name"} - -
-
- -
-
handleSort('name')}> - Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')} -
-
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(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_memory_mb' : 'avg_memory_mb')}>Avg Mem
-
handleSort('peak_memory_mb')}>Peak
-
Insights
-
- -
- {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 - )} -
-
- ))} -
- -
- - Real Memory (PSS) used for accuracy. Global mode aggregates processes by application name for performance. -
-
- - {/* Process Detail Side Panel/Modal */} - {selectedProcess && ( -
-
-
-
-
- {report.mode === ProfilingMode.Targeted ? `Process Inspector (PID: ${selectedProcess.pid})` : "Application Cluster Inspector"} -
-

{selectedProcess.name}

-
- -
- -
-
-
-
Total Avg CPU
-
- {(report.mode === ProfilingMode.Targeted ? selectedProcess.inclusive_avg_cpu : selectedProcess.avg_cpu).toFixed(1)}% -
-

Combined impact of this application.

-
-
-
Total Avg RAM
-
- {formatMB(report.mode === ProfilingMode.Targeted ? selectedProcess.inclusive_avg_memory_mb : selectedProcess.avg_memory_mb)} -
-

Proportional memory footprint.

-
-
- -
-
Resource History (Summed)
-
- - - - - - - - - - - - - - - - { - if (name === 'memory_mb') return [formatMB(value), 'RAM']; - if (name === 'cpu_usage') return [`${value.toFixed(1)}%`, 'CPU']; - return [value, name]; - }} - /> - - - - -
-
- -
-

Profiling Details

-
-
- Peak Recorded Load - {selectedProcess.peak_cpu.toFixed(1)}% -
-
- Peak Recorded RAM - {formatMB(selectedProcess.peak_memory_mb)} -
-
- - {report.mode === ProfilingMode.Targeted ? "Child Process Count" : "Active Instances Count"} - - - {report.mode === ProfilingMode.Targeted ? selectedProcess.children.length : selectedProcess.instance_count} - -
-
-
- -
-

Profiling Insights

-
- {selectedProcess.warnings.length > 0 ? selectedProcess.warnings.map((w, idx) => ( -
- - {w} -
- )) : ( -
- - Healthy Performance Profile -
- )} -
-
-
-
-
- )} -
-
- ); -} - -function formatDuration(seconds: number) { - const m = Math.floor(seconds / 60); - const s = Math.floor(seconds % 60); - return `${m}:${s.toString().padStart(2, '0')}`; -} - export default App; diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx new file mode 100644 index 0000000..2ea5d21 --- /dev/null +++ b/src/components/Dashboard.tsx @@ -0,0 +1,256 @@ +import { useState } from 'react'; +import { + Activity, Cpu, Server, Database, Play, Square, Shield, Target, Download +} from 'lucide-react'; +import { AreaChart, Area, ResponsiveContainer } from 'recharts'; +import { invoke } from '@tauri-apps/api/core'; +import { SystemStats, ProfilingReport } from '../types'; +import { cn, formatBytes, formatDuration } from '../utils'; + +interface Props { + stats: SystemStats; + history: { time: string; cpu: number }[]; + onRecordingToggle: () => void; + onImport: (report: ProfilingReport) => void; +} + +export function Dashboard({ stats, history, onRecordingToggle, onImport }: Props) { + const [searchTerm, setSearchBar] = useState(""); + const [pinnedPid, setPinnedPid] = useState(null); + + const avgCpu = stats.cpu_usage.reduce((a, b) => a + b, 0) / stats.cpu_usage.length; + const memoryPercent = (stats.used_memory / stats.total_memory) * 100; + + const filteredLiveProcs = stats.processes.filter(p => + p.name.toLowerCase().includes(searchTerm.toLowerCase()) || + p.pid.toString().includes(searchTerm) + ); + + const startTargeted = async (pid: number) => { + await invoke('start_targeted_profiling', { pid }); + }; + + const killProcess = async (pid: number) => { + try { + await invoke('run_as_admin', { command: `kill -9 ${pid}` }); + } catch (e) { + alert(`Failed to kill process: ${e}`); + } + }; + + const handleImport = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const data = JSON.parse(event.target?.result as string) as ProfilingReport; + if (data.aggregated_processes && data.timeline) { + onImport(data); + } else { + alert('Invalid report format'); + } + } catch (err) { + alert('Failed to parse JSON'); + } + }; + reader.readAsText(file); + }; + + return ( +
+
+
+
+ +
+ SysPulse +
+
+ +
+ +
+
+ +
+ {stats.is_recording ? ( +
+
+
+
+
+ +
+
+
+

Profiling Active

+

{formatDuration(stats.recording_duration)}

+
+
+
+
Global CPU
+
{avgCpu.toFixed(1)}%
+
+
+
Global RAM
+
{memoryPercent.toFixed(1)}%
+
+
+
+ + Performance optimized snapshot mode enabled +
+
+ ) : ( + <> +
+
+
CPU Load
+
+ {avgCpu.toFixed(1)} + % +
+
+ + + + + + + + + + + +
+
+ +
+
Memory
+
+ {formatBytes(stats.used_memory)} + used +
+
+
+ {memoryPercent.toFixed(1)}% Utilized + {formatBytes(stats.total_memory, 0)} Total +
+
+
+
+
+
+ +
+
Tasks
+
+ {stats.processes.length} + Processes +
+
+ Live system feed. Target a process to profile its hierarchy in detail. +
+
+
+ +
+
+

+ + Live Feed +

+
+
+ setSearchBar(e.target.value)} + className="bg-surface1/50 border border-surface2 rounded-xl px-4 py-2 text-xs font-bold text-text placeholder:text-overlay1 focus:outline-none focus:ring-2 focus:ring-blue/30 w-64 transition-all" + /> +
+
+
+ Real-time +
+
+
+ +
+
Process
+
PID
+
CPU %
+
Memory
+
Actions
+
+ +
+ {filteredLiveProcs.map((proc) => ( +
setPinnedPid(pinnedPid === proc.pid ? null : proc.pid)} + > +
+
+ {proc.name} +
+
{proc.pid}
+
{proc.cpu_usage.toFixed(1)}%
+
{formatBytes(proc.memory, 0)}
+
+ + +
+
+ ))} +
+
+ + )} +
+
+ ); +} diff --git a/src/components/ProcessInspector.tsx b/src/components/ProcessInspector.tsx new file mode 100644 index 0000000..ce0c6a9 --- /dev/null +++ b/src/components/ProcessInspector.tsx @@ -0,0 +1,124 @@ +import { X, AlertTriangle, CheckSquare } from 'lucide-react'; +import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'; +import { AggregatedProcess, ProfilingMode } from '../types'; +import { formatMB } from '../utils'; + +interface Props { + selectedProcess: AggregatedProcess; + mode: ProfilingMode; + onClose: () => void; +} + +export function ProcessInspector({ selectedProcess, mode, onClose }: Props) { + return ( +
+
+
+
+
+ {mode === ProfilingMode.Targeted ? `Process Inspector (PID: ${selectedProcess.pid})` : "Application Cluster Inspector"} +
+

{selectedProcess.name}

+
+ +
+ +
+
+
+
Total Avg CPU
+
+ {(mode === ProfilingMode.Targeted ? selectedProcess.inclusive_avg_cpu : selectedProcess.avg_cpu).toFixed(1)}% +
+

Combined impact of this application.

+
+
+
Total Avg RAM
+
+ {formatMB(mode === ProfilingMode.Targeted ? selectedProcess.inclusive_avg_memory_mb : selectedProcess.avg_memory_mb)} +
+

Proportional memory footprint.

+
+
+ +
+
Resource History (Summed)
+
+ + + + + + + + + + + + + + + + { + if (name === 'memory_mb') return [formatMB(value), 'RAM']; + if (name === 'cpu_usage') return [`${value.toFixed(1)}%`, 'CPU']; + return [value, name]; + }} + /> + + + + +
+
+ +
+

Profiling Details

+
+
+ Peak Recorded Load + {selectedProcess.peak_cpu.toFixed(1)}% +
+
+ Peak Recorded RAM + {formatMB(selectedProcess.peak_memory_mb)} +
+
+ + {mode === ProfilingMode.Targeted ? "Child Process Count" : "Active Instances Count"} + + + {mode === ProfilingMode.Targeted ? selectedProcess.children.length : selectedProcess.instance_count} + +
+
+
+ +
+

Profiling Insights

+
+ {selectedProcess.warnings.length > 0 ? selectedProcess.warnings.map((w, idx) => ( +
+ + {w} +
+ )) : ( +
+ + Healthy Performance Profile +
+ )} +
+
+
+
+
+ ); +} diff --git a/src/components/ReportView.tsx b/src/components/ReportView.tsx new file mode 100644 index 0000000..5935d1f --- /dev/null +++ b/src/components/ReportView.tsx @@ -0,0 +1,336 @@ +import React, { useState, useMemo, Fragment } from 'react'; +import { + Activity, Server, Play, AlertTriangle, ArrowLeft, Shield, Save, Eye, EyeOff +} from 'lucide-react'; +import { + AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid +} from 'recharts'; +import { invoke } from '@tauri-apps/api/core'; +import { AggregatedProcess, ProfilingMode, ProfilingReport } from '../types'; +import { cn, formatBytes, formatMB, formatDuration } from '../utils'; +import { ProcessInspector } from './ProcessInspector'; + +interface Props { + report: ProfilingReport; + onBack: () => void; +} + +type SortField = 'name' | 'pid' | 'inclusive_avg_cpu' | 'peak_cpu' | 'inclusive_avg_memory_mb' | 'peak_memory_mb' | 'avg_cpu' | 'avg_memory_mb'; + +export function ReportView({ report, onBack }: Props) { + 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); + const [expandedNodes, setExpandedNodes] = useState>(new Set()); + + const toggleExpand = (pid: number) => { + const newExpanded = new Set(expandedNodes); + if (newExpanded.has(pid)) newExpanded.delete(pid); + else newExpanded.add(pid); + setExpandedNodes(newExpanded); + }; + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortOrder('desc'); + } + }; + + const filteredProcesses = useMemo(() => { + return report.aggregated_processes.filter(p => !hideProfiler || !p.is_syspulse); + }, [report, hideProfiler]); + + const sortedProcesses = useMemo(() => { + return [...filteredProcesses] + .sort((a, b) => { + const valA = a[sortField as keyof AggregatedProcess]; + const valB = b[sortField as keyof AggregatedProcess]; + + if (typeof valA === 'string' && typeof valB === 'string') { + return sortOrder === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA); + } + + const numA = (valA as number) ?? 0; + const numB = (valB as number) ?? 0; + + return sortOrder === 'asc' ? numA - numB : numB - numA; + }); + }, [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(() => { + if (report.mode === ProfilingMode.Targeted) { + const root = sortedProcesses[0]; + if (root) { + return { cpu: root.inclusive_avg_cpu, mem: root.inclusive_avg_memory_mb }; + } + } + 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, report.mode]); + + const saveReport = async () => { + try { + const path = await invoke('save_report', { report }); + alert(`Report saved to: ${path}`); + } catch (e) { + alert(`Failed to save: ${e}`); + } + }; + + const renderTreeRows = (nodes: AggregatedProcess[], depth = 0): React.ReactNode => { + return nodes.map((proc) => { + const isExpanded = expandedNodes.has(proc.pid); + const hasChildren = proc.children && proc.children.length > 0; + + return ( + +
{ + e.stopPropagation(); + setSelectedProcess(proc); + }} + > +
+ {hasChildren ? ( + + ) : ( +
+ )} +
+ {proc.name} +
+ +
{proc.pid}
+ +
+ {proc.inclusive_avg_cpu.toFixed(1)}% + {proc.children.length > 0 && ({proc.avg_cpu.toFixed(1)}% self)} +
+ +
{proc.peak_cpu.toFixed(1)}%
+ +
+ {formatMB(proc.inclusive_avg_memory_mb)} + {proc.children.length > 0 && ({formatMB(proc.avg_memory_mb)} self)} +
+ +
{formatMB(proc.peak_memory_mb, 0)}
+ +
+ {proc.warnings.length > 0 ? proc.warnings.map((w, idx) => ( + + {w} + + )) : ( + proc.children.length > 0 ? {proc.children.length} units : null + )} +
+
+ {hasChildren && isExpanded && renderTreeRows( + proc.children.filter(c => !hideProfiler || !c.is_syspulse), + depth + 1 + )} + + ); + }); + }; + + return ( +
+
+
+
+ +
+ + {report.mode === ProfilingMode.Targeted ? `Target: ${report.target_name || "Process"}` : "Global Report"} + +
+
+ + + +
+
+ +
+
+
+
Session Duration
+
{formatDuration(report.duration_seconds)}
+
+
+
Impact Avg CPU
+
{summaryStats.cpu.toFixed(1)}%
+
+
+
Impact Avg RAM
+
{formatMB(summaryStats.mem)}
+
+
+
Issue Alerts
+
+ {filteredProcesses.filter(p => p.warnings.length > 0).length} +
+
+
+ +
+
+

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

+
+
+ + + + + + + + + + + + { + if (name === 'mem') return [formatBytes(value * 1024 * 1024 * 1024), 'RAM']; + if (name === 'cpu') return [`${value.toFixed(1)}%`, 'CPU']; + return [value, name]; + }} + /> + + + + +
+
+ +
+
+

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

+
+ {report.mode === ProfilingMode.Targeted && ( +
+
Inclusive Sum +
+ )} + + {report.mode === ProfilingMode.Targeted ? "Toggle Nodes to Expand" : "Grouped by Application Name"} + +
+
+ +
+
handleSort('name')}> + Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')} +
+
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(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_memory_mb' : 'avg_memory_mb')}>Avg Mem
+
handleSort('peak_memory_mb')}>Peak
+
Insights
+
+ +
+ {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 + )} +
+
+ ))} +
+ +
+ + Real Memory (PSS) used for accuracy. Global mode aggregates processes by application name for performance. +
+
+ + {selectedProcess && ( + setSelectedProcess(null)} + /> + )} +
+
+ ); +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..39b35ec --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,63 @@ +export enum ProfilingMode { + Global = "Global", + Targeted = "Targeted" +} + +export interface ProcessStats { + pid: number; + name: string; + cpu_usage: number; + memory: number; + status: string; + user_id?: string; + is_syspulse: boolean; +} + +export interface SystemStats { + cpu_usage: number[]; + total_memory: number; + used_memory: number; + processes: ProcessStats[]; + is_recording: boolean; + recording_duration: number; +} + +export interface TimelinePoint { + time: string; + cpu_total: number; + mem_total_gb: number; + cpu_profiler: number; + mem_profiler_gb: number; +} + +export interface ProcessHistoryPoint { + time: string; + cpu_usage: number; + memory_mb: number; +} + +export interface AggregatedProcess { + pid: number; + name: string; + avg_cpu: number; + peak_cpu: number; + avg_memory_mb: number; + peak_memory_mb: number; + inclusive_avg_cpu: number; + inclusive_avg_memory_mb: number; + instance_count: number; + warnings: string[]; + history: ProcessHistoryPoint[]; + is_syspulse: boolean; + children: AggregatedProcess[]; +} + +export interface ProfilingReport { + start_time: string; + end_time: string; + duration_seconds: number; + mode: ProfilingMode; + target_name?: string; + timeline: TimelinePoint[]; + aggregated_processes: AggregatedProcess[]; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..d539d39 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,25 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatBytes(bytes: number, decimals = 1) { + if (!bytes || bytes === 0) return '0 B'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; +} + +export function formatMB(mb: number, decimals = 1) { + return formatBytes(mb * 1024 * 1024, decimals); +} + +export function formatDuration(seconds: number) { + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${m}:${s.toString().padStart(2, '0')}`; +}