mod context;
mod event_dto;
mod event_reconstructor;
mod helpers;
mod inference;
#[allow(dead_code)]
mod render_methods;
mod report_builder;
mod system_info;
mod template_registry;
mod types;
pub use types::*;
pub use event_dto::{build_data_index, DashboardEventDTO, DataIndex, EventSummary};
pub use event_reconstructor::rebuild_allocations_from_events;
pub use template_registry::{
DashboardTemplate as RegisteredTemplate, TemplateKind, TemplateRegistry,
};
use crate::analysis::memory_passport_tracker::MemoryPassportTracker;
use crate::tracker::Tracker;
use handlebars::Handlebars;
use std::path::PathBuf;
use std::sync::Arc;
const TAILWIND_SCRIPT: &str = include_str!("../templates/assets/tailwind.min.js");
const CHART_SCRIPT: &str = include_str!("../templates/assets/chart.min.js");
const D3_SCRIPT: &str = include_str!("../templates/assets/d3.min.js");
const FONTS_CSS: &str = include_str!("../templates/assets/fonts.css");
pub struct DashboardRenderer {
handlebars: Handlebars<'static>,
template_registry: Option<TemplateRegistry>,
}
impl DashboardRenderer {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
Self::with_external_templates(None)
}
pub fn with_external_templates(
templetes_dir: Option<PathBuf>,
) -> Result<Self, Box<dyn std::error::Error>> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let external_dir = templetes_dir.unwrap_or_else(|| manifest_dir.join("templetes"));
let mut registry = TemplateRegistry::with_built_in_templates(&manifest_dir)?;
registry.set_external_base(external_dir.clone());
registry.load_external_templates(&external_dir)?;
let mut handlebars = Handlebars::new();
helpers::register_helpers(&mut handlebars);
Ok(Self {
handlebars,
template_registry: Some(registry),
})
}
pub fn render_with_template(
&self,
template_id: &str,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
let registry = self
.template_registry
.as_ref()
.ok_or("Template registry not initialized")?;
let mut data = serde_json::to_value(context)
.map_err(|e| format!("Failed to serialize context: {}", e))?;
if let Some(obj) = data.as_object_mut() {
obj.insert(
"tailwind_script".to_string(),
serde_json::Value::String(TAILWIND_SCRIPT.to_string()),
);
obj.insert(
"chart_script".to_string(),
serde_json::Value::String(CHART_SCRIPT.to_string()),
);
obj.insert(
"d3_script".to_string(),
serde_json::Value::String(D3_SCRIPT.to_string()),
);
obj.insert(
"fonts_css".to_string(),
serde_json::Value::String(FONTS_CSS.to_string()),
);
let poll_raw = context.poll_latency_mean_ms;
let poll_fmt = if poll_raw > 0.0 {
format!("{:.2}", poll_raw)
} else {
"—".to_string()
};
obj.insert(
"poll_latency_mean_ms_fmt".to_string(),
serde_json::Value::String(poll_fmt),
);
let thread_mem_total: usize =
context.threads.iter().map(|t| t.current_memory_bytes).sum();
let thread_mem_fmt = if thread_mem_total >= 1_000_000 {
format!("{:.1} MB", thread_mem_total as f64 / 1_000_000.0)
} else if thread_mem_total >= 1_000 {
format!("{:.1} KB", thread_mem_total as f64 / 1_000.0)
} else {
format!("{} B", thread_mem_total)
};
obj.insert(
"thread_memory_total".to_string(),
serde_json::Value::Number(thread_mem_total.into()),
);
obj.insert(
"thread_memory_total_fmt".to_string(),
serde_json::Value::String(thread_mem_fmt),
);
obj.insert(
"total_smart_pointers".to_string(),
serde_json::Value::Number(context.circular_references.total_smart_pointers.into()),
);
let mut sp_breakdown: std::collections::BTreeMap<String, usize> =
std::collections::BTreeMap::new();
for alloc in &context.allocations {
if alloc.is_smart_pointer {
*sp_breakdown
.entry(alloc.smart_pointer_type.clone())
.or_insert(0) += 1;
}
}
obj.insert(
"smart_pointer_breakdown".to_string(),
serde_json::to_value(&sp_breakdown)
.unwrap_or(serde_json::Value::Object(Default::default())),
);
}
registry.render(template_id, &data)
}
pub fn list_templates(&self) -> Vec<String> {
match &self.template_registry {
Some(reg) => reg.template_ids(),
None => vec!["dashboard_unified".to_string()],
}
}
pub fn build_context_from_tracker(
&self,
tracker: &Tracker,
passport_tracker: &Arc<MemoryPassportTracker>,
) -> Result<DashboardContext, Box<dyn std::error::Error>> {
self.build_context_from_tracker_with_async(tracker, passport_tracker, None)
}
pub fn build_context_from_tracker_with_async(
&self,
tracker: &Tracker,
passport_tracker: &Arc<MemoryPassportTracker>,
async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
) -> Result<DashboardContext, Box<dyn std::error::Error>> {
context::build_context_from_tracker_with_async(tracker, passport_tracker, async_tracker)
}
pub fn render_from_tracker(
&self,
tracker: &Tracker,
passport_tracker: &Arc<MemoryPassportTracker>,
) -> Result<String, Box<dyn std::error::Error>> {
let context = self.build_context_from_tracker(tracker, passport_tracker)?;
self.render_dashboard(&context)
}
pub fn render_dashboard(
&self,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
self.render_unified_dashboard(context)
}
pub fn render_standalone_dashboard(
&self,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
self.render_unified_dashboard(context)
}
pub fn render_unified_dashboard(
&self,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
self.render_with_template("dashboard_unified", context)
}
pub fn render_final_dashboard(
&self,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
self.render_with_template("dashboard_unified", context)
}
pub fn render_binary_dashboard(
&self,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
render_methods::render_binary_dashboard(&self.handlebars, context)
}
pub fn render_clean_dashboard(
&self,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
render_methods::render_clean_dashboard(&self.handlebars, context)
}
pub fn render_hybrid_dashboard(
&self,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
render_methods::render_hybrid_dashboard(&self.handlebars, context)
}
pub fn render_performance_dashboard(
&self,
context: &DashboardContext,
) -> Result<String, Box<dyn std::error::Error>> {
render_methods::render_performance_dashboard(&self.handlebars, context)
}
}
impl Default for DashboardRenderer {
fn default() -> Self {
Self::new().expect("Failed to create dashboard renderer")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render_engine::dashboard::renderer::types::{
AsyncSummary, CircularReferenceReport, DashboardContext, OwnershipGraphInfo,
SystemResources,
};
fn create_empty_context() -> DashboardContext {
DashboardContext {
title: "Test".to_string(),
export_timestamp: "2024-01-01".to_string(),
total_memory: "0 B".to_string(),
total_allocations: 0,
active_allocations: 0,
peak_memory: "0 B".to_string(),
thread_count: 0,
passport_count: 0,
leak_count: 0,
unsafe_count: 0,
ffi_count: 0,
allocations: vec![],
relationships: vec![],
unsafe_reports: vec![],
passport_details: vec![],
allocations_count: 0,
relationships_count: 0,
unsafe_reports_count: 0,
json_data: "{}".to_string(),
os_name: "test".to_string(),
architecture: "test".to_string(),
cpu_cores: 1,
system_resources: SystemResources {
os_name: "test".to_string(),
os_version: "1.0".to_string(),
architecture: "test".to_string(),
cpu_cores: 1,
total_physical: "0 B".to_string(),
available_physical: "0 B".to_string(),
used_physical: "0 B".to_string(),
page_size: 4096,
cpu_usage_pct: 0.0,
total_physical_bytes: 0,
used_physical_bytes: 0,
},
threads: vec![],
async_tasks: vec![],
async_summary: AsyncSummary {
total_tasks: 0,
active_tasks: 0,
total_allocations: 0,
total_memory_bytes: 0,
peak_memory_bytes: 0,
completed: 0,
leaked: 0,
zombie: 0,
success_rate: 0.0,
},
health_score: 100,
health_status: "Good".to_string(),
safe_ops_count: 0,
high_risk_count: 0,
clean_passport_count: 0,
active_passport_count: 0,
leaked_passport_count: 0,
ffi_tracked_count: 0,
safe_code_percent: 100,
ownership_graph: OwnershipGraphInfo {
total_nodes: 0,
total_edges: 0,
total_cycles: 0,
rc_clone_count: 0,
arc_clone_count: 0,
has_issues: false,
issues: vec![],
root_cause: None,
},
top_allocation_sites: vec![],
top_leaked_allocations: vec![],
top_temporary_churn: vec![],
circular_references: CircularReferenceReport {
count: 0,
total_leaked_memory: 0,
pointers_in_cycles: 0,
total_smart_pointers: 0,
has_cycles: false,
},
task_graph_json: "{}".to_string(),
ffi_call_topology: Default::default(),
symbol_table: vec![],
symbol_table_count: 0,
stack_integrity: Default::default(),
resource_bars: vec![],
thread_timeline: vec![],
thread_timeline_count: 0,
waker_efficiency_grid: vec![],
poll_latency_mean_ms: 0.0,
poll_latency_samples: vec![],
task_topology_nodes: vec![],
task_topology_nodes_count: 0,
task_topology_edges: vec![],
task_topology_edges_count: 0,
streaming_topology_stats: Default::default(),
trace_logs: vec![],
neighbor_density_histogram: vec![],
dependency_graph_nodes: vec![],
selected_node_detail: None,
thread_affinity_grid: vec![],
scheduler_lag_bars: vec![],
scheduler_lag_ms: 0,
migration_rate_pct: 0.0,
system_uptime_formatted: String::new(),
thread_event_log: vec![],
thread_policies: vec![],
resource_limits: vec![],
}
}
#[test]
fn test_dashboard_renderer_creation() {
let result = DashboardRenderer::new();
assert!(
result.is_ok(),
"DashboardRenderer should create successfully"
);
}
#[test]
fn test_dashboard_renderer_default() {
let renderer = DashboardRenderer::default();
let _ = &renderer;
}
#[test]
fn test_render_unified_dashboard() {
let renderer = DashboardRenderer::new().expect("Should create renderer");
let context = create_empty_context();
let result = renderer.render_unified_dashboard(&context);
assert!(
result.is_ok(),
"Should render unified dashboard successfully"
);
}
#[test]
fn test_render_final_dashboard() {
let renderer = DashboardRenderer::new().expect("Should create renderer");
let context = create_empty_context();
let result = renderer.render_final_dashboard(&context);
assert!(result.is_ok(), "Should render final dashboard successfully");
}
#[test]
fn test_render_dashboard() {
let renderer = DashboardRenderer::new().expect("Should create renderer");
let context = create_empty_context();
let result = renderer.render_dashboard(&context);
assert!(result.is_ok(), "Should render dashboard successfully");
}
#[test]
fn test_render_standalone_dashboard() {
let renderer = DashboardRenderer::new().expect("Should create renderer");
let context = create_empty_context();
let result = renderer.render_standalone_dashboard(&context);
assert!(
result.is_ok(),
"Should render standalone dashboard successfully"
);
}
}