Skip to main content

memscope_rs/render_engine/
export.rs

1//! Export functionality for the render engine
2//!
3//! This module provides export functionality for memory tracking data,
4//! including JSON export, lifetime analysis, and variable relationships.
5
6use tracing::{debug, warn};
7
8use crate::analysis::is_virtual_pointer;
9use crate::analysis::memory_passport_tracker::MemoryPassportTracker;
10use crate::analysis::node_id::NodeId;
11use crate::analysis::ownership_graph::{EdgeKind, OwnershipGraph, OwnershipOp};
12use crate::capture::platform::memory_info::PlatformMemoryInfo;
13use crate::core::{MemScopeError, MemScopeResult};
14use crate::render_engine::dashboard::{rebuild_allocations_from_events, DashboardRenderer};
15use crate::snapshot::{ActiveAllocation, MemorySnapshot, ThreadMemoryStats};
16use crate::tracker::Tracker;
17use rayon::prelude::*;
18use serde_json::json;
19use std::{
20    collections::HashMap,
21    fs::File,
22    io::{BufWriter, Write},
23    path::Path,
24    sync::Arc,
25};
26
27/// Optimization level for JSON export
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum OptimizationLevel {
30    /// Low optimization - maximum compatibility
31    Low,
32    /// Medium optimization - balanced
33    #[default]
34    Medium,
35    /// High optimization - maximum performance
36    High,
37    /// Maximum optimization - aggressive optimization
38    Maximum,
39}
40
41/// Schema validator for JSON export
42#[derive(Debug, Clone, Default)]
43pub struct SchemaValidator {
44    strict_mode: bool,
45}
46
47impl SchemaValidator {
48    pub fn new() -> Self {
49        Self { strict_mode: false }
50    }
51
52    pub fn with_strict_mode(mut self, strict: bool) -> Self {
53        self.strict_mode = strict;
54        self
55    }
56
57    pub fn validate(&self, data: &serde_json::Value) -> Result<(), String> {
58        if !data.is_object() {
59            return Err("Export data must be a JSON object".to_string());
60        }
61
62        let obj = data.as_object().ok_or("Invalid JSON object")?;
63
64        if self.strict_mode {
65            let required_fields = ["timestamp", "allocations", "stats"];
66            for field in &required_fields {
67                if !obj.contains_key(*field) {
68                    return Err(format!("Missing required field: {}", field));
69                }
70            }
71        }
72
73        Ok(())
74    }
75}
76
77#[derive(Debug, Clone)]
78pub struct ExportJsonOptions {
79    pub parallel_processing: bool,
80    pub buffer_size: usize,
81    pub use_compact_format: Option<bool>,
82    pub enable_type_cache: bool,
83    pub batch_size: usize,
84    pub streaming_writer: bool,
85    pub schema_validation: bool,
86    pub adaptive_optimization: bool,
87    pub max_cache_size: usize,
88    pub security_analysis: bool,
89    pub include_low_severity: bool,
90    pub integrity_hashes: bool,
91    pub fast_export_mode: bool,
92    pub auto_fast_export_threshold: Option<usize>,
93    pub thread_count: Option<usize>,
94}
95
96impl Default for ExportJsonOptions {
97    fn default() -> Self {
98        Self {
99            parallel_processing: true,
100            buffer_size: 256 * 1024,
101            use_compact_format: None,
102            enable_type_cache: true,
103            batch_size: 1000,
104            streaming_writer: true,
105            schema_validation: false,
106            adaptive_optimization: true,
107            max_cache_size: 10_000,
108            security_analysis: false,
109            include_low_severity: false,
110            integrity_hashes: false,
111            fast_export_mode: false,
112            auto_fast_export_threshold: Some(10_000),
113            thread_count: None,
114        }
115    }
116}
117
118impl ExportJsonOptions {
119    pub fn fast_export_mode(mut self, enabled: bool) -> Self {
120        self.fast_export_mode = enabled;
121        self
122    }
123
124    pub fn security_analysis(mut self, enabled: bool) -> Self {
125        self.security_analysis = enabled;
126        self
127    }
128
129    pub fn streaming_writer(mut self, enabled: bool) -> Self {
130        self.streaming_writer = enabled;
131        self
132    }
133
134    pub fn schema_validation(mut self, enabled: bool) -> Self {
135        self.schema_validation = enabled;
136        self
137    }
138
139    pub fn integrity_hashes(mut self, enabled: bool) -> Self {
140        self.integrity_hashes = enabled;
141        self
142    }
143
144    pub fn batch_size(mut self, size: usize) -> Self {
145        self.batch_size = size;
146        self
147    }
148
149    pub fn adaptive_optimization(mut self, enabled: bool) -> Self {
150        self.adaptive_optimization = enabled;
151        self
152    }
153
154    pub fn max_cache_size(mut self, size: usize) -> Self {
155        self.max_cache_size = size;
156        self
157    }
158
159    pub fn include_low_severity(mut self, include: bool) -> Self {
160        self.include_low_severity = include;
161        self
162    }
163
164    pub fn thread_count(mut self, count: Option<usize>) -> Self {
165        self.thread_count = count;
166        self
167    }
168}
169
170pub fn export_snapshot_to_json(
171    snapshot: &MemorySnapshot,
172    output_path: &Path,
173    options: &ExportJsonOptions,
174) -> Result<(), Box<dyn std::error::Error>> {
175    // Create parent directory if it doesn't exist
176    if let Some(parent) = output_path.parent() {
177        if !parent.as_os_str().is_empty() {
178            std::fs::create_dir_all(parent)?;
179        }
180    }
181
182    let allocations: Vec<&ActiveAllocation> = snapshot.active_allocations.values().collect();
183    let processed = process_allocations(&allocations, options)?;
184
185    // Use output_path as the base directory for generated files
186    let output_dir = if output_path.extension().is_some() {
187        // If output_path has an extension, treat it as a file and use its parent
188        output_path.parent().unwrap_or(Path::new("."))
189    } else {
190        output_path
191    };
192
193    generate_memory_analysis_json(output_dir, &processed, options)?;
194    generate_lifetime_json(output_dir, &processed, options)?;
195    generate_thread_analysis_json(output_dir, &snapshot.thread_stats, options)?;
196
197    Ok(())
198}
199
200fn process_allocations(
201    allocations: &[&ActiveAllocation],
202    options: &ExportJsonOptions,
203) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
204    if options.parallel_processing && allocations.len() > options.batch_size {
205        let chunk_size = (allocations.len() / num_cpus::get()).max(1);
206        Ok(allocations
207            .par_chunks(chunk_size)
208            .flat_map(process_allocation_batch)
209            .collect())
210    } else {
211        Ok(process_allocation_batch(allocations))
212    }
213}
214
215fn process_allocation_batch(allocations: &[&ActiveAllocation]) -> Vec<serde_json::Value> {
216    let current_time = std::time::SystemTime::now()
217        .duration_since(std::time::UNIX_EPOCH)
218        .map(|d| d.as_nanos() as u64)
219        .unwrap_or(0);
220
221    allocations
222        .iter()
223        .map(|alloc| {
224            let type_info = get_or_compute_type_info(
225                alloc.type_name.as_deref().unwrap_or("unknown"),
226                alloc.size,
227            );
228
229            let lifetime_ms = if alloc.allocated_at > 0 {
230                (current_time.saturating_sub(alloc.allocated_at)) / 1_000_000
231            } else {
232                0
233            };
234
235            let address = match alloc.ptr {
236                Some(ptr) => format!("0x{:x}", ptr),
237                None => "N/A".to_string(),
238            };
239
240            let mut entry = json!({
241                "address": address,
242                "size": alloc.size,
243                "type": type_info,
244                "timestamp": alloc.allocated_at,
245                "thread_id": alloc.thread_id,
246                "lifetime_ms": lifetime_ms,
247            });
248
249            if let Some(ref var_name) = alloc.var_name {
250                entry["var_name"] = serde_json::json!(var_name);
251            }
252
253            if let Some(ref type_name) = alloc.type_name {
254                entry["type_name"] = serde_json::json!(type_name);
255            }
256
257            if let Some(ref module_path) = alloc.module_path {
258                // Filter out library paths, only show user project paths
259                if !is_library_module_path(module_path) {
260                    entry["module_path"] = serde_json::json!(module_path);
261                }
262            }
263
264            entry
265        })
266        .collect()
267}
268
269/// Check if a module path belongs to a library (std, core, alloc, or external crates)
270fn is_library_module_path(module_path: &str) -> bool {
271    // Filter out standard library paths
272    if module_path.starts_with("std::")
273        || module_path.starts_with("core::")
274        || module_path.starts_with("alloc::")
275    {
276        return true;
277    }
278
279    // Filter out memscope_rs library paths
280    if module_path.starts_with("memscope_rs::") {
281        return true;
282    }
283
284    // Filter out other common library prefixes
285    let library_prefixes = [
286        "tokio::",
287        "serde::",
288        "async_trait::",
289        "futures::",
290        "log::",
291        "tracing::",
292        "chrono::",
293        "indexmap::",
294        "rustc_hash::",
295        "parking_lot::",
296        "crossbeam::",
297        "rayon::",
298        "dashmap::",
299        "ahash::",
300        "hashbrown::",
301    ];
302
303    for prefix in library_prefixes.iter() {
304        if module_path.starts_with(prefix) {
305            return true;
306        }
307    }
308
309    false
310}
311
312fn get_or_compute_type_info(type_name: &str, size: usize) -> String {
313    // Check for Vec but not VecDeque
314    if (type_name.contains("Vec<") || type_name.contains("vec::Vec<"))
315        && !type_name.contains("VecDeque")
316    {
317        "dynamic_array".to_string()
318    } else if type_name == "str"
319        || type_name == "String"
320        || type_name.contains("&str")
321        || type_name.contains("alloc::string::String")
322    {
323        "string".to_string()
324    } else if type_name.contains("Box") || type_name.contains("Rc") || type_name.contains("Arc") {
325        "smart_pointer".to_string()
326    } else if type_name.contains("[") && type_name.contains("u8") {
327        "byte_array".to_string()
328    } else if size > 1024 * 1024 {
329        "large_buffer".to_string()
330    } else {
331        "custom".to_string()
332    }
333}
334
335fn generate_memory_analysis_json<P: AsRef<Path>>(
336    output_path: P,
337    allocations: &[serde_json::Value],
338    options: &ExportJsonOptions,
339) -> Result<(), Box<dyn std::error::Error>> {
340    let total_size: usize = allocations
341        .iter()
342        .filter_map(|a| a.get("size").and_then(|s| s.as_u64()))
343        .map(|s| s as usize)
344        .sum();
345
346    let type_distribution: HashMap<String, usize> = {
347        let mut dist = HashMap::new();
348        for alloc in allocations {
349            if let Some(t) = alloc.get("type").and_then(|t| t.as_str()) {
350                *dist.entry(t.to_string()).or_insert(0) += 1;
351            }
352        }
353        dist
354    };
355
356    let data = json!({
357        "metadata": {
358            "export_version": "2.0",
359            "export_timestamp": chrono::Utc::now().to_rfc3339(),
360            "specification": "memscope-rs memory analysis",
361            "total_allocations": allocations.len(),
362            "total_size_bytes": total_size
363        },
364        "allocations": allocations,
365        "statistics": {
366            "total_allocations": allocations.len(),
367            "total_size_bytes": total_size,
368            "average_size_bytes": if allocations.is_empty() { 0 } else { total_size / allocations.len() }
369        },
370        "type_distribution": type_distribution
371    });
372
373    let path = output_path.as_ref().join("memory_analysis.json");
374    write_json_optimized(path, &data, options)?;
375    Ok(())
376}
377
378fn generate_lifetime_json<P: AsRef<Path>>(
379    output_path: P,
380    allocations: &[serde_json::Value],
381    options: &ExportJsonOptions,
382) -> Result<(), Box<dyn std::error::Error>> {
383    let ownership_histories: Vec<serde_json::Value> = allocations
384        .iter()
385        .map(|alloc| {
386            json!({
387                "address": alloc.get("address"),
388                "var_name": alloc.get("var_name"),
389                "type_name": alloc.get("type_name"),
390                "size": alloc.get("size"),
391                "timestamp_alloc": alloc.get("timestamp"),
392                "timestamp_dealloc": null,
393                "lifetime_ms": alloc.get("lifetime_ms"),
394                "events": [
395                    {
396                        "event_type": "Created",
397                        "timestamp": alloc.get("timestamp"),
398                        "context": "initial_allocation"
399                    }
400                ]
401            })
402        })
403        .collect();
404
405    let lifetime_data = json!({
406        "metadata": {
407            "export_version": "2.0",
408            "export_timestamp": chrono::Utc::now().to_rfc3339(),
409            "specification": "memscope-rs lifetime tracking",
410            "total_tracked_allocations": ownership_histories.len()
411        },
412        "ownership_histories": ownership_histories
413    });
414
415    let lifetime_path = output_path.as_ref().join("lifetime.json");
416    write_json_optimized(lifetime_path, &lifetime_data, options)?;
417    Ok(())
418}
419
420fn generate_thread_analysis_json<P: AsRef<Path>>(
421    output_path: P,
422    thread_stats: &HashMap<u64, ThreadMemoryStats>,
423    options: &ExportJsonOptions,
424) -> Result<(), Box<dyn std::error::Error>> {
425    let thread_analysis: Vec<serde_json::Value> = thread_stats
426        .values()
427        .map(|stats| {
428            json!({
429                "thread_id": stats.thread_id,
430                "allocation_count": stats.allocation_count,
431                "total_allocated": stats.total_allocated,
432                "current_memory": stats.current_memory,
433                "peak_memory": stats.peak_memory,
434            })
435        })
436        .collect();
437
438    let data = json!({
439        "metadata": {
440            "export_version": "2.0",
441            "export_timestamp": chrono::Utc::now().to_rfc3339(),
442            "specification": "thread analysis",
443            "total_threads": thread_analysis.len()
444        },
445        "thread_analysis": thread_analysis
446    });
447
448    let path = output_path.as_ref().join("thread_analysis.json");
449    write_json_optimized(path, &data, options)?;
450    Ok(())
451}
452
453fn write_json_optimized<P: AsRef<Path>>(
454    path: P,
455    data: &serde_json::Value,
456    options: &ExportJsonOptions,
457) -> Result<(), Box<dyn std::error::Error>> {
458    let path = path.as_ref();
459
460    let estimated_size = estimate_json_size(data);
461    let use_compact = options
462        .use_compact_format
463        .unwrap_or(estimated_size > 1_000_000);
464
465    if options.streaming_writer && estimated_size > 500_000 {
466        let file = File::create(path)?;
467        let mut writer = BufWriter::with_capacity(options.buffer_size, file);
468
469        if use_compact {
470            serde_json::to_writer(&mut writer, data)?;
471        } else {
472            serde_json::to_writer_pretty(&mut writer, data)?;
473        }
474
475        writer.flush()?;
476    } else {
477        let json_string = if use_compact {
478            serde_json::to_string(data)?
479        } else {
480            serde_json::to_string_pretty(data)?
481        };
482        std::fs::write(path, json_string)?;
483    }
484
485    Ok(())
486}
487
488fn estimate_json_size(data: &serde_json::Value) -> usize {
489    match data {
490        serde_json::Value::Object(map) => {
491            map.values().map(estimate_json_size).sum::<usize>() + map.len() * 20
492        }
493        serde_json::Value::Array(arr) => {
494            arr.iter().map(estimate_json_size).sum::<usize>() + arr.len() * 10
495        }
496        serde_json::Value::String(s) => s.len(),
497        serde_json::Value::Number(n) => n.to_string().len(),
498        _ => 10,
499    }
500}
501
502#[derive(Debug, thiserror::Error)]
503pub enum ExportError {
504    #[error("IO error: {0}")]
505    Io(#[from] std::io::Error),
506
507    #[error("JSON error: {0}")]
508    Json(#[from] serde_json::Error),
509
510    #[error("Export failed: {0}")]
511    ExportFailed(String),
512}
513
514pub fn export_all_json<P: AsRef<Path>>(
515    path: P,
516    tracker: &Tracker,
517    passport_tracker: &Arc<MemoryPassportTracker>,
518    async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
519) -> MemScopeResult<()> {
520    let path_ref = path.as_ref();
521
522    // Use event_store as unified data source (includes both HeapOwner and Container allocations)
523    let events = tracker.event_store().snapshot();
524    let allocations = rebuild_allocations_from_events(&events);
525    let snapshot = MemorySnapshot::from_allocation_infos(allocations.clone());
526    let options = ExportJsonOptions::default();
527
528    std::fs::create_dir_all(path_ref)
529        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
530
531    debug!("Starting export_snapshot_to_json");
532
533    export_snapshot_to_json(&snapshot, path_ref, &options)
534        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
535
536    debug!("Completed export_snapshot_to_json");
537
538    debug!("Starting export_memory_passports_json");
539
540    export_memory_passports_json(path_ref, passport_tracker)
541        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
542
543    debug!("Completed export_memory_passports_json");
544
545    debug!("Starting export_leak_detection_json");
546
547    export_leak_detection_json(path_ref, passport_tracker)
548        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
549
550    debug!("Completed export_leak_detection_json");
551
552    debug!("Starting export_unsafe_ffi_json");
553
554    export_unsafe_ffi_json(path_ref, passport_tracker)
555        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
556
557    debug!("Completed export_unsafe_ffi_json");
558
559    debug!("Starting export_system_resources_json");
560
561    export_system_resources_json(path_ref)
562        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
563
564    debug!("Completed export_system_resources_json");
565
566    debug!("Starting export_async_analysis_json");
567
568    export_async_analysis_json(path_ref, async_tracker)
569        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
570
571    debug!("Completed export_async_analysis_json");
572
573    debug!("Starting export_ownership_graph_json");
574
575    let typed_allocations: Vec<crate::capture::types::AllocationInfo> =
576        allocations.clone().into_iter().collect();
577
578    debug!(
579        allocations = typed_allocations.len(),
580        "Converted allocations to typed format"
581    );
582
583    export_ownership_graph_json(path_ref, &typed_allocations, tracker.event_store())
584        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
585
586    debug!("Completed export_ownership_graph_json");
587
588    // Export task graph JSON
589    export_task_graph_json(path_ref)
590        .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
591
592    debug!("Completed export_task_graph_json");
593
594    debug!("All exports completed successfully");
595
596    Ok(())
597}
598
599/// Export task graph JSON
600///
601/// This function exports the task graph including nodes (tasks) and edges (parent-child relationships).
602pub fn export_task_graph_json<P: AsRef<Path>>(base_path: P) -> MemScopeResult<()> {
603    use crate::task_registry::global_registry;
604
605    let base_path = base_path.as_ref();
606    let registry = global_registry();
607    let graph = registry.export_graph();
608
609    let json_string = serde_json::to_string_pretty(&graph)
610        .map_err(|e| MemScopeError::error("export", "export_task_graph_json", e.to_string()))?;
611
612    let file_path = base_path.join("task_graph.json");
613    std::fs::write(&file_path, json_string)
614        .map_err(|e| MemScopeError::error("export", "export_task_graph_json", e.to_string()))?;
615
616    tracing::info!("✅ Task graph JSON exported to: {:?}", file_path);
617
618    Ok(())
619}
620
621/// Export async task analysis to JSON
622pub fn export_async_analysis_json<P: AsRef<Path>>(
623    path: P,
624    async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
625) -> MemScopeResult<()> {
626    let path_ref = path.as_ref();
627    let stats = async_tracker.get_stats();
628    let profiles = async_tracker.get_all_profiles();
629    let snapshot = async_tracker.snapshot();
630
631    let async_data = json!({
632        "summary": {
633            "total_tasks": stats.total_tasks,
634            "active_tasks": stats.active_tasks,
635            "total_allocations": stats.total_allocations,
636            "total_memory_bytes": stats.total_memory,
637            "active_memory_bytes": stats.active_memory,
638            "peak_memory_bytes": stats.peak_memory,
639        },
640        "task_profiles": profiles.iter().map(|p| json!({
641            "task_id": p.task_id,
642            "task_name": p.task_name,
643            "task_type": format!("{:?}", p.task_type),
644            "created_at_ms": p.created_at_ms,
645            "completed_at_ms": p.completed_at_ms,
646            "total_bytes": p.total_bytes,
647            "current_memory": p.current_memory,
648            "peak_memory": p.peak_memory,
649            "total_allocations": p.total_allocations,
650            "total_deallocations": p.total_deallocations,
651            "duration_ns": p.duration_ns,
652            "allocation_rate": p.allocation_rate,
653            "efficiency_score": p.efficiency_score,
654            "average_allocation_size": p.average_allocation_size,
655            "is_completed": p.is_completed(),
656            "has_potential_leak": p.has_potential_leak(),
657        })).collect::<Vec<_>>(),
658        "allocations": snapshot.allocations.iter().map(|a| json!({
659            "ptr": format!("0x{:x}", a.ptr),
660            "size": a.size,
661            "timestamp": a.timestamp,
662            "task_id": a.task_id,
663            "var_name": a.var_name,
664            "type_name": a.type_name,
665        })).collect::<Vec<_>>(),
666    });
667
668    let async_path = path_ref.join("async_analysis.json");
669    let file = File::create(async_path)
670        .map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
671    let mut writer = BufWriter::new(file);
672    serde_json::to_writer_pretty(&mut writer, &async_data)
673        .map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
674    writer
675        .flush()
676        .map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
677
678    Ok(())
679}
680
681/// Dashboard template type — the single merged template id, plus a Custom variant
682/// for registry-based rendering of external templates.
683#[derive(Debug, Clone, PartialEq, Eq, Default)]
684pub enum DashboardTemplate {
685    /// Unified merged dashboard (default) — bundles all 8 modes in one HTML file
686    #[default]
687    Unified,
688    /// Custom template by ID (for registry-based rendering of external templates)
689    Custom(String),
690}
691
692impl std::fmt::Display for DashboardTemplate {
693    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
694        match self {
695            DashboardTemplate::Unified => write!(f, "dashboard_unified"),
696            DashboardTemplate::Custom(id) => write!(f, "{}", id),
697        }
698    }
699}
700
701impl DashboardTemplate {
702    /// Get the template ID used by the Handlebars registry
703    pub fn template_id(&self) -> &str {
704        match self {
705            DashboardTemplate::Unified => "dashboard_unified",
706            DashboardTemplate::Custom(id) => id.as_str(),
707        }
708    }
709
710    /// Get a human-readable name for this template
711    pub fn name(&self) -> &str {
712        match self {
713            DashboardTemplate::Unified => "Unified Dashboard",
714            DashboardTemplate::Custom(_) => "Custom Template",
715        }
716    }
717}
718
719/// Export HTML dashboard from tracker data
720///
721/// This function generates a complete HTML dashboard from the tracker data,
722/// including memory analysis, variable relationships, unsafe/FFI tracking,
723/// and system resources. The dashboard is rendered using Handlebars templates.
724pub fn export_dashboard_html<P: AsRef<Path>>(
725    path: P,
726    tracker: &Tracker,
727    passport_tracker: &Arc<MemoryPassportTracker>,
728) -> MemScopeResult<()> {
729    export_dashboard_html_with_template(
730        path,
731        tracker,
732        passport_tracker,
733        DashboardTemplate::default(),
734        None,
735    )
736}
737
738/// Export HTML dashboard with async tracker support
739pub fn export_dashboard_html_with_async<P: AsRef<Path>>(
740    path: P,
741    tracker: &Tracker,
742    passport_tracker: &Arc<MemoryPassportTracker>,
743    async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
744) -> MemScopeResult<()> {
745    export_dashboard_html_with_template(
746        path,
747        tracker,
748        passport_tracker,
749        DashboardTemplate::default(),
750        Some(async_tracker),
751    )
752}
753
754/// Export HTML dashboard with specific template
755///
756/// This function generates a complete HTML dashboard from the tracker data
757/// using the specified template type.
758pub fn export_dashboard_html_with_template<P: AsRef<Path>>(
759    path: P,
760    tracker: &Tracker,
761    passport_tracker: &Arc<MemoryPassportTracker>,
762    template: DashboardTemplate,
763    async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
764) -> MemScopeResult<()> {
765    let path_ref = path.as_ref();
766
767    // Create output directory if it doesn't exist
768    std::fs::create_dir_all(path_ref).map_err(|e| {
769        MemScopeError::error(
770            "export",
771            "export_dashboard_html_with_template",
772            format!("Failed to create output directory: {}", e),
773        )
774    })?;
775
776    // Create dashboard renderer
777    let renderer = DashboardRenderer::new().map_err(|e| {
778        MemScopeError::error(
779            "export",
780            "export_dashboard_html_with_template",
781            format!("Failed to create dashboard renderer: {}", e),
782        )
783    })?;
784
785    // Render HTML from tracker data using selected template
786    let context = renderer
787        .build_context_from_tracker_with_async(tracker, passport_tracker, async_tracker)
788        .map_err(|e| {
789            MemScopeError::error(
790                "export",
791                "export_dashboard_html_with_template",
792                format!("Failed to build context: {}", e),
793            )
794        })?;
795
796    let html_content = match template {
797        // The unified template and any custom external template are rendered via the registry
798        DashboardTemplate::Unified | DashboardTemplate::Custom(_) => {
799            let template_id = template.template_id();
800            renderer
801                .render_with_template(template_id, &context)
802                .map_err(|e| {
803                    MemScopeError::error(
804                        "export",
805                        "export_dashboard_html_with_template",
806                        format!("Failed to render {} template: {}", template.name(), e),
807                    )
808                })?
809        }
810    };
811
812    // Write HTML to file
813    let output_file = path_ref.join(format!("{}_dashboard.html", template));
814    std::fs::write(&output_file, html_content).map_err(|e| {
815        MemScopeError::error(
816            "export",
817            "export_dashboard_html_with_template",
818            format!("Failed to write HTML file: {}", e),
819        )
820    })?;
821
822    tracing::info!("✅ Dashboard HTML exported to: {:?}", output_file);
823
824    Ok(())
825}
826
827pub fn export_memory_passports_json<P: AsRef<Path>>(
828    base_path: P,
829    passport_tracker: &Arc<MemoryPassportTracker>,
830) -> MemScopeResult<()> {
831    let base_path = base_path.as_ref();
832    let passports = passport_tracker.get_all_passports();
833
834    let passport_data: Vec<_> = passports
835        .values()
836        .map(|p| {
837            serde_json::json!({
838                "passport_id": p.passport_id,
839                "allocation_ptr": format!("0x{:x}", p.allocation_ptr),
840                "size_bytes": p.size_bytes,
841                "created_at": p.created_at,
842                "lifecycle_events": p.lifecycle_events.len(),
843                "status": format!("{:?}", p.status_at_shutdown),
844            })
845        })
846        .collect();
847
848    let json_data = serde_json::json!({
849        "metadata": {
850            "export_version": "2.0",
851            "specification": "memory passport tracking",
852            "total_passports": passports.len()
853        },
854        "memory_passports": passport_data,
855    });
856
857    let file_path = base_path.join("memory_passports.json");
858    let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
859        MemScopeError::error("export", "export_memory_passports_json", e.to_string())
860    })?;
861    std::fs::write(&file_path, json_string).map_err(|e| {
862        MemScopeError::error("export", "export_memory_passports_json", e.to_string())
863    })?;
864
865    Ok(())
866}
867
868pub fn export_leak_detection_json<P: AsRef<Path>>(
869    base_path: P,
870    passport_tracker: &Arc<MemoryPassportTracker>,
871) -> MemScopeResult<()> {
872    let base_path = base_path.as_ref();
873    let leak_result = passport_tracker.detect_leaks_at_shutdown();
874
875    let leak_details: Vec<_> = leak_result
876        .leak_details
877        .iter()
878        .map(|detail| {
879            serde_json::json!({
880                "passport_id": detail.passport_id,
881                "memory_address": format!("0x{:x}", detail.memory_address),
882                "size_bytes": detail.size_bytes,
883                "lifecycle_summary": detail.lifecycle_summary,
884            })
885        })
886        .collect();
887
888    let json_data = serde_json::json!({
889        "metadata": {
890            "export_version": "2.0",
891            "specification": "leak detection",
892            "leaks_detected": leak_result.total_leaks
893        },
894        "leak_detection": {
895            "total_leaks": leak_result.total_leaks,
896            "leak_details": leak_details
897        }
898    });
899
900    let file_path = base_path.join("leak_detection.json");
901    let json_string = serde_json::to_string_pretty(&json_data)
902        .map_err(|e| MemScopeError::error("export", "export_leak_detection_json", e.to_string()))?;
903    std::fs::write(&file_path, json_string)
904        .map_err(|e| MemScopeError::error("export", "export_leak_detection_json", e.to_string()))?;
905
906    Ok(())
907}
908
909pub fn export_unsafe_ffi_json<P: AsRef<Path>>(
910    base_path: P,
911    passport_tracker: &Arc<MemoryPassportTracker>,
912) -> MemScopeResult<()> {
913    use crate::analysis::memory_passport_tracker::PassportStatus;
914
915    let base_path = base_path.as_ref();
916    let passports = passport_tracker.get_all_passports();
917
918    let ffi_reports: Vec<_> = passports
919        .values()
920        .filter(|p| {
921            matches!(
922                p.status_at_shutdown,
923                PassportStatus::HandoverToFfi
924                    | PassportStatus::InForeignCustody
925                    | PassportStatus::FreedByForeign
926            )
927        })
928        .map(|p| {
929            serde_json::json!({
930                "passport_id": p.passport_id,
931                "allocation_ptr": format!("0x{:x}", p.allocation_ptr),
932                "size_bytes": p.size_bytes,
933                "status": format!("{:?}", p.status_at_shutdown),
934                "created_at": p.created_at,
935                "boundary_events": p.lifecycle_events.iter().map(|e| {
936                    serde_json::json!({
937                        "timestamp": e.timestamp,
938                        "event_type": format!("{:?}", e.event_type),
939                        "context": e.context,
940                    })
941                }).collect::<Vec<_>>(),
942            })
943        })
944        .collect();
945
946    let json_data = serde_json::json!({
947        "metadata": {
948            "export_version": "2.0",
949            "specification": "unsafe FFI tracking",
950            "total_ffi_reports": ffi_reports.len(),
951            "total_memory_passports": passports.len()
952        },
953        "unsafe_reports": ffi_reports,
954        "memory_passports": passports.len()
955    });
956
957    let file_path = base_path.join("unsafe_ffi.json");
958    let json_string = serde_json::to_string_pretty(&json_data)
959        .map_err(|e| MemScopeError::error("export", "export_unsafe_ffi_json", e.to_string()))?;
960    std::fs::write(&file_path, json_string)
961        .map_err(|e| MemScopeError::error("export", "export_unsafe_ffi_json", e.to_string()))?;
962
963    Ok(())
964}
965
966/// Export system resource monitoring data to JSON
967///
968/// This function exports comprehensive system resource statistics including:
969/// - Memory statistics (virtual, physical, process, system)
970/// - CPU information (cores, cache, system info)
971/// - Memory pressure indicators
972pub fn export_system_resources_json<P: AsRef<Path>>(base_path: P) -> MemScopeResult<()> {
973    let base_path = base_path.as_ref();
974
975    // Collect memory statistics
976    let mut memory_info = PlatformMemoryInfo::new();
977    let _ = memory_info.initialize();
978
979    let memory_stats = match memory_info.collect_stats() {
980        Ok(stats) => stats,
981        Err(e) => {
982            warn!(error = %e, "Failed to collect memory stats");
983            return Err(MemScopeError::error(
984                "export",
985                "export_system_resources_json",
986                e.to_string(),
987            ));
988        }
989    };
990
991    // Collect system information
992    let system_info = match memory_info.get_system_info() {
993        Ok(info) => info,
994        Err(e) => {
995            warn!(error = %e, "Failed to collect system info");
996            return Err(MemScopeError::error(
997                "export",
998                "export_system_resources_json",
999                e.to_string(),
1000            ));
1001        }
1002    };
1003
1004    // Build JSON structure
1005    let json_data = serde_json::json!({
1006        "metadata": {
1007            "export_version": "2.0",
1008            "specification": "system resource monitoring",
1009            "timestamp": std::time::SystemTime::now()
1010                .duration_since(std::time::UNIX_EPOCH)
1011                .unwrap_or_default()
1012                .as_secs()
1013        },
1014        "system_info": {
1015            "os_name": system_info.os_name,
1016            "os_version": system_info.os_version,
1017            "architecture": system_info.architecture,
1018            "cpu_cores": system_info.cpu_cores,
1019            "page_size": system_info.page_size,
1020            "large_page_size": system_info.large_page_size,
1021            "cpu_cache": {
1022                "l1_cache_size": system_info.cpu_cache.l1_cache_size,
1023                "l2_cache_size": system_info.cpu_cache.l2_cache_size,
1024                "l3_cache_size": system_info.cpu_cache.l3_cache_size,
1025                "cache_line_size": system_info.cpu_cache.cache_line_size
1026            },
1027            "mmu_info": {
1028                "virtual_address_bits": system_info.mmu_info.virtual_address_bits,
1029                "physical_address_bits": system_info.mmu_info.physical_address_bits,
1030                "aslr_enabled": system_info.mmu_info.aslr_enabled,
1031                "nx_bit_supported": system_info.mmu_info.nx_bit_supported
1032            }
1033        },
1034        "memory_stats": {
1035            "virtual_memory": {
1036                "total_virtual": memory_stats.virtual_memory.total_virtual,
1037                "available_virtual": memory_stats.virtual_memory.available_virtual,
1038                "used_virtual": memory_stats.virtual_memory.used_virtual,
1039                "reserved": memory_stats.virtual_memory.reserved,
1040                "committed": memory_stats.virtual_memory.committed
1041            },
1042            "physical_memory": {
1043                "total_physical": memory_stats.physical_memory.total_physical,
1044                "available_physical": memory_stats.physical_memory.available_physical,
1045                "used_physical": memory_stats.physical_memory.used_physical,
1046                "cached": memory_stats.physical_memory.cached,
1047                "buffers": memory_stats.physical_memory.buffers,
1048                "swap": {
1049                    "total_swap": memory_stats.physical_memory.swap.total_swap,
1050                    "used_swap": memory_stats.physical_memory.swap.used_swap,
1051                    "available_swap": memory_stats.physical_memory.swap.available_swap,
1052                    "swap_in_rate": memory_stats.physical_memory.swap.swap_in_rate,
1053                    "swap_out_rate": memory_stats.physical_memory.swap.swap_out_rate
1054                }
1055            },
1056            "process_memory": {
1057                "virtual_size": memory_stats.process_memory.virtual_size,
1058                "resident_size": memory_stats.process_memory.resident_size,
1059                "shared_size": memory_stats.process_memory.shared_size,
1060                "private_size": memory_stats.process_memory.private_size,
1061                "heap_size": memory_stats.process_memory.heap_size,
1062                "stack_size": memory_stats.process_memory.stack_size,
1063                "mapped_files": memory_stats.process_memory.mapped_files,
1064                "peak_usage": memory_stats.process_memory.peak_usage
1065            },
1066            "system_memory": {
1067                "allocation_count": memory_stats.system_memory.allocation_count,
1068                "deallocation_count": memory_stats.system_memory.deallocation_count,
1069                "active_allocations": memory_stats.system_memory.active_allocations,
1070                "total_allocated": memory_stats.system_memory.total_allocated,
1071                "total_deallocated": memory_stats.system_memory.total_deallocated,
1072                "fragmentation_level": memory_stats.system_memory.fragmentation_level,
1073                "large_pages": {
1074                    "supported": memory_stats.system_memory.large_pages.supported,
1075                    "total_large_pages": memory_stats.system_memory.large_pages.total_large_pages,
1076                    "used_large_pages": memory_stats.system_memory.large_pages.used_large_pages,
1077                    "page_size": memory_stats.system_memory.large_pages.page_size
1078                }
1079            },
1080            "pressure_indicators": {
1081                "pressure_level": format!("{:?}", memory_stats.pressure_indicators.pressure_level),
1082                "low_memory": memory_stats.pressure_indicators.low_memory,
1083                "swapping_active": memory_stats.pressure_indicators.swapping_active,
1084                "allocation_failure_rate": memory_stats.pressure_indicators.allocation_failure_rate,
1085                "gc_pressure": memory_stats.pressure_indicators.gc_pressure
1086            }
1087        }
1088    });
1089
1090    let file_path = base_path.join("system_resources.json");
1091    let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
1092        MemScopeError::error("export", "export_system_resources_json", e.to_string())
1093    })?;
1094    std::fs::write(&file_path, json_string).map_err(|e| {
1095        MemScopeError::error("export", "export_system_resources_json", e.to_string())
1096    })?;
1097
1098    Ok(())
1099}
1100
1101/// Export ownership graph analysis to JSON
1102///
1103/// This function exports the ownership graph including:
1104/// - Node information (objects with their types and sizes)
1105/// - Edge information (clone relationships)
1106/// - Detected cycles (Rc/Arc retain cycles)
1107/// - Diagnostics (clone storms, cycle warnings)
1108pub fn export_ownership_graph_json<P: AsRef<Path>>(
1109    base_path: P,
1110    allocations: &[crate::capture::types::AllocationInfo],
1111    event_store: &crate::event_store::EventStore,
1112) -> MemScopeResult<()> {
1113    let base_path = base_path.as_ref();
1114
1115    // Build ownership graph from allocations
1116    let graph = build_ownership_graph_from_allocations(allocations, event_store);
1117
1118    // Get diagnostics
1119    let diagnostics = graph.diagnostics(50);
1120
1121    // Get borrow history from global BorrowAnalyzer for integration
1122    let borrow_analyzer = crate::analysis::borrow_analysis::get_global_borrow_analyzer();
1123    let borrow_history = borrow_analyzer.get_borrow_history();
1124
1125    // Convert nodes to JSON
1126    let nodes_json: Vec<_> = graph
1127        .nodes
1128        .iter()
1129        .map(|node| {
1130            json!({
1131                "id": format!("0x{:x}", node.id.0),
1132                "type_name": node.type_name,
1133                "size": node.size,
1134                "stack_ptr": node.stack_ptr.map(|p| format!("0x{:x}", p)),
1135            })
1136        })
1137        .collect();
1138
1139    // Convert edges to JSON (including borrow edges from BorrowAnalyzer)
1140    let mut edges_json: Vec<_> = graph
1141        .edges
1142        .iter()
1143        .map(|edge| {
1144            json!({
1145                "from": format!("0x{:x}", edge.from.0),
1146                "to": format!("0x{:x}", edge.to.0),
1147                "kind": match edge.op {
1148                    EdgeKind::Owns => "Owns",
1149                    EdgeKind::Contains => "Contains",
1150                    EdgeKind::Borrows => "Borrows",
1151                    EdgeKind::RcClone => "RcClone",
1152                    EdgeKind::ArcClone => "ArcClone",
1153                    EdgeKind::Move => "Move",
1154                    EdgeKind::SharedBorrow => "SharedBorrow",
1155                    EdgeKind::MutBorrow => "MutBorrow",
1156                },
1157            })
1158        })
1159        .collect();
1160
1161    // Add borrow edges from BorrowAnalyzer
1162    for event in &borrow_history {
1163        let edge_kind = match event.borrow_info.borrow_type {
1164            crate::analysis::borrow_analysis::BorrowType::Immutable => "SharedBorrow",
1165            crate::analysis::borrow_analysis::BorrowType::Mutable => "MutBorrow",
1166            crate::analysis::borrow_analysis::BorrowType::Shared => "Borrows",
1167            crate::analysis::borrow_analysis::BorrowType::Weak => "Borrows",
1168        };
1169
1170        edges_json.push(json!({
1171            "from": format!("0x{:x}", event.borrow_info.ptr),
1172            "to": format!("0x{:x}", event.borrow_info.ptr),
1173            "kind": edge_kind,
1174            "var_name": event.borrow_info.var_name,
1175            "thread_id": event.borrow_info.thread_id,
1176            "borrow_id": format!("{:?}", event.borrow_info.id),
1177        }));
1178    }
1179
1180    // Convert cycles to JSON
1181    let cycles_json: Vec<_> = graph
1182        .cycles
1183        .iter()
1184        .map(|cycle| {
1185            let nodes: Vec<_> = cycle.iter().map(|id| format!("0x{:x}", id.0)).collect();
1186            json!({
1187                "nodes": nodes,
1188            })
1189        })
1190        .collect();
1191
1192    // Convert issues to JSON
1193    let issues_json: Vec<_> = diagnostics
1194        .issues
1195        .iter()
1196        .map(|issue| match issue {
1197            crate::analysis::ownership_graph::DiagnosticIssue::RcCycle { nodes, cycle_type } => {
1198                json!({
1199                    "type": "RcCycle",
1200                    "cycle_type": format!("{:?}", cycle_type),
1201                    "nodes": nodes.iter().map(|id| format!("0x{:x}", id.0)).collect::<Vec<_>>(),
1202                    "severity": "error",
1203                })
1204            }
1205            crate::analysis::ownership_graph::DiagnosticIssue::ArcCloneStorm {
1206                clone_count,
1207                threshold,
1208            } => {
1209                json!({
1210                    "type": "ArcCloneStorm",
1211                    "clone_count": clone_count,
1212                    "threshold": threshold,
1213                    "severity": "warning",
1214                })
1215            }
1216        })
1217        .collect();
1218
1219    // Build root cause info
1220    let root_cause_json = graph.find_root_cause().map(|rc| {
1221        json!({
1222            "cause": match rc.root_cause {
1223                crate::analysis::ownership_graph::RootCause::ArcCloneStorm => "ArcCloneStorm",
1224                crate::analysis::ownership_graph::RootCause::RcCycle => "RcCycle",
1225            },
1226            "description": rc.description,
1227            "impact": rc.impact,
1228        })
1229    });
1230
1231    let json_data = json!({
1232        "metadata": {
1233            "export_version": "2.0",
1234            "specification": "ownership graph analysis",
1235            "timestamp": std::time::SystemTime::now()
1236                .duration_since(std::time::UNIX_EPOCH)
1237                .unwrap_or_default()
1238                .as_secs()
1239        },
1240        "summary": {
1241            "total_nodes": graph.nodes.len(),
1242            "total_edges": graph.edges.len(),
1243            "total_cycles": graph.cycles.len(),
1244            "rc_clone_count": diagnostics.rc_clone_count,
1245            "arc_clone_count": diagnostics.arc_clone_count,
1246            "has_issues": diagnostics.has_issues(),
1247        },
1248        "nodes": nodes_json,
1249        "edges": edges_json,
1250        "cycles": cycles_json,
1251        "diagnostics": {
1252            "issues": issues_json,
1253            "root_cause": root_cause_json,
1254        },
1255    });
1256
1257    let file_path = base_path.join("ownership_graph.json");
1258    let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
1259        MemScopeError::error("export", "export_ownership_graph_json", e.to_string())
1260    })?;
1261    std::fs::write(&file_path, json_string).map_err(|e| {
1262        MemScopeError::error("export", "export_ownership_graph_json", e.to_string())
1263    })?;
1264
1265    Ok(())
1266}
1267
1268/// Build ownership graph from allocation data
1269fn build_ownership_graph_from_allocations(
1270    allocations: &[crate::capture::types::AllocationInfo],
1271    event_store: &crate::event_store::EventStore,
1272) -> OwnershipGraph {
1273    debug!(
1274        allocations = allocations.len(),
1275        "Starting build_ownership_graph"
1276    );
1277    use crate::analysis::relation_inference::{detect_containers, Relation, RelationGraphBuilder};
1278    use crate::event_store::MemoryEventType;
1279
1280    // Convert allocations to passport format for graph building
1281    // IMPORTANT: For allocations with ptr=0, assign virtual unique IDs to avoid collisions
1282    debug!("Converting allocations to passports");
1283    let passports: Vec<(
1284        NodeId,
1285        String,
1286        usize,
1287        Vec<crate::analysis::ownership_graph::OwnershipEvent>,
1288    )> = allocations
1289        .iter()
1290        .enumerate()
1291        .map(|(idx, alloc)| {
1292            let unique_ptr = if alloc.ptr == 0 {
1293                crate::analysis::VIRTUAL_PTR_BASE + idx
1294            } else {
1295                alloc.ptr
1296            };
1297            let id = NodeId::from_ptr(unique_ptr);
1298            let type_name = alloc
1299                .type_name
1300                .clone()
1301                .unwrap_or_else(|| "unknown".to_string());
1302            let size = alloc.size;
1303
1304            // Generate ownership events from allocation info
1305            let events = vec![crate::analysis::ownership_graph::OwnershipEvent::new(
1306                alloc.timestamp_alloc,
1307                OwnershipOp::Create,
1308                id,
1309                None,
1310            )];
1311
1312            // Detect smart pointer clones from type name
1313            if type_name.contains("Arc<") || type_name.contains("Rc<") {
1314                // Check for clone patterns
1315                // Note: In real tracking, clone events would be recorded at runtime
1316                // Here we infer from type and allocation patterns
1317            }
1318
1319            (id, type_name, size, events)
1320        })
1321        .collect();
1322    debug!(passports = passports.len(), "Created passports");
1323
1324    debug!("Building initial ownership graph");
1325    let mut graph = OwnershipGraph::build(&passports);
1326    debug!(
1327        nodes = graph.nodes.len(),
1328        edges = graph.edges.len(),
1329        "Initial graph built"
1330    );
1331
1332    // Build separate lists for HeapOwner and Container allocations
1333    // with their original indices preserved for correct edge mapping
1334
1335    // Step 1: Collect HeapOwner allocations
1336    let _heap_owner_original_indices: Vec<usize> = allocations
1337        .iter()
1338        .enumerate()
1339        .filter(|(_, a)| a.timestamp_dealloc.is_none())
1340        .map(|(i, _)| i)
1341        .collect();
1342
1343    let heap_owner_allocations: Vec<ActiveAllocation> = allocations
1344        .iter()
1345        .enumerate()
1346        .filter(|(_, a)| a.timestamp_dealloc.is_none())
1347        .filter_map(|(_idx, a)| {
1348            // Skip Container types (ptr is 0 or virtual pointer)
1349            if a.ptr == 0 || is_virtual_pointer(a.ptr) {
1350                return None;
1351            }
1352
1353            Some(ActiveAllocation {
1354                ptr: Some(a.ptr),
1355                kind: crate::core::types::TrackKind::HeapOwner {
1356                    ptr: a.ptr,
1357                    size: a.size,
1358                },
1359                size: a.size,
1360                allocated_at: a.timestamp_alloc,
1361                var_name: a.var_name.clone(),
1362                type_name: a.type_name.clone(),
1363                thread_id: a.thread_id_u64,
1364                call_stack_hash: None,
1365                module_path: a.module_path.clone(),
1366                stack_ptr: a.stack_ptr,
1367            })
1368        })
1369        .collect();
1370
1371    // Update stack_ptr for HeapOwner nodes from AllocationInfo
1372    for (i, alloc) in heap_owner_allocations.iter().enumerate() {
1373        if i < graph.nodes.len() {
1374            graph.nodes[i].stack_ptr = alloc.stack_ptr;
1375        }
1376    }
1377
1378    // Step 2: Collect Container events from event_store
1379    // IMPORTANT: Only include containers that match HeapOwner thread_id
1380    let valid_thread_ids: std::collections::HashSet<u64> =
1381        heap_owner_allocations.iter().map(|a| a.thread_id).collect();
1382
1383    let container_events: Vec<_> = event_store
1384        .snapshot()
1385        .into_iter()
1386        .filter(|e| e.event_type == MemoryEventType::Metadata)
1387        .filter(|e| valid_thread_ids.contains(&e.thread_id))
1388        .filter_map(|e| {
1389            let type_name = e.type_name.clone().unwrap_or_default();
1390            let var_name = e.var_name.clone().unwrap_or_default();
1391            let is_container = type_name.contains("HashMap")
1392                || type_name.contains("BTreeMap")
1393                || type_name.contains("VecDeque")
1394                || type_name.contains("RefCell")
1395                || type_name.contains("RwLock");
1396            if is_container {
1397                return Some((e, type_name, var_name));
1398            }
1399            None
1400        })
1401        .collect();
1402
1403    // Assign virtual pointers to Container allocations to avoid ptr=0 collisions
1404    let container_allocations: Vec<ActiveAllocation> = container_events
1405        .iter()
1406        .enumerate()
1407        .map(|(idx, (e, type_name, var_name))| {
1408            let virtual_ptr = 0x300000000u64 as usize + idx;
1409            ActiveAllocation {
1410                ptr: Some(virtual_ptr),
1411                kind: crate::core::types::TrackKind::Container,
1412                size: e.size.max(1),
1413                allocated_at: e.timestamp,
1414                var_name: Some(var_name.clone()),
1415                type_name: Some(type_name.clone()),
1416                thread_id: e.thread_id,
1417                call_stack_hash: e.call_stack_hash,
1418                module_path: e.module_path.clone(),
1419                stack_ptr: None,
1420            }
1421        })
1422        .collect();
1423
1424    let mut all_for_relation: Vec<ActiveAllocation> = Vec::new();
1425    all_for_relation.extend(heap_owner_allocations.clone());
1426    all_for_relation.extend(container_allocations.clone());
1427
1428    // Step 4: Run container detection with appropriate config
1429    debug!("Running container detection");
1430    let container_config = crate::analysis::relation_inference::ContainerConfig {
1431        time_window_ns: 10_000_000, // 10ms (more permissive for test scenarios)
1432        size_ratio: 10000,          // Allow very large ratio for containers with small size
1433        lookahead: 10,              // Look at more candidates
1434    };
1435
1436    let container_edges = detect_containers(&all_for_relation, Some(container_config));
1437    debug!(
1438        container_edges = container_edges.len(),
1439        "Container detection completed"
1440    );
1441
1442    // Step 5: Run RelationGraphBuilder for HeapOwner edges (Owns, Shares, Clone)
1443    debug!("Running RelationGraphBuilder");
1444    let relation_graph = RelationGraphBuilder::build(&heap_owner_allocations, None);
1445    debug!(
1446        edges = relation_graph.edges.len(),
1447        "RelationGraphBuilder completed"
1448    );
1449
1450    // Step 6: Add Container nodes to graph
1451    // Use virtual pointer range for Container IDs to avoid collisions with HeapOwner IDs
1452    const CONTAINER_PTR_BASE: usize = 0x300000000;
1453    let heap_owner_count = graph.nodes.len();
1454    for (idx, (e, type_name, _var_name)) in container_events.iter().enumerate() {
1455        let node_id = NodeId::from_ptr(CONTAINER_PTR_BASE + idx);
1456        graph.nodes.push(crate::analysis::ownership_graph::Node {
1457            id: node_id,
1458            type_name: type_name.clone(),
1459            size: e.size,
1460            stack_ptr: None,
1461        });
1462    }
1463
1464    // Step 7: Add HeapOwner edges (Owns, Shares, Clone)
1465    for edge in &relation_graph.edges {
1466        let from_id = graph.nodes[edge.from].id;
1467        let to_id = graph.nodes[edge.to].id;
1468
1469        let edge_kind = match edge.relation {
1470            Relation::Owns => EdgeKind::Owns,
1471            Relation::Contains => EdgeKind::Contains,
1472            Relation::Slice => EdgeKind::Borrows,
1473            Relation::Clone => EdgeKind::RcClone,
1474            Relation::Shares => EdgeKind::ArcClone,
1475            Relation::Evolution => EdgeKind::Contains,
1476            Relation::ArcClone => EdgeKind::ArcClone,
1477            Relation::RcClone => EdgeKind::RcClone,
1478            Relation::ImmutableBorrow => EdgeKind::SharedBorrow,
1479            Relation::MutableBorrow => EdgeKind::MutBorrow,
1480        };
1481
1482        graph.edges.push(crate::analysis::ownership_graph::Edge {
1483            from: from_id,
1484            to: to_id,
1485            op: edge_kind,
1486        });
1487    }
1488
1489    // Step 8: Add Container → HeapOwner edges (Contains)
1490    for edge in &container_edges {
1491        // edge.from is Container index in all_for_relation
1492        // edge.to is HeapOwner index in all_for_relation
1493        let from_all_idx = edge.from;
1494        let to_all_idx = edge.to;
1495
1496        // Validate indices
1497        if from_all_idx < heap_owner_count || to_all_idx >= heap_owner_count {
1498            continue; // Invalid edge indices
1499        }
1500
1501        // Convert from container index to graph node index
1502        let container_graph_idx = from_all_idx - heap_owner_count;
1503        if container_graph_idx >= container_events.len() {
1504            continue; // Invalid container index
1505        }
1506
1507        let from_id = graph.nodes[heap_owner_count + container_graph_idx].id;
1508        let to_id = graph.nodes[to_all_idx].id;
1509
1510        graph.edges.push(crate::analysis::ownership_graph::Edge {
1511            from: from_id,
1512            to: to_id,
1513            op: EdgeKind::Contains,
1514        });
1515    }
1516
1517    debug!(
1518        nodes = graph.nodes.len(),
1519        edges = graph.edges.len(),
1520        "Final ownership graph built"
1521    );
1522    graph
1523}
1524
1525#[cfg(test)]
1526mod tests {
1527    use super::*;
1528
1529    /// Objective: Verify that OptimizationLevel enum variants exist.
1530    /// Invariants: All variants should be accessible.
1531    #[test]
1532    fn test_optimization_level_variants() {
1533        let _low = OptimizationLevel::Low;
1534        let _medium = OptimizationLevel::Medium;
1535        let _high = OptimizationLevel::High;
1536        let _maximum = OptimizationLevel::Maximum;
1537    }
1538
1539    /// Objective: Verify that OptimizationLevel default is Medium.
1540    /// Invariants: Default should return Medium variant.
1541    #[test]
1542    fn test_optimization_level_default() {
1543        let level = OptimizationLevel::default();
1544        assert!(
1545            matches!(level, OptimizationLevel::Medium),
1546            "Default should be Medium"
1547        );
1548    }
1549
1550    /// Objective: Verify that SchemaValidator creates with default settings.
1551    /// Invariants: New validator should have strict_mode disabled.
1552    #[test]
1553    fn test_schema_validator_new() {
1554        let validator = SchemaValidator::new();
1555        let data = serde_json::json!({"test": "value"});
1556        let result = validator.validate(&data);
1557        assert!(result.is_ok(), "Validation should pass for any object");
1558    }
1559
1560    /// Objective: Verify that SchemaValidator with strict mode validates required fields.
1561    /// Invariants: Strict mode should require timestamp, allocations, stats fields.
1562    #[test]
1563    fn test_schema_validator_strict_mode() {
1564        let validator = SchemaValidator::new().with_strict_mode(true);
1565
1566        let missing_fields = serde_json::json!({"other": "data"});
1567        let result = validator.validate(&missing_fields);
1568        assert!(result.is_err(), "Should fail with missing required fields");
1569
1570        let valid_data = serde_json::json!({
1571            "timestamp": 123,
1572            "allocations": [],
1573            "stats": {}
1574        });
1575        let result = validator.validate(&valid_data);
1576        assert!(result.is_ok(), "Should pass with all required fields");
1577    }
1578
1579    /// Objective: Verify that SchemaValidator rejects non-object data.
1580    /// Invariants: Non-object JSON should be rejected.
1581    #[test]
1582    fn test_schema_validator_non_object() {
1583        let validator = SchemaValidator::new();
1584        let data = serde_json::json!("not an object");
1585        let result = validator.validate(&data);
1586        assert!(result.is_err(), "Should reject non-object data");
1587    }
1588
1589    /// Objective: Verify that ExportJsonOptions has correct defaults.
1590    /// Invariants: Default options should enable parallel processing and streaming.
1591    #[test]
1592    fn test_export_json_options_default() {
1593        let options = ExportJsonOptions::default();
1594        assert!(
1595            options.parallel_processing,
1596            "parallel_processing should be true by default"
1597        );
1598        assert!(
1599            options.streaming_writer,
1600            "streaming_writer should be true by default"
1601        );
1602        assert!(
1603            options.enable_type_cache,
1604            "enable_type_cache should be true by default"
1605        );
1606        assert!(
1607            options.adaptive_optimization,
1608            "adaptive_optimization should be true by default"
1609        );
1610        assert!(
1611            !options.schema_validation,
1612            "schema_validation should be false by default"
1613        );
1614        assert!(
1615            !options.security_analysis,
1616            "security_analysis should be false by default"
1617        );
1618    }
1619
1620    /// Objective: Verify that ExportJsonOptions builder methods work.
1621    /// Invariants: Builder methods should set corresponding fields.
1622    #[test]
1623    fn test_export_json_options_builders() {
1624        let options = ExportJsonOptions::default()
1625            .fast_export_mode(true)
1626            .security_analysis(true)
1627            .streaming_writer(false)
1628            .schema_validation(true)
1629            .integrity_hashes(true)
1630            .batch_size(500)
1631            .adaptive_optimization(false)
1632            .max_cache_size(5000)
1633            .include_low_severity(true)
1634            .thread_count(Some(4));
1635
1636        assert!(options.fast_export_mode, "fast_export_mode should be true");
1637        assert!(
1638            options.security_analysis,
1639            "security_analysis should be true"
1640        );
1641        assert!(
1642            !options.streaming_writer,
1643            "streaming_writer should be false"
1644        );
1645        assert!(
1646            options.schema_validation,
1647            "schema_validation should be true"
1648        );
1649        assert!(options.integrity_hashes, "integrity_hashes should be true");
1650        assert_eq!(options.batch_size, 500, "batch_size should be 500");
1651        assert!(
1652            !options.adaptive_optimization,
1653            "adaptive_optimization should be false"
1654        );
1655        assert_eq!(
1656            options.max_cache_size, 5000,
1657            "max_cache_size should be 5000"
1658        );
1659        assert!(
1660            options.include_low_severity,
1661            "include_low_severity should be true"
1662        );
1663        assert_eq!(
1664            options.thread_count,
1665            Some(4),
1666            "thread_count should be Some(4)"
1667        );
1668    }
1669
1670    /// Objective: Verify that ExportError variants exist and display correctly.
1671    /// Invariants: Error variants should be displayable.
1672    #[test]
1673    fn test_export_error_variants() {
1674        let io_err = ExportError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
1675        let json_err = ExportError::Json(serde_json::from_str::<i32>("invalid").unwrap_err());
1676        let export_err = ExportError::ExportFailed("test error".to_string());
1677
1678        assert!(
1679            format!("{}", io_err).contains("IO error"),
1680            "Should contain IO error"
1681        );
1682        assert!(
1683            format!("{}", json_err).contains("JSON error"),
1684            "Should contain JSON error"
1685        );
1686        assert!(
1687            format!("{}", export_err).contains("Export failed"),
1688            "Should contain Export failed"
1689        );
1690    }
1691
1692    /// Objective: Verify that estimate_json_size works for different JSON types.
1693    /// Invariants: Size estimation should be reasonable.
1694    #[test]
1695    fn test_estimate_json_size() {
1696        let string_val = serde_json::json!("hello world");
1697        let num_val = serde_json::json!(42);
1698        let array_val = serde_json::json!([1, 2, 3]);
1699        let object_val = serde_json::json!({"key": "value"});
1700
1701        let string_size = estimate_json_size(&string_val);
1702        let num_size = estimate_json_size(&num_val);
1703        let array_size = estimate_json_size(&array_val);
1704        let object_size = estimate_json_size(&object_val);
1705
1706        assert!(string_size > 0, "String size should be positive");
1707        assert!(num_size > 0, "Number size should be positive");
1708        assert!(array_size > 0, "Array size should be positive");
1709        assert!(object_size > 0, "Object size should be positive");
1710    }
1711
1712    /// Objective: Verify that get_or_compute_type_info categorizes types correctly.
1713    /// Invariants: Type categorization should match expected patterns.
1714    #[test]
1715    fn test_get_or_compute_type_info() {
1716        assert_eq!(
1717            get_or_compute_type_info("Vec<i32>", 100),
1718            "dynamic_array",
1719            "Vec should be dynamic_array"
1720        );
1721        assert_eq!(
1722            get_or_compute_type_info("String", 24),
1723            "string",
1724            "String should be string"
1725        );
1726        assert_eq!(
1727            get_or_compute_type_info("Box<i32>", 8),
1728            "smart_pointer",
1729            "Box should be smart_pointer"
1730        );
1731        assert_eq!(
1732            get_or_compute_type_info("Rc<String>", 8),
1733            "smart_pointer",
1734            "Rc should be smart_pointer"
1735        );
1736        assert_eq!(
1737            get_or_compute_type_info("Arc<i32>", 16),
1738            "smart_pointer",
1739            "Arc should be smart_pointer"
1740        );
1741        assert_eq!(
1742            get_or_compute_type_info("[u8; 100]", 100),
1743            "byte_array",
1744            "u8 array should be byte_array"
1745        );
1746        assert_eq!(
1747            get_or_compute_type_info("CustomType", 1024 * 1024 * 2),
1748            "large_buffer",
1749            "Large allocation should be large_buffer"
1750        );
1751        assert_eq!(
1752            get_or_compute_type_info("MyType", 100),
1753            "custom",
1754            "Unknown type should be custom"
1755        );
1756    }
1757}