1use crate::analysis::memory_passport_tracker::MemoryPassportTracker;
7use crate::analysis::ownership_graph::{EdgeKind, ObjectId, OwnershipGraph, OwnershipOp};
8use crate::capture::platform::memory_info::PlatformMemoryInfo;
9use crate::core::{MemScopeError, MemScopeResult};
10use crate::render_engine::dashboard::DashboardRenderer;
11use crate::snapshot::{ActiveAllocation, MemorySnapshot, ThreadMemoryStats};
12use crate::tracker::Tracker;
13use rayon::prelude::*;
14use serde_json::json;
15use std::{
16 collections::HashMap,
17 fs::File,
18 io::{BufWriter, Write},
19 path::Path,
20 sync::Arc,
21};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum OptimizationLevel {
26 Low,
28 #[default]
30 Medium,
31 High,
33 Maximum,
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct SchemaValidator {
40 strict_mode: bool,
41}
42
43impl SchemaValidator {
44 pub fn new() -> Self {
45 Self { strict_mode: false }
46 }
47
48 pub fn with_strict_mode(mut self, strict: bool) -> Self {
49 self.strict_mode = strict;
50 self
51 }
52
53 pub fn validate(&self, data: &serde_json::Value) -> Result<(), String> {
54 if !data.is_object() {
55 return Err("Export data must be a JSON object".to_string());
56 }
57
58 let obj = data.as_object().ok_or("Invalid JSON object")?;
59
60 if self.strict_mode {
61 let required_fields = ["timestamp", "allocations", "stats"];
62 for field in &required_fields {
63 if !obj.contains_key(*field) {
64 return Err(format!("Missing required field: {}", field));
65 }
66 }
67 }
68
69 Ok(())
70 }
71}
72
73#[derive(Debug, Clone)]
74pub struct ExportJsonOptions {
75 pub parallel_processing: bool,
76 pub buffer_size: usize,
77 pub use_compact_format: Option<bool>,
78 pub enable_type_cache: bool,
79 pub batch_size: usize,
80 pub streaming_writer: bool,
81 pub schema_validation: bool,
82 pub adaptive_optimization: bool,
83 pub max_cache_size: usize,
84 pub security_analysis: bool,
85 pub include_low_severity: bool,
86 pub integrity_hashes: bool,
87 pub fast_export_mode: bool,
88 pub auto_fast_export_threshold: Option<usize>,
89 pub thread_count: Option<usize>,
90}
91
92impl Default for ExportJsonOptions {
93 fn default() -> Self {
94 Self {
95 parallel_processing: true,
96 buffer_size: 256 * 1024,
97 use_compact_format: None,
98 enable_type_cache: true,
99 batch_size: 1000,
100 streaming_writer: true,
101 schema_validation: false,
102 adaptive_optimization: true,
103 max_cache_size: 10_000,
104 security_analysis: false,
105 include_low_severity: false,
106 integrity_hashes: false,
107 fast_export_mode: false,
108 auto_fast_export_threshold: Some(10_000),
109 thread_count: None,
110 }
111 }
112}
113
114impl ExportJsonOptions {
115 pub fn fast_export_mode(mut self, enabled: bool) -> Self {
116 self.fast_export_mode = enabled;
117 self
118 }
119
120 pub fn security_analysis(mut self, enabled: bool) -> Self {
121 self.security_analysis = enabled;
122 self
123 }
124
125 pub fn streaming_writer(mut self, enabled: bool) -> Self {
126 self.streaming_writer = enabled;
127 self
128 }
129
130 pub fn schema_validation(mut self, enabled: bool) -> Self {
131 self.schema_validation = enabled;
132 self
133 }
134
135 pub fn integrity_hashes(mut self, enabled: bool) -> Self {
136 self.integrity_hashes = enabled;
137 self
138 }
139
140 pub fn batch_size(mut self, size: usize) -> Self {
141 self.batch_size = size;
142 self
143 }
144
145 pub fn adaptive_optimization(mut self, enabled: bool) -> Self {
146 self.adaptive_optimization = enabled;
147 self
148 }
149
150 pub fn max_cache_size(mut self, size: usize) -> Self {
151 self.max_cache_size = size;
152 self
153 }
154
155 pub fn include_low_severity(mut self, include: bool) -> Self {
156 self.include_low_severity = include;
157 self
158 }
159
160 pub fn thread_count(mut self, count: Option<usize>) -> Self {
161 self.thread_count = count;
162 self
163 }
164}
165
166pub fn export_snapshot_to_json(
167 snapshot: &MemorySnapshot,
168 output_path: &Path,
169 options: &ExportJsonOptions,
170) -> Result<(), Box<dyn std::error::Error>> {
171 if let Some(parent) = output_path.parent() {
173 if !parent.as_os_str().is_empty() {
174 std::fs::create_dir_all(parent)?;
175 }
176 }
177
178 let allocations: Vec<&ActiveAllocation> = snapshot.active_allocations.values().collect();
179 let processed = process_allocations(&allocations, options)?;
180
181 let output_dir = if output_path.extension().is_some() {
183 output_path.parent().unwrap_or(Path::new("."))
185 } else {
186 output_path
187 };
188
189 generate_memory_analysis_json(output_dir, &processed, options)?;
190 generate_lifetime_json(output_dir, &processed, options)?;
191 generate_thread_analysis_json(output_dir, &snapshot.thread_stats, options)?;
192
193 Ok(())
194}
195
196fn process_allocations(
197 allocations: &[&ActiveAllocation],
198 options: &ExportJsonOptions,
199) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
200 if options.parallel_processing && allocations.len() > options.batch_size {
201 let chunk_size = (allocations.len() / num_cpus::get()).max(1);
202 Ok(allocations
203 .par_chunks(chunk_size)
204 .flat_map(process_allocation_batch)
205 .collect())
206 } else {
207 Ok(process_allocation_batch(allocations))
208 }
209}
210
211fn process_allocation_batch(allocations: &[&ActiveAllocation]) -> Vec<serde_json::Value> {
212 let current_time = std::time::SystemTime::now()
213 .duration_since(std::time::UNIX_EPOCH)
214 .map(|d| d.as_nanos() as u64)
215 .unwrap_or(0);
216
217 allocations
218 .iter()
219 .map(|alloc| {
220 let type_info = get_or_compute_type_info(
221 alloc.type_name.as_deref().unwrap_or("unknown"),
222 alloc.size,
223 );
224
225 let lifetime_ms = if alloc.allocated_at > 0 {
226 (current_time.saturating_sub(alloc.allocated_at)) / 1_000_000
227 } else {
228 0
229 };
230
231 let mut entry = json!({
232 "address": format!("0x{:x}", alloc.ptr),
233 "size": alloc.size,
234 "type": type_info,
235 "timestamp": alloc.allocated_at,
236 "thread_id": alloc.thread_id,
237 "lifetime_ms": lifetime_ms,
238 });
239
240 if let Some(ref var_name) = alloc.var_name {
241 entry["var_name"] = serde_json::json!(var_name);
242 }
243
244 if let Some(ref type_name) = alloc.type_name {
245 entry["type_name"] = serde_json::json!(type_name);
246 }
247
248 entry
249 })
250 .collect()
251}
252
253fn get_or_compute_type_info(type_name: &str, size: usize) -> String {
254 if (type_name.contains("Vec<") || type_name.contains("vec::Vec<"))
256 && !type_name.contains("VecDeque")
257 {
258 "dynamic_array".to_string()
259 } else if type_name == "str"
260 || type_name == "String"
261 || type_name.contains("&str")
262 || type_name.contains("alloc::string::String")
263 {
264 "string".to_string()
265 } else if type_name.contains("Box") || type_name.contains("Rc") || type_name.contains("Arc") {
266 "smart_pointer".to_string()
267 } else if type_name.contains("[") && type_name.contains("u8") {
268 "byte_array".to_string()
269 } else if size > 1024 * 1024 {
270 "large_buffer".to_string()
271 } else {
272 "custom".to_string()
273 }
274}
275
276fn generate_memory_analysis_json<P: AsRef<Path>>(
277 output_path: P,
278 allocations: &[serde_json::Value],
279 options: &ExportJsonOptions,
280) -> Result<(), Box<dyn std::error::Error>> {
281 let total_size: usize = allocations
282 .iter()
283 .filter_map(|a| a.get("size").and_then(|s| s.as_u64()))
284 .map(|s| s as usize)
285 .sum();
286
287 let type_distribution: HashMap<String, usize> = {
288 let mut dist = HashMap::new();
289 for alloc in allocations {
290 if let Some(t) = alloc.get("type").and_then(|t| t.as_str()) {
291 *dist.entry(t.to_string()).or_insert(0) += 1;
292 }
293 }
294 dist
295 };
296
297 let data = json!({
298 "metadata": {
299 "export_version": "2.0",
300 "export_timestamp": chrono::Utc::now().to_rfc3339(),
301 "specification": "memscope-rs memory analysis",
302 "total_allocations": allocations.len(),
303 "total_size_bytes": total_size
304 },
305 "allocations": allocations,
306 "statistics": {
307 "total_allocations": allocations.len(),
308 "total_size_bytes": total_size,
309 "average_size_bytes": if allocations.is_empty() { 0 } else { total_size / allocations.len() }
310 },
311 "type_distribution": type_distribution
312 });
313
314 let path = output_path.as_ref().join("memory_analysis.json");
315 write_json_optimized(path, &data, options)?;
316 Ok(())
317}
318
319fn generate_lifetime_json<P: AsRef<Path>>(
320 output_path: P,
321 allocations: &[serde_json::Value],
322 options: &ExportJsonOptions,
323) -> Result<(), Box<dyn std::error::Error>> {
324 let ownership_histories: Vec<serde_json::Value> = allocations
325 .iter()
326 .map(|alloc| {
327 json!({
328 "address": alloc.get("address"),
329 "var_name": alloc.get("var_name"),
330 "type_name": alloc.get("type_name"),
331 "size": alloc.get("size"),
332 "timestamp_alloc": alloc.get("timestamp"),
333 "timestamp_dealloc": null,
334 "lifetime_ms": alloc.get("lifetime_ms"),
335 "events": [
336 {
337 "event_type": "Created",
338 "timestamp": alloc.get("timestamp"),
339 "context": "initial_allocation"
340 }
341 ]
342 })
343 })
344 .collect();
345
346 let lifetime_data = json!({
347 "metadata": {
348 "export_version": "2.0",
349 "export_timestamp": chrono::Utc::now().to_rfc3339(),
350 "specification": "memscope-rs lifetime tracking",
351 "total_tracked_allocations": ownership_histories.len()
352 },
353 "ownership_histories": ownership_histories
354 });
355
356 let lifetime_path = output_path.as_ref().join("lifetime.json");
357 write_json_optimized(lifetime_path, &lifetime_data, options)?;
358 Ok(())
359}
360
361fn generate_thread_analysis_json<P: AsRef<Path>>(
362 output_path: P,
363 thread_stats: &HashMap<u64, ThreadMemoryStats>,
364 options: &ExportJsonOptions,
365) -> Result<(), Box<dyn std::error::Error>> {
366 let thread_analysis: Vec<serde_json::Value> = thread_stats
367 .values()
368 .map(|stats| {
369 json!({
370 "thread_id": stats.thread_id,
371 "allocation_count": stats.allocation_count,
372 "total_allocated": stats.total_allocated,
373 "current_memory": stats.current_memory,
374 "peak_memory": stats.peak_memory,
375 })
376 })
377 .collect();
378
379 let data = json!({
380 "metadata": {
381 "export_version": "2.0",
382 "export_timestamp": chrono::Utc::now().to_rfc3339(),
383 "specification": "thread analysis",
384 "total_threads": thread_analysis.len()
385 },
386 "thread_analysis": thread_analysis
387 });
388
389 let path = output_path.as_ref().join("thread_analysis.json");
390 write_json_optimized(path, &data, options)?;
391 Ok(())
392}
393
394fn write_json_optimized<P: AsRef<Path>>(
395 path: P,
396 data: &serde_json::Value,
397 options: &ExportJsonOptions,
398) -> Result<(), Box<dyn std::error::Error>> {
399 let path = path.as_ref();
400
401 let estimated_size = estimate_json_size(data);
402 let use_compact = options
403 .use_compact_format
404 .unwrap_or(estimated_size > 1_000_000);
405
406 if options.streaming_writer && estimated_size > 500_000 {
407 let file = File::create(path)?;
408 let mut writer = BufWriter::with_capacity(options.buffer_size, file);
409
410 if use_compact {
411 serde_json::to_writer(&mut writer, data)?;
412 } else {
413 serde_json::to_writer_pretty(&mut writer, data)?;
414 }
415
416 writer.flush()?;
417 } else {
418 let json_string = if use_compact {
419 serde_json::to_string(data)?
420 } else {
421 serde_json::to_string_pretty(data)?
422 };
423 std::fs::write(path, json_string)?;
424 }
425
426 Ok(())
427}
428
429fn estimate_json_size(data: &serde_json::Value) -> usize {
430 match data {
431 serde_json::Value::Object(map) => {
432 map.values().map(estimate_json_size).sum::<usize>() + map.len() * 20
433 }
434 serde_json::Value::Array(arr) => {
435 arr.iter().map(estimate_json_size).sum::<usize>() + arr.len() * 10
436 }
437 serde_json::Value::String(s) => s.len(),
438 serde_json::Value::Number(n) => n.to_string().len(),
439 _ => 10,
440 }
441}
442
443#[derive(Debug, thiserror::Error)]
444pub enum ExportError {
445 #[error("IO error: {0}")]
446 Io(#[from] std::io::Error),
447
448 #[error("JSON error: {0}")]
449 Json(#[from] serde_json::Error),
450
451 #[error("Export failed: {0}")]
452 ExportFailed(String),
453}
454
455pub fn export_all_json<P: AsRef<Path>>(
456 path: P,
457 tracker: &Tracker,
458 passport_tracker: &Arc<MemoryPassportTracker>,
459 async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
460) -> MemScopeResult<()> {
461 let path_ref = path.as_ref();
462
463 let allocations = tracker.inner().get_active_allocations().unwrap_or_default();
464 let snapshot = MemorySnapshot::from_allocation_infos(allocations.clone());
465 let options = ExportJsonOptions::default();
466
467 std::fs::create_dir_all(path_ref)
468 .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
469
470 export_snapshot_to_json(&snapshot, path_ref, &options)
471 .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
472
473 export_memory_passports_json(path_ref, passport_tracker)
474 .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
475 export_leak_detection_json(path_ref, passport_tracker)
476 .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
477 export_unsafe_ffi_json(path_ref, passport_tracker)
478 .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
479 export_system_resources_json(path_ref)
480 .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
481 export_async_analysis_json(path_ref, async_tracker)
482 .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
483 let typed_allocations: Vec<crate::capture::types::AllocationInfo> =
485 allocations.clone().into_iter().map(|a| a.into()).collect();
486 export_ownership_graph_json(path_ref, &typed_allocations)
487 .map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
488
489 Ok(())
490}
491
492pub fn export_async_analysis_json<P: AsRef<Path>>(
494 path: P,
495 async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
496) -> MemScopeResult<()> {
497 let path_ref = path.as_ref();
498 let stats = async_tracker.get_stats();
499 let profiles = async_tracker.get_all_profiles();
500 let snapshot = async_tracker.snapshot();
501
502 let async_data = json!({
503 "summary": {
504 "total_tasks": stats.total_tasks,
505 "active_tasks": stats.active_tasks,
506 "total_allocations": stats.total_allocations,
507 "total_memory_bytes": stats.total_memory,
508 "active_memory_bytes": stats.active_memory,
509 "peak_memory_bytes": stats.peak_memory,
510 },
511 "task_profiles": profiles.iter().map(|p| json!({
512 "task_id": p.task_id,
513 "task_name": p.task_name,
514 "task_type": format!("{:?}", p.task_type),
515 "created_at_ms": p.created_at_ms,
516 "completed_at_ms": p.completed_at_ms,
517 "total_bytes": p.total_bytes,
518 "current_memory": p.current_memory,
519 "peak_memory": p.peak_memory,
520 "total_allocations": p.total_allocations,
521 "total_deallocations": p.total_deallocations,
522 "duration_ns": p.duration_ns,
523 "allocation_rate": p.allocation_rate,
524 "efficiency_score": p.efficiency_score,
525 "average_allocation_size": p.average_allocation_size,
526 "is_completed": p.is_completed(),
527 "has_potential_leak": p.has_potential_leak(),
528 })).collect::<Vec<_>>(),
529 "allocations": snapshot.allocations.iter().map(|a| json!({
530 "ptr": format!("0x{:x}", a.ptr),
531 "size": a.size,
532 "timestamp": a.timestamp,
533 "task_id": a.task_id,
534 "var_name": a.var_name,
535 "type_name": a.type_name,
536 })).collect::<Vec<_>>(),
537 });
538
539 let async_path = path_ref.join("async_analysis.json");
540 let file = File::create(async_path)
541 .map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
542 let mut writer = BufWriter::new(file);
543 serde_json::to_writer_pretty(&mut writer, &async_data)
544 .map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
545 writer
546 .flush()
547 .map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
548
549 Ok(())
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
554pub enum DashboardTemplate {
555 #[default]
557 Unified,
558 Final,
561}
562
563impl std::fmt::Display for DashboardTemplate {
564 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
565 match self {
566 DashboardTemplate::Unified => write!(f, "dashboard_unified"),
567 DashboardTemplate::Final => write!(f, "dashboard_final"),
568 }
569 }
570}
571
572pub fn export_dashboard_html<P: AsRef<Path>>(
578 path: P,
579 tracker: &Tracker,
580 passport_tracker: &Arc<MemoryPassportTracker>,
581) -> MemScopeResult<()> {
582 export_dashboard_html_with_template(
583 path,
584 tracker,
585 passport_tracker,
586 DashboardTemplate::default(),
587 None,
588 )
589}
590
591pub fn export_dashboard_html_with_async<P: AsRef<Path>>(
593 path: P,
594 tracker: &Tracker,
595 passport_tracker: &Arc<MemoryPassportTracker>,
596 async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
597) -> MemScopeResult<()> {
598 export_dashboard_html_with_template(
599 path,
600 tracker,
601 passport_tracker,
602 DashboardTemplate::default(),
603 Some(async_tracker),
604 )
605}
606
607pub fn export_dashboard_html_with_template<P: AsRef<Path>>(
612 path: P,
613 tracker: &Tracker,
614 passport_tracker: &Arc<MemoryPassportTracker>,
615 template: DashboardTemplate,
616 async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
617) -> MemScopeResult<()> {
618 let path_ref = path.as_ref();
619
620 std::fs::create_dir_all(path_ref).map_err(|e| {
622 MemScopeError::error(
623 "export",
624 "export_dashboard_html_with_template",
625 format!("Failed to create output directory: {}", e),
626 )
627 })?;
628
629 let renderer = DashboardRenderer::new().map_err(|e| {
631 MemScopeError::error(
632 "export",
633 "export_dashboard_html_with_template",
634 format!("Failed to create dashboard renderer: {}", e),
635 )
636 })?;
637
638 let context = renderer
640 .build_context_from_tracker_with_async(tracker, passport_tracker, async_tracker)
641 .map_err(|e| {
642 MemScopeError::error(
643 "export",
644 "export_dashboard_html_with_template",
645 format!("Failed to build context: {}", e),
646 )
647 })?;
648
649 let html_content = match template {
650 DashboardTemplate::Final => renderer.render_final_dashboard(&context).map_err(|e| {
651 MemScopeError::error(
652 "export",
653 "export_dashboard_html_with_template",
654 format!("Failed to render final dashboard: {}", e),
655 )
656 })?,
657 DashboardTemplate::Unified => renderer.render_unified_dashboard(&context).map_err(|e| {
658 MemScopeError::error(
659 "export",
660 "export_dashboard_html_with_template",
661 format!("Failed to render dashboard: {}", e),
662 )
663 })?,
664 };
665
666 let output_file = path_ref.join(format!("{}_dashboard.html", template));
668 std::fs::write(&output_file, html_content).map_err(|e| {
669 MemScopeError::error(
670 "export",
671 "export_dashboard_html_with_template",
672 format!("Failed to write HTML file: {}", e),
673 )
674 })?;
675
676 tracing::info!("✅ Dashboard HTML exported to: {:?}", output_file);
677
678 Ok(())
679}
680
681pub fn export_memory_passports_json<P: AsRef<Path>>(
682 base_path: P,
683 passport_tracker: &Arc<MemoryPassportTracker>,
684) -> MemScopeResult<()> {
685 let base_path = base_path.as_ref();
686 let passports = passport_tracker.get_all_passports();
687
688 let passport_data: Vec<_> = passports
689 .values()
690 .map(|p| {
691 serde_json::json!({
692 "passport_id": p.passport_id,
693 "allocation_ptr": format!("0x{:x}", p.allocation_ptr),
694 "size_bytes": p.size_bytes,
695 "created_at": p.created_at,
696 "lifecycle_events": p.lifecycle_events.len(),
697 "status": format!("{:?}", p.status_at_shutdown),
698 })
699 })
700 .collect();
701
702 let json_data = serde_json::json!({
703 "metadata": {
704 "export_version": "2.0",
705 "specification": "memory passport tracking",
706 "total_passports": passports.len()
707 },
708 "memory_passports": passport_data,
709 });
710
711 let file_path = base_path.join("memory_passports.json");
712 let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
713 MemScopeError::error("export", "export_memory_passports_json", e.to_string())
714 })?;
715 std::fs::write(&file_path, json_string).map_err(|e| {
716 MemScopeError::error("export", "export_memory_passports_json", e.to_string())
717 })?;
718
719 Ok(())
720}
721
722pub fn export_leak_detection_json<P: AsRef<Path>>(
723 base_path: P,
724 passport_tracker: &Arc<MemoryPassportTracker>,
725) -> MemScopeResult<()> {
726 let base_path = base_path.as_ref();
727 let leak_result = passport_tracker.detect_leaks_at_shutdown();
728
729 let leak_details: Vec<_> = leak_result
730 .leak_details
731 .iter()
732 .map(|detail| {
733 serde_json::json!({
734 "passport_id": detail.passport_id,
735 "memory_address": format!("0x{:x}", detail.memory_address),
736 "size_bytes": detail.size_bytes,
737 "lifecycle_summary": detail.lifecycle_summary,
738 })
739 })
740 .collect();
741
742 let json_data = serde_json::json!({
743 "metadata": {
744 "export_version": "2.0",
745 "specification": "leak detection",
746 "leaks_detected": leak_result.total_leaks
747 },
748 "leak_detection": {
749 "total_leaks": leak_result.total_leaks,
750 "leak_details": leak_details
751 }
752 });
753
754 let file_path = base_path.join("leak_detection.json");
755 let json_string = serde_json::to_string_pretty(&json_data)
756 .map_err(|e| MemScopeError::error("export", "export_leak_detection_json", e.to_string()))?;
757 std::fs::write(&file_path, json_string)
758 .map_err(|e| MemScopeError::error("export", "export_leak_detection_json", e.to_string()))?;
759
760 Ok(())
761}
762
763pub fn export_unsafe_ffi_json<P: AsRef<Path>>(
764 base_path: P,
765 passport_tracker: &Arc<MemoryPassportTracker>,
766) -> MemScopeResult<()> {
767 use crate::analysis::memory_passport_tracker::PassportStatus;
768
769 let base_path = base_path.as_ref();
770 let passports = passport_tracker.get_all_passports();
771
772 let ffi_reports: Vec<_> = passports
773 .values()
774 .filter(|p| {
775 matches!(
776 p.status_at_shutdown,
777 PassportStatus::HandoverToFfi
778 | PassportStatus::InForeignCustody
779 | PassportStatus::FreedByForeign
780 )
781 })
782 .map(|p| {
783 serde_json::json!({
784 "passport_id": p.passport_id,
785 "allocation_ptr": format!("0x{:x}", p.allocation_ptr),
786 "size_bytes": p.size_bytes,
787 "status": format!("{:?}", p.status_at_shutdown),
788 "created_at": p.created_at,
789 "boundary_events": p.lifecycle_events.iter().map(|e| {
790 serde_json::json!({
791 "timestamp": e.timestamp,
792 "event_type": format!("{:?}", e.event_type),
793 "context": e.context,
794 })
795 }).collect::<Vec<_>>(),
796 })
797 })
798 .collect();
799
800 let json_data = serde_json::json!({
801 "metadata": {
802 "export_version": "2.0",
803 "specification": "unsafe FFI tracking",
804 "total_ffi_reports": ffi_reports.len(),
805 "total_memory_passports": passports.len()
806 },
807 "unsafe_reports": ffi_reports,
808 "memory_passports": passports.len()
809 });
810
811 let file_path = base_path.join("unsafe_ffi.json");
812 let json_string = serde_json::to_string_pretty(&json_data)
813 .map_err(|e| MemScopeError::error("export", "export_unsafe_ffi_json", e.to_string()))?;
814 std::fs::write(&file_path, json_string)
815 .map_err(|e| MemScopeError::error("export", "export_unsafe_ffi_json", e.to_string()))?;
816
817 Ok(())
818}
819
820pub fn export_system_resources_json<P: AsRef<Path>>(base_path: P) -> MemScopeResult<()> {
827 let base_path = base_path.as_ref();
828
829 let mut memory_info = PlatformMemoryInfo::new();
831 let _ = memory_info.initialize();
832
833 let memory_stats = match memory_info.collect_stats() {
834 Ok(stats) => stats,
835 Err(e) => {
836 eprintln!("Warning: Failed to collect memory stats: {}", e);
837 return Err(MemScopeError::error(
838 "export",
839 "export_system_resources_json",
840 e.to_string(),
841 ));
842 }
843 };
844
845 let system_info = match memory_info.get_system_info() {
847 Ok(info) => info,
848 Err(e) => {
849 eprintln!("Warning: Failed to collect system info: {}", e);
850 return Err(MemScopeError::error(
851 "export",
852 "export_system_resources_json",
853 e.to_string(),
854 ));
855 }
856 };
857
858 let json_data = serde_json::json!({
860 "metadata": {
861 "export_version": "2.0",
862 "specification": "system resource monitoring",
863 "timestamp": std::time::SystemTime::now()
864 .duration_since(std::time::UNIX_EPOCH)
865 .unwrap_or_default()
866 .as_secs()
867 },
868 "system_info": {
869 "os_name": system_info.os_name,
870 "os_version": system_info.os_version,
871 "architecture": system_info.architecture,
872 "cpu_cores": system_info.cpu_cores,
873 "page_size": system_info.page_size,
874 "large_page_size": system_info.large_page_size,
875 "cpu_cache": {
876 "l1_cache_size": system_info.cpu_cache.l1_cache_size,
877 "l2_cache_size": system_info.cpu_cache.l2_cache_size,
878 "l3_cache_size": system_info.cpu_cache.l3_cache_size,
879 "cache_line_size": system_info.cpu_cache.cache_line_size
880 },
881 "mmu_info": {
882 "virtual_address_bits": system_info.mmu_info.virtual_address_bits,
883 "physical_address_bits": system_info.mmu_info.physical_address_bits,
884 "aslr_enabled": system_info.mmu_info.aslr_enabled,
885 "nx_bit_supported": system_info.mmu_info.nx_bit_supported
886 }
887 },
888 "memory_stats": {
889 "virtual_memory": {
890 "total_virtual": memory_stats.virtual_memory.total_virtual,
891 "available_virtual": memory_stats.virtual_memory.available_virtual,
892 "used_virtual": memory_stats.virtual_memory.used_virtual,
893 "reserved": memory_stats.virtual_memory.reserved,
894 "committed": memory_stats.virtual_memory.committed
895 },
896 "physical_memory": {
897 "total_physical": memory_stats.physical_memory.total_physical,
898 "available_physical": memory_stats.physical_memory.available_physical,
899 "used_physical": memory_stats.physical_memory.used_physical,
900 "cached": memory_stats.physical_memory.cached,
901 "buffers": memory_stats.physical_memory.buffers,
902 "swap": {
903 "total_swap": memory_stats.physical_memory.swap.total_swap,
904 "used_swap": memory_stats.physical_memory.swap.used_swap,
905 "available_swap": memory_stats.physical_memory.swap.available_swap,
906 "swap_in_rate": memory_stats.physical_memory.swap.swap_in_rate,
907 "swap_out_rate": memory_stats.physical_memory.swap.swap_out_rate
908 }
909 },
910 "process_memory": {
911 "virtual_size": memory_stats.process_memory.virtual_size,
912 "resident_size": memory_stats.process_memory.resident_size,
913 "shared_size": memory_stats.process_memory.shared_size,
914 "private_size": memory_stats.process_memory.private_size,
915 "heap_size": memory_stats.process_memory.heap_size,
916 "stack_size": memory_stats.process_memory.stack_size,
917 "mapped_files": memory_stats.process_memory.mapped_files,
918 "peak_usage": memory_stats.process_memory.peak_usage
919 },
920 "system_memory": {
921 "allocation_count": memory_stats.system_memory.allocation_count,
922 "deallocation_count": memory_stats.system_memory.deallocation_count,
923 "active_allocations": memory_stats.system_memory.active_allocations,
924 "total_allocated": memory_stats.system_memory.total_allocated,
925 "total_deallocated": memory_stats.system_memory.total_deallocated,
926 "fragmentation_level": memory_stats.system_memory.fragmentation_level,
927 "large_pages": {
928 "supported": memory_stats.system_memory.large_pages.supported,
929 "total_large_pages": memory_stats.system_memory.large_pages.total_large_pages,
930 "used_large_pages": memory_stats.system_memory.large_pages.used_large_pages,
931 "page_size": memory_stats.system_memory.large_pages.page_size
932 }
933 },
934 "pressure_indicators": {
935 "pressure_level": format!("{:?}", memory_stats.pressure_indicators.pressure_level),
936 "low_memory": memory_stats.pressure_indicators.low_memory,
937 "swapping_active": memory_stats.pressure_indicators.swapping_active,
938 "allocation_failure_rate": memory_stats.pressure_indicators.allocation_failure_rate,
939 "gc_pressure": memory_stats.pressure_indicators.gc_pressure
940 }
941 }
942 });
943
944 let file_path = base_path.join("system_resources.json");
945 let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
946 MemScopeError::error("export", "export_system_resources_json", e.to_string())
947 })?;
948 std::fs::write(&file_path, json_string).map_err(|e| {
949 MemScopeError::error("export", "export_system_resources_json", e.to_string())
950 })?;
951
952 Ok(())
953}
954
955pub fn export_ownership_graph_json<P: AsRef<Path>>(
963 base_path: P,
964 allocations: &[crate::capture::types::AllocationInfo],
965) -> MemScopeResult<()> {
966 let base_path = base_path.as_ref();
967
968 let graph = build_ownership_graph_from_allocations(allocations);
970
971 let diagnostics = graph.diagnostics(50);
973
974 let nodes_json: Vec<_> = graph
976 .nodes
977 .iter()
978 .map(|node| {
979 json!({
980 "id": format!("0x{:x}", node.id.0),
981 "type_name": node.type_name,
982 "size": node.size,
983 })
984 })
985 .collect();
986
987 let edges_json: Vec<_> = graph
989 .edges
990 .iter()
991 .map(|edge| {
992 json!({
993 "from": format!("0x{:x}", edge.from.0),
994 "to": format!("0x{:x}", edge.to.0),
995 "kind": match edge.op {
996 EdgeKind::Owns => "Owns",
997 EdgeKind::Borrows => "Borrows",
998 EdgeKind::RcClone => "RcClone",
999 EdgeKind::ArcClone => "ArcClone",
1000 },
1001 })
1002 })
1003 .collect();
1004
1005 let cycles_json: Vec<_> = graph
1007 .cycles
1008 .iter()
1009 .map(|cycle| {
1010 let nodes: Vec<_> = cycle.iter().map(|id| format!("0x{:x}", id.0)).collect();
1011 json!({
1012 "nodes": nodes,
1013 })
1014 })
1015 .collect();
1016
1017 let issues_json: Vec<_> = diagnostics
1019 .issues
1020 .iter()
1021 .map(|issue| match issue {
1022 crate::analysis::ownership_graph::DiagnosticIssue::RcCycle { nodes, cycle_type } => {
1023 json!({
1024 "type": "RcCycle",
1025 "cycle_type": format!("{:?}", cycle_type),
1026 "nodes": nodes.iter().map(|id| format!("0x{:x}", id.0)).collect::<Vec<_>>(),
1027 "severity": "error",
1028 })
1029 }
1030 crate::analysis::ownership_graph::DiagnosticIssue::ArcCloneStorm {
1031 clone_count,
1032 threshold,
1033 } => {
1034 json!({
1035 "type": "ArcCloneStorm",
1036 "clone_count": clone_count,
1037 "threshold": threshold,
1038 "severity": "warning",
1039 })
1040 }
1041 })
1042 .collect();
1043
1044 let root_cause_json = graph.find_root_cause().map(|rc| {
1046 json!({
1047 "cause": match rc.root_cause {
1048 crate::analysis::ownership_graph::RootCause::ArcCloneStorm => "ArcCloneStorm",
1049 crate::analysis::ownership_graph::RootCause::RcCycle => "RcCycle",
1050 },
1051 "description": rc.description,
1052 "impact": rc.impact,
1053 })
1054 });
1055
1056 let json_data = json!({
1057 "metadata": {
1058 "export_version": "2.0",
1059 "specification": "ownership graph analysis",
1060 "timestamp": std::time::SystemTime::now()
1061 .duration_since(std::time::UNIX_EPOCH)
1062 .unwrap_or_default()
1063 .as_secs()
1064 },
1065 "summary": {
1066 "total_nodes": graph.nodes.len(),
1067 "total_edges": graph.edges.len(),
1068 "total_cycles": graph.cycles.len(),
1069 "rc_clone_count": diagnostics.rc_clone_count,
1070 "arc_clone_count": diagnostics.arc_clone_count,
1071 "has_issues": diagnostics.has_issues(),
1072 },
1073 "nodes": nodes_json,
1074 "edges": edges_json,
1075 "cycles": cycles_json,
1076 "diagnostics": {
1077 "issues": issues_json,
1078 "root_cause": root_cause_json,
1079 },
1080 });
1081
1082 let file_path = base_path.join("ownership_graph.json");
1083 let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
1084 MemScopeError::error("export", "export_ownership_graph_json", e.to_string())
1085 })?;
1086 std::fs::write(&file_path, json_string).map_err(|e| {
1087 MemScopeError::error("export", "export_ownership_graph_json", e.to_string())
1088 })?;
1089
1090 Ok(())
1091}
1092
1093fn build_ownership_graph_from_allocations(
1095 allocations: &[crate::capture::types::AllocationInfo],
1096) -> OwnershipGraph {
1097 use crate::analysis::relation_inference::{Relation, RelationGraphBuilder};
1098
1099 let passports: Vec<(
1101 ObjectId,
1102 String,
1103 usize,
1104 Vec<crate::analysis::ownership_graph::OwnershipEvent>,
1105 )> = allocations
1106 .iter()
1107 .map(|alloc| {
1108 let id = ObjectId::from_ptr(alloc.ptr);
1109 let type_name = alloc
1110 .type_name
1111 .clone()
1112 .unwrap_or_else(|| "unknown".to_string());
1113 let size = alloc.size;
1114
1115 let events = vec![crate::analysis::ownership_graph::OwnershipEvent::new(
1117 alloc.timestamp_alloc,
1118 OwnershipOp::Create,
1119 id,
1120 None,
1121 )];
1122
1123 if type_name.contains("Arc<") || type_name.contains("Rc<") {
1125 }
1129
1130 (id, type_name, size, events)
1131 })
1132 .collect();
1133
1134 let mut graph = OwnershipGraph::build(&passports);
1135
1136 let active_allocations: Vec<ActiveAllocation> = allocations
1138 .iter()
1139 .filter(|a| a.timestamp_dealloc.is_none())
1140 .map(|a| ActiveAllocation {
1141 ptr: a.ptr,
1142 size: a.size,
1143 allocated_at: a.timestamp_alloc,
1144 var_name: a.var_name.clone(),
1145 type_name: a.type_name.clone(),
1146 thread_id: 0,
1147 call_stack_hash: None,
1148 })
1149 .collect();
1150
1151 let relation_graph = RelationGraphBuilder::build(&active_allocations, None);
1152
1153 for edge in &relation_graph.edges {
1155 let from_id = ObjectId(edge.from as u64);
1156 let to_id = ObjectId(edge.to as u64);
1157
1158 let edge_kind = match edge.relation {
1159 Relation::Owner => EdgeKind::Owns,
1160 Relation::Slice => EdgeKind::Borrows,
1161 Relation::Clone => EdgeKind::RcClone,
1162 Relation::Shared => EdgeKind::ArcClone,
1163 };
1164
1165 graph.edges.push(crate::analysis::ownership_graph::Edge {
1166 from: from_id,
1167 to: to_id,
1168 op: edge_kind,
1169 });
1170 }
1171
1172 graph
1173}