Skip to main content

ruvector_memopt/apps/
browser.rs

1//! Browser memory optimization
2//!
3//! Detects and manages memory usage for major browsers:
4//! - Google Chrome / Chromium
5//! - Mozilla Firefox
6//! - Apple Safari
7//! - Microsoft Edge
8//! - Arc Browser
9//! - Brave Browser
10//! - Opera / Opera GX
11//! - Vivaldi
12
13use super::{AppCategory, AppInfo, AppProcess, OptimizationAction, OptimizationResult};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use sysinfo::{System, ProcessesToUpdate, Pid};
17
18/// Browser identification patterns
19#[derive(Debug, Clone)]
20pub struct BrowserPattern {
21    pub name: &'static str,
22    pub display_name: &'static str,
23    /// Main process patterns (case-insensitive)
24    pub main_patterns: &'static [&'static str],
25    /// Helper/renderer process patterns
26    pub helper_patterns: &'static [&'static str],
27    /// GPU process patterns
28    pub gpu_patterns: &'static [&'static str],
29    /// Extension/plugin patterns
30    pub extension_patterns: &'static [&'static str],
31}
32
33/// Known browser patterns
34pub const BROWSERS: &[BrowserPattern] = &[
35    BrowserPattern {
36        name: "chrome",
37        display_name: "Google Chrome",
38        main_patterns: &["google chrome", "chrome.exe", "chrome"],
39        helper_patterns: &["chrome helper", "google chrome helper", "chromedriver"],
40        gpu_patterns: &["chrome helper (gpu)", "chrome gpu"],
41        extension_patterns: &["chrome helper (renderer)", "chrome helper (plugin)"],
42    },
43    BrowserPattern {
44        name: "firefox",
45        display_name: "Mozilla Firefox",
46        main_patterns: &["firefox", "firefox.exe"],
47        helper_patterns: &["firefox helper", "plugin-container", "firefox-bin"],
48        gpu_patterns: &["firefox gpu"],
49        extension_patterns: &["web content", "webextensions"],
50    },
51    BrowserPattern {
52        name: "safari",
53        display_name: "Apple Safari",
54        main_patterns: &["safari", "safari.app"],
55        helper_patterns: &["safari web content", "webkit networking", "safari networking"],
56        gpu_patterns: &["safari graphics"],
57        extension_patterns: &["safari extension"],
58    },
59    BrowserPattern {
60        name: "edge",
61        display_name: "Microsoft Edge",
62        main_patterns: &["microsoft edge", "msedge", "msedge.exe"],
63        helper_patterns: &["microsoft edge helper", "msedge helper"],
64        gpu_patterns: &["msedge helper (gpu)"],
65        extension_patterns: &["msedge helper (renderer)"],
66    },
67    BrowserPattern {
68        name: "arc",
69        display_name: "Arc Browser",
70        main_patterns: &["arc", "arc.app"],
71        helper_patterns: &["arc helper", "arc helper (renderer)"],
72        gpu_patterns: &["arc helper (gpu)"],
73        extension_patterns: &["arc helper (plugin)"],
74    },
75    BrowserPattern {
76        name: "brave",
77        display_name: "Brave Browser",
78        main_patterns: &["brave browser", "brave.exe", "brave"],
79        helper_patterns: &["brave browser helper", "brave helper"],
80        gpu_patterns: &["brave browser helper (gpu)"],
81        extension_patterns: &["brave browser helper (renderer)"],
82    },
83    BrowserPattern {
84        name: "opera",
85        display_name: "Opera",
86        main_patterns: &["opera", "opera.exe", "opera gx"],
87        helper_patterns: &["opera helper", "opera gx helper"],
88        gpu_patterns: &["opera helper (gpu)"],
89        extension_patterns: &["opera helper (renderer)"],
90    },
91    BrowserPattern {
92        name: "vivaldi",
93        display_name: "Vivaldi",
94        main_patterns: &["vivaldi", "vivaldi.exe"],
95        helper_patterns: &["vivaldi helper"],
96        gpu_patterns: &["vivaldi helper (gpu)"],
97        extension_patterns: &["vivaldi helper (renderer)"],
98    },
99];
100
101/// Detailed browser process info
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct BrowserProcess {
104    pub pid: u32,
105    pub name: String,
106    pub process_type: BrowserProcessType,
107    pub memory_mb: f64,
108    pub cpu_percent: f32,
109}
110
111/// Type of browser process
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113pub enum BrowserProcessType {
114    Main,
115    Renderer,
116    GPU,
117    Extension,
118    Plugin,
119    Utility,
120    Network,
121    Unknown,
122}
123
124/// Browser instance info
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct BrowserInfo {
127    pub name: String,
128    pub display_name: String,
129    pub total_memory_mb: f64,
130    pub total_cpu_percent: f32,
131    pub process_count: usize,
132    pub estimated_tabs: usize,
133    pub main_pid: Option<u32>,
134    pub pids: Vec<u32>,
135    pub processes: Vec<BrowserProcess>,
136    pub gpu_memory_mb: f64,
137    pub renderer_memory_mb: f64,
138    pub extension_memory_mb: f64,
139}
140
141impl BrowserInfo {
142    /// Get suggested action based on resource usage
143    pub fn get_suggested_action(&self) -> OptimizationAction {
144        if self.total_memory_mb > 4000.0 {
145            OptimizationAction::Restart
146        } else if self.total_memory_mb > 2000.0 {
147            OptimizationAction::ReduceTabs {
148                suggested_count: self.estimated_tabs / 2,
149            }
150        } else if self.total_memory_mb > 1000.0 && self.estimated_tabs > 20 {
151            OptimizationAction::SuspendTabs
152        } else if self.total_memory_mb > 500.0 {
153            OptimizationAction::TrimMemory
154        } else {
155            OptimizationAction::None
156        }
157    }
158
159    /// Memory per estimated tab
160    pub fn memory_per_tab(&self) -> f64 {
161        if self.estimated_tabs > 0 {
162            self.total_memory_mb / self.estimated_tabs as f64
163        } else {
164            0.0
165        }
166    }
167}
168
169/// Browser memory optimizer
170pub struct BrowserOptimizer {
171    system: System,
172    browsers: HashMap<String, BrowserInfo>,
173    last_update: std::time::Instant,
174}
175
176impl BrowserOptimizer {
177    pub fn new() -> Self {
178        let mut system = System::new_all();
179        system.refresh_processes(ProcessesToUpdate::All, true);
180
181        Self {
182            system,
183            browsers: HashMap::new(),
184            last_update: std::time::Instant::now(),
185        }
186    }
187
188    /// Refresh process data
189    pub fn refresh(&mut self) {
190        self.system.refresh_processes(ProcessesToUpdate::All, true);
191        self.detect_browsers();
192        self.last_update = std::time::Instant::now();
193    }
194
195    /// Detect all running browsers
196    fn detect_browsers(&mut self) {
197        self.browsers.clear();
198
199        for pattern in BROWSERS {
200            let mut browser_info = BrowserInfo {
201                name: pattern.name.to_string(),
202                display_name: pattern.display_name.to_string(),
203                total_memory_mb: 0.0,
204                total_cpu_percent: 0.0,
205                process_count: 0,
206                estimated_tabs: 0,
207                main_pid: None,
208                pids: Vec::new(),
209                processes: Vec::new(),
210                gpu_memory_mb: 0.0,
211                renderer_memory_mb: 0.0,
212                extension_memory_mb: 0.0,
213            };
214
215            for (pid, process) in self.system.processes() {
216                let name = process.name().to_string_lossy().to_lowercase();
217                let memory_mb = process.memory() as f64 / (1024.0 * 1024.0);
218                let cpu_percent = process.cpu_usage();
219
220                let process_type = self.classify_process(&name, pattern);
221
222                if process_type != BrowserProcessType::Unknown {
223                    let pid_u32 = pid.as_u32();
224                    let browser_proc = BrowserProcess {
225                        pid: pid_u32,
226                        name: process.name().to_string_lossy().to_string(),
227                        process_type,
228                        memory_mb,
229                        cpu_percent,
230                    };
231
232                    browser_info.total_memory_mb += memory_mb;
233                    browser_info.total_cpu_percent += cpu_percent;
234                    browser_info.process_count += 1;
235                    browser_info.pids.push(pid_u32);
236
237                    match process_type {
238                        BrowserProcessType::Main => {
239                            browser_info.main_pid = Some(pid.as_u32());
240                        }
241                        BrowserProcessType::GPU => {
242                            browser_info.gpu_memory_mb += memory_mb;
243                        }
244                        BrowserProcessType::Renderer => {
245                            browser_info.renderer_memory_mb += memory_mb;
246                            browser_info.estimated_tabs += 1;
247                        }
248                        BrowserProcessType::Extension => {
249                            browser_info.extension_memory_mb += memory_mb;
250                        }
251                        _ => {}
252                    }
253
254                    browser_info.processes.push(browser_proc);
255                }
256            }
257
258            // Only include if we found any processes
259            if browser_info.process_count > 0 {
260                // Estimate tabs from renderer processes (each tab ~= 1 renderer)
261                // But there's usually at least 1 renderer even with no tabs
262                if browser_info.estimated_tabs > 0 {
263                    browser_info.estimated_tabs = browser_info.estimated_tabs.saturating_sub(1).max(1);
264                }
265
266                self.browsers.insert(pattern.name.to_string(), browser_info);
267            }
268        }
269    }
270
271    /// Classify a process based on browser pattern
272    fn classify_process(&self, name: &str, pattern: &BrowserPattern) -> BrowserProcessType {
273        // Check main process first
274        for p in pattern.main_patterns {
275            if name.contains(p) && !name.contains("helper") {
276                return BrowserProcessType::Main;
277            }
278        }
279
280        // Check GPU process
281        for p in pattern.gpu_patterns {
282            if name.contains(p) {
283                return BrowserProcessType::GPU;
284            }
285        }
286
287        // Check extension process
288        for p in pattern.extension_patterns {
289            if name.contains(p) {
290                return BrowserProcessType::Extension;
291            }
292        }
293
294        // Check helper/renderer process
295        for p in pattern.helper_patterns {
296            if name.contains(p) {
297                return BrowserProcessType::Renderer;
298            }
299        }
300
301        // Check if it matches any main pattern (catch-all for related processes)
302        for p in pattern.main_patterns {
303            if name.contains(p) {
304                return BrowserProcessType::Utility;
305            }
306        }
307
308        BrowserProcessType::Unknown
309    }
310
311    /// Get all detected browsers
312    pub fn get_browsers(&self) -> Vec<&BrowserInfo> {
313        self.browsers.values().collect()
314    }
315
316    /// Get browser by name
317    pub fn get_browser(&self, name: &str) -> Option<&BrowserInfo> {
318        self.browsers.get(name)
319    }
320
321    /// Get total browser memory usage
322    pub fn total_memory_mb(&self) -> f64 {
323        self.browsers.values().map(|b| b.total_memory_mb).sum()
324    }
325
326    /// Get total browser CPU usage
327    pub fn total_cpu_percent(&self) -> f32 {
328        self.browsers.values().map(|b| b.total_cpu_percent).sum()
329    }
330
331    /// Get browser with highest memory usage
332    pub fn highest_memory_browser(&self) -> Option<&BrowserInfo> {
333        self.browsers
334            .values()
335            .max_by(|a, b| a.total_memory_mb.partial_cmp(&b.total_memory_mb).unwrap())
336    }
337
338    /// Get optimization suggestions for all browsers
339    pub fn get_suggestions(&self) -> Vec<(String, OptimizationAction, String)> {
340        let mut suggestions = Vec::new();
341
342        for browser in self.browsers.values() {
343            let action = browser.get_suggested_action();
344            if action != OptimizationAction::None {
345                let reason = match &action {
346                    OptimizationAction::Restart => {
347                        format!(
348                            "{} is using {:.0} MB - consider restarting to free memory",
349                            browser.display_name, browser.total_memory_mb
350                        )
351                    }
352                    OptimizationAction::ReduceTabs { suggested_count } => {
353                        format!(
354                            "{} has ~{} tabs using {:.0} MB - consider closing some tabs (suggest {})",
355                            browser.display_name,
356                            browser.estimated_tabs,
357                            browser.total_memory_mb,
358                            suggested_count
359                        )
360                    }
361                    OptimizationAction::SuspendTabs => {
362                        format!(
363                            "{} has ~{} tabs - consider using a tab suspender extension",
364                            browser.display_name, browser.estimated_tabs
365                        )
366                    }
367                    OptimizationAction::TrimMemory => {
368                        format!(
369                            "{} is using {:.0} MB - memory can be trimmed",
370                            browser.display_name, browser.total_memory_mb
371                        )
372                    }
373                    _ => continue,
374                };
375
376                suggestions.push((browser.display_name.clone(), action, reason));
377            }
378        }
379
380        // Sort by memory (highest first)
381        suggestions.sort_by(|a, b| {
382            let mem_a = self.browsers.values().find(|browser| browser.display_name == a.0).map(|browser| browser.total_memory_mb).unwrap_or(0.0);
383            let mem_b = self.browsers.values().find(|browser| browser.display_name == b.0).map(|browser| browser.total_memory_mb).unwrap_or(0.0);
384            mem_b.partial_cmp(&mem_a).unwrap()
385        });
386
387        suggestions
388    }
389
390    /// Attempt to trim browser memory (platform-specific)
391    #[cfg(target_os = "windows")]
392    pub fn trim_browser_memory(&self, browser_name: &str) -> OptimizationResult {
393        use std::os::raw::c_void;
394
395        let browser = match self.browsers.get(browser_name) {
396            Some(b) => b,
397            None => {
398                return OptimizationResult {
399                    app_name: browser_name.to_string(),
400                    action: OptimizationAction::TrimMemory,
401                    success: false,
402                    memory_freed_mb: 0.0,
403                    message: "Browser not found".to_string(),
404                }
405            }
406        };
407
408        let mut total_freed = 0.0;
409        let mut trimmed = 0;
410
411        for proc in &browser.processes {
412            unsafe {
413                use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_QUERY_INFORMATION};
414                use windows::Win32::System::ProcessStatus::EmptyWorkingSet;
415
416                if let Ok(handle) = OpenProcess(
417                    PROCESS_SET_QUOTA | PROCESS_QUERY_INFORMATION,
418                    false,
419                    proc.pid,
420                ) {
421                    let before = proc.memory_mb;
422                    if EmptyWorkingSet(handle).is_ok() {
423                        // Estimate ~30% reduction
424                        total_freed += before * 0.3;
425                        trimmed += 1;
426                    }
427                    let _ = windows::Win32::Foundation::CloseHandle(handle);
428                }
429            }
430        }
431
432        OptimizationResult {
433            app_name: browser.display_name.clone(),
434            action: OptimizationAction::TrimMemory,
435            success: trimmed > 0,
436            memory_freed_mb: total_freed,
437            message: format!("Trimmed {} processes, estimated {:.0} MB freed", trimmed, total_freed),
438        }
439    }
440
441    #[cfg(target_os = "macos")]
442    pub fn trim_browser_memory(&self, browser_name: &str) -> OptimizationResult {
443        let browser = match self.browsers.get(browser_name) {
444            Some(b) => b,
445            None => {
446                return OptimizationResult {
447                    app_name: browser_name.to_string(),
448                    action: OptimizationAction::TrimMemory,
449                    success: false,
450                    memory_freed_mb: 0.0,
451                    message: "Browser not found".to_string(),
452                }
453            }
454        };
455
456        // On macOS, we can use memory_pressure notification or purge
457        // For individual apps, we can send SIGURG or use madvise hints
458        // But direct memory trimming isn't as straightforward as Windows
459
460        OptimizationResult {
461            app_name: browser.display_name.clone(),
462            action: OptimizationAction::TrimMemory,
463            success: false,
464            memory_freed_mb: 0.0,
465            message: format!(
466                "{} using {:.0} MB. On macOS, use 'Optimize Now' for system-wide cleanup or restart the browser.",
467                browser.display_name, browser.total_memory_mb
468            ),
469        }
470    }
471
472    #[cfg(not(any(target_os = "windows", target_os = "macos")))]
473    pub fn trim_browser_memory(&self, browser_name: &str) -> OptimizationResult {
474        OptimizationResult {
475            app_name: browser_name.to_string(),
476            action: OptimizationAction::TrimMemory,
477            success: false,
478            memory_freed_mb: 0.0,
479            message: "Memory trimming not supported on this platform".to_string(),
480        }
481    }
482
483    /// Print browser summary
484    pub fn print_summary(&self) {
485        println!("\n🌐 Browser Memory Usage\n");
486        println!("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”");
487        println!("│ Browser              │ Memory    │ CPU      │ Tabs  │ Processes   │");
488        println!("ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤");
489
490        let mut browsers: Vec<_> = self.browsers.values().collect();
491        browsers.sort_by(|a, b| b.total_memory_mb.partial_cmp(&a.total_memory_mb).unwrap());
492
493        for browser in &browsers {
494            let mem_indicator = if browser.total_memory_mb > 2000.0 {
495                "šŸ”“"
496            } else if browser.total_memory_mb > 1000.0 {
497                "🟔"
498            } else {
499                "🟢"
500            };
501
502            println!(
503                "│ {} {:18} │ {:>7.0} MB │ {:>6.1}%  │ {:>5} │ {:>11} │",
504                mem_indicator,
505                truncate(&browser.display_name, 18),
506                browser.total_memory_mb,
507                browser.total_cpu_percent,
508                browser.estimated_tabs,
509                browser.process_count
510            );
511        }
512
513        println!("ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜");
514
515        let total_mem: f64 = browsers.iter().map(|b| b.total_memory_mb).sum();
516        let total_cpu: f32 = browsers.iter().map(|b| b.total_cpu_percent).sum();
517        println!(
518            "\nTotal: {:.0} MB memory, {:.1}% CPU across {} browsers",
519            total_mem,
520            total_cpu,
521            browsers.len()
522        );
523
524        // Print suggestions
525        let suggestions = self.get_suggestions();
526        if !suggestions.is_empty() {
527            println!("\nšŸ’” Suggestions:");
528            for (_, _, reason) in suggestions.iter().take(3) {
529                println!("   • {}", reason);
530            }
531        }
532    }
533}
534
535impl Default for BrowserOptimizer {
536    fn default() -> Self {
537        Self::new()
538    }
539}
540
541fn truncate(s: &str, max: usize) -> String {
542    if s.len() <= max {
543        format!("{:width$}", s, width = max)
544    } else {
545        format!("{}...", &s[..max - 3])
546    }
547}