Skip to main content

ruvector_memopt/apps/
suggestions.rs

1//! Smart suggestions engine
2//!
3//! Analyzes system state and provides intelligent optimization recommendations:
4//! - Prioritizes suggestions by impact
5//! - Considers user patterns and usage
6//! - Provides actionable recommendations
7//! - Learns from system behavior
8
9use 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/// Optimization suggestion
20#[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/// Suggestion priority level
33#[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/// System memory pressure level
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum MemoryPressure {
55    Low,      // <50% used
56    Normal,   // 50-70% used
57    High,     // 70-85% used
58    Critical, // >85% used
59}
60
61/// Smart suggestions engine
62pub 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    /// Refresh all data and generate suggestions
82    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    /// Get current memory pressure level
92    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    /// Generate all suggestions
114    fn generate_suggestions(&mut self) {
115        self.suggestions.clear();
116
117        let pressure = self.memory_pressure();
118
119        // Browser suggestions
120        self.add_browser_suggestions(pressure);
121
122        // Electron app suggestions
123        self.add_electron_suggestions(pressure);
124
125        // Docker suggestions
126        self.add_docker_suggestions(pressure);
127
128        // General high-memory process suggestions
129        self.add_general_suggestions(pressure);
130
131        // Sort by priority (highest first) then by estimated savings
132        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    /// Add browser-related suggestions
140    fn add_browser_suggestions(&mut self, pressure: MemoryPressure) {
141        for browser in self.browser_optimizer.get_browsers() {
142            // Tab reduction suggestions
143            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; // ~30% savings
154
155                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            // High memory browser warning
173            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    /// Add Electron app suggestions
192    fn add_electron_suggestions(&mut self, pressure: MemoryPressure) {
193        for app in self.electron_manager.get_apps() {
194            // Bloated app warning
195            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            // Very high memory apps
226            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    /// Add Docker container suggestions
245    fn add_docker_suggestions(&mut self, pressure: MemoryPressure) {
246        if !self.docker_manager.is_available() {
247            return;
248        }
249
250        // Idle containers
251        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, // Pausing doesn't free memory but saves CPU
271                    app_name: Some(container.name.clone()),
272                    pids: Vec::new(),
273                });
274            }
275        }
276
277        // High memory containers
278        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    /// Add general process suggestions
298    fn add_general_suggestions(&mut self, pressure: MemoryPressure) {
299        // Find high-memory processes not covered by specific optimizers
300        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            // Skip if already covered
318            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            // High memory processes
326            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        // Memory pressure warning
350        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    /// Get all suggestions
372    pub fn get_suggestions(&self) -> &[Suggestion] {
373        &self.suggestions
374    }
375
376    /// Get suggestions by category
377    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    /// Get suggestions by priority
385    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    /// Get top N suggestions
393    pub fn get_top(&self, n: usize) -> Vec<&Suggestion> {
394        self.suggestions.iter().take(n).collect()
395    }
396
397    /// Get total potential savings
398    pub fn total_potential_savings(&self) -> f64 {
399        self.suggestions.iter().map(|s| s.estimated_savings_mb).sum()
400    }
401
402    /// Print suggestions summary
403    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}