1use crate::core::types::AllocationInfo;
2use serde::{Deserialize, Serialize};
3use std::fs::File;
4use std::io::{BufWriter, Write};
5use std::path::Path;
6use std::sync::atomic::{AtomicUsize, Ordering};
7use std::time::{Duration, Instant};
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum LifecycleExportError {
12 #[error("I/O error: {0}")]
13 Io(#[from] std::io::Error),
14
15 #[error("Serialization error: {0}")]
16 Serialization(#[from] serde_json::Error),
17
18 #[error("Export error: {0}")]
19 Other(String),
20}
21
22pub type Result<T> = std::result::Result<T, LifecycleExportError>;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum OwnershipEventType {
26 Allocated,
27 Cloned,
28 Dropped,
29 OwnershipTransferred,
30 Borrowed,
31 BorrowReleased,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(tag = "type")]
36pub enum OwnershipEventDetails {
37 Allocated { size: usize, type_name: String },
38 Cloned { source_ptr: usize },
39 Dropped { reason: String },
40 OwnershipTransferred { new_owner: String },
41 Borrowed { is_mutable: bool, scope: String },
42 BorrowReleased { is_mutable: bool },
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct OwnershipEvent {
47 pub timestamp: u64,
48 pub event_type: OwnershipEventType,
49 pub source_stack_id: u64,
50 pub details: OwnershipEventDetails,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum ShutdownStatus {
55 Reclaimed,
56 FreedByForeign,
57 InForeignCustody,
58 Leaked,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ObjectLifecycle {
63 pub allocation_ptr: usize,
64 pub size_bytes: usize,
65 pub type_name: String,
66 pub var_name: Option<String>,
67 pub ownership_history: Vec<OwnershipEvent>,
68 pub status_at_shutdown: ShutdownStatus,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ExportMetadata {
73 pub timestamp: String,
74 pub version: String,
75 pub total_objects: usize,
76 pub export_duration_ms: u64,
77}
78
79#[derive(Debug, Clone)]
80pub struct LifecycleExportConfig {
81 pub include_system_allocations: bool,
82 pub pretty_print: bool,
83 pub batch_size: usize,
84}
85
86impl Default for LifecycleExportConfig {
87 fn default() -> Self {
88 Self {
89 include_system_allocations: false,
90 pretty_print: true,
91 batch_size: 1000,
92 }
93 }
94}
95
96#[derive(Debug)]
97pub struct ExportStats {
98 pub objects_exported: usize,
99 pub processing_time: Duration,
100 pub output_size: u64,
101}
102
103pub struct LifecycleExporter {
104 config: LifecycleExportConfig,
105 next_stack_id: AtomicUsize,
106}
107
108impl LifecycleExporter {
109 pub fn new(config: LifecycleExportConfig) -> Self {
110 Self {
111 config,
112 next_stack_id: AtomicUsize::new(1),
113 }
114 }
115
116 pub fn export_lifecycle_data<P: AsRef<Path>>(
117 &self,
118 allocations: &[AllocationInfo],
119 output_path: P,
120 ) -> Result<ExportStats> {
121 let start_time = Instant::now();
122 let output_file = File::create(&output_path)?;
123 let mut writer = BufWriter::new(output_file);
124
125 writer.write_all(b"{\"objects\":[")?;
127
128 let mut first = true;
129 let mut objects_exported = 0;
130
131 for chunk in allocations.chunks(self.config.batch_size) {
132 for alloc in chunk {
133 if let Some(lifecycle) = self.build_object_lifecycle(alloc)? {
134 if !first {
135 writer.write_all(b",")?;
136 }
137 first = false;
138
139 let json = if self.config.pretty_print {
140 serde_json::to_vec_pretty(&lifecycle)?
141 } else {
142 serde_json::to_vec(&lifecycle)?
143 };
144
145 writer.write_all(&json)?;
146 objects_exported += 1;
147 }
148 }
149 }
150
151 let export_duration = start_time.elapsed();
153 let metadata = ExportMetadata {
154 timestamp: chrono::Utc::now().to_rfc3339(),
155 version: env!("CARGO_PKG_VERSION").to_string(),
156 total_objects: objects_exported,
157 export_duration_ms: export_duration.as_millis() as u64,
158 };
159
160 let metadata_json = if self.config.pretty_print {
161 serde_json::to_string_pretty(&metadata)?
162 } else {
163 serde_json::to_string(&metadata)?
164 };
165
166 write!(writer, "],\"metadata\":{metadata_json}}}")?;
167
168 writer.flush()?;
169
170 Ok(ExportStats {
171 objects_exported,
172 processing_time: export_duration,
173 output_size: output_path.as_ref().metadata()?.len(),
174 })
175 }
176
177 fn build_object_lifecycle(&self, alloc: &AllocationInfo) -> Result<Option<ObjectLifecycle>> {
178 if !self.config.include_system_allocations && alloc.var_name.is_none() {
180 return Ok(None);
181 }
182
183 let ownership_history = self.build_ownership_history(alloc)?;
184
185 Ok(Some(ObjectLifecycle {
186 allocation_ptr: alloc.ptr,
187 size_bytes: alloc.size,
188 type_name: alloc
189 .type_name
190 .clone()
191 .unwrap_or_else(|| "unknown".to_string()),
192 var_name: alloc.var_name.clone(),
193 ownership_history,
194 status_at_shutdown: self.determine_shutdown_status(alloc),
195 }))
196 }
197
198 fn build_ownership_history(&self, alloc: &AllocationInfo) -> Result<Vec<OwnershipEvent>> {
199 let mut events = Vec::new();
200
201 events.push(OwnershipEvent {
203 timestamp: alloc.timestamp_alloc,
204 event_type: OwnershipEventType::Allocated,
205 source_stack_id: self.next_stack_id(),
206 details: OwnershipEventDetails::Allocated {
207 size: alloc.size,
208 type_name: alloc
209 .type_name
210 .clone()
211 .unwrap_or_else(|| "unknown".to_string()),
212 },
213 });
214
215 if let Some(dealloc_time) = alloc.timestamp_dealloc {
220 events.push(OwnershipEvent {
221 timestamp: dealloc_time,
222 event_type: OwnershipEventType::Dropped,
223 source_stack_id: self.next_stack_id(),
224 details: OwnershipEventDetails::Dropped {
225 reason: "Deallocation".to_string(),
226 },
227 });
228 }
229
230 Ok(events)
231 }
232
233 fn determine_shutdown_status(&self, alloc: &AllocationInfo) -> ShutdownStatus {
234 let is_leaked = alloc.timestamp_dealloc.is_none();
235 match (alloc.timestamp_dealloc, is_leaked) {
236 (Some(_), _) => ShutdownStatus::Reclaimed,
237 (None, true) => ShutdownStatus::Leaked,
238 (None, false) => ShutdownStatus::InForeignCustody,
239 }
240 }
241
242 fn next_stack_id(&self) -> u64 {
243 self.next_stack_id.fetch_add(1, Ordering::SeqCst) as u64
244 }
245}
246
247pub fn export_lifecycle_data<P: AsRef<Path>>(
269 allocations: &[AllocationInfo],
270 output_path: P,
271 config: Option<LifecycleExportConfig>,
272) -> Result<ExportStats> {
273 let config = config.unwrap_or_default();
274 let exporter = LifecycleExporter::new(config);
275 exporter.export_lifecycle_data(allocations, output_path)
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::core::types::AllocationInfo;
282 use std::fs;
283 use std::path::PathBuf;
284 use tempfile::TempDir;
285
286 fn create_test_allocation(
287 ptr: usize,
288 size: usize,
289 type_name: Option<String>,
290 var_name: Option<String>,
291 timestamp_alloc: u64,
292 timestamp_dealloc: Option<u64>,
293 ) -> AllocationInfo {
294 AllocationInfo {
295 ptr,
296 size,
297 var_name,
298 type_name,
299 scope_name: None,
300 timestamp_alloc,
301 timestamp_dealloc,
302 thread_id: "test_thread".to_string(),
303 borrow_count: 0,
304 stack_trace: None,
305 is_leaked: timestamp_dealloc.is_none(),
306 lifetime_ms: timestamp_dealloc.map(|dealloc| dealloc.saturating_sub(timestamp_alloc)),
307 borrow_info: None,
308 clone_info: None,
309 ownership_history_available: false,
310 smart_pointer_info: None,
311 memory_layout: None,
312 generic_info: None,
313 dynamic_type_info: None,
314 runtime_state: None,
315 stack_allocation: None,
316 temporary_object: None,
317 fragmentation_analysis: None,
318 generic_instantiation: None,
319 type_relationships: None,
320 type_usage: None,
321 function_call_tracking: None,
322 lifecycle_tracking: None,
323 access_tracking: None,
324 drop_chain_analysis: None,
325 }
326 }
327
328 fn create_temp_file_path(temp_dir: &TempDir, filename: &str) -> PathBuf {
329 temp_dir.path().join(filename)
330 }
331
332 #[test]
333 fn test_lifecycle_exporter_creation() {
334 let config = LifecycleExportConfig::default();
335 let exporter = LifecycleExporter::new(config);
336
337 assert_eq!(exporter.next_stack_id.load(Ordering::SeqCst), 1);
339 }
340
341 #[test]
342 fn test_default_config() {
343 let config = LifecycleExportConfig::default();
344
345 assert!(!config.include_system_allocations);
346 assert!(config.pretty_print);
347 assert_eq!(config.batch_size, 1000);
348 }
349
350 #[test]
351 fn test_export_empty_allocations() {
352 let temp_dir = TempDir::new().expect("Failed to create temp directory");
353 let output_path = create_temp_file_path(&temp_dir, "empty_lifecycle.json");
354
355 let config = LifecycleExportConfig::default();
356 let exporter = LifecycleExporter::new(config);
357
358 let allocations = vec![];
359 let result = exporter.export_lifecycle_data(&allocations, &output_path);
360
361 assert!(result.is_ok());
362 let stats = result.unwrap();
363 assert_eq!(stats.objects_exported, 0);
364 assert!(stats.output_size > 0); let content = fs::read_to_string(&output_path).expect("Failed to read output file");
368 assert!(content.contains("\"objects\":[]"));
369 assert!(content.contains("\"metadata\""));
370 }
371
372 #[test]
373 fn test_export_single_allocation() {
374 let temp_dir = TempDir::new().expect("Failed to create temp directory");
375 let output_path = create_temp_file_path(&temp_dir, "single_lifecycle.json");
376
377 let config = LifecycleExportConfig::default();
378 let exporter = LifecycleExporter::new(config);
379
380 let allocations = vec![create_test_allocation(
381 0x1000,
382 64,
383 Some("String".to_string()),
384 Some("test_var".to_string()),
385 1000,
386 Some(2000),
387 )];
388
389 let result = exporter.export_lifecycle_data(&allocations, &output_path);
390
391 assert!(result.is_ok());
392 let stats = result.unwrap();
393 assert_eq!(stats.objects_exported, 1);
394
395 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
397 assert!(content.contains("\"allocation_ptr\": 4096"));
398 assert!(content.contains("\"size_bytes\": 64"));
399 assert!(content.contains("\"type_name\": \"String\""));
400 assert!(content.contains("\"var_name\": \"test_var\""));
401 }
402
403 #[test]
404 fn test_export_multiple_allocations() {
405 let temp_dir = TempDir::new().expect("Failed to create temp directory");
406 let output_path = create_temp_file_path(&temp_dir, "multiple_lifecycle.json");
407
408 let config = LifecycleExportConfig::default();
409 let exporter = LifecycleExporter::new(config);
410
411 let allocations = vec![
412 create_test_allocation(
413 0x1000,
414 64,
415 Some("String".to_string()),
416 Some("var1".to_string()),
417 1000,
418 Some(2000),
419 ),
420 create_test_allocation(
421 0x2000,
422 128,
423 Some("Vec<i32>".to_string()),
424 Some("var2".to_string()),
425 1500,
426 None,
427 ),
428 ];
429
430 let result = exporter.export_lifecycle_data(&allocations, &output_path);
431
432 assert!(result.is_ok());
433 let stats = result.unwrap();
434 assert_eq!(stats.objects_exported, 2);
435
436 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
438 assert!(content.contains("\"allocation_ptr\": 4096"));
439 assert!(content.contains("\"allocation_ptr\": 8192"));
440 assert!(content.contains("\"var1\""));
441 assert!(content.contains("\"var2\""));
442 }
443
444 #[test]
445 fn test_system_allocations_filtering() {
446 let temp_dir = TempDir::new().expect("Failed to create temp directory");
447 let output_path = create_temp_file_path(&temp_dir, "filtered_lifecycle.json");
448
449 let config = LifecycleExportConfig {
450 include_system_allocations: false,
451 pretty_print: true,
452 batch_size: 1000,
453 };
454 let exporter = LifecycleExporter::new(config);
455
456 let allocations = vec![
457 create_test_allocation(
459 0x1000,
460 64,
461 Some("String".to_string()),
462 Some("user_var".to_string()),
463 1000,
464 Some(2000),
465 ),
466 create_test_allocation(
468 0x2000,
469 128,
470 Some("SystemAlloc".to_string()),
471 None, 1500,
473 None,
474 ),
475 ];
476
477 let result = exporter.export_lifecycle_data(&allocations, &output_path);
478
479 assert!(result.is_ok());
480 let stats = result.unwrap();
481 assert_eq!(stats.objects_exported, 1); let content = fs::read_to_string(&output_path).expect("Failed to read output file");
484 assert!(content.contains("\"user_var\""));
485 assert!(!content.contains("\"SystemAlloc\""));
486 }
487
488 #[test]
489 fn test_include_system_allocations() {
490 let temp_dir = TempDir::new().expect("Failed to create temp directory");
491 let output_path = create_temp_file_path(&temp_dir, "with_system_lifecycle.json");
492
493 let config = LifecycleExportConfig {
494 include_system_allocations: true,
495 pretty_print: true,
496 batch_size: 1000,
497 };
498 let exporter = LifecycleExporter::new(config);
499
500 let allocations = vec![
501 create_test_allocation(
502 0x1000,
503 64,
504 Some("String".to_string()),
505 Some("user_var".to_string()),
506 1000,
507 Some(2000),
508 ),
509 create_test_allocation(
510 0x2000,
511 128,
512 Some("SystemAlloc".to_string()),
513 None,
514 1500,
515 None,
516 ),
517 ];
518
519 let result = exporter.export_lifecycle_data(&allocations, &output_path);
520
521 assert!(result.is_ok());
522 let stats = result.unwrap();
523 assert_eq!(stats.objects_exported, 2); }
525
526 #[test]
527 fn test_ownership_event_types() {
528 let temp_dir = TempDir::new().expect("Failed to create temp directory");
529 let output_path = create_temp_file_path(&temp_dir, "events_lifecycle.json");
530
531 let config = LifecycleExportConfig::default();
532 let exporter = LifecycleExporter::new(config);
533
534 let allocations = vec![create_test_allocation(
535 0x1000,
536 64,
537 Some("String".to_string()),
538 Some("test_var".to_string()),
539 1000,
540 Some(2000),
541 )];
542
543 let result = exporter.export_lifecycle_data(&allocations, &output_path);
544 assert!(result.is_ok());
545
546 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
547
548 assert!(content.contains("\"Allocated\""));
550 assert!(content.contains("\"Dropped\""));
551 assert!(content.contains("\"ownership_history\""));
552 }
553
554 #[test]
555 fn test_shutdown_status_determination() {
556 let config = LifecycleExportConfig::default();
557 let exporter = LifecycleExporter::new(config);
558
559 let reclaimed_alloc = create_test_allocation(
561 0x1000,
562 64,
563 Some("String".to_string()),
564 Some("var1".to_string()),
565 1000,
566 Some(2000),
567 );
568 let status = exporter.determine_shutdown_status(&reclaimed_alloc);
569 assert!(matches!(status, ShutdownStatus::Reclaimed));
570
571 let leaked_alloc = create_test_allocation(
573 0x2000,
574 128,
575 Some("Vec<i32>".to_string()),
576 Some("var2".to_string()),
577 1500,
578 None,
579 );
580 let status = exporter.determine_shutdown_status(&leaked_alloc);
581 assert!(matches!(status, ShutdownStatus::Leaked));
582 }
583
584 #[test]
585 fn test_batch_processing() {
586 let temp_dir = TempDir::new().expect("Failed to create temp directory");
587 let output_path = create_temp_file_path(&temp_dir, "batch_lifecycle.json");
588
589 let config = LifecycleExportConfig {
590 include_system_allocations: true,
591 pretty_print: false,
592 batch_size: 2, };
594 let exporter = LifecycleExporter::new(config);
595
596 let allocations = vec![
597 create_test_allocation(
598 0x1000,
599 64,
600 Some("String".to_string()),
601 Some("var1".to_string()),
602 1000,
603 Some(2000),
604 ),
605 create_test_allocation(
606 0x2000,
607 128,
608 Some("Vec<i32>".to_string()),
609 Some("var2".to_string()),
610 1500,
611 None,
612 ),
613 create_test_allocation(
614 0x3000,
615 256,
616 Some("HashMap".to_string()),
617 Some("var3".to_string()),
618 2000,
619 Some(3000),
620 ),
621 ];
622
623 let result = exporter.export_lifecycle_data(&allocations, &output_path);
624
625 assert!(result.is_ok());
626 let stats = result.unwrap();
627 assert_eq!(stats.objects_exported, 3);
628 }
629
630 #[test]
631 fn test_compact_output_format() {
632 let temp_dir = TempDir::new().expect("Failed to create temp directory");
633 let output_path = create_temp_file_path(&temp_dir, "compact_lifecycle.json");
634
635 let config = LifecycleExportConfig {
636 include_system_allocations: true,
637 pretty_print: false, batch_size: 1000,
639 };
640 let exporter = LifecycleExporter::new(config);
641
642 let allocations = vec![create_test_allocation(
643 0x1000,
644 64,
645 Some("String".to_string()),
646 Some("test_var".to_string()),
647 1000,
648 Some(2000),
649 )];
650
651 let result = exporter.export_lifecycle_data(&allocations, &output_path);
652 assert!(result.is_ok());
653
654 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
655
656 assert!(!content.contains(" ")); assert!(!content.contains("\n ")); }
660
661 #[test]
662 fn test_convenience_function() {
663 let temp_dir = TempDir::new().expect("Failed to create temp directory");
664 let output_path = create_temp_file_path(&temp_dir, "convenience_lifecycle.json");
665
666 let allocations = vec![create_test_allocation(
667 0x1000,
668 64,
669 Some("String".to_string()),
670 Some("test_var".to_string()),
671 1000,
672 Some(2000),
673 )];
674
675 let config = LifecycleExportConfig {
677 include_system_allocations: true,
678 pretty_print: false,
679 batch_size: 500,
680 };
681
682 let result = export_lifecycle_data(&allocations, &output_path, Some(config));
683 assert!(result.is_ok());
684
685 let stats = result.unwrap();
686 assert_eq!(stats.objects_exported, 1);
687 assert!(stats.output_size > 0);
688 }
689
690 #[test]
691 fn test_convenience_function_default_config() {
692 let temp_dir = TempDir::new().expect("Failed to create temp directory");
693 let output_path = create_temp_file_path(&temp_dir, "convenience_default_lifecycle.json");
694
695 let allocations = vec![create_test_allocation(
696 0x1000,
697 64,
698 Some("String".to_string()),
699 Some("test_var".to_string()),
700 1000,
701 Some(2000),
702 )];
703
704 let result = export_lifecycle_data(&allocations, &output_path, None);
706 assert!(result.is_ok());
707
708 let stats = result.unwrap();
709 assert_eq!(stats.objects_exported, 1);
710 }
711
712 #[test]
713 fn test_stack_id_generation() {
714 let config = LifecycleExportConfig::default();
715 let exporter = LifecycleExporter::new(config);
716
717 let id1 = exporter.next_stack_id();
718 let id2 = exporter.next_stack_id();
719 let id3 = exporter.next_stack_id();
720
721 assert_eq!(id1, 1);
722 assert_eq!(id2, 2);
723 assert_eq!(id3, 3);
724 }
725
726 #[test]
727 fn test_metadata_generation() {
728 let temp_dir = TempDir::new().expect("Failed to create temp directory");
729 let output_path = create_temp_file_path(&temp_dir, "metadata_lifecycle.json");
730
731 let config = LifecycleExportConfig::default();
732 let exporter = LifecycleExporter::new(config);
733
734 let allocations = vec![create_test_allocation(
735 0x1000,
736 64,
737 Some("String".to_string()),
738 Some("test_var".to_string()),
739 1000,
740 Some(2000),
741 )];
742
743 let result = exporter.export_lifecycle_data(&allocations, &output_path);
744 assert!(result.is_ok());
745
746 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
747
748 assert!(content.contains("\"metadata\""));
750 assert!(content.contains("\"timestamp\""));
751 assert!(content.contains("\"version\""));
752 assert!(content.contains("\"total_objects\": 1"));
753 assert!(content.contains("\"export_duration_ms\""));
754 }
755
756 #[test]
757 fn test_unknown_type_handling() {
758 let temp_dir = TempDir::new().expect("Failed to create temp directory");
759 let output_path = create_temp_file_path(&temp_dir, "unknown_type_lifecycle.json");
760
761 let config = LifecycleExportConfig::default();
762 let exporter = LifecycleExporter::new(config);
763
764 let allocations = vec![create_test_allocation(
765 0x1000,
766 64,
767 None, Some("test_var".to_string()),
769 1000,
770 Some(2000),
771 )];
772
773 let result = exporter.export_lifecycle_data(&allocations, &output_path);
774 assert!(result.is_ok());
775
776 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
777 assert!(content.contains("\"type_name\": \"unknown\""));
778 }
779}