1use crate::memory_debug::{get_memory_stats, AllocationInfo, MemoryStats};
35use std::fmt;
36
37const HORIZONTAL_BAR: char = '─';
39const FILLED_BLOCK: char = '█';
41const HALF_BLOCK: char = '▌';
43
44pub struct AllocationTimeline {
49 buckets: Vec<TimeBucket>,
51 time_span: std::time::Duration,
53}
54
55#[derive(Debug, Clone)]
57struct TimeBucket {
58 #[allow(dead_code)]
60 start_time: std::time::Instant,
61 bytes_allocated: usize,
63 #[allow(dead_code)]
65 allocation_count: usize,
66}
67
68impl AllocationTimeline {
69 pub fn new() -> Self {
71 Self {
72 buckets: Vec::new(),
73 time_span: std::time::Duration::from_secs(60),
74 }
75 }
76
77 pub fn with_time_span(mut self, duration: std::time::Duration) -> Self {
79 self.time_span = duration;
80 self
81 }
82
83 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 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 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 chart.push_str(&format!(" {}", Self::format_bytes(threshold)));
131 chart.push('\n');
132 }
133
134 chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
136 chart.push('\n');
137
138 chart
139 }
140
141 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 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
182pub struct SizeHistogram {
187 bins: Vec<HistogramBin>,
189}
190
191#[derive(Debug, Clone)]
193struct HistogramBin {
194 min_size: usize,
196 max_size: usize,
198 count: usize,
200 total_bytes: usize,
202}
203
204impl SizeHistogram {
205 pub fn new() -> Self {
207 Self { bins: Vec::new() }
208 }
209
210 pub fn build_from_allocations(&mut self, allocations: &[AllocationInfo]) {
216 let bin_edges = vec![
218 0,
219 1024, 10 * 1024, 100 * 1024, 1024 * 1024, 10 * 1024 * 1024, 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 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 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 chart.push_str("Allocation Size Distribution\n");
269 chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
270 chart.push('\n');
271
272 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
308pub struct MemoryMap {
313 regions: Vec<MemoryRegion>,
315}
316
317#[derive(Debug, Clone)]
319struct MemoryRegion {
320 start_offset: usize,
322 size: usize,
324 label: String,
326 is_allocated: bool,
328}
329
330impl MemoryMap {
331 pub fn new() -> Self {
333 Self {
334 regions: Vec::new(),
335 }
336 }
337
338 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 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 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 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 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 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
418pub struct AllocationSummary {
420 stats: MemoryStats,
422}
423
424impl AllocationSummary {
425 pub fn new() -> Self {
427 Self {
428 stats: get_memory_stats().unwrap_or_default(),
429 }
430 }
431
432 pub fn from_stats(stats: MemoryStats) -> Self {
434 Self { stats }
435 }
436
437 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 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 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('█')); }
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 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 assert!((efficiency - 80.0).abs() < 0.01);
632 }
633}