sorting_race/lib/
memory_graph.rs

1//! Memory graph widget for visualizing memory usage per algorithm
2
3use ratatui::{
4    buffer::Buffer,
5    layout::Rect,
6    style::{Color, Style},
7    widgets::{Block, Widget},
8};
9use std::collections::HashMap;
10
11/// Memory usage data for a single algorithm
12#[derive(Debug, Clone)]
13pub struct MemoryData {
14    pub current: usize,
15    pub peak: usize,
16    pub history: Vec<usize>,
17    pub has_started: bool,  // Track if algorithm has started processing
18}
19
20impl MemoryData {
21    pub fn new() -> Self {
22        Self {
23            current: 0,
24            peak: 0,
25            history: Vec::new(),
26            has_started: false,
27        }
28    }
29
30    pub fn update(&mut self, current: usize, max_history: usize) {
31        // Mark as started when we get first non-zero update
32        if current > 0 && !self.has_started {
33            self.has_started = true;
34        }
35
36        self.current = current;
37        if current > self.peak {
38            self.peak = current;
39        }
40
41        self.history.push(current);
42        if self.history.len() > max_history {
43            self.history.remove(0);
44        }
45    }
46
47    pub fn reset(&mut self) {
48        self.current = 0;
49        self.peak = 0;
50        self.history.clear();
51        self.has_started = false;
52    }
53}
54
55impl Default for MemoryData {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61/// Widget for displaying memory usage graphs
62#[derive(Debug, Clone)]
63pub struct MemoryGraph {
64    data: HashMap<String, MemoryData>,
65    max_history: usize,
66    style: Style,
67    current_style: Style,
68    peak_style: Style,
69    block: Option<Block<'static>>,
70    title: Option<String>,
71    show_values: bool,
72}
73
74impl MemoryGraph {
75    /// Create a new memory graph
76    pub fn new() -> Self {
77        Self {
78            data: HashMap::new(),
79            max_history: 100,
80            style: Style::default(),
81            current_style: Style::default().fg(Color::Green),
82            peak_style: Style::default().fg(Color::Red),
83            block: None,
84            title: None,
85            show_values: true,
86        }
87    }
88
89    /// Set the maximum history length
90    pub fn max_history(mut self, max: usize) -> Self {
91        self.max_history = max.max(1);
92        self
93    }
94
95    /// Set the base style
96    pub fn style(mut self, style: Style) -> Self {
97        self.style = style;
98        self
99    }
100
101    /// Set the style for current memory values
102    pub fn current_style(mut self, style: Style) -> Self {
103        self.current_style = style;
104        self
105    }
106
107    /// Set the style for peak memory values
108    pub fn peak_style(mut self, style: Style) -> Self {
109        self.peak_style = style;
110        self
111    }
112
113    /// Set the block
114    pub fn block(mut self, block: Block<'static>) -> Self {
115        self.block = Some(block);
116        self
117    }
118
119    /// Set the title
120    pub fn title<T>(mut self, title: T) -> Self 
121    where
122        T: Into<String>,
123    {
124        self.title = Some(title.into());
125        self
126    }
127
128    /// Set whether to show values
129    pub fn show_values(mut self, show: bool) -> Self {
130        self.show_values = show;
131        self
132    }
133
134    /// Update memory usage for an algorithm
135    pub fn update_algorithm(&mut self, name: &str, current_bytes: usize) {
136        let entry = self.data.entry(name.to_string()).or_default();
137        entry.update(current_bytes, self.max_history);
138    }
139
140    /// Get memory data for an algorithm
141    pub fn get_algorithm_data(&self, name: &str) -> Option<&MemoryData> {
142        self.data.get(name)
143    }
144
145    /// Get all algorithm names
146    pub fn algorithm_names(&self) -> Vec<&String> {
147        self.data.keys().collect()
148    }
149
150    /// Check if the memory graph has any data
151    pub fn is_empty(&self) -> bool {
152        self.data.is_empty()
153    }
154
155    /// Clear all data
156    pub fn clear(&mut self) {
157        self.data.clear();
158    }
159
160    /// Reset all algorithms' memory data (for race restart)
161    pub fn reset_all(&mut self) {
162        for data in self.data.values_mut() {
163            data.reset();
164        }
165    }
166
167    /// Format bytes into human readable string
168    fn format_bytes(bytes: usize) -> String {
169        const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
170        let mut size = bytes as f64;
171        let mut unit_index = 0;
172
173        while size >= 1024.0 && unit_index < UNITS.len() - 1 {
174            size /= 1024.0;
175            unit_index += 1;
176        }
177
178        if unit_index == 0 {
179            format!("{}B", bytes)
180        } else {
181            format!("{:.1}{}", size, UNITS[unit_index])
182        }
183    }
184
185    /// Render the memory graph to a buffer
186    pub fn render_widget(&self, area: Rect, buf: &mut Buffer) {
187        if area.width < 3 || area.height < 3 {
188            return;
189        }
190
191        let inner_area = if let Some(ref block) = self.block {
192            let inner = block.inner(area);
193            block.render(area, buf);
194            inner
195        } else {
196            area
197        };
198
199        if self.data.is_empty() {
200            // Show "no data" message in the middle of the area
201            let msg = "No memory data available";
202            let msg_x = inner_area.left() + (inner_area.width.saturating_sub(msg.len() as u16)) / 2;
203            let msg_y = inner_area.top() + inner_area.height / 2;
204
205            for (i, ch) in msg.chars().enumerate() {
206                let x = msg_x + i as u16;
207                if x < inner_area.right() && msg_y < inner_area.bottom() {
208                    buf[(x, msg_y)]
209                        .set_symbol(&ch.to_string())
210                        .set_style(self.style);
211                }
212            }
213            return;
214        }
215
216        let mut algorithms: Vec<_> = self.data.keys().cloned().collect();
217        algorithms.sort(); // Sort for consistent display order
218        let algorithm_count = algorithms.len();
219        
220        if algorithm_count == 0 {
221            return;
222        }
223
224        let line_height = if inner_area.height > algorithm_count as u16 {
225            inner_area.height / algorithm_count as u16
226        } else {
227            1
228        };
229
230        // Find maximum memory usage for scaling
231        let max_memory = self.data.values()
232            .map(|data| data.peak.max(data.current))
233            .max()
234            .unwrap_or(1);
235
236        for (i, algorithm) in algorithms.iter().enumerate() {
237            if let Some(memory_data) = self.data.get(algorithm) {
238                let y_start = inner_area.top() + (i as u16 * line_height);
239                let _y_end = (y_start + line_height).min(inner_area.bottom());
240
241                // Render algorithm name
242                let name_y = y_start;
243                if name_y < inner_area.bottom() {
244                    let display_name = if algorithm.len() > 10 {
245                        &algorithm[..10]
246                    } else {
247                        algorithm
248                    };
249                    
250                    for (char_idx, ch) in display_name.chars().enumerate() {
251                        let char_x = inner_area.left() + char_idx as u16;
252                        if char_x < inner_area.right() {
253                            buf[(char_x, name_y)]
254                                .set_symbol(&ch.to_string())
255                                .set_style(self.style);
256                        }
257                    }
258                }
259
260                // Render memory usage bars
261                if line_height > 1 && y_start + 1 < inner_area.bottom() {
262                    let bar_y = y_start + 1;
263                    let available_width = inner_area.width.saturating_sub(15); // Leave space for values
264                    
265                    // Current memory bar - only show if algorithm has started
266                    if max_memory > 0 && memory_data.has_started {
267                        let current_width = if memory_data.current > 0 {
268                            // Ensure at least 1 character width for any non-zero memory
269                            ((memory_data.current as f64 / max_memory as f64) * available_width as f64).max(1.0) as u16
270                        } else {
271                            0
272                        };
273                        for x in 0..current_width {
274                            let char_x = inner_area.left() + x;
275                            if char_x < inner_area.right() && bar_y < inner_area.bottom() {
276                                buf[(char_x, bar_y)]
277                                    .set_symbol("█")
278                                    .set_style(self.current_style);
279                            }
280                        }
281
282                        // Peak memory indicator (show as different character)
283                        let peak_width = ((memory_data.peak as f64 / max_memory as f64) * available_width as f64) as u16;
284                        if peak_width > current_width {
285                            for x in current_width..peak_width {
286                                let char_x = inner_area.left() + x;
287                                if char_x < inner_area.right() && bar_y < inner_area.bottom() {
288                                    buf[(char_x, bar_y)]
289                                        .set_symbol("░")
290                                        .set_style(self.peak_style);
291                                }
292                            }
293                        }
294                    }
295
296                    // Show values if enabled and algorithm has started
297                    if self.show_values && memory_data.has_started {
298                        let values_x = inner_area.right().saturating_sub(14);
299                        if values_x > inner_area.left() {
300                            let current_str = Self::format_bytes(memory_data.current);
301                            let peak_str = Self::format_bytes(memory_data.peak);
302                            let value_text = format!("{}/{}", current_str, peak_str);
303
304                            for (char_idx, ch) in value_text.chars().enumerate() {
305                                let char_x = values_x + char_idx as u16;
306                                if char_x < inner_area.right() && bar_y < inner_area.bottom() {
307                                    buf[(char_x, bar_y)]
308                                        .set_symbol(&ch.to_string())
309                                        .set_style(self.style);
310                                }
311                            }
312                        }
313                    } else if self.show_values && !memory_data.has_started {
314                        // Show "-" for algorithms that haven't started
315                        let values_x = inner_area.right().saturating_sub(14);
316                        if values_x > inner_area.left() && bar_y < inner_area.bottom() {
317                            buf[(values_x, bar_y)]
318                                .set_symbol("-")
319                                .set_style(self.style);
320                        }
321                    }
322                }
323            }
324        }
325    }
326}
327
328impl Default for MemoryGraph {
329    fn default() -> Self {
330        Self::new()
331    }
332}
333
334impl Widget for MemoryGraph {
335    fn render(self, area: Rect, buf: &mut Buffer) {
336        self.render_widget(area, buf);
337    }
338}
339
340impl Widget for &MemoryGraph {
341    fn render(self, area: Rect, buf: &mut Buffer) {
342        self.render_widget(area, buf);
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use ratatui::{
350        buffer::Buffer,
351        layout::Rect,
352    };
353
354    #[test]
355    fn test_memory_graph_creation() {
356        let graph = MemoryGraph::new();
357        assert_eq!(graph.data.len(), 0);
358        assert_eq!(graph.max_history, 100);
359    }
360
361    #[test]
362    fn test_update_algorithm() {
363        let mut graph = MemoryGraph::new();
364        graph.update_algorithm("QuickSort", 1024);
365        graph.update_algorithm("MergeSort", 2048);
366
367        assert_eq!(graph.data.len(), 2);
368        
369        let quick_data = graph.get_algorithm_data("QuickSort").unwrap();
370        assert_eq!(quick_data.current, 1024);
371        assert_eq!(quick_data.peak, 1024);
372        
373        let merge_data = graph.get_algorithm_data("MergeSort").unwrap();
374        assert_eq!(merge_data.current, 2048);
375        assert_eq!(merge_data.peak, 2048);
376    }
377
378    #[test]
379    fn test_peak_memory_tracking() {
380        let mut graph = MemoryGraph::new();
381        graph.update_algorithm("TestSort", 1024);
382        graph.update_algorithm("TestSort", 2048);
383        graph.update_algorithm("TestSort", 1536);
384
385        let data = graph.get_algorithm_data("TestSort").unwrap();
386        assert_eq!(data.current, 1536);
387        assert_eq!(data.peak, 2048);
388        assert_eq!(data.history.len(), 3);
389    }
390
391    #[test]
392    fn test_history_limit() {
393        let mut graph = MemoryGraph::new().max_history(3);
394        for i in 1..=5 {
395            graph.update_algorithm("TestSort", i * 100);
396        }
397
398        let data = graph.get_algorithm_data("TestSort").unwrap();
399        assert_eq!(data.history.len(), 3);
400        assert_eq!(data.history, vec![300, 400, 500]);
401    }
402
403    #[test]
404    fn test_format_bytes() {
405        assert_eq!(MemoryGraph::format_bytes(512), "512B");
406        assert_eq!(MemoryGraph::format_bytes(1024), "1.0KB");
407        assert_eq!(MemoryGraph::format_bytes(1536), "1.5KB");
408        assert_eq!(MemoryGraph::format_bytes(1048576), "1.0MB");
409        assert_eq!(MemoryGraph::format_bytes(1073741824), "1.0GB");
410    }
411
412    #[test]
413    fn test_render_widget() {
414        let mut graph = MemoryGraph::new();
415        graph.update_algorithm("QuickSort", 1024);
416        graph.update_algorithm("MergeSort", 2048);
417
418        let area = Rect::new(0, 0, 50, 10);
419        let mut buffer = Buffer::empty(area);
420
421        graph.render_widget(area, &mut buffer);
422        
423        // Should not panic and should have some content
424        let content = buffer.content();
425        assert!(!content.is_empty());
426    }
427
428    #[test]
429    fn test_clear() {
430        let mut graph = MemoryGraph::new();
431        graph.update_algorithm("TestSort", 1024);
432        assert_eq!(graph.data.len(), 1);
433
434        graph.clear();
435        assert_eq!(graph.data.len(), 0);
436    }
437}