1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum OptimizationLevel {
30 Low,
32 #[default]
34 Medium,
35 High,
37 Maximum,
39}
40
41#[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 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 let output_dir = if output_path.extension().is_some() {
187 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 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
269fn is_library_module_path(module_path: &str) -> bool {
271 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 if module_path.starts_with("memscope_rs::") {
281 return true;
282 }
283
284 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 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 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(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
599pub 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
621pub 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#[derive(Debug, Clone, PartialEq, Eq, Default)]
684pub enum DashboardTemplate {
685 #[default]
687 Unified,
688 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 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 pub fn name(&self) -> &str {
712 match self {
713 DashboardTemplate::Unified => "Unified Dashboard",
714 DashboardTemplate::Custom(_) => "Custom Template",
715 }
716 }
717}
718
719pub 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
738pub 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
754pub 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 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 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 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 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 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
966pub fn export_system_resources_json<P: AsRef<Path>>(base_path: P) -> MemScopeResult<()> {
973 let base_path = base_path.as_ref();
974
975 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 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 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
1101pub 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 let graph = build_ownership_graph_from_allocations(allocations, event_store);
1117
1118 let diagnostics = graph.diagnostics(50);
1120
1121 let borrow_analyzer = crate::analysis::borrow_analysis::get_global_borrow_analyzer();
1123 let borrow_history = borrow_analyzer.get_borrow_history();
1124
1125 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 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 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 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 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 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
1268fn 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 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 let events = vec![crate::analysis::ownership_graph::OwnershipEvent::new(
1306 alloc.timestamp_alloc,
1307 OwnershipOp::Create,
1308 id,
1309 None,
1310 )];
1311
1312 if type_name.contains("Arc<") || type_name.contains("Rc<") {
1314 }
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 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 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 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 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 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 debug!("Running container detection");
1430 let container_config = crate::analysis::relation_inference::ContainerConfig {
1431 time_window_ns: 10_000_000, size_ratio: 10000, lookahead: 10, };
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 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 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 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 for edge in &container_edges {
1491 let from_all_idx = edge.from;
1494 let to_all_idx = edge.to;
1495
1496 if from_all_idx < heap_owner_count || to_all_idx >= heap_owner_count {
1498 continue; }
1500
1501 let container_graph_idx = from_all_idx - heap_owner_count;
1503 if container_graph_idx >= container_events.len() {
1504 continue; }
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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}