1use super::{AppCategory, AppInfo, AppProcess, OptimizationAction, OptimizationResult};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use sysinfo::{System, ProcessesToUpdate, Pid};
17
18#[derive(Debug, Clone)]
20pub struct BrowserPattern {
21 pub name: &'static str,
22 pub display_name: &'static str,
23 pub main_patterns: &'static [&'static str],
25 pub helper_patterns: &'static [&'static str],
27 pub gpu_patterns: &'static [&'static str],
29 pub extension_patterns: &'static [&'static str],
31}
32
33pub 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#[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#[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#[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 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 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
169pub 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 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 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 if browser_info.process_count > 0 {
260 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 fn classify_process(&self, name: &str, pattern: &BrowserPattern) -> BrowserProcessType {
273 for p in pattern.main_patterns {
275 if name.contains(p) && !name.contains("helper") {
276 return BrowserProcessType::Main;
277 }
278 }
279
280 for p in pattern.gpu_patterns {
282 if name.contains(p) {
283 return BrowserProcessType::GPU;
284 }
285 }
286
287 for p in pattern.extension_patterns {
289 if name.contains(p) {
290 return BrowserProcessType::Extension;
291 }
292 }
293
294 for p in pattern.helper_patterns {
296 if name.contains(p) {
297 return BrowserProcessType::Renderer;
298 }
299 }
300
301 for p in pattern.main_patterns {
303 if name.contains(p) {
304 return BrowserProcessType::Utility;
305 }
306 }
307
308 BrowserProcessType::Unknown
309 }
310
311 pub fn get_browsers(&self) -> Vec<&BrowserInfo> {
313 self.browsers.values().collect()
314 }
315
316 pub fn get_browser(&self, name: &str) -> Option<&BrowserInfo> {
318 self.browsers.get(name)
319 }
320
321 pub fn total_memory_mb(&self) -> f64 {
323 self.browsers.values().map(|b| b.total_memory_mb).sum()
324 }
325
326 pub fn total_cpu_percent(&self) -> f32 {
328 self.browsers.values().map(|b| b.total_cpu_percent).sum()
329 }
330
331 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 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 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 #[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 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 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 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 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}