refactor: modularize backend and frontend for better maintainability
This commit is contained in:
769
src/App.tsx
769
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<SystemStats | null>(null);
|
||||
const [history, setHistory] = useState<{ time: string; cpu: number }[]>([]);
|
||||
const [report, setReport] = useState<ProfilingReport | null>(null);
|
||||
const [searchTerm, setSearchBar] = useState("");
|
||||
const [pinnedPid, setPinnedPid] = useState<number | null>(null);
|
||||
|
||||
// Initial report check
|
||||
useEffect(() => {
|
||||
@@ -163,46 +71,9 @@ function App() {
|
||||
const reportData = await invoke<ProfilingReport>('stop_profiling');
|
||||
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<HTMLInputElement>) => {
|
||||
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}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (view === 'report' && report) {
|
||||
@@ -216,635 +87,17 @@ 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 animate-pulse">Initializing SysPulse...</div>;
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col h-screen bg-base text-text">
|
||||
<div data-tauri-drag-region className="titlebar shrink-0">
|
||||
<div className="titlebar-drag gap-2 px-4" data-tauri-drag-region>
|
||||
<div className="w-7 h-7 rounded-xl bg-gradient-to-br from-mauve via-blue to-teal flex items-center justify-center shadow-xl shadow-mauve/20 border border-white/10">
|
||||
<Activity size={16} className="text-base" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="font-black tracking-tighter uppercase italic text-xl">SysPulse</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 px-4">
|
||||
<label className="flex items-center gap-2 text-subtext0 hover:text-text transition-colors text-[10px] font-black uppercase tracking-widest cursor-pointer group">
|
||||
<Download size={14} className="text-blue group-hover:scale-110 transition-transform" />
|
||||
<span>Import Run</span>
|
||||
<input type="file" accept=".json" onChange={handleImport} className="hidden" />
|
||||
</label>
|
||||
<div className="h-4 w-px bg-surface1 mx-1" />
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-xl",
|
||||
stats.is_recording
|
||||
? "bg-red text-base animate-pulse ring-4 ring-red/20"
|
||||
: "bg-green text-base hover:scale-105 active:scale-95 hover:shadow-green/20"
|
||||
)}
|
||||
onClick={toggleRecording}
|
||||
>
|
||||
{stats.is_recording ? <Square size={12} fill="currentColor" /> : <Play size={12} fill="currentColor" />}
|
||||
{stats.is_recording ? `STOP (${formatDuration(stats.recording_duration)})` : (pinnedPid ? "PROFILE TARGET" : "START GLOBAL")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="flex-1 overflow-hidden flex flex-col p-6 gap-6">
|
||||
{stats.is_recording ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-8 animate-in fade-in zoom-in duration-500">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-red/20 blur-3xl rounded-full scale-150 animate-pulse" />
|
||||
<div className="relative bg-surface0 p-12 rounded-[2.5rem] border-4 border-red/30 shadow-2xl overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-red/10 to-transparent" />
|
||||
<Activity size={64} className="text-red animate-pulse relative z-10" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-4xl font-black text-text tracking-tighter uppercase italic drop-shadow-lg">Profiling Active</h2>
|
||||
<p className="text-red font-mono text-2xl font-black tabular-nums">{formatDuration(stats.recording_duration)}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 w-full max-w-md">
|
||||
<div className="bg-surface0 p-6 rounded-[1.5rem] border border-surface1 shadow-xl text-center">
|
||||
<div className="text-[10px] font-black text-overlay2 mb-2 uppercase tracking-widest">Global CPU</div>
|
||||
<div className="text-3xl font-black text-blue tabular-nums">{avgCpu.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="bg-surface0 p-6 rounded-[1.5rem] border border-surface1 shadow-xl text-center">
|
||||
<div className="text-[10px] font-black text-overlay2 mb-2 uppercase tracking-widest">Global RAM</div>
|
||||
<div className="text-3xl font-black text-mauve tabular-nums">{memoryPercent.toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-overlay1 text-sm font-bold bg-surface0/50 px-4 py-2 rounded-full border border-surface1">
|
||||
<Shield size={14} className="text-green" />
|
||||
<span>Performance optimized snapshot mode enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 shrink-0">
|
||||
<div className="card group bg-gradient-to-b from-surface0 to-base/50">
|
||||
<div className="card-title"><Cpu size={16} className="text-blue" strokeWidth={3} /> CPU Load</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="stat-value text-blue tabular-nums">{avgCpu.toFixed(1)}</span>
|
||||
<span className="text-subtext0 font-black mb-2">%</span>
|
||||
</div>
|
||||
<div className="h-16 mt-4">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={history}>
|
||||
<defs>
|
||||
<linearGradient id="cpuDash" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--blue)" stopOpacity={0.5}/>
|
||||
<stop offset="95%" stopColor="var(--blue)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area type="monotone" dataKey="cpu" stroke="var(--blue)" fill="url(#cpuDash)" strokeWidth={3} isAnimationActive={false} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-gradient-to-b from-surface0 to-base/50">
|
||||
<div className="card-title"><Database size={16} className="text-mauve" strokeWidth={3} /> Memory</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="stat-value text-mauve tabular-nums">{formatBytes(stats.used_memory)}</span>
|
||||
<span className="text-subtext0 font-black mb-2">used</span>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-between text-[10px] font-black text-overlay2 uppercase mb-2">
|
||||
<span>{memoryPercent.toFixed(1)}% Utilized</span>
|
||||
<span>{formatBytes(stats.total_memory, 0)} Total</span>
|
||||
</div>
|
||||
<div className="h-3 bg-surface1/50 rounded-full overflow-hidden border border-surface2">
|
||||
<div className="h-full bg-gradient-to-r from-mauve to-pink transition-all duration-700 shadow-lg shadow-mauve/20" style={{ width: `${memoryPercent}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-gradient-to-b from-surface0 to-base/50">
|
||||
<div className="card-title"><Server size={16} className="text-green" strokeWidth={3} /> Tasks</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="stat-value text-green tabular-nums">{stats.processes.length}</span>
|
||||
<span className="text-subtext0 font-black mb-2">Processes</span>
|
||||
</div>
|
||||
<div className="mt-4 text-xs text-subtext1 font-medium leading-relaxed bg-base/30 p-3 rounded-xl border border-surface1">
|
||||
Live system feed. Target a process to profile its hierarchy in detail.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card flex-1 flex flex-col min-h-0 overflow-hidden shadow-[0_35px_60px_-15px_rgba(0,0,0,0.5)] border border-surface1/50">
|
||||
<div className="flex justify-between items-center mb-6 px-2 shrink-0">
|
||||
<h3 className="text-xl font-black tracking-tighter uppercase italic flex items-center gap-2">
|
||||
<Activity size={24} className="text-red" strokeWidth={3} />
|
||||
Live Feed
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="SEARCH PROCESS..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[10px] bg-red/10 text-red border border-red/20 px-3 py-1.5 rounded-full font-black uppercase tracking-widest flex items-center gap-1.5">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-red animate-pulse" />
|
||||
Real-time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_80px_100px_120px_120px] gap-4 px-6 py-4 bg-surface1/50 rounded-2xl font-black uppercase tracking-widest text-[10px] text-overlay1 mb-2">
|
||||
<div>Process</div>
|
||||
<div className="text-right">PID</div>
|
||||
<div className="text-right">CPU %</div>
|
||||
<div className="text-right">Memory</div>
|
||||
<div className="text-center">Actions</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto flex-1 custom-scrollbar px-2">
|
||||
{filteredLiveProcs.map((proc) => (
|
||||
<div
|
||||
key={proc.pid}
|
||||
className={cn(
|
||||
"grid grid-cols-[1fr_80px_100px_120px_120px] gap-4 px-4 py-4 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl cursor-pointer",
|
||||
pinnedPid === proc.pid ? "bg-blue/10 border-blue/30" : ""
|
||||
)}
|
||||
onClick={() => setPinnedPid(pinnedPid === proc.pid ? null : proc.pid)}
|
||||
>
|
||||
<div className="font-black text-text truncate text-sm flex items-center gap-3" title={proc.name}>
|
||||
<div className={cn(
|
||||
"w-2 h-2 rounded-full transition-colors",
|
||||
proc.is_syspulse ? "bg-mauve" : (pinnedPid === proc.pid ? "bg-blue" : "bg-surface2 group-hover:bg-blue")
|
||||
)} />
|
||||
{proc.name}
|
||||
</div>
|
||||
<div className="font-mono text-overlay2 text-xs text-right self-center">{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-mauve font-black text-sm text-right self-center tabular-nums">{formatBytes(proc.memory, 0)}</div>
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); startTargeted(proc.pid); }}
|
||||
className="opacity-0 group-hover:opacity-100 p-2 hover:bg-blue/20 text-blue rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
title="Target Profile"
|
||||
>
|
||||
<Target size={16} strokeWidth={2.5} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); killProcess(proc.pid); }}
|
||||
className="opacity-0 group-hover:opacity-100 p-2 hover:bg-red/20 text-red rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
title="Terminate"
|
||||
>
|
||||
<Shield size={16} strokeWidth={2.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<SortField>(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_cpu' : 'avg_cpu');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
const [selectedProcess, setSelectedProcess] = useState<AggregatedProcess | null>(null);
|
||||
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) => {
|
||||
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<string>('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 (
|
||||
<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">
|
||||
{formatMB(proc.inclusive_avg_memory_mb)}
|
||||
{proc.children.length > 0 && <span className="block text-[9px] text-overlay1 font-normal">({formatMB(proc.avg_memory_mb)} self)</span>}
|
||||
</div>
|
||||
|
||||
<div className="text-subtext0 text-[11px] font-bold text-right self-center tabular-nums">{formatMB(proc.peak_memory_mb, 0)}</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.filter(c => !hideProfiler || !c.is_syspulse),
|
||||
depth + 1
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-base text-text">
|
||||
<div className="titlebar shrink-0">
|
||||
<div className="titlebar-drag gap-2 px-4">
|
||||
<div className="w-7 h-7 rounded-xl bg-gradient-to-br from-mauve via-blue to-teal flex items-center justify-center shadow-xl shadow-mauve/20 border border-white/10">
|
||||
<Activity size={16} className="text-base" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="font-black tracking-tighter uppercase italic text-xl">
|
||||
{report.mode === ProfilingMode.Targeted ? `Target: ${report.target_name || "Process"}` : "Global Report"}
|
||||
</span>
|
||||
</div>
|
||||
<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"
|
||||
)}
|
||||
>
|
||||
{hideProfiler ? <EyeOff size={14} /> : <Eye size={14} />} {hideProfiler ? "SHOW PROFILER" : "HIDE PROFILER"}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Save size={14} className="text-blue" /> SAVE JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-mauve/10 hover:bg-mauve/20 border border-mauve/30 rounded-xl text-xs font-black transition-all text-mauve uppercase tracking-widest"
|
||||
>
|
||||
<ArrowLeft size={14} /> DASHBOARD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-6 flex flex-col gap-6 custom-scrollbar relative">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 shrink-0">
|
||||
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-mauve/30 transition-colors">
|
||||
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Session Duration</div>
|
||||
<div className="text-2xl font-black text-text font-mono tabular-nums">{formatDuration(report.duration_seconds)}</div>
|
||||
</div>
|
||||
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-blue/30 transition-colors">
|
||||
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Impact Avg CPU</div>
|
||||
<div className="text-2xl font-black text-blue tabular-nums">{summaryStats.cpu.toFixed(1)}%</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="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Impact Avg RAM</div>
|
||||
<div className="text-2xl font-black text-mauve tabular-nums">{formatMB(summaryStats.mem)}</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="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Issue Alerts</div>
|
||||
<div className="text-2xl font-black text-red tabular-nums">
|
||||
{filteredProcesses.filter(p => p.warnings.length > 0).length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card h-72 border border-surface1/50 shadow-2xl bg-gradient-to-b from-surface0 to-base/50">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-sm font-black uppercase italic tracking-widest flex items-center gap-2">
|
||||
<Activity size={16} className="text-blue" strokeWidth={3} /> {hideProfiler ? "Reactive Analysis Profile (Profiler Hidden)" : "Full System Load Profile"}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={reactiveTimeline}>
|
||||
<defs>
|
||||
<linearGradient id="cpuReportGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--blue)" stopOpacity={0.6}/>
|
||||
<stop offset="95%" stopColor="var(--blue)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--surface1)" vertical={false} opacity={0.5} />
|
||||
<XAxis dataKey="time" stroke="var(--subtext0)" tick={{fontSize: 9}} minTickGap={50} />
|
||||
<YAxis stroke="var(--subtext0)" tick={{fontSize: 9}} />
|
||||
<Tooltip
|
||||
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') return [formatBytes(value * 1024 * 1024 * 1024), 'RAM'];
|
||||
if (name === 'cpu') return [`${value.toFixed(1)}%`, 'CPU'];
|
||||
return [value, name];
|
||||
<Dashboard
|
||||
stats={stats}
|
||||
history={history}
|
||||
onRecordingToggle={toggleRecording}
|
||||
onImport={(importedReport) => {
|
||||
setReport(importedReport);
|
||||
setView('report');
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="cpu" name="CPU %" stroke="var(--blue)" fill="url(#cpuReportGradient)" strokeWidth={3} isAnimationActive={true} />
|
||||
<Area type="monotone" dataKey="mem" name="MEM GB" stroke="var(--mauve)" fill="none" strokeWidth={2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h3 className="text-xl font-black tracking-tighter uppercase italic flex items-center gap-2">
|
||||
<Server size={24} className="text-green" strokeWidth={3} /> {report.mode === ProfilingMode.Targeted ? "Hierarchical Analysis" : "Aggregated Resource Matrix"}
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
{report.mode === ProfilingMode.Targeted && (
|
||||
<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">
|
||||
{report.mode === ProfilingMode.Targeted ? "Toggle Nodes to Expand" : "Grouped by Application Name"}
|
||||
</span>
|
||||
</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="cursor-pointer hover:text-text transition-colors flex items-center gap-1" onClick={() => handleSort('name')}>
|
||||
Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</div>
|
||||
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('pid')}>
|
||||
{report.mode === ProfilingMode.Targeted ? "PID" : "Units"}
|
||||
</div>
|
||||
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_cpu' : 'avg_cpu')}>Avg 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(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_memory_mb' : 'avg_memory_mb')}>Avg 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>
|
||||
|
||||
<div className="overflow-y-auto flex-1 custom-scrollbar px-2">
|
||||
{report.mode === ProfilingMode.Targeted ? renderTreeRows(sortedProcesses) : sortedProcesses.map((proc, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => 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"
|
||||
>
|
||||
<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={cn(
|
||||
"w-2 h-2 rounded-full shrink-0",
|
||||
proc.is_syspulse ? "bg-mauve" : "bg-surface2 group-hover:bg-blue"
|
||||
)} />
|
||||
{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">{formatMB(proc.avg_memory_mb)}</div>
|
||||
<div className="text-subtext0 text-[11px] font-bold text-right self-center tabular-nums">{formatMB(proc.peak_memory_mb, 0)}</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 Cluster</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-surface0/50 border-t border-surface1 text-[9px] text-overlay1 font-medium italic flex items-center gap-2">
|
||||
<Shield size={10} className="text-blue" />
|
||||
<span>Real Memory (PSS) used for accuracy. Global mode aggregates processes by application name for performance.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Process Detail Side Panel/Modal */}
|
||||
{selectedProcess && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-end bg-base/80 backdrop-blur-sm p-8 animate-in slide-in-from-right duration-300">
|
||||
<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>
|
||||
<div className="text-xs font-black text-blue uppercase tracking-widest mb-1">
|
||||
{report.mode === ProfilingMode.Targeted ? `Process Inspector (PID: ${selectedProcess.pid})` : "Application Cluster Inspector"}
|
||||
</div>
|
||||
<h2 className="text-3xl font-black text-text tracking-tighter uppercase italic truncate max-w-md">{selectedProcess.name}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedProcess(null)}
|
||||
className="p-2 hover:bg-surface1 rounded-full transition-colors"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg">
|
||||
<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 tabular-nums">
|
||||
{(report.mode === ProfilingMode.Targeted ? selectedProcess.inclusive_avg_cpu : selectedProcess.avg_cpu).toFixed(1)}%
|
||||
</div>
|
||||
<p className="text-xs text-subtext1 mt-2">Combined impact of this application.</p>
|
||||
</div>
|
||||
<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">Total Avg RAM</div>
|
||||
<div className="text-4xl font-black text-mauve tabular-nums">
|
||||
{formatMB(report.mode === ProfilingMode.Targeted ? selectedProcess.inclusive_avg_memory_mb : selectedProcess.avg_memory_mb)}
|
||||
</div>
|
||||
<p className="text-xs text-subtext1 mt-2">Proportional memory footprint.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card h-64 shrink-0 bg-base border border-surface1">
|
||||
<div className="card-title mb-4">Resource History (Summed)</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={selectedProcess.history}>
|
||||
<defs>
|
||||
<linearGradient id="procCpuGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--blue)" stopOpacity={0.6}/>
|
||||
<stop offset="95%" stopColor="var(--blue)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="procMemGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--mauve)" stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor="var(--mauve)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--surface1)" vertical={false} opacity={0.3} />
|
||||
<XAxis dataKey="time" stroke="var(--subtext0)" tick={{fontSize: 9}} hide />
|
||||
<YAxis stroke="var(--subtext0)" tick={{fontSize: 9}} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: 'var(--crust)', border: '1px solid var(--surface1)', borderRadius: '12px' }}
|
||||
formatter={(value: number, name: string) => {
|
||||
if (name === 'memory_mb') return [formatMB(value), 'RAM'];
|
||||
if (name === 'cpu_usage') return [`${value.toFixed(1)}%`, 'CPU'];
|
||||
return [value, name];
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="cpu_usage" name="CPU %" stroke="var(--blue)" fill="url(#procCpuGrad)" strokeWidth={3} />
|
||||
<Area type="monotone" dataKey="memory_mb" name="Mem (PSS)" stroke="var(--mauve)" fill="url(#procMemGrad)" strokeWidth={2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-black text-overlay2 uppercase tracking-widest">Profiling 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 Recorded Load</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 Recorded RAM</span>
|
||||
<span className="font-black text-mauve">{formatMB(selectedProcess.peak_memory_mb)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-subtext1">
|
||||
{report.mode === ProfilingMode.Targeted ? "Child Process Count" : "Active Instances Count"}
|
||||
</span>
|
||||
<span className="font-black text-green">
|
||||
{report.mode === ProfilingMode.Targeted ? selectedProcess.children.length : selectedProcess.instance_count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-black text-overlay2 uppercase tracking-widest">Profiling Insights</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
{selectedProcess.warnings.length > 0 ? selectedProcess.warnings.map((w, idx) => (
|
||||
<div key={idx} className="bg-red/10 border border-red/20 p-4 rounded-xl flex items-center gap-3 text-red">
|
||||
<AlertTriangle size={20} />
|
||||
<span className="font-bold text-sm uppercase italic tracking-tight">{w}</span>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="bg-green/10 border border-green/20 p-4 rounded-xl flex items-center gap-3 text-green">
|
||||
<CheckSquare size={20} />
|
||||
<span className="font-bold text-sm uppercase italic tracking-tight">Healthy Performance Profile</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
256
src/components/Dashboard.tsx
Normal file
256
src/components/Dashboard.tsx
Normal file
@@ -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<number | null>(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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="flex flex-col h-screen bg-base text-text">
|
||||
<div data-tauri-drag-region className="titlebar shrink-0">
|
||||
<div className="titlebar-drag gap-2 px-4" data-tauri-drag-region>
|
||||
<div className="w-7 h-7 rounded-xl bg-gradient-to-br from-mauve via-blue to-teal flex items-center justify-center shadow-xl shadow-mauve/20 border border-white/10">
|
||||
<Activity size={16} className="text-base" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="font-black tracking-tighter uppercase italic text-xl">SysPulse</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 px-4">
|
||||
<label className="flex items-center gap-2 text-subtext0 hover:text-text transition-colors text-[10px] font-black uppercase tracking-widest cursor-pointer group">
|
||||
<Download size={14} className="text-blue group-hover:scale-110 transition-transform" />
|
||||
<span>Import Run</span>
|
||||
<input type="file" accept=".json" onChange={handleImport} className="hidden" />
|
||||
</label>
|
||||
<div className="h-4 w-px bg-surface1 mx-1" />
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-xl",
|
||||
stats.is_recording
|
||||
? "bg-red text-base animate-pulse ring-4 ring-red/20"
|
||||
: "bg-green text-base hover:scale-105 active:scale-95 hover:shadow-green/20"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!stats.is_recording && pinnedPid) {
|
||||
startTargeted(pinnedPid);
|
||||
} else {
|
||||
onRecordingToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{stats.is_recording ? <Square size={12} fill="currentColor" /> : <Play size={12} fill="currentColor" />}
|
||||
{stats.is_recording ? `STOP (${formatDuration(stats.recording_duration)})` : (pinnedPid ? "PROFILE TARGET" : "START GLOBAL")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="flex-1 overflow-hidden flex flex-col p-6 gap-6">
|
||||
{stats.is_recording ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-8 animate-in fade-in zoom-in duration-500">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-red/20 blur-3xl rounded-full scale-150 animate-pulse" />
|
||||
<div className="relative bg-surface0 p-12 rounded-[2.5rem] border-4 border-red/30 shadow-2xl overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-red/10 to-transparent" />
|
||||
<Activity size={64} className="text-red animate-pulse relative z-10" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-4xl font-black text-text tracking-tighter uppercase italic drop-shadow-lg">Profiling Active</h2>
|
||||
<p className="text-red font-mono text-2xl font-black tabular-nums">{formatDuration(stats.recording_duration)}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 w-full max-w-md">
|
||||
<div className="bg-surface0 p-6 rounded-[1.5rem] border border-surface1 shadow-xl text-center">
|
||||
<div className="text-[10px] font-black text-overlay2 mb-2 uppercase tracking-widest">Global CPU</div>
|
||||
<div className="text-3xl font-black text-blue tabular-nums">{avgCpu.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="bg-surface0 p-6 rounded-[1.5rem] border border-surface1 shadow-xl text-center">
|
||||
<div className="text-[10px] font-black text-overlay2 mb-2 uppercase tracking-widest">Global RAM</div>
|
||||
<div className="text-3xl font-black text-mauve tabular-nums">{memoryPercent.toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-overlay1 text-sm font-bold bg-surface0/50 px-4 py-2 rounded-full border border-surface1">
|
||||
<Shield size={14} className="text-green" />
|
||||
<span>Performance optimized snapshot mode enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 shrink-0">
|
||||
<div className="card group bg-gradient-to-b from-surface0 to-base/50">
|
||||
<div className="card-title"><Cpu size={16} className="text-blue" strokeWidth={3} /> CPU Load</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="stat-value text-blue tabular-nums">{avgCpu.toFixed(1)}</span>
|
||||
<span className="text-subtext0 font-black mb-2">%</span>
|
||||
</div>
|
||||
<div className="h-16 mt-4">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={history}>
|
||||
<defs>
|
||||
<linearGradient id="cpuDash" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--blue)" stopOpacity={0.5}/>
|
||||
<stop offset="95%" stopColor="var(--blue)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area type="monotone" dataKey="cpu" stroke="var(--blue)" fill="url(#cpuDash)" strokeWidth={3} isAnimationActive={false} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-gradient-to-b from-surface0 to-base/50">
|
||||
<div className="card-title"><Database size={16} className="text-mauve" strokeWidth={3} /> Memory</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="stat-value text-mauve tabular-nums">{formatBytes(stats.used_memory)}</span>
|
||||
<span className="text-subtext0 font-black mb-2">used</span>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-between text-[10px] font-black text-overlay2 uppercase mb-2">
|
||||
<span>{memoryPercent.toFixed(1)}% Utilized</span>
|
||||
<span>{formatBytes(stats.total_memory, 0)} Total</span>
|
||||
</div>
|
||||
<div className="h-3 bg-surface1/50 rounded-full overflow-hidden border border-surface2">
|
||||
<div className="h-full bg-gradient-to-r from-mauve to-pink transition-all duration-700 shadow-lg shadow-mauve/20" style={{ width: `${memoryPercent}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-gradient-to-b from-surface0 to-base/50">
|
||||
<div className="card-title"><Server size={16} className="text-green" strokeWidth={3} /> Tasks</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="stat-value text-green tabular-nums">{stats.processes.length}</span>
|
||||
<span className="text-subtext0 font-black mb-2">Processes</span>
|
||||
</div>
|
||||
<div className="mt-4 text-xs text-subtext1 font-medium leading-relaxed bg-base/30 p-3 rounded-xl border border-surface1">
|
||||
Live system feed. Target a process to profile its hierarchy in detail.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card flex-1 flex flex-col min-h-0 overflow-hidden shadow-[0_35px_60px_-15px_rgba(0,0,0,0.5)] border border-surface1/50">
|
||||
<div className="flex justify-between items-center mb-6 px-2 shrink-0">
|
||||
<h3 className="text-xl font-black tracking-tighter uppercase italic flex items-center gap-2">
|
||||
<Activity size={24} className="text-red" strokeWidth={3} />
|
||||
Live Feed
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="SEARCH PROCESS..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[10px] bg-red/10 text-red border border-red/20 px-3 py-1.5 rounded-full font-black uppercase tracking-widest flex items-center gap-1.5">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-red animate-pulse" />
|
||||
Real-time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_80px_100px_120px_120px] gap-4 px-6 py-4 bg-surface1/50 rounded-2xl font-black uppercase tracking-widest text-[10px] text-overlay1 mb-2">
|
||||
<div>Process</div>
|
||||
<div className="text-right">PID</div>
|
||||
<div className="text-right">CPU %</div>
|
||||
<div className="text-right">Memory</div>
|
||||
<div className="text-center">Actions</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto flex-1 custom-scrollbar px-2">
|
||||
{filteredLiveProcs.map((proc) => (
|
||||
<div
|
||||
key={proc.pid}
|
||||
className={cn(
|
||||
"grid grid-cols-[1fr_80px_100px_120px_120px] gap-4 px-4 py-4 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl cursor-pointer",
|
||||
pinnedPid === proc.pid ? "bg-blue/10 border-blue/30" : ""
|
||||
)}
|
||||
onClick={() => setPinnedPid(pinnedPid === proc.pid ? null : proc.pid)}
|
||||
>
|
||||
<div className="font-black text-text truncate text-sm flex items-center gap-3" title={proc.name}>
|
||||
<div className={cn(
|
||||
"w-2 h-2 rounded-full transition-colors",
|
||||
proc.is_syspulse ? "bg-mauve" : (pinnedPid === proc.pid ? "bg-blue" : "bg-surface2 group-hover:bg-blue")
|
||||
)} />
|
||||
{proc.name}
|
||||
</div>
|
||||
<div className="font-mono text-overlay2 text-xs text-right self-center">{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-mauve font-black text-sm text-right self-center tabular-nums">{formatBytes(proc.memory, 0)}</div>
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); startTargeted(proc.pid); }}
|
||||
className="opacity-0 group-hover:opacity-100 p-2 hover:bg-blue/20 text-blue rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
title="Target Profile"
|
||||
>
|
||||
<Target size={16} strokeWidth={2.5} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); killProcess(proc.pid); }}
|
||||
className="opacity-0 group-hover:opacity-100 p-2 hover:bg-red/20 text-red rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
title="Terminate"
|
||||
>
|
||||
<Shield size={16} strokeWidth={2.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
src/components/ProcessInspector.tsx
Normal file
124
src/components/ProcessInspector.tsx
Normal file
@@ -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 (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-end bg-base/80 backdrop-blur-sm p-8 animate-in slide-in-from-right duration-300">
|
||||
<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>
|
||||
<div className="text-xs font-black text-blue uppercase tracking-widest mb-1">
|
||||
{mode === ProfilingMode.Targeted ? `Process Inspector (PID: ${selectedProcess.pid})` : "Application Cluster Inspector"}
|
||||
</div>
|
||||
<h2 className="text-3xl font-black text-text tracking-tighter uppercase italic truncate max-w-md">{selectedProcess.name}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-surface1 rounded-full transition-colors"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg">
|
||||
<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 tabular-nums">
|
||||
{(mode === ProfilingMode.Targeted ? selectedProcess.inclusive_avg_cpu : selectedProcess.avg_cpu).toFixed(1)}%
|
||||
</div>
|
||||
<p className="text-xs text-subtext1 mt-2">Combined impact of this application.</p>
|
||||
</div>
|
||||
<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">Total Avg RAM</div>
|
||||
<div className="text-4xl font-black text-mauve tabular-nums">
|
||||
{formatMB(mode === ProfilingMode.Targeted ? selectedProcess.inclusive_avg_memory_mb : selectedProcess.avg_memory_mb)}
|
||||
</div>
|
||||
<p className="text-xs text-subtext1 mt-2">Proportional memory footprint.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card h-64 shrink-0 bg-base border border-surface1">
|
||||
<div className="card-title mb-4">Resource History (Summed)</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={selectedProcess.history}>
|
||||
<defs>
|
||||
<linearGradient id="procCpuGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--blue)" stopOpacity={0.6}/>
|
||||
<stop offset="95%" stopColor="var(--blue)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="procMemGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--mauve)" stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor="var(--mauve)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--surface1)" vertical={false} opacity={0.3} />
|
||||
<XAxis dataKey="time" stroke="var(--subtext0)" tick={{fontSize: 9}} hide />
|
||||
<YAxis stroke="var(--subtext0)" tick={{fontSize: 9}} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: 'var(--crust)', border: '1px solid var(--surface1)', borderRadius: '12px' }}
|
||||
formatter={(value: number, name: string) => {
|
||||
if (name === 'memory_mb') return [formatMB(value), 'RAM'];
|
||||
if (name === 'cpu_usage') return [`${value.toFixed(1)}%`, 'CPU'];
|
||||
return [value, name];
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="cpu_usage" name="CPU %" stroke="var(--blue)" fill="url(#procCpuGrad)" strokeWidth={3} />
|
||||
<Area type="monotone" dataKey="memory_mb" name="Mem (PSS)" stroke="var(--mauve)" fill="url(#procMemGrad)" strokeWidth={2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-black text-overlay2 uppercase tracking-widest">Profiling 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 Recorded Load</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 Recorded RAM</span>
|
||||
<span className="font-black text-mauve">{formatMB(selectedProcess.peak_memory_mb)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-subtext1">
|
||||
{mode === ProfilingMode.Targeted ? "Child Process Count" : "Active Instances Count"}
|
||||
</span>
|
||||
<span className="font-black text-green">
|
||||
{mode === ProfilingMode.Targeted ? selectedProcess.children.length : selectedProcess.instance_count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-black text-overlay2 uppercase tracking-widest">Profiling Insights</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
{selectedProcess.warnings.length > 0 ? selectedProcess.warnings.map((w, idx) => (
|
||||
<div key={idx} className="bg-red/10 border border-red/20 p-4 rounded-xl flex items-center gap-3 text-red">
|
||||
<AlertTriangle size={20} />
|
||||
<span className="font-bold text-sm uppercase italic tracking-tight">{w}</span>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="bg-green/10 border border-green/20 p-4 rounded-xl flex items-center gap-3 text-green">
|
||||
<CheckSquare size={20} />
|
||||
<span className="font-bold text-sm uppercase italic tracking-tight">Healthy Performance Profile</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
336
src/components/ReportView.tsx
Normal file
336
src/components/ReportView.tsx
Normal file
@@ -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<SortField>(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_cpu' : 'avg_cpu');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
const [selectedProcess, setSelectedProcess] = useState<AggregatedProcess | null>(null);
|
||||
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) => {
|
||||
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<string>('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 (
|
||||
<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">
|
||||
{formatMB(proc.inclusive_avg_memory_mb)}
|
||||
{proc.children.length > 0 && <span className="block text-[9px] text-overlay1 font-normal">({formatMB(proc.avg_memory_mb)} self)</span>}
|
||||
</div>
|
||||
|
||||
<div className="text-subtext0 text-[11px] font-bold text-right self-center tabular-nums">{formatMB(proc.peak_memory_mb, 0)}</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.filter(c => !hideProfiler || !c.is_syspulse),
|
||||
depth + 1
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-base text-text">
|
||||
<div className="titlebar shrink-0">
|
||||
<div className="titlebar-drag gap-2 px-4">
|
||||
<div className="w-7 h-7 rounded-xl bg-gradient-to-br from-mauve via-blue to-teal flex items-center justify-center shadow-xl shadow-mauve/20 border border-white/10">
|
||||
<Activity size={16} className="text-base" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="font-black tracking-tighter uppercase italic text-xl">
|
||||
{report.mode === ProfilingMode.Targeted ? `Target: ${report.target_name || "Process"}` : "Global Report"}
|
||||
</span>
|
||||
</div>
|
||||
<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"
|
||||
)}
|
||||
>
|
||||
{hideProfiler ? <EyeOff size={14} /> : <Eye size={14} />} {hideProfiler ? "SHOW PROFILER" : "HIDE PROFILER"}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Save size={14} className="text-blue" /> SAVE JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-mauve/10 hover:bg-mauve/20 border border-mauve/30 rounded-xl text-xs font-black transition-all text-mauve uppercase tracking-widest"
|
||||
>
|
||||
<ArrowLeft size={14} /> DASHBOARD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-6 flex flex-col gap-6 custom-scrollbar relative">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 shrink-0">
|
||||
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-mauve/30 transition-colors">
|
||||
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Session Duration</div>
|
||||
<div className="text-2xl font-black text-text font-mono tabular-nums">{formatDuration(report.duration_seconds)}</div>
|
||||
</div>
|
||||
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-blue/30 transition-colors">
|
||||
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Impact Avg CPU</div>
|
||||
<div className="text-2xl font-black text-blue tabular-nums">{summaryStats.cpu.toFixed(1)}%</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="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Impact Avg RAM</div>
|
||||
<div className="text-2xl font-black text-mauve tabular-nums">{formatMB(summaryStats.mem)}</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="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Issue Alerts</div>
|
||||
<div className="text-2xl font-black text-red tabular-nums">
|
||||
{filteredProcesses.filter(p => p.warnings.length > 0).length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card h-72 border border-surface1/50 shadow-2xl bg-gradient-to-b from-surface0 to-base/50">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-sm font-black uppercase italic tracking-widest flex items-center gap-2">
|
||||
<Activity size={16} className="text-blue" strokeWidth={3} /> {hideProfiler ? "Reactive Analysis Profile (Profiler Hidden)" : "Full System Load Profile"}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={reactiveTimeline}>
|
||||
<defs>
|
||||
<linearGradient id="cpuReportGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--blue)" stopOpacity={0.6}/>
|
||||
<stop offset="95%" stopColor="var(--blue)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--surface1)" vertical={false} opacity={0.5} />
|
||||
<XAxis dataKey="time" stroke="var(--subtext0)" tick={{fontSize: 9}} minTickGap={50} />
|
||||
<YAxis stroke="var(--subtext0)" tick={{fontSize: 9}} />
|
||||
<Tooltip
|
||||
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') return [formatBytes(value * 1024 * 1024 * 1024), 'RAM'];
|
||||
if (name === 'cpu') return [`${value.toFixed(1)}%`, 'CPU'];
|
||||
return [value, name];
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="cpu" name="CPU %" stroke="var(--blue)" fill="url(#cpuReportGradient)" strokeWidth={3} isAnimationActive={true} />
|
||||
<Area type="monotone" dataKey="mem" name="MEM GB" stroke="var(--mauve)" fill="none" strokeWidth={2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h3 className="text-xl font-black tracking-tighter uppercase italic flex items-center gap-2">
|
||||
<Server size={24} className="text-green" strokeWidth={3} /> {report.mode === ProfilingMode.Targeted ? "Hierarchical Analysis" : "Aggregated Resource Matrix"}
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
{report.mode === ProfilingMode.Targeted && (
|
||||
<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">
|
||||
{report.mode === ProfilingMode.Targeted ? "Toggle Nodes to Expand" : "Grouped by Application Name"}
|
||||
</span>
|
||||
</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="cursor-pointer hover:text-text transition-colors flex items-center gap-1" onClick={() => handleSort('name')}>
|
||||
Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</div>
|
||||
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('pid')}>
|
||||
{report.mode === ProfilingMode.Targeted ? "PID" : "Units"}
|
||||
</div>
|
||||
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_cpu' : 'avg_cpu')}>Avg 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(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_memory_mb' : 'avg_memory_mb')}>Avg 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>
|
||||
|
||||
<div className="overflow-y-auto flex-1 custom-scrollbar px-2">
|
||||
{report.mode === ProfilingMode.Targeted ? renderTreeRows(sortedProcesses) : sortedProcesses.map((proc, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => 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"
|
||||
>
|
||||
<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={cn(
|
||||
"w-2 h-2 rounded-full shrink-0",
|
||||
proc.is_syspulse ? "bg-mauve" : "bg-surface2 group-hover:bg-blue"
|
||||
)} />
|
||||
{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">{formatMB(proc.avg_memory_mb)}</div>
|
||||
<div className="text-subtext0 text-[11px] font-bold text-right self-center tabular-nums">{formatMB(proc.peak_memory_mb, 0)}</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 Cluster</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-surface0/50 border-t border-surface1 text-[9px] text-overlay1 font-medium italic flex items-center gap-2">
|
||||
<Shield size={10} className="text-blue" />
|
||||
<span>Real Memory (PSS) used for accuracy. Global mode aggregates processes by application name for performance.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedProcess && (
|
||||
<ProcessInspector
|
||||
selectedProcess={selectedProcess}
|
||||
mode={report.mode}
|
||||
onClose={() => setSelectedProcess(null)}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
src/types/index.ts
Normal file
63
src/types/index.ts
Normal file
@@ -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[];
|
||||
}
|
||||
25
src/utils/index.ts
Normal file
25
src/utils/index.ts
Normal file
@@ -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')}`;
|
||||
}
|
||||
Reference in New Issue
Block a user