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;
11#[allow(dead_code)]
12mod render_methods;
13mod report_builder;
14mod system_info;
15mod template_registry;
16mod types;
17
18pub use types::*;
19
20// Re-export for external use
21pub use event_dto::{build_data_index, DashboardEventDTO, DataIndex, EventSummary};
22pub use event_reconstructor::rebuild_allocations_from_events;
23pub use template_registry::{
24    DashboardTemplate as RegisteredTemplate, TemplateKind, TemplateRegistry,
25};
26
27use crate::analysis::memory_passport_tracker::MemoryPassportTracker;
28use crate::tracker::Tracker;
29use handlebars::Handlebars;
30use std::path::PathBuf;
31use std::sync::Arc;
32
33/// CDN asset scripts embedded at compile time for offline use
34const TAILWIND_SCRIPT: &str = include_str!("../templates/assets/tailwind.min.js");
35const CHART_SCRIPT: &str = include_str!("../templates/assets/chart.min.js");
36const D3_SCRIPT: &str = include_str!("../templates/assets/d3.min.js");
37const FONTS_CSS: &str = include_str!("../templates/assets/fonts.css");
38
39/// Dashboard renderer with template registry support
40pub struct DashboardRenderer {
41    handlebars: Handlebars<'static>,
42    /// Template registry for managing multiple templates
43    template_registry: Option<TemplateRegistry>,
44}
45
46impl DashboardRenderer {
47    /// Create a new dashboard renderer (built-in templates only)
48    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
49        Self::with_external_templates(None)
50    }
51
52    /// Create a new dashboard renderer with optional external template directory
53    ///
54    /// The single built-in merged dashboard template lives at
55    /// `src/render_engine/dashboard/templates/dashboard_unified.html`.
56    /// External templates (if any) are loaded from `<manifest>/templetes/<dir>/code.html`.
57    pub fn with_external_templates(
58        templetes_dir: Option<PathBuf>,
59    ) -> Result<Self, Box<dyn std::error::Error>> {
60        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
61        let external_dir = templetes_dir.unwrap_or_else(|| manifest_dir.join("templetes"));
62
63        // Build template registry from the single merged built-in template.
64        // The templates_dir argument is unused now (kept only for API stability);
65        // pass a placeholder that TemplateRegistry will ignore.
66        let mut registry = TemplateRegistry::with_built_in_templates(&manifest_dir)?;
67
68        // Always try to load external templates
69        registry.set_external_base(external_dir.clone());
70        registry.load_external_templates(&external_dir)?;
71
72        // For backward compatibility, keep a separate handlebars for old/direct template names
73        let mut handlebars = Handlebars::new();
74        helpers::register_helpers(&mut handlebars);
75
76        Ok(Self {
77            handlebars,
78            template_registry: Some(registry),
79        })
80    }
81
82    /// Render using the template registry (new API)
83    ///
84    /// Injects embedded asset scripts (tailwind, fonts, chart, d3) into the context
85    /// so templates can use `{{{tailwind_script}}}`, `{{{fonts_css}}}`, etc.
86    pub fn render_with_template(
87        &self,
88        template_id: &str,
89        context: &DashboardContext,
90    ) -> Result<String, Box<dyn std::error::Error>> {
91        let registry = self
92            .template_registry
93            .as_ref()
94            .ok_or("Template registry not initialized")?;
95
96        let mut data = serde_json::to_value(context)
97            .map_err(|e| format!("Failed to serialize context: {}", e))?;
98
99        // Inject asset scripts
100        if let Some(obj) = data.as_object_mut() {
101            obj.insert(
102                "tailwind_script".to_string(),
103                serde_json::Value::String(TAILWIND_SCRIPT.to_string()),
104            );
105            obj.insert(
106                "chart_script".to_string(),
107                serde_json::Value::String(CHART_SCRIPT.to_string()),
108            );
109            obj.insert(
110                "d3_script".to_string(),
111                serde_json::Value::String(D3_SCRIPT.to_string()),
112            );
113            obj.insert(
114                "fonts_css".to_string(),
115                serde_json::Value::String(FONTS_CSS.to_string()),
116            );
117            // Inject formatted poll latency field (not a native field on DashboardContext)
118            let poll_raw = context.poll_latency_mean_ms;
119            let poll_fmt = if poll_raw > 0.0 {
120                format!("{:.2}", poll_raw)
121            } else {
122                "—".to_string()
123            };
124            obj.insert(
125                "poll_latency_mean_ms_fmt".to_string(),
126                serde_json::Value::String(poll_fmt),
127            );
128            // Inject thread_memory_total_fmt (not a native field on DashboardContext)
129            let thread_mem_total: usize =
130                context.threads.iter().map(|t| t.current_memory_bytes).sum();
131            let thread_mem_fmt = if thread_mem_total >= 1_000_000 {
132                format!("{:.1} MB", thread_mem_total as f64 / 1_000_000.0)
133            } else if thread_mem_total >= 1_000 {
134                format!("{:.1} KB", thread_mem_total as f64 / 1_000.0)
135            } else {
136                format!("{} B", thread_mem_total)
137            };
138            obj.insert(
139                "thread_memory_total".to_string(),
140                serde_json::Value::Number(thread_mem_total.into()),
141            );
142            obj.insert(
143                "thread_memory_total_fmt".to_string(),
144                serde_json::Value::String(thread_mem_fmt),
145            );
146            // Inject total_smart_pointers (native field, but needed in template_data)
147            obj.insert(
148                "total_smart_pointers".to_string(),
149                serde_json::Value::Number(context.circular_references.total_smart_pointers.into()),
150            );
151            // Smart pointer type breakdown
152            let mut sp_breakdown: std::collections::BTreeMap<String, usize> =
153                std::collections::BTreeMap::new();
154            for alloc in &context.allocations {
155                if alloc.is_smart_pointer {
156                    *sp_breakdown
157                        .entry(alloc.smart_pointer_type.clone())
158                        .or_insert(0) += 1;
159                }
160            }
161            obj.insert(
162                "smart_pointer_breakdown".to_string(),
163                serde_json::to_value(&sp_breakdown)
164                    .unwrap_or(serde_json::Value::Object(Default::default())),
165            );
166        }
167
168        registry.render(template_id, &data)
169    }
170
171    /// List available template IDs
172    pub fn list_templates(&self) -> Vec<String> {
173        match &self.template_registry {
174            Some(reg) => reg.template_ids(),
175            None => vec!["dashboard_unified".to_string()],
176        }
177    }
178
179    /// Build dashboard context from tracker data
180    pub fn build_context_from_tracker(
181        &self,
182        tracker: &Tracker,
183        passport_tracker: &Arc<MemoryPassportTracker>,
184    ) -> Result<DashboardContext, Box<dyn std::error::Error>> {
185        self.build_context_from_tracker_with_async(tracker, passport_tracker, None)
186    }
187
188    /// Build dashboard context from tracker data with async support
189    pub fn build_context_from_tracker_with_async(
190        &self,
191        tracker: &Tracker,
192        passport_tracker: &Arc<MemoryPassportTracker>,
193        async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
194    ) -> Result<DashboardContext, Box<dyn std::error::Error>> {
195        context::build_context_from_tracker_with_async(tracker, passport_tracker, async_tracker)
196    }
197
198    /// Render dashboard from tracker data (for standalone template)
199    pub fn render_from_tracker(
200        &self,
201        tracker: &Tracker,
202        passport_tracker: &Arc<MemoryPassportTracker>,
203    ) -> Result<String, Box<dyn std::error::Error>> {
204        let context = self.build_context_from_tracker(tracker, passport_tracker)?;
205        self.render_dashboard(&context)
206    }
207
208    /// Render dashboard from context
209    pub fn render_dashboard(
210        &self,
211        context: &DashboardContext,
212    ) -> Result<String, Box<dyn std::error::Error>> {
213        self.render_unified_dashboard(context)
214    }
215
216    /// Render standalone dashboard (no external dependencies, works with file:// protocol)
217    pub fn render_standalone_dashboard(
218        &self,
219        context: &DashboardContext,
220    ) -> Result<String, Box<dyn std::error::Error>> {
221        self.render_unified_dashboard(context)
222    }
223
224    /// Render unified dashboard — uses the merged dashboard_unified template
225    pub fn render_unified_dashboard(
226        &self,
227        context: &DashboardContext,
228    ) -> Result<String, Box<dyn std::error::Error>> {
229        self.render_with_template("dashboard_unified", context)
230    }
231
232    /// Render final dashboard — now delegates to the same unified template
233    pub fn render_final_dashboard(
234        &self,
235        context: &DashboardContext,
236    ) -> Result<String, Box<dyn std::error::Error>> {
237        self.render_with_template("dashboard_unified", context)
238    }
239
240    /// Render binary dashboard (legacy template)
241    pub fn render_binary_dashboard(
242        &self,
243        context: &DashboardContext,
244    ) -> Result<String, Box<dyn std::error::Error>> {
245        render_methods::render_binary_dashboard(&self.handlebars, context)
246    }
247
248    /// Render clean dashboard (legacy template)
249    pub fn render_clean_dashboard(
250        &self,
251        context: &DashboardContext,
252    ) -> Result<String, Box<dyn std::error::Error>> {
253        render_methods::render_clean_dashboard(&self.handlebars, context)
254    }
255
256    /// Render hybrid dashboard (legacy template)
257    pub fn render_hybrid_dashboard(
258        &self,
259        context: &DashboardContext,
260    ) -> Result<String, Box<dyn std::error::Error>> {
261        render_methods::render_hybrid_dashboard(&self.handlebars, context)
262    }
263
264    /// Render performance dashboard (legacy template)
265    pub fn render_performance_dashboard(
266        &self,
267        context: &DashboardContext,
268    ) -> Result<String, Box<dyn std::error::Error>> {
269        render_methods::render_performance_dashboard(&self.handlebars, context)
270    }
271}
272
273impl Default for DashboardRenderer {
274    fn default() -> Self {
275        Self::new().expect("Failed to create dashboard renderer")
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use crate::render_engine::dashboard::renderer::types::{
283        AsyncSummary, CircularReferenceReport, DashboardContext, OwnershipGraphInfo,
284        SystemResources,
285    };
286
287    fn create_empty_context() -> DashboardContext {
288        DashboardContext {
289            title: "Test".to_string(),
290            export_timestamp: "2024-01-01".to_string(),
291            total_memory: "0 B".to_string(),
292            total_allocations: 0,
293            active_allocations: 0,
294            peak_memory: "0 B".to_string(),
295            thread_count: 0,
296            passport_count: 0,
297            leak_count: 0,
298            unsafe_count: 0,
299            ffi_count: 0,
300            allocations: vec![],
301            relationships: vec![],
302            unsafe_reports: vec![],
303            passport_details: vec![],
304            allocations_count: 0,
305            relationships_count: 0,
306            unsafe_reports_count: 0,
307            json_data: "{}".to_string(),
308            os_name: "test".to_string(),
309            architecture: "test".to_string(),
310            cpu_cores: 1,
311            system_resources: SystemResources {
312                os_name: "test".to_string(),
313                os_version: "1.0".to_string(),
314                architecture: "test".to_string(),
315                cpu_cores: 1,
316                total_physical: "0 B".to_string(),
317                available_physical: "0 B".to_string(),
318                used_physical: "0 B".to_string(),
319                page_size: 4096,
320                cpu_usage_pct: 0.0,
321                total_physical_bytes: 0,
322                used_physical_bytes: 0,
323            },
324            threads: vec![],
325            async_tasks: vec![],
326            async_summary: AsyncSummary {
327                total_tasks: 0,
328                active_tasks: 0,
329                total_allocations: 0,
330                total_memory_bytes: 0,
331                peak_memory_bytes: 0,
332                completed: 0,
333                leaked: 0,
334                zombie: 0,
335                success_rate: 0.0,
336            },
337            health_score: 100,
338            health_status: "Good".to_string(),
339            safe_ops_count: 0,
340            high_risk_count: 0,
341            clean_passport_count: 0,
342            active_passport_count: 0,
343            leaked_passport_count: 0,
344            ffi_tracked_count: 0,
345            safe_code_percent: 100,
346            ownership_graph: OwnershipGraphInfo {
347                total_nodes: 0,
348                total_edges: 0,
349                total_cycles: 0,
350                rc_clone_count: 0,
351                arc_clone_count: 0,
352                has_issues: false,
353                issues: vec![],
354                root_cause: None,
355            },
356            top_allocation_sites: vec![],
357            top_leaked_allocations: vec![],
358            top_temporary_churn: vec![],
359            circular_references: CircularReferenceReport {
360                count: 0,
361                total_leaked_memory: 0,
362                pointers_in_cycles: 0,
363                total_smart_pointers: 0,
364                has_cycles: false,
365            },
366            task_graph_json: "{}".to_string(),
367            ffi_call_topology: Default::default(),
368            symbol_table: vec![],
369            symbol_table_count: 0,
370            stack_integrity: Default::default(),
371            resource_bars: vec![],
372            thread_timeline: vec![],
373            thread_timeline_count: 0,
374            waker_efficiency_grid: vec![],
375            poll_latency_mean_ms: 0.0,
376            poll_latency_samples: vec![],
377            task_topology_nodes: vec![],
378            task_topology_nodes_count: 0,
379            task_topology_edges: vec![],
380            task_topology_edges_count: 0,
381            streaming_topology_stats: Default::default(),
382            trace_logs: vec![],
383            neighbor_density_histogram: vec![],
384            dependency_graph_nodes: vec![],
385            selected_node_detail: None,
386            thread_affinity_grid: vec![],
387            scheduler_lag_bars: vec![],
388            scheduler_lag_ms: 0,
389            migration_rate_pct: 0.0,
390            system_uptime_formatted: String::new(),
391            thread_event_log: vec![],
392            thread_policies: vec![],
393            resource_limits: vec![],
394        }
395    }
396
397    /// Objective: Verify that DashboardRenderer creates successfully.
398    /// Invariants: Renderer must be created with valid templates registered.
399    #[test]
400    fn test_dashboard_renderer_creation() {
401        let result = DashboardRenderer::new();
402        assert!(
403            result.is_ok(),
404            "DashboardRenderer should create successfully"
405        );
406    }
407
408    /// Objective: Verify that DashboardRenderer implements Default.
409    /// Invariants: Default should create a valid renderer instance.
410    #[test]
411    fn test_dashboard_renderer_default() {
412        let renderer = DashboardRenderer::default();
413        let _ = &renderer;
414    }
415
416    /// Objective: Verify that render_unified_dashboard works with minimal context.
417    /// Invariants: Should render without errors for valid context.
418    #[test]
419    fn test_render_unified_dashboard() {
420        let renderer = DashboardRenderer::new().expect("Should create renderer");
421        let context = create_empty_context();
422        let result = renderer.render_unified_dashboard(&context);
423        assert!(
424            result.is_ok(),
425            "Should render unified dashboard successfully"
426        );
427    }
428
429    /// Objective: Verify that render_final_dashboard works with minimal context.
430    /// Invariants: Should render without errors for valid context.
431    #[test]
432    fn test_render_final_dashboard() {
433        let renderer = DashboardRenderer::new().expect("Should create renderer");
434        let context = create_empty_context();
435        let result = renderer.render_final_dashboard(&context);
436        assert!(result.is_ok(), "Should render final dashboard successfully");
437    }
438
439    /// Objective: Verify that render_dashboard delegates to unified dashboard.
440    /// Invariants: Should produce same output as render_unified_dashboard.
441    #[test]
442    fn test_render_dashboard() {
443        let renderer = DashboardRenderer::new().expect("Should create renderer");
444        let context = create_empty_context();
445        let result = renderer.render_dashboard(&context);
446        assert!(result.is_ok(), "Should render dashboard successfully");
447    }
448
449    /// Objective: Verify that render_standalone_dashboard delegates correctly.
450    /// Invariants: Should produce same output as unified dashboard.
451    #[test]
452    fn test_render_standalone_dashboard() {
453        let renderer = DashboardRenderer::new().expect("Should create renderer");
454        let context = create_empty_context();
455        let result = renderer.render_standalone_dashboard(&context);
456        assert!(
457            result.is_ok(),
458            "Should render standalone dashboard successfully"
459        );
460    }
461}