1use super::{
10 browser::BrowserOptimizer,
11 docker::DockerManager,
12 electron::ElectronManager,
13 leaks::LeakDetector,
14 AppCategory, OptimizationAction,
15};
16use serde::{Deserialize, Serialize};
17use sysinfo::{System, ProcessesToUpdate};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Suggestion {
22 pub priority: SuggestionPriority,
23 pub category: AppCategory,
24 pub title: String,
25 pub description: String,
26 pub action: OptimizationAction,
27 pub estimated_savings_mb: f64,
28 pub app_name: Option<String>,
29 pub pids: Vec<u32>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
34pub enum SuggestionPriority {
35 Low = 1,
36 Medium = 2,
37 High = 3,
38 Critical = 4,
39}
40
41impl std::fmt::Display for SuggestionPriority {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 SuggestionPriority::Low => write!(f, "Low"),
45 SuggestionPriority::Medium => write!(f, "Medium"),
46 SuggestionPriority::High => write!(f, "High"),
47 SuggestionPriority::Critical => write!(f, "Critical"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum MemoryPressure {
55 Low, Normal, High, Critical, }
60
61pub struct SmartSuggestions {
63 system: System,
64 browser_optimizer: BrowserOptimizer,
65 electron_manager: ElectronManager,
66 docker_manager: DockerManager,
67 suggestions: Vec<Suggestion>,
68}
69
70impl SmartSuggestions {
71 pub fn new() -> Self {
72 Self {
73 system: System::new_all(),
74 browser_optimizer: BrowserOptimizer::new(),
75 electron_manager: ElectronManager::new(),
76 docker_manager: DockerManager::new(),
77 suggestions: Vec::new(),
78 }
79 }
80
81 pub fn refresh(&mut self) {
83 self.system.refresh_all();
84 self.browser_optimizer.refresh();
85 self.electron_manager.refresh();
86 self.docker_manager.refresh();
87
88 self.generate_suggestions();
89 }
90
91 pub fn memory_pressure(&self) -> MemoryPressure {
93 let total = self.system.total_memory();
94 let used = self.system.used_memory();
95
96 if total == 0 {
97 return MemoryPressure::Normal;
98 }
99
100 let percent = (used as f64 / total as f64) * 100.0;
101
102 if percent > 85.0 {
103 MemoryPressure::Critical
104 } else if percent > 70.0 {
105 MemoryPressure::High
106 } else if percent > 50.0 {
107 MemoryPressure::Normal
108 } else {
109 MemoryPressure::Low
110 }
111 }
112
113 fn generate_suggestions(&mut self) {
115 self.suggestions.clear();
116
117 let pressure = self.memory_pressure();
118
119 self.add_browser_suggestions(pressure);
121
122 self.add_electron_suggestions(pressure);
124
125 self.add_docker_suggestions(pressure);
127
128 self.add_general_suggestions(pressure);
130
131 self.suggestions.sort_by(|a, b| {
133 b.priority
134 .cmp(&a.priority)
135 .then(b.estimated_savings_mb.partial_cmp(&a.estimated_savings_mb).unwrap())
136 });
137 }
138
139 fn add_browser_suggestions(&mut self, pressure: MemoryPressure) {
141 for browser in self.browser_optimizer.get_browsers() {
142 if browser.estimated_tabs > 20 {
144 let priority = if browser.total_memory_mb > 2000.0 || pressure == MemoryPressure::Critical {
145 SuggestionPriority::High
146 } else if browser.total_memory_mb > 1000.0 || pressure == MemoryPressure::High {
147 SuggestionPriority::Medium
148 } else {
149 SuggestionPriority::Low
150 };
151
152 let suggested_tabs = (browser.estimated_tabs / 2).max(10);
153 let estimated_savings = browser.total_memory_mb * 0.3; self.suggestions.push(Suggestion {
156 priority,
157 category: AppCategory::Browser,
158 title: format!("Reduce {} tabs", browser.name),
159 description: format!(
160 "{} has ~{} tabs using {:.0} MB. Consider reducing to {} tabs.",
161 browser.name, browser.estimated_tabs, browser.total_memory_mb, suggested_tabs
162 ),
163 action: OptimizationAction::ReduceTabs {
164 suggested_count: suggested_tabs,
165 },
166 estimated_savings_mb: estimated_savings,
167 app_name: Some(browser.name.clone()),
168 pids: browser.pids.clone(),
169 });
170 }
171
172 if browser.total_memory_mb > 3000.0 {
174 self.suggestions.push(Suggestion {
175 priority: SuggestionPriority::High,
176 category: AppCategory::Browser,
177 title: format!("Restart {}", browser.name),
178 description: format!(
179 "{} is using {:.0} MB - consider restarting to free memory",
180 browser.name, browser.total_memory_mb
181 ),
182 action: OptimizationAction::Restart,
183 estimated_savings_mb: browser.total_memory_mb * 0.5,
184 app_name: Some(browser.name.clone()),
185 pids: browser.pids.clone(),
186 });
187 }
188 }
189 }
190
191 fn add_electron_suggestions(&mut self, pressure: MemoryPressure) {
193 for app in self.electron_manager.get_apps() {
194 if app.is_bloated() {
196 let priority = if app.total_memory_mb > 1000.0 || pressure == MemoryPressure::Critical {
197 SuggestionPriority::High
198 } else if pressure == MemoryPressure::High {
199 SuggestionPriority::Medium
200 } else {
201 SuggestionPriority::Low
202 };
203
204 let excess = app.total_memory_mb - app.baseline_memory_mb;
205
206 self.suggestions.push(Suggestion {
207 priority,
208 category: AppCategory::Electron,
209 title: format!("Restart {}", app.display_name),
210 description: format!(
211 "{} is using {:.0}% more memory than expected ({:.0} MB vs {:.0} MB baseline). Restart to reclaim ~{:.0} MB.",
212 app.display_name,
213 app.memory_overhead_percent - 100.0,
214 app.total_memory_mb,
215 app.baseline_memory_mb,
216 excess
217 ),
218 action: OptimizationAction::Restart,
219 estimated_savings_mb: excess,
220 app_name: Some(app.display_name.clone()),
221 pids: app.pids.clone(),
222 });
223 }
224
225 if app.total_memory_mb > 1500.0 {
227 self.suggestions.push(Suggestion {
228 priority: SuggestionPriority::High,
229 category: AppCategory::Electron,
230 title: format!("{} high memory", app.display_name),
231 description: format!(
232 "{} is using {:.0} MB across {} processes. Consider closing if not needed.",
233 app.display_name, app.total_memory_mb, app.process_count
234 ),
235 action: OptimizationAction::Close,
236 estimated_savings_mb: app.total_memory_mb,
237 app_name: Some(app.display_name.clone()),
238 pids: app.pids.clone(),
239 });
240 }
241 }
242 }
243
244 fn add_docker_suggestions(&mut self, pressure: MemoryPressure) {
246 if !self.docker_manager.is_available() {
247 return;
248 }
249
250 for container in self.docker_manager.get_idle_containers() {
252 if container.memory_mb > 200.0 {
253 let priority = if pressure == MemoryPressure::Critical {
254 SuggestionPriority::High
255 } else if pressure == MemoryPressure::High {
256 SuggestionPriority::Medium
257 } else {
258 SuggestionPriority::Low
259 };
260
261 self.suggestions.push(Suggestion {
262 priority,
263 category: AppCategory::Container,
264 title: format!("Pause container {}", container.name),
265 description: format!(
266 "Container '{}' is idle but using {:.0} MB. Pause to save resources.",
267 container.name, container.memory_mb
268 ),
269 action: OptimizationAction::PauseContainer,
270 estimated_savings_mb: 0.0, app_name: Some(container.name.clone()),
272 pids: Vec::new(),
273 });
274 }
275 }
276
277 for container in self.docker_manager.get_containers() {
279 if container.memory_mb > 2000.0 {
280 self.suggestions.push(Suggestion {
281 priority: SuggestionPriority::Medium,
282 category: AppCategory::Container,
283 title: format!("Container {} high memory", container.name),
284 description: format!(
285 "Container '{}' ({}) is using {:.0} MB ({:.0}% of limit).",
286 container.name, container.image, container.memory_mb, container.memory_percent
287 ),
288 action: OptimizationAction::StopContainer,
289 estimated_savings_mb: container.memory_mb,
290 app_name: Some(container.name.clone()),
291 pids: Vec::new(),
292 });
293 }
294 }
295 }
296
297 fn add_general_suggestions(&mut self, pressure: MemoryPressure) {
299 let browser_pids: std::collections::HashSet<u32> = self
301 .browser_optimizer
302 .get_browsers()
303 .iter()
304 .flat_map(|b| b.pids.clone())
305 .collect();
306
307 let electron_pids: std::collections::HashSet<u32> = self
308 .electron_manager
309 .get_apps()
310 .iter()
311 .flat_map(|a| a.pids.clone())
312 .collect();
313
314 for (pid, process) in self.system.processes() {
315 let pid_u32 = pid.as_u32();
316
317 if browser_pids.contains(&pid_u32) || electron_pids.contains(&pid_u32) {
319 continue;
320 }
321
322 let memory_mb = process.memory() as f64 / (1024.0 * 1024.0);
323 let name = process.name().to_string_lossy().to_string();
324
325 if memory_mb > 1000.0 {
327 let priority = if memory_mb > 2000.0 || pressure == MemoryPressure::Critical {
328 SuggestionPriority::High
329 } else {
330 SuggestionPriority::Medium
331 };
332
333 self.suggestions.push(Suggestion {
334 priority,
335 category: AppCategory::Other,
336 title: format!("{} high memory", name),
337 description: format!(
338 "'{}' (PID {}) is using {:.0} MB. Consider closing if not needed.",
339 name, pid_u32, memory_mb
340 ),
341 action: OptimizationAction::Close,
342 estimated_savings_mb: memory_mb,
343 app_name: Some(name),
344 pids: vec![pid_u32],
345 });
346 }
347 }
348
349 if pressure == MemoryPressure::Critical {
351 let total = self.system.total_memory() as f64 / (1024.0 * 1024.0);
352 let used = self.system.used_memory() as f64 / (1024.0 * 1024.0);
353 let available = total - used;
354
355 self.suggestions.push(Suggestion {
356 priority: SuggestionPriority::Critical,
357 category: AppCategory::System,
358 title: "Critical memory pressure".to_string(),
359 description: format!(
360 "Only {:.0} MB available out of {:.0} MB total ({:.0}% used). System may become unstable.",
361 available, total, (used / total) * 100.0
362 ),
363 action: OptimizationAction::None,
364 estimated_savings_mb: 0.0,
365 app_name: None,
366 pids: Vec::new(),
367 });
368 }
369 }
370
371 pub fn get_suggestions(&self) -> &[Suggestion] {
373 &self.suggestions
374 }
375
376 pub fn get_by_category(&self, category: AppCategory) -> Vec<&Suggestion> {
378 self.suggestions
379 .iter()
380 .filter(|s| s.category == category)
381 .collect()
382 }
383
384 pub fn get_by_priority(&self, priority: SuggestionPriority) -> Vec<&Suggestion> {
386 self.suggestions
387 .iter()
388 .filter(|s| s.priority == priority)
389 .collect()
390 }
391
392 pub fn get_top(&self, n: usize) -> Vec<&Suggestion> {
394 self.suggestions.iter().take(n).collect()
395 }
396
397 pub fn total_potential_savings(&self) -> f64 {
399 self.suggestions.iter().map(|s| s.estimated_savings_mb).sum()
400 }
401
402 pub fn print_summary(&self) {
404 let pressure = self.memory_pressure();
405 let pressure_icon = match pressure {
406 MemoryPressure::Low => "š¢",
407 MemoryPressure::Normal => "š”",
408 MemoryPressure::High => "š ",
409 MemoryPressure::Critical => "š“",
410 };
411
412 println!("\nš” Smart Optimization Suggestions\n");
413 println!("Memory Pressure: {} {:?}", pressure_icon, pressure);
414
415 let total = self.system.total_memory() as f64 / (1024.0 * 1024.0);
416 let used = self.system.used_memory() as f64 / (1024.0 * 1024.0);
417 println!(
418 "Memory Usage: {:.0} MB / {:.0} MB ({:.0}%)\n",
419 used,
420 total,
421 (used / total) * 100.0
422 );
423
424 if self.suggestions.is_empty() {
425 println!("ā
No optimization suggestions at this time.");
426 return;
427 }
428
429 println!("āāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāā");
430 println!("ā Priority ā Suggestion ā Est. Savings ā");
431 println!("āāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāā¤");
432
433 for suggestion in self.suggestions.iter().take(10) {
434 let priority_icon = match suggestion.priority {
435 SuggestionPriority::Critical => "š“ Crit",
436 SuggestionPriority::High => "š High",
437 SuggestionPriority::Medium => "š” Med",
438 SuggestionPriority::Low => "š¢ Low",
439 };
440
441 let savings = if suggestion.estimated_savings_mb > 0.0 {
442 format!("{:.0} MB", suggestion.estimated_savings_mb)
443 } else {
444 "-".to_string()
445 };
446
447 println!(
448 "ā {:8} ā {:28} ā {:>13} ā",
449 priority_icon,
450 truncate(&suggestion.title, 28),
451 savings
452 );
453 }
454
455 println!("āāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāā");
456
457 let total_savings = self.total_potential_savings();
458 if total_savings > 0.0 {
459 println!("\nš° Total potential savings: {:.0} MB", total_savings);
460 }
461
462 println!("\nš Details:");
463 for (i, suggestion) in self.suggestions.iter().take(5).enumerate() {
464 println!("{}. {}", i + 1, suggestion.description);
465 }
466
467 if self.suggestions.len() > 5 {
468 println!(" ... and {} more suggestions", self.suggestions.len() - 5);
469 }
470 }
471}
472
473impl Default for SmartSuggestions {
474 fn default() -> Self {
475 Self::new()
476 }
477}
478
479fn truncate(s: &str, max: usize) -> String {
480 if s.len() <= max {
481 format!("{:width$}", s, width = max)
482 } else {
483 format!("{}...", &s[..max - 3])
484 }
485}