Skip to main content

memscope_rs/render_engine/dashboard/renderer/
mod.rs

1//! Dashboard renderer using Handlebars templates.
2//!
3//! This module provides the main dashboard renderer that generates
4//! HTML reports from memory tracking data.
5
6mod context;
7mod event_dto;
8mod event_reconstructor;
9mod helpers;
10mod inference;
11mod render_methods;
12mod report_builder;
13mod system_info;
14mod types;
15
16pub use types::*;
17
18// Re-export for external use
19pub use event_dto::{build_data_index, DashboardEventDTO, DataIndex, EventSummary};
20pub use event_reconstructor::rebuild_allocations_from_events;
21
22use crate::analysis::memory_passport_tracker::MemoryPassportTracker;
23use crate::tracker::Tracker;
24use handlebars::Handlebars;
25use std::sync::Arc;
26
27/// Dashboard renderer
28pub struct DashboardRenderer {
29    handlebars: Handlebars<'static>,
30}
31
32impl DashboardRenderer {
33    /// Create a new dashboard renderer
34    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
35        let mut handlebars = Handlebars::new();
36
37        let template_path = format!(
38            "{}/src/render_engine/dashboard/templates/dashboard_unified.html",
39            env!("CARGO_MANIFEST_DIR")
40        );
41        handlebars.register_template_file("dashboard_unified", &template_path)?;
42
43        let final_path = format!(
44            "{}/src/render_engine/dashboard/templates/dashboard_final.html",
45            env!("CARGO_MANIFEST_DIR")
46        );
47        handlebars.register_template_file("dashboard_final", &final_path)?;
48
49        helpers::register_helpers(&mut handlebars);
50
51        Ok(Self { handlebars })
52    }
53
54    /// Build dashboard context from tracker data
55    pub fn build_context_from_tracker(
56        &self,
57        tracker: &Tracker,
58        passport_tracker: &Arc<MemoryPassportTracker>,
59    ) -> Result<DashboardContext, Box<dyn std::error::Error>> {
60        self.build_context_from_tracker_with_async(tracker, passport_tracker, None)
61    }
62
63    /// Build dashboard context from tracker data with async support
64    pub fn build_context_from_tracker_with_async(
65        &self,
66        tracker: &Tracker,
67        passport_tracker: &Arc<MemoryPassportTracker>,
68        async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
69    ) -> Result<DashboardContext, Box<dyn std::error::Error>> {
70        context::build_context_from_tracker_with_async(tracker, passport_tracker, async_tracker)
71    }
72
73    /// Render dashboard from tracker data (for standalone template)
74    pub fn render_from_tracker(
75        &self,
76        tracker: &Tracker,
77        passport_tracker: &Arc<MemoryPassportTracker>,
78    ) -> Result<String, Box<dyn std::error::Error>> {
79        let context = self.build_context_from_tracker(tracker, passport_tracker)?;
80        self.render_dashboard(&context)
81    }
82
83    /// Render dashboard from context
84    pub fn render_dashboard(
85        &self,
86        context: &DashboardContext,
87    ) -> Result<String, Box<dyn std::error::Error>> {
88        self.render_unified_dashboard(context)
89    }
90
91    /// Render standalone dashboard (no external dependencies, works with file:// protocol)
92    pub fn render_standalone_dashboard(
93        &self,
94        context: &DashboardContext,
95    ) -> Result<String, Box<dyn std::error::Error>> {
96        self.render_unified_dashboard(context)
97    }
98
99    /// Render unified dashboard (multi-mode in single HTML)
100    pub fn render_unified_dashboard(
101        &self,
102        context: &DashboardContext,
103    ) -> Result<String, Box<dyn std::error::Error>> {
104        render_methods::render_unified_dashboard(&self.handlebars, context)
105    }
106
107    /// Render final dashboard (new investigation console template)
108    pub fn render_final_dashboard(
109        &self,
110        context: &DashboardContext,
111    ) -> Result<String, Box<dyn std::error::Error>> {
112        render_methods::render_final_dashboard(&self.handlebars, context)
113    }
114
115    /// Render binary dashboard (legacy template)
116    pub fn render_binary_dashboard(
117        &self,
118        context: &DashboardContext,
119    ) -> Result<String, Box<dyn std::error::Error>> {
120        render_methods::render_binary_dashboard(&self.handlebars, context)
121    }
122
123    /// Render clean dashboard (legacy template)
124    pub fn render_clean_dashboard(
125        &self,
126        context: &DashboardContext,
127    ) -> Result<String, Box<dyn std::error::Error>> {
128        render_methods::render_clean_dashboard(&self.handlebars, context)
129    }
130
131    /// Render hybrid dashboard (legacy template)
132    pub fn render_hybrid_dashboard(
133        &self,
134        context: &DashboardContext,
135    ) -> Result<String, Box<dyn std::error::Error>> {
136        render_methods::render_hybrid_dashboard(&self.handlebars, context)
137    }
138
139    /// Render performance dashboard (legacy template)
140    pub fn render_performance_dashboard(
141        &self,
142        context: &DashboardContext,
143    ) -> Result<String, Box<dyn std::error::Error>> {
144        render_methods::render_performance_dashboard(&self.handlebars, context)
145    }
146}
147
148impl Default for DashboardRenderer {
149    fn default() -> Self {
150        Self::new().expect("Failed to create dashboard renderer")
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::render_engine::dashboard::renderer::types::{
158        AsyncSummary, CircularReferenceReport, DashboardContext, OwnershipGraphInfo,
159        SystemResources,
160    };
161
162    fn create_empty_context() -> DashboardContext {
163        DashboardContext {
164            title: "Test".to_string(),
165            export_timestamp: "2024-01-01".to_string(),
166            total_memory: "0 B".to_string(),
167            total_allocations: 0,
168            active_allocations: 0,
169            peak_memory: "0 B".to_string(),
170            thread_count: 0,
171            passport_count: 0,
172            leak_count: 0,
173            unsafe_count: 0,
174            ffi_count: 0,
175            allocations: vec![],
176            relationships: vec![],
177            unsafe_reports: vec![],
178            passport_details: vec![],
179            allocations_count: 0,
180            relationships_count: 0,
181            unsafe_reports_count: 0,
182            json_data: "{}".to_string(),
183            os_name: "test".to_string(),
184            architecture: "test".to_string(),
185            cpu_cores: 1,
186            system_resources: SystemResources {
187                os_name: "test".to_string(),
188                os_version: "1.0".to_string(),
189                architecture: "test".to_string(),
190                cpu_cores: 1,
191                total_physical: "0 B".to_string(),
192                available_physical: "0 B".to_string(),
193                used_physical: "0 B".to_string(),
194                page_size: 4096,
195            },
196            threads: vec![],
197            async_tasks: vec![],
198            async_summary: AsyncSummary {
199                total_tasks: 0,
200                active_tasks: 0,
201                total_allocations: 0,
202                total_memory_bytes: 0,
203                peak_memory_bytes: 0,
204            },
205            health_score: 100,
206            health_status: "Good".to_string(),
207            safe_ops_count: 0,
208            high_risk_count: 0,
209            clean_passport_count: 0,
210            active_passport_count: 0,
211            leaked_passport_count: 0,
212            ffi_tracked_count: 0,
213            safe_code_percent: 100,
214            ownership_graph: OwnershipGraphInfo {
215                total_nodes: 0,
216                total_edges: 0,
217                total_cycles: 0,
218                rc_clone_count: 0,
219                arc_clone_count: 0,
220                has_issues: false,
221                issues: vec![],
222                root_cause: None,
223            },
224            top_allocation_sites: vec![],
225            top_leaked_allocations: vec![],
226            top_temporary_churn: vec![],
227            circular_references: CircularReferenceReport {
228                count: 0,
229                total_leaked_memory: 0,
230                pointers_in_cycles: 0,
231                total_smart_pointers: 0,
232                has_cycles: false,
233            },
234            task_graph_json: "{}".to_string(),
235        }
236    }
237
238    /// Objective: Verify that DashboardRenderer creates successfully.
239    /// Invariants: Renderer must be created with valid templates registered.
240    #[test]
241    fn test_dashboard_renderer_creation() {
242        let result = DashboardRenderer::new();
243        assert!(
244            result.is_ok(),
245            "DashboardRenderer should create successfully"
246        );
247    }
248
249    /// Objective: Verify that DashboardRenderer implements Default.
250    /// Invariants: Default should create a valid renderer instance.
251    #[test]
252    fn test_dashboard_renderer_default() {
253        let renderer = DashboardRenderer::default();
254        let _ = &renderer;
255    }
256
257    /// Objective: Verify that render_unified_dashboard works with minimal context.
258    /// Invariants: Should render without errors for valid context.
259    #[test]
260    fn test_render_unified_dashboard() {
261        let renderer = DashboardRenderer::new().expect("Should create renderer");
262        let context = create_empty_context();
263        let result = renderer.render_unified_dashboard(&context);
264        assert!(
265            result.is_ok(),
266            "Should render unified dashboard successfully"
267        );
268    }
269
270    /// Objective: Verify that render_final_dashboard works with minimal context.
271    /// Invariants: Should render without errors for valid context.
272    #[test]
273    fn test_render_final_dashboard() {
274        let renderer = DashboardRenderer::new().expect("Should create renderer");
275        let context = create_empty_context();
276        let result = renderer.render_final_dashboard(&context);
277        assert!(result.is_ok(), "Should render final dashboard successfully");
278    }
279
280    /// Objective: Verify that render_dashboard delegates to unified dashboard.
281    /// Invariants: Should produce same output as render_unified_dashboard.
282    #[test]
283    fn test_render_dashboard() {
284        let renderer = DashboardRenderer::new().expect("Should create renderer");
285        let context = create_empty_context();
286        let result = renderer.render_dashboard(&context);
287        assert!(result.is_ok(), "Should render dashboard successfully");
288    }
289
290    /// Objective: Verify that render_standalone_dashboard delegates correctly.
291    /// Invariants: Should produce same output as unified dashboard.
292    #[test]
293    fn test_render_standalone_dashboard() {
294        let renderer = DashboardRenderer::new().expect("Should create renderer");
295        let context = create_empty_context();
296        let result = renderer.render_standalone_dashboard(&context);
297        assert!(
298            result.is_ok(),
299            "Should render standalone dashboard successfully"
300        );
301    }
302}