From ec142b09bdd86e407f2a5653df64f5537799d40b Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 22 Feb 2026 17:39:34 +0100 Subject: [PATCH] feat: initialize SysPulse-rs profiler project with Tauri v2 and React --- .gitignore | 41 +++ README.md | 79 +++++ index.html | 12 + package.json | 33 ++ postcss.config.js | 6 + src-tauri/Cargo.toml | 19 ++ src-tauri/build.rs | 3 + src-tauri/icons/128x128.png | Bin 0 -> 15875 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 15875 bytes src-tauri/icons/32x32.png | Bin 0 -> 15875 bytes src-tauri/icons/icon.png | Bin 0 -> 15875 bytes src-tauri/src/main.rs | 343 +++++++++++++++++++++ src-tauri/tauri.conf.json | 37 +++ src/App.tsx | 536 +++++++++++++++++++++++++++++++++ src/index.css | 144 +++++++++ src/main.tsx | 10 + tailwind.config.js | 40 +++ tsconfig.json | 21 ++ tsconfig.node.json | 10 + vite.config.ts | 18 ++ 20 files changed, 1352 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.html create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src/App.tsx create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a1209b --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Node.js +node_modules/ +dist/ +dist-ssr/ +*.local +.npm +.eslintcache +.stylelintcache +.node_repl_history +package-lock.json + +# Rust +target/ +src-tauri/target/ +Cargo.lock +src-tauri/Cargo.lock + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store + +# Environment +.env +.env.* + +# Tauri v2 +src-tauri/gen/ +src-tauri/target/ +src-tauri/icons/*.ico +src-tauri/icons/*.icns +src-tauri/icons/*.png +!src-tauri/icons/icon.png +!src-tauri/icons/32x32.png +!src-tauri/icons/128x128.png +!src-tauri/icons/128x128@2x.png + +# Reports (SysPulse specific) +syspulse_report_*.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..52ff3cb --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# ⚡ SysPulse + +A professional, high-performance Linux system profiler built with **Rust** and **Tauri v2**. Monitor your Wayland system's performance, identify resource-hungry processes, and generate detailed profiling reports with a beautiful **Catppuccin Mocha** interface. + +--- + +## 🚀 Usage + +1. **Live Dashboard**: Monitor real-time CPU and Memory load. Use the "Hide SysPulse" toggle to exclude the profiler's own overhead from the results. +2. **Recording**: Click **Record Profile** to start a session. The app automatically switches to a **Minimal Footprint Mode** to ensure the most accurate results by reducing UI overhead. +3. **Analysis**: Stop the recording to view a comprehensive **Profiling Report**. +4. **Inspection**: Click any process in the report matrix to open the **Process Inspector**, showing a dedicated time-series graph of that application's resource consumption throughout the session. +5. **Admin Control**: Hover over any process and click the **Shield** icon to terminate it (uses `pkexec` for secure sudo authentication). + +--- + +## 🏗️ Architecture + +- **Backend**: Rust (Tauri v2) + - Uses the `sysinfo` crate for low-level system data retrieval. + - Implements an asynchronous snapshot system to capture performance data without blocking. + - Provides secure process management via Linux `pkexec`. +- **Frontend**: React + TypeScript + Tailwind CSS + - **Recharts**: For high-performance time-series visualization. + - **Lucide-React**: For a clean, modern icon system. + - **Framer Motion**: For smooth, native-feeling transitions. +- **Styling**: Catppuccin Mocha theme for a professional, low-strain developer experience. + +--- + +## 🛠️ Development Setup + +### Prerequisites + +Ensure you have the following system dependencies installed (Ubuntu/Debian example): + +```bash +sudo apt update +sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev +``` + +### Installation + +1. **Clone the repository**: + ```bash + git clone https://github.com/your-username/syspulse-rs.git + cd syspulse-rs + ``` + +2. **Install Node dependencies**: + ```bash + npm install + ``` + +3. **Start Development Mode**: + ```bash + npm run tauri dev + ``` + +--- + +## 📦 Build Instructions + +To generate a production-ready binary and system packages (Debian, RPM, AppImage): + +```bash +npm run tauri build +``` + +The output will be located in: +`src-tauri/target/release/bundle/` + +*Note: Building an AppImage requires `appimagetool` and `squashfs-tools` to be installed on your system.* + +--- + +## 🛡️ License + +This project is open-source. See the [LICENSE](LICENSE) file for details. diff --git a/index.html b/index.html new file mode 100644 index 0000000..7908bb9 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + SysPulse + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..fc726ca --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "syspulse", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "2.1.0", + "lucide-react": "^0.300.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "recharts": "^2.10.0", + "framer-motion": "^10.16.0", + "clsx": "^2.0.0", + "tailwind-merge": "^2.0.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "typescript": "^5.2.0", + "vite": "^5.0.0", + "@tauri-apps/cli": "2.1.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..601914b --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "syspulse-rs" +version = "0.1.0" +description = "A professional Linux system profiler" +authors = ["narl"] +edition = "2021" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sysinfo = "0.30" +chrono = "0.4" +tokio = { version = "1.0", features = ["full"] } +log = "0.4" +env_logger = "0.10" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..795b9b7 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..327b53894cdd640939e4a230a86d61148812ccd8 GIT binary patch literal 15875 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 Z{|95z^%}P1kA46E002ovPDHLkV1inr@0tJr literal 0 HcmV?d00001 diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..327b53894cdd640939e4a230a86d61148812ccd8 GIT binary patch literal 15875 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 Z{|95z^%}P1kA46E002ovPDHLkV1inr@0tJr literal 0 HcmV?d00001 diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..327b53894cdd640939e4a230a86d61148812ccd8 GIT binary patch literal 15875 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 Z{|95z^%}P1kA46E002ovPDHLkV1inr@0tJr literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..327b53894cdd640939e4a230a86d61148812ccd8 GIT binary patch literal 15875 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 Z{|95z^%}P1kA46E002ovPDHLkV1inr@0tJr literal 0 HcmV?d00001 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..ec5505d --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,343 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use sysinfo::System; +use std::sync::Mutex; +use std::process::Command; +use tauri::State; +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; +use chrono::{DateTime, Utc}; + +// --- Data Structures --- + +#[derive(Serialize, Clone)] +struct SystemStats { + cpu_usage: Vec, + total_memory: u64, + used_memory: u64, + processes: Vec, + is_recording: bool, + recording_duration: u64, // seconds +} + +#[derive(Serialize, Clone, Debug)] +struct ProcessStats { + pid: u32, + name: String, + cpu_usage: f32, + memory: u64, + status: String, + user_id: Option, +} + +#[derive(Clone)] +struct Snapshot { + timestamp: DateTime, + cpu_usage: Vec, + used_memory: u64, + processes: Vec, +} + +struct ProfilingSession { + is_active: bool, + start_time: Option>, + snapshots: Vec, +} + +struct AppState { + sys: Mutex, + profiling: Mutex, +} + +// --- Report Structures --- + +#[derive(Serialize, Deserialize)] +struct Report { + start_time: String, + end_time: String, + duration_seconds: i64, + timeline: Vec, + aggregated_processes: Vec, +} + +#[derive(Serialize, Deserialize)] +struct TimelinePoint { + time: String, + avg_cpu: f32, + memory_gb: f32, +} + +#[derive(Serialize, Deserialize)] +struct ProcessHistoryPoint { + time: String, + cpu_usage: f32, + memory_mb: f32, +} + +#[derive(Serialize, Deserialize)] +struct AggregatedProcess { + name: String, + avg_cpu: f32, + peak_cpu: f32, + avg_memory_mb: f32, + peak_memory_mb: f32, + instance_count: usize, + warnings: Vec, + history: Vec, +} + +// --- Commands --- + +#[tauri::command] +fn get_system_stats( + state: State, + exclude_self: bool, + minimal: bool +) -> SystemStats { + let mut sys = state.sys.lock().unwrap(); + let mut profiling = state.profiling.lock().unwrap(); + + if minimal { + sys.refresh_cpu(); + sys.refresh_memory(); + } else { + sys.refresh_all(); + } + + let self_pid = std::process::id(); + let cpu_usage: Vec = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(); + let total_memory = sys.total_memory(); + let used_memory = sys.used_memory(); + + let mut processes: Vec = if minimal && !profiling.is_active { + Vec::new() + } else { + sys.processes().iter() + .filter(|(pid, _)| !exclude_self || pid.as_u32() != self_pid) + .map(|(pid, process)| { + ProcessStats { + pid: pid.as_u32(), + name: process.name().to_string(), + cpu_usage: process.cpu_usage(), + memory: process.memory(), + status: format!("{:?}", process.status()), + user_id: process.user_id().map(|uid| uid.to_string()), + } + }).collect() + }; + + if profiling.is_active { + // Even in minimal mode, if recording we need the processes for the report + if minimal { + sys.refresh_processes(); + processes = sys.processes().iter() + .filter(|(pid, _)| !exclude_self || pid.as_u32() != self_pid) + .map(|(pid, process)| { + ProcessStats { + pid: pid.as_u32(), + name: process.name().to_string(), + cpu_usage: process.cpu_usage(), + memory: process.memory(), + status: format!("{:?}", process.status()), + user_id: process.user_id().map(|uid| uid.to_string()), + } + }).collect(); + } + + profiling.snapshots.push(Snapshot { + timestamp: Utc::now(), + cpu_usage: cpu_usage.clone(), + used_memory, + processes: processes.clone(), + }); + } + + let recording_duration = if let Some(start) = profiling.start_time { + (Utc::now() - start).num_seconds() as u64 + } else { + 0 + }; + + if !minimal { + processes.sort_by(|a, b| b.cpu_usage.partial_cmp(&a.cpu_usage).unwrap_or(std::cmp::Ordering::Equal)); + processes.truncate(50); + } else if !profiling.is_active { + processes.clear(); + } + + SystemStats { + cpu_usage, + total_memory, + used_memory, + processes, + is_recording: profiling.is_active, + recording_duration, + } +} + +#[tauri::command] +fn save_report(report: Report) -> Result { + let json = serde_json::to_string_pretty(&report).map_err(|e| e.to_string())?; + let path = format!("syspulse_report_{}.json", Utc::now().format("%Y%m%d_%H%M%S")); + std::fs::write(&path, json).map_err(|e| e.to_string())?; + Ok(path) +} + +#[tauri::command] +fn start_profiling(state: State) { + let mut profiling = state.profiling.lock().unwrap(); + profiling.is_active = true; + profiling.start_time = Some(Utc::now()); + profiling.snapshots.clear(); +} + +#[tauri::command] +fn stop_profiling(state: State) -> Report { + let mut profiling = state.profiling.lock().unwrap(); + profiling.is_active = false; + + let start = profiling.start_time.unwrap_or(Utc::now()); + let end = Utc::now(); + let duration = (end - start).num_seconds(); + + // 1. Generate Session Timeline + let timeline: Vec = profiling.snapshots.iter().map(|s| { + let avg_cpu = s.cpu_usage.iter().sum::() / s.cpu_usage.len() as f32; + TimelinePoint { + time: s.timestamp.format("%H:%M:%S").to_string(), + avg_cpu, + memory_gb: s.used_memory as f32 / 1024.0 / 1024.0 / 1024.0, + } + }).collect(); + + // 2. Aggregate Processes over Time + let mut process_map: HashMap> = HashMap::new(); + let mut peak_stats: HashMap = HashMap::new(); // (Peak CPU, Peak Mem) + let mut unique_pids: HashMap> = HashMap::new(); + let mut status_flags: HashMap = HashMap::new(); // Zombie check + + for snapshot in &profiling.snapshots { + let mut snapshot_procs: HashMap = HashMap::new(); + + for proc in &snapshot.processes { + let entry = snapshot_procs.entry(proc.name.clone()).or_default(); + entry.0 += proc.cpu_usage; + entry.1 += proc.memory; + + unique_pids.entry(proc.name.clone()).or_default().insert(proc.pid); + if proc.status.contains("Zombie") { + status_flags.insert(proc.name.clone(), true); + } + } + + // Record history for all processes seen in this snapshot + for (name, (cpu, mem)) in snapshot_procs { + let hist_entry = process_map.entry(name.clone()).or_default(); + let mem_mb = mem as f32 / 1024.0 / 1024.0; + + hist_entry.push(ProcessHistoryPoint { + time: snapshot.timestamp.format("%H:%M:%S").to_string(), + cpu_usage: cpu, + memory_mb: mem_mb, + }); + + let peaks = peak_stats.entry(name).or_insert((0.0, 0.0)); + if cpu > peaks.0 { peaks.0 = cpu; } + if mem_mb > peaks.1 { peaks.1 = mem_mb; } + } + } + + let mut aggregated_processes: Vec = Vec::new(); + let num_snapshots = profiling.snapshots.len() as f32; + + for (name, history) in process_map { + let (peak_cpu, peak_mem) = peak_stats.get(&name).cloned().unwrap_or((0.0, 0.0)); + let count = unique_pids.get(&name).map(|s| s.len()).unwrap_or(0); + + // Average over the whole SESSION (zeros for snapshots where not present) + let total_cpu_sum: f32 = history.iter().map(|h| h.cpu_usage).sum(); + let total_mem_sum: f32 = history.iter().map(|h| h.memory_mb).sum(); + + let avg_cpu = if num_snapshots > 0.0 { total_cpu_sum / num_snapshots } else { 0.0 }; + let avg_mem = if num_snapshots > 0.0 { total_mem_sum / num_snapshots } else { 0.0 }; + + let mut warnings = Vec::new(); + if status_flags.get(&name).cloned().unwrap_or(false) { + warnings.push("Zombie Process Detected".to_string()); + } + if peak_cpu > 80.0 { + warnings.push("High Peak Load".to_string()); + } + if peak_mem > 2048.0 { + warnings.push("Heavy Memory usage".to_string()); + } + + aggregated_processes.push(AggregatedProcess { + name, + avg_cpu, + peak_cpu, + avg_memory_mb: avg_mem, + peak_memory_mb: peak_mem, + instance_count: count, + warnings, + history, + }); + } + + // Sort by Average CPU descending + aggregated_processes.sort_by(|a, b| b.avg_cpu.partial_cmp(&a.avg_cpu).unwrap_or(std::cmp::Ordering::Equal)); + + Report { + start_time: start.to_rfc3339(), + end_time: end.to_rfc3339(), + duration_seconds: duration, + timeline, + aggregated_processes, + } +} + +#[tauri::command] +fn run_as_admin(command: String) -> Result { + // Uses pkexec to run a command as root. + // CAUTION: This is a simple implementation. In production, validate inputs carefully. + // Splitting command for safety is hard, so we assume 'command' is a simple executable name or safe string. + + // Example usage from frontend: "kill -9 1234" -> pkexec kill -9 1234 + + let output = Command::new("pkexec") + .arg("sh") + .arg("-c") + .arg(&command) + .output() + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } +} + +fn main() { + tauri::Builder::default() + .manage(AppState { + sys: Mutex::new(System::new_all()), + profiling: Mutex::new(ProfilingSession { + is_active: false, + start_time: None, + snapshots: Vec::new(), + }) + }) + .invoke_handler(tauri::generate_handler![ + get_system_stats, + start_profiling, + stop_profiling, + run_as_admin, + save_report + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..1f5b095 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,37 @@ +{ + "productName": "syspulse", + "version": "0.1.0", + "identifier": "com.syspulse.app", + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "SysPulse", + "width": 1200, + "height": 800, + "resizable": true, + "decorations": false, + "transparent": true + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..af8a315 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,536 @@ +import { useState, useEffect, useMemo } 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, CheckSquare, Square as SquareIcon, Save, X +} from 'lucide-react'; +import { clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +// --- Types --- + +interface ProcessStats { + pid: number; + name: string; + cpu_usage: number; + memory: number; + status: string; + user_id?: string; +} + +interface SystemStats { + cpu_usage: number[]; + total_memory: number; + used_memory: number; + processes: ProcessStats[]; + is_recording: boolean; + recording_duration: number; +} + +interface TimelinePoint { + time: string; + avg_cpu: number; + memory_gb: number; +} + +interface ProcessHistoryPoint { + time: string; + cpu_usage: number; + memory_mb: number; +} + +interface AggregatedProcess { + name: string; + avg_cpu: number; + peak_cpu: number; + avg_memory_mb: number; + peak_memory_mb: number; + instance_count: number; + warnings: string[]; + history: ProcessHistoryPoint[]; +} + +interface ProfilingReport { + start_time: string; + end_time: string; + duration_seconds: number; + timeline: TimelinePoint[]; + aggregated_processes: AggregatedProcess[]; +} + +function cn(...inputs: (string | undefined | null | false)[]) { + return twMerge(clsx(inputs)); +} + +function App() { + const [view, setView] = useState<'dashboard' | 'report'>('dashboard'); + const [stats, setStats] = useState(null); + const [history, setHistory] = useState<{ time: string; cpu: number }[]>([]); + const [excludeSelf, setExcludeSelf] = useState(true); + const [report, setReport] = useState(null); + + useEffect(() => { + const fetchStats = async () => { + try { + const isRecording = stats?.is_recording ?? false; + const data = await invoke('get_system_stats', { + excludeSelf, + minimal: isRecording || view === 'report' + }); + setStats(data); + + const avgCpu = data.cpu_usage.reduce((a, b) => a + b, 0) / data.cpu_usage.length; + + setHistory(prev => { + const newHistory = [...prev, { time: new Date().toLocaleTimeString(), cpu: avgCpu }]; + if (newHistory.length > 60) newHistory.shift(); + return newHistory; + }); + } catch (e) { + console.error(e); + } + }; + + fetchStats(); + const interval = setInterval(fetchStats, 1000); + return () => clearInterval(interval); + }, [excludeSelf, view, stats?.is_recording]); + + const toggleRecording = async () => { + if (stats?.is_recording) { + const reportData = await invoke('stop_profiling'); + setReport(reportData); + setView('report'); + } else { + await invoke('start_profiling'); + } + }; + + 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) { + return ( + setView('dashboard')} + /> + ); + } + + if (!stats) return
Loading System Data...
; + + const avgCpu = stats.cpu_usage.reduce((a, b) => a + b, 0) / stats.cpu_usage.length; + const memoryPercent = (stats.used_memory / stats.total_memory) * 100; + + return ( +
+
+
+ + SysPulse +
+
+ + +
+
+ +
+ {stats.is_recording ? ( +
+
+
+
+ +
+
+
+

Profiling Active

+

{formatDuration(stats.recording_duration)}

+
+
+
+
CPU
+
{avgCpu.toFixed(1)}%
+
+
+
RAM
+
{memoryPercent.toFixed(1)}%
+
+
+

Minimal footprint mode enabled to ensure accurate results.

+
+ ) : ( + <> +
+
+
CPU Load
+
+ {avgCpu.toFixed(1)} + % +
+
+ + + + + + + + + + + +
+
+ +
+
Memory
+
+ {(stats.used_memory / 1024 / 1024 / 1024).toFixed(1)} + GB used +
+
+
+ {memoryPercent.toFixed(1)}% + {(stats.total_memory / 1024 / 1024 / 1024).toFixed(0)} GB +
+
+
+
+
+
+ +
+
Tasks
+
+ {stats.processes.length} + active +
+
+ Profiling will merge child processes into their parents for consolidated analysis. +
+
+
+ +
+
+

+ + Live Process Feed +

+
Top 50 Consumers
+
+ +
+
Name
+
PID
+
CPU
+
Memory
+
Action
+
+ +
+ {stats.processes.map((proc) => ( +
+
{proc.name}
+
{proc.pid}
+
{proc.cpu_usage.toFixed(1)}%
+
{(proc.memory / 1024 / 1024).toFixed(0)} MB
+
+ +
+
+ ))} +
+
+ + )} +
+
+ ); +} + +type SortField = 'name' | 'avg_cpu' | 'peak_cpu' | 'avg_memory_mb' | 'peak_memory_mb' | 'instance_count'; + +function ReportView({ report, onBack }: { report: ProfilingReport, onBack: () => void }) { + const [sortField, setSortField] = useState('avg_cpu'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + const [selectedProcess, setSelectedProcess] = useState(null); + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortOrder('desc'); + } + }; + + const sortedProcesses = useMemo(() => { + return [...report.aggregated_processes].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; + }); + }, [report, sortField, sortOrder]); + + const saveReport = async () => { + try { + const path = await invoke('save_report', { report }); + alert(`Report saved to: ${path}`); + } catch (e) { + alert(`Failed to save: ${e}`); + } + }; + + return ( +
+
+
+ + Profiling Report +
+
+ + +
+
+ +
+
+
+
Session Time
+
{formatDuration(report.duration_seconds)}
+
+
+
End of Session
+
{new Date(report.end_time).toLocaleTimeString()}
+
+
+
Uniques
+
{report.aggregated_processes.length}
+
+
+
Issue Alerts
+
+ {report.aggregated_processes.filter(p => p.warnings.length > 0).length} +
+
+
+ +
+
+

+ Session Load Profile +

+
+
+ + + + + + + + + + + + + + + + +
+
+ +
+
+

+ Analysis Matrix +

+ CLICK PROCESS TO INSPECT +
+ +
+
handleSort('name')}> + Process {sortField === 'name' && (sortOrder === 'asc' ? '↑' : '↓')} +
+
handleSort('instance_count')}>Units
+
handleSort('avg_cpu')}>Avg CPU
+
handleSort('peak_cpu')}>Peak CPU
+
handleSort('avg_memory_mb')}>Avg Mem
+
handleSort('peak_memory_mb')}>Peak Mem
+
Insights
+
+ +
+ {sortedProcesses.map((proc, i) => ( +
setSelectedProcess(proc)} + className="table-row grid grid-cols-[2fr_0.8fr_1fr_1fr_1fr_1fr_2fr] gap-2 px-4 py-3 border-b border-surface1/20 hover:bg-surface1/30 cursor-pointer transition-colors group" + > +
{proc.name}
+
{proc.instance_count}
+
{proc.avg_cpu.toFixed(1)}%
+
{proc.peak_cpu.toFixed(1)}%
+
{proc.avg_memory_mb.toFixed(0)}MB
+
{proc.peak_memory_mb.toFixed(0)}MB
+
+ {proc.warnings.map((w, idx) => ( + + {w} + + ))} +
+
+ ))} +
+
+ + {/* Process Detail Side Panel/Modal */} + {selectedProcess && ( +
+
+
+
+
Process Inspector
+

{selectedProcess.name}

+
+ +
+ +
+
+
+
Instances Detected
+
{selectedProcess.instance_count}
+

Maximum concurrent processes seen with this name.

+
+
+
Average Impact
+
{selectedProcess.avg_cpu.toFixed(1)}%
+

Mean CPU usage across the entire session.

+
+
+ +
+
Resource History (Summed)
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ +
+

Profiling Insights

+
+ {selectedProcess.warnings.length > 0 ? selectedProcess.warnings.map((w, idx) => ( +
+ + {w} +
+ )) : ( +
+ + Optimal Performance Profile +
+ )} +
+
+
+
+
+ )} +
+
+ ); +} + +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; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..6acb653 --- /dev/null +++ b/src/index.css @@ -0,0 +1,144 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + /* Catppuccin Mocha */ + --rosewater: #f5e0dc; + --flamingo: #f2cdcd; + --pink: #f5c2e7; + --mauve: #cba6f7; + --red: #f38ba8; + --maroon: #eba0ac; + --peach: #fab387; + --yellow: #f9e2af; + --green: #a6e3a1; + --teal: #94e2d5; + --sky: #89dceb; + --sapphire: #74c7ec; + --blue: #89b4fa; + --lavender: #b4befe; + --text: #cdd6f4; + --subtext1: #bac2de; + --subtext0: #a6adc8; + --overlay2: #9399b2; + --overlay1: #7f849c; + --overlay0: #6c7086; + --surface2: #585b70; + --surface1: #45475a; + --surface0: #313244; + --base: #1e1e2e; + --mantle: #181825; + --crust: #11111b; +} + +body { + margin: 0; + padding: 0; + background-color: var(--base); + color: var(--text); + font-family: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + overflow: hidden; + -webkit-font-smoothing: antialiased; +} + +.titlebar { + height: 48px; + background: var(--mantle); + user-select: none; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--surface0); + box-shadow: 0 4px 20px rgba(0,0,0,0.2); + z-index: 50; +} + +.titlebar-drag { + flex: 1; + height: 100%; + display: flex; + align-items: center; +} + +.card { + background: var(--surface0); + border-radius: 20px; + padding: 24px; + box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.6); +} + +.card-title { + color: var(--subtext0); + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.stat-value { + font-size: 36px; + font-weight: 900; + letter-spacing: -0.05em; + color: var(--text); + line-height: 1; +} + +.custom-scrollbar::-webkit-scrollbar { + width: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: var(--surface1); + border-radius: 10px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: var(--surface2); +} + +@keyframes pulse-soft { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.8; transform: scale(0.98); } +} + +.animate-pulse-soft { + animation: pulse-soft 2s infinite ease-in-out; +} + +.bg-red { background-color: var(--red); } +.bg-green { background-color: var(--green); } +.bg-blue { background-color: var(--blue); } +.bg-mauve { background-color: var(--mauve); } +.bg-surface0 { background-color: var(--surface0); } +.bg-surface1 { background-color: var(--surface1); } +.bg-base { background-color: var(--base); } + +.text-red { color: var(--red); } +.text-green { color: var(--green); } +.text-blue { color: var(--blue); } +.text-mauve { color: var(--mauve); } +.text-text { color: var(--text); } +.text-subtext0 { color: var(--subtext0); } +.text-subtext1 { color: var(--subtext1); } +.text-overlay1 { color: var(--overlay1); } +.text-overlay2 { color: var(--overlay2); } + +.border-surface1 { border-color: var(--surface1); } +.border-red { border-color: var(--red); } diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..3d7150d --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..ac86235 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,40 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + base: "#1e1e2e", + mantle: "#181825", + crust: "#11111b", + surface0: "#313244", + surface1: "#45475a", + surface2: "#585b70", + overlay0: "#6c7086", + overlay1: "#7f849c", + overlay2: "#9399b2", + subtext0: "#a6adc8", + subtext1: "#bac2de", + text: "#cdd6f4", + lavender: "#b4befe", + blue: "#89b4fa", + sapphire: "#74c7ec", + sky: "#89dceb", + teal: "#94e2d5", + green: "#a6e3a1", + yellow: "#f9e2af", + peach: "#fab387", + maroon: "#eba0ac", + red: "#f38ba8", + mauve: "#cba6f7", + pink: "#f5c2e7", + flamingo: "#f2cdcd", + rosewater: "#f5e0dc", + } + }, + }, + plugins: [], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3934b8f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..44c1ae7 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + clearScreen: false, + server: { + port: 1420, + strictPort: true, + }, + envPrefix: ['VITE_', 'TAURI_'], + build: { + target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13', + minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, + sourcemap: !!process.env.TAURI_DEBUG, + }, +})