feat: implement collapsible tree view with inclusive resource metrics and improved memory calculation

This commit is contained in:
2026-02-22 23:33:35 +01:00
parent 6b2a851c3e
commit df45df6712

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo, Fragment } from 'react';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { import {
AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer,
@@ -45,15 +45,19 @@ interface ProcessHistoryPoint {
} }
interface AggregatedProcess { interface AggregatedProcess {
pid: number;
name: string; name: string;
avg_cpu: number; avg_cpu: number;
peak_cpu: number; peak_cpu: number;
avg_memory_mb: number; avg_memory_mb: number;
peak_memory_mb: number; peak_memory_mb: number;
inclusive_avg_cpu: number;
inclusive_avg_memory_mb: number;
instance_count: number; instance_count: number;
warnings: string[]; warnings: string[];
history: ProcessHistoryPoint[]; history: ProcessHistoryPoint[];
is_syspulse: boolean; is_syspulse: boolean;
children: AggregatedProcess[];
} }
interface ProfilingReport { interface ProfilingReport {
@@ -148,7 +152,7 @@ function App() {
); );
} }
if (!stats) return <div className="h-screen w-screen flex items-center justify-center text-text bg-base font-black italic tracking-tighter uppercase text-2xl">Initializing SysPulse...</div>; if (!stats) return <div className="h-screen w-screen flex items-center justify-center text-text bg-base font-black italic tracking-tighter uppercase text-2xl animate-pulse">Initializing SysPulse...</div>;
const avgCpu = stats.cpu_usage.reduce((a, b) => a + b, 0) / stats.cpu_usage.length; 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 memoryPercent = (stats.used_memory / stats.total_memory) * 100;
@@ -261,7 +265,7 @@ function App() {
<span className="text-subtext0 font-black mb-2">Processes</span> <span className="text-subtext0 font-black mb-2">Processes</span>
</div> </div>
<div className="mt-4 text-xs text-subtext1 font-medium leading-relaxed bg-base/30 p-3 rounded-xl border border-surface1"> <div className="mt-4 text-xs text-subtext1 font-medium leading-relaxed bg-base/30 p-3 rounded-xl border border-surface1">
Automatic aggregation is active. Child processes are merged for cleaner profiling. Live system feed. Profiling session will provide a full hierarchical tree.
</div> </div>
</div> </div>
</div> </div>
@@ -296,7 +300,7 @@ function App() {
)} /> )} />
{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 tabular-nums">{proc.pid}</div>
<div className="text-blue font-black text-sm text-right self-center tabular-nums">{proc.cpu_usage.toFixed(1)}%</div> <div className="text-blue font-black text-sm text-right self-center tabular-nums">{proc.cpu_usage.toFixed(1)}%</div>
<div className="text-mauve font-black text-sm text-right self-center tabular-nums">{(proc.memory / 1024 / 1024).toFixed(0)} MB</div> <div className="text-mauve font-black text-sm text-right self-center tabular-nums">{(proc.memory / 1024 / 1024).toFixed(0)} MB</div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
@@ -319,13 +323,21 @@ function App() {
); );
} }
type SortField = 'name' | 'avg_cpu' | 'peak_cpu' | 'avg_memory_mb' | 'peak_memory_mb' | 'instance_count'; type SortField = 'name' | 'pid' | 'inclusive_avg_cpu' | 'peak_cpu' | 'inclusive_avg_memory_mb' | 'peak_memory_mb';
function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () => void }) { function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () => void }) {
const [sortField, setSortField] = useState<SortField>('avg_cpu'); const [sortField, setSortField] = useState<SortField>('inclusive_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 [hideProfiler, setHideProfiler] = useState(true);
const [expandedNodes, setExpandedNodes] = useState<Set<number>>(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) => { const handleSort = (field: SortField) => {
if (sortField === field) { if (sortField === field) {
@@ -363,6 +375,73 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
} }
}; };
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 (
<Fragment key={proc.pid}>
<div
className="grid grid-cols-[1fr_80px_100px_100px_100px_100px_200px] gap-4 px-4 py-3 border-b border-surface1/20 hover:bg-surface1/20 cursor-pointer transition-all rounded-xl group relative"
onClick={(e) => {
e.stopPropagation();
setSelectedProcess(proc);
}}
>
<div className="font-black text-text truncate text-sm flex items-center gap-2 group-hover:text-blue transition-colors" style={{ paddingLeft: depth * 20 }}>
{hasChildren ? (
<button
onClick={(e) => {
e.stopPropagation();
toggleExpand(proc.pid);
}}
className="p-1 hover:bg-surface2 rounded transition-colors"
>
<Play size={10} className={cn("transition-transform duration-200 fill-current", isExpanded ? "rotate-90" : "")} />
</button>
) : (
<div className="w-4" />
)}
<div className={cn(
"w-2 h-2 rounded-full shrink-0",
proc.is_syspulse ? "bg-mauve" : "bg-surface2 group-hover:bg-blue"
)} />
<span title={proc.name}>{proc.name}</span>
</div>
<div className="text-overlay2 font-mono text-xs text-right self-center tabular-nums">{proc.pid}</div>
<div className="text-blue font-black text-sm text-right self-center tabular-nums">
{proc.inclusive_avg_cpu.toFixed(1)}%
{proc.children.length > 0 && <span className="block text-[9px] text-overlay1 font-normal">({proc.avg_cpu.toFixed(1)}% self)</span>}
</div>
<div className="text-subtext0 text-[11px] font-bold text-right self-center tabular-nums">{proc.peak_cpu.toFixed(1)}%</div>
<div className="text-mauve font-black text-sm text-right self-center tabular-nums">
{proc.inclusive_avg_memory_mb.toFixed(0)}MB
{proc.children.length > 0 && <span className="block text-[9px] text-overlay1 font-normal">({proc.avg_memory_mb.toFixed(0)}MB self)</span>}
</div>
<div className="text-subtext0 text-[11px] font-bold text-right self-center tabular-nums">{proc.peak_memory_mb.toFixed(0)}MB</div>
<div className="flex flex-wrap gap-1.5 pl-4 items-center">
{proc.warnings.length > 0 ? proc.warnings.map((w, idx) => (
<span key={idx} className="bg-red/10 text-red text-[9px] font-black px-2 py-1 rounded-lg border border-red/20 flex items-center gap-1 uppercase tracking-tighter">
<AlertTriangle size={8} strokeWidth={3} /> {w}
</span>
)) : (
proc.children.length > 0 ? <span className="text-[10px] text-overlay1 font-bold uppercase tracking-widest">{proc.children.length} units</span> : null
)}
</div>
</div>
{hasChildren && isExpanded && renderTreeRows(proc.children, depth + 1)}
</Fragment>
);
});
};
return ( return (
<div className="flex flex-col h-screen bg-base text-text"> <div className="flex flex-col h-screen bg-base text-text">
<div className="titlebar shrink-0"> <div className="titlebar shrink-0">
@@ -410,7 +489,7 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="text-xl font-bold text-text tabular-nums">{new Date(report.end_time).toLocaleTimeString()}</div> <div className="text-xl font-bold text-text tabular-nums">{new Date(report.end_time).toLocaleTimeString()}</div>
</div> </div>
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-green/30 transition-colors"> <div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-green/30 transition-colors">
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Unique Processes</div> <div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Root Processes</div>
<div className="text-2xl font-black text-blue tabular-nums">{report.aggregated_processes.length}</div> <div className="text-2xl font-black text-blue tabular-nums">{report.aggregated_processes.length}</div>
</div> </div>
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-red/30 transition-colors"> <div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-red/30 transition-colors">
@@ -453,50 +532,35 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="card flex-1 flex flex-col min-h-0 border border-surface1/50 shadow-[0_35px_60px_-15px_rgba(0,0,0,0.5)] overflow-hidden"> <div className="card flex-1 flex flex-col min-h-0 border border-surface1/50 shadow-[0_35px_60px_-15px_rgba(0,0,0,0.5)] overflow-hidden">
<div className="flex justify-between items-center mb-6 px-2"> <div className="flex justify-between items-center mb-6 px-2">
<h3 className="text-xl font-black tracking-tighter uppercase italic flex items-center gap-2"> <h3 className="text-xl font-black tracking-tighter uppercase italic flex items-center gap-2">
<Server size={24} className="text-green" strokeWidth={3} /> Analysis Matrix <Server size={24} className="text-green" strokeWidth={3} /> Hierarchical Matrix
</h3> </h3>
<span className="text-[10px] bg-surface1 px-3 py-1.5 rounded-full font-black text-overlay1 uppercase tracking-widest">Select Process to Inspect</span> <div className="flex items-center gap-4">
<div className="flex items-center gap-1.5 text-[9px] font-bold text-overlay1 uppercase">
<div className="w-2 h-2 rounded-full bg-blue/40" /> Inclusive Sum
</div>
<span className="text-[10px] bg-surface1 px-3 py-1.5 rounded-full font-black text-overlay1 uppercase tracking-widest">Toggle Nodes to Expand</span>
</div>
</div> </div>
<div className="grid grid-cols-[1fr_80px_100px_100px_100px_100px_200px] gap-4 px-6 py-4 bg-surface1/50 rounded-2xl font-black uppercase tracking-widest text-[10px] text-overlay1 mb-2"> <div className="grid grid-cols-[1fr_80px_100px_100px_100px_100px_200px] gap-4 px-6 py-4 bg-surface1/50 rounded-2xl font-black uppercase tracking-widest text-[10px] text-overlay1 mb-2">
<div className="cursor-pointer hover:text-text transition-colors flex items-center gap-1" onClick={() => handleSort('name')}> <div className="cursor-pointer hover:text-text transition-colors flex items-center gap-1" onClick={() => handleSort('name')}>
Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')} Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
</div> </div>
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('instance_count')}>Units</div> <div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('pid')}>PID</div>
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('avg_cpu')}>Avg CPU</div> <div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('inclusive_avg_cpu')}>Total CPU</div>
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('peak_cpu')}>Peak CPU</div> <div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('peak_cpu')}>Peak</div>
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('avg_memory_mb')}>Avg Mem</div> <div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('inclusive_avg_memory_mb')}>Total Mem</div>
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('peak_memory_mb')}>Peak Mem</div> <div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('peak_memory_mb')}>Peak</div>
<div className="pl-4">Insights</div> <div className="pl-4">Insights</div>
</div> </div>
<div className="overflow-y-auto flex-1 custom-scrollbar px-2"> <div className="overflow-y-auto flex-1 custom-scrollbar px-2">
{sortedProcesses.map((proc, i) => ( {renderTreeRows(sortedProcesses)}
<div </div>
key={i}
onClick={() => setSelectedProcess(proc)} <div className="p-4 bg-surface0/50 border-t border-surface1 text-[9px] text-overlay1 font-medium italic flex items-center gap-2">
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" <AlertTriangle size={10} />
> <span>Memory is Resident Set Size (RSS). Summed totals may exceed physical RAM due to shared segments.</span>
<div className="font-black text-text truncate text-sm flex items-center gap-3 group-hover:text-blue transition-colors" title={proc.name}>
<div className="w-2 h-2 rounded-full bg-surface2 group-hover:bg-blue transition-colors shrink-0" />
{proc.name}
</div>
<div className="text-overlay2 font-mono text-xs text-right self-center tabular-nums">{proc.instance_count}</div>
<div className="text-blue font-black text-sm text-right self-center tabular-nums">{proc.avg_cpu.toFixed(1)}%</div>
<div className="text-subtext0 text-[11px] font-bold text-right self-center tabular-nums">{proc.peak_cpu.toFixed(1)}%</div>
<div className="text-mauve font-black text-sm text-right self-center tabular-nums">{proc.avg_memory_mb.toFixed(0)}MB</div>
<div className="text-subtext0 text-[11px] font-bold text-right self-center tabular-nums">{proc.peak_memory_mb.toFixed(0)}MB</div>
<div className="flex flex-wrap gap-1.5 pl-4 items-center">
{proc.warnings.length > 0 ? proc.warnings.map((w, idx) => (
<span key={idx} className="bg-red/10 text-red text-[9px] font-black px-2 py-1 rounded-lg border border-red/20 flex items-center gap-1 uppercase tracking-tighter">
<AlertTriangle size={8} strokeWidth={3} /> {w}
</span>
)) : (
<span className="text-[10px] text-green/40 font-black uppercase italic tracking-tighter">Healthy</span>
)}
</div>
</div>
))}
</div> </div>
</div> </div>
@@ -506,7 +570,7 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="w-full max-w-2xl h-full bg-mantle rounded-3xl border border-surface1 shadow-3xl flex flex-col overflow-hidden"> <div className="w-full max-w-2xl h-full bg-mantle rounded-3xl border border-surface1 shadow-3xl flex flex-col overflow-hidden">
<div className="p-8 border-b border-surface1 flex justify-between items-start shrink-0"> <div className="p-8 border-b border-surface1 flex justify-between items-start shrink-0">
<div> <div>
<div className="text-xs font-black text-blue uppercase tracking-widest mb-1">Process Inspector</div> <div className="text-xs font-black text-blue uppercase tracking-widest mb-1">Process Inspector (PID: {selectedProcess.pid})</div>
<h2 className="text-3xl font-black text-text tracking-tighter uppercase italic truncate max-w-md">{selectedProcess.name}</h2> <h2 className="text-3xl font-black text-text tracking-tighter uppercase italic truncate max-w-md">{selectedProcess.name}</h2>
</div> </div>
<button <button
@@ -520,19 +584,19 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="flex-1 overflow-y-auto p-8 flex flex-col gap-8 custom-scrollbar"> <div className="flex-1 overflow-y-auto p-8 flex flex-col gap-8 custom-scrollbar">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg"> <div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg">
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-2">Instances Detected</div> <div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-2">Total Avg CPU</div>
<div className="text-4xl font-black text-text">{selectedProcess.instance_count}</div> <div className="text-4xl font-black text-text tabular-nums">{selectedProcess.inclusive_avg_cpu.toFixed(1)}%</div>
<p className="text-xs text-subtext1 mt-2">Maximum concurrent processes seen with this name.</p> <p className="text-xs text-subtext1 mt-2">Sum of this process and all its children.</p>
</div> </div>
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg"> <div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg">
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-2">Average Impact</div> <div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-2">Total Avg RAM</div>
<div className="text-4xl font-black text-blue">{selectedProcess.avg_cpu.toFixed(1)}%</div> <div className="text-4xl font-black text-mauve tabular-nums">{selectedProcess.inclusive_avg_memory_mb.toFixed(0)}MB</div>
<p className="text-xs text-subtext1 mt-2">Mean CPU usage across the entire session.</p> <p className="text-xs text-subtext1 mt-2">Combined RSS memory of the subtree.</p>
</div> </div>
</div> </div>
<div className="card h-64 shrink-0 bg-base border border-surface1"> <div className="card h-64 shrink-0 bg-base border border-surface1">
<div className="card-title mb-4">Resource History (Summed)</div> <div className="card-title mb-4">Self-Resource History (Exclusive)</div>
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={selectedProcess.history}> <AreaChart data={selectedProcess.history}>
@@ -552,13 +616,31 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<Tooltip <Tooltip
contentStyle={{ background: 'var(--crust)', border: '1px solid var(--surface1)', borderRadius: '12px' }} contentStyle={{ background: 'var(--crust)', border: '1px solid var(--surface1)', borderRadius: '12px' }}
/> />
<Area type="monotone" dataKey="cpu_usage" name="Total CPU %" stroke="var(--blue)" fill="url(#procCpuGrad)" strokeWidth={3} /> <Area type="monotone" dataKey="cpu_usage" name="Self CPU %" stroke="var(--blue)" fill="url(#procCpuGrad)" strokeWidth={3} />
<Area type="monotone" dataKey="memory_mb" name="Total Mem (MB)" stroke="var(--mauve)" fill="url(#procMemGrad)" strokeWidth={2} /> <Area type="monotone" dataKey="memory_mb" name="Self Mem (MB)" stroke="var(--mauve)" fill="url(#procMemGrad)" strokeWidth={2} />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</div> </div>
<div className="space-y-4">
<h4 className="text-xs font-black text-overlay2 uppercase tracking-widest">Process Details</h4>
<div className="bg-surface0 border border-surface1 rounded-2xl p-6 space-y-3">
<div className="flex justify-between text-sm">
<span className="text-subtext1">Peak Self CPU</span>
<span className="font-black text-blue">{selectedProcess.peak_cpu.toFixed(1)}%</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-subtext1">Peak Self RAM</span>
<span className="font-black text-mauve">{selectedProcess.peak_memory_mb.toFixed(0)}MB</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-subtext1">Child Process Count</span>
<span className="font-black text-green">{selectedProcess.children.length}</span>
</div>
</div>
</div>
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-xs font-black text-overlay2 uppercase tracking-widest">Profiling Insights</h4> <h4 className="text-xs font-black text-overlay2 uppercase tracking-widest">Profiling Insights</h4>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">