updated profiling options

This commit is contained in:
2026-02-23 17:56:53 +01:00
parent d46f057867
commit 8a24fa5689
2 changed files with 279 additions and 85 deletions

View File

@@ -3,7 +3,7 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use sysinfo::System; use sysinfo::{System, Pid};
use std::sync::Mutex; use std::sync::Mutex;
use std::process::Command; use std::process::Command;
use tauri::State; use tauri::State;
@@ -14,6 +14,7 @@ use std::fs;
use clap::Parser; use clap::Parser;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use rayon::prelude::*;
// --- CLI --- // --- CLI ---
@@ -36,6 +37,12 @@ struct Cli {
// --- Data Structures --- // --- Data Structures ---
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)]
enum ProfilingMode {
Global,
Targeted,
}
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
struct SystemStats { struct SystemStats {
cpu_usage: Vec<f32>, cpu_usage: Vec<f32>,
@@ -68,6 +75,8 @@ struct Snapshot {
struct ProfilingSession { struct ProfilingSession {
is_active: bool, is_active: bool,
mode: ProfilingMode,
target_pid: Option<u32>,
start_time: Option<DateTime<Utc>>, start_time: Option<DateTime<Utc>>,
snapshots: Vec<Snapshot>, snapshots: Vec<Snapshot>,
} }
@@ -86,6 +95,8 @@ struct Report {
start_time: String, start_time: String,
end_time: String, end_time: String,
duration_seconds: i64, duration_seconds: i64,
mode: ProfilingMode,
target_name: Option<String>,
timeline: Vec<TimelinePoint>, timeline: Vec<TimelinePoint>,
aggregated_processes: Vec<AggregatedProcess>, aggregated_processes: Vec<AggregatedProcess>,
} }
@@ -93,8 +104,10 @@ struct Report {
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
struct TimelinePoint { struct TimelinePoint {
time: String, time: String,
avg_cpu: f32, cpu_total: f32,
memory_gb: f32, mem_total_gb: f32,
cpu_profiler: f32,
mem_profiler_gb: f32,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
@@ -220,19 +233,36 @@ fn collect_snapshot(
} }
} }
fn generate_report(start_time: DateTime<Utc>, snapshots: Vec<Snapshot>) -> Report { fn generate_report(start_time: DateTime<Utc>, snapshots: Vec<Snapshot>, mode: ProfilingMode, target_pid: Option<u32>) -> Report {
let end_time = Utc::now(); let end_time = Utc::now();
let duration = (end_time - start_time).num_seconds(); let duration = (end_time - start_time).num_seconds();
let timeline: Vec<TimelinePoint> = snapshots.iter().map(|s| { let timeline: Vec<TimelinePoint> = snapshots.iter().map(|s| {
let avg_cpu = s.cpu_usage.iter().sum::<f32>() / s.cpu_usage.len() as f32; let cpu_total = s.cpu_usage.iter().sum::<f32>() / s.cpu_usage.len() as f32;
let mem_total_gb = s.used_memory as f32 / 1024.0 / 1024.0 / 1024.0;
let profiler_stats = s.processes.iter()
.filter(|p| p.is_syspulse)
.fold((0.0, 0), |acc, p| (acc.0 + p.cpu_usage, acc.1 + p.memory));
TimelinePoint { TimelinePoint {
time: s.timestamp.format("%H:%M:%S").to_string(), time: s.timestamp.format("%H:%M:%S").to_string(),
avg_cpu, cpu_total,
memory_gb: s.used_memory as f32 / 1024.0 / 1024.0 / 1024.0, mem_total_gb,
cpu_profiler: profiler_stats.0 / s.cpu_usage.len() as f32,
mem_profiler_gb: profiler_stats.1 as f32 / 1024.0 / 1024.0 / 1024.0,
} }
}).collect(); }).collect();
let mut target_name = None;
if let Some(tpid) = target_pid {
if let Some(snapshot) = snapshots.first() {
if let Some(p) = snapshot.processes.iter().find(|p| p.pid == tpid) {
target_name = Some(p.name.clone());
}
}
}
let mut pid_map: HashMap<u32, (String, Option<u32>, Vec<ProcessHistoryPoint>, f32, f32, bool, bool)> = HashMap::new(); let mut pid_map: HashMap<u32, (String, Option<u32>, Vec<ProcessHistoryPoint>, f32, f32, bool, bool)> = HashMap::new();
let num_snapshots = snapshots.len() as f32; let num_snapshots = snapshots.len() as f32;
@@ -332,14 +362,82 @@ fn generate_report(start_time: DateTime<Utc>, snapshots: Vec<Snapshot>) -> Repor
final_roots.push(node); final_roots.push(node);
} }
} }
final_roots.sort_by(|a, b| b.inclusive_avg_cpu.partial_cmp(&a.inclusive_avg_cpu).unwrap_or(std::cmp::Ordering::Equal));
Report { if mode == ProfilingMode::Global {
start_time: start_time.to_rfc3339(), // Global mode aggregation: Group by NAME, and flattened
end_time: end_time.to_rfc3339(), let mut name_groups: HashMap<String, AggregatedProcess> = HashMap::new();
duration_seconds: duration,
timeline, fn flatten_to_groups(node: AggregatedProcess, groups: &mut HashMap<String, AggregatedProcess>) {
aggregated_processes: final_roots, let entry = groups.entry(node.name.clone()).or_insert_with(|| {
let mut base = node.clone();
base.children = Vec::new();
base.instance_count = 0;
base.inclusive_avg_cpu = 0.0;
base.inclusive_avg_memory_mb = 0.0;
base.avg_cpu = 0.0;
base.avg_memory_mb = 0.0;
base
});
entry.avg_cpu += node.avg_cpu;
entry.avg_memory_mb += node.avg_memory_mb;
entry.instance_count += 1;
if node.peak_cpu > entry.peak_cpu { entry.peak_cpu = node.peak_cpu; }
if node.peak_memory_mb > entry.peak_memory_mb { entry.peak_memory_mb = node.peak_memory_mb; }
for child in node.children {
flatten_to_groups(child, groups);
}
}
for root in final_roots {
flatten_to_groups(root, &mut name_groups);
}
let mut flattened: Vec<AggregatedProcess> = name_groups.into_values().collect();
flattened.sort_by(|a, b| b.avg_cpu.partial_cmp(&a.avg_cpu).unwrap_or(std::cmp::Ordering::Equal));
Report {
start_time: start_time.to_rfc3339(),
end_time: end_time.to_rfc3339(),
duration_seconds: duration,
mode,
target_name,
timeline,
aggregated_processes: flattened,
}
} else {
// Targeted mode: Return only the target root(s) and their hierarchy
let mut targeted_roots = Vec::new();
if let Some(tpid) = target_pid {
// Find target or its closest surviving ancestor/child in the tree
// Actually final_roots already contains the full tree.
// We want only the root that contains our target PID.
fn find_target_node(roots: &mut Vec<AggregatedProcess>, tpid: u32) -> Option<AggregatedProcess> {
for i in 0..roots.len() {
if roots[i].pid == tpid {
return Some(roots.remove(i));
}
if let Some(found) = find_target_node(&mut roots[i].children, tpid) {
return Some(found);
}
}
None
}
if let Some(target_tree) = find_target_node(&mut final_roots, tpid) {
targeted_roots.push(target_tree);
}
}
Report {
start_time: start_time.to_rfc3339(),
end_time: end_time.to_rfc3339(),
duration_seconds: duration,
mode,
target_name,
timeline,
aggregated_processes: targeted_roots,
}
} }
} }
@@ -354,8 +452,6 @@ fn get_system_stats(state: State<AppState>, minimal: bool) -> SystemStats {
let self_pid = std::process::id(); let self_pid = std::process::id();
let syspulse_pids = get_syspulse_pids(self_pid, &sys); let syspulse_pids = get_syspulse_pids(self_pid, &sys);
// NO PSS collection during live dashboard. ONLY during profiling or after.
// This is the primary cause of high CPU/I/O.
let snapshot = collect_snapshot(&mut sys, &syspulse_pids, &mut pss_cache, profiling.is_active); let snapshot = collect_snapshot(&mut sys, &syspulse_pids, &mut pss_cache, profiling.is_active);
if profiling.is_active { if profiling.is_active {
@@ -400,6 +496,18 @@ fn save_report(report: Report) -> Result<String, String> {
fn start_profiling(state: State<AppState>) { fn start_profiling(state: State<AppState>) {
let mut profiling = state.profiling.lock().unwrap(); let mut profiling = state.profiling.lock().unwrap();
profiling.is_active = true; profiling.is_active = true;
profiling.mode = ProfilingMode::Global;
profiling.target_pid = None;
profiling.start_time = Some(Utc::now());
profiling.snapshots.clear();
}
#[tauri::command]
fn start_targeted_profiling(state: State<AppState>, pid: u32) {
let mut profiling = state.profiling.lock().unwrap();
profiling.is_active = true;
profiling.mode = ProfilingMode::Targeted;
profiling.target_pid = Some(pid);
profiling.start_time = Some(Utc::now()); profiling.start_time = Some(Utc::now());
profiling.snapshots.clear(); profiling.snapshots.clear();
} }
@@ -409,7 +517,7 @@ fn stop_profiling(state: State<AppState>) -> Report {
let mut profiling = state.profiling.lock().unwrap(); let mut profiling = state.profiling.lock().unwrap();
profiling.is_active = false; profiling.is_active = false;
let snapshots: Vec<Snapshot> = profiling.snapshots.drain(..).collect(); let snapshots: Vec<Snapshot> = profiling.snapshots.drain(..).collect();
generate_report(profiling.start_time.unwrap_or(Utc::now()), snapshots) generate_report(profiling.start_time.unwrap_or(Utc::now()), snapshots, profiling.mode, profiling.target_pid)
} }
#[tauri::command] #[tauri::command]
@@ -456,7 +564,7 @@ fn main() {
} }
} }
let report = generate_report(start_time, snapshots); let report = generate_report(start_time, snapshots, ProfilingMode::Global, None);
let json = serde_json::to_string_pretty(&report).unwrap(); let json = serde_json::to_string_pretty(&report).unwrap();
let out_path = cli.output.unwrap_or_else(|| PathBuf::from(format!("syspulse_report_{}.json", Utc::now().format("%Y%m%d_%H%M%S")))); let out_path = cli.output.unwrap_or_else(|| PathBuf::from(format!("syspulse_report_{}.json", Utc::now().format("%Y%m%d_%H%M%S"))));
fs::write(&out_path, json).expect("Failed to write report"); fs::write(&out_path, json).expect("Failed to write report");
@@ -474,6 +582,8 @@ fn main() {
sys: Mutex::new(System::new_all()), sys: Mutex::new(System::new_all()),
profiling: Mutex::new(ProfilingSession { profiling: Mutex::new(ProfilingSession {
is_active: false, is_active: false,
mode: ProfilingMode::Global,
target_pid: None,
start_time: None, start_time: None,
snapshots: Vec::new(), snapshots: Vec::new(),
}), }),
@@ -484,6 +594,7 @@ fn main() {
get_system_stats, get_system_stats,
get_initial_report, get_initial_report,
start_profiling, start_profiling,
start_targeted_profiling,
stop_profiling, stop_profiling,
run_as_admin, run_as_admin,
save_report save_report

View File

@@ -6,13 +6,18 @@ import {
} from 'recharts'; } from 'recharts';
import { import {
Activity, Cpu, Server, Database, Play, Square, Activity, Cpu, Server, Database, Play, Square,
AlertTriangle, ArrowLeft, Shield, Save, X, Download, CheckSquare AlertTriangle, ArrowLeft, Shield, Save, X, Download, CheckSquare, Target, Eye, EyeOff
} from 'lucide-react'; } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
// --- Types --- // --- Types ---
enum ProfilingMode {
Global = "Global",
Targeted = "Targeted"
}
interface ProcessStats { interface ProcessStats {
pid: number; pid: number;
name: string; name: string;
@@ -34,8 +39,10 @@ interface SystemStats {
interface TimelinePoint { interface TimelinePoint {
time: string; time: string;
avg_cpu: number; cpu_total: number;
memory_gb: number; mem_total_gb: number;
cpu_profiler: number;
mem_profiler_gb: number;
} }
interface ProcessHistoryPoint { interface ProcessHistoryPoint {
@@ -64,6 +71,8 @@ interface ProfilingReport {
start_time: string; start_time: string;
end_time: string; end_time: string;
duration_seconds: number; duration_seconds: number;
mode: ProfilingMode;
target_name?: string;
timeline: TimelinePoint[]; timeline: TimelinePoint[];
aggregated_processes: AggregatedProcess[]; aggregated_processes: AggregatedProcess[];
} }
@@ -157,6 +166,10 @@ function App() {
} }
}; };
const startTargeted = async (pid: number) => {
await invoke('start_targeted_profiling', { pid });
};
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
@@ -246,18 +259,18 @@ function App() {
<p className="text-red font-mono text-2xl font-black tabular-nums">{formatDuration(stats.recording_duration)}</p> <p className="text-red font-mono text-2xl font-black tabular-nums">{formatDuration(stats.recording_duration)}</p>
</div> </div>
<div className="grid grid-cols-2 gap-4 w-full max-w-md"> <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"> <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-[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 className="text-3xl font-black text-blue tabular-nums">{avgCpu.toFixed(1)}%</div>
</div> </div>
<div className="bg-surface0 p-6 rounded-[1.5rem] border border-surface1 shadow-xl"> <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-[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 className="text-3xl font-black text-mauve tabular-nums">{memoryPercent.toFixed(1)}%</div>
</div> </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"> <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" /> <Shield size={14} className="text-green" />
<span>Minimal footprint mode enabled</span> <span>Performance optimized snapshot mode enabled</span>
</div> </div>
</div> </div>
) : ( ) : (
@@ -287,13 +300,13 @@ function App() {
<div className="card bg-gradient-to-b from-surface0 to-base/50"> <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} /> Memory</div>
<div className="flex items-end gap-2"> <div className="flex items-end gap-2">
<span className="stat-value text-mauve tabular-nums">{(stats.used_memory / 1024 / 1024 / 1024).toFixed(1)}</span> <span className="stat-value text-mauve tabular-nums">{formatBytes(stats.used_memory)}</span>
<span className="text-subtext0 font-black mb-2">GB used</span> <span className="text-subtext0 font-black mb-2">used</span>
</div> </div>
<div className="mt-6"> <div className="mt-6">
<div className="flex justify-between text-[10px] font-black text-overlay2 uppercase mb-2"> <div className="flex justify-between text-[10px] font-black text-overlay2 uppercase mb-2">
<span>{memoryPercent.toFixed(1)}% Utilized</span> <span>{memoryPercent.toFixed(1)}% Utilized</span>
<span>{(stats.total_memory / 1024 / 1024 / 1024).toFixed(0)} GB Total</span> <span>{formatBytes(stats.total_memory, 0)} Total</span>
</div> </div>
<div className="h-3 bg-surface1/50 rounded-full overflow-hidden border border-surface2"> <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 className="h-full bg-gradient-to-r from-mauve to-pink transition-all duration-700 shadow-lg shadow-mauve/20" style={{ width: `${memoryPercent}%` }} />
@@ -308,7 +321,7 @@ function App() {
<span className="text-subtext0 font-black mb-2">Processes</span> <span className="text-subtext0 font-black mb-2">Processes</span>
</div> </div>
<div className="mt-4 text-xs text-subtext1 font-medium leading-relaxed bg-base/30 p-3 rounded-xl border border-surface1"> <div className="mt-4 text-xs text-subtext1 font-medium leading-relaxed bg-base/30 p-3 rounded-xl border border-surface1">
Live system feed. Profiling session will provide a full hierarchical tree. Live system feed. Target a process to profile its hierarchy in detail.
</div> </div>
</div> </div>
</div> </div>
@@ -325,17 +338,17 @@ function App() {
</div> </div>
</div> </div>
<div className="grid grid-cols-[1fr_100px_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_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>Process</div>
<div className="text-right">PID</div> <div className="text-right">PID</div>
<div className="text-right">CPU %</div> <div className="text-right">CPU %</div>
<div className="text-right">Memory</div> <div className="text-right">Memory</div>
<div className="text-center">Action</div> <div className="text-center">Actions</div>
</div> </div>
<div className="overflow-y-auto flex-1 custom-scrollbar px-2"> <div className="overflow-y-auto flex-1 custom-scrollbar px-2">
{stats.processes.map((proc) => ( {stats.processes.map((proc) => (
<div key={proc.pid} className="grid grid-cols-[1fr_100px_100px_120px_80px] gap-4 px-4 py-4 border-b border-surface1/20 group hover:bg-surface1/20 transition-all rounded-xl"> <div key={proc.pid} className="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">
<div className="font-black text-text truncate text-sm flex items-center gap-3" title={proc.name}> <div className="font-black text-text truncate text-sm flex items-center gap-3" title={proc.name}>
<div className={cn( <div className={cn(
"w-2 h-2 rounded-full transition-colors", "w-2 h-2 rounded-full transition-colors",
@@ -343,17 +356,24 @@ function App() {
)} /> )} />
{proc.name} {proc.name}
</div> </div>
<div className="font-mono text-overlay2 text-xs text-right self-center tabular-nums">{proc.pid}</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-blue font-black text-sm text-right self-center tabular-nums">{proc.cpu_usage.toFixed(1)}%</div>
<div className="text-mauve font-black text-sm text-right self-center tabular-nums">{(proc.memory / 1024 / 1024).toFixed(0)} MB</div> <div className="text-mauve font-black text-sm text-right self-center tabular-nums">{formatBytes(proc.memory, 0)}</div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center gap-2">
<button <button
onClick={() => killProcess(proc.pid)} onClick={() => startTargeted(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" 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="Terminate Process" title="Target Profile"
> >
<Shield size={16} strokeWidth={2.5} /> <Target size={16} strokeWidth={2.5} />
</button> </button>
<button
onClick={() => 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>
))} ))}
@@ -366,10 +386,10 @@ function App() {
); );
} }
type SortField = 'name' | 'pid' | 'inclusive_avg_cpu' | 'peak_cpu' | 'inclusive_avg_memory_mb' | 'peak_memory_mb'; 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 }) { function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () => void }) {
const [sortField, setSortField] = useState<SortField>('inclusive_avg_cpu'); const [sortField, setSortField] = useState<SortField>(report.mode === ProfilingMode.Targeted ? 'inclusive_avg_cpu' : 'avg_cpu');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [selectedProcess, setSelectedProcess] = useState<AggregatedProcess | null>(null); const [selectedProcess, setSelectedProcess] = useState<AggregatedProcess | null>(null);
const [hideProfiler, setHideProfiler] = useState(true); const [hideProfiler, setHideProfiler] = useState(true);
@@ -391,9 +411,12 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
} }
}; };
const filteredProcesses = useMemo(() => {
return report.aggregated_processes.filter(p => !hideProfiler || !p.is_syspulse);
}, [report, hideProfiler]);
const sortedProcesses = useMemo(() => { const sortedProcesses = useMemo(() => {
return [...report.aggregated_processes] return [...filteredProcesses]
.filter(p => !hideProfiler || !p.is_syspulse)
.sort((a, b) => { .sort((a, b) => {
const valA = a[sortField as keyof AggregatedProcess]; const valA = a[sortField as keyof AggregatedProcess];
const valB = b[sortField as keyof AggregatedProcess]; const valB = b[sortField as keyof AggregatedProcess];
@@ -407,7 +430,21 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
return sortOrder === 'asc' ? numA - numB : numB - numA; return sortOrder === 'asc' ? numA - numB : numB - numA;
}); });
}, [report, sortField, sortOrder, hideProfiler]); }, [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(() => {
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]);
const saveReport = async () => { const saveReport = async () => {
try { try {
@@ -495,7 +532,9 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<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"> <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} /> <Activity size={16} className="text-base" strokeWidth={3} />
</div> </div>
<span className="font-black tracking-tighter uppercase italic text-xl">Profiling Report</span> <span className="font-black tracking-tighter uppercase italic text-xl">
{report.mode === ProfilingMode.Targeted ? `Target: ${report.target_name || "Process"}` : "Global Report"}
</span>
</div> </div>
<div className="flex gap-4 px-4"> <div className="flex gap-4 px-4">
<button <button
@@ -507,7 +546,7 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
: "bg-surface1/50 border-surface2 text-subtext0 hover:text-text" : "bg-surface1/50 border-surface2 text-subtext0 hover:text-text"
)} )}
> >
<Shield size={14} /> {hideProfiler ? "SHOW PROFILER" : "HIDE PROFILER"} {hideProfiler ? <EyeOff size={14} /> : <Eye size={14} />} {hideProfiler ? "SHOW PROFILER" : "HIDE PROFILER"}
</button> </button>
<button <button
onClick={saveReport} onClick={saveReport}
@@ -531,17 +570,17 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="text-2xl font-black text-text font-mono tabular-nums">{formatDuration(report.duration_seconds)}</div> <div className="text-2xl font-black text-text font-mono tabular-nums">{formatDuration(report.duration_seconds)}</div>
</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="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">Session End</div> <div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Impact Avg CPU</div>
<div className="text-xl font-bold text-text tabular-nums">{new Date(report.end_time).toLocaleTimeString()}</div> <div className="text-2xl font-black text-blue tabular-nums">{summaryStats.cpu.toFixed(1)}%</div>
</div> </div>
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-green/30 transition-colors"> <div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-green/30 transition-colors">
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Root Processes</div> <div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Impact Avg RAM</div>
<div className="text-2xl font-black text-blue tabular-nums">{report.aggregated_processes.length}</div> <div className="text-2xl font-black text-mauve tabular-nums">{formatMB(summaryStats.mem)}</div>
</div> </div>
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-red/30 transition-colors"> <div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-xl text-center group hover:border-red/30 transition-colors">
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-1">Issue Alerts</div> <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"> <div className="text-2xl font-black text-red tabular-nums">
{report.aggregated_processes.filter(p => p.warnings.length > 0).length} {filteredProcesses.filter(p => p.warnings.length > 0).length}
</div> </div>
</div> </div>
</div> </div>
@@ -549,12 +588,12 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="card h-72 border border-surface1/50 shadow-2xl bg-gradient-to-b from-surface0 to-base/50"> <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"> <div className="flex justify-between items-center mb-4">
<h3 className="text-sm font-black uppercase italic tracking-widest flex items-center gap-2"> <h3 className="text-sm font-black uppercase italic tracking-widest flex items-center gap-2">
<Activity size={16} className="text-blue" strokeWidth={3} /> Session Load Profile <Activity size={16} className="text-blue" strokeWidth={3} /> {hideProfiler ? "Reactive Analysis Profile (Profiler Hidden)" : "Full System Load Profile"}
</h3> </h3>
</div> </div>
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={report.timeline}> <AreaChart data={reactiveTimeline}>
<defs> <defs>
<linearGradient id="cpuReportGradient" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="cpuReportGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--blue)" stopOpacity={0.6}/> <stop offset="5%" stopColor="var(--blue)" stopOpacity={0.6}/>
@@ -568,13 +607,13 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
contentStyle={{ background: 'var(--mantle)', border: '1px solid var(--surface1)', borderRadius: '12px', fontSize: '11px', fontWeight: 'bold' }} contentStyle={{ background: 'var(--mantle)', border: '1px solid var(--surface1)', borderRadius: '12px', fontSize: '11px', fontWeight: 'bold' }}
itemStyle={{ color: 'var(--text)' }} itemStyle={{ color: 'var(--text)' }}
formatter={(value: number, name: string) => { formatter={(value: number, name: string) => {
if (name === 'MEM GB') return [formatBytes(value * 1024 * 1024 * 1024), 'RAM']; if (name === 'mem') return [formatBytes(value * 1024 * 1024 * 1024), 'RAM'];
if (name === 'CPU %') return [`${value.toFixed(1)}%`, 'CPU']; if (name === 'cpu') return [`${value.toFixed(1)}%`, 'CPU'];
return [value, name]; return [value, name];
}} }}
/> />
<Area type="monotone" dataKey="avg_cpu" name="CPU %" stroke="var(--blue)" fill="url(#cpuReportGradient)" strokeWidth={3} isAnimationActive={true} /> <Area type="monotone" dataKey="cpu" name="CPU %" stroke="var(--blue)" fill="url(#cpuReportGradient)" strokeWidth={3} isAnimationActive={true} />
<Area type="monotone" dataKey="memory_gb" name="MEM GB" stroke="var(--mauve)" fill="none" strokeWidth={2} /> <Area type="monotone" dataKey="mem" name="MEM GB" stroke="var(--mauve)" fill="none" strokeWidth={2} />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@@ -583,13 +622,17 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="card flex-1 flex flex-col min-h-0 border border-surface1/50 shadow-[0_35px_60px_-15px_rgba(0,0,0,0.5)] overflow-hidden"> <div className="card flex-1 flex flex-col min-h-0 border border-surface1/50 shadow-[0_35px_60px_-15px_rgba(0,0,0,0.5)] overflow-hidden">
<div className="flex justify-between items-center mb-6 px-2"> <div className="flex justify-between items-center mb-6 px-2">
<h3 className="text-xl font-black tracking-tighter uppercase italic flex items-center gap-2"> <h3 className="text-xl font-black tracking-tighter uppercase italic flex items-center gap-2">
<Server size={24} className="text-green" strokeWidth={3} /> Hierarchical Matrix <Server size={24} className="text-green" strokeWidth={3} /> {report.mode === ProfilingMode.Targeted ? "Hierarchical Analysis" : "Aggregated Resource Matrix"}
</h3> </h3>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-1.5 text-[9px] font-bold text-overlay1 uppercase"> {report.mode === ProfilingMode.Targeted && (
<div className="w-2 h-2 rounded-full bg-blue/40" /> Inclusive Sum <div className="flex items-center gap-1.5 text-[9px] font-bold text-overlay1 uppercase">
</div> <div className="w-2 h-2 rounded-full bg-blue/40" /> Inclusive Sum
<span className="text-[10px] bg-surface1 px-3 py-1.5 rounded-full font-black text-overlay1 uppercase tracking-widest">Toggle Nodes to Expand</span> </div>
)}
<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> </div>
@@ -597,21 +640,51 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="cursor-pointer hover:text-text transition-colors flex items-center gap-1" onClick={() => handleSort('name')}> <div className="cursor-pointer hover:text-text transition-colors flex items-center gap-1" onClick={() => handleSort('name')}>
Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')} Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
</div> </div>
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('pid')}>PID</div> <div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('pid')}>
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('inclusive_avg_cpu')}>Total CPU</div> {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('peak_cpu')}>Peak</div>
<div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('inclusive_avg_memory_mb')}>Total Mem</div> <div className="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort(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="text-right cursor-pointer hover:text-text transition-colors" onClick={() => handleSort('peak_memory_mb')}>Peak</div>
<div className="pl-4">Insights</div> <div className="pl-4">Insights</div>
</div> </div>
<div className="overflow-y-auto flex-1 custom-scrollbar px-2"> <div className="overflow-y-auto flex-1 custom-scrollbar px-2">
{renderTreeRows(sortedProcesses)} {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>
<div className="p-4 bg-surface0/50 border-t border-surface1 text-[9px] text-overlay1 font-medium italic flex items-center gap-2"> <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" /> <Shield size={10} className="text-blue" />
<span>Memory is Proportional Set Size (PSS). It includes private memory plus a proportional share of shared libraries, providing an accurate sum of total system impact.</span> <span>Real Memory (PSS) used for accuracy. Global mode aggregates processes by application name for performance.</span>
</div> </div>
</div> </div>
@@ -621,7 +694,9 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="w-full max-w-2xl h-full bg-mantle rounded-3xl border border-surface1 shadow-3xl flex flex-col overflow-hidden"> <div className="w-full max-w-2xl h-full bg-mantle rounded-3xl border border-surface1 shadow-3xl flex flex-col overflow-hidden">
<div className="p-8 border-b border-surface1 flex justify-between items-start shrink-0"> <div className="p-8 border-b border-surface1 flex justify-between items-start shrink-0">
<div> <div>
<div className="text-xs font-black text-blue uppercase tracking-widest mb-1">Process Inspector (PID: {selectedProcess.pid})</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> <h2 className="text-3xl font-black text-text tracking-tighter uppercase italic truncate max-w-md">{selectedProcess.name}</h2>
</div> </div>
<button <button
@@ -636,18 +711,22 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg"> <div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg">
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-2">Total Avg CPU</div> <div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-2">Total Avg CPU</div>
<div className="text-4xl font-black text-text tabular-nums">{selectedProcess.inclusive_avg_cpu.toFixed(1)}%</div> <div className="text-4xl font-black text-text tabular-nums">
<p className="text-xs text-subtext1 mt-2">Sum of this process and all its children.</p> {(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>
<div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg"> <div className="bg-surface0 p-6 rounded-2xl border border-surface1 shadow-lg">
<div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-2">Total Avg RAM</div> <div className="text-[10px] font-black text-overlay2 uppercase tracking-widest mb-2">Total Avg RAM</div>
<div className="text-4xl font-black text-mauve tabular-nums">{formatMB(selectedProcess.inclusive_avg_memory_mb)}</div> <div className="text-4xl font-black text-mauve tabular-nums">
<p className="text-xs text-subtext1 mt-2">Combined RSS memory of the subtree.</p> {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> </div>
<div className="card h-64 shrink-0 bg-base border border-surface1"> <div className="card h-64 shrink-0 bg-base border border-surface1">
<div className="card-title mb-4">Self-Resource History (Exclusive)</div> <div className="card-title mb-4">Resource History (Summed)</div>
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={selectedProcess.history}> <AreaChart data={selectedProcess.history}>
@@ -667,32 +746,36 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
<Tooltip <Tooltip
contentStyle={{ background: 'var(--crust)', border: '1px solid var(--surface1)', borderRadius: '12px' }} contentStyle={{ background: 'var(--crust)', border: '1px solid var(--surface1)', borderRadius: '12px' }}
formatter={(value: number, name: string) => { formatter={(value: number, name: string) => {
if (name === 'Self Mem (MB)') return [formatMB(value), 'RAM (Self)']; if (name === 'memory_mb') return [formatMB(value), 'RAM'];
if (name === 'Self CPU %') return [`${value.toFixed(1)}%`, 'CPU (Self)']; if (name === 'cpu_usage') return [`${value.toFixed(1)}%`, 'CPU'];
return [value, name]; return [value, name];
}} }}
/> />
<Area type="monotone" dataKey="cpu_usage" name="Self CPU %" stroke="var(--blue)" fill="url(#procCpuGrad)" strokeWidth={3} /> <Area type="monotone" dataKey="cpu_usage" name="CPU %" stroke="var(--blue)" fill="url(#procCpuGrad)" strokeWidth={3} />
<Area type="monotone" dataKey="memory_mb" name="Self Mem (MB)" stroke="var(--mauve)" fill="url(#procMemGrad)" strokeWidth={2} /> <Area type="monotone" dataKey="memory_mb" name="Mem (PSS)" stroke="var(--mauve)" fill="url(#procMemGrad)" strokeWidth={2} />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-xs font-black text-overlay2 uppercase tracking-widest">Process Details</h4> <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="bg-surface0 border border-surface1 rounded-2xl p-6 space-y-3">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-subtext1">Peak Self CPU</span> <span className="text-subtext1">Peak Recorded Load</span>
<span className="font-black text-blue">{selectedProcess.peak_cpu.toFixed(1)}%</span> <span className="font-black text-blue">{selectedProcess.peak_cpu.toFixed(1)}%</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-subtext1">Peak Self RAM</span> <span className="text-subtext1">Peak Recorded RAM</span>
<span className="font-black text-mauve">{formatMB(selectedProcess.peak_memory_mb)}</span> <span className="font-black text-mauve">{formatMB(selectedProcess.peak_memory_mb)}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-subtext1">Child Process Count</span> <span className="text-subtext1">
<span className="font-black text-green">{selectedProcess.children.length}</span> {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>
</div> </div>
@@ -708,7 +791,7 @@ function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () =>
)) : ( )) : (
<div className="bg-green/10 border border-green/20 p-4 rounded-xl flex items-center gap-3 text-green"> <div className="bg-green/10 border border-green/20 p-4 rounded-xl flex items-center gap-3 text-green">
<CheckSquare size={20} /> <CheckSquare size={20} />
<span className="font-bold text-sm uppercase italic tracking-tight">Optimal Performance Profile</span> <span className="font-bold text-sm uppercase italic tracking-tight">Healthy Performance Profile</span>
</div> </div>
)} )}
</div> </div>