Skip to main content

torsh_core/
memory_visualization.rs

1//! Memory Allocation Visualization Tools
2//!
3//! Provides comprehensive visualization capabilities for memory allocations,
4//! building on top of the memory_debug module to present allocation data
5//! in easy-to-understand visual formats.
6//!
7//! # Features
8//!
9//! - **Allocation Timeline**: Visualize allocations over time
10//! - **Size Distribution**: Histogram of allocation sizes
11//! - **Leak Heatmap**: Visual representation of potential memory leaks
12//! - **Thread Allocation Map**: Per-thread allocation visualization
13//! - **ASCII Charts**: Terminal-friendly allocation charts
14//! - **Memory Maps**: Visual memory layout representation
15//!
16//! # Examples
17//!
18//! ```rust
19//! use torsh_core::memory_visualization::{
20//!     AllocationTimeline, SizeHistogram, MemoryMap
21//! };
22//!
23//! // Create an allocation timeline
24//! let timeline = AllocationTimeline::new();
25//! let chart = timeline.render_ascii(80, 20); // 80x20 character chart
26//! println!("{}", chart);
27//!
28//! // Generate a size distribution histogram
29//! let histogram = SizeHistogram::new();
30//! let viz = histogram.render_ascii(60, 15);
31//! println!("{}", viz);
32//! ```
33
34use crate::memory_debug::{get_memory_stats, AllocationInfo, MemoryStats};
35use std::fmt;
36
37/// ASCII character for horizontal bar in charts
38const HORIZONTAL_BAR: char = '─';
39/// ASCII character for filled block in histograms
40const FILLED_BLOCK: char = '█';
41/// ASCII character for half block in histograms
42const HALF_BLOCK: char = '▌';
43
44/// Allocation timeline visualization
45///
46/// Provides a time-series view of memory allocations, showing
47/// allocation patterns, spikes, and trends over time.
48pub struct AllocationTimeline {
49    /// Time buckets for grouping allocations
50    buckets: Vec<TimeBucket>,
51    /// Total time span covered
52    time_span: std::time::Duration,
53}
54
55/// Time bucket for allocation aggregation
56#[derive(Debug, Clone)]
57struct TimeBucket {
58    /// Start time of this bucket
59    #[allow(dead_code)]
60    start_time: std::time::Instant,
61    /// Total bytes allocated in this bucket
62    bytes_allocated: usize,
63    /// Number of allocations in this bucket
64    #[allow(dead_code)]
65    allocation_count: usize,
66}
67
68impl AllocationTimeline {
69    /// Create a new allocation timeline with default parameters
70    pub fn new() -> Self {
71        Self {
72            buckets: Vec::new(),
73            time_span: std::time::Duration::from_secs(60),
74        }
75    }
76
77    /// Set the time span for the timeline
78    pub fn with_time_span(mut self, duration: std::time::Duration) -> Self {
79        self.time_span = duration;
80        self
81    }
82
83    /// Render the timeline as ASCII art
84    ///
85    /// # Arguments
86    ///
87    /// * `width` - Width of the chart in characters
88    /// * `height` - Height of the chart in characters
89    ///
90    /// # Returns
91    ///
92    /// String containing the ASCII chart
93    pub fn render_ascii(&self, width: usize, height: usize) -> String {
94        if self.buckets.is_empty() {
95            return self.render_empty_chart(width, height);
96        }
97
98        let max_bytes = self
99            .buckets
100            .iter()
101            .map(|b| b.bytes_allocated)
102            .max()
103            .unwrap_or(1);
104
105        let mut chart = String::with_capacity(width * height * 2);
106
107        // Title
108        chart.push_str("Memory Allocation Timeline\n");
109        chart.push_str(&format!("Max: {} bytes\n", Self::format_bytes(max_bytes)));
110        chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
111        chart.push('\n');
112
113        // Render bars
114        for row in (0..height).rev() {
115            let threshold = (max_bytes * row) / height.max(1);
116
117            for bucket in &self.buckets {
118                let bar_height = (bucket.bytes_allocated * height) / max_bytes.max(1);
119
120                if bar_height > row {
121                    chart.push(FILLED_BLOCK);
122                } else if bar_height == row {
123                    chart.push(HALF_BLOCK);
124                } else {
125                    chart.push(' ');
126                }
127            }
128
129            // Y-axis label
130            chart.push_str(&format!(" {}", Self::format_bytes(threshold)));
131            chart.push('\n');
132        }
133
134        // X-axis
135        chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
136        chart.push('\n');
137
138        chart
139    }
140
141    /// Render an empty chart placeholder
142    fn render_empty_chart(&self, width: usize, height: usize) -> String {
143        let mut chart = String::new();
144        chart.push_str("Memory Allocation Timeline\n");
145        chart.push_str("No data available\n");
146        chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
147        chart.push('\n');
148
149        for _ in 0..height {
150            chart.push_str(&" ".repeat(width));
151            chart.push('\n');
152        }
153
154        chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
155        chart
156    }
157
158    /// Format bytes in human-readable form
159    fn format_bytes(bytes: usize) -> String {
160        const KB: usize = 1024;
161        const MB: usize = KB * 1024;
162        const GB: usize = MB * 1024;
163
164        if bytes >= GB {
165            format!("{:.2} GB", bytes as f64 / GB as f64)
166        } else if bytes >= MB {
167            format!("{:.2} MB", bytes as f64 / MB as f64)
168        } else if bytes >= KB {
169            format!("{:.2} KB", bytes as f64 / KB as f64)
170        } else {
171            format!("{} B", bytes)
172        }
173    }
174}
175
176impl Default for AllocationTimeline {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182/// Size distribution histogram
183///
184/// Visualizes the distribution of allocation sizes, helping identify
185/// allocation patterns and potential optimization opportunities.
186pub struct SizeHistogram {
187    /// Histogram bins
188    bins: Vec<HistogramBin>,
189}
190
191/// Histogram bin for size distribution
192#[derive(Debug, Clone)]
193struct HistogramBin {
194    /// Minimum size in this bin (inclusive)
195    min_size: usize,
196    /// Maximum size in this bin (exclusive)
197    max_size: usize,
198    /// Count of allocations in this bin
199    count: usize,
200    /// Total bytes in this bin
201    total_bytes: usize,
202}
203
204impl SizeHistogram {
205    /// Create a new size histogram
206    pub fn new() -> Self {
207        Self { bins: Vec::new() }
208    }
209
210    /// Build histogram from allocation data
211    ///
212    /// # Arguments
213    ///
214    /// * `allocations` - Slice of allocation information
215    pub fn build_from_allocations(&mut self, allocations: &[AllocationInfo]) {
216        // Create logarithmic bins
217        let bin_edges = vec![
218            0,
219            1024,             // 1 KB
220            10 * 1024,        // 10 KB
221            100 * 1024,       // 100 KB
222            1024 * 1024,      // 1 MB
223            10 * 1024 * 1024, // 10 MB
224            usize::MAX,
225        ];
226
227        self.bins.clear();
228        for i in 0..bin_edges.len() - 1 {
229            self.bins.push(HistogramBin {
230                min_size: bin_edges[i],
231                max_size: bin_edges[i + 1],
232                count: 0,
233                total_bytes: 0,
234            });
235        }
236
237        // Fill bins
238        for alloc in allocations {
239            for bin in &mut self.bins {
240                if alloc.size >= bin.min_size && alloc.size < bin.max_size {
241                    bin.count += 1;
242                    bin.total_bytes += alloc.size;
243                    break;
244                }
245            }
246        }
247    }
248
249    /// Render the histogram as ASCII art
250    ///
251    /// # Arguments
252    ///
253    /// * `width` - Width of the chart in characters
254    /// * `height` - Height of the chart in characters
255    ///
256    /// # Returns
257    ///
258    /// String containing the ASCII histogram
259    pub fn render_ascii(&self, width: usize, height: usize) -> String {
260        if self.bins.is_empty() {
261            return "Size Distribution Histogram\nNo data available\n".to_string();
262        }
263
264        let max_count = self.bins.iter().map(|b| b.count).max().unwrap_or(1);
265        let mut chart = String::with_capacity(width * height * 2);
266
267        // Title
268        chart.push_str("Allocation Size Distribution\n");
269        chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
270        chart.push('\n');
271
272        // Render histogram bars
273        for bin in &self.bins {
274            let bar_length = (bin.count * width) / max_count.max(1);
275            let label = format!(
276                "{:>8} - {:<8}",
277                AllocationTimeline::format_bytes(bin.min_size),
278                if bin.max_size == usize::MAX {
279                    "MAX".to_string()
280                } else {
281                    AllocationTimeline::format_bytes(bin.max_size)
282                }
283            );
284
285            chart.push_str(&label);
286            chart.push_str(": ");
287            chart.push_str(&FILLED_BLOCK.to_string().repeat(bar_length));
288            chart.push_str(&format!(
289                " ({}, {})\n",
290                bin.count,
291                AllocationTimeline::format_bytes(bin.total_bytes)
292            ));
293        }
294
295        chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
296        chart.push('\n');
297
298        chart
299    }
300}
301
302impl Default for SizeHistogram {
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308/// Memory map visualization
309///
310/// Provides a visual representation of memory layout, showing
311/// active allocations and their positions in memory.
312pub struct MemoryMap {
313    /// Memory regions to visualize
314    regions: Vec<MemoryRegion>,
315}
316
317/// Memory region for visualization
318#[derive(Debug, Clone)]
319struct MemoryRegion {
320    /// Start address (relative)
321    start_offset: usize,
322    /// Size in bytes
323    size: usize,
324    /// Region label
325    label: String,
326    /// Whether this region is allocated
327    is_allocated: bool,
328}
329
330impl MemoryMap {
331    /// Create a new memory map
332    pub fn new() -> Self {
333        Self {
334            regions: Vec::new(),
335        }
336    }
337
338    /// Add a region to the memory map
339    pub fn add_region(&mut self, offset: usize, size: usize, label: String, is_allocated: bool) {
340        self.regions.push(MemoryRegion {
341            start_offset: offset,
342            size,
343            label,
344            is_allocated,
345        });
346    }
347
348    /// Render the memory map as ASCII art
349    ///
350    /// # Arguments
351    ///
352    /// * `width` - Width of the visualization in characters
353    ///
354    /// # Returns
355    ///
356    /// String containing the ASCII memory map
357    pub fn render_ascii(&self, width: usize) -> String {
358        if self.regions.is_empty() {
359            return "Memory Map\nNo regions defined\n".to_string();
360        }
361
362        let max_offset = self
363            .regions
364            .iter()
365            .map(|r| r.start_offset + r.size)
366            .max()
367            .unwrap_or(1);
368
369        let mut chart = String::with_capacity(width * self.regions.len() * 3);
370
371        // Title
372        chart.push_str("Memory Layout Map\n");
373        chart.push_str(&format!(
374            "Total: {}\n",
375            AllocationTimeline::format_bytes(max_offset)
376        ));
377        chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
378        chart.push('\n');
379
380        // Render each region
381        for region in &self.regions {
382            let start_pos = (region.start_offset * width) / max_offset.max(1);
383            let region_width = (region.size * width) / max_offset.max(1).max(1);
384
385            let fill_char = if region.is_allocated {
386                FILLED_BLOCK
387            } else {
388                '░'
389            };
390
391            // Region visualization
392            chart.push_str(&" ".repeat(start_pos));
393            chart.push_str(&fill_char.to_string().repeat(region_width.max(1)));
394            chart.push('\n');
395
396            // Label
397            chart.push_str(&format!(
398                "{}: {} @ +{}\n",
399                region.label,
400                AllocationTimeline::format_bytes(region.size),
401                AllocationTimeline::format_bytes(region.start_offset)
402            ));
403        }
404
405        chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
406        chart.push('\n');
407
408        chart
409    }
410}
411
412impl Default for MemoryMap {
413    fn default() -> Self {
414        Self::new()
415    }
416}
417
418/// Allocation summary statistics with visualization
419pub struct AllocationSummary {
420    /// Memory statistics
421    stats: MemoryStats,
422}
423
424impl AllocationSummary {
425    /// Create a new allocation summary
426    pub fn new() -> Self {
427        Self {
428            stats: get_memory_stats().unwrap_or_default(),
429        }
430    }
431
432    /// Create from existing statistics
433    pub fn from_stats(stats: MemoryStats) -> Self {
434        Self { stats }
435    }
436
437    /// Render summary as formatted text
438    pub fn render(&self) -> String {
439        format!(
440            r#"
441Memory Allocation Summary
442═══════════════════════════════════════════════════════
443
444Active Allocations:  {}
445Total Allocated:     {}
446Peak Memory:         {}
447Lifetime Allocated:  {}
448Lifetime Freed:      {}
449
450Allocation Rate:     {} allocations
451Deallocation Rate:   {} deallocations
452Average Size:        {}
453
454Fragmentation:       {:.2}%
455Efficiency:          {:.2}%
456"#,
457            self.stats.active_allocations,
458            AllocationTimeline::format_bytes(self.stats.total_allocated),
459            AllocationTimeline::format_bytes(self.stats.peak_allocated),
460            AllocationTimeline::format_bytes(self.stats.lifetime_allocated),
461            AllocationTimeline::format_bytes(self.stats.lifetime_deallocated),
462            self.stats.total_allocations,
463            self.stats.total_deallocations,
464            AllocationTimeline::format_bytes(self.stats.average_allocation_size as usize),
465            self.calculate_fragmentation(),
466            self.calculate_efficiency(),
467        )
468    }
469
470    /// Calculate memory fragmentation percentage
471    fn calculate_fragmentation(&self) -> f64 {
472        if self.stats.peak_allocated == 0 {
473            return 0.0;
474        }
475
476        let fragmentation = ((self.stats.peak_allocated - self.stats.total_allocated) as f64
477            / self.stats.peak_allocated as f64)
478            * 100.0;
479
480        fragmentation.max(0.0).min(100.0)
481    }
482
483    /// Calculate allocation efficiency percentage
484    fn calculate_efficiency(&self) -> f64 {
485        if self.stats.lifetime_allocated == 0 {
486            return 100.0;
487        }
488
489        let efficiency =
490            (self.stats.lifetime_deallocated as f64 / self.stats.lifetime_allocated as f64) * 100.0;
491
492        efficiency.max(0.0).min(100.0)
493    }
494}
495
496impl Default for AllocationSummary {
497    fn default() -> Self {
498        Self::new()
499    }
500}
501
502impl fmt::Display for AllocationSummary {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        write!(f, "{}", self.render())
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    #[test]
513    fn test_allocation_timeline_render() {
514        let timeline = AllocationTimeline::new();
515        let chart = timeline.render_ascii(60, 10);
516
517        assert!(chart.contains("Memory Allocation Timeline"));
518        assert!(chart.contains("No data available"));
519    }
520
521    #[test]
522    fn test_size_histogram_render() {
523        let histogram = SizeHistogram::new();
524        let chart = histogram.render_ascii(60, 10);
525
526        assert!(chart.contains("Size Distribution Histogram"));
527        assert!(chart.contains("No data available"));
528    }
529
530    #[test]
531    fn test_size_histogram_with_data() {
532        use std::alloc::Layout;
533        use std::time::Instant;
534
535        let mut histogram = SizeHistogram::new();
536        let allocations = vec![
537            AllocationInfo {
538                id: 1,
539                size: 512,
540                layout: Layout::from_size_align(512, 8).expect("Layout should be valid"),
541                timestamp: Instant::now(),
542                backtrace: None,
543                tag: None,
544                is_active: true,
545                thread_id: std::thread::current().id(),
546            },
547            AllocationInfo {
548                id: 2,
549                size: 2048,
550                layout: Layout::from_size_align(2048, 8).expect("Layout should be valid"),
551                timestamp: Instant::now(),
552                backtrace: None,
553                tag: None,
554                is_active: true,
555                thread_id: std::thread::current().id(),
556            },
557        ];
558
559        histogram.build_from_allocations(&allocations);
560        let chart = histogram.render_ascii(60, 10);
561
562        assert!(chart.contains("Allocation Size Distribution"));
563        assert!(chart.contains('█')); // Should have bars
564    }
565
566    #[test]
567    fn test_memory_map_render() {
568        let mut map = MemoryMap::new();
569        map.add_region(0, 1024, "Tensor A".to_string(), true);
570        map.add_region(1024, 2048, "Tensor B".to_string(), true);
571        map.add_region(3072, 512, "Free Space".to_string(), false);
572
573        let chart = map.render_ascii(80);
574
575        assert!(chart.contains("Memory Layout Map"));
576        assert!(chart.contains("Tensor A"));
577        assert!(chart.contains("Tensor B"));
578        assert!(chart.contains("Free Space"));
579    }
580
581    #[test]
582    fn test_allocation_summary() {
583        let summary = AllocationSummary::new();
584        let rendered = summary.render();
585
586        assert!(rendered.contains("Memory Allocation Summary"));
587        assert!(rendered.contains("Active Allocations"));
588        assert!(rendered.contains("Peak Memory"));
589        assert!(rendered.contains("Fragmentation"));
590        assert!(rendered.contains("Efficiency"));
591    }
592
593    #[test]
594    fn test_format_bytes() {
595        assert_eq!(AllocationTimeline::format_bytes(512), "512 B");
596        assert_eq!(AllocationTimeline::format_bytes(2048), "2.00 KB");
597        assert_eq!(AllocationTimeline::format_bytes(1024 * 1024), "1.00 MB");
598        assert_eq!(
599            AllocationTimeline::format_bytes(1024 * 1024 * 1024),
600            "1.00 GB"
601        );
602    }
603
604    #[test]
605    fn test_fragmentation_calculation() {
606        let stats = MemoryStats {
607            total_allocated: 800,
608            peak_allocated: 1000,
609            ..Default::default()
610        };
611
612        let summary = AllocationSummary::from_stats(stats);
613        let fragmentation = summary.calculate_fragmentation();
614
615        // (1000 - 800) / 1000 = 0.2 = 20%
616        assert!((fragmentation - 20.0).abs() < 0.01);
617    }
618
619    #[test]
620    fn test_efficiency_calculation() {
621        let stats = MemoryStats {
622            lifetime_allocated: 10000,
623            lifetime_deallocated: 8000,
624            ..Default::default()
625        };
626
627        let summary = AllocationSummary::from_stats(stats);
628        let efficiency = summary.calculate_efficiency();
629
630        // 8000 / 10000 = 0.8 = 80%
631        assert!((efficiency - 80.0).abs() < 0.01);
632    }
633}