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