1use crate::analysis::memory_passport_tracker::MemoryPassportTracker;
4use crate::tracker::Tracker;
5use handlebars::Handlebars;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8
9const HIGH_RISK_PENALTY: f64 = 10.0;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DashboardContext {
16 pub title: String,
18 pub export_timestamp: String,
20 pub total_memory: String,
22 pub total_allocations: usize,
24 pub active_allocations: usize,
26 pub peak_memory: String,
28 pub thread_count: usize,
30 pub passport_count: usize,
32 pub leak_count: usize,
34 pub unsafe_count: usize,
36 pub ffi_count: usize,
38 pub allocations: Vec<AllocationInfo>,
40 pub relationships: Vec<RelationshipInfo>,
42 pub unsafe_reports: Vec<UnsafeReport>,
44 pub passport_details: Vec<PassportDetail>,
46 pub allocations_count: usize,
48 pub relationships_count: usize,
50 pub unsafe_reports_count: usize,
52 pub json_data: String,
54 pub os_name: String,
56 pub architecture: String,
58 pub cpu_cores: usize,
60 pub system_resources: SystemResources,
62 pub threads: Vec<ThreadInfo>,
64 pub async_tasks: Vec<AsyncTaskInfo>,
66 pub async_summary: AsyncSummary,
68 pub health_score: u32,
70 pub health_status: String,
72 pub safe_ops_count: usize,
74 pub high_risk_count: usize,
76 pub clean_passport_count: usize,
78 pub active_passport_count: usize,
80 pub leaked_passport_count: usize,
82 pub ffi_tracked_count: usize,
84 pub safe_code_percent: u32,
86 pub ownership_graph: OwnershipGraphInfo,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct OwnershipGraphInfo {
93 pub total_nodes: usize,
95 pub total_edges: usize,
97 pub total_cycles: usize,
99 pub rc_clone_count: usize,
101 pub arc_clone_count: usize,
103 pub has_issues: bool,
105 pub issues: Vec<OwnershipIssue>,
107 pub root_cause: Option<RootCauseInfo>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct OwnershipIssue {
114 pub issue_type: String,
116 pub severity: String,
118 pub description: String,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct RootCauseInfo {
125 pub cause: String,
127 pub description: String,
129 pub impact: String,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct AsyncTaskInfo {
136 pub task_id: u64,
138 pub task_name: String,
140 pub task_type: String,
142 pub total_bytes: u64,
144 pub current_memory: u64,
146 pub peak_memory: u64,
148 pub total_allocations: u64,
150 pub duration_ms: f64,
152 pub efficiency_score: f64,
154 pub is_completed: bool,
156 pub has_potential_leak: bool,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct AsyncSummary {
163 pub total_tasks: usize,
165 pub active_tasks: usize,
167 pub total_allocations: usize,
169 pub total_memory_bytes: usize,
171 pub peak_memory_bytes: usize,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct AllocationInfo {
178 pub address: String,
180 pub type_name: String,
182 pub size: usize,
184 pub var_name: String,
186 pub timestamp: String,
188 pub thread_id: String,
190 pub immutable_borrows: usize,
192 pub mutable_borrows: usize,
193 pub is_clone: bool,
195 pub clone_count: usize,
196 pub timestamp_alloc: u64,
198 pub timestamp_dealloc: u64,
200 pub lifetime_ms: f64,
202 pub is_leaked: bool,
204 pub allocation_type: String,
206 pub is_smart_pointer: bool,
208 pub smart_pointer_type: String,
210 pub source_file: Option<String>,
212 pub source_line: Option<u32>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218struct ThreadStats {
219 id: u64,
221 allocations: usize,
223 memory: usize,
225 peak: usize,
227 status: String,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233struct TimelineAllocation {
234 timestamp: u64,
236 thread_id: u64,
238 size: usize,
240 var_name: Option<String>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246struct ThreadConflict {
247 description: String,
249 threads: String,
251 #[serde(rename = "type")]
253 conflict_type: String,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct RelationshipInfo {
259 pub source_ptr: String,
261 pub source_var_name: String,
263 pub target_ptr: String,
265 pub target_var_name: String,
267 pub relationship_type: String,
269 pub strength: f64,
271 pub type_name: String,
273 pub color: String,
275 pub is_part_of_cycle: bool,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct UnsafeReport {
282 pub passport_id: String,
284 pub allocation_ptr: String,
286 pub var_name: String,
288 pub type_name: String,
290 pub size_bytes: usize,
292 pub created_at: u64,
294 pub updated_at: u64,
296 pub status: String,
298 pub lifecycle_events: Vec<LifecycleEventInfo>,
300 pub cross_boundary_events: Vec<BoundaryEventInfo>,
302 pub is_leaked: bool,
304 pub risk_level: String,
306 pub risk_factors: Vec<String>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct LifecycleEventInfo {
313 pub event_type: String,
315 pub timestamp: u64,
317 pub context: String,
319 pub icon: String,
321 pub color: String,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct BoundaryEventInfo {
328 pub event_type: String,
330 pub from_context: String,
332 pub to_context: String,
334 pub timestamp: u64,
336 pub icon: String,
338 pub color: String,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct PassportDetail {
345 pub passport_id: String,
347 pub allocation_ptr: String,
349 pub var_name: String,
351 pub type_name: String,
353 pub size_bytes: usize,
355 pub status: String,
357 pub created_at: u64,
359 pub updated_at: u64,
361 pub is_leaked: bool,
363 pub ffi_tracked: bool,
365 pub lifecycle_events: Vec<LifecycleEventInfo>,
367 pub cross_boundary_events: Vec<BoundaryEventInfo>,
369 pub risk_level: String,
371 pub risk_confidence: f64,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct SystemResources {
378 pub os_name: String,
380 pub os_version: String,
382 pub architecture: String,
384 pub cpu_cores: u32,
386 pub total_physical: String,
388 pub available_physical: String,
390 pub used_physical: String,
392 pub page_size: u64,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct ThreadInfo {
399 pub thread_id: String,
401 pub thread_summary: String,
403 pub allocation_count: usize,
405 pub current_memory: String,
407 pub peak_memory: String,
409 pub total_allocated: String,
411 pub current_memory_bytes: usize,
413 pub peak_memory_bytes: usize,
415 pub total_allocated_bytes: usize,
417}
418
419struct ThreadAggregator {
420 allocation_count: usize,
421 current_memory: usize,
422 peak_memory: usize,
423 total_allocated: usize,
424}
425
426pub struct DashboardRenderer {
428 handlebars: Handlebars<'static>,
429}
430
431impl DashboardRenderer {
432 pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
434 let mut handlebars = Handlebars::new();
435
436 let template_path = format!(
437 "{}/src/render_engine/dashboard/templates/dashboard_unified.html",
438 env!("CARGO_MANIFEST_DIR")
439 );
440 handlebars.register_template_file("dashboard_unified", &template_path)?;
441
442 let final_path = format!(
443 "{}/src/render_engine/dashboard/templates/dashboard_final.html",
444 env!("CARGO_MANIFEST_DIR")
445 );
446 handlebars.register_template_file("dashboard_final", &final_path)?;
447
448 handlebars.register_helper("format_bytes", Box::new(format_bytes_helper));
449 handlebars.register_helper("gt", Box::new(greater_than_helper));
450 handlebars.register_helper("contains", Box::new(contains_helper));
451 handlebars.register_helper("json", Box::new(json_helper));
452
453 Ok(Self { handlebars })
454 }
455
456 fn extract_user_source_file(stack_trace: &Option<Vec<String>>) -> Option<String> {
458 if let Some(ref frames) = stack_trace {
459 for frame in frames {
460 let frame_lower = frame.to_lowercase();
461 if !frame_lower.contains("/rustc/")
462 && !frame_lower.contains("/library/")
463 && !frame_lower.contains("memscope")
464 && !frame_lower.contains(".cargo/registry")
465 && !frame_lower.contains("/src/core/")
466 && !frame_lower.contains("/src/capture/")
467 && !frame_lower.contains("/src/unified/")
468 && !frame_lower.contains("/src/tracker")
469 {
470 if let Some(file_part) = frame.split(':').next() {
471 let file_name = file_part.split('/').next_back().unwrap_or(file_part);
472 if !file_name.starts_with('<') && file_name.contains(".rs") {
473 return Some(file_part.to_string());
474 }
475 }
476 }
477 }
478 }
479 None
480 }
481
482 fn extract_user_source_line(stack_trace: &Option<Vec<String>>) -> Option<u32> {
484 if let Some(ref frames) = stack_trace {
485 for frame in frames {
486 let frame_lower = frame.to_lowercase();
487 if !frame_lower.contains("/rustc/")
488 && !frame_lower.contains("/library/")
489 && !frame_lower.contains("memscope")
490 && !frame_lower.contains(".cargo/registry")
491 && !frame_lower.contains("/src/core/")
492 && !frame_lower.contains("/src/capture/")
493 && !frame_lower.contains("/src/unified/")
494 && !frame_lower.contains("/src/tracker")
495 {
496 if let Some(line_part) = frame.rsplit(':').next() {
497 if let Ok(line) = line_part.parse::<u32>() {
498 return Some(line);
499 }
500 }
501 }
502 }
503 }
504 None
505 }
506
507 fn build_relationships_from_inference(
509 allocations: &[crate::capture::backends::core_types::AllocationInfo],
510 alloc_info: &[AllocationInfo],
511 ) -> Vec<RelationshipInfo> {
512 use crate::analysis::relation_inference::{Relation, RelationGraphBuilder};
513 use crate::snapshot::types::ActiveAllocation;
514
515 let active_allocations: Vec<ActiveAllocation> = allocations
517 .iter()
518 .map(|a| ActiveAllocation {
519 ptr: a.ptr,
520 size: a.size,
521 allocated_at: a.allocated_at_ns,
522 var_name: a.var_name.clone(),
523 type_name: a.type_name.clone(),
524 thread_id: a.thread_id,
525 call_stack_hash: None,
526 })
527 .collect();
528
529 let graph = RelationGraphBuilder::build(&active_allocations, None);
530
531 let mut relationships = Vec::new();
532
533 for edge in &graph.edges {
534 let from_alloc = alloc_info.get(edge.from);
535 let to_alloc = alloc_info.get(edge.to);
536
537 let (rel_type, color, strength) = match edge.relation {
538 Relation::Owner => ("ownership_transfer", "#dc2626", 1.0),
539 Relation::Slice => ("immutable_borrow", "#3b82f6", 0.8),
540 Relation::Clone => ("clone", "#10b981", 0.9),
541 Relation::Shared => ("Arc", "#8b5cf6", 0.7),
542 };
543
544 let source_addr = from_alloc
545 .and_then(|a| {
546 if a.address.starts_with("0x") {
547 usize::from_str_radix(&a.address[2..], 16).ok()
548 } else {
549 None
550 }
551 })
552 .unwrap_or(edge.from);
553
554 let target_addr = to_alloc
555 .and_then(|a| {
556 if a.address.starts_with("0x") {
557 usize::from_str_radix(&a.address[2..], 16).ok()
558 } else {
559 None
560 }
561 })
562 .unwrap_or(edge.to);
563
564 relationships.push(RelationshipInfo {
565 source_ptr: format!("0x{:x}", source_addr),
566 source_var_name: from_alloc
567 .map(|a| a.var_name.clone())
568 .unwrap_or_else(|| format!("alloc_{}", edge.from)),
569 target_ptr: format!("0x{:x}", target_addr),
570 target_var_name: to_alloc
571 .map(|a| a.var_name.clone())
572 .unwrap_or_else(|| format!("alloc_{}", edge.to)),
573 relationship_type: rel_type.to_string(),
574 strength,
575 type_name: from_alloc
576 .map(|a| a.type_name.clone())
577 .unwrap_or_else(|| "unknown".to_string()),
578 color: color.to_string(),
579 is_part_of_cycle: false,
580 });
581 }
582
583 if relationships.len() > 500 {
585 relationships.truncate(500);
586 }
587
588 relationships
589 }
590
591 fn infer_type_from_size(size: usize) -> String {
593 match size {
594 8 => "*mut c_void (30%)".to_string(),
595 16 => "&[T] (25%)".to_string(),
596 24 => "Vec<_>/String (15%)".to_string(),
597 32 | 48 | 64 => "CStruct (10%)".to_string(),
598 n if n.is_power_of_two() && n >= 64 => {
599 format!("Vec<_>/[u8] ({}%)", 10 + n.trailing_zeros() as u8)
600 }
601 n if (32..=256).contains(&n) => "[u8] (10%)".to_string(),
602 _ => "unknown".to_string(),
603 }
604 }
605
606 fn get_inferred_type_name(type_name: &str, size: usize) -> String {
608 if type_name != "unknown" && type_name != "-" && !type_name.is_empty() {
609 return type_name.to_string();
610 }
611 Self::infer_type_from_size(size)
612 }
613
614 pub fn build_context_from_tracker(
616 &self,
617 tracker: &Tracker,
618 passport_tracker: &Arc<MemoryPassportTracker>,
619 ) -> Result<DashboardContext, Box<dyn std::error::Error>> {
620 self.build_context_from_tracker_with_async(tracker, passport_tracker, None)
621 }
622
623 pub fn build_context_from_tracker_with_async(
625 &self,
626 tracker: &Tracker,
627 passport_tracker: &Arc<MemoryPassportTracker>,
628 async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
629 ) -> Result<DashboardContext, Box<dyn std::error::Error>> {
630 let allocations = tracker.inner().get_active_allocations().unwrap_or_default();
631 let passports = passport_tracker.get_all_passports();
632 let analysis = tracker.analyze();
633
634 let total_memory: usize = allocations.iter().map(|a| a.size).sum();
635
636 let alloc_info: Vec<AllocationInfo> = allocations
638 .iter()
639 .map(|a| {
640 let original_type_name =
641 a.type_name.clone().unwrap_or_else(|| "unknown".to_string());
642 let type_name = Self::get_inferred_type_name(&original_type_name, a.size);
643 let timestamp_alloc = a.allocated_at_ns;
644 let timestamp_dealloc = 0u64;
645 let lifetime_ms = 0.0;
646
647 let is_smart_pointer = type_name.contains("Arc")
649 || type_name.contains("Rc")
650 || type_name.contains("Box");
651 let smart_pointer_type = if type_name.contains("Arc") {
652 "Arc".to_string()
653 } else if type_name.contains("Rc") {
654 "Rc".to_string()
655 } else if type_name.contains("Box") {
656 "Box".to_string()
657 } else {
658 String::new()
659 };
660
661 AllocationInfo {
662 address: format!("0x{:x}", a.ptr),
663 type_name: type_name.clone(),
664 size: a.size,
665 var_name: a.var_name.clone().unwrap_or_else(|| "unknown".to_string()),
666 timestamp: format!("{:?}", a.allocated_at_ns),
667 thread_id: format!("{}", a.thread_id),
668 immutable_borrows: 0,
669 mutable_borrows: 0,
670 is_clone: false,
671 clone_count: 0,
672 timestamp_alloc,
673 timestamp_dealloc,
674 lifetime_ms,
675 is_leaked: true,
676 allocation_type: "heap".to_string(),
677 is_smart_pointer,
678 smart_pointer_type,
679 source_file: Self::extract_user_source_file(&a.stack_trace),
680 source_line: Self::extract_user_source_line(&a.stack_trace),
681 }
682 })
683 .collect();
684
685 let mut relationships = Self::build_relationships_from_inference(&allocations, &alloc_info);
687
688 let cycle_edges: std::collections::HashSet<(String, String)> = {
690 let rel_tuples: Vec<(String, String, String)> = relationships
691 .iter()
692 .map(|r| {
693 (
694 r.source_ptr.clone(),
695 r.target_ptr.clone(),
696 r.type_name.clone(),
697 )
698 })
699 .collect();
700 let result = crate::analysis::detect_cycles_in_relationships(&rel_tuples);
701 result.cycle_edges
702 };
703
704 for rel in &mut relationships {
705 if cycle_edges.contains(&(rel.source_ptr.clone(), rel.target_ptr.clone())) {
706 rel.is_part_of_cycle = true;
707 rel.color = "#ef4444".to_string();
708 }
709 }
710
711 let unsafe_reports: Vec<UnsafeReport> = passports.values()
713 .filter(|p| !p.lifecycle_events.is_empty())
714 .map(|p| {
715 let lifecycle_events: Vec<LifecycleEventInfo> = p.lifecycle_events.iter()
717 .map(|event| {
718 let (icon, color, context) = match &event.event_type {
719 crate::analysis::memory_passport_tracker::PassportEventType::AllocatedInRust => {
720 ("🟢".to_string(), "#10b981".to_string(), "Rust Allocation".to_string())
721 }
722 crate::analysis::memory_passport_tracker::PassportEventType::HandoverToFfi => {
723 ("⬇️".to_string(), "#f59e0b".to_string(), "Handover to FFI".to_string())
724 }
725 crate::analysis::memory_passport_tracker::PassportEventType::FreedByForeign => {
726 ("🔵".to_string(), "#3b82f6".to_string(), "Freed by Foreign".to_string())
727 }
728 crate::analysis::memory_passport_tracker::PassportEventType::ReclaimedByRust => {
729 ("⬆️".to_string(), "#10b981".to_string(), "Reclaimed by Rust".to_string())
730 }
731 crate::analysis::memory_passport_tracker::PassportEventType::BoundaryAccess => {
732 ("🔄".to_string(), "#8b5cf6".to_string(), "Boundary Access".to_string())
733 }
734 crate::analysis::memory_passport_tracker::PassportEventType::OwnershipTransfer => {
735 ("↔️".to_string(), "#dc2626".to_string(), "Ownership Transfer".to_string())
736 }
737 crate::analysis::memory_passport_tracker::PassportEventType::ValidationCheck => {
738 ("✅".to_string(), "#10b981".to_string(), "Validation Check".to_string())
739 }
740 crate::analysis::memory_passport_tracker::PassportEventType::CorruptionDetected => {
741 ("🚨".to_string(), "#dc2626".to_string(), "Corruption Detected".to_string())
742 }
743 };
744
745 LifecycleEventInfo {
746 event_type: format!("{:?}", event.event_type),
747 timestamp: event.timestamp,
748 context,
749 icon,
750 color,
751 }
752 })
753 .collect();
754
755 let cross_boundary_events: Vec<BoundaryEventInfo> = lifecycle_events.iter()
757 .filter(|e| e.event_type.contains("Handover") || e.event_type.contains("Reclaimed"))
758 .map(|e| {
759 let (event_type, from, to, icon, color) = if e.event_type.contains("HandoverToFfi") {
760 ("RustToFfi".to_string(), "Rust".to_string(), "FFI".to_string(), "⬇️".to_string(), "#f59e0b".to_string())
761 } else if e.event_type.contains("ReclaimedByRust") {
762 ("FfiToRust".to_string(), "FFI".to_string(), "Rust".to_string(), "⬆️".to_string(), "#10b981".to_string())
763 } else {
764 (e.event_type.clone(), "Unknown".to_string(), "Unknown".to_string(), "❓".to_string(), "#6b7280".to_string())
765 };
766
767 BoundaryEventInfo {
768 event_type,
769 from_context: from,
770 to_context: to,
771 timestamp: e.timestamp,
772 icon,
773 color,
774 }
775 })
776 .collect();
777
778 let is_leaked = p.status_at_shutdown == crate::analysis::memory_passport_tracker::PassportStatus::InForeignCustody ||
780 p.status_at_shutdown == crate::analysis::memory_passport_tracker::PassportStatus::HandoverToFfi;
781 let risk_level = if is_leaked {
782 "high".to_string()
783 } else if !cross_boundary_events.is_empty() {
784 "medium".to_string()
785 } else {
786 "low".to_string()
787 };
788
789 let var_name = if p.var_name != "-" {
791 p.var_name.clone()
792 } else {
793 allocations.iter()
794 .find(|a| a.ptr == p.allocation_ptr)
795 .and_then(|a| a.var_name.clone())
796 .unwrap_or_else(|| "-".to_string())
797 };
798
799 let type_name = if p.type_name != "-" {
800 p.type_name.clone()
801 } else {
802 let from_alloc = allocations.iter()
803 .find(|a| a.ptr == p.allocation_ptr)
804 .and_then(|a| a.type_name.clone())
805 .unwrap_or_else(|| "-".to_string());
806
807 if from_alloc != "-" {
808 from_alloc
809 } else {
810 Self::infer_type_from_size(p.size_bytes)
811 }
812 };
813
814 let mut risk_factors = Vec::new();
816 if is_leaked {
817 risk_factors.push("Memory leaked at shutdown".to_string());
818 }
819 if !cross_boundary_events.is_empty() {
820 risk_factors.push(format!("Crosses FFI boundary {} times", cross_boundary_events.len()));
821 }
822 if cross_boundary_events.len() > 3 {
823 risk_factors.push("Frequent boundary crossings".to_string());
824 }
825
826 UnsafeReport {
827 passport_id: p.passport_id.clone(),
828 allocation_ptr: format!("0x{:x}", p.allocation_ptr),
829 var_name,
830 type_name,
831 size_bytes: p.size_bytes,
832 created_at: p.created_at,
833 updated_at: p.updated_at,
834 status: format!("{:?}", p.status_at_shutdown),
835 lifecycle_events,
836 cross_boundary_events,
837 is_leaked,
838 risk_level,
839 risk_factors,
840 }
841 })
842 .collect();
843
844 let passport_details: Vec<PassportDetail> = passports.values()
846 .map(|p| {
847 let lifecycle_events: Vec<LifecycleEventInfo> = p.lifecycle_events.iter()
849 .map(|event| {
850 let (icon, color, context) = match &event.event_type {
851 crate::analysis::memory_passport_tracker::PassportEventType::AllocatedInRust => {
852 ("🟢".to_string(), "#10b981".to_string(), "Rust Allocation".to_string())
853 }
854 crate::analysis::memory_passport_tracker::PassportEventType::HandoverToFfi => {
855 ("⬇️".to_string(), "#f59e0b".to_string(), "Handover to FFI".to_string())
856 }
857 crate::analysis::memory_passport_tracker::PassportEventType::FreedByForeign => {
858 ("🔵".to_string(), "#3b82f6".to_string(), "Freed by Foreign".to_string())
859 }
860 crate::analysis::memory_passport_tracker::PassportEventType::ReclaimedByRust => {
861 ("⬆️".to_string(), "#10b981".to_string(), "Reclaimed by Rust".to_string())
862 }
863 crate::analysis::memory_passport_tracker::PassportEventType::BoundaryAccess => {
864 ("🔄".to_string(), "#8b5cf6".to_string(), "Boundary Access".to_string())
865 }
866 crate::analysis::memory_passport_tracker::PassportEventType::OwnershipTransfer => {
867 ("↔️".to_string(), "#dc2626".to_string(), "Ownership Transfer".to_string())
868 }
869 crate::analysis::memory_passport_tracker::PassportEventType::ValidationCheck => {
870 ("✅".to_string(), "#10b981".to_string(), "Validation Check".to_string())
871 }
872 crate::analysis::memory_passport_tracker::PassportEventType::CorruptionDetected => {
873 ("🚨".to_string(), "#dc2626".to_string(), "Corruption Detected".to_string())
874 }
875 };
876
877 LifecycleEventInfo {
878 event_type: format!("{:?}", event.event_type),
879 timestamp: event.timestamp,
880 context,
881 icon: icon.to_string(),
882 color,
883 }
884 })
885 .collect();
886
887 let cross_boundary_events: Vec<BoundaryEventInfo> = lifecycle_events.iter()
889 .filter(|e| e.event_type.contains("Handover") || e.event_type.contains("Reclaimed"))
890 .map(|e| {
891 let (event_type, from, to, icon, color) = if e.event_type.contains("HandoverToFfi") {
892 ("RustToFfi".to_string(), "Rust".to_string(), "FFI".to_string(), "⬇️".to_string(), "#f59e0b".to_string())
893 } else if e.event_type.contains("ReclaimedByRust") {
894 ("FfiToRust".to_string(), "FFI".to_string(), "Rust".to_string(), "⬆️".to_string(), "#10b981".to_string())
895 } else {
896 (e.event_type.clone(), "Unknown".to_string(), "Unknown".to_string(), "❓".to_string(), "#6b7280".to_string())
897 };
898
899 BoundaryEventInfo {
900 event_type,
901 from_context: from,
902 to_context: to,
903 timestamp: e.timestamp,
904 icon,
905 color,
906 }
907 })
908 .collect();
909
910 let var_name = if p.var_name != "-" {
912 p.var_name.clone()
913 } else {
914 allocations.iter()
915 .find(|a| a.ptr == p.allocation_ptr)
916 .and_then(|a| a.var_name.clone())
917 .unwrap_or_else(|| "-".to_string())
918 };
919
920 let type_name = if p.type_name != "-" {
921 p.type_name.clone()
922 } else {
923 let from_alloc = allocations.iter()
924 .find(|a| a.ptr == p.allocation_ptr)
925 .and_then(|a| a.type_name.clone())
926 .unwrap_or_else(|| "-".to_string());
927
928 if from_alloc != "-" {
929 from_alloc
930 } else {
931 Self::infer_type_from_size(p.size_bytes)
932 }
933 };
934
935 let is_leaked = p.status_at_shutdown == crate::analysis::memory_passport_tracker::PassportStatus::InForeignCustody ||
937 p.status_at_shutdown == crate::analysis::memory_passport_tracker::PassportStatus::HandoverToFfi;
938 let risk_level = if is_leaked {
939 "high".to_string()
940 } else if !cross_boundary_events.is_empty() {
941 "medium".to_string()
942 } else {
943 "low".to_string()
944 };
945
946 PassportDetail {
947 passport_id: p.passport_id.clone(),
948 allocation_ptr: format!("0x{:x}", p.allocation_ptr),
949 var_name,
950 type_name,
951 size_bytes: p.size_bytes,
952 status: format!("{:?}", p.status_at_shutdown),
953 created_at: p.created_at,
954 updated_at: p.updated_at,
955 is_leaked,
956 ffi_tracked: !cross_boundary_events.is_empty(),
957 lifecycle_events,
958 cross_boundary_events,
959 risk_level,
960 risk_confidence: 0.85, }
962 })
963 .collect();
964
965 let leak_result = passport_tracker.detect_leaks_at_shutdown();
967 let leak_count = leak_result.leaked_passports.len();
968
969 let thread_data = Self::aggregate_thread_data(&alloc_info);
971
972 #[derive(serde::Serialize)]
975 struct DashboardData<'a> {
976 allocations: &'a [AllocationInfo],
977 relationships: &'a [RelationshipInfo],
978 unsafe_reports: &'a [UnsafeReport],
979 threads: &'a [ThreadInfo],
980 passport_details: &'a [PassportDetail],
981 active_allocations: usize,
982 total_allocations: usize,
983 leak_count: usize,
984 async_tasks: &'a [AsyncTaskInfo],
985 async_summary: &'a AsyncSummary,
986 ownership_graph: &'a OwnershipGraphInfo,
987 }
988
989 let async_tasks = Self::build_async_tasks(async_tracker);
990 let async_summary = Self::build_async_summary(async_tracker);
991 let ownership_graph = Self::build_ownership_graph_info(&allocations);
992 let data = DashboardData {
993 allocations: &alloc_info,
994 relationships: &relationships,
995 unsafe_reports: &unsafe_reports,
996 threads: &thread_data,
997 passport_details: &passport_details,
998 active_allocations: analysis.active_allocations,
999 total_allocations: analysis.total_allocations,
1000 leak_count,
1001 async_tasks: &async_tasks,
1002 async_summary: &async_summary,
1003 ownership_graph: &ownership_graph,
1004 };
1005
1006 let json_data: String = serde_json::to_string(&data)
1007 .map_err(|e| format!("Failed to serialize dashboard data: {}", e))?;
1008
1009 let (
1011 os_name,
1012 os_version,
1013 architecture,
1014 cpu_cores,
1015 page_size,
1016 total_physical,
1017 available_physical,
1018 used_physical,
1019 ) = {
1020 #[cfg(target_os = "macos")]
1021 {
1022 let os_version = unsafe {
1024 let mut size: libc::size_t = 256;
1025 let mut buf = [0u8; 256];
1026 if libc::sysctlbyname(
1027 c"kern.osrelease".as_ptr(),
1028 buf.as_mut_ptr() as *mut libc::c_void,
1029 &mut size,
1030 std::ptr::null_mut(),
1031 0,
1032 ) == 0
1033 {
1034 String::from_utf8_lossy(&buf[..size - 1]).to_string()
1035 } else {
1036 "Unknown".to_string()
1037 }
1038 };
1039
1040 let architecture = unsafe {
1042 let mut size: libc::size_t = 256;
1043 let mut buf = [0u8; 256];
1044 if libc::sysctlbyname(
1045 c"hw.machine".as_ptr(),
1046 buf.as_mut_ptr() as *mut libc::c_void,
1047 &mut size,
1048 std::ptr::null_mut(),
1049 0,
1050 ) == 0
1051 {
1052 let arch_str = String::from_utf8_lossy(&buf[..size - 1]).to_string();
1053 if arch_str.contains("arm64") || arch_str.contains("arm") {
1054 "arm64".to_string()
1055 } else {
1056 arch_str
1057 }
1058 } else {
1059 "unknown".to_string()
1060 }
1061 };
1062
1063 let mut size = std::mem::size_of::<u32>();
1065 let mut cpu_cores: u32 = 1;
1066 unsafe {
1067 let mut mib: [libc::c_int; 2] = [libc::CTL_HW, libc::HW_NCPU];
1068 if libc::sysctl(
1069 mib.as_mut_ptr(),
1070 mib.len() as libc::c_uint,
1071 &mut cpu_cores as *mut u32 as *mut libc::c_void,
1072 &mut size,
1073 std::ptr::null_mut(),
1074 0,
1075 ) == 0
1076 {
1077 }
1079 }
1080
1081 let mut page_size: u64 = 4096;
1083 unsafe {
1084 size = std::mem::size_of::<u64>();
1085 if libc::sysctlbyname(
1086 c"hw.pagesize".as_ptr(),
1087 &mut page_size as *mut u64 as *mut libc::c_void,
1088 &mut size,
1089 std::ptr::null_mut(),
1090 0,
1091 ) != 0
1092 {
1093 page_size = 4096;
1094 }
1095 }
1096
1097 let mut total: u64 = 0;
1099 let mut size = std::mem::size_of::<u64>();
1100 unsafe {
1101 let mut mib: [libc::c_int; 2] = [libc::CTL_HW, libc::HW_MEMSIZE];
1102 if libc::sysctl(
1103 mib.as_mut_ptr(),
1104 mib.len() as libc::c_uint,
1105 &mut total as *mut u64 as *mut libc::c_void,
1106 &mut size,
1107 std::ptr::null_mut(),
1108 0,
1109 ) != 0
1110 {
1111 total = 16 * 1024 * 1024 * 1024; }
1113 }
1114
1115 let mut vm_stats: libc::vm_statistics64 = unsafe { std::mem::zeroed() };
1117 let mut count = libc::HOST_VM_INFO64_COUNT;
1118 let (available_physical, used_physical) = unsafe {
1119 if libc::host_statistics64(
1120 mach2::mach_init::mach_host_self(),
1121 libc::HOST_VM_INFO64,
1122 &mut vm_stats as *mut _ as libc::host_info64_t,
1123 &mut count,
1124 ) != 0
1125 {
1126 (total / 2, total / 2)
1128 } else {
1129 let free = vm_stats.free_count as u64 * page_size;
1130 let active = vm_stats.active_count as u64 * page_size;
1131 let inactive = vm_stats.inactive_count as u64 * page_size;
1132 let wired = vm_stats.wire_count as u64 * page_size;
1133 let used = active + wired;
1134 let available = free + inactive;
1135
1136 (available, used)
1137 }
1138 };
1139
1140 (
1141 "macOS".to_string(),
1142 os_version,
1143 architecture,
1144 cpu_cores,
1145 page_size,
1146 total,
1147 available_physical,
1148 used_physical,
1149 )
1150 }
1151
1152 #[cfg(not(target_os = "macos"))]
1153 {
1154 (
1155 "Unknown".to_string(),
1156 "Unknown".to_string(),
1157 "unknown".to_string(),
1158 1,
1159 4096,
1160 16_u64 * 1024 * 1024 * 1024,
1161 8_u64 * 1024 * 1024 * 1024,
1162 8_u64 * 1024 * 1024 * 1024,
1163 )
1164 }
1165 };
1166
1167 let high_risk_count = unsafe_reports
1168 .iter()
1169 .filter(|r| r.risk_level == "high")
1170 .count();
1171 let clean_passport_count = passport_details.iter().filter(|p| !p.is_leaked).count();
1172 let active_passport_count = passport_details
1173 .iter()
1174 .filter(|p| p.status == "active")
1175 .count();
1176 let leaked_passport_count = passport_details.iter().filter(|p| p.is_leaked).count();
1177 let ffi_tracked_count = passport_details.iter().filter(|p| p.ffi_tracked).count();
1178 let total_allocs = alloc_info.len().max(1);
1179 let unsafe_count = unsafe_reports.len();
1180 let leak_score = (100.0 - (leak_count as f64 / total_allocs as f64) * 100.0).max(0.0);
1181 let unsafe_score = (100.0 - (unsafe_count as f64 / total_allocs as f64) * 50.0).max(0.0);
1182 let risk_score = (100.0 - high_risk_count as f64 * HIGH_RISK_PENALTY).max(0.0);
1183 let health_score = ((leak_score + unsafe_score + risk_score) / 3.0).round() as u32;
1184 let health_status = if health_score >= 80 {
1185 "✅ Excellent"
1186 } else if health_score >= 60 {
1187 "⚠️ Good"
1188 } else {
1189 "🚨 Needs Attention"
1190 };
1191 let safe_ops_count = total_allocs.saturating_sub(unsafe_count);
1192 let safe_code_percent =
1193 ((safe_ops_count as f64 / total_allocs as f64) * 100.0).round() as u32;
1194
1195 let context = DashboardContext {
1196 title: "MemScope Dashboard".to_string(),
1197 export_timestamp: chrono::Utc::now()
1198 .format("%Y-%m-%d %H:%M:%S UTC")
1199 .to_string(),
1200 total_memory: format_bytes(total_memory),
1201 total_allocations: analysis.total_allocations,
1202 active_allocations: analysis.active_allocations,
1203 peak_memory: format_bytes(analysis.peak_memory_bytes as usize),
1204 thread_count: 1,
1205 passport_count: passports.len(),
1206 leak_count,
1207 unsafe_count: unsafe_reports.len(),
1208 ffi_count: unsafe_reports.len(),
1209 allocations: alloc_info.clone(),
1210 relationships: relationships.clone(),
1211 unsafe_reports: unsafe_reports.clone(),
1212 passport_details: passport_details.clone(),
1213 allocations_count: alloc_info.len(),
1214 relationships_count: relationships.len(),
1215 unsafe_reports_count: unsafe_reports.len(),
1216 json_data,
1217 os_name: os_name.clone(),
1218 architecture: architecture.clone(),
1219 cpu_cores: cpu_cores as usize,
1220 system_resources: SystemResources {
1221 os_name: os_name.clone(),
1222 os_version: os_version.clone(),
1223 architecture: architecture.clone(),
1224 cpu_cores,
1225 total_physical: format_bytes(total_physical as usize),
1226 available_physical: format_bytes(available_physical as usize),
1227 used_physical: format_bytes(used_physical as usize),
1228 page_size,
1229 },
1230 threads: Self::aggregate_thread_data(&alloc_info),
1231 async_tasks: Self::build_async_tasks(async_tracker),
1232 async_summary: Self::build_async_summary(async_tracker),
1233 health_score,
1234 health_status: health_status.to_string(),
1235 safe_ops_count,
1236 high_risk_count,
1237 clean_passport_count,
1238 active_passport_count,
1239 leaked_passport_count,
1240 ffi_tracked_count,
1241 safe_code_percent,
1242 ownership_graph: Self::build_ownership_graph_info(&allocations),
1243 };
1244
1245 Ok(context)
1246 }
1247
1248 fn build_ownership_graph_info(
1250 allocations: &[crate::capture::backends::core_types::AllocationInfo],
1251 ) -> OwnershipGraphInfo {
1252 use crate::analysis::ownership_graph::{
1253 DiagnosticIssue, ObjectId, OwnershipGraph, OwnershipOp,
1254 };
1255
1256 let passports: Vec<(
1258 ObjectId,
1259 String,
1260 usize,
1261 Vec<crate::analysis::ownership_graph::OwnershipEvent>,
1262 )> = allocations
1263 .iter()
1264 .map(|alloc| {
1265 let id = ObjectId::from_ptr(alloc.ptr);
1266 let type_name = alloc
1267 .type_name
1268 .clone()
1269 .unwrap_or_else(|| "unknown".to_string());
1270 let size = alloc.size;
1271
1272 let events = vec![crate::analysis::ownership_graph::OwnershipEvent::new(
1274 alloc.allocated_at_ns,
1275 OwnershipOp::Create,
1276 id,
1277 None,
1278 )];
1279
1280 (id, type_name, size, events)
1281 })
1282 .collect();
1283
1284 let graph = OwnershipGraph::build(&passports);
1285 let diagnostics = graph.diagnostics(50);
1286
1287 let issues = diagnostics
1289 .issues
1290 .iter()
1291 .map(|issue| match issue {
1292 DiagnosticIssue::RcCycle { cycle_type, .. } => OwnershipIssue {
1293 issue_type: "RcCycle".to_string(),
1294 severity: "error".to_string(),
1295 description: format!("{:?} retain cycle detected", cycle_type),
1296 },
1297 DiagnosticIssue::ArcCloneStorm {
1298 clone_count,
1299 threshold,
1300 } => OwnershipIssue {
1301 issue_type: "ArcCloneStorm".to_string(),
1302 severity: "warning".to_string(),
1303 description: format!(
1304 "Arc clone storm: {} clones (threshold: {})",
1305 clone_count, threshold
1306 ),
1307 },
1308 })
1309 .collect();
1310
1311 let root_cause = graph.find_root_cause().map(|rc| RootCauseInfo {
1313 cause: format!("{:?}", rc.root_cause),
1314 description: rc.description,
1315 impact: rc.impact,
1316 });
1317
1318 OwnershipGraphInfo {
1319 total_nodes: graph.nodes.len(),
1320 total_edges: graph.edges.len(),
1321 total_cycles: graph.cycles.len(),
1322 rc_clone_count: diagnostics.rc_clone_count,
1323 arc_clone_count: diagnostics.arc_clone_count,
1324 has_issues: diagnostics.has_issues(),
1325 issues,
1326 root_cause,
1327 }
1328 }
1329
1330 fn build_async_tasks(
1331 async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
1332 ) -> Vec<AsyncTaskInfo> {
1333 if let Some(tracker) = async_tracker {
1334 let profiles = tracker.get_all_profiles();
1335 profiles
1336 .into_iter()
1337 .map(|p| {
1338 let is_completed = p.is_completed();
1339 let has_potential_leak = p.has_potential_leak();
1340 let task_type_str = format!("{:?}", p.task_type);
1341 AsyncTaskInfo {
1342 task_id: p.task_id,
1343 task_name: p.task_name,
1344 task_type: task_type_str,
1345 total_bytes: p.total_bytes,
1346 current_memory: p.current_memory,
1347 peak_memory: p.peak_memory,
1348 total_allocations: p.total_allocations,
1349 duration_ms: p.duration_ns as f64 / 1_000_000.0,
1350 efficiency_score: p.efficiency_score,
1351 is_completed,
1352 has_potential_leak,
1353 }
1354 })
1355 .collect()
1356 } else {
1357 Vec::new()
1358 }
1359 }
1360
1361 fn build_async_summary(
1362 async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
1363 ) -> AsyncSummary {
1364 if let Some(tracker) = async_tracker {
1365 let stats = tracker.get_stats();
1366 AsyncSummary {
1367 total_tasks: stats.total_tasks,
1368 active_tasks: stats.active_tasks,
1369 total_allocations: stats.total_allocations,
1370 total_memory_bytes: stats.total_memory,
1371 peak_memory_bytes: stats.peak_memory,
1372 }
1373 } else {
1374 AsyncSummary {
1375 total_tasks: 0,
1376 active_tasks: 0,
1377 total_allocations: 0,
1378 total_memory_bytes: 0,
1379 peak_memory_bytes: 0,
1380 }
1381 }
1382 }
1383
1384 fn aggregate_thread_data(allocations: &[AllocationInfo]) -> Vec<ThreadInfo> {
1385 use std::collections::HashMap;
1386 let mut thread_map: HashMap<String, ThreadAggregator> = HashMap::new();
1387
1388 for alloc in allocations {
1389 let entry = thread_map
1390 .entry(alloc.thread_id.clone())
1391 .or_insert_with(|| ThreadAggregator {
1392 allocation_count: 0,
1393 current_memory: 0,
1394 peak_memory: 0,
1395 total_allocated: 0,
1396 });
1397 entry.allocation_count += 1;
1398 entry.current_memory += alloc.size;
1399 entry.total_allocated += alloc.size;
1400 if alloc.size > entry.peak_memory {
1401 entry.peak_memory = alloc.size;
1402 }
1403 }
1404
1405 thread_map
1406 .into_iter()
1407 .map(|(raw_tid, agg)| {
1408 let summary = format!(
1409 "{} allocs, {}",
1410 agg.allocation_count,
1411 format_bytes(agg.current_memory)
1412 );
1413 let thread_id = format_thread_id(&raw_tid);
1414 ThreadInfo {
1415 thread_id,
1416 thread_summary: summary,
1417 allocation_count: agg.allocation_count,
1418 current_memory: format_bytes(agg.current_memory),
1419 peak_memory: format_bytes(agg.peak_memory),
1420 total_allocated: format_bytes(agg.total_allocated),
1421 current_memory_bytes: agg.current_memory,
1422 peak_memory_bytes: agg.peak_memory,
1423 total_allocated_bytes: agg.total_allocated,
1424 }
1425 })
1426 .collect()
1427 }
1428
1429 pub fn render_from_tracker(
1431 &self,
1432 tracker: &Tracker,
1433 passport_tracker: &Arc<MemoryPassportTracker>,
1434 ) -> Result<String, Box<dyn std::error::Error>> {
1435 let context = self.build_context_from_tracker(tracker, passport_tracker)?;
1436 self.render_dashboard(&context)
1437 }
1438
1439 pub fn render_dashboard(
1441 &self,
1442 context: &DashboardContext,
1443 ) -> Result<String, Box<dyn std::error::Error>> {
1444 self.render_unified_dashboard(context)
1445 }
1446
1447 pub fn render_standalone_dashboard(
1449 &self,
1450 context: &DashboardContext,
1451 ) -> Result<String, Box<dyn std::error::Error>> {
1452 self.render_unified_dashboard(context)
1453 }
1454
1455 pub fn render_unified_dashboard(
1457 &self,
1458 context: &DashboardContext,
1459 ) -> Result<String, Box<dyn std::error::Error>> {
1460 let mut template_data = std::collections::BTreeMap::new();
1461 template_data.insert(
1462 "title".to_string(),
1463 serde_json::Value::String(context.title.clone()),
1464 );
1465 template_data.insert(
1466 "export_timestamp".to_string(),
1467 serde_json::Value::String(context.export_timestamp.clone()),
1468 );
1469 template_data.insert(
1470 "total_memory".to_string(),
1471 serde_json::Value::String(context.total_memory.clone()),
1472 );
1473 template_data.insert(
1474 "total_allocations".to_string(),
1475 serde_json::Value::Number(context.total_allocations.into()),
1476 );
1477 template_data.insert(
1478 "active_allocations".to_string(),
1479 serde_json::Value::Number(context.active_allocations.into()),
1480 );
1481 template_data.insert(
1482 "peak_memory".to_string(),
1483 serde_json::Value::String(context.peak_memory.clone()),
1484 );
1485 template_data.insert(
1486 "thread_count".to_string(),
1487 serde_json::Value::Number(context.thread_count.into()),
1488 );
1489 template_data.insert(
1490 "passport_count".to_string(),
1491 serde_json::Value::Number(context.passport_count.into()),
1492 );
1493 template_data.insert(
1494 "leak_count".to_string(),
1495 serde_json::Value::Number(context.leak_count.into()),
1496 );
1497 template_data.insert(
1498 "unsafe_count".to_string(),
1499 serde_json::Value::Number(context.unsafe_count.into()),
1500 );
1501 template_data.insert(
1502 "ffi_count".to_string(),
1503 serde_json::Value::Number(context.ffi_count.into()),
1504 );
1505 template_data.insert(
1506 "allocations_count".to_string(),
1507 serde_json::Value::Number(context.allocations_count.into()),
1508 );
1509 template_data.insert(
1510 "relationships_count".to_string(),
1511 serde_json::Value::Number(context.relationships_count.into()),
1512 );
1513 template_data.insert(
1514 "unsafe_reports_count".to_string(),
1515 serde_json::Value::Number(context.unsafe_reports_count.into()),
1516 );
1517 template_data.insert(
1518 "os_name".to_string(),
1519 serde_json::Value::String(context.os_name.clone()),
1520 );
1521 template_data.insert(
1522 "architecture".to_string(),
1523 serde_json::Value::String(context.architecture.clone()),
1524 );
1525 template_data.insert(
1526 "cpu_cores".to_string(),
1527 serde_json::Value::Number(context.cpu_cores.into()),
1528 );
1529 template_data.insert(
1530 "json_data".to_string(),
1531 serde_json::Value::String(context.json_data.clone()),
1532 );
1533 template_data.insert(
1534 "health_score".to_string(),
1535 serde_json::Value::Number(context.health_score.into()),
1536 );
1537 template_data.insert(
1538 "health_status".to_string(),
1539 serde_json::Value::String(context.health_status.clone()),
1540 );
1541 template_data.insert(
1542 "safe_ops_count".to_string(),
1543 serde_json::Value::Number(context.safe_ops_count.into()),
1544 );
1545 template_data.insert(
1546 "high_risk_count".to_string(),
1547 serde_json::Value::Number(context.high_risk_count.into()),
1548 );
1549 template_data.insert(
1550 "clean_passport_count".to_string(),
1551 serde_json::Value::Number(context.clean_passport_count.into()),
1552 );
1553 template_data.insert(
1554 "active_passport_count".to_string(),
1555 serde_json::Value::Number(context.active_passport_count.into()),
1556 );
1557 template_data.insert(
1558 "leaked_passport_count".to_string(),
1559 serde_json::Value::Number(context.leaked_passport_count.into()),
1560 );
1561 template_data.insert(
1562 "ffi_tracked_count".to_string(),
1563 serde_json::Value::Number(context.ffi_tracked_count.into()),
1564 );
1565 template_data.insert(
1566 "safe_code_percent".to_string(),
1567 serde_json::Value::Number(context.safe_code_percent.into()),
1568 );
1569
1570 template_data.insert(
1571 "allocations".to_string(),
1572 serde_json::to_value(&context.allocations)?,
1573 );
1574 template_data.insert(
1575 "passport_details".to_string(),
1576 serde_json::to_value(&context.passport_details)?,
1577 );
1578 template_data.insert(
1579 "relationships".to_string(),
1580 serde_json::to_value(&context.relationships)?,
1581 );
1582 template_data.insert(
1583 "unsafe_reports".to_string(),
1584 serde_json::to_value(&context.unsafe_reports)?,
1585 );
1586 template_data.insert(
1587 "threads".to_string(),
1588 serde_json::to_value(&context.threads)?,
1589 );
1590 template_data.insert(
1591 "ownership_graph".to_string(),
1592 serde_json::to_value(&context.ownership_graph)?,
1593 );
1594
1595 self.handlebars
1596 .render("dashboard_unified", &template_data)
1597 .map_err(|e| format!("Template rendering error: {}", e).into())
1598 }
1599
1600 pub fn render_final_dashboard(
1602 &self,
1603 context: &DashboardContext,
1604 ) -> Result<String, Box<dyn std::error::Error>> {
1605 let mut template_data = std::collections::BTreeMap::new();
1606 template_data.insert(
1607 "title".to_string(),
1608 serde_json::Value::String(context.title.clone()),
1609 );
1610 template_data.insert(
1611 "export_timestamp".to_string(),
1612 serde_json::Value::String(context.export_timestamp.clone()),
1613 );
1614 template_data.insert(
1615 "total_memory".to_string(),
1616 serde_json::Value::String(context.total_memory.clone()),
1617 );
1618 template_data.insert(
1619 "total_allocations".to_string(),
1620 serde_json::Value::Number(context.total_allocations.into()),
1621 );
1622 template_data.insert(
1623 "active_allocations".to_string(),
1624 serde_json::Value::Number(context.active_allocations.into()),
1625 );
1626 template_data.insert(
1627 "peak_memory".to_string(),
1628 serde_json::Value::String(context.peak_memory.clone()),
1629 );
1630 template_data.insert(
1631 "thread_count".to_string(),
1632 serde_json::Value::Number(context.thread_count.into()),
1633 );
1634 template_data.insert(
1635 "passport_count".to_string(),
1636 serde_json::Value::Number(context.passport_count.into()),
1637 );
1638 template_data.insert(
1639 "leak_count".to_string(),
1640 serde_json::Value::Number(context.leak_count.into()),
1641 );
1642 template_data.insert(
1643 "unsafe_count".to_string(),
1644 serde_json::Value::Number(context.unsafe_count.into()),
1645 );
1646 template_data.insert(
1647 "ffi_count".to_string(),
1648 serde_json::Value::Number(context.ffi_count.into()),
1649 );
1650 template_data.insert(
1651 "health_score".to_string(),
1652 serde_json::Value::Number(context.health_score.into()),
1653 );
1654 template_data.insert(
1655 "health_status".to_string(),
1656 serde_json::Value::String(context.health_status.clone()),
1657 );
1658 template_data.insert(
1659 "safe_ops_count".to_string(),
1660 serde_json::Value::Number(context.safe_ops_count.into()),
1661 );
1662 template_data.insert(
1663 "high_risk_count".to_string(),
1664 serde_json::Value::Number(context.high_risk_count.into()),
1665 );
1666 template_data.insert(
1667 "clean_passport_count".to_string(),
1668 serde_json::Value::Number(context.clean_passport_count.into()),
1669 );
1670 template_data.insert(
1671 "active_passport_count".to_string(),
1672 serde_json::Value::Number(context.active_passport_count.into()),
1673 );
1674 template_data.insert(
1675 "leaked_passport_count".to_string(),
1676 serde_json::Value::Number(context.leaked_passport_count.into()),
1677 );
1678 template_data.insert(
1679 "ffi_tracked_count".to_string(),
1680 serde_json::Value::Number(context.ffi_tracked_count.into()),
1681 );
1682 template_data.insert(
1683 "safe_code_percent".to_string(),
1684 serde_json::Value::Number(context.safe_code_percent.into()),
1685 );
1686 template_data.insert(
1687 "os_name".to_string(),
1688 serde_json::Value::String(context.os_name.clone()),
1689 );
1690 template_data.insert(
1691 "architecture".to_string(),
1692 serde_json::Value::String(context.architecture.clone()),
1693 );
1694 template_data.insert(
1695 "cpu_cores".to_string(),
1696 serde_json::Value::Number(context.cpu_cores.into()),
1697 );
1698 template_data.insert(
1699 "json_data".to_string(),
1700 serde_json::Value::String(context.json_data.clone()),
1701 );
1702 template_data.insert(
1703 "allocations".to_string(),
1704 serde_json::to_value(&context.allocations)?,
1705 );
1706 template_data.insert(
1707 "passport_details".to_string(),
1708 serde_json::to_value(&context.passport_details)?,
1709 );
1710 template_data.insert(
1711 "relationships".to_string(),
1712 serde_json::to_value(&context.relationships)?,
1713 );
1714 template_data.insert(
1715 "unsafe_reports".to_string(),
1716 serde_json::to_value(&context.unsafe_reports)?,
1717 );
1718 template_data.insert(
1719 "threads".to_string(),
1720 serde_json::to_value(&context.threads)?,
1721 );
1722 template_data.insert(
1723 "async_tasks".to_string(),
1724 serde_json::to_value(&context.async_tasks)?,
1725 );
1726 template_data.insert(
1727 "ownership_graph".to_string(),
1728 serde_json::to_value(&context.ownership_graph)?,
1729 );
1730
1731 self.handlebars
1732 .render("dashboard_final", &template_data)
1733 .map_err(|e| format!("Template rendering error: {}", e).into())
1734 }
1735
1736 pub fn render_binary_dashboard(
1738 &self,
1739 context: &DashboardContext,
1740 ) -> Result<String, Box<dyn std::error::Error>> {
1741 let legacy_data = self.to_legacy_binary_data(context);
1742 let mut template_data = std::collections::BTreeMap::new();
1743 template_data.insert("BINARY_DATA".to_string(), legacy_data);
1744 template_data.insert(
1745 "PROJECT_NAME".to_string(),
1746 serde_json::Value::String("MemScope Memory Analysis".to_string()),
1747 );
1748
1749 self.handlebars
1750 .render("binary_dashboard", &template_data)
1751 .map_err(|e| format!("Template rendering error: {}", e).into())
1752 }
1753
1754 pub fn render_clean_dashboard(
1756 &self,
1757 context: &DashboardContext,
1758 ) -> Result<String, Box<dyn std::error::Error>> {
1759 let legacy_data = self.to_legacy_binary_data(context);
1760 let mut template_data = std::collections::BTreeMap::new();
1761 template_data.insert("BINARY_DATA".to_string(), legacy_data.clone());
1762 template_data.insert("json_data".to_string(), legacy_data);
1763 template_data.insert(
1764 "PROJECT_NAME".to_string(),
1765 serde_json::Value::String("MemScope Memory Analysis".to_string()),
1766 );
1767
1768 self.handlebars
1769 .render("clean_dashboard", &template_data)
1770 .map_err(|e| format!("Template rendering error: {}", e).into())
1771 }
1772
1773 pub fn render_hybrid_dashboard(
1775 &self,
1776 context: &DashboardContext,
1777 ) -> Result<String, Box<dyn std::error::Error>> {
1778 let variables_data = serde_json::Value::Array(
1779 context
1780 .allocations
1781 .iter()
1782 .map(|a| {
1783 let mut map = serde_json::Map::new();
1784 map.insert(
1785 "var_name".to_string(),
1786 serde_json::Value::String(a.var_name.clone()),
1787 );
1788 map.insert(
1789 "type_name".to_string(),
1790 serde_json::Value::String(a.type_name.clone()),
1791 );
1792 map.insert("size".to_string(), serde_json::Value::Number(a.size.into()));
1793 map.insert(
1794 "address".to_string(),
1795 serde_json::Value::String(a.address.clone()),
1796 );
1797 map.insert(
1798 "is_leaked".to_string(),
1799 serde_json::Value::Bool(a.is_leaked),
1800 );
1801 map.insert(
1802 "timestamp_alloc".to_string(),
1803 serde_json::Value::Number(a.timestamp_alloc.into()),
1804 );
1805 map.insert(
1806 "timestamp_dealloc".to_string(),
1807 serde_json::Value::Number(a.timestamp_dealloc.into()),
1808 );
1809 map.insert(
1810 "thread_id".to_string(),
1811 serde_json::Value::String(a.thread_id.clone()),
1812 );
1813 serde_json::Value::Object(map)
1814 })
1815 .collect(),
1816 );
1817
1818 let threads_data = serde_json::Value::Array(
1819 context
1820 .threads
1821 .iter()
1822 .map(|t| {
1823 let mut map = serde_json::Map::new();
1824 map.insert(
1825 "thread_id".to_string(),
1826 serde_json::Value::String(t.thread_id.clone()),
1827 );
1828 map.insert(
1829 "allocation_count".to_string(),
1830 serde_json::Value::String(t.allocation_count.to_string()),
1831 );
1832 map.insert(
1833 "current_memory".to_string(),
1834 serde_json::Value::String(t.current_memory.clone()),
1835 );
1836 map.insert(
1837 "peak_memory".to_string(),
1838 serde_json::Value::String(t.peak_memory.clone()),
1839 );
1840 map.insert(
1841 "total_allocated".to_string(),
1842 serde_json::Value::String(t.total_allocated.clone()),
1843 );
1844 serde_json::Value::Object(map)
1845 })
1846 .collect(),
1847 );
1848
1849 let tasks_data = serde_json::Value::Array(Vec::new());
1850
1851 let total_memory: usize = context.allocations.iter().map(|a| a.size).sum();
1852 let efficiency = if context.total_allocations > 0 {
1853 (context.active_allocations as f64 / context.total_allocations as f64 * 100.0) as usize
1854 } else {
1855 100
1856 };
1857
1858 let mut template_data = std::collections::BTreeMap::new();
1859 template_data.insert("VARIABLES_DATA".to_string(), variables_data);
1860 template_data.insert("THREADS_DATA".to_string(), threads_data);
1861 template_data.insert("TASKS_DATA".to_string(), tasks_data);
1862 template_data.insert(
1863 "PROJECT_NAME".to_string(),
1864 serde_json::Value::String("MemScope Memory Analysis".to_string()),
1865 );
1866 template_data.insert(
1867 "TOTAL_MEMORY".to_string(),
1868 serde_json::Value::String(format_bytes(total_memory)),
1869 );
1870 template_data.insert(
1871 "TOTAL_VARIABLES".to_string(),
1872 serde_json::Value::Number(context.allocations.len().into()),
1873 );
1874 template_data.insert(
1875 "THREAD_COUNT".to_string(),
1876 serde_json::Value::Number(context.thread_count.into()),
1877 );
1878 template_data.insert(
1879 "EFFICIENCY".to_string(),
1880 serde_json::Value::String(format!("{}%", efficiency)),
1881 );
1882
1883 self.handlebars
1884 .render("hybrid_dashboard", &template_data)
1885 .map_err(|e| format!("Template rendering error: {}", e).into())
1886 }
1887
1888 pub fn render_performance_dashboard(
1890 &self,
1891 context: &DashboardContext,
1892 ) -> Result<String, Box<dyn std::error::Error>> {
1893 let performance_data = serde_json::json!({
1895 "allocations": context.allocations.iter().map(|a| {
1896 serde_json::json!({
1897 "timestamp": a.timestamp_alloc,
1898 "memory": a.size,
1899 "var_name": a.var_name,
1900 "type_name": a.type_name
1901 })
1902 }).collect::<Vec<_>>(),
1903 "total_memory": context.total_memory,
1904 "peak_memory": context.peak_memory,
1905 "allocations_count": context.total_allocations,
1906 "thread_count": context.thread_count
1907 });
1908
1909 let fragmentation = if context.total_allocations > 0 {
1913 let deallocated = context
1914 .total_allocations
1915 .saturating_sub(context.active_allocations);
1916 deallocated as f64 / context.total_allocations as f64 * 100.0
1917 } else {
1918 0.0
1919 };
1920
1921 let reclamation_rate = if context.total_allocations > 0 {
1924 let active_ratio = context.active_allocations as f64 / context.total_allocations as f64;
1925 (1.0 - active_ratio) * 100.0
1926 } else {
1927 100.0
1928 };
1929
1930 let efficiency_data = serde_json::json!({
1931 "memory_efficiency": if context.total_allocations > 0 {
1932 context.active_allocations as f64 / context.total_allocations as f64 * 100.0
1933 } else { 100.0 },
1934 "fragmentation": format!("{:.1}", fragmentation),
1935 "reclamation_rate": format!("{:.1}", reclamation_rate),
1936 "average_size": if context.allocations.is_empty() {
1937 0
1938 } else {
1939 context.allocations.iter().map(|a| a.size).sum::<usize>() / context.allocations.len()
1940 }
1941 });
1942
1943 let mut template_data = std::collections::BTreeMap::new();
1944 template_data.insert(
1945 "PERFORMANCE_DATA",
1946 serde_json::to_string(&performance_data)?,
1947 );
1948 template_data.insert("EFFICIENCY_DATA", serde_json::to_string(&efficiency_data)?);
1949 template_data.insert("PROJECT_NAME", "MemScope Memory Analysis".to_string());
1950
1951 self.handlebars
1952 .render("performance_dashboard", &template_data)
1953 .map_err(|e| format!("Template rendering error: {}", e).into())
1954 }
1955
1956 pub fn render_multithread_dashboard(
1958 &self,
1959 context: &DashboardContext,
1960 ) -> Result<String, Box<dyn std::error::Error>> {
1961 let threads_data = self.prepare_thread_data(context)?;
1962 let allocation_data = self.prepare_allocation_timeline_data(context)?;
1963 let conflict_data = self.prepare_conflict_data(context)?;
1964
1965 let conflict_count = conflict_data.as_array().map(|a| a.len()).unwrap_or(0);
1966 let mut template_data = std::collections::BTreeMap::new();
1967 template_data.insert("THREADS_DATA".to_string(), threads_data);
1968 template_data.insert("ALLOCATION_DATA".to_string(), allocation_data);
1969 template_data.insert("CONFLICT_DATA".to_string(), conflict_data);
1970 template_data.insert(
1971 "PROJECT_NAME".to_string(),
1972 serde_json::Value::String("MemScope Memory Analysis".to_string()),
1973 );
1974 template_data.insert(
1975 "THREAD_COUNT".to_string(),
1976 serde_json::Value::Number(context.thread_count.into()),
1977 );
1978 template_data.insert(
1979 "TOTAL_MEMORY".to_string(),
1980 serde_json::Value::String(context.total_memory.clone()),
1981 );
1982 template_data.insert(
1983 "TOTAL_ALLOCATIONS".to_string(),
1984 serde_json::Value::Number(context.total_allocations.into()),
1985 );
1986 template_data.insert(
1987 "CONFLICT_COUNT".to_string(),
1988 serde_json::Value::Number(conflict_count.into()),
1989 );
1990
1991 self.handlebars
1992 .render("multithread_template", &template_data)
1993 .map_err(|e| format!("Template rendering error: {}", e).into())
1994 }
1995
1996 fn prepare_thread_data(
1998 &self,
1999 context: &DashboardContext,
2000 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
2001 let mut thread_map: std::collections::HashMap<String, ThreadStats> =
2002 std::collections::HashMap::new();
2003
2004 for allocation in &context.allocations {
2005 let thread_id = allocation.thread_id.clone();
2006 let stats = thread_map
2007 .entry(thread_id.clone())
2008 .or_insert_with(|| ThreadStats {
2009 id: thread_id.parse::<u64>().unwrap_or(0),
2010 allocations: 0,
2011 memory: 0,
2012 peak: 0,
2013 status: "active".to_string(),
2014 });
2015
2016 stats.allocations += 1;
2017 stats.memory += allocation.size;
2018 if allocation.size > stats.peak {
2019 stats.peak = allocation.size;
2020 }
2021 }
2022
2023 let threads: Vec<ThreadStats> = thread_map.into_values().collect();
2024 serde_json::to_value(&threads).map_err(|e| e.into())
2025 }
2026
2027 fn prepare_allocation_timeline_data(
2029 &self,
2030 context: &DashboardContext,
2031 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
2032 let timeline: Vec<TimelineAllocation> = context
2033 .allocations
2034 .iter()
2035 .map(|a| TimelineAllocation {
2036 timestamp: a.timestamp_alloc,
2037 thread_id: a.thread_id.parse::<u64>().unwrap_or(0),
2038 size: a.size,
2039 var_name: Some(a.var_name.clone()),
2040 })
2041 .collect();
2042
2043 serde_json::to_value(&timeline).map_err(|e| e.into())
2044 }
2045
2046 fn prepare_conflict_data(
2048 &self,
2049 context: &DashboardContext,
2050 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
2051 let mut conflicts: Vec<ThreadConflict> = Vec::new();
2052
2053 let mut address_map: std::collections::HashMap<String, Vec<&AllocationInfo>> =
2054 std::collections::HashMap::new();
2055
2056 for allocation in &context.allocations {
2057 address_map
2058 .entry(allocation.address.clone())
2059 .or_default()
2060 .push(allocation);
2061 }
2062
2063 for (address, allocations) in &address_map {
2064 if allocations.len() > 1 {
2065 let thread_ids: Vec<u64> = allocations
2066 .iter()
2067 .map(|a| a.thread_id.parse::<u64>().unwrap_or(0))
2068 .collect();
2069 let unique_threads: std::collections::HashSet<u64> =
2070 thread_ids.iter().cloned().collect();
2071
2072 if unique_threads.len() > 1 {
2073 conflicts.push(ThreadConflict {
2074 description: format!(
2075 "Address {} accessed by {} threads",
2076 address,
2077 unique_threads.len()
2078 ),
2079 threads: thread_ids
2080 .iter()
2081 .map(|t| t.to_string())
2082 .collect::<Vec<_>>()
2083 .join(", "),
2084 conflict_type: "Data Race".to_string(),
2085 });
2086 }
2087 }
2088 }
2089
2090 serde_json::to_value(&conflicts).map_err(|e| e.into())
2091 }
2092
2093 fn build_async_base_map(
2095 context: &DashboardContext,
2096 subtitle: &str,
2097 ) -> serde_json::Map<String, serde_json::Value> {
2098 let mut map = serde_json::Map::new();
2099 map.insert(
2100 "title".to_string(),
2101 serde_json::Value::String("Async Performance Dashboard".to_string()),
2102 );
2103 map.insert(
2104 "subtitle".to_string(),
2105 serde_json::Value::String(subtitle.to_string()),
2106 );
2107 map.insert(
2108 "total_tasks".to_string(),
2109 serde_json::Value::Number(context.allocations.len().into()),
2110 );
2111 map.insert(
2112 "active_tasks".to_string(),
2113 serde_json::Value::Number(context.active_allocations.into()),
2114 );
2115 map.insert(
2116 "completed_tasks".to_string(),
2117 serde_json::Value::Number(
2118 context
2119 .allocations
2120 .iter()
2121 .filter(|a| a.timestamp_dealloc > 0)
2122 .count()
2123 .into(),
2124 ),
2125 );
2126 map.insert(
2127 "failed_tasks".to_string(),
2128 serde_json::Value::Number(0.into()),
2129 );
2130 map.insert(
2131 "cpu_usage_avg".to_string(),
2132 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2133 );
2134 map.insert(
2135 "cpu_usage_peak".to_string(),
2136 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2137 );
2138 map.insert(
2139 "cpu_cores".to_string(),
2140 serde_json::Value::Number(context.cpu_cores.into()),
2141 );
2142 map.insert(
2143 "context_switches".to_string(),
2144 serde_json::Value::Number(0.into()),
2145 );
2146 map.insert(
2147 "total_memory_mb".to_string(),
2148 serde_json::Value::Number(
2149 serde_json::Number::from_f64(Self::parse_bytes_to_mb(&context.total_memory))
2150 .unwrap(),
2151 ),
2152 );
2153 map.insert(
2154 "peak_memory_mb".to_string(),
2155 serde_json::Value::Number(
2156 serde_json::Number::from_f64(Self::parse_bytes_to_mb(&context.peak_memory))
2157 .unwrap(),
2158 ),
2159 );
2160 map.insert(
2161 "total_allocations".to_string(),
2162 serde_json::Value::Number(context.total_allocations.into()),
2163 );
2164 map.insert(
2165 "memory_efficiency".to_string(),
2166 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2167 );
2168 map.insert(
2169 "io_throughput".to_string(),
2170 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2171 );
2172 map.insert(
2173 "total_read_mb".to_string(),
2174 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2175 );
2176 map.insert(
2177 "total_write_mb".to_string(),
2178 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2179 );
2180 map.insert(
2181 "total_io_ops".to_string(),
2182 serde_json::Value::Number(0.into()),
2183 );
2184 map.insert(
2185 "network_throughput".to_string(),
2186 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2187 );
2188 map.insert(
2189 "total_sent_mb".to_string(),
2190 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2191 );
2192 map.insert(
2193 "total_received_mb".to_string(),
2194 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2195 );
2196 map.insert(
2197 "avg_latency".to_string(),
2198 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2199 );
2200 map.insert(
2201 "efficiency_score".to_string(),
2202 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2203 );
2204 map.insert(
2205 "resource_balance".to_string(),
2206 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2207 );
2208 map.insert(
2209 "bottleneck_count".to_string(),
2210 serde_json::Value::Number(0.into()),
2211 );
2212 map.insert(
2213 "optimization_potential".to_string(),
2214 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2215 );
2216 map.insert(
2217 "futures_count".to_string(),
2218 serde_json::Value::Number(0.into()),
2219 );
2220 map.insert(
2221 "total_polls".to_string(),
2222 serde_json::Value::Number(0.into()),
2223 );
2224 map.insert(
2225 "avg_poll_time".to_string(),
2226 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2227 );
2228 map.insert(
2229 "ready_rate".to_string(),
2230 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2231 );
2232 map.insert(
2233 "cpu_intensive_count".to_string(),
2234 serde_json::Value::Number(0.into()),
2235 );
2236 map.insert(
2237 "cpu_avg_efficiency".to_string(),
2238 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2239 );
2240 map.insert(
2241 "cpu_intensive_tasks".to_string(),
2242 serde_json::Value::Array(vec![]),
2243 );
2244 map.insert(
2245 "memory_intensive_count".to_string(),
2246 serde_json::Value::Number(0.into()),
2247 );
2248 map.insert(
2249 "memory_avg_efficiency".to_string(),
2250 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2251 );
2252 map.insert(
2253 "memory_intensive_tasks".to_string(),
2254 serde_json::Value::Array(vec![]),
2255 );
2256 map.insert(
2257 "io_intensive_count".to_string(),
2258 serde_json::Value::Number(0.into()),
2259 );
2260 map.insert(
2261 "io_avg_efficiency".to_string(),
2262 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2263 );
2264 map.insert(
2265 "io_intensive_tasks".to_string(),
2266 serde_json::Value::Array(vec![]),
2267 );
2268 map.insert(
2269 "network_intensive_count".to_string(),
2270 serde_json::Value::Number(0.into()),
2271 );
2272 map.insert(
2273 "network_avg_efficiency".to_string(),
2274 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2275 );
2276 map.insert(
2277 "network_intensive_tasks".to_string(),
2278 serde_json::Value::Array(vec![]),
2279 );
2280 map.insert(
2281 "executor_utilization".to_string(),
2282 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2283 );
2284 map.insert(
2285 "avg_queue_length".to_string(),
2286 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2287 );
2288 map.insert(
2289 "blocking_tasks_count".to_string(),
2290 serde_json::Value::Number(0.into()),
2291 );
2292 map.insert(
2293 "deadlock_risk".to_string(),
2294 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2295 );
2296 map.insert(
2297 "gc_pressure".to_string(),
2298 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2299 );
2300 map.insert(
2301 "avg_fragmentation".to_string(),
2302 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2303 );
2304 map.insert(
2305 "peak_alloc_rate".to_string(),
2306 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2307 );
2308 map.insert(
2309 "waker_efficiency".to_string(),
2310 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2311 );
2312 map.insert(
2313 "immediate_ready_percent".to_string(),
2314 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2315 );
2316 map
2317 }
2318
2319 fn parse_bytes_to_mb(bytes_str: &str) -> f64 {
2321 let num_str: String = bytes_str
2322 .chars()
2323 .filter(|c| c.is_ascii_digit() || *c == '.')
2324 .collect();
2325 let num: f64 = num_str.parse().unwrap_or(0.0);
2326 if bytes_str.contains("GB") {
2327 num * 1024.0
2328 } else if bytes_str.contains("MB") {
2329 num
2330 } else if bytes_str.contains("KB") {
2331 num / 1024.0
2332 } else {
2333 num / 1024.0 / 1024.0
2334 }
2335 }
2336
2337 pub fn render_async_template(
2339 &self,
2340 context: &DashboardContext,
2341 ) -> Result<String, Box<dyn std::error::Error>> {
2342 let has_async_tasks = context.allocations.iter().any(|a| {
2343 a.type_name.contains("Future")
2344 || a.type_name.contains("Task")
2345 || a.type_name.contains("async")
2346 || a.type_name.contains("Waker")
2347 });
2348
2349 let async_data = if has_async_tasks {
2350 self.prepare_async_data(context)?
2351 } else {
2352 serde_json::Value::Object(Self::build_async_base_map(
2353 context,
2354 "No async tasks detected",
2355 ))
2356 };
2357
2358 let mut template_data = std::collections::BTreeMap::new();
2359 if let serde_json::Value::Object(map) = &async_data {
2360 for (key, value) in map {
2361 template_data.insert(key.clone(), value.clone());
2362 }
2363 }
2364 template_data.insert(
2365 "PROJECT_NAME".to_string(),
2366 serde_json::Value::String("MemScope Async Performance Analysis".to_string()),
2367 );
2368
2369 self.handlebars
2370 .render("async_template", &template_data)
2371 .map_err(|e| format!("Template rendering error: {}", e).into())
2372 }
2373
2374 fn prepare_async_data(
2376 &self,
2377 context: &DashboardContext,
2378 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
2379 let mut cpu_intensive_tasks: Vec<serde_json::Value> = Vec::new();
2380 let mut memory_intensive_tasks: Vec<serde_json::Value> = Vec::new();
2381 let mut io_intensive_tasks: Vec<serde_json::Value> = Vec::new();
2382 let mut network_intensive_tasks: Vec<serde_json::Value> = Vec::new();
2383
2384 for (idx, alloc) in context.allocations.iter().enumerate() {
2385 let task_type = if alloc.type_name.contains("Future") {
2386 "future"
2387 } else if alloc.type_name.contains("Task") {
2388 "task"
2389 } else if alloc.type_name.contains("Channel") {
2390 "channel"
2391 } else {
2392 "async_op"
2393 };
2394
2395 let status = if alloc.is_leaked {
2396 "leaked"
2397 } else if alloc.timestamp_dealloc > 0 {
2398 "completed"
2399 } else {
2400 "active"
2401 };
2402 let mut task_map = serde_json::Map::new();
2403 task_map.insert("task_id".to_string(), serde_json::Value::Number(idx.into()));
2404 task_map.insert(
2405 "task_name".to_string(),
2406 serde_json::Value::String(if alloc.var_name.is_empty() {
2407 format!("async_{}", idx)
2408 } else {
2409 alloc.var_name.clone()
2410 }),
2411 );
2412 task_map.insert(
2413 "source_file".to_string(),
2414 serde_json::Value::String("unknown".to_string()),
2415 );
2416 task_map.insert(
2417 "source_line".to_string(),
2418 serde_json::Value::Number(0.into()),
2419 );
2420 task_map.insert(
2421 "task_type".to_string(),
2422 serde_json::Value::String(task_type.to_string()),
2423 );
2424 task_map.insert(
2425 "cpu_usage".to_string(),
2426 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2427 );
2428 task_map.insert(
2429 "cpu_cycles".to_string(),
2430 serde_json::Value::Number(0.into()),
2431 );
2432 task_map.insert(
2433 "instructions".to_string(),
2434 serde_json::Value::Number(0.into()),
2435 );
2436 task_map.insert(
2437 "cache_misses".to_string(),
2438 serde_json::Value::Number(0.into()),
2439 );
2440 task_map.insert(
2441 "allocated_mb".to_string(),
2442 serde_json::Value::Number(
2443 serde_json::Number::from_f64(alloc.size as f64 / 1024.0 / 1024.0).unwrap(),
2444 ),
2445 );
2446 task_map.insert(
2447 "memory_usage_percent".to_string(),
2448 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2449 );
2450 task_map.insert(
2451 "peak_memory_mb".to_string(),
2452 serde_json::Value::Number(
2453 serde_json::Number::from_f64(alloc.size as f64 / 1024.0 / 1024.0).unwrap(),
2454 ),
2455 );
2456 task_map.insert(
2457 "allocation_count".to_string(),
2458 serde_json::Value::Number(1.into()),
2459 );
2460 task_map.insert(
2461 "heap_fragmentation".to_string(),
2462 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2463 );
2464 task_map.insert(
2465 "bytes_read_mb".to_string(),
2466 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2467 );
2468 task_map.insert(
2469 "bytes_written_mb".to_string(),
2470 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2471 );
2472 task_map.insert(
2473 "avg_latency_us".to_string(),
2474 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2475 );
2476 task_map.insert(
2477 "queue_depth".to_string(),
2478 serde_json::Value::Number(0.into()),
2479 );
2480 task_map.insert(
2481 "bytes_sent_mb".to_string(),
2482 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2483 );
2484 task_map.insert(
2485 "bytes_received_mb".to_string(),
2486 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2487 );
2488 task_map.insert(
2489 "active_connections".to_string(),
2490 serde_json::Value::Number(0.into()),
2491 );
2492 task_map.insert(
2493 "avg_latency_ms".to_string(),
2494 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2495 );
2496 task_map.insert(
2497 "status".to_string(),
2498 serde_json::Value::String(status.to_string()),
2499 );
2500 task_map.insert(
2501 "duration_ms".to_string(),
2502 serde_json::Value::Number(serde_json::Number::from_f64(alloc.lifetime_ms).unwrap()),
2503 );
2504 let task_data = serde_json::Value::Object(task_map);
2505
2506 if alloc.type_name.contains("Future") || alloc.type_name.contains("Stream") {
2507 cpu_intensive_tasks.push(task_data);
2508 } else if alloc.type_name.contains("Channel") || alloc.type_name.contains("Mutex") {
2509 memory_intensive_tasks.push(task_data);
2510 } else if alloc.type_name.contains("Tcp") || alloc.type_name.contains("Udp") {
2511 network_intensive_tasks.push(task_data);
2512 } else {
2513 io_intensive_tasks.push(task_data);
2514 }
2515 }
2516
2517 let memory_efficiency = if context.total_allocations > 0 {
2518 context.active_allocations as f64 / context.total_allocations as f64 * 100.0
2519 } else {
2520 100.0
2521 };
2522
2523 let mut map = serde_json::Map::new();
2524 map.insert(
2525 "title".to_string(),
2526 serde_json::Value::String("Async Performance Dashboard".to_string()),
2527 );
2528 map.insert(
2529 "subtitle".to_string(),
2530 serde_json::Value::String("Rust Async Runtime Analysis".to_string()),
2531 );
2532 map.insert(
2533 "total_tasks".to_string(),
2534 serde_json::Value::Number(context.allocations.len().into()),
2535 );
2536 map.insert(
2537 "active_tasks".to_string(),
2538 serde_json::Value::Number(context.active_allocations.into()),
2539 );
2540 map.insert(
2541 "completed_tasks".to_string(),
2542 serde_json::Value::Number(
2543 context
2544 .allocations
2545 .iter()
2546 .filter(|a| a.timestamp_dealloc > 0)
2547 .count()
2548 .into(),
2549 ),
2550 );
2551 map.insert(
2552 "failed_tasks".to_string(),
2553 serde_json::Value::Number(context.leak_count.into()),
2554 );
2555 map.insert(
2556 "cpu_usage_avg".to_string(),
2557 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2558 );
2559 map.insert(
2560 "cpu_usage_peak".to_string(),
2561 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2562 );
2563 map.insert(
2564 "cpu_cores".to_string(),
2565 serde_json::Value::Number(context.cpu_cores.into()),
2566 );
2567 map.insert(
2568 "context_switches".to_string(),
2569 serde_json::Value::Number(0.into()),
2570 );
2571 map.insert(
2572 "total_memory_mb".to_string(),
2573 serde_json::Value::Number(
2574 serde_json::Number::from_f64(Self::parse_bytes_to_mb(&context.total_memory))
2575 .unwrap(),
2576 ),
2577 );
2578 map.insert(
2579 "peak_memory_mb".to_string(),
2580 serde_json::Value::Number(
2581 serde_json::Number::from_f64(Self::parse_bytes_to_mb(&context.peak_memory))
2582 .unwrap(),
2583 ),
2584 );
2585 map.insert(
2586 "total_allocations".to_string(),
2587 serde_json::Value::Number(context.total_allocations.into()),
2588 );
2589 map.insert(
2590 "memory_efficiency".to_string(),
2591 serde_json::Value::Number(serde_json::Number::from_f64(memory_efficiency).unwrap()),
2592 );
2593 map.insert(
2594 "io_throughput".to_string(),
2595 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2596 );
2597 map.insert(
2598 "total_read_mb".to_string(),
2599 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2600 );
2601 map.insert(
2602 "total_write_mb".to_string(),
2603 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2604 );
2605 map.insert(
2606 "total_io_ops".to_string(),
2607 serde_json::Value::Number(0.into()),
2608 );
2609 map.insert(
2610 "network_throughput".to_string(),
2611 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2612 );
2613 map.insert(
2614 "total_sent_mb".to_string(),
2615 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2616 );
2617 map.insert(
2618 "total_received_mb".to_string(),
2619 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2620 );
2621 map.insert(
2622 "avg_latency".to_string(),
2623 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2624 );
2625 map.insert(
2626 "efficiency_score".to_string(),
2627 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2628 );
2629 map.insert(
2630 "resource_balance".to_string(),
2631 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2632 );
2633 map.insert(
2634 "bottleneck_count".to_string(),
2635 serde_json::Value::Number(0.into()),
2636 );
2637 map.insert(
2638 "optimization_potential".to_string(),
2639 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2640 );
2641 map.insert(
2642 "futures_count".to_string(),
2643 serde_json::Value::Number(cpu_intensive_tasks.len().into()),
2644 );
2645 map.insert(
2646 "total_polls".to_string(),
2647 serde_json::Value::Number(0.into()),
2648 );
2649 map.insert(
2650 "avg_poll_time".to_string(),
2651 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2652 );
2653 map.insert(
2654 "ready_rate".to_string(),
2655 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2656 );
2657 map.insert(
2658 "cpu_intensive_count".to_string(),
2659 serde_json::Value::Number(cpu_intensive_tasks.len().into()),
2660 );
2661 map.insert(
2662 "cpu_avg_efficiency".to_string(),
2663 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2664 );
2665 map.insert(
2666 "cpu_intensive_tasks".to_string(),
2667 serde_json::Value::Array(cpu_intensive_tasks),
2668 );
2669 map.insert(
2670 "memory_intensive_count".to_string(),
2671 serde_json::Value::Number(memory_intensive_tasks.len().into()),
2672 );
2673 map.insert(
2674 "memory_avg_efficiency".to_string(),
2675 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2676 );
2677 map.insert(
2678 "memory_intensive_tasks".to_string(),
2679 serde_json::Value::Array(memory_intensive_tasks),
2680 );
2681 map.insert(
2682 "io_intensive_count".to_string(),
2683 serde_json::Value::Number(io_intensive_tasks.len().into()),
2684 );
2685 map.insert(
2686 "io_avg_efficiency".to_string(),
2687 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2688 );
2689 map.insert(
2690 "io_intensive_tasks".to_string(),
2691 serde_json::Value::Array(io_intensive_tasks),
2692 );
2693 map.insert(
2694 "network_intensive_count".to_string(),
2695 serde_json::Value::Number(network_intensive_tasks.len().into()),
2696 );
2697 map.insert(
2698 "network_avg_efficiency".to_string(),
2699 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2700 );
2701 map.insert(
2702 "network_intensive_tasks".to_string(),
2703 serde_json::Value::Array(network_intensive_tasks),
2704 );
2705 map.insert(
2706 "executor_utilization".to_string(),
2707 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2708 );
2709 map.insert(
2710 "avg_queue_length".to_string(),
2711 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2712 );
2713 map.insert(
2714 "blocking_tasks_count".to_string(),
2715 serde_json::Value::Number(0.into()),
2716 );
2717 map.insert(
2718 "deadlock_risk".to_string(),
2719 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2720 );
2721 map.insert(
2722 "gc_pressure".to_string(),
2723 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2724 );
2725 map.insert(
2726 "avg_fragmentation".to_string(),
2727 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2728 );
2729 map.insert(
2730 "peak_alloc_rate".to_string(),
2731 serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
2732 );
2733 map.insert(
2734 "waker_efficiency".to_string(),
2735 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2736 );
2737 map.insert(
2738 "immediate_ready_percent".to_string(),
2739 serde_json::Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
2740 );
2741 Ok(serde_json::Value::Object(map))
2742 }
2743
2744 fn to_legacy_binary_data(&self, context: &DashboardContext) -> serde_json::Value {
2746 let mut type_counts: std::collections::HashMap<String, usize> =
2748 std::collections::HashMap::new();
2749 let mut total_size: usize = 0;
2750
2751 for alloc in &context.allocations {
2752 let type_name =
2753 if alloc.type_name.contains("Vec") || alloc.type_name.contains("vec::Vec") {
2754 "dynamic_array"
2755 } else if alloc.type_name.contains("String") || alloc.type_name.contains("str") {
2756 "string"
2757 } else if alloc.type_name.contains("Box")
2758 || alloc.type_name.contains("Rc")
2759 || alloc.type_name.contains("Arc")
2760 {
2761 "smart_pointer"
2762 } else if alloc.type_name.contains("[") && alloc.type_name.contains("u8") {
2763 "byte_array"
2764 } else if alloc.size > 1024 * 1024 {
2765 "large_buffer"
2766 } else {
2767 "custom"
2768 }
2769 .to_string();
2770
2771 *type_counts.entry(type_name).or_insert(0) += 1;
2772 total_size += alloc.size;
2773 }
2774
2775 let average_size = if context.allocations.is_empty() {
2777 0
2778 } else {
2779 total_size / context.allocations.len()
2780 };
2781
2782 let lifetime_events: Vec<serde_json::Value> = context.allocations.iter().map(|a| {
2784 serde_json::json!({
2785 "address": a.address,
2786 "events": [{
2787 "context": "initial_allocation",
2788 "event_type": "Created",
2789 "timestamp": a.timestamp_alloc
2790 }],
2791 "lifetime_ms": a.lifetime_ms,
2792 "size": a.size,
2793 "timestamp_alloc": a.timestamp_alloc,
2794 "timestamp_dealloc": if a.timestamp_dealloc > 0 { Some(a.timestamp_dealloc) } else { None },
2795 "type_name": a.type_name,
2796 "var_name": a.var_name
2797 })
2798 }).collect();
2799
2800 serde_json::json!({
2801 "memory_analysis": {
2802 "allocations": context.allocations.iter().map(|a| {
2803 serde_json::json!({
2804 "var_name": a.var_name,
2805 "type_name": a.type_name,
2806 "size": a.size,
2807 "address": a.address,
2808 "timestamp": a.timestamp,
2809 "timestamp_alloc": a.timestamp_alloc,
2810 "timestamp_dealloc": if a.timestamp_dealloc > 0 { Some(a.timestamp_dealloc) } else { None },
2811 "lifetime_ms": a.lifetime_ms,
2812 "is_leaked": a.is_leaked,
2813 "thread_id": a.thread_id,
2814 "immutable_borrows": a.immutable_borrows,
2815 "mutable_borrows": a.mutable_borrows,
2816 "is_clone": a.is_clone,
2817 "clone_count": a.clone_count,
2818 "allocation_type": a.allocation_type,
2819 "is_smart_pointer": a.is_smart_pointer,
2820 "smart_pointer_type": a.smart_pointer_type,
2821 "borrow_info": {
2822 "immutable_borrows": a.immutable_borrows,
2823 "max_concurrent_borrows": a.immutable_borrows + a.mutable_borrows,
2824 "mutable_borrows": a.mutable_borrows
2825 },
2826 "clone_info": {
2827 "clone_count": a.clone_count,
2828 "is_clone": a.is_clone,
2829 "original_ptr": null
2830 },
2831 "ownership_history_available": false,
2832 "type": if a.type_name.contains("Vec") || a.type_name.contains("vec::Vec") {
2833 "dynamic_array"
2834 } else if a.type_name.contains("String") || a.type_name.contains("str") {
2835 "string"
2836 } else if a.type_name.contains("Box") || a.type_name.contains("Rc") || a.type_name.contains("Arc") {
2837 "smart_pointer"
2838 } else {
2839 "custom"
2840 }
2841 })
2842 }).collect::<Vec<_>>(),
2843 "metadata": {
2844 "export_timestamp": context.export_timestamp,
2845 "export_version": "2.0",
2846 "specification": "memscope-rs memory analysis",
2847 "total_allocations": context.allocations.len(),
2848 "total_size_bytes": total_size
2849 },
2850 "statistics": {
2851 "average_size_bytes": average_size,
2852 "total_allocations": context.allocations.len(),
2853 "total_size_bytes": total_size
2854 },
2855 "type_distribution": type_counts
2856 },
2857 "lifetime": {
2858 "metadata": {
2859 "export_timestamp": context.export_timestamp,
2860 "export_version": "2.0",
2861 "specification": "memscope-rs lifetime tracking",
2862 "total_tracked_allocations": context.allocations.len()
2863 },
2864 "ownership_histories": lifetime_events
2865 },
2866 "complex_types": {
2867 "smart_pointers": context.allocations.iter().filter(|a| a.is_smart_pointer).count(),
2868 "collections": context.allocations.iter().filter(|a| {
2869 a.type_name.contains("Vec") || a.type_name.contains("HashMap") || a.type_name.contains("BTreeMap")
2870 }).count()
2871 },
2872 "unsafe_ffi": {
2873 "passports": context.passport_details,
2874 "reports": context.unsafe_reports,
2875 "cross_boundary_events": context.unsafe_reports.iter()
2876 .flat_map(|r| r.cross_boundary_events.iter())
2877 .count()
2878 },
2879 "performance": {
2880 "total_memory": context.total_memory,
2881 "peak_memory": context.peak_memory,
2882 "total_allocations": context.total_allocations,
2883 "active_allocations": context.active_allocations,
2884 "thread_count": context.thread_count,
2885 "passport_count": context.passport_count,
2886 "leak_count": context.leak_count,
2887 "unsafe_count": context.unsafe_count,
2888 "ffi_count": context.ffi_count
2889 },
2890 "system_resources": {
2891 "os_name": context.os_name,
2892 "architecture": context.architecture,
2893 "cpu_cores": context.cpu_cores,
2894 "system_info": context.system_resources
2895 },
2896 "threads": context.threads
2897 })
2898 }
2899}
2900
2901fn format_thread_id(raw: &str) -> String {
2903 if raw.starts_with("ThreadId(") && raw.ends_with(')') {
2904 let num = &raw[9..raw.len() - 1];
2905 format!("Thread-{}", num)
2906 } else {
2907 raw.to_string()
2908 }
2909}
2910
2911fn format_bytes(bytes: usize) -> String {
2913 const KB: usize = 1024;
2914 const MB: usize = KB * 1024;
2915 const GB: usize = MB * 1024;
2916 const TB: usize = GB * 1024;
2917 const PB: usize = TB * 1024;
2918
2919 if bytes >= PB {
2920 format!("{:.2} PB", bytes as f64 / PB as f64)
2921 } else if bytes >= TB {
2922 format!("{:.2} TB", bytes as f64 / TB as f64)
2923 } else if bytes >= GB {
2924 format!("{:.2} GB", bytes as f64 / GB as f64)
2925 } else if bytes >= MB {
2926 format!("{:.2} MB", bytes as f64 / MB as f64)
2927 } else if bytes >= KB {
2928 format!("{:.2} KB", bytes as f64 / KB as f64)
2929 } else {
2930 format!("{} bytes", bytes)
2931 }
2932}
2933
2934fn format_bytes_helper(
2936 h: &handlebars::Helper,
2937 _: &handlebars::Handlebars,
2938 _: &handlebars::Context,
2939 _: &mut handlebars::RenderContext,
2940 out: &mut dyn handlebars::Output,
2941) -> handlebars::HelperResult {
2942 let param = h.param(0).unwrap().value();
2943 if let Some(bytes) = param.as_u64() {
2944 let formatted = format_bytes(bytes as usize);
2945 out.write(&formatted)?;
2946 }
2947 Ok(())
2948}
2949
2950fn greater_than_helper(
2951 h: &handlebars::Helper,
2952 _: &handlebars::Handlebars,
2953 _: &handlebars::Context,
2954 _: &mut handlebars::RenderContext,
2955 out: &mut dyn handlebars::Output,
2956) -> handlebars::HelperResult {
2957 let param1 = h.param(0).unwrap().value();
2958 let param2 = h.param(1).unwrap().value();
2959
2960 if let (Some(v1), Some(v2)) = (param1.as_u64(), param2.as_u64()) {
2961 if v1 > v2 {
2962 out.write("true")?;
2963 }
2964 }
2965 Ok(())
2966}
2967
2968fn contains_helper(
2969 h: &handlebars::Helper,
2970 _: &handlebars::Handlebars,
2971 _: &handlebars::Context,
2972 _: &mut handlebars::RenderContext,
2973 out: &mut dyn handlebars::Output,
2974) -> handlebars::HelperResult {
2975 let haystack = h.param(0).unwrap().value();
2976 let needle = h.param(1).unwrap().value();
2977
2978 if let (Some(h_str), Some(n_str)) = (haystack.as_str(), needle.as_str()) {
2979 if h_str.contains(n_str) {
2980 out.write("true")?;
2981 }
2982 }
2983 Ok(())
2984}
2985
2986fn json_helper(
2987 h: &handlebars::Helper,
2988 _: &handlebars::Handlebars,
2989 _: &handlebars::Context,
2990 _: &mut handlebars::RenderContext,
2991 out: &mut dyn handlebars::Output,
2992) -> handlebars::HelperResult {
2993 let param = h.param(0).unwrap().value();
2994 let json_string = serde_json::to_string(param).map_err(|e| {
2995 handlebars::RenderErrorReason::Other(format!("Failed to serialize to JSON: {}", e))
2996 })?;
2997 out.write(&json_string)?;
2998 Ok(())
2999}
3000
3001impl Default for DashboardRenderer {
3002 fn default() -> Self {
3003 Self::new().expect("Failed to create dashboard renderer")
3004 }
3005}