use crate::memory_debug::{get_memory_stats, AllocationInfo, MemoryStats};
use std::fmt;
const HORIZONTAL_BAR: char = '─';
const FILLED_BLOCK: char = '█';
const HALF_BLOCK: char = '▌';
pub struct AllocationTimeline {
buckets: Vec<TimeBucket>,
time_span: std::time::Duration,
}
#[derive(Debug, Clone)]
struct TimeBucket {
#[allow(dead_code)]
start_time: std::time::Instant,
bytes_allocated: usize,
#[allow(dead_code)]
allocation_count: usize,
}
impl AllocationTimeline {
pub fn new() -> Self {
Self {
buckets: Vec::new(),
time_span: std::time::Duration::from_secs(60),
}
}
pub fn with_time_span(mut self, duration: std::time::Duration) -> Self {
self.time_span = duration;
self
}
pub fn render_ascii(&self, width: usize, height: usize) -> String {
if self.buckets.is_empty() {
return self.render_empty_chart(width, height);
}
let max_bytes = self
.buckets
.iter()
.map(|b| b.bytes_allocated)
.max()
.unwrap_or(1);
let mut chart = String::with_capacity(width * height * 2);
chart.push_str("Memory Allocation Timeline\n");
chart.push_str(&format!("Max: {} bytes\n", Self::format_bytes(max_bytes)));
chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
chart.push('\n');
for row in (0..height).rev() {
let threshold = (max_bytes * row) / height.max(1);
for bucket in &self.buckets {
let bar_height = (bucket.bytes_allocated * height) / max_bytes.max(1);
if bar_height > row {
chart.push(FILLED_BLOCK);
} else if bar_height == row {
chart.push(HALF_BLOCK);
} else {
chart.push(' ');
}
}
chart.push_str(&format!(" {}", Self::format_bytes(threshold)));
chart.push('\n');
}
chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
chart.push('\n');
chart
}
fn render_empty_chart(&self, width: usize, height: usize) -> String {
let mut chart = String::new();
chart.push_str("Memory Allocation Timeline\n");
chart.push_str("No data available\n");
chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
chart.push('\n');
for _ in 0..height {
chart.push_str(&" ".repeat(width));
chart.push('\n');
}
chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
chart
}
fn format_bytes(bytes: usize) -> String {
const KB: usize = 1024;
const MB: usize = KB * 1024;
const GB: usize = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
}
impl Default for AllocationTimeline {
fn default() -> Self {
Self::new()
}
}
pub struct SizeHistogram {
bins: Vec<HistogramBin>,
}
#[derive(Debug, Clone)]
struct HistogramBin {
min_size: usize,
max_size: usize,
count: usize,
total_bytes: usize,
}
impl SizeHistogram {
pub fn new() -> Self {
Self { bins: Vec::new() }
}
pub fn build_from_allocations(&mut self, allocations: &[AllocationInfo]) {
let bin_edges = vec![
0,
1024, 10 * 1024, 100 * 1024, 1024 * 1024, 10 * 1024 * 1024, usize::MAX,
];
self.bins.clear();
for i in 0..bin_edges.len() - 1 {
self.bins.push(HistogramBin {
min_size: bin_edges[i],
max_size: bin_edges[i + 1],
count: 0,
total_bytes: 0,
});
}
for alloc in allocations {
for bin in &mut self.bins {
if alloc.size >= bin.min_size && alloc.size < bin.max_size {
bin.count += 1;
bin.total_bytes += alloc.size;
break;
}
}
}
}
pub fn render_ascii(&self, width: usize, height: usize) -> String {
if self.bins.is_empty() {
return "Size Distribution Histogram\nNo data available\n".to_string();
}
let max_count = self.bins.iter().map(|b| b.count).max().unwrap_or(1);
let mut chart = String::with_capacity(width * height * 2);
chart.push_str("Allocation Size Distribution\n");
chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
chart.push('\n');
for bin in &self.bins {
let bar_length = (bin.count * width) / max_count.max(1);
let label = format!(
"{:>8} - {:<8}",
AllocationTimeline::format_bytes(bin.min_size),
if bin.max_size == usize::MAX {
"MAX".to_string()
} else {
AllocationTimeline::format_bytes(bin.max_size)
}
);
chart.push_str(&label);
chart.push_str(": ");
chart.push_str(&FILLED_BLOCK.to_string().repeat(bar_length));
chart.push_str(&format!(
" ({}, {})\n",
bin.count,
AllocationTimeline::format_bytes(bin.total_bytes)
));
}
chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
chart.push('\n');
chart
}
}
impl Default for SizeHistogram {
fn default() -> Self {
Self::new()
}
}
pub struct MemoryMap {
regions: Vec<MemoryRegion>,
}
#[derive(Debug, Clone)]
struct MemoryRegion {
start_offset: usize,
size: usize,
label: String,
is_allocated: bool,
}
impl MemoryMap {
pub fn new() -> Self {
Self {
regions: Vec::new(),
}
}
pub fn add_region(&mut self, offset: usize, size: usize, label: String, is_allocated: bool) {
self.regions.push(MemoryRegion {
start_offset: offset,
size,
label,
is_allocated,
});
}
pub fn render_ascii(&self, width: usize) -> String {
if self.regions.is_empty() {
return "Memory Map\nNo regions defined\n".to_string();
}
let max_offset = self
.regions
.iter()
.map(|r| r.start_offset + r.size)
.max()
.unwrap_or(1);
let mut chart = String::with_capacity(width * self.regions.len() * 3);
chart.push_str("Memory Layout Map\n");
chart.push_str(&format!(
"Total: {}\n",
AllocationTimeline::format_bytes(max_offset)
));
chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
chart.push('\n');
for region in &self.regions {
let start_pos = (region.start_offset * width) / max_offset.max(1);
let region_width = (region.size * width) / max_offset.max(1).max(1);
let fill_char = if region.is_allocated {
FILLED_BLOCK
} else {
'░'
};
chart.push_str(&" ".repeat(start_pos));
chart.push_str(&fill_char.to_string().repeat(region_width.max(1)));
chart.push('\n');
chart.push_str(&format!(
"{}: {} @ +{}\n",
region.label,
AllocationTimeline::format_bytes(region.size),
AllocationTimeline::format_bytes(region.start_offset)
));
}
chart.push_str(&HORIZONTAL_BAR.to_string().repeat(width));
chart.push('\n');
chart
}
}
impl Default for MemoryMap {
fn default() -> Self {
Self::new()
}
}
pub struct AllocationSummary {
stats: MemoryStats,
}
impl AllocationSummary {
pub fn new() -> Self {
Self {
stats: get_memory_stats().unwrap_or_default(),
}
}
pub fn from_stats(stats: MemoryStats) -> Self {
Self { stats }
}
pub fn render(&self) -> String {
format!(
r#"
Memory Allocation Summary
═══════════════════════════════════════════════════════
Active Allocations: {}
Total Allocated: {}
Peak Memory: {}
Lifetime Allocated: {}
Lifetime Freed: {}
Allocation Rate: {} allocations
Deallocation Rate: {} deallocations
Average Size: {}
Fragmentation: {:.2}%
Efficiency: {:.2}%
"#,
self.stats.active_allocations,
AllocationTimeline::format_bytes(self.stats.total_allocated),
AllocationTimeline::format_bytes(self.stats.peak_allocated),
AllocationTimeline::format_bytes(self.stats.lifetime_allocated),
AllocationTimeline::format_bytes(self.stats.lifetime_deallocated),
self.stats.total_allocations,
self.stats.total_deallocations,
AllocationTimeline::format_bytes(self.stats.average_allocation_size as usize),
self.calculate_fragmentation(),
self.calculate_efficiency(),
)
}
fn calculate_fragmentation(&self) -> f64 {
if self.stats.peak_allocated == 0 {
return 0.0;
}
let fragmentation = ((self.stats.peak_allocated - self.stats.total_allocated) as f64
/ self.stats.peak_allocated as f64)
* 100.0;
fragmentation.max(0.0).min(100.0)
}
fn calculate_efficiency(&self) -> f64 {
if self.stats.lifetime_allocated == 0 {
return 100.0;
}
let efficiency =
(self.stats.lifetime_deallocated as f64 / self.stats.lifetime_allocated as f64) * 100.0;
efficiency.max(0.0).min(100.0)
}
}
impl Default for AllocationSummary {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for AllocationSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.render())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allocation_timeline_render() {
let timeline = AllocationTimeline::new();
let chart = timeline.render_ascii(60, 10);
assert!(chart.contains("Memory Allocation Timeline"));
assert!(chart.contains("No data available"));
}
#[test]
fn test_size_histogram_render() {
let histogram = SizeHistogram::new();
let chart = histogram.render_ascii(60, 10);
assert!(chart.contains("Size Distribution Histogram"));
assert!(chart.contains("No data available"));
}
#[test]
fn test_size_histogram_with_data() {
use std::alloc::Layout;
use std::time::Instant;
let mut histogram = SizeHistogram::new();
let allocations = vec![
AllocationInfo {
id: 1,
size: 512,
layout: Layout::from_size_align(512, 8).expect("Layout should be valid"),
timestamp: Instant::now(),
backtrace: None,
tag: None,
is_active: true,
thread_id: std::thread::current().id(),
},
AllocationInfo {
id: 2,
size: 2048,
layout: Layout::from_size_align(2048, 8).expect("Layout should be valid"),
timestamp: Instant::now(),
backtrace: None,
tag: None,
is_active: true,
thread_id: std::thread::current().id(),
},
];
histogram.build_from_allocations(&allocations);
let chart = histogram.render_ascii(60, 10);
assert!(chart.contains("Allocation Size Distribution"));
assert!(chart.contains('█')); }
#[test]
fn test_memory_map_render() {
let mut map = MemoryMap::new();
map.add_region(0, 1024, "Tensor A".to_string(), true);
map.add_region(1024, 2048, "Tensor B".to_string(), true);
map.add_region(3072, 512, "Free Space".to_string(), false);
let chart = map.render_ascii(80);
assert!(chart.contains("Memory Layout Map"));
assert!(chart.contains("Tensor A"));
assert!(chart.contains("Tensor B"));
assert!(chart.contains("Free Space"));
}
#[test]
fn test_allocation_summary() {
let summary = AllocationSummary::new();
let rendered = summary.render();
assert!(rendered.contains("Memory Allocation Summary"));
assert!(rendered.contains("Active Allocations"));
assert!(rendered.contains("Peak Memory"));
assert!(rendered.contains("Fragmentation"));
assert!(rendered.contains("Efficiency"));
}
#[test]
fn test_format_bytes() {
assert_eq!(AllocationTimeline::format_bytes(512), "512 B");
assert_eq!(AllocationTimeline::format_bytes(2048), "2.00 KB");
assert_eq!(AllocationTimeline::format_bytes(1024 * 1024), "1.00 MB");
assert_eq!(
AllocationTimeline::format_bytes(1024 * 1024 * 1024),
"1.00 GB"
);
}
#[test]
fn test_fragmentation_calculation() {
let stats = MemoryStats {
total_allocated: 800,
peak_allocated: 1000,
..Default::default()
};
let summary = AllocationSummary::from_stats(stats);
let fragmentation = summary.calculate_fragmentation();
assert!((fragmentation - 20.0).abs() < 0.01);
}
#[test]
fn test_efficiency_calculation() {
let stats = MemoryStats {
lifetime_allocated: 10000,
lifetime_deallocated: 8000,
..Default::default()
};
let summary = AllocationSummary::from_stats(stats);
let efficiency = summary.calculate_efficiency();
assert!((efficiency - 80.0).abs() < 0.01);
}
}