Skip to main content

ruvector_memopt/apps/
electron.rs

1//! Electron app detection and memory management
2//!
3//! Electron apps are essentially embedded Chromium browsers and often
4//! consume significant memory. This module detects common Electron apps:
5//! - VS Code
6//! - Discord
7//! - Slack
8//! - Microsoft Teams
9//! - Notion
10//! - Figma
11//! - Spotify
12//! - Obsidian
13//! - 1Password
14//! - Postman
15//! - And many more...
16
17use super::{AppCategory, AppInfo, OptimizationAction, OptimizationResult};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use sysinfo::{System, ProcessesToUpdate, Pid};
21
22/// Known Electron app patterns
23#[derive(Debug, Clone)]
24pub struct ElectronAppPattern {
25    pub name: &'static str,
26    pub display_name: &'static str,
27    pub patterns: &'static [&'static str],
28    pub category: AppCategory,
29    /// Expected baseline memory (MB) - for leak detection
30    pub baseline_memory_mb: f64,
31}
32
33/// Known Electron applications
34pub const ELECTRON_APPS: &[ElectronAppPattern] = &[
35    ElectronAppPattern {
36        name: "vscode",
37        display_name: "Visual Studio Code",
38        patterns: &["code", "code.exe", "code helper", "code - insiders"],
39        category: AppCategory::Development,
40        baseline_memory_mb: 300.0,
41    },
42    ElectronAppPattern {
43        name: "discord",
44        display_name: "Discord",
45        patterns: &["discord", "discord.exe", "discord helper"],
46        category: AppCategory::Communication,
47        baseline_memory_mb: 200.0,
48    },
49    ElectronAppPattern {
50        name: "slack",
51        display_name: "Slack",
52        patterns: &["slack", "slack.exe", "slack helper"],
53        category: AppCategory::Communication,
54        baseline_memory_mb: 250.0,
55    },
56    ElectronAppPattern {
57        name: "teams",
58        display_name: "Microsoft Teams",
59        patterns: &["teams", "ms-teams", "microsoft teams", "teams.exe"],
60        category: AppCategory::Communication,
61        baseline_memory_mb: 300.0,
62    },
63    ElectronAppPattern {
64        name: "notion",
65        display_name: "Notion",
66        patterns: &["notion", "notion.exe", "notion helper"],
67        category: AppCategory::Development,
68        baseline_memory_mb: 200.0,
69    },
70    ElectronAppPattern {
71        name: "figma",
72        display_name: "Figma",
73        patterns: &["figma", "figma.exe", "figma helper", "figma agent"],
74        category: AppCategory::Creative,
75        baseline_memory_mb: 400.0,
76    },
77    ElectronAppPattern {
78        name: "spotify",
79        display_name: "Spotify",
80        patterns: &["spotify", "spotify.exe", "spotify helper"],
81        category: AppCategory::Media,
82        baseline_memory_mb: 150.0,
83    },
84    ElectronAppPattern {
85        name: "obsidian",
86        display_name: "Obsidian",
87        patterns: &["obsidian", "obsidian.exe", "obsidian helper"],
88        category: AppCategory::Development,
89        baseline_memory_mb: 150.0,
90    },
91    ElectronAppPattern {
92        name: "1password",
93        display_name: "1Password",
94        patterns: &["1password", "1password.exe", "1password helper"],
95        category: AppCategory::System,
96        baseline_memory_mb: 100.0,
97    },
98    ElectronAppPattern {
99        name: "postman",
100        display_name: "Postman",
101        patterns: &["postman", "postman.exe", "postman helper"],
102        category: AppCategory::Development,
103        baseline_memory_mb: 300.0,
104    },
105    ElectronAppPattern {
106        name: "whatsapp",
107        display_name: "WhatsApp",
108        patterns: &["whatsapp", "whatsapp.exe", "whatsapp helper"],
109        category: AppCategory::Communication,
110        baseline_memory_mb: 150.0,
111    },
112    ElectronAppPattern {
113        name: "signal",
114        display_name: "Signal",
115        patterns: &["signal", "signal.exe", "signal helper"],
116        category: AppCategory::Communication,
117        baseline_memory_mb: 150.0,
118    },
119    ElectronAppPattern {
120        name: "telegram",
121        display_name: "Telegram Desktop",
122        patterns: &["telegram", "telegram.exe", "telegram desktop"],
123        category: AppCategory::Communication,
124        baseline_memory_mb: 150.0,
125    },
126    ElectronAppPattern {
127        name: "cursor",
128        display_name: "Cursor",
129        patterns: &["cursor", "cursor.exe", "cursor helper"],
130        category: AppCategory::Development,
131        baseline_memory_mb: 300.0,
132    },
133    ElectronAppPattern {
134        name: "windsurf",
135        display_name: "Windsurf",
136        patterns: &["windsurf", "windsurf.exe", "windsurf helper"],
137        category: AppCategory::Development,
138        baseline_memory_mb: 300.0,
139    },
140    ElectronAppPattern {
141        name: "zed",
142        display_name: "Zed",
143        patterns: &["zed", "zed.exe", "zed helper"],
144        category: AppCategory::Development,
145        baseline_memory_mb: 200.0,
146    },
147    ElectronAppPattern {
148        name: "linear",
149        display_name: "Linear",
150        patterns: &["linear", "linear.exe", "linear helper"],
151        category: AppCategory::Development,
152        baseline_memory_mb: 200.0,
153    },
154    ElectronAppPattern {
155        name: "hyper",
156        display_name: "Hyper Terminal",
157        patterns: &["hyper", "hyper.exe", "hyper helper"],
158        category: AppCategory::Development,
159        baseline_memory_mb: 150.0,
160    },
161    ElectronAppPattern {
162        name: "atom",
163        display_name: "Atom",
164        patterns: &["atom", "atom.exe", "atom helper"],
165        category: AppCategory::Development,
166        baseline_memory_mb: 250.0,
167    },
168    ElectronAppPattern {
169        name: "bitwarden",
170        display_name: "Bitwarden",
171        patterns: &["bitwarden", "bitwarden.exe", "bitwarden helper"],
172        category: AppCategory::System,
173        baseline_memory_mb: 100.0,
174    },
175    ElectronAppPattern {
176        name: "mongodb-compass",
177        display_name: "MongoDB Compass",
178        patterns: &["mongodb compass", "mongodb-compass", "compass"],
179        category: AppCategory::Development,
180        baseline_memory_mb: 300.0,
181    },
182    ElectronAppPattern {
183        name: "insomnia",
184        display_name: "Insomnia",
185        patterns: &["insomnia", "insomnia.exe"],
186        category: AppCategory::Development,
187        baseline_memory_mb: 200.0,
188    },
189    ElectronAppPattern {
190        name: "loom",
191        display_name: "Loom",
192        patterns: &["loom", "loom.exe", "loom helper"],
193        category: AppCategory::Media,
194        baseline_memory_mb: 200.0,
195    },
196    ElectronAppPattern {
197        name: "gitkraken",
198        display_name: "GitKraken",
199        patterns: &["gitkraken", "gitkraken.exe"],
200        category: AppCategory::Development,
201        baseline_memory_mb: 250.0,
202    },
203];
204
205/// Electron app instance info
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ElectronAppInfo {
208    pub name: String,
209    pub display_name: String,
210    pub category: AppCategory,
211    pub total_memory_mb: f64,
212    pub total_cpu_percent: f32,
213    pub process_count: usize,
214    pub main_pid: Option<u32>,
215    pub pids: Vec<u32>,
216    pub baseline_memory_mb: f64,
217    pub memory_overhead_percent: f64,
218    pub is_running: bool,
219}
220
221impl ElectronAppInfo {
222    /// Check if app is using more memory than expected
223    pub fn is_bloated(&self) -> bool {
224        self.total_memory_mb > self.baseline_memory_mb * 2.0
225    }
226
227    /// Check if likely has a memory leak
228    pub fn likely_memory_leak(&self) -> bool {
229        self.memory_overhead_percent > 200.0
230    }
231
232    /// Get suggested action
233    pub fn get_suggested_action(&self) -> OptimizationAction {
234        if self.total_memory_mb > 1500.0 {
235            OptimizationAction::Restart
236        } else if self.is_bloated() {
237            OptimizationAction::TrimMemory
238        } else {
239            OptimizationAction::None
240        }
241    }
242}
243
244/// Electron app manager
245pub struct ElectronManager {
246    system: System,
247    apps: HashMap<String, ElectronAppInfo>,
248    last_update: std::time::Instant,
249}
250
251impl ElectronManager {
252    pub fn new() -> Self {
253        let mut system = System::new_all();
254        system.refresh_processes(ProcessesToUpdate::All, true);
255
256        Self {
257            system,
258            apps: HashMap::new(),
259            last_update: std::time::Instant::now(),
260        }
261    }
262
263    /// Refresh process data
264    pub fn refresh(&mut self) {
265        self.system.refresh_processes(ProcessesToUpdate::All, true);
266        self.detect_electron_apps();
267        self.last_update = std::time::Instant::now();
268    }
269
270    /// Detect all running Electron apps
271    fn detect_electron_apps(&mut self) {
272        self.apps.clear();
273
274        for pattern in ELECTRON_APPS {
275            let mut app_info = ElectronAppInfo {
276                name: pattern.name.to_string(),
277                display_name: pattern.display_name.to_string(),
278                category: pattern.category,
279                total_memory_mb: 0.0,
280                total_cpu_percent: 0.0,
281                process_count: 0,
282                main_pid: None,
283                pids: Vec::new(),
284                baseline_memory_mb: pattern.baseline_memory_mb,
285                memory_overhead_percent: 0.0,
286                is_running: false,
287            };
288
289            for (pid, process) in self.system.processes() {
290                let name = process.name().to_string_lossy().to_lowercase();
291
292                // Check if process matches any pattern
293                let matches = pattern.patterns.iter().any(|p| name.contains(p));
294
295                if matches {
296                    let memory_mb = process.memory() as f64 / (1024.0 * 1024.0);
297                    let cpu_percent = process.cpu_usage();
298
299                    app_info.total_memory_mb += memory_mb;
300                    app_info.total_cpu_percent += cpu_percent;
301                    app_info.process_count += 1;
302                    app_info.pids.push(pid.as_u32());
303                    app_info.is_running = true;
304
305                    // First matching process is usually the main process
306                    if app_info.main_pid.is_none() && !name.contains("helper") {
307                        app_info.main_pid = Some(pid.as_u32());
308                    }
309                }
310            }
311
312            if app_info.is_running {
313                // Calculate memory overhead
314                app_info.memory_overhead_percent =
315                    (app_info.total_memory_mb / app_info.baseline_memory_mb) * 100.0;
316
317                self.apps.insert(pattern.name.to_string(), app_info);
318            }
319        }
320    }
321
322    /// Get all detected Electron apps
323    pub fn get_apps(&self) -> Vec<&ElectronAppInfo> {
324        self.apps.values().collect()
325    }
326
327    /// Get app by name
328    pub fn get_app(&self, name: &str) -> Option<&ElectronAppInfo> {
329        self.apps.get(name)
330    }
331
332    /// Get total Electron app memory
333    pub fn total_memory_mb(&self) -> f64 {
334        self.apps.values().map(|a| a.total_memory_mb).sum()
335    }
336
337    /// Get apps that are bloated
338    pub fn get_bloated_apps(&self) -> Vec<&ElectronAppInfo> {
339        self.apps.values().filter(|a| a.is_bloated()).collect()
340    }
341
342    /// Get optimization suggestions
343    pub fn get_suggestions(&self) -> Vec<(String, OptimizationAction, String)> {
344        let mut suggestions = Vec::new();
345
346        for app in self.apps.values() {
347            let action = app.get_suggested_action();
348            if action != OptimizationAction::None {
349                let reason = match &action {
350                    OptimizationAction::Restart => {
351                        format!(
352                            "{} is using {:.0} MB ({:.0}% of baseline) - restart recommended",
353                            app.display_name,
354                            app.total_memory_mb,
355                            app.memory_overhead_percent
356                        )
357                    }
358                    OptimizationAction::TrimMemory => {
359                        format!(
360                            "{} is using {:.0} MB ({:.0}% of expected {:.0} MB)",
361                            app.display_name,
362                            app.total_memory_mb,
363                            app.memory_overhead_percent,
364                            app.baseline_memory_mb
365                        )
366                    }
367                    _ => continue,
368                };
369
370                suggestions.push((app.display_name.clone(), action, reason));
371            }
372        }
373
374        suggestions.sort_by(|a, b| {
375            let mem_a = self.apps.values().find(|app| app.display_name == a.0)
376                .map(|a| a.total_memory_mb).unwrap_or(0.0);
377            let mem_b = self.apps.values().find(|app| app.display_name == b.0)
378                .map(|a| a.total_memory_mb).unwrap_or(0.0);
379            mem_b.partial_cmp(&mem_a).unwrap()
380        });
381
382        suggestions
383    }
384
385    /// Print Electron apps summary
386    pub fn print_summary(&self) {
387        println!("\n⚡ Electron Apps Memory Usage\n");
388        println!("┌──────────────────────┬───────────┬──────────┬───────────┬──────────┐");
389        println!("│ Application          │ Memory    │ Baseline │ Overhead  │ Status   │");
390        println!("├──────────────────────┼───────────┼──────────┼───────────┼──────────┤");
391
392        let mut apps: Vec<_> = self.apps.values().collect();
393        apps.sort_by(|a, b| b.total_memory_mb.partial_cmp(&a.total_memory_mb).unwrap());
394
395        for app in &apps {
396            let status = if app.total_memory_mb > 1000.0 {
397                "🔴 Heavy"
398            } else if app.is_bloated() {
399                "🟡 Bloated"
400            } else {
401                "🟢 Normal"
402            };
403
404            println!(
405                "│ {:20} │ {:>7.0} MB │ {:>6.0} MB │ {:>8.0}% │ {:8} │",
406                truncate(&app.display_name, 20),
407                app.total_memory_mb,
408                app.baseline_memory_mb,
409                app.memory_overhead_percent,
410                status
411            );
412        }
413
414        println!("└──────────────────────┴───────────┴──────────┴───────────┴──────────┘");
415
416        let total: f64 = apps.iter().map(|a| a.total_memory_mb).sum();
417        println!(
418            "\nTotal: {:.0} MB across {} Electron apps ({} processes)",
419            total,
420            apps.len(),
421            apps.iter().map(|a| a.process_count).sum::<usize>()
422        );
423
424        let bloated = self.get_bloated_apps();
425        if !bloated.is_empty() {
426            println!("\n💡 Suggestions:");
427            for app in bloated.iter().take(3) {
428                println!(
429                    "   • {} is using {:.0}% more memory than expected - consider restarting",
430                    app.display_name,
431                    app.memory_overhead_percent - 100.0
432                );
433            }
434        }
435    }
436}
437
438impl Default for ElectronManager {
439    fn default() -> Self {
440        Self::new()
441    }
442}
443
444fn truncate(s: &str, max: usize) -> String {
445    if s.len() <= max {
446        format!("{:width$}", s, width = max)
447    } else {
448        format!("{}...", &s[..max - 3])
449    }
450}