feat: complete rewrite for accurate PSS memory and smart user-focused hierarchy
This commit is contained in:
15
build_log.txt
Normal file
15
build_log.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
> syspulse@0.1.0 build
|
||||
> tsc && vite build
|
||||
|
||||
vite v6.4.1 building for production...
|
||||
transforming...
|
||||
✓ 2352 modules transformed.
|
||||
rendering chunks...
|
||||
computing gzip size...
|
||||
dist/index.html 0.55 kB │ gzip: 0.31 kB
|
||||
dist/assets/index-C5k9P4b0.css 23.62 kB │ gzip: 5.27 kB
|
||||
dist/assets/index--56uGBza.js 16.97 kB │ gzip: 4.60 kB
|
||||
dist/assets/charts-vendor-CdA6MGxy.js 310.46 kB │ gzip: 79.84 kB
|
||||
dist/assets/vendor-B6fi2Fb1.js 313.23 kB │ gzip: 99.70 kB
|
||||
✓ built in 8.00s
|
||||
20
src/App.tsx
20
src/App.tsx
@@ -1,10 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { GlobalStats } from './types';
|
||||
import { GlobalStats, ProfilingReport } from './types';
|
||||
import { Dashboard } from './components/Dashboard';
|
||||
import { ReportView } from './components/ReportView';
|
||||
|
||||
function App() {
|
||||
const [stats, setStats] = useState<GlobalStats | null>(null);
|
||||
const [report, setReport] = useState<ProfilingReport | null>(null);
|
||||
const [history, setHistory] = useState<{ time: string; cpu: number }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -14,9 +16,7 @@ function App() {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const data = await invoke<GlobalStats>('get_latest_stats');
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
setStats(data);
|
||||
|
||||
setHistory(prev => {
|
||||
@@ -25,7 +25,7 @@ function App() {
|
||||
return newHistory;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch stats:', e);
|
||||
console.error(e);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
timeoutId = window.setTimeout(fetchStats, 1000);
|
||||
@@ -42,17 +42,19 @@ function App() {
|
||||
|
||||
if (!stats) return (
|
||||
<div className="h-screen w-screen flex flex-col items-center justify-center text-text bg-base font-black italic tracking-tighter uppercase text-2xl animate-pulse">
|
||||
Initializing SysPulse
|
||||
<div className="mt-4 w-48 h-1 bg-surface1 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue animate-[loading_2s_infinite]" style={{ width: '30%' }} />
|
||||
</div>
|
||||
Initializing SysPulse...
|
||||
</div>
|
||||
);
|
||||
|
||||
if (report) {
|
||||
return <ReportView report={report} onBack={() => setReport(null)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dashboard
|
||||
stats={stats}
|
||||
history={history}
|
||||
history={history}
|
||||
onShowReport={(rep) => setReport(rep)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { useState, useMemo, Fragment } from 'react';
|
||||
import { useState, Fragment } from 'react';
|
||||
import {
|
||||
Activity, Cpu, Server, Database, Shield, ChevronRight, ChevronDown, Search
|
||||
Activity, Cpu, Server, Database, Target, Play, Square, ChevronRight, ChevronDown, Search, Download
|
||||
} from 'lucide-react';
|
||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
import { GlobalStats, ProcessNode } from '../types';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { GlobalStats, ProcessNode, ProfilingReport } from '../types';
|
||||
import { cn, formatBytes } from '../utils';
|
||||
|
||||
interface Props {
|
||||
stats: GlobalStats;
|
||||
history: { time: string; cpu: number }[];
|
||||
onShowReport: (report: ProfilingReport) => void;
|
||||
}
|
||||
|
||||
export function Dashboard({ stats, history }: Props) {
|
||||
export function Dashboard({ stats, history, onShowReport }: Props) {
|
||||
const [searchTerm, setSearchBar] = useState("");
|
||||
const [expandedPids, setExpandedPids] = useState<Set<number>>(new Set());
|
||||
const [pinnedPid, setPinnedPid] = useState<number | null>(null);
|
||||
|
||||
const memoryPercent = (Number(stats.mem_used) / Number(stats.mem_total)) * 100;
|
||||
|
||||
@@ -24,43 +27,54 @@ export function Dashboard({ stats, history }: Props) {
|
||||
setExpandedPids(next);
|
||||
};
|
||||
|
||||
// Flatten the tree for the list view if searching, or keep it hierarchical
|
||||
const filteredTree = useMemo(() => {
|
||||
if (!searchTerm) return stats.process_tree;
|
||||
|
||||
const search = searchTerm.toLowerCase();
|
||||
const result: ProcessNode[] = [];
|
||||
|
||||
function searchRecursive(nodes: ProcessNode[]) {
|
||||
for (const node of nodes) {
|
||||
if (node.name.toLowerCase().includes(search) || node.pid.toString().includes(search)) {
|
||||
result.push(node);
|
||||
}
|
||||
searchRecursive(node.children);
|
||||
}
|
||||
const toggleRecording = async () => {
|
||||
if (stats.is_profiling) {
|
||||
const report = await invoke<ProfilingReport | null>('stop_profiling');
|
||||
if (report) onShowReport(report);
|
||||
} else {
|
||||
await invoke('start_profiling', { target_pid: pinnedPid });
|
||||
}
|
||||
|
||||
searchRecursive(stats.process_tree);
|
||||
return result;
|
||||
}, [stats.process_tree, searchTerm]);
|
||||
};
|
||||
|
||||
const handleImport = (e: React.ChangeEvent<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;
|
||||
onShowReport(data);
|
||||
} catch (err) {
|
||||
alert('Failed to parse report');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const renderProcessNode = (node: ProcessNode, depth = 0) => {
|
||||
const isExpanded = expandedPids.has(node.pid);
|
||||
const hasChildren = node.children.length > 0;
|
||||
const totalCpu = node.cpu_self + node.cpu_children;
|
||||
const totalMem = node.mem_rss + node.mem_children;
|
||||
const isPinned = pinnedPid === node.pid;
|
||||
|
||||
if (searchTerm && !node.name.toLowerCase().includes(searchTerm.toLowerCase()) && !node.pid.toString().includes(searchTerm)) {
|
||||
// Simple search for now: if node doesn't match, search children
|
||||
if (!node.children.some(c => c.name.toLowerCase().includes(searchTerm.toLowerCase()))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={node.pid}>
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-[1fr_80px_100px_120px_80px] gap-4 px-4 py-3 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl items-center",
|
||||
depth > 0 && "bg-base/10"
|
||||
"grid grid-cols-[1fr_80px_100px_120px_100px] gap-4 px-4 py-3 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl items-center cursor-pointer",
|
||||
isPinned && "bg-blue/10 border-blue/30"
|
||||
)}
|
||||
onClick={() => setPinnedPid(isPinned ? null : node.pid)}
|
||||
>
|
||||
<div className="font-bold text-text truncate text-sm flex items-center gap-2" style={{ paddingLeft: depth * 16 }}>
|
||||
{hasChildren && !searchTerm ? (
|
||||
<button onClick={() => toggleExpand(node.pid)} className="p-1 hover:bg-surface2 rounded">
|
||||
{hasChildren ? (
|
||||
<button onClick={(e) => { e.stopPropagation(); toggleExpand(node.pid); }} className="p-1 hover:bg-surface2 rounded">
|
||||
{isExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
</button>
|
||||
) : <div className="w-6" />}
|
||||
@@ -68,20 +82,21 @@ export function Dashboard({ stats, history }: Props) {
|
||||
</div>
|
||||
<div className="font-mono text-overlay2 text-xs text-right">{node.pid}</div>
|
||||
<div className="text-blue font-black text-sm text-right tabular-nums">
|
||||
{totalCpu.toFixed(1)}%
|
||||
{depth === 0 && node.cpu_children > 0 && <span className="block text-[9px] text-overlay1 font-normal">Self: {node.cpu_self.toFixed(1)}%</span>}
|
||||
{node.cpu_inclusive.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-mauve font-black text-sm text-right tabular-nums">
|
||||
{formatBytes(totalMem)}
|
||||
{depth === 0 && node.mem_children > 0 && <span className="block text-[9px] text-overlay1 font-normal">Self: {formatBytes(node.mem_rss)}</span>}
|
||||
{formatBytes(node.mem_pss_inclusive)}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<button className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-red/20 text-red rounded-lg transition-all">
|
||||
<Shield size={14} />
|
||||
<div className="flex justify-center gap-2">
|
||||
<button
|
||||
className={cn("p-1.5 rounded-lg transition-all", isPinned ? "text-blue opacity-100" : "opacity-0 group-hover:opacity-100 text-overlay1 hover:text-blue")}
|
||||
onClick={(e) => { e.stopPropagation(); setPinnedPid(isPinned ? null : node.pid); }}
|
||||
>
|
||||
<Target size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{hasChildren && isExpanded && !searchTerm && node.children.map(child => renderProcessNode(child, depth + 1))}
|
||||
{hasChildren && isExpanded && node.children.map(child => renderProcessNode(child, depth + 1))}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -96,10 +111,24 @@ export function Dashboard({ stats, history }: Props) {
|
||||
<span className="font-black tracking-tighter uppercase italic text-xl">SysPulse</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 px-4">
|
||||
<div className="text-[10px] bg-surface1 px-3 py-1.5 rounded-full font-black text-overlay1 uppercase tracking-widest flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green animate-pulse" />
|
||||
Live Monitor
|
||||
</div>
|
||||
<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</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_profiling
|
||||
? "bg-red text-base animate-pulse ring-4 ring-red/20"
|
||||
: "bg-green text-base hover:scale-105 active:scale-95"
|
||||
)}
|
||||
onClick={toggleRecording}
|
||||
>
|
||||
{stats.is_profiling ? <Square size={12} fill="currentColor" /> : <Play size={12} fill="currentColor" />}
|
||||
{stats.is_profiling ? `STOP` : (pinnedPid ? "PROFILE TARGET" : "START GLOBAL")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,7 +156,7 @@ export function Dashboard({ stats, history }: Props) {
|
||||
</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="card-title"><Database size={16} className="text-mauve" strokeWidth={3} /> Real Memory (PSS)</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="stat-value text-mauve tabular-nums">{formatBytes(stats.mem_used)}</span>
|
||||
<span className="text-subtext0 font-black mb-2">used</span>
|
||||
@@ -144,13 +173,13 @@ export function Dashboard({ stats, history }: Props) {
|
||||
</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} /> Process Stack</div>
|
||||
<div className="card-title"><Server size={16} className="text-green" strokeWidth={3} /> Smart View</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="stat-value text-green tabular-nums">{stats.process_count}</span>
|
||||
<span className="text-subtext0 font-black mb-2">Active</span>
|
||||
<span className="stat-value text-green tabular-nums">{stats.smart_tree.length}</span>
|
||||
<span className="text-subtext0 font-black mb-2">Entry points</span>
|
||||
</div>
|
||||
<div className="mt-4 text-xs text-subtext1 font-medium leading-relaxed bg-base/30 p-3 rounded-xl border border-surface1">
|
||||
Deep hierarchical tracking enabled. Child process overhead is summed into parent totals.
|
||||
Top-level processes are grouped logically. Shared memory is distributed fairly via PSS.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,32 +188,30 @@ export function Dashboard({ stats, history }: Props) {
|
||||
<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} />
|
||||
System Hierarchy
|
||||
Process Hierarchy
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-overlay1" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="SEARCH HIERARCHY..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchBar(e.target.value)}
|
||||
className="bg-surface1/50 border border-surface2 rounded-xl pl-9 pr-4 py-2 text-xs font-bold text-text placeholder:text-overlay1 focus:outline-none focus:ring-2 focus:ring-blue/30 w-64 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-overlay1" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="SEARCH..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchBar(e.target.value)}
|
||||
className="bg-surface1/50 border border-surface2 rounded-xl pl-9 pr-4 py-2 text-xs font-bold text-text placeholder:text-overlay1 focus:outline-none focus:ring-2 focus:ring-blue/30 w-64 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_80px_100px_120px_80px] 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_120px_100px] gap-4 px-6 py-4 bg-surface1/50 rounded-2xl font-black uppercase tracking-widest text-[10px] text-overlay1 mb-2">
|
||||
<div>Hierarchy</div>
|
||||
<div className="text-right">PID</div>
|
||||
<div className="text-right">Total CPU</div>
|
||||
<div className="text-right">Total RAM</div>
|
||||
<div className="text-center">Action</div>
|
||||
<div className="text-right">Inclusive CPU</div>
|
||||
<div className="text-right">Inclusive PSS</div>
|
||||
<div className="text-center">Target</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto flex-1 custom-scrollbar px-2">
|
||||
{filteredTree.map(node => renderProcessNode(node))}
|
||||
{stats.smart_tree.map(node => renderProcessNode(node))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
172
src/components/ReportView.tsx
Normal file
172
src/components/ReportView.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useState, Fragment } from 'react';
|
||||
import {
|
||||
Activity, Server, ArrowLeft, ChevronRight, ChevronDown, Save, FileJson, Clock, Target, AlertTriangle
|
||||
} from 'lucide-react';
|
||||
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { ProcessNode, ProfilingReport } from '../types';
|
||||
import { formatBytes, formatDuration } from '../utils';
|
||||
|
||||
interface Props {
|
||||
report: ProfilingReport;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function ReportView({ report, onBack }: Props) {
|
||||
const [expandedPids, setExpandedPids] = useState<Set<number>>(new Set());
|
||||
|
||||
const toggleExpand = (pid: number) => {
|
||||
const next = new Set(expandedPids);
|
||||
if (next.has(pid)) next.delete(pid);
|
||||
else next.add(pid);
|
||||
setExpandedPids(next);
|
||||
};
|
||||
|
||||
const saveReport = async () => {
|
||||
try {
|
||||
const path = await invoke<string>('save_report', { report });
|
||||
alert(`Report saved to: ${path}`);
|
||||
} catch (e) {
|
||||
alert(`Failed to save: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
const renderProcessNode = (node: ProcessNode, depth = 0) => {
|
||||
const isExpanded = expandedPids.has(node.pid);
|
||||
const hasChildren = node.children.length > 0;
|
||||
|
||||
return (
|
||||
<Fragment key={node.pid}>
|
||||
<div
|
||||
className="grid grid-cols-[1fr_80px_100px_120px_100px] gap-4 px-4 py-3 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl items-center"
|
||||
>
|
||||
<div className="font-bold text-text truncate text-sm flex items-center gap-2" style={{ paddingLeft: depth * 16 }}>
|
||||
{hasChildren ? (
|
||||
<button onClick={() => toggleExpand(node.pid)} className="p-1 hover:bg-surface2 rounded">
|
||||
{isExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
</button>
|
||||
) : <div className="w-6" />}
|
||||
<span title={node.name}>{node.name}</span>
|
||||
</div>
|
||||
<div className="font-mono text-overlay2 text-xs text-right">{node.pid}</div>
|
||||
<div className="text-blue font-black text-sm text-right tabular-nums">
|
||||
{node.cpu_inclusive.toFixed(1)}%
|
||||
{hasChildren && <span className="block text-[9px] text-overlay1 font-normal">Self: {node.cpu_self.toFixed(1)}%</span>}
|
||||
</div>
|
||||
<div className="text-mauve font-black text-sm text-right tabular-nums">
|
||||
{formatBytes(node.mem_pss_inclusive)}
|
||||
{hasChildren && <span className="block text-[9px] text-overlay1 font-normal">Self: {formatBytes(node.mem_pss_self)}</span>}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
{node.cpu_inclusive > 50 && <AlertTriangle size={14} className="text-red" />}
|
||||
</div>
|
||||
</div>
|
||||
{hasChildren && isExpanded && node.children.map(child => renderProcessNode(child, 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">
|
||||
Profiling Report
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-4 px-4">
|
||||
<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">
|
||||
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1 flex items-center justify-center gap-1"><Clock size={10} /> 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">
|
||||
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1 flex items-center justify-center gap-1"><Target size={10} /> Target</div>
|
||||
<div className="text-xl font-bold text-text truncate">{report.target_pid ? `PID ${report.target_pid}` : "Whole System"}</div>
|
||||
</div>
|
||||
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center">
|
||||
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1 flex items-center justify-center gap-1"><Server size={10} /> Nodes</div>
|
||||
<div className="text-2xl font-black text-blue tabular-nums">{report.aggregated_tree.length}</div>
|
||||
</div>
|
||||
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center">
|
||||
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1 flex items-center justify-center gap-1"><FileJson size={10} /> Snapshots</div>
|
||||
<div className="text-2xl font-black text-mauve tabular-nums">{report.snapshots.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} /> Load Over Time
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={report.snapshots}>
|
||||
<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="timestamp" hide />
|
||||
<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_used') return [formatBytes(value), 'System RAM'];
|
||||
if (name === 'cpu_usage') return [`${value.toFixed(1)}%`, 'System CPU'];
|
||||
return [value, name];
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="cpu_usage" name="CPU %" stroke="var(--blue)" fill="url(#cpuReportGradient)" strokeWidth={3} isAnimationActive={true} />
|
||||
</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} /> Hierarchical Analysis
|
||||
</h3>
|
||||
<span className="text-[10px] bg-surface1 px-3 py-1.5 rounded-full font-black text-overlay1 uppercase tracking-widest">Averaged across session</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_80px_100px_120px_100px] gap-4 px-6 py-4 bg-surface1/50 rounded-2xl font-black uppercase tracking-widest text-[10px] text-overlay1 mb-2">
|
||||
<div>Hierarchy</div>
|
||||
<div className="text-right">PID</div>
|
||||
<div className="text-right">Avg CPU</div>
|
||||
<div className="text-right">Avg PSS</div>
|
||||
<div className="text-center">Alerts</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto flex-1 custom-scrollbar px-2">
|
||||
{report.aggregated_tree.map(node => renderProcessNode(node))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,9 @@ export interface ProcessNode {
|
||||
pid: number;
|
||||
name: string;
|
||||
cpu_self: number;
|
||||
cpu_children: number;
|
||||
mem_rss: number;
|
||||
mem_children: number;
|
||||
cpu_inclusive: number;
|
||||
mem_pss_self: number;
|
||||
mem_pss_inclusive: number;
|
||||
children: ProcessNode[];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,30 @@ export interface GlobalStats {
|
||||
cpu_total: number;
|
||||
mem_used: number;
|
||||
mem_total: number;
|
||||
process_tree: ProcessNode[];
|
||||
smart_tree: ProcessNode[];
|
||||
process_count: number;
|
||||
is_profiling: boolean;
|
||||
}
|
||||
|
||||
export interface ProfilingReport {
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
duration_seconds: number;
|
||||
snapshots: ReportSnapshot[];
|
||||
target_pid?: number;
|
||||
aggregated_tree: ProcessNode[];
|
||||
}
|
||||
|
||||
export interface ReportSnapshot {
|
||||
timestamp: string;
|
||||
cpu_usage: number;
|
||||
mem_used: number;
|
||||
top_processes: ProcessSnapshot[];
|
||||
}
|
||||
|
||||
export interface ProcessSnapshot {
|
||||
pid: number;
|
||||
name: string;
|
||||
cpu: number;
|
||||
pss: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user