memscope_rs/export/
lifecycle_exporter.rs

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        // Write start of JSON array
126        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        // Write end of JSON array and metadata
152        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        // Skip system allocations if configured
179        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        // Add allocation event
202        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        // Additional events from allocation history can be added here
216        // This would be populated from the allocation's history if available
217
218        // Add deallocation event if applicable
219        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
247/// Convenience function for one-shot lifecycle data export
248///
249/// # Arguments
250/// * `allocations` - Slice of allocation info to export
251/// * `output_path` - Path where to save the exported JSON file
252/// * `config` - Optional configuration for the export
253///
254/// # Example
255/// ```no_run
256/// use memscope_rs::export::{export_lifecycle_data, LifecycleExportConfig};
257/// use memscope_rs::core::types::AllocationInfo;
258///
259/// let allocations = vec![]; // Your allocations here
260/// let config = LifecycleExportConfig {
261///     include_system_allocations: false,
262///     pretty_print: true,
263///     batch_size: 1000,
264/// };
265///
266/// export_lifecycle_data(&allocations, "lifecycle.json", Some(config)).unwrap();
267/// ```
268pub 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        // Verify initial state
338        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); // Should still have metadata
365
366        // Verify file content
367        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        // Verify file content
396        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        // Verify file content contains both allocations
437        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            // User allocation (should be included)
458            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            // System allocation (should be excluded)
467            create_test_allocation(
468                0x2000,
469                128,
470                Some("SystemAlloc".to_string()),
471                None, // No var_name indicates system allocation
472                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); // Only user allocation
482
483        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); // Both allocations
524    }
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        // Should contain allocation and deallocation events
549        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        // Test reclaimed status (has deallocation timestamp)
560        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        // Test leaked status (no deallocation timestamp)
572        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, // Small batch size for testing
593        };
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, // Compact format
638            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        // Compact format should not have extra whitespace
657        assert!(!content.contains("  ")); // No double spaces
658        assert!(!content.contains("\n  ")); // No indented newlines
659    }
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        // Test with custom config
676        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        // Test with default config (None)
705        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        // Verify metadata fields
749        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, // No type name
768            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}