1use crate::core::tracker::MemoryTracker;
4use crate::core::types::{AllocationInfo, MemoryStats, TrackingResult, TypeMemoryUsage};
5use crate::utils::{format_bytes, get_category_color, simplify_type_name};
6
7fn calculate_allocation_percentiles(allocations: &[AllocationInfo]) -> (usize, usize) {
10 if allocations.is_empty() {
11 return (0, 0);
12 }
13
14 let mut sizes: Vec<usize> = allocations.iter().map(|a| a.size).collect();
16 sizes.sort_unstable();
17
18 let len = sizes.len();
19
20 let median = if len % 2 == 0 {
22 (sizes[len / 2 - 1] + sizes[len / 2]) / 2
23 } else {
24 sizes[len / 2]
25 };
26
27 let p95_index = ((len as f64) * 0.95) as usize;
29 let p95 = if p95_index >= len {
30 sizes[len - 1]
31 } else {
32 sizes[p95_index]
33 };
34
35 (median, p95)
36}
37use std::collections::HashMap;
38use std::fs::File;
39use std::io::Write;
40use std::path::Path;
41use svg::node::element::{Circle, Rectangle, Text as SvgText};
42use svg::Document;
43
44pub fn enhance_type_information(
46 memory_by_type: &[TypeMemoryUsage],
47 allocations: &[AllocationInfo],
48) -> Vec<EnhancedTypeInfo> {
49 let mut enhanced_types = Vec::new();
50 let mut inner_type_stats: std::collections::HashMap<String, (usize, usize, Vec<String>)> =
51 std::collections::HashMap::new();
52
53 for usage in memory_by_type {
54 if usage.type_name == "Unknown" {
56 continue;
57 }
58
59 let (simplified_name, category, subcategory) =
61 analyze_type_with_detailed_subcategory(&usage.type_name);
62
63 let variable_names: Vec<String> = allocations
65 .iter()
66 .filter_map(|alloc| {
67 if let (Some(var_name), Some(type_name)) = (&alloc.var_name, &alloc.type_name) {
68 let (alloc_simplified, _, _) =
69 analyze_type_with_detailed_subcategory(type_name);
70 if alloc_simplified == simplified_name {
71 Some(var_name.to_string())
72 } else {
73 None
74 }
75 } else {
76 None
77 }
78 })
79 .take(5) .collect();
81
82 enhanced_types.push(EnhancedTypeInfo {
84 simplified_name,
85 category,
86 subcategory,
87 total_size: usage.total_size,
88 allocation_count: usage.allocation_count,
89 variable_names,
90 });
91
92 extract_and_accumulate_inner_types_enhanced(
94 &usage.type_name,
95 usage.total_size,
96 usage.allocation_count,
97 &mut inner_type_stats,
98 );
99 }
100
101 for (inner_type, (total_size, allocation_count, var_names)) in inner_type_stats {
103 let (simplified_name, category, subcategory) =
104 analyze_type_with_detailed_subcategory(&inner_type);
105 enhanced_types.push(EnhancedTypeInfo {
108 simplified_name,
109 category,
110 subcategory,
111 total_size,
112 allocation_count,
113 variable_names: var_names.into_iter().take(5).collect(),
114 });
115 }
116
117 enhanced_types
124}
125
126fn analyze_type_with_detailed_subcategory(type_name: &str) -> (String, String, String) {
128 let clean_type = type_name.trim();
129
130 if clean_type.is_empty() || clean_type == "Unknown" {
132 return (
133 "Unknown Type".to_string(),
134 "Unknown".to_string(),
135 "Other".to_string(),
136 );
137 }
138
139 if clean_type.contains("Box<") {
141 let inner = extract_generic_inner_type(clean_type, "Box");
142 return (inner, "Smart Pointers".to_string(), "Box<T>".to_string());
143 }
144
145 if clean_type.contains("Rc<") {
146 let inner = extract_generic_inner_type(clean_type, "Rc");
147 return (inner, "Smart Pointers".to_string(), "Rc<T>".to_string());
148 }
149
150 if clean_type.contains("Arc<") {
151 let inner = extract_generic_inner_type(clean_type, "Arc");
152 return (inner, "Smart Pointers".to_string(), "Arc<T>".to_string());
153 }
154
155 if clean_type.contains("Vec<") || clean_type.contains("vec::Vec") {
157 let inner = extract_generic_inner_type(clean_type, "Vec");
158 return (
159 format!("Vec<{inner}>"),
160 "Collections".to_string(),
161 "Vec<T>".to_string(),
162 );
163 }
164
165 if clean_type.contains("HashMap") || clean_type.contains("hash_map") {
166 return (
167 "HashMap<K,V>".to_string(),
168 "Collections".to_string(),
169 "HashMap<K,V>".to_string(),
170 );
171 }
172
173 if clean_type.contains("HashSet") || clean_type.contains("hash_set") {
174 return (
175 "HashSet<T>".to_string(),
176 "Collections".to_string(),
177 "HashSet<T>".to_string(),
178 );
179 }
180
181 if clean_type.contains("BTreeMap") || clean_type.contains("btree_map") {
182 return (
183 "BTreeMap<K,V>".to_string(),
184 "Collections".to_string(),
185 "BTreeMap<K,V>".to_string(),
186 );
187 }
188
189 if clean_type.contains("BTreeSet") || clean_type.contains("btree_set") {
190 return (
191 "BTreeSet<T>".to_string(),
192 "Collections".to_string(),
193 "BTreeSet<T>".to_string(),
194 );
195 }
196
197 if clean_type.contains("VecDeque") || clean_type.contains("vec_deque") {
198 return (
199 "VecDeque<T>".to_string(),
200 "Collections".to_string(),
201 "VecDeque<T>".to_string(),
202 );
203 }
204
205 if clean_type.contains("LinkedList") {
206 return (
207 "LinkedList<T>".to_string(),
208 "Collections".to_string(),
209 "LinkedList<T>".to_string(),
210 );
211 }
212
213 if clean_type.contains("String") || clean_type.contains("string::String") {
215 return (
216 "String".to_string(),
217 "Basic Types".to_string(),
218 "Strings".to_string(),
219 );
220 }
221
222 if clean_type.contains("&str") || clean_type == "str" {
223 return (
224 "&str".to_string(),
225 "Basic Types".to_string(),
226 "Strings".to_string(),
227 );
228 }
229
230 if clean_type == "i32" || clean_type.ends_with("::i32") {
232 return (
233 "i32".to_string(),
234 "Basic Types".to_string(),
235 "Integers".to_string(),
236 );
237 }
238 if clean_type == "i64" || clean_type.ends_with("::i64") {
239 return (
240 "i64".to_string(),
241 "Basic Types".to_string(),
242 "Integers".to_string(),
243 );
244 }
245 if clean_type == "u32" || clean_type.ends_with("::u32") {
246 return (
247 "u32".to_string(),
248 "Basic Types".to_string(),
249 "Integers".to_string(),
250 );
251 }
252 if clean_type == "u64" || clean_type.ends_with("::u64") {
253 return (
254 "u64".to_string(),
255 "Basic Types".to_string(),
256 "Integers".to_string(),
257 );
258 }
259 if clean_type == "usize" || clean_type.ends_with("::usize") {
260 return (
261 "usize".to_string(),
262 "Basic Types".to_string(),
263 "Integers".to_string(),
264 );
265 }
266 if clean_type == "isize" || clean_type.ends_with("::isize") {
267 return (
268 "isize".to_string(),
269 "Basic Types".to_string(),
270 "Integers".to_string(),
271 );
272 }
273 if clean_type == "i8" || clean_type.ends_with("::i8") {
274 return (
275 "i8".to_string(),
276 "Basic Types".to_string(),
277 "Integers".to_string(),
278 );
279 }
280 if clean_type == "u8" || clean_type.ends_with("::u8") {
281 return (
282 "u8".to_string(),
283 "Basic Types".to_string(),
284 "Integers".to_string(),
285 );
286 }
287 if clean_type == "i16" || clean_type.ends_with("::i16") {
288 return (
289 "i16".to_string(),
290 "Basic Types".to_string(),
291 "Integers".to_string(),
292 );
293 }
294 if clean_type == "u16" || clean_type.ends_with("::u16") {
295 return (
296 "u16".to_string(),
297 "Basic Types".to_string(),
298 "Integers".to_string(),
299 );
300 }
301
302 if clean_type == "f32" || clean_type.ends_with("::f32") {
304 return (
305 "f32".to_string(),
306 "Basic Types".to_string(),
307 "Floats".to_string(),
308 );
309 }
310 if clean_type == "f64" || clean_type.ends_with("::f64") {
311 return (
312 "f64".to_string(),
313 "Basic Types".to_string(),
314 "Floats".to_string(),
315 );
316 }
317
318 if clean_type == "bool" || clean_type.ends_with("::bool") {
320 return (
321 "bool".to_string(),
322 "Basic Types".to_string(),
323 "Booleans".to_string(),
324 );
325 }
326 if clean_type == "char" || clean_type.ends_with("::char") {
327 return (
328 "char".to_string(),
329 "Basic Types".to_string(),
330 "Characters".to_string(),
331 );
332 }
333
334 let (simplified_name, category) = simplify_type_name(clean_type);
336 (simplified_name, category, "Other".to_string())
337}
338
339fn extract_generic_inner_type(type_name: &str, container: &str) -> String {
341 if let Some(start) = type_name.find(&format!("{container}<")) {
342 let start = start + container.len() + 1;
343 if let Some(end) = type_name[start..].rfind('>') {
344 let inner = &type_name[start..start + end];
345 return inner.split("::").last().unwrap_or(inner).to_string();
346 }
347 }
348 "?".to_string()
349}
350
351fn extract_and_accumulate_inner_types_enhanced(
353 type_name: &str,
354 size: usize,
355 count: usize,
356 stats: &mut std::collections::HashMap<String, (usize, usize, Vec<String>)>,
357) {
358 let inner_types = extract_inner_primitive_types_enhanced(type_name);
360
361 for inner_type in inner_types {
367 let entry = stats.entry(inner_type).or_insert((0, 0, Vec::new()));
368 entry.0 += size / 4; entry.1 += count;
370 entry.2.push(format!("from {type_name}"));
371 }
373}
374
375fn extract_inner_primitive_types_enhanced(type_name: &str) -> Vec<String> {
377 let mut inner_types = Vec::new();
378
379 let primitives = [
381 "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
382 "f32", "f64", "bool", "char",
383 ];
384
385 for primitive in &primitives {
386 if type_name.contains(primitive) {
387 if type_name.contains(&format!("{primitive}>"))
389 || type_name.contains(&format!("{primitive},"))
390 || type_name.contains(&format!(" {primitive}"))
391 || type_name.ends_with(primitive)
392 {
393 inner_types.push(primitive.to_string());
394 }
395 }
396 }
397
398 inner_types
399}
400
401pub fn categorize_allocations(allocations: &[AllocationInfo]) -> Vec<AllocationCategory> {
403 let mut categories: HashMap<String, AllocationCategory> = HashMap::new();
404
405 for allocation in allocations {
406 if allocation.var_name.is_none()
408 || allocation.type_name.as_ref().is_none_or(|t| t == "Unknown")
409 {
410 continue;
411 }
412
413 let type_name = allocation
414 .type_name
415 .as_ref()
416 .expect("Type name should be available after None check");
417 let (_, category_name) = simplify_type_name(type_name);
418 let category_name_clone = category_name.clone();
419
420 let category =
421 categories
422 .entry(category_name.clone())
423 .or_insert_with(|| AllocationCategory {
424 name: category_name_clone,
425 allocations: Vec::new(),
426 total_size: 0,
427 color: get_category_color(&category_name),
428 });
429
430 category.allocations.push(allocation.clone());
431 category.total_size += allocation.size;
432 }
433
434 let mut result: Vec<_> = categories.into_values().collect();
435 result.sort_by(|a, b| b.total_size.cmp(&a.total_size));
436 result
437}
438
439pub fn categorize_enhanced_allocations(
442 enhanced_types: &[EnhancedTypeInfo],
443) -> Vec<AllocationCategory> {
444 let mut categories: HashMap<String, AllocationCategory> = HashMap::new();
445
446 for enhanced_type in enhanced_types {
447 if enhanced_type.simplified_name == "Unknown" {
449 continue;
450 }
451
452 let category_name = &enhanced_type.category;
453
454 let category = categories
455 .entry(category_name.to_string())
456 .or_insert_with(|| AllocationCategory {
457 name: category_name.to_string(),
458 allocations: Vec::new(),
459 total_size: 0,
460 color: get_category_color(category_name),
461 });
462
463 let mut synthetic_allocation = AllocationInfo::new(0, enhanced_type.total_size);
465 synthetic_allocation.var_name = Some(enhanced_type.variable_names.join(", "));
466 synthetic_allocation.type_name = Some(enhanced_type.simplified_name.clone());
467
468 category.allocations.push(synthetic_allocation);
469 category.total_size += enhanced_type.total_size;
470 }
471
472 let mut result: Vec<_> = categories.into_values().collect();
473 result.sort_by(|a, b| b.total_size.cmp(&a.total_size));
474 result
475}
476
477#[derive(Debug, Clone)]
479pub struct EnhancedTypeInfo {
480 pub simplified_name: String,
482 pub category: String,
484 pub subcategory: String,
486 pub total_size: usize,
488 pub allocation_count: usize,
490 pub variable_names: Vec<String>,
492}
493
494#[derive(Debug, Clone)]
496pub struct AllocationCategory {
497 pub name: String,
499 pub allocations: Vec<AllocationInfo>,
501 pub total_size: usize,
503 pub color: String,
505}
506
507pub fn export_enhanced_svg<P: AsRef<Path>>(tracker: &MemoryTracker, path: P) -> TrackingResult<()> {
509 let path = path.as_ref();
510
511 if let Some(parent) = path.parent() {
513 if !parent.exists() {
514 std::fs::create_dir_all(parent)?;
515 }
516 }
517
518 let active_allocations = tracker.get_active_allocations()?;
519 let memory_by_type = tracker.get_memory_by_type()?;
520 let stats = tracker.get_stats()?;
521
522 let enhanced_memory_by_type = enhance_type_information(&memory_by_type, &active_allocations);
524 let _categorized_allocations = categorize_allocations(&active_allocations);
525
526 let mut document = Document::new()
528 .set("viewBox", (0, 0, 1800, 400))
529 .set("width", 1800)
530 .set("height", 400)
531 .set(
532 "style",
533 "background: linear-gradient(135deg, #2C3E50 0%, #34495E 100%); font-family: 'Segoe UI', Arial, sans-serif;",
534 );
535
536 let title = SvgText::new("Top 3 Memory Analysis - Compact View")
540 .set("x", 700)
541 .set("y", 30)
542 .set("text-anchor", "middle")
543 .set("font-size", 24)
544 .set("font-weight", "bold")
545 .set("fill", "#FFFFFF");
546 document = document.add(title);
547
548 if !enhanced_memory_by_type.is_empty() {
550 document = add_compact_type_chart(document, &enhanced_memory_by_type)?;
551 }
552
553 document = add_compact_summary(document, &stats, &active_allocations)?;
555
556 let mut file = File::create(path)?;
558 write!(file, "{document}")?;
559
560 Ok(())
561}
562
563fn add_compact_type_chart(
565 mut document: Document,
566 types: &[EnhancedTypeInfo],
567) -> TrackingResult<Document> {
568 let chart_x = 100;
569 let chart_y = 60;
570 let chart_width = 1200;
571
572 if types.is_empty() {
573 let no_data = SvgText::new("No tracked variables found")
574 .set("x", chart_x + chart_width / 2)
575 .set("y", 200)
576 .set("text-anchor", "middle")
577 .set("font-size", 16)
578 .set("fill", "#E74C3C");
579 document = document.add(no_data);
580 return Ok(document);
581 }
582
583 let max_size = types.iter().map(|t| t.total_size).max().unwrap_or(1);
584
585 for (i, type_info) in types.iter().take(3).enumerate() {
587 let y = chart_y + (i as i32) * 80;
588
589 let bg_bar = Rectangle::new()
591 .set("x", chart_x)
592 .set("y", y)
593 .set("width", 600)
594 .set("height", 30)
595 .set("fill", "#34495E")
596 .set("stroke", "#ECF0F1")
597 .set("stroke-width", 1)
598 .set("rx", 6);
599 document = document.add(bg_bar);
600
601 let bar_width = ((type_info.total_size as f64 / max_size as f64) * 600.0) as i32;
603 let color = get_category_color(&type_info.category);
604 let progress_bar = Rectangle::new()
605 .set("x", chart_x)
606 .set("y", y)
607 .set("width", bar_width)
608 .set("height", 30)
609 .set("fill", color)
610 .set("rx", 6);
611 document = document.add(progress_bar);
612
613 let content_text = format!(
615 "{} ({} vars) | Total: {}",
616 type_info.simplified_name,
617 type_info.allocation_count,
618 format_bytes(type_info.total_size)
619 );
620
621 let content_label = SvgText::new(content_text)
622 .set("x", chart_x + 10)
623 .set("y", y + 20)
624 .set("font-size", 12)
625 .set("font-weight", "600")
626 .set("fill", "#FFFFFF")
627 .set("text-shadow", "1px 1px 2px rgba(0,0,0,0.8)");
628 document = document.add(content_label);
629
630 let percentage = (type_info.total_size as f64 / max_size as f64 * 100.0) as i32;
632 let percent_label = SvgText::new(format!("{percentage}%"))
633 .set("x", chart_x + 720)
634 .set("y", y + 20)
635 .set("font-size", 14)
636 .set("font-weight", "bold")
637 .set("fill", "#ECF0F1");
638 document = document.add(percent_label);
639
640 let var_names_text = if type_info.variable_names.is_empty() {
642 "no tracked vars".to_string()
643 } else {
644 format!("Variables: {}", type_info.variable_names.join(", "))
645 };
646
647 let vars_label = SvgText::new(var_names_text)
648 .set("x", chart_x + 10)
649 .set("y", y + 45)
650 .set("font-size", 9)
651 .set("fill", "#94A3B8")
652 .set("font-style", "italic");
653 document = document.add(vars_label);
654 }
655
656 Ok(document)
657}
658
659fn add_compact_summary(
661 mut document: Document,
662 stats: &MemoryStats,
663 allocations: &[AllocationInfo],
664) -> TrackingResult<Document> {
665 let tracked_vars = allocations.iter().filter(|a| a.var_name.is_some()).count();
666
667 let summary_text = format!(
668 "Showing TOP 3 memory-consuming types | Total tracked: {} variables | Active memory: {}",
669 tracked_vars,
670 format_bytes(stats.active_memory)
671 );
672
673 let summary = SvgText::new(summary_text)
674 .set("x", 700)
675 .set("y", 370)
676 .set("text-anchor", "middle")
677 .set("font-size", 14)
678 .set("font-weight", "bold")
679 .set("fill", "#ECF0F1");
680 document = document.add(summary);
681
682 Ok(document)
683}
684
685pub fn add_enhanced_header(
687 mut document: Document,
688 stats: &MemoryStats,
689 allocations: &[AllocationInfo],
690) -> TrackingResult<Document> {
691 let title = SvgText::new("Rust Memory Usage Analysis")
693 .set("x", 900)
694 .set("y", 40)
695 .set("text-anchor", "middle")
696 .set("font-size", 28)
697 .set("font-weight", "300")
698 .set("fill", "#2c3e50")
699 .set("style", "letter-spacing: 1px;");
700
701 document = document.add(title);
702
703 let active_memory = stats.active_memory;
705 let peak_memory = stats.peak_memory;
706 let active_allocations = stats.active_allocations;
707
708 let memory_reclamation_rate = if stats.total_allocated > 0 {
709 (stats.total_deallocated as f64 / stats.total_allocated as f64) * 100.0
710 } else {
711 0.0
712 };
713
714 let allocator_efficiency = if stats.peak_memory > 0 {
715 (stats.active_memory as f64 / stats.peak_memory as f64) * 100.0
716 } else {
717 0.0
718 };
719
720 let (median_alloc_size, p95_alloc_size) = calculate_allocation_percentiles(allocations);
721
722 let memory_fragmentation = if stats.peak_memory > 0 {
723 ((stats.peak_memory - stats.active_memory) as f64 / stats.peak_memory as f64) * 100.0
724 } else {
725 0.0
726 };
727
728 let metrics = [
730 (
731 "Active Memory",
732 format_bytes(active_memory),
733 (active_memory as f64 / peak_memory.max(1) as f64 * 100.0).min(100.0),
734 "#3498db",
735 ),
736 (
737 "Peak Memory",
738 {
739 let formatted = format_bytes(peak_memory);
740 tracing::info!(
741 "SVG add_enhanced_header - Formatting peak_memory: {} bytes -> {}",
742 peak_memory,
743 formatted
744 );
745 formatted
746 },
747 100.0,
748 "#e74c3c",
749 ),
750 (
751 "Active Allocs",
752 format!("{active_allocations}"),
753 (active_allocations as f64 / 1000.0 * 100.0).min(100.0),
754 "#2ecc71",
755 ),
756 (
757 "Reclamation",
758 format!("{memory_reclamation_rate:.1}%"),
759 memory_reclamation_rate,
760 "#f39c12",
761 ),
762 (
763 "Efficiency",
764 format!("{allocator_efficiency:.1}%"),
765 allocator_efficiency,
766 "#9b59b6",
767 ),
768 (
769 "Median Size",
770 format_bytes(median_alloc_size),
771 (median_alloc_size as f64 / 1024.0 * 100.0).min(100.0),
772 "#1abc9c",
773 ),
774 (
775 "P95 Size",
776 format_bytes(p95_alloc_size),
777 (p95_alloc_size as f64 / 4096.0 * 100.0).min(100.0),
778 "#e67e22",
779 ),
780 (
781 "Fragmentation",
782 format!("{memory_fragmentation:.1}%"),
783 memory_fragmentation,
784 "#95a5a6",
785 ),
786 ];
787
788 let card_width = 200;
790 let card_height = 120;
791 let start_x = 50;
792 let start_y = 130;
793 let spacing_x = 220;
794
795 let header = SvgText::new("KEY PERFORMANCE METRICS")
797 .set("x", 900)
798 .set("y", start_y - 20)
799 .set("text-anchor", "middle")
800 .set("font-size", 11)
801 .set("font-weight", "600")
802 .set("fill", "#7f8c8d")
803 .set("style", "letter-spacing: 2px;");
804 document = document.add(header);
805
806 for (i, (title, value, percentage, color)) in metrics.iter().enumerate() {
808 let x = start_x + i * spacing_x;
809 let y = start_y;
810
811 let card_bg = Rectangle::new()
813 .set("x", x)
814 .set("y", y)
815 .set("width", card_width)
816 .set("height", card_height)
817 .set("fill", "#ffffff")
818 .set("stroke", "none")
819 .set("rx", 12)
820 .set("style", "filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));");
821
822 if i == 0 {
824 }
826
827 document = document.add(card_bg);
828
829 let ring_center_x = x + 40;
831 let ring_center_y = y + 60;
832 let ring_radius = 25;
833
834 let ring_bg = Circle::new()
835 .set("cx", ring_center_x)
836 .set("cy", ring_center_y)
837 .set("r", ring_radius)
838 .set("fill", "none")
839 .set("stroke", "#ecf0f1")
840 .set("stroke-width", 6);
841 document = document.add(ring_bg);
842
843 let circumference = 2.0 * std::f64::consts::PI * ring_radius as f64;
845 let progress_offset = circumference * (1.0 - percentage / 100.0);
846
847 let progress_ring = Circle::new()
848 .set("cx", ring_center_x)
849 .set("cy", ring_center_y)
850 .set("r", ring_radius)
851 .set("fill", "none")
852 .set("stroke", *color)
853 .set("stroke-width", 6)
854 .set("stroke-linecap", "round")
855 .set(
856 "stroke-dasharray",
857 format!("{circumference} {circumference}"),
858 )
859 .set("stroke-dashoffset", progress_offset)
860 .set(
861 "transform",
862 format!("rotate(-90 {ring_center_x} {ring_center_y})"),
863 )
864 .set("style", "transition: stroke-dashoffset 0.5s ease;");
865 document = document.add(progress_ring);
866
867 let percent_text = SvgText::new(format!("{percentage:.0}%"))
869 .set("x", ring_center_x)
870 .set("y", ring_center_y + 4)
871 .set("text-anchor", "middle")
872 .set("font-size", 12)
873 .set("font-weight", "bold")
874 .set("fill", *color);
875 document = document.add(percent_text);
876
877 let title_text = SvgText::new(*title)
879 .set("x", x + 90)
880 .set("y", y + 35)
881 .set("font-size", 12)
882 .set("font-weight", "600")
883 .set("fill", "#2c3e50");
884 document = document.add(title_text);
885
886 let value_text = SvgText::new(value)
888 .set("x", x + 90)
889 .set("y", y + 55)
890 .set("font-size", 16)
891 .set("font-weight", "bold")
892 .set("fill", "#2c3e50");
893 document = document.add(value_text);
894
895 let status_color = if *percentage >= 80.0 {
897 "#e74c3c" } else if *percentage >= 50.0 {
899 "#f39c12" } else {
901 "#27ae60" };
903
904 let status_dot = Circle::new()
905 .set("cx", x + 90)
906 .set("cy", y + 75)
907 .set("r", 4)
908 .set("fill", status_color);
909 document = document.add(status_dot);
910
911 let status_text = if *percentage >= 80.0 {
913 "HIGH"
914 } else if *percentage >= 50.0 {
915 "MEDIUM"
916 } else {
917 "OPTIMAL"
918 };
919
920 let status_label = SvgText::new(status_text)
921 .set("x", x + 105)
922 .set("y", y + 79)
923 .set("font-size", 9)
924 .set("font-weight", "600")
925 .set("fill", status_color);
926 document = document.add(status_label);
927 }
928
929 Ok(document)
930}
931
932pub fn add_enhanced_type_chart(
934 mut document: Document,
935 types: &[EnhancedTypeInfo],
936) -> TrackingResult<Document> {
937 let chart_x = 50;
939 let chart_y = 320; let chart_width = 850;
941 let chart_height = 300; let title = SvgText::new("Memory Usage by Type - Treemap Visualization")
945 .set("x", chart_x + chart_width / 2)
946 .set("y", chart_y - 10)
947 .set("text-anchor", "middle")
948 .set("font-size", 16)
949 .set("font-weight", "bold")
950 .set("fill", "#2c3e50");
951
952 document = document.add(title);
953
954 let styles = svg::node::element::Style::new(
956 r#"
957 .integrated-treemap-rect {
958 transition: all 0.3s ease;
959 cursor: pointer;
960 stroke: #ffffff;
961 stroke-width: 2;
962 }
963 .integrated-treemap-rect:hover {
964 stroke: #2c3e50;
965 stroke-width: 3;
966 filter: brightness(1.1);
967 }
968 .integrated-treemap-label {
969 fill: #ffffff;
970 font-weight: 700;
971 text-anchor: middle;
972 dominant-baseline: middle;
973 pointer-events: none;
974 text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
975 }
976 .integrated-treemap-percentage {
977 fill: #f8f9fa;
978 font-weight: 600;
979 text-anchor: middle;
980 dominant-baseline: middle;
981 pointer-events: none;
982 text-shadow: 1px 1px 2px rgba(0,0,0,0.6);
983 }
984 "#,
985 );
986 document = document.add(styles);
987
988 let treemap_area = IntegratedTreemapArea {
990 x: chart_x as f64,
991 y: chart_y as f64,
992 width: chart_width as f64,
993 height: chart_height as f64,
994 };
995
996 document = render_real_data_treemap(document, treemap_area, types)?;
998
999 Ok(document)
1002}
1003
1004#[derive(Debug, Clone)]
1006struct IntegratedTreemapArea {
1007 x: f64,
1008 y: f64,
1009 width: f64,
1010 height: f64,
1011}
1012
1013fn render_real_data_treemap(
1016 mut document: Document,
1017 area: IntegratedTreemapArea,
1018 types: &[EnhancedTypeInfo],
1019) -> TrackingResult<Document> {
1020 if types.is_empty() {
1021 let no_data_rect = Rectangle::new()
1022 .set("x", area.x)
1023 .set("y", area.y)
1024 .set("width", area.width)
1025 .set("height", area.height)
1026 .set("fill", "#f8f9fa")
1027 .set("stroke", "#dee2e6")
1028 .set("stroke-width", 2)
1029 .set("rx", 10);
1030 document = document.add(no_data_rect);
1031
1032 let no_data_text = SvgText::new("No Memory Type Data Available")
1033 .set("x", area.x + area.width / 2.0)
1034 .set("y", area.y + area.height / 2.0)
1035 .set("text-anchor", "middle")
1036 .set("font-size", 16)
1037 .set("font-weight", "bold")
1038 .set("fill", "#6c757d");
1039 document = document.add(no_data_text);
1040
1041 return Ok(document);
1042 }
1043
1044 let total_memory: usize = types.iter().map(|t| t.total_size).sum();
1046 if total_memory == 0 {
1047 return Ok(document);
1048 }
1049
1050 let mut collections_types = Vec::new();
1052 let mut basic_types = Vec::new();
1053 let mut smart_pointers_types = Vec::new();
1054 let mut other_types = Vec::new();
1055
1056 for type_info in types {
1057 match type_info.category.as_str() {
1058 "Collections" => collections_types.push(type_info),
1059 "Basic Types" => basic_types.push(type_info),
1060 "Strings" => basic_types.push(type_info), "Smart Pointers" => smart_pointers_types.push(type_info),
1062 _ => other_types.push(type_info),
1063 }
1064 }
1065
1066 let _collections_total: usize = collections_types.iter().map(|t| t.total_size).sum();
1068 let _basic_types_total: usize = basic_types.iter().map(|t| t.total_size).sum();
1069 let _smart_pointers_total: usize = smart_pointers_types.iter().map(|t| t.total_size).sum();
1070 let _other_total: usize = other_types.iter().map(|t| t.total_size).sum();
1071
1072 let layout_strategy = analyze_data_distribution(
1074 &collections_types,
1075 &basic_types,
1076 &smart_pointers_types,
1077 &other_types,
1078 total_memory,
1079 );
1080
1081 let treemap_data = build_adaptive_treemap_data(
1083 collections_types,
1084 basic_types,
1085 smart_pointers_types,
1086 other_types,
1087 total_memory,
1088 &layout_strategy,
1089 );
1090
1091 document = render_adaptive_treemap(document, area, &treemap_data, &layout_strategy)?;
1093
1094 Ok(document)
1095}
1096
1097#[derive(Debug, Clone)]
1099struct TreemapNode {
1100 name: String,
1102 size: usize,
1104 percentage: f64,
1106 color: String,
1108 children: Vec<TreemapNode>,
1110}
1111
1112#[derive(Debug, Clone)]
1114#[allow(clippy::enum_variant_names)]
1115enum TreemapLayoutStrategy {
1116 FullLayout,
1118 DominantCategoryLayout { dominant_category: String },
1120 MinimalLayout,
1122 CollectionsOnlyLayout,
1124 BasicTypesOnlyLayout,
1126}
1127
1128fn analyze_data_distribution(
1130 collections_types: &[&EnhancedTypeInfo],
1131 basic_types: &[&EnhancedTypeInfo],
1132 smart_pointers_types: &[&EnhancedTypeInfo],
1133 _other_types: &[&EnhancedTypeInfo],
1134 total_memory: usize,
1135) -> TreemapLayoutStrategy {
1136 let collections_total: usize = collections_types.iter().map(|t| t.total_size).sum();
1137 let basic_types_total: usize = basic_types.iter().map(|t| t.total_size).sum();
1138 let smart_pointers_total: usize = smart_pointers_types.iter().map(|t| t.total_size).sum();
1139
1140 let collections_percentage = if total_memory > 0 {
1141 (collections_total as f64 / total_memory as f64) * 100.0
1142 } else {
1143 0.0
1144 };
1145 let basic_types_percentage = if total_memory > 0 {
1146 (basic_types_total as f64 / total_memory as f64) * 100.0
1147 } else {
1148 0.0
1149 };
1150 let smart_pointers_percentage = if total_memory > 0 {
1151 (smart_pointers_total as f64 / total_memory as f64) * 100.0
1152 } else {
1153 0.0
1154 };
1155
1156 let active_categories = [
1158 (collections_total > 0, "Collections"),
1159 (basic_types_total > 0, "Basic Types"),
1160 (smart_pointers_total > 0, "Smart Pointers"),
1161 ]
1162 .iter()
1163 .filter(|(active, _)| *active)
1164 .count();
1165
1166 match active_categories {
1167 0 => TreemapLayoutStrategy::MinimalLayout,
1168 1 => {
1169 if collections_percentage > 80.0 {
1171 TreemapLayoutStrategy::CollectionsOnlyLayout
1172 } else if basic_types_percentage > 80.0 {
1173 TreemapLayoutStrategy::BasicTypesOnlyLayout
1174 } else {
1175 TreemapLayoutStrategy::DominantCategoryLayout {
1176 dominant_category: if collections_total > basic_types_total
1177 && collections_total > smart_pointers_total
1178 {
1179 "Collections".to_string()
1180 } else if basic_types_total > smart_pointers_total {
1181 "Basic Types".to_string()
1182 } else {
1183 "Smart Pointers".to_string()
1184 },
1185 }
1186 }
1187 }
1188 2 => {
1189 if collections_percentage > 70.0
1191 || basic_types_percentage > 70.0
1192 || smart_pointers_percentage > 70.0
1193 {
1194 TreemapLayoutStrategy::DominantCategoryLayout {
1195 dominant_category: if collections_total > basic_types_total
1196 && collections_total > smart_pointers_total
1197 {
1198 "Collections".to_string()
1199 } else if basic_types_total > smart_pointers_total {
1200 "Basic Types".to_string()
1201 } else {
1202 "Smart Pointers".to_string()
1203 },
1204 }
1205 } else {
1206 TreemapLayoutStrategy::FullLayout
1207 }
1208 }
1209 _ => TreemapLayoutStrategy::FullLayout, }
1211}
1212
1213fn build_adaptive_treemap_data(
1215 collections_types: Vec<&EnhancedTypeInfo>,
1216 basic_types: Vec<&EnhancedTypeInfo>,
1217 smart_pointers_types: Vec<&EnhancedTypeInfo>,
1218 _other_types: Vec<&EnhancedTypeInfo>,
1219 total_memory: usize,
1220 strategy: &TreemapLayoutStrategy,
1221) -> Vec<TreemapNode> {
1222 let mut treemap_nodes = Vec::new();
1223
1224 match strategy {
1225 TreemapLayoutStrategy::MinimalLayout => {
1226 treemap_nodes.push(TreemapNode {
1228 name: "No Significant Memory Usage".to_string(),
1229 size: total_memory.max(1),
1230 percentage: 100.0,
1231 color: "#95a5a6".to_string(),
1232 children: Vec::new(),
1233 });
1234 }
1235 TreemapLayoutStrategy::CollectionsOnlyLayout => {
1236 let collections_total: usize = collections_types.iter().map(|t| t.total_size).sum();
1238 if collections_total > 0 {
1239 treemap_nodes.push(build_collections_node(&collections_types, total_memory));
1240 }
1241 }
1242 TreemapLayoutStrategy::BasicTypesOnlyLayout => {
1243 let basic_types_total: usize = basic_types.iter().map(|t| t.total_size).sum();
1245 if basic_types_total > 0 {
1246 treemap_nodes.push(build_basic_types_node(&basic_types, total_memory));
1247 }
1248 }
1249 TreemapLayoutStrategy::DominantCategoryLayout { dominant_category } => {
1250 match dominant_category.as_str() {
1252 "Collections" => {
1253 let collections_total: usize =
1254 collections_types.iter().map(|t| t.total_size).sum();
1255 if collections_total > 0 {
1256 treemap_nodes
1257 .push(build_collections_node(&collections_types, total_memory));
1258 }
1259 add_simple_categories(
1261 &mut treemap_nodes,
1262 &basic_types,
1263 &smart_pointers_types,
1264 total_memory,
1265 );
1266 }
1267 "Basic Types" => {
1268 let basic_types_total: usize = basic_types.iter().map(|t| t.total_size).sum();
1269 if basic_types_total > 0 {
1270 treemap_nodes.push(build_basic_types_node(&basic_types, total_memory));
1271 }
1272 add_simple_categories_alt(
1274 &mut treemap_nodes,
1275 &collections_types,
1276 &smart_pointers_types,
1277 total_memory,
1278 );
1279 }
1280 _ => {
1281 build_full_layout(
1283 &mut treemap_nodes,
1284 &collections_types,
1285 &basic_types,
1286 &smart_pointers_types,
1287 total_memory,
1288 );
1289 }
1290 }
1291 }
1292 TreemapLayoutStrategy::FullLayout => {
1293 build_full_layout(
1295 &mut treemap_nodes,
1296 &collections_types,
1297 &basic_types,
1298 &smart_pointers_types,
1299 total_memory,
1300 );
1301 }
1302 }
1303
1304 treemap_nodes
1305}
1306
1307fn build_collections_node(
1309 collections_types: &[&EnhancedTypeInfo],
1310 total_memory: usize,
1311) -> TreemapNode {
1312 let collections_total: usize = collections_types.iter().map(|t| t.total_size).sum();
1313
1314 if collections_total == 0 {
1319 return TreemapNode {
1320 name: "Collections".to_string(),
1321 size: 1,
1322 percentage: 0.0,
1323 color: "#ecf0f1".to_string(),
1324 children: Vec::new(),
1325 };
1326 }
1327
1328 let mut subcategory_groups: std::collections::HashMap<String, Vec<&EnhancedTypeInfo>> =
1330 std::collections::HashMap::new();
1331
1332 for collection_type in collections_types {
1333 subcategory_groups
1334 .entry(collection_type.subcategory.clone())
1335 .or_default()
1336 .push(collection_type);
1337 }
1338
1339 let mut collections_children = Vec::new();
1340
1341 let subcategory_colors = [
1343 ("Vec<T>", "#e74c3c"),
1344 ("HashMap<K,V>", "#3498db"),
1345 ("HashSet<T>", "#9b59b6"),
1346 ("BTreeMap<K,V>", "#2ecc71"),
1347 ("BTreeSet<T>", "#27ae60"),
1348 ("VecDeque<T>", "#f39c12"),
1349 ("LinkedList<T>", "#e67e22"),
1350 ("Other", "#95a5a6"),
1351 ]
1352 .iter()
1353 .cloned()
1354 .collect::<std::collections::HashMap<&str, &str>>();
1355
1356 for (subcategory, types) in subcategory_groups {
1357 let category_total: usize = types.iter().map(|t| t.total_size).sum();
1358
1359 if category_total > 0 {
1367 let relative_percentage = (category_total as f64 / collections_total as f64) * 100.0;
1368 let color = subcategory_colors
1369 .get(subcategory.as_str())
1370 .unwrap_or(&"#95a5a6")
1371 .to_string();
1372
1373 collections_children.push(TreemapNode {
1374 name: format!("{subcategory} ({relative_percentage:.1}%)"),
1375 size: category_total,
1376 percentage: (category_total as f64 / total_memory as f64) * 100.0,
1377 color,
1378 children: Vec::new(),
1379 });
1380 }
1381 }
1382
1383 collections_children.sort_by(|a, b| b.size.cmp(&a.size));
1385
1386 TreemapNode {
1387 name: format!(
1388 "Collections ({:.1}%)",
1389 (collections_total as f64 / total_memory as f64) * 100.0
1390 ),
1391 size: collections_total,
1392 percentage: (collections_total as f64 / total_memory as f64) * 100.0,
1393 color: "#ecf0f1".to_string(),
1394 children: collections_children,
1395 }
1396}
1397
1398fn build_basic_types_node(basic_types: &[&EnhancedTypeInfo], total_memory: usize) -> TreemapNode {
1400 let basic_types_total: usize = basic_types.iter().map(|t| t.total_size).sum();
1401
1402 if basic_types_total == 0 {
1409 return TreemapNode {
1410 name: "Basic Types".to_string(),
1411 size: 1,
1412 percentage: 0.0,
1413 color: "#ecf0f1".to_string(),
1414 children: Vec::new(),
1415 };
1416 }
1417
1418 let mut subcategory_groups: std::collections::HashMap<String, Vec<&EnhancedTypeInfo>> =
1420 std::collections::HashMap::new();
1421
1422 for basic_type in basic_types {
1423 subcategory_groups
1424 .entry(basic_type.subcategory.clone())
1425 .or_default()
1426 .push(basic_type);
1427 }
1428
1429 let mut basic_types_children = Vec::new();
1430
1431 let subcategory_colors = [
1433 ("Strings", "#2ecc71"),
1434 ("Integers", "#3498db"),
1435 ("Floats", "#e74c3c"),
1436 ("Booleans", "#f39c12"),
1437 ("Characters", "#9b59b6"),
1438 ("Arrays", "#1abc9c"),
1439 ("References", "#e67e22"),
1440 ("Other", "#95a5a6"),
1441 ]
1442 .iter()
1443 .cloned()
1444 .collect::<std::collections::HashMap<&str, &str>>();
1445
1446 for (subcategory, types) in subcategory_groups {
1447 let category_total: usize = types.iter().map(|t| t.total_size).sum();
1448
1449 if category_total > 0 {
1457 let relative_percentage = (category_total as f64 / basic_types_total as f64) * 100.0;
1458 let color = subcategory_colors
1459 .get(subcategory.as_str())
1460 .unwrap_or(&"#95a5a6")
1461 .to_string();
1462
1463 basic_types_children.push(TreemapNode {
1464 name: format!("{subcategory} ({relative_percentage:.1}%)"),
1465 size: category_total,
1466 percentage: (category_total as f64 / total_memory as f64) * 100.0,
1467 color,
1468 children: Vec::new(),
1469 });
1470 }
1471 }
1472
1473 basic_types_children.sort_by(|a, b| b.size.cmp(&a.size));
1475
1476 TreemapNode {
1483 name: format!(
1484 "Basic Types ({:.1}%)",
1485 (basic_types_total as f64 / total_memory as f64) * 100.0
1486 ),
1487 size: basic_types_total,
1488 percentage: (basic_types_total as f64 / total_memory as f64) * 100.0,
1489 color: "#ecf0f1".to_string(),
1490 children: basic_types_children,
1491 }
1492}
1493
1494fn add_simple_categories(
1496 treemap_nodes: &mut Vec<TreemapNode>,
1497 basic_types: &[&EnhancedTypeInfo],
1498 smart_pointers_types: &[&EnhancedTypeInfo],
1499 total_memory: usize,
1500) {
1501 let basic_types_total: usize = basic_types.iter().map(|t| t.total_size).sum();
1502 if basic_types_total > 0 {
1503 treemap_nodes.push(TreemapNode {
1504 name: "Basic Types".to_string(),
1505 size: basic_types_total,
1506 percentage: (basic_types_total as f64 / total_memory as f64) * 100.0,
1507 color: "#f39c12".to_string(),
1508 children: Vec::new(),
1509 });
1510 }
1511
1512 let smart_pointers_total: usize = smart_pointers_types.iter().map(|t| t.total_size).sum();
1513 if smart_pointers_total > 0 {
1514 treemap_nodes.push(TreemapNode {
1515 name: "Smart Pointers".to_string(),
1516 size: smart_pointers_total,
1517 percentage: (smart_pointers_total as f64 / total_memory as f64) * 100.0,
1518 color: "#9b59b6".to_string(),
1519 children: Vec::new(),
1520 });
1521 }
1522}
1523
1524fn add_simple_categories_alt(
1526 treemap_nodes: &mut Vec<TreemapNode>,
1527 collections_types: &[&EnhancedTypeInfo],
1528 smart_pointers_types: &[&EnhancedTypeInfo],
1529 total_memory: usize,
1530) {
1531 let collections_total: usize = collections_types.iter().map(|t| t.total_size).sum();
1532 if collections_total > 0 {
1533 treemap_nodes.push(TreemapNode {
1534 name: "Collections".to_string(),
1535 size: collections_total,
1536 percentage: (collections_total as f64 / total_memory as f64) * 100.0,
1537 color: "#3498db".to_string(),
1538 children: Vec::new(),
1539 });
1540 }
1541
1542 let smart_pointers_total: usize = smart_pointers_types.iter().map(|t| t.total_size).sum();
1543 if smart_pointers_total > 0 {
1544 treemap_nodes.push(TreemapNode {
1545 name: "Smart Pointers".to_string(),
1546 size: smart_pointers_total,
1547 percentage: (smart_pointers_total as f64 / total_memory as f64) * 100.0,
1548 color: "#9b59b6".to_string(),
1549 children: Vec::new(),
1550 });
1551 }
1552}
1553
1554fn build_full_layout(
1556 treemap_nodes: &mut Vec<TreemapNode>,
1557 collections_types: &[&EnhancedTypeInfo],
1558 basic_types: &[&EnhancedTypeInfo],
1559 smart_pointers_types: &[&EnhancedTypeInfo],
1560 total_memory: usize,
1561) {
1562 let collections_total: usize = collections_types.iter().map(|t| t.total_size).sum();
1563 if collections_total > 0 {
1564 treemap_nodes.push(build_collections_node(collections_types, total_memory));
1565 }
1566
1567 let basic_types_total: usize = basic_types.iter().map(|t| t.total_size).sum();
1568 if basic_types_total > 0 {
1569 treemap_nodes.push(build_basic_types_node(basic_types, total_memory));
1570 }
1571
1572 let smart_pointers_total: usize = smart_pointers_types.iter().map(|t| t.total_size).sum();
1573 if smart_pointers_total > 0 {
1574 treemap_nodes.push(TreemapNode {
1575 name: "Smart Pointers".to_string(),
1576 size: smart_pointers_total,
1577 percentage: (smart_pointers_total as f64 / total_memory as f64) * 100.0,
1578 color: "#9b59b6".to_string(),
1579 children: Vec::new(),
1580 });
1581 }
1582}
1583
1584fn render_adaptive_treemap(
1586 document: Document,
1587 area: IntegratedTreemapArea,
1588 treemap_data: &[TreemapNode],
1589 _strategy: &TreemapLayoutStrategy,
1590) -> TrackingResult<Document> {
1591 render_squarified_treemap(document, area, treemap_data)
1593}
1594
1595fn render_squarified_treemap(
1597 mut document: Document,
1598 area: IntegratedTreemapArea,
1599 nodes: &[TreemapNode],
1600) -> TrackingResult<Document> {
1601 if nodes.is_empty() {
1602 return render_empty_treemap(document, area);
1603 }
1604
1605 let container_bg = Rectangle::new()
1607 .set("class", "integrated-treemap-container")
1608 .set("x", area.x)
1609 .set("y", area.y)
1610 .set("width", area.width)
1611 .set("height", area.height)
1612 .set("fill", "#ecf0f1")
1613 .set("stroke", "#bdc3c7")
1614 .set("stroke-width", 2)
1615 .set("rx", 20);
1616 document = document.add(container_bg);
1617
1618 let total_size: usize = nodes.iter().map(|n| n.size).sum();
1620 if total_size == 0 {
1621 return render_empty_treemap(document, area);
1622 }
1623
1624 let padding = 15.0;
1626 let band_spacing = 10.0;
1627 let available_width = area.width - (padding * 2.0);
1628 let available_height =
1629 area.height - (padding * 2.0) - (band_spacing * (nodes.len() - 1) as f64);
1630
1631 let mut current_y = area.y + padding;
1632
1633 let mut sorted_nodes = nodes.to_vec();
1635 sorted_nodes.sort_by(|a, b| b.size.cmp(&a.size));
1636
1637 for node in &sorted_nodes {
1638 let band_height = (node.size as f64 / total_size as f64) * available_height;
1640
1641 if !node.children.is_empty() {
1643 document = render_smart_horizontal_band(
1644 document,
1645 area.x + padding,
1646 current_y,
1647 available_width,
1648 band_height,
1649 node,
1650 )?;
1651 } else {
1652 document = render_simple_horizontal_band(
1653 document,
1654 area.x + padding,
1655 current_y,
1656 available_width,
1657 band_height,
1658 node,
1659 )?;
1660 }
1661
1662 current_y += band_height + band_spacing;
1663 }
1664
1665 Ok(document)
1668}
1669
1670fn render_empty_treemap(
1672 mut document: Document,
1673 area: IntegratedTreemapArea,
1674) -> TrackingResult<Document> {
1675 let container_bg = Rectangle::new()
1676 .set("class", "integrated-treemap-container")
1677 .set("x", area.x)
1678 .set("y", area.y)
1679 .set("width", area.width)
1680 .set("height", area.height)
1681 .set("fill", "#f8f9fa")
1682 .set("stroke", "#dee2e6")
1683 .set("stroke-width", 2)
1684 .set("rx", 20);
1685 document = document.add(container_bg);
1686
1687 let no_data_text = SvgText::new("No Memory Type Data Available")
1688 .set("x", area.x + area.width / 2.0)
1689 .set("y", area.y + area.height / 2.0)
1690 .set("text-anchor", "middle")
1691 .set("font-size", 16)
1692 .set("font-weight", "bold")
1693 .set("fill", "#6c757d");
1694 document = document.add(no_data_text);
1695
1696 Ok(document)
1697}
1698
1699fn render_smart_horizontal_band(
1701 mut document: Document,
1702 x: f64,
1703 y: f64,
1704 width: f64,
1705 height: f64,
1706 node: &TreemapNode,
1707) -> TrackingResult<Document> {
1708 let band_bg = Rectangle::new()
1710 .set("class", "integrated-treemap-rect")
1711 .set("x", x)
1712 .set("y", y)
1713 .set("width", width)
1714 .set("height", height)
1715 .set("fill", node.color.as_str())
1716 .set("rx", 18);
1717 document = document.add(band_bg);
1718
1719 let title_y = y + 20.0;
1721 let title = SvgText::new(format!("{} - {:.0}%", node.name, node.percentage))
1722 .set("class", "integrated-treemap-label")
1723 .set("x", x + width / 2.0)
1724 .set("y", title_y)
1725 .set("font-size", 16);
1726 document = document.add(title);
1727
1728 let children_y = title_y + 10.0;
1730 let children_height = height - 30.0;
1731 let children_padding = 10.0;
1732 let available_children_width = width - (children_padding * 2.0);
1733
1734 let total_children_size: usize = node.children.iter().map(|c| c.size).sum();
1736 if total_children_size == 0 {
1737 return Ok(document);
1738 }
1739
1740 let mut current_x = x + children_padding;
1741
1742 for child in &node.children {
1743 let child_width =
1745 (child.size as f64 / total_children_size as f64) * available_children_width;
1746
1747 let child_rect = Rectangle::new()
1749 .set("class", "integrated-treemap-rect")
1750 .set("x", current_x)
1751 .set("y", children_y)
1752 .set("width", child_width - 5.0) .set("height", children_height - 10.0)
1754 .set("fill", child.color.as_str())
1755 .set("rx", 18);
1756 document = document.add(child_rect);
1757
1758 let (child_name, child_relative_percentage) = extract_name_and_percentage(&child.name);
1760
1761 let child_label = SvgText::new(child_name)
1762 .set("class", "integrated-treemap-label")
1763 .set("x", current_x + child_width / 2.0)
1764 .set("y", children_y + children_height / 2.0 - 5.0)
1765 .set("font-size", calculate_font_size(child_width))
1766 .set("font-weight", "bold");
1767 document = document.add(child_label);
1768
1769 let percentage_text = if let Some(pct) = child_relative_percentage {
1771 format!("({pct})")
1772 } else {
1773 format!(
1774 "({:.0}%)",
1775 (child.size as f64 / total_children_size as f64) * 100.0
1776 )
1777 };
1778
1779 let child_percentage = SvgText::new(percentage_text)
1780 .set("class", "integrated-treemap-percentage")
1781 .set("x", current_x + child_width / 2.0)
1782 .set("y", children_y + children_height / 2.0 + 15.0)
1783 .set("font-size", calculate_font_size(child_width) - 2);
1784 document = document.add(child_percentage);
1785
1786 current_x += child_width;
1787 }
1788
1789 Ok(document)
1790}
1791
1792fn render_simple_horizontal_band(
1794 mut document: Document,
1795 x: f64,
1796 y: f64,
1797 width: f64,
1798 height: f64,
1799 node: &TreemapNode,
1800) -> TrackingResult<Document> {
1801 let band_rect = Rectangle::new()
1803 .set("class", "integrated-treemap-rect")
1804 .set("x", x)
1805 .set("y", y)
1806 .set("width", width)
1807 .set("height", height)
1808 .set("fill", node.color.as_str())
1809 .set("rx", 18);
1810 document = document.add(band_rect);
1811
1812 let label = SvgText::new(&node.name)
1814 .set("class", "integrated-treemap-label")
1815 .set("x", x + width / 2.0)
1816 .set("y", y + height / 2.0 - 5.0)
1817 .set("font-size", 16)
1818 .set("font-weight", "bold");
1819 document = document.add(label);
1820
1821 let percentage_label = SvgText::new(format!("{:.0}%", node.percentage))
1823 .set("class", "integrated-treemap-percentage")
1824 .set("x", x + width / 2.0)
1825 .set("y", y + height / 2.0 + 15.0)
1826 .set("font-size", 12);
1827 document = document.add(percentage_label);
1828
1829 Ok(document)
1830}
1831
1832fn extract_name_and_percentage(formatted_name: &str) -> (&str, Option<&str>) {
1834 if let Some(open_paren) = formatted_name.find(" (") {
1835 if let Some(close_paren) = formatted_name.find(')') {
1836 let name = &formatted_name[..open_paren];
1837 let percentage = &formatted_name[open_paren + 2..close_paren];
1838 return (name, Some(percentage));
1839 }
1840 }
1841 (formatted_name, None)
1842}
1843
1844fn calculate_font_size(width: f64) -> i32 {
1846 if width > 200.0 {
1847 15
1848 } else if width > 100.0 {
1849 13
1850 } else if width > 50.0 {
1851 11
1852 } else {
1853 9
1854 }
1855}
1856
1857pub fn add_categorized_allocations(
1859 mut document: Document,
1860 categories: &[AllocationCategory],
1861) -> TrackingResult<Document> {
1862 let chart_x = 50;
1863 let chart_y = 680; let chart_width = 850;
1865 let chart_height = 280; let bg = Rectangle::new()
1869 .set("x", chart_x)
1870 .set("y", chart_y)
1871 .set("width", chart_width)
1872 .set("height", chart_height)
1873 .set("fill", "white")
1874 .set("stroke", "#bdc3c7")
1875 .set("stroke-width", 2)
1876 .set("rx", 20); document = document.add(bg);
1879
1880 let title = SvgText::new("Tracked Variables by Category")
1882 .set("x", chart_x + chart_width / 2)
1883 .set("y", chart_y - 10)
1884 .set("text-anchor", "middle")
1885 .set("font-size", 16)
1886 .set("font-weight", "bold")
1887 .set("fill", "#2c3e50");
1888
1889 document = document.add(title);
1890
1891 if categories.is_empty() {
1892 let no_data = SvgText::new("No tracked variables found")
1893 .set("x", chart_x + chart_width / 2)
1894 .set("y", chart_y + chart_height / 2)
1895 .set("text-anchor", "middle")
1896 .set("font-size", 14)
1897 .set("fill", "#7f8c8d");
1898
1899 document = document.add(no_data);
1900 return Ok(document);
1901 }
1902
1903 let max_size = categories.iter().map(|c| c.total_size).max().unwrap_or(1);
1905 let bar_height = (chart_height - 60) / categories.len().min(8);
1906
1907 for (i, category) in categories.iter().take(8).enumerate() {
1908 let y = chart_y + 30 + i * bar_height;
1909 let bar_width =
1910 ((category.total_size as f64 / max_size as f64) * (chart_width - 200) as f64) as i32;
1911
1912 let bar = Rectangle::new()
1914 .set("x", chart_x + 150)
1915 .set("y", y)
1916 .set("width", bar_width)
1917 .set("height", bar_height - 5)
1918 .set("fill", category.color.as_str())
1919 .set("stroke", "#34495e")
1920 .set("stroke-width", 1)
1921 .set("rx", 12); document = document.add(bar);
1924
1925 let name_text = SvgText::new(&category.name)
1927 .set("x", chart_x + 10)
1928 .set("y", y + bar_height / 2 + 4)
1929 .set("font-size", 12)
1930 .set("font-weight", "600")
1931 .set("fill", "#2c3e50");
1932
1933 document = document.add(name_text);
1934
1935 let var_names: Vec<String> = category
1937 .allocations
1938 .iter()
1939 .filter_map(|a| {
1940 if let Some(var_name) = &a.var_name {
1941 let type_name = a.type_name.as_deref().unwrap_or("Unknown");
1942 let (simplified_type, _) = simplify_type_name(type_name);
1943 let short_var = if var_name.len() > 12 {
1945 format!("{}...", &var_name[..9])
1946 } else {
1947 var_name.to_string()
1948 };
1949 Some(format!("{short_var}({simplified_type})"))
1950 } else {
1951 None
1952 }
1953 })
1954 .take(3) .collect();
1956
1957 let mut display_text = if var_names.is_empty() {
1958 format!(
1959 "{} ({} vars)",
1960 format_bytes(category.total_size),
1961 category.allocations.len()
1962 )
1963 } else {
1964 format!(
1965 "{} - Vars: {}",
1966 format_bytes(category.total_size),
1967 var_names.join(", ")
1968 )
1969 };
1970
1971 let max_text_width = chart_width - 180; let estimated_char_width = 7; let max_chars = (max_text_width / estimated_char_width) as usize;
1975
1976 if display_text.len() > max_chars {
1977 display_text = format!("{}...", &display_text[..max_chars.saturating_sub(3)]);
1978 }
1979
1980 let size_text = SvgText::new(display_text)
1981 .set("x", chart_x + 160)
1982 .set("y", y + bar_height / 2 + 4)
1983 .set("font-size", 11) .set("font-weight", "bold")
1985 .set("fill", "#FFFFFF")
1986 .set("text-shadow", "1px 1px 2px rgba(0,0,0,0.8)");
1987
1988 document = document.add(size_text);
1989 }
1990
1991 Ok(document)
1992}
1993
1994pub fn add_memory_timeline(
1996 mut document: Document,
1997 allocations: &[AllocationInfo],
1998 _stats: &MemoryStats,
1999) -> TrackingResult<Document> {
2000 let chart_x = 50;
2001 let chart_y = 1380; let chart_width = 1700;
2003 let chart_height = 300;
2004
2005 let bg = Rectangle::new()
2007 .set("x", chart_x)
2008 .set("y", chart_y)
2009 .set("width", chart_width)
2010 .set("height", chart_height)
2011 .set("fill", "white")
2012 .set("stroke", "#bdc3c7")
2013 .set("stroke-width", 1)
2014 .set("rx", 5);
2015
2016 document = document.add(bg);
2017
2018 let title = SvgText::new("Variable Allocation Timeline")
2020 .set("x", chart_x + chart_width / 2)
2021 .set("y", chart_y - 10)
2022 .set("text-anchor", "middle")
2023 .set("font-size", 16)
2024 .set("font-weight", "bold")
2025 .set("fill", "#2c3e50");
2026
2027 document = document.add(title);
2028
2029 if allocations.is_empty() {
2030 let no_data = SvgText::new("No allocation data available")
2031 .set("x", chart_x + chart_width / 2)
2032 .set("y", chart_y + chart_height / 2)
2033 .set("text-anchor", "middle")
2034 .set("font-size", 14)
2035 .set("fill", "#7f8c8d");
2036
2037 document = document.add(no_data);
2038 return Ok(document);
2039 }
2040
2041 let mut tracked_allocs: Vec<_> = allocations
2043 .iter()
2044 .filter(|a| a.var_name.is_some())
2045 .collect();
2046
2047 tracked_allocs.sort_by(|a, b| {
2049 let size_cmp = b.size.cmp(&a.size);
2050 if size_cmp == std::cmp::Ordering::Equal {
2051 let a_duration =
2053 a.timestamp_dealloc.unwrap_or(a.timestamp_alloc + 1000) - a.timestamp_alloc;
2054 let b_duration =
2055 b.timestamp_dealloc.unwrap_or(b.timestamp_alloc + 1000) - b.timestamp_alloc;
2056 b_duration.cmp(&a_duration)
2057 } else {
2058 size_cmp
2059 }
2060 });
2061
2062 if tracked_allocs.is_empty() {
2063 let no_data = SvgText::new("No tracked variables found")
2064 .set("x", chart_x + chart_width / 2)
2065 .set("y", chart_y + chart_height / 2)
2066 .set("text-anchor", "middle")
2067 .set("font-size", 14)
2068 .set("fill", "#7f8c8d");
2069
2070 document = document.add(no_data);
2071 return Ok(document);
2072 }
2073
2074 let min_time = tracked_allocs
2075 .iter()
2076 .map(|a| a.timestamp_alloc)
2077 .min()
2078 .unwrap_or(0);
2079 let max_time = tracked_allocs
2080 .iter()
2081 .map(|a| a.timestamp_alloc)
2082 .max()
2083 .unwrap_or(min_time + 1);
2084 let _time_range = (max_time - min_time).max(1);
2085
2086 let label_width = 500; let timeline_width = chart_width - label_width - 80; let max_items = 10; let available_height = chart_height - 120; let row_height = (available_height / max_items as i32).clamp(18, 22);
2094
2095 for (i, allocation) in tracked_allocs.iter().take(max_items).enumerate() {
2097 let time_ratio = if max_time > min_time {
2099 let alloc_time = allocation.timestamp_alloc.max(min_time); (alloc_time - min_time) as f64 / (max_time - min_time) as f64
2101 } else {
2102 0.5 };
2104 let x = chart_x + 30 + (time_ratio * timeline_width as f64) as i32;
2105 let y = chart_y + 40 + (i as i32 * row_height);
2106
2107 let x = x.min(chart_x + timeline_width - 10).max(chart_x + 30);
2109
2110 let color = if let Some(type_name) = &allocation.type_name {
2112 let (_, category) = simplify_type_name(type_name);
2113 get_category_color(&category)
2114 } else {
2115 "#95a5a6".to_string()
2116 };
2117
2118 let point = Circle::new()
2120 .set("cx", x)
2121 .set("cy", y)
2122 .set("r", 5)
2123 .set("fill", color)
2124 .set("stroke", "#2c3e50")
2125 .set("stroke-width", 2);
2126
2127 document = document.add(point);
2128
2129 let label_start_x = chart_x + timeline_width + 40;
2131 let line = svg::node::element::Line::new()
2132 .set("x1", x + 5)
2133 .set("y1", y)
2134 .set("x2", label_start_x - 5)
2135 .set("y2", y)
2136 .set("stroke", "#bdc3c7")
2137 .set("stroke-width", 1)
2138 .set("stroke-dasharray", "2,3");
2139
2140 document = document.add(line);
2141
2142 if let Some(var_name) = &allocation.var_name {
2144 let type_name = allocation.type_name.as_deref().unwrap_or("Unknown");
2145 let (simplified_type, _) = simplify_type_name(type_name);
2146 let size_info = format_bytes(allocation.size);
2147
2148 let max_var_name_length = 20;
2150 let max_type_length = 12;
2151
2152 let truncated_var_name = if var_name.len() > max_var_name_length {
2153 format!("{}...", &var_name[..max_var_name_length - 3])
2154 } else {
2155 var_name.to_string()
2156 };
2157
2158 let truncated_type = if simplified_type.len() > max_type_length {
2159 format!("{}...", &simplified_type[..max_type_length - 3])
2160 } else {
2161 simplified_type
2162 };
2163
2164 let label_text = format!("{truncated_var_name} ({truncated_type}) {size_info}");
2165
2166 let label = SvgText::new(label_text)
2167 .set("x", label_start_x)
2168 .set("y", y + 4)
2169 .set("font-size", 9) .set("font-weight", "500")
2171 .set("fill", "#2c3e50");
2172
2173 document = document.add(label);
2174 }
2175 }
2176
2177 let axis_y = chart_y + chart_height - 40;
2179 let axis_line = svg::node::element::Line::new()
2180 .set("x1", chart_x + 20)
2181 .set("y1", axis_y)
2182 .set("x2", chart_x + timeline_width)
2183 .set("y2", axis_y)
2184 .set("stroke", "#34495e")
2185 .set("stroke-width", 2);
2186
2187 document = document.add(axis_line);
2188
2189 let start_label = SvgText::new("Timeline")
2191 .set("x", chart_x + 20)
2192 .set("y", axis_y + 20)
2193 .set("font-size", 12)
2194 .set("font-weight", "600")
2195 .set("fill", "#7f8c8d");
2196
2197 document = document.add(start_label);
2198
2199 let total_tracked = tracked_allocs.len();
2201 if total_tracked > max_items {
2202 let note_text = format!("Note: Showing top {max_items} variables with highest memory usage and longest lifecycles (out of {total_tracked} total tracked variables)");
2203 let note = SvgText::new(note_text)
2204 .set("x", chart_x + 20)
2205 .set("y", chart_y + chart_height - 10)
2206 .set("font-size", 11)
2207 .set("font-style", "italic")
2208 .set("fill", "#7f8c8d");
2209 document = document.add(note);
2210 }
2211
2212 Ok(document)
2213}
2214
2215pub fn add_fragmentation_analysis(
2217 mut document: Document,
2218 allocations: &[AllocationInfo],
2219) -> TrackingResult<Document> {
2220 let chart_x = 950;
2221 let chart_y = 320; let chart_width = 750; let chart_height = 300;
2224
2225 let bg = Rectangle::new()
2227 .set("x", chart_x)
2228 .set("y", chart_y)
2229 .set("width", chart_width)
2230 .set("height", chart_height)
2231 .set("fill", "white")
2232 .set("stroke", "#f39c12")
2233 .set("stroke-width", 2)
2234 .set("rx", 10);
2235
2236 document = document.add(bg);
2237
2238 let title = SvgText::new("Memory Fragmentation Analysis")
2240 .set("x", chart_x + chart_width / 2)
2241 .set("y", chart_y - 10)
2242 .set("text-anchor", "middle")
2243 .set("font-size", 18)
2244 .set("font-weight", "bold")
2245 .set("fill", "#2c3e50");
2246
2247 document = document.add(title);
2248
2249 if allocations.is_empty() {
2250 let no_data = SvgText::new("No allocation data available")
2251 .set("x", chart_x + chart_width / 2)
2252 .set("y", chart_y + chart_height / 2)
2253 .set("text-anchor", "middle")
2254 .set("font-size", 14)
2255 .set("fill", "#7f8c8d");
2256
2257 document = document.add(no_data);
2258 return Ok(document);
2259 }
2260
2261 let size_buckets = [
2263 (0, 64, "Tiny (0-64B)"),
2264 (65, 256, "Small (65-256B)"),
2265 (257, 1024, "Medium (257B-1KB)"),
2266 (1025, 4096, "Large (1-4KB)"),
2267 (4097, 16384, "XLarge (4-16KB)"),
2268 (16385, usize::MAX, "Huge (>16KB)"),
2269 ];
2270
2271 let mut bucket_counts = vec![0; size_buckets.len()];
2272
2273 for allocation in allocations {
2274 for (i, &(min, max, _)) in size_buckets.iter().enumerate() {
2275 if allocation.size >= min && allocation.size <= max {
2276 bucket_counts[i] += 1;
2277 break;
2278 }
2279 }
2280 }
2281
2282 let max_count = bucket_counts.iter().max().copied().unwrap_or(1);
2283 let bar_width = (chart_width - 100) / size_buckets.len();
2284
2285 for (i, (&(_, _, label), &count)) in size_buckets.iter().zip(bucket_counts.iter()).enumerate() {
2287 let x = chart_x + 50 + i * bar_width;
2288 let bar_height = if max_count > 0 {
2289 (count as f64 / max_count as f64 * (chart_height - 80) as f64) as i32
2290 } else {
2291 0
2292 };
2293 let y = chart_y + chart_height - 40 - bar_height;
2294
2295 let color = match i {
2297 0..=1 => "#27ae60", 2..=3 => "#f39c12", _ => "#e74c3c", };
2301
2302 let bar = Rectangle::new()
2303 .set("x", x)
2304 .set("y", y)
2305 .set("width", bar_width - 5)
2306 .set("height", bar_height)
2307 .set("fill", color)
2308 .set("stroke", "#2c3e50")
2309 .set("stroke-width", 1);
2310
2311 document = document.add(bar);
2312
2313 let count_text = SvgText::new(count.to_string())
2315 .set("x", x + bar_width / 2)
2316 .set("y", y - 5)
2317 .set("text-anchor", "middle")
2318 .set("font-size", 12)
2319 .set("font-weight", "bold")
2320 .set("fill", "#2c3e50");
2321
2322 document = document.add(count_text);
2323
2324 let size_text = SvgText::new(label)
2326 .set("x", x + bar_width / 2)
2327 .set("y", chart_y + chart_height - 10)
2328 .set("text-anchor", "middle")
2329 .set("font-size", 10)
2330 .set("fill", "#7f8c8d");
2331
2332 document = document.add(size_text);
2333 }
2334
2335 Ok(document)
2336}
2337
2338pub fn add_callstack_analysis(
2340 mut document: Document,
2341 allocations: &[AllocationInfo],
2342) -> TrackingResult<Document> {
2343 let chart_x = 950;
2344 let chart_y = 680; let chart_width = 750; let chart_height = 300;
2347
2348 let bg = Rectangle::new()
2350 .set("x", chart_x)
2351 .set("y", chart_y)
2352 .set("width", chart_width)
2353 .set("height", chart_height)
2354 .set("fill", "white")
2355 .set("stroke", "#9b59b6")
2356 .set("stroke-width", 2)
2357 .set("rx", 10);
2358
2359 document = document.add(bg);
2360
2361 let title = SvgText::new("Call Stack Analysis")
2363 .set("x", chart_x + chart_width / 2)
2364 .set("y", chart_y - 10)
2365 .set("text-anchor", "middle")
2366 .set("font-size", 18)
2367 .set("font-weight", "bold")
2368 .set("fill", "#2c3e50");
2369
2370 document = document.add(title);
2371
2372 let mut source_stats: HashMap<String, (usize, usize)> = HashMap::new();
2374
2375 for allocation in allocations {
2376 let source_key = if let Some(var_name) = &allocation.var_name {
2378 if let Some(type_name) = &allocation.type_name {
2380 let (simplified_type, _) = simplify_type_name(type_name);
2381 format!(
2382 "{}({}) memory: {}",
2383 var_name,
2384 simplified_type,
2385 format_bytes(allocation.size)
2386 )
2387 } else {
2388 format!(
2389 "{}(Unknown Type) memory: {}",
2390 var_name,
2391 format_bytes(allocation.size)
2392 )
2393 }
2394 } else if let Some(type_name) = &allocation.type_name {
2395 let (simplified_type, _) = simplify_type_name(type_name);
2397
2398 if type_name.contains("std::") || type_name.contains("alloc::") {
2400 format!("System/Runtime {simplified_type} (untracked)")
2401 } else if simplified_type.contains("Vec") {
2402 "Internal Vec allocations (untracked)".to_string()
2403 } else if simplified_type.contains("String") {
2404 "Internal String allocations (untracked)".to_string()
2405 } else if simplified_type.contains("HashMap") {
2406 "Internal HashMap allocations (untracked)".to_string()
2407 } else {
2408 format!("Internal {simplified_type} allocations (untracked)")
2409 }
2410 } else {
2411 "System/Runtime allocations (no type info)".to_string()
2413 };
2414
2415 let entry = source_stats.entry(source_key).or_insert((0, 0));
2416 entry.0 += 1;
2417 entry.1 += allocation.size;
2418 }
2419
2420 if source_stats.is_empty() {
2421 let no_data = SvgText::new("No call stack data available")
2422 .set("x", chart_x + chart_width / 2)
2423 .set("y", chart_y + chart_height / 2)
2424 .set("text-anchor", "middle")
2425 .set("font-size", 14)
2426 .set("fill", "#7f8c8d");
2427
2428 document = document.add(no_data);
2429 return Ok(document);
2430 }
2431
2432 let mut sorted_sources: Vec<_> = source_stats.iter().collect();
2434 sorted_sources.sort_by(|a, b| b.1 .1.cmp(&a.1 .1));
2435
2436 let max_size = sorted_sources
2437 .first()
2438 .map(|(_, (_, size))| *size)
2439 .unwrap_or(1);
2440
2441 for (i, (source, (count, total_size))) in sorted_sources.iter().take(10).enumerate() {
2443 let y = chart_y + 40 + i * 25;
2444 let node_size = ((*total_size as f64 / max_size as f64) * 15.0 + 5.0) as i32;
2445
2446 let colors = ["#e74c3c", "#f39c12", "#27ae60", "#3498db", "#9b59b6"];
2448 let color = colors[i % colors.len()];
2449
2450 let node = Circle::new()
2451 .set("cx", chart_x + 50)
2452 .set("cy", y)
2453 .set("r", node_size)
2454 .set("fill", color)
2455 .set("stroke", "#2c3e50")
2456 .set("stroke-width", 2);
2457
2458 document = document.add(node);
2459
2460 let source_text = format!("{source} ({count} allocs, {total_size} bytes)");
2462
2463 let label = SvgText::new(source_text)
2464 .set("x", chart_x + 80)
2465 .set("y", y + 5)
2466 .set("font-size", 11)
2467 .set("font-weight", "500")
2468 .set("fill", "#2c3e50");
2469
2470 document = document.add(label);
2471 }
2472
2473 Ok(document)
2474}
2475
2476pub fn add_memory_growth_trends(
2478 mut document: Document,
2479 allocations: &[AllocationInfo],
2480 stats: &MemoryStats,
2481) -> TrackingResult<Document> {
2482 let chart_x = 50;
2483 let chart_y = 1040; let chart_width = 1700;
2485 let chart_height = 300;
2486
2487 let bg = Rectangle::new()
2489 .set("x", chart_x)
2490 .set("y", chart_y)
2491 .set("width", chart_width)
2492 .set("height", chart_height)
2493 .set("fill", "white")
2494 .set("stroke", "#27ae60")
2495 .set("stroke-width", 2)
2496 .set("rx", 10);
2497
2498 document = document.add(bg);
2499
2500 let title = SvgText::new("Memory Growth Trends")
2502 .set("x", chart_x + chart_width / 2)
2503 .set("y", chart_y - 10)
2504 .set("text-anchor", "middle")
2505 .set("font-size", 18)
2506 .set("font-weight", "bold")
2507 .set("fill", "#2c3e50");
2508
2509 document = document.add(title);
2510
2511 if allocations.is_empty() {
2512 let no_data = SvgText::new("No allocation data available")
2513 .set("x", chart_x + chart_width / 2)
2514 .set("y", chart_y + chart_height / 2)
2515 .set("text-anchor", "middle")
2516 .set("font-size", 14)
2517 .set("fill", "#7f8c8d");
2518
2519 document = document.add(no_data);
2520 return Ok(document);
2521 }
2522
2523 let time_points = 10;
2525 let point_width = (chart_width - 100) / time_points;
2526
2527 for i in 0..time_points {
2528 let x = chart_x + 50 + i * point_width;
2529 let simulated_memory = stats.active_memory / time_points * (i + 1);
2530 let y = chart_y + chart_height
2531 - 50
2532 - ((simulated_memory as f64 / stats.peak_memory as f64) * (chart_height - 100) as f64)
2533 as i32;
2534
2535 let point = Circle::new()
2537 .set("cx", x)
2538 .set("cy", y)
2539 .set("r", 4)
2540 .set("fill", "#27ae60")
2541 .set("stroke", "#2c3e50")
2542 .set("stroke-width", 1);
2543
2544 document = document.add(point);
2545
2546 if i > 0 {
2548 let prev_x = chart_x + 50 + (i - 1) * point_width;
2549 let prev_memory = stats.active_memory / time_points * i;
2550 let prev_y = chart_y + chart_height
2551 - 50
2552 - ((prev_memory as f64 / stats.peak_memory as f64) * (chart_height - 100) as f64)
2553 as i32;
2554
2555 let line = svg::node::element::Line::new()
2556 .set("x1", prev_x)
2557 .set("y1", prev_y)
2558 .set("x2", x)
2559 .set("y2", y)
2560 .set("stroke", "#27ae60")
2561 .set("stroke-width", 2);
2562
2563 document = document.add(line);
2564 }
2565 }
2566
2567 let peak_y = chart_y + 50;
2569 let peak_line = svg::node::element::Line::new()
2570 .set("x1", chart_x + 50)
2571 .set("y1", peak_y)
2572 .set("x2", chart_x + chart_width - 50)
2573 .set("y2", peak_y)
2574 .set("stroke", "#e74c3c")
2575 .set("stroke-width", 2)
2576 .set("stroke-dasharray", "10,5");
2577
2578 document = document.add(peak_line);
2579
2580 let peak_label = SvgText::new(format!("Peak: {} bytes", stats.peak_memory))
2581 .set("x", chart_x + chart_width - 100)
2582 .set("y", peak_y - 10)
2583 .set("font-size", 12)
2584 .set("font-weight", "bold")
2585 .set("fill", "#e74c3c");
2586
2587 document = document.add(peak_label);
2588
2589 Ok(document)
2590}
2591
2592pub fn add_interactive_legend(mut document: Document) -> TrackingResult<Document> {
2594 let legend_x = 50;
2595 let legend_y = 1720; let legend_width = 850;
2597 let legend_height = 250;
2598
2599 let bg = Rectangle::new()
2601 .set("x", legend_x)
2602 .set("y", legend_y)
2603 .set("width", legend_width)
2604 .set("height", legend_height)
2605 .set("fill", "white")
2606 .set("stroke", "#34495e")
2607 .set("stroke-width", 2)
2608 .set("rx", 10);
2609
2610 document = document.add(bg);
2611
2612 let title = SvgText::new("Interactive Legend & Guide")
2614 .set("x", legend_x + legend_width / 2)
2615 .set("y", legend_y - 10)
2616 .set("text-anchor", "middle")
2617 .set("font-size", 18)
2618 .set("font-weight", "bold")
2619 .set("fill", "#2c3e50");
2620
2621 document = document.add(title);
2622
2623 let legend_items = [
2625 ("#e74c3c", "High Memory Usage / Critical"),
2626 ("#f39c12", "Medium Usage / Warning"),
2627 ("#27ae60", "Low Usage / Good"),
2628 ("#3498db", "Performance Metrics"),
2629 ("#9b59b6", "Call Stack Data"),
2630 ("#34495e", "General Information"),
2631 ];
2632
2633 for (i, (color, description)) in legend_items.iter().enumerate() {
2634 let x = legend_x + 30 + (i % 3) * 220;
2635 let y = legend_y + 40 + (i / 3) * 40;
2636
2637 let swatch = Rectangle::new()
2639 .set("x", x)
2640 .set("y", y - 10)
2641 .set("width", 20)
2642 .set("height", 15)
2643 .set("fill", *color)
2644 .set("stroke", "#2c3e50")
2645 .set("stroke-width", 1);
2646
2647 document = document.add(swatch);
2648
2649 let desc_text = SvgText::new(*description)
2651 .set("x", x + 30)
2652 .set("y", y)
2653 .set("font-size", 12)
2654 .set("fill", "#2c3e50");
2655
2656 document = document.add(desc_text);
2657 }
2658
2659 Ok(document)
2660}
2661
2662pub fn add_comprehensive_summary(
2664 mut document: Document,
2665 stats: &MemoryStats,
2666 allocations: &[AllocationInfo],
2667) -> TrackingResult<Document> {
2668 let summary_x = 950;
2669 let summary_y = 1720; let summary_width = 800;
2671 let summary_height = 250;
2672
2673 let bg = Rectangle::new()
2675 .set("x", summary_x)
2676 .set("y", summary_y)
2677 .set("width", summary_width)
2678 .set("height", summary_height)
2679 .set("fill", "white")
2680 .set("stroke", "#2c3e50")
2681 .set("stroke-width", 2)
2682 .set("rx", 10);
2683
2684 document = document.add(bg);
2685
2686 let title = SvgText::new("Memory Analysis Summary")
2688 .set("x", summary_x + summary_width / 2)
2689 .set("y", summary_y - 10)
2690 .set("text-anchor", "middle")
2691 .set("font-size", 18)
2692 .set("font-weight", "bold")
2693 .set("fill", "#2c3e50");
2694
2695 document = document.add(title);
2696
2697 let tracked_vars = allocations.iter().filter(|a| a.var_name.is_some()).count();
2699 let avg_size = if !allocations.is_empty() {
2700 allocations.iter().map(|a| a.size).sum::<usize>() / allocations.len()
2701 } else {
2702 0
2703 };
2704
2705 let summary_items = [
2706 format!("Total Active Allocations: {}", stats.active_allocations),
2707 format!(
2708 "Tracked Variables: {} ({:.1}%)",
2709 tracked_vars,
2710 if stats.active_allocations > 0 {
2711 tracked_vars as f64 / stats.active_allocations as f64 * 100.0
2712 } else {
2713 0.0
2714 }
2715 ),
2716 format!("Average Allocation Size: {avg_size} bytes"),
2717 format!(
2718 "Memory Efficiency: {:.1}%",
2719 if stats.total_allocations > 0 {
2720 stats.total_deallocations as f64 / stats.total_allocations as f64 * 100.0
2721 } else {
2722 0.0
2723 }
2724 ),
2725 format!(
2726 "Peak vs Current: {} vs {} bytes",
2727 stats.peak_memory, stats.active_memory
2728 ),
2729 ];
2730
2731 for (i, item) in summary_items.iter().enumerate() {
2732 let summary_text = SvgText::new(item)
2733 .set("x", summary_x + 30)
2734 .set("y", summary_y + 40 + i * 25)
2735 .set("font-size", 13)
2736 .set("font-weight", "500")
2737 .set("fill", "#2c3e50");
2738
2739 document = document.add(summary_text);
2740 }
2741
2742 Ok(document)
2743}
2744
2745pub fn add_enhanced_timeline_dashboard(
2748 mut document: Document,
2749 stats: &MemoryStats,
2750 allocations: &[AllocationInfo],
2751) -> TrackingResult<Document> {
2752 let dashboard_y = 120;
2754 let dashboard_height = 200;
2755
2756 let memory_efficiency = if stats.peak_memory > 0 {
2758 (stats.active_memory as f64 / stats.peak_memory as f64) * 100.0
2759 } else {
2760 0.0
2761 };
2762
2763 let allocation_efficiency = if stats.total_allocations > 0 {
2764 (stats.active_allocations as f64 / stats.total_allocations as f64) * 100.0
2765 } else {
2766 0.0
2767 };
2768
2769 let fragmentation_ratio = if stats.total_allocated > 0 {
2770 (1.0 - (stats.active_memory as f64 / stats.total_allocated as f64)) * 100.0
2771 } else {
2772 0.0
2773 };
2774
2775 let dashboard_bg = Rectangle::new()
2777 .set("x", 50)
2778 .set("y", dashboard_y)
2779 .set("width", 1700)
2780 .set("height", dashboard_height)
2781 .set("fill", "rgba(255,255,255,0.1)")
2782 .set("stroke", "rgba(255,255,255,0.2)")
2783 .set("stroke-width", 1)
2784 .set("rx", 12);
2785 document = document.add(dashboard_bg);
2786
2787 let dashboard_title = SvgText::new("Performance Dashboard")
2789 .set("x", 900)
2790 .set("y", dashboard_y + 25)
2791 .set("text-anchor", "middle")
2792 .set("font-size", 18)
2793 .set("font-weight", "bold")
2794 .set("fill", "#FFFFFF");
2795 document = document.add(dashboard_title);
2796
2797 let metrics = [
2799 ("Active Memory", memory_efficiency, "#3498db"),
2800 ("Allocation Efficiency", allocation_efficiency, "#2ecc71"),
2801 (
2802 "Memory Fragmentation",
2803 100.0 - fragmentation_ratio,
2804 "#e74c3c",
2805 ),
2806 (
2807 "Peak Usage",
2808 if stats.peak_memory > 0 {
2809 (stats.active_memory as f64 / stats.peak_memory as f64) * 100.0
2810 } else {
2811 0.0
2812 },
2813 "#f39c12",
2814 ),
2815 ];
2816
2817 for (i, (label, percentage, color)) in metrics.iter().enumerate() {
2818 let x = 200 + i * 350;
2819 let y = dashboard_y + 100;
2820 let radius = 40;
2821
2822 let bg_circle = Circle::new()
2824 .set("cx", x)
2825 .set("cy", y)
2826 .set("r", radius)
2827 .set("fill", "none")
2828 .set("stroke", "rgba(255,255,255,0.2)")
2829 .set("stroke-width", 8);
2830 document = document.add(bg_circle);
2831
2832 let circumference = 2.0 * std::f64::consts::PI * radius as f64;
2834 let progress = circumference * (percentage / 100.0);
2835 let dash_array = format!("{} {}", progress, circumference - progress);
2836
2837 let progress_circle = Circle::new()
2838 .set("cx", x)
2839 .set("cy", y)
2840 .set("r", radius)
2841 .set("fill", "none")
2842 .set("stroke", *color)
2843 .set("stroke-width", 8)
2844 .set("stroke-dasharray", dash_array)
2845 .set("stroke-dashoffset", 0)
2846 .set("transform", format!("rotate(-90 {x} {y})"))
2847 .set("stroke-linecap", "round");
2848 document = document.add(progress_circle);
2849
2850 let percentage_text = SvgText::new(format!("{percentage:.0}%"))
2852 .set("x", x)
2853 .set("y", y + 5)
2854 .set("text-anchor", "middle")
2855 .set("font-size", 16)
2856 .set("font-weight", "bold")
2857 .set("fill", "#FFFFFF");
2858 document = document.add(percentage_text);
2859
2860 let label_text = SvgText::new(*label)
2862 .set("x", x)
2863 .set("y", y + 65)
2864 .set("text-anchor", "middle")
2865 .set("font-size", 12)
2866 .set("fill", "#FFFFFF");
2867 document = document.add(label_text);
2868 }
2869
2870 let chart_x = 50;
2872 let chart_y = 350; let chart_width = 1700;
2874 let chart_height = 350; let bg = Rectangle::new()
2878 .set("x", chart_x)
2879 .set("y", chart_y)
2880 .set("width", chart_width)
2881 .set("height", chart_height)
2882 .set("fill", "white")
2883 .set("stroke", "#bdc3c7")
2884 .set("stroke-width", 1)
2885 .set("rx", 8);
2886
2887 document = document.add(bg);
2888
2889 let title = SvgText::new("Memory Allocation Timeline")
2891 .set("x", chart_x + chart_width / 2)
2892 .set("y", chart_y - 15)
2893 .set("text-anchor", "middle")
2894 .set("font-size", 16)
2895 .set("font-weight", "bold")
2896 .set("fill", "#2c3e50");
2897
2898 document = document.add(title);
2899
2900 let tracked_vars: Vec<&AllocationInfo> = allocations
2902 .iter()
2903 .filter(|a| a.var_name.is_some())
2904 .collect();
2905
2906 if tracked_vars.is_empty() {
2907 let no_data = SvgText::new("No tracked variables found for timeline visualization")
2908 .set("x", chart_x + chart_width / 2)
2909 .set("y", chart_y + chart_height / 2)
2910 .set("text-anchor", "middle")
2911 .set("font-size", 14)
2912 .set("fill", "#7f8c8d");
2913 document = document.add(no_data);
2914 return Ok(document);
2915 }
2916
2917 let mut sorted_tracked_vars: Vec<_> = tracked_vars.clone();
2919 sorted_tracked_vars.sort_by(|a, b| {
2920 let size_cmp = b.size.cmp(&a.size);
2921 if size_cmp == std::cmp::Ordering::Equal {
2922 let a_duration =
2924 a.timestamp_dealloc.unwrap_or(a.timestamp_alloc + 1000) - a.timestamp_alloc;
2925 let b_duration =
2926 b.timestamp_dealloc.unwrap_or(b.timestamp_alloc + 1000) - b.timestamp_alloc;
2927 b_duration.cmp(&a_duration)
2928 } else {
2929 size_cmp
2930 }
2931 });
2932
2933 let max_vars_to_show = 10;
2935 let limited_tracked_vars: Vec<_> = sorted_tracked_vars
2936 .into_iter()
2937 .take(max_vars_to_show)
2938 .collect();
2939
2940 let mut scope_groups: std::collections::HashMap<String, Vec<&AllocationInfo>> =
2942 std::collections::HashMap::new();
2943 for var in &limited_tracked_vars {
2944 let scope_name = extract_scope_name(
2945 var.var_name
2946 .as_ref()
2947 .unwrap_or(&"unknown_variable".to_string()),
2948 );
2949 scope_groups.entry(scope_name).or_default().push(*var);
2950 }
2951
2952 let mut sorted_scopes: Vec<_> = scope_groups.into_iter().collect();
2954 sorted_scopes.sort_by(|a, b| a.0.cmp(&b.0));
2955
2956 let max_time = limited_tracked_vars
2958 .iter()
2959 .map(|a| a.timestamp_alloc)
2960 .max()
2961 .unwrap_or(1000) as f64;
2962 let min_time = limited_tracked_vars
2963 .iter()
2964 .map(|a| a.timestamp_alloc)
2965 .min()
2966 .unwrap_or(0) as f64;
2967 let time_range = (max_time - min_time).max(1.0);
2968
2969 let plot_x = chart_x + 200; let plot_y = chart_y + 50;
2972 let plot_width = chart_width - 350; let plot_height = chart_height - 100;
2974
2975 let row_height = (plot_height as f64 / sorted_scopes.len().max(1) as f64).clamp(25.0, 40.0);
2976
2977 let time_axis = svg::node::element::Line::new()
2979 .set("x1", plot_x)
2980 .set("y1", plot_y + plot_height)
2981 .set("x2", plot_x + plot_width)
2982 .set("y2", plot_y + plot_height)
2983 .set("stroke", "#34495e")
2984 .set("stroke-width", 2);
2985 document = document.add(time_axis);
2986
2987 let time_units = ["0ms", "0.25ms", "0.5ms", "0.75ms", "1ms"];
2989 for (i, item) in time_units.iter().enumerate() {
2990 let x = plot_x + (i * plot_width / 4);
2991
2992 let tick = svg::node::element::Line::new()
2993 .set("x1", x)
2994 .set("y1", plot_y + plot_height)
2995 .set("x2", x)
2996 .set("y2", plot_y + plot_height + 5)
2997 .set("stroke", "#34495e")
2998 .set("stroke-width", 1);
2999 document = document.add(tick);
3000
3001 let time_label = item;
3003 let label = SvgText::new(*time_label)
3004 .set("x", x)
3005 .set("y", plot_y + plot_height + 18)
3006 .set("text-anchor", "middle")
3007 .set("font-size", 10)
3008 .set("font-weight", "500")
3009 .set("fill", "#2c3e50");
3010 document = document.add(label);
3011 }
3012
3013 let x_label = SvgText::new("Execution Time (milliseconds)")
3015 .set("x", plot_x + plot_width / 2)
3016 .set("y", plot_y + plot_height + 40)
3017 .set("text-anchor", "middle")
3018 .set("font-size", 12)
3019 .set("font-weight", "bold")
3020 .set("fill", "#2c3e50");
3021 document = document.add(x_label);
3022
3023 let scale_markers = [
3027 (16, "16B"),
3028 (256, "256B"),
3029 (1024, "1KB"),
3030 (4096, "4KB"),
3031 (16384, "16KB"),
3032 ];
3033
3034 for (size, label) in scale_markers.iter() {
3035 {
3037 let log_size = (*size as f64).ln();
3038 let log_min = 1.0_f64.ln();
3039 let log_max = 16384.0_f64.ln();
3040 let log_range = log_max - log_min;
3041
3042 if log_range > 0.0 {
3043 let y_pos = plot_y + plot_height
3044 - ((log_size - log_min) / log_range * plot_height as f64) as i32;
3045
3046 let marker_line = svg::node::element::Line::new()
3048 .set("x1", plot_x - 5)
3049 .set("y1", y_pos)
3050 .set("x2", plot_x + 5)
3051 .set("y2", y_pos)
3052 .set("stroke", "#7f8c8d")
3053 .set("stroke-width", 1);
3054 document = document.add(marker_line);
3055
3056 let marker_label = SvgText::new(*label)
3058 .set("x", plot_x - 15)
3059 .set("y", y_pos + 3)
3060 .set("text-anchor", "end")
3061 .set("font-size", 10)
3062 .set("fill", "#7f8c8d");
3063 document = document.add(marker_label);
3064 }
3065 }
3066 }
3067
3068 let time_markers = 5;
3070 for i in 0..=time_markers {
3071 let time_point = min_time + (time_range * i as f64 / time_markers as f64);
3072 let x_pos = plot_x + (i * plot_width / time_markers);
3073
3074 let time_marker_line = svg::node::element::Line::new()
3076 .set("x1", x_pos)
3077 .set("y1", plot_y + plot_height - 5)
3078 .set("x2", x_pos)
3079 .set("y2", plot_y + plot_height + 5)
3080 .set("stroke", "#7f8c8d")
3081 .set("stroke-width", 1);
3082 document = document.add(time_marker_line);
3083
3084 let formatted_time = if time_point < 1000.0 {
3086 format!("{time_point:.1}ms")
3087 } else if time_point < 60000.0 {
3088 format!("{:.2}s", time_point / 1000.0)
3089 } else {
3090 format!("{:.1}m", time_point / 60000.0)
3091 };
3092
3093 let time_label = SvgText::new(formatted_time)
3094 .set("x", x_pos)
3095 .set("y", plot_y + plot_height + 20)
3096 .set("text-anchor", "middle")
3097 .set("font-size", 10)
3098 .set("font-weight", "500")
3099 .set("fill", "#2c3e50");
3100 document = document.add(time_label);
3101 }
3102
3103 let categorized = categorize_allocations(allocations);
3105 let mut category_colors: HashMap<String, String> = HashMap::new();
3106 for category in &categorized {
3107 category_colors.insert(category.name.clone(), category.color.clone());
3108 }
3109
3110 let mut sizes: Vec<usize> = allocations
3112 .iter()
3113 .map(|a| a.size)
3114 .filter(|&s| s > 0)
3115 .collect();
3116 sizes.sort_unstable();
3117 let _p95_threshold = if !sizes.is_empty() {
3118 let p95_index = (sizes.len() as f64 * 0.95) as usize;
3119 sizes[p95_index.min(sizes.len() - 1)]
3120 } else {
3121 0
3122 };
3123
3124 for (scope_index, (scope_name, scope_vars)) in sorted_scopes.iter().enumerate() {
3126 let scope_y = plot_y + 10 + (scope_index as f64 * row_height) as i32; let scope_colors = [
3130 "#3498db", "#e74c3c", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c",
3131 ];
3132 let scope_color = scope_colors[scope_index % scope_colors.len()];
3133
3134 let scope_bg = Rectangle::new()
3136 .set("x", plot_x)
3137 .set("y", scope_y)
3138 .set("width", plot_width)
3139 .set("height", (row_height * 0.8) as i32)
3140 .set("fill", scope_color)
3141 .set("opacity", 0.2)
3142 .set("rx", 5);
3143 document = document.add(scope_bg);
3144
3145 let scope_label = SvgText::new(format!("Scope: {scope_name}"))
3147 .set("x", plot_x - 15)
3148 .set("y", scope_y + (row_height * 0.4) as i32)
3149 .set("text-anchor", "end")
3150 .set("font-size", 11)
3151 .set("font-weight", "bold")
3152 .set("fill", scope_color);
3153 document = document.add(scope_label);
3154
3155 for (var_index, alloc) in scope_vars.iter().enumerate() {
3157 let default_name = "unnamed_variable".to_string();
3158 let var_name = alloc.var_name.as_ref().unwrap_or(&default_name);
3159
3160 let start_x = plot_x as i32
3162 + ((alloc.timestamp_alloc as f64 - min_time) / time_range * plot_width as f64)
3163 as i32;
3164 let var_y = scope_y + 5 + (var_index as f64 * 8.0) as i32; let (_, category) = if let Some(type_name) = &alloc.type_name {
3168 simplify_type_name(type_name)
3169 } else {
3170 (
3171 "Unknown".to_string(),
3172 "Runtime/System Allocation".to_string(),
3173 )
3174 };
3175 let var_color = get_category_color(&category);
3176
3177 let max_size = scope_vars.iter().map(|a| a.size).max().unwrap_or(1024) as f64;
3179 let min_size = scope_vars
3180 .iter()
3181 .map(|a| a.size)
3182 .filter(|&s| s > 0)
3183 .min()
3184 .unwrap_or(1) as f64;
3185 let size_ratio = if max_size > min_size {
3186 ((alloc.size as f64).ln() - min_size.ln()) / (max_size.ln() - min_size.ln())
3187 } else {
3188 0.5
3189 };
3190 let bar_width = ((plot_width as f64 * 0.4 * size_ratio) + 30.0) as i32;
3191
3192 let plot_x_i32 = plot_x as i32;
3194 let plot_width_i32 = plot_width as i32;
3195 let var_bar = Rectangle::new()
3196 .set("x", start_x.max(plot_x_i32))
3197 .set("y", var_y)
3198 .set(
3199 "width",
3200 bar_width.min(plot_x_i32 + plot_width_i32 - start_x.max(plot_x_i32)),
3201 )
3202 .set("height", 6)
3203 .set("fill", var_color)
3204 .set("stroke", "#ffffff")
3205 .set("stroke-width", 1)
3206 .set("rx", 2)
3207 .set("opacity", 0.9);
3208 document = document.add(var_bar);
3209
3210 let var_info = format!("{}: {}", var_name, format_bytes(alloc.size));
3212 let var_label = SvgText::new(if var_info.len() > 25 {
3213 format!("{}...", &var_info[..22])
3214 } else {
3215 var_info
3216 })
3217 .set("x", start_x.max(plot_x_i32) + 5)
3218 .set("y", var_y + 4)
3219 .set("font-size", 7)
3220 .set("font-weight", "500")
3221 .set("fill", "#2c3e50");
3222 document = document.add(var_label);
3223 }
3224 }
3225
3226 let legend_x = chart_x + 20;
3228 let legend_y = chart_y + 20;
3229 let legend_width = 200; let legend_height = 120; let legend_bg = Rectangle::new()
3234 .set("x", legend_x - 10)
3235 .set("y", legend_y - 15)
3236 .set("width", legend_width)
3237 .set("height", legend_height)
3238 .set("fill", "rgba(255,255,255,0.0)") .set("stroke", "none") .set("stroke-width", 0)
3241 .set("rx", 5);
3242 document = document.add(legend_bg);
3243
3244 let legend_title = SvgText::new("Type Categories")
3245 .set("x", legend_x)
3246 .set("y", legend_y)
3247 .set("font-size", 10) .set("font-weight", "bold")
3249 .set("fill", "#2c3e50");
3250 document = document.add(legend_title);
3251
3252 let unknown_count = allocations
3254 .iter()
3255 .filter(|a| {
3256 if let Some(type_name) = &a.type_name {
3257 let (_, category) = simplify_type_name(type_name);
3258 category == "Unknown"
3259 } else {
3260 true }
3262 })
3263 .count();
3264
3265 let unknown_legend_y = legend_y + 15;
3266
3267 let unknown_color_square = Rectangle::new()
3269 .set("x", legend_x)
3270 .set("y", unknown_legend_y - 6)
3271 .set("width", 8)
3272 .set("height", 8)
3273 .set("fill", "#95a5a6");
3274 document = document.add(unknown_color_square);
3275
3276 let unknown_label = if unknown_count > 0 {
3278 format!("System ({unknown_count} allocs)")
3279 } else {
3280 "No System Allocs".to_string()
3281 };
3282
3283 let unknown_text = SvgText::new(unknown_label)
3284 .set("x", legend_x + 12)
3285 .set("y", unknown_legend_y - 1)
3286 .set("font-size", 8)
3287 .set("fill", "#2c3e50");
3288 document = document.add(unknown_text);
3289
3290 for (i, category) in categorized.iter().take(4).enumerate() {
3291 let legend_item_y = legend_y + 30 + (i as i32) * 15; let color_square = Rectangle::new()
3296 .set("x", legend_x)
3297 .set("y", legend_item_y - 6)
3298 .set("width", 8)
3299 .set("height", 8)
3300 .set("fill", category.color.as_str());
3301 document = document.add(color_square);
3302
3303 let category_name = if category.name.len() > 15 {
3305 format!("{}...", &category.name[..12])
3306 } else {
3307 category.name.clone()
3308 };
3309
3310 let category_text = SvgText::new(category_name)
3311 .set("x", legend_x + 12)
3312 .set("y", legend_item_y - 1)
3313 .set("font-size", 8) .set("fill", "#2c3e50");
3315 document = document.add(category_text);
3316 }
3317
3318 let total_tracked = tracked_vars.len();
3320 if total_tracked > max_vars_to_show {
3321 let note_text = format!("Note: Showing top {max_vars_to_show} variables with highest memory usage and longest lifecycles (out of {total_tracked} total tracked variables)");
3322 let note = SvgText::new(note_text)
3323 .set("x", chart_x + 20)
3324 .set("y", chart_y + chart_height - 10)
3325 .set("font-size", 11)
3326 .set("font-style", "italic")
3327 .set("fill", "#7f8c8d");
3328 document = document.add(note);
3329 }
3330
3331 Ok(document)
3332}
3333
3334pub fn add_memory_heatmap(
3336 document: Document,
3337 _allocations: &[AllocationInfo],
3338) -> TrackingResult<Document> {
3339 Ok(document)
3342}
3343
3344fn extract_scope_name(var_name: &str) -> String {
3346 if var_name.contains("global") {
3348 "Global Scope".to_string()
3349 } else if var_name.contains("static") {
3350 "Static Scope".to_string()
3351 } else if var_name.contains("boxed") {
3352 "Boxed Allocations".to_string()
3353 } else if var_name.contains("shared") || var_name.contains("arc") || var_name.contains("rc") {
3354 "Shared References".to_string()
3355 } else if var_name.contains("node") {
3356 "Graph Nodes".to_string()
3357 } else if var_name.contains("mutable") {
3358 "Mutable Data".to_string()
3359 } else if var_name.contains("_") {
3360 let prefix = var_name.split('_').next().unwrap_or("Unknown");
3362 format!("{} Scope", prefix.to_ascii_uppercase())
3363 } else {
3364 if var_name.len() > 6 {
3366 format!("{} Scope", &var_name[..6].to_ascii_uppercase())
3367 } else {
3368 "Main Scope".to_string()
3369 }
3370 }
3371}
3372
3373#[cfg(test)]
3374mod tests {
3375 use super::*;
3376 use crate::core::types::{AllocationInfo, TypeMemoryUsage};
3377
3378 fn create_test_allocation(
3380 size: usize,
3381 type_name: Option<String>,
3382 var_name: Option<String>,
3383 ) -> AllocationInfo {
3384 let mut alloc = AllocationInfo::new(0, size);
3385 alloc.type_name = type_name;
3386 alloc.var_name = var_name;
3387 alloc
3388 }
3389
3390 fn create_test_type_usage(
3392 type_name: String,
3393 total_size: usize,
3394 allocation_count: usize,
3395 ) -> TypeMemoryUsage {
3396 TypeMemoryUsage {
3397 type_name,
3398 total_size,
3399 allocation_count,
3400 average_size: if allocation_count > 0 {
3401 total_size as f64 / allocation_count as f64
3402 } else {
3403 0.0
3404 },
3405 peak_size: total_size,
3406 current_size: total_size,
3407 efficiency_score: 1.0,
3408 }
3409 }
3410
3411 #[test]
3412 fn test_calculate_allocation_percentiles_empty() {
3413 let allocations = vec![];
3414 let (median, p95) = calculate_allocation_percentiles(&allocations);
3415 assert_eq!(median, 0);
3416 assert_eq!(p95, 0);
3417 }
3418
3419 #[test]
3420 fn test_calculate_allocation_percentiles_single() {
3421 let allocations = vec![create_test_allocation(100, None, None)];
3422 let (median, p95) = calculate_allocation_percentiles(&allocations);
3423 assert_eq!(median, 100);
3424 assert_eq!(p95, 100);
3425 }
3426
3427 #[test]
3428 fn test_calculate_allocation_percentiles_even_count() {
3429 let allocations = vec![
3430 create_test_allocation(100, None, None),
3431 create_test_allocation(200, None, None),
3432 create_test_allocation(300, None, None),
3433 create_test_allocation(400, None, None),
3434 ];
3435 let (median, p95) = calculate_allocation_percentiles(&allocations);
3436 assert_eq!(median, 250); assert_eq!(p95, 400); }
3439
3440 #[test]
3441 fn test_calculate_allocation_percentiles_odd_count() {
3442 let allocations = vec![
3443 create_test_allocation(100, None, None),
3444 create_test_allocation(200, None, None),
3445 create_test_allocation(300, None, None),
3446 ];
3447 let (median, p95) = calculate_allocation_percentiles(&allocations);
3448 assert_eq!(median, 200); assert_eq!(p95, 300); }
3451
3452 #[test]
3453 fn test_extract_generic_inner_type() {
3454 assert_eq!(extract_generic_inner_type("Vec<i32>", "Vec"), "i32");
3455 assert_eq!(
3456 extract_generic_inner_type("HashMap<String, i32>", "HashMap"),
3457 "String, i32"
3458 );
3459 assert_eq!(
3460 extract_generic_inner_type("Option<String>", "Option"),
3461 "String"
3462 );
3463 assert_eq!(
3464 extract_generic_inner_type("std::vec::Vec<u64>", "Vec"),
3465 "u64"
3466 );
3467 assert_eq!(extract_generic_inner_type("NoGeneric", "Vec"), "?");
3468 assert_eq!(
3469 extract_generic_inner_type("Vec<std::string::String>", "Vec"),
3470 "String"
3471 );
3472 }
3473
3474 #[test]
3475 fn test_extract_inner_primitive_types_enhanced() {
3476 let result = extract_inner_primitive_types_enhanced("Vec<i32>");
3477 assert!(result.contains(&"i32".to_string()));
3478
3479 let result = extract_inner_primitive_types_enhanced("HashMap<String, u64>");
3480 assert!(result.contains(&"u64".to_string()));
3482
3483 let result = extract_inner_primitive_types_enhanced("Option<bool>");
3484 assert!(result.contains(&"bool".to_string()));
3485
3486 let result = extract_inner_primitive_types_enhanced("NoGeneric");
3487 assert!(result.is_empty());
3488 }
3489
3490 #[test]
3491 fn test_categorize_allocations_empty() {
3492 let allocations = vec![];
3493 let categories = categorize_allocations(&allocations);
3494 assert!(categories.is_empty());
3495 }
3496
3497 #[test]
3498 fn test_categorize_allocations_with_unknown_types() {
3499 let allocations = vec![
3500 create_test_allocation(100, None, Some("var1".to_string())),
3501 create_test_allocation(200, Some("Unknown".to_string()), Some("var2".to_string())),
3502 ];
3503 let categories = categorize_allocations(&allocations);
3504 assert!(categories.is_empty()); }
3506
3507 #[test]
3508 fn test_categorize_allocations_valid() {
3509 let allocations = vec![
3510 create_test_allocation(100, Some("Vec<i32>".to_string()), Some("vec1".to_string())),
3511 create_test_allocation(200, Some("String".to_string()), Some("str1".to_string())),
3512 create_test_allocation(150, Some("Vec<u64>".to_string()), Some("vec2".to_string())),
3513 ];
3514 let categories = categorize_allocations(&allocations);
3515
3516 assert!(!categories.is_empty());
3517 assert!(categories[0].total_size >= categories.get(1).map_or(0, |c| c.total_size));
3519 }
3520
3521 #[test]
3522 fn test_enhance_type_information_empty() {
3523 let memory_by_type = vec![];
3524 let allocations = vec![];
3525 let enhanced = enhance_type_information(&memory_by_type, &allocations);
3526 assert!(enhanced.is_empty());
3527 }
3528
3529 #[test]
3530 fn test_enhance_type_information_basic() {
3531 let memory_by_type = vec![
3532 create_test_type_usage("Vec<i32>".to_string(), 1000, 5),
3533 create_test_type_usage("String".to_string(), 500, 3),
3534 ];
3535 let allocations = vec![
3536 create_test_allocation(
3537 200,
3538 Some("Vec<i32>".to_string()),
3539 Some("my_vec".to_string()),
3540 ),
3541 create_test_allocation(
3542 100,
3543 Some("String".to_string()),
3544 Some("my_string".to_string()),
3545 ),
3546 ];
3547
3548 let enhanced = enhance_type_information(&memory_by_type, &allocations);
3549 assert!(!enhanced.is_empty());
3550
3551 let vec_info = enhanced.iter().find(|e| e.simplified_name.contains("Vec"));
3553 assert!(vec_info.is_some());
3554
3555 let string_info = enhanced
3556 .iter()
3557 .find(|e| e.simplified_name.contains("String"));
3558 assert!(string_info.is_some());
3559 }
3560
3561 #[test]
3562 fn test_categorize_enhanced_allocations_empty() {
3563 let enhanced_types = vec![];
3564 let categories = categorize_enhanced_allocations(&enhanced_types);
3565 assert!(categories.is_empty());
3566 }
3567
3568 #[test]
3569 fn test_categorize_enhanced_allocations_with_unknown() {
3570 let enhanced_types = vec![EnhancedTypeInfo {
3571 simplified_name: "Unknown".to_string(),
3572 category: "Unknown".to_string(),
3573 subcategory: "Unknown".to_string(),
3574 total_size: 100,
3575 allocation_count: 1,
3576 variable_names: vec!["var1".to_string()],
3577 }];
3578 let categories = categorize_enhanced_allocations(&enhanced_types);
3579 assert!(categories.is_empty()); }
3581
3582 #[test]
3583 fn test_categorize_enhanced_allocations_valid() {
3584 let enhanced_types = vec![
3585 EnhancedTypeInfo {
3586 simplified_name: "Vec".to_string(),
3587 category: "Collections".to_string(),
3588 subcategory: "Vec<T>".to_string(),
3589 total_size: 1000,
3590 allocation_count: 5,
3591 variable_names: vec!["vec1".to_string(), "vec2".to_string()],
3592 },
3593 EnhancedTypeInfo {
3594 simplified_name: "String".to_string(),
3595 category: "Basic Types".to_string(),
3596 subcategory: "Strings".to_string(),
3597 total_size: 500,
3598 allocation_count: 3,
3599 variable_names: vec!["str1".to_string()],
3600 },
3601 ];
3602
3603 let categories = categorize_enhanced_allocations(&enhanced_types);
3604 assert_eq!(categories.len(), 2);
3605
3606 assert!(categories[0].total_size >= categories[1].total_size);
3608
3609 let category_names: Vec<&String> = categories.iter().map(|c| &c.name).collect();
3611 assert!(category_names.contains(&&"Collections".to_string()));
3612 assert!(category_names.contains(&&"Basic Types".to_string()));
3613 }
3614
3615 #[test]
3616 fn test_extract_scope_name_patterns() {
3617 assert_eq!(extract_scope_name("global_var"), "Global Scope");
3618 assert_eq!(extract_scope_name("static_data"), "Static Scope");
3619 assert_eq!(extract_scope_name("boxed_value"), "Boxed Allocations");
3620 assert_eq!(extract_scope_name("shared_ptr"), "Shared References");
3621 assert_eq!(extract_scope_name("arc_data"), "Shared References");
3622 assert_eq!(extract_scope_name("rc_value"), "Shared References");
3623 assert_eq!(extract_scope_name("node_data"), "Graph Nodes");
3624 assert_eq!(extract_scope_name("mutable_ref"), "Mutable Data");
3625 assert_eq!(extract_scope_name("test_variable"), "TEST Scope");
3626 assert_eq!(extract_scope_name("verylongname"), "VERYLO Scope");
3627 assert_eq!(extract_scope_name("short"), "Main Scope");
3628 }
3629
3630 #[test]
3631 fn test_analyze_type_with_detailed_subcategory() {
3632 let (simplified, category, subcategory) =
3633 analyze_type_with_detailed_subcategory("Vec<i32>");
3634 assert_eq!(simplified, "Vec<i32>"); assert_eq!(category, "Collections");
3636 assert_eq!(subcategory, "Vec<T>");
3637
3638 let (simplified, category, subcategory) =
3639 analyze_type_with_detailed_subcategory("HashMap<String, u64>");
3640 assert_eq!(simplified, "HashMap<K,V>");
3641 assert_eq!(category, "Collections");
3642 assert_eq!(subcategory, "HashMap<K,V>");
3643
3644 let (simplified, category, subcategory) = analyze_type_with_detailed_subcategory("String");
3645 assert_eq!(simplified, "String");
3646 assert_eq!(category, "Basic Types");
3647 assert_eq!(subcategory, "Strings");
3648
3649 let (simplified, category, subcategory) = analyze_type_with_detailed_subcategory("i32");
3650 assert_eq!(simplified, "i32");
3651 assert_eq!(category, "Basic Types");
3652 assert_eq!(subcategory, "Integers");
3653
3654 let (simplified, category, subcategory) =
3655 analyze_type_with_detailed_subcategory("Box<String>");
3656 assert_eq!(simplified, "String"); assert_eq!(category, "Smart Pointers");
3658 assert_eq!(subcategory, "Box<T>");
3659 }
3660
3661 #[test]
3662 fn test_extract_name_and_percentage() {
3663 let (name, percentage) = extract_name_and_percentage("Collections (75.5%)");
3664 assert_eq!(name, "Collections");
3665 assert_eq!(percentage, Some("75.5%"));
3666
3667 let (name, percentage) = extract_name_and_percentage("Basic Types");
3668 assert_eq!(name, "Basic Types");
3669 assert_eq!(percentage, None);
3670
3671 let (name, percentage) = extract_name_and_percentage("Vec<T> (100.0%)");
3672 assert_eq!(name, "Vec<T>");
3673 assert_eq!(percentage, Some("100.0%"));
3674 }
3675
3676 #[test]
3677 fn test_calculate_font_size() {
3678 assert_eq!(calculate_font_size(50.0), 9); assert_eq!(calculate_font_size(100.0), 11); assert_eq!(calculate_font_size(200.0), 13); assert_eq!(calculate_font_size(400.0), 15); assert_eq!(calculate_font_size(1000.0), 15); }
3684
3685 #[test]
3686 fn test_build_collections_node_empty() {
3687 let collections_types = vec![];
3688 let total_memory = 1000;
3689 let node = build_collections_node(&collections_types, total_memory);
3690
3691 assert_eq!(node.name, "Collections");
3692 assert_eq!(node.size, 1);
3693 assert_eq!(node.percentage, 0.0);
3694 assert!(node.children.is_empty());
3695 }
3696
3697 #[test]
3698 fn test_build_collections_node_with_data() {
3699 let enhanced_type = EnhancedTypeInfo {
3700 simplified_name: "Vec".to_string(),
3701 category: "Collections".to_string(),
3702 subcategory: "Vec<T>".to_string(),
3703 total_size: 500,
3704 allocation_count: 3,
3705 variable_names: vec!["vec1".to_string()],
3706 };
3707 let collections_types = vec![&enhanced_type];
3708 let total_memory = 1000;
3709 let node = build_collections_node(&collections_types, total_memory);
3710
3711 assert!(node.name.contains("Collections"));
3712 assert_eq!(node.size, 500);
3713 assert_eq!(node.percentage, 50.0);
3714 assert!(!node.children.is_empty());
3715 }
3716
3717 #[test]
3718 fn test_build_basic_types_node_empty() {
3719 let basic_types = vec![];
3720 let total_memory = 1000;
3721 let node = build_basic_types_node(&basic_types, total_memory);
3722
3723 assert_eq!(node.name, "Basic Types");
3724 assert_eq!(node.size, 1);
3725 assert_eq!(node.percentage, 0.0);
3726 assert!(node.children.is_empty());
3727 }
3728
3729 #[test]
3730 fn test_build_basic_types_node_with_data() {
3731 let enhanced_type = EnhancedTypeInfo {
3732 simplified_name: "String".to_string(),
3733 category: "Basic Types".to_string(),
3734 subcategory: "Strings".to_string(),
3735 total_size: 300,
3736 allocation_count: 2,
3737 variable_names: vec!["str1".to_string()],
3738 };
3739 let basic_types = vec![&enhanced_type];
3740 let total_memory = 1000;
3741 let node = build_basic_types_node(&basic_types, total_memory);
3742
3743 assert!(node.name.contains("Basic Types"));
3744 assert_eq!(node.size, 300);
3745 assert_eq!(node.percentage, 30.0);
3746 assert!(!node.children.is_empty());
3747 }
3748
3749 #[test]
3750 fn test_analyze_data_distribution_minimal() {
3751 let collections_types = vec![];
3752 let basic_types = vec![];
3753 let smart_pointers_types = vec![];
3754 let other_types = vec![];
3755 let total_memory = 1000;
3756
3757 let strategy = analyze_data_distribution(
3758 &collections_types,
3759 &basic_types,
3760 &smart_pointers_types,
3761 &other_types,
3762 total_memory,
3763 );
3764
3765 matches!(strategy, TreemapLayoutStrategy::MinimalLayout);
3766 }
3767
3768 #[test]
3769 fn test_analyze_data_distribution_collections_dominant() {
3770 let enhanced_type = EnhancedTypeInfo {
3771 simplified_name: "Vec".to_string(),
3772 category: "Collections".to_string(),
3773 subcategory: "Vec<T>".to_string(),
3774 total_size: 900,
3775 allocation_count: 5,
3776 variable_names: vec!["vec1".to_string()],
3777 };
3778 let collections_types = vec![&enhanced_type];
3779 let basic_types = vec![];
3780 let smart_pointers_types = vec![];
3781 let other_types = vec![];
3782 let total_memory = 1000;
3783
3784 let strategy = analyze_data_distribution(
3785 &collections_types,
3786 &basic_types,
3787 &smart_pointers_types,
3788 &other_types,
3789 total_memory,
3790 );
3791
3792 matches!(strategy, TreemapLayoutStrategy::CollectionsOnlyLayout);
3793 }
3794
3795 #[test]
3796 fn test_treemap_node_creation() {
3797 let node = TreemapNode {
3798 name: "Test Node".to_string(),
3799 size: 100,
3800 percentage: 50.0,
3801 color: "#ff0000".to_string(),
3802 children: vec![],
3803 };
3804
3805 assert_eq!(node.name, "Test Node");
3806 assert_eq!(node.size, 100);
3807 assert_eq!(node.percentage, 50.0);
3808 assert_eq!(node.color, "#ff0000");
3809 assert!(node.children.is_empty());
3810 }
3811
3812 #[test]
3813 fn test_allocation_category_creation() {
3814 let category = AllocationCategory {
3815 name: "Test Category".to_string(),
3816 allocations: vec![],
3817 total_size: 500,
3818 color: "#00ff00".to_string(),
3819 };
3820
3821 assert_eq!(category.name, "Test Category");
3822 assert_eq!(category.total_size, 500);
3823 assert_eq!(category.color, "#00ff00");
3824 assert!(category.allocations.is_empty());
3825 }
3826
3827 #[test]
3828 fn test_enhanced_type_info_creation() {
3829 let enhanced_type = EnhancedTypeInfo {
3830 simplified_name: "TestType".to_string(),
3831 category: "TestCategory".to_string(),
3832 subcategory: "TestSubcategory".to_string(),
3833 total_size: 1000,
3834 allocation_count: 10,
3835 variable_names: vec!["var1".to_string(), "var2".to_string()],
3836 };
3837
3838 assert_eq!(enhanced_type.simplified_name, "TestType");
3839 assert_eq!(enhanced_type.category, "TestCategory");
3840 assert_eq!(enhanced_type.subcategory, "TestSubcategory");
3841 assert_eq!(enhanced_type.total_size, 1000);
3842 assert_eq!(enhanced_type.allocation_count, 10);
3843 assert_eq!(enhanced_type.variable_names.len(), 2);
3844 }
3845
3846 #[test]
3848 fn test_add_memory_heatmap() {
3849 let document = svg::Document::new();
3850 let allocations = vec![];
3851
3852 let result = add_memory_heatmap(document, &allocations);
3853 assert!(result.is_ok());
3854 }
3855}