1mod 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
20pub 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
33const 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
39pub struct DashboardRenderer {
41 handlebars: Handlebars<'static>,
42 template_registry: Option<TemplateRegistry>,
44}
45
46impl DashboardRenderer {
47 pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
49 Self::with_external_templates(None)
50 }
51
52 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 let mut registry = TemplateRegistry::with_built_in_templates(&manifest_dir)?;
67
68 registry.set_external_base(external_dir.clone());
70 registry.load_external_templates(&external_dir)?;
71
72 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 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 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 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 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 obj.insert(
148 "total_smart_pointers".to_string(),
149 serde_json::Value::Number(context.circular_references.total_smart_pointers.into()),
150 );
151 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[test]
411 fn test_dashboard_renderer_default() {
412 let renderer = DashboardRenderer::default();
413 let _ = &renderer;
414 }
415
416 #[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 #[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 #[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 #[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}