From d9f4c74b9fb644f942d99076990945f8c8f4b5a3 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 23 Feb 2026 20:28:45 +0100 Subject: [PATCH] feat: complete rewrite for accurate PSS memory and smart user-focused hierarchy --- build_log.txt | 15 +++ src/App.tsx | 20 ++-- src/components/Dashboard.tsx | 151 +++++++++++++++++------------ src/components/ReportView.tsx | 172 ++++++++++++++++++++++++++++++++++ src/types/index.ts | 32 ++++++- 5 files changed, 315 insertions(+), 75 deletions(-) create mode 100644 build_log.txt create mode 100644 src/components/ReportView.tsx diff --git a/build_log.txt b/build_log.txt new file mode 100644 index 0000000..fff8461 --- /dev/null +++ b/build_log.txt @@ -0,0 +1,15 @@ + +> syspulse@0.1.0 build +> tsc && vite build + +vite v6.4.1 building for production... +transforming... +✓ 2352 modules transformed. +rendering chunks... +computing gzip size... +dist/index.html 0.55 kB │ gzip: 0.31 kB +dist/assets/index-C5k9P4b0.css 23.62 kB │ gzip: 5.27 kB +dist/assets/index--56uGBza.js 16.97 kB │ gzip: 4.60 kB +dist/assets/charts-vendor-CdA6MGxy.js 310.46 kB │ gzip: 79.84 kB +dist/assets/vendor-B6fi2Fb1.js 313.23 kB │ gzip: 99.70 kB +✓ built in 8.00s diff --git a/src/App.tsx b/src/App.tsx index dfd7093..17db3aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,12 @@ import { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; -import { GlobalStats } from './types'; +import { GlobalStats, ProfilingReport } from './types'; import { Dashboard } from './components/Dashboard'; +import { ReportView } from './components/ReportView'; function App() { const [stats, setStats] = useState(null); + const [report, setReport] = useState(null); const [history, setHistory] = useState<{ time: string; cpu: number }[]>([]); useEffect(() => { @@ -14,9 +16,7 @@ function App() { const fetchStats = async () => { try { const data = await invoke('get_latest_stats'); - if (!isMounted) return; - setStats(data); setHistory(prev => { @@ -25,7 +25,7 @@ function App() { return newHistory; }); } catch (e) { - console.error('Failed to fetch stats:', e); + console.error(e); } finally { if (isMounted) { timeoutId = window.setTimeout(fetchStats, 1000); @@ -42,17 +42,19 @@ function App() { if (!stats) return (
- Initializing SysPulse -
-
-
+ Initializing SysPulse...
); + if (report) { + return setReport(null)} />; + } + return ( setReport(rep)} /> ); } diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index e7a8529..466c0a3 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,19 +1,22 @@ -import { useState, useMemo, Fragment } from 'react'; +import { useState, Fragment } from 'react'; import { - Activity, Cpu, Server, Database, Shield, ChevronRight, ChevronDown, Search + Activity, Cpu, Server, Database, Target, Play, Square, ChevronRight, ChevronDown, Search, Download } from 'lucide-react'; import { AreaChart, Area, ResponsiveContainer } from 'recharts'; -import { GlobalStats, ProcessNode } from '../types'; +import { invoke } from '@tauri-apps/api/core'; +import { GlobalStats, ProcessNode, ProfilingReport } from '../types'; import { cn, formatBytes } from '../utils'; interface Props { stats: GlobalStats; history: { time: string; cpu: number }[]; + onShowReport: (report: ProfilingReport) => void; } -export function Dashboard({ stats, history }: Props) { +export function Dashboard({ stats, history, onShowReport }: Props) { const [searchTerm, setSearchBar] = useState(""); const [expandedPids, setExpandedPids] = useState>(new Set()); + const [pinnedPid, setPinnedPid] = useState(null); const memoryPercent = (Number(stats.mem_used) / Number(stats.mem_total)) * 100; @@ -24,43 +27,54 @@ export function Dashboard({ stats, history }: Props) { setExpandedPids(next); }; - // Flatten the tree for the list view if searching, or keep it hierarchical - const filteredTree = useMemo(() => { - if (!searchTerm) return stats.process_tree; - - const search = searchTerm.toLowerCase(); - const result: ProcessNode[] = []; - - function searchRecursive(nodes: ProcessNode[]) { - for (const node of nodes) { - if (node.name.toLowerCase().includes(search) || node.pid.toString().includes(search)) { - result.push(node); - } - searchRecursive(node.children); - } + const toggleRecording = async () => { + if (stats.is_profiling) { + const report = await invoke('stop_profiling'); + if (report) onShowReport(report); + } else { + await invoke('start_profiling', { target_pid: pinnedPid }); } - - searchRecursive(stats.process_tree); - return result; - }, [stats.process_tree, searchTerm]); + }; + + 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; + onShowReport(data); + } catch (err) { + alert('Failed to parse report'); + } + }; + reader.readAsText(file); + }; const renderProcessNode = (node: ProcessNode, depth = 0) => { const isExpanded = expandedPids.has(node.pid); const hasChildren = node.children.length > 0; - const totalCpu = node.cpu_self + node.cpu_children; - const totalMem = node.mem_rss + node.mem_children; + const isPinned = pinnedPid === node.pid; + + if (searchTerm && !node.name.toLowerCase().includes(searchTerm.toLowerCase()) && !node.pid.toString().includes(searchTerm)) { + // Simple search for now: if node doesn't match, search children + if (!node.children.some(c => c.name.toLowerCase().includes(searchTerm.toLowerCase()))) { + return null; + } + } return (
0 && "bg-base/10" + "grid grid-cols-[1fr_80px_100px_120px_100px] gap-4 px-4 py-3 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl items-center cursor-pointer", + isPinned && "bg-blue/10 border-blue/30" )} + onClick={() => setPinnedPid(isPinned ? null : node.pid)} >
- {hasChildren && !searchTerm ? ( - ) :
} @@ -68,20 +82,21 @@ export function Dashboard({ stats, history }: Props) {
{node.pid}
- {totalCpu.toFixed(1)}% - {depth === 0 && node.cpu_children > 0 && Self: {node.cpu_self.toFixed(1)}%} + {node.cpu_inclusive.toFixed(1)}%
- {formatBytes(totalMem)} - {depth === 0 && node.mem_children > 0 && Self: {formatBytes(node.mem_rss)}} + {formatBytes(node.mem_pss_inclusive)}
-
-
- {hasChildren && isExpanded && !searchTerm && node.children.map(child => renderProcessNode(child, depth + 1))} + {hasChildren && isExpanded && node.children.map(child => renderProcessNode(child, depth + 1))} ); }; @@ -96,10 +111,24 @@ export function Dashboard({ stats, history }: Props) { SysPulse
-
-
- Live Monitor -
+ +
+
@@ -127,7 +156,7 @@ export function Dashboard({ stats, history }: Props) {
-
Memory
+
Real Memory (PSS)
{formatBytes(stats.mem_used)} used @@ -144,13 +173,13 @@ export function Dashboard({ stats, history }: Props) {
-
Process Stack
+
Smart View
- {stats.process_count} - Active + {stats.smart_tree.length} + Entry points
- Deep hierarchical tracking enabled. Child process overhead is summed into parent totals. + Top-level processes are grouped logically. Shared memory is distributed fairly via PSS.
@@ -159,32 +188,30 @@ export function Dashboard({ stats, history }: Props) {

- System Hierarchy + Process Hierarchy

-
-
- - setSearchBar(e.target.value)} - className="bg-surface1/50 border border-surface2 rounded-xl pl-9 pr-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" - /> -
+
+ + setSearchBar(e.target.value)} + className="bg-surface1/50 border border-surface2 rounded-xl pl-9 pr-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" + />
-
+
Hierarchy
PID
-
Total CPU
-
Total RAM
-
Action
+
Inclusive CPU
+
Inclusive PSS
+
Target
- {filteredTree.map(node => renderProcessNode(node))} + {stats.smart_tree.map(node => renderProcessNode(node))}
diff --git a/src/components/ReportView.tsx b/src/components/ReportView.tsx new file mode 100644 index 0000000..f91fbdd --- /dev/null +++ b/src/components/ReportView.tsx @@ -0,0 +1,172 @@ +import { useState, Fragment } from 'react'; +import { + Activity, Server, ArrowLeft, ChevronRight, ChevronDown, Save, FileJson, Clock, Target, AlertTriangle +} from 'lucide-react'; +import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'; +import { invoke } from '@tauri-apps/api/core'; +import { ProcessNode, ProfilingReport } from '../types'; +import { formatBytes, formatDuration } from '../utils'; + +interface Props { + report: ProfilingReport; + onBack: () => void; +} + +export function ReportView({ report, onBack }: Props) { + const [expandedPids, setExpandedPids] = useState>(new Set()); + + const toggleExpand = (pid: number) => { + const next = new Set(expandedPids); + if (next.has(pid)) next.delete(pid); + else next.add(pid); + setExpandedPids(next); + }; + + const saveReport = async () => { + try { + const path = await invoke('save_report', { report }); + alert(`Report saved to: ${path}`); + } catch (e) { + alert(`Failed to save: ${e}`); + } + }; + + const renderProcessNode = (node: ProcessNode, depth = 0) => { + const isExpanded = expandedPids.has(node.pid); + const hasChildren = node.children.length > 0; + + return ( + +
+
+ {hasChildren ? ( + + ) :
} + {node.name} +
+
{node.pid}
+
+ {node.cpu_inclusive.toFixed(1)}% + {hasChildren && Self: {node.cpu_self.toFixed(1)}%} +
+
+ {formatBytes(node.mem_pss_inclusive)} + {hasChildren && Self: {formatBytes(node.mem_pss_self)}} +
+
+ {node.cpu_inclusive > 50 && } +
+
+ {hasChildren && isExpanded && node.children.map(child => renderProcessNode(child, depth + 1))} + + ); + }; + + return ( +
+
+
+
+ +
+ + Profiling Report + +
+
+ + +
+
+ +
+
+
+
Duration
+
{formatDuration(report.duration_seconds)}
+
+
+
Target
+
{report.target_pid ? `PID ${report.target_pid}` : "Whole System"}
+
+
+
Nodes
+
{report.aggregated_tree.length}
+
+
+
Snapshots
+
{report.snapshots.length}
+
+
+ +
+
+

+ Load Over Time +

+
+
+ + + + + + + + + + + + { + if (name === 'mem_used') return [formatBytes(value), 'System RAM']; + if (name === 'cpu_usage') return [`${value.toFixed(1)}%`, 'System CPU']; + return [value, name]; + }} + /> + + + +
+
+ +
+
+

+ Hierarchical Analysis +

+ Averaged across session +
+ +
+
Hierarchy
+
PID
+
Avg CPU
+
Avg PSS
+
Alerts
+
+ +
+ {report.aggregated_tree.map(node => renderProcessNode(node))} +
+
+
+
+ ); +} diff --git a/src/types/index.ts b/src/types/index.ts index 5d16194..e6ecfb4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,9 +2,9 @@ export interface ProcessNode { pid: number; name: string; cpu_self: number; - cpu_children: number; - mem_rss: number; - mem_children: number; + cpu_inclusive: number; + mem_pss_self: number; + mem_pss_inclusive: number; children: ProcessNode[]; } @@ -12,6 +12,30 @@ export interface GlobalStats { cpu_total: number; mem_used: number; mem_total: number; - process_tree: ProcessNode[]; + smart_tree: ProcessNode[]; process_count: number; + is_profiling: boolean; +} + +export interface ProfilingReport { + start_time: string; + end_time: string; + duration_seconds: number; + snapshots: ReportSnapshot[]; + target_pid?: number; + aggregated_tree: ProcessNode[]; +} + +export interface ReportSnapshot { + timestamp: string; + cpu_usage: number; + mem_used: number; + top_processes: ProcessSnapshot[]; +} + +export interface ProcessSnapshot { + pid: number; + name: string; + cpu: number; + pss: number; }