memscope_rs/core/
lifecycle_summary.rs

1//! Lifecycle summary generation for enhanced memory analysis
2//!
3//! This module provides functionality to generate high-level lifecycle summaries
4//! from detailed ownership history, creating the data needed for lifetime.json export.
5
6use crate::core::ownership_history::{
7    BorrowInfo, CloneInfo, OwnershipHistoryRecorder, OwnershipSummary,
8};
9use crate::core::types::AllocationInfo;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Generator for lifecycle summaries and lifetime.json export
14pub struct LifecycleSummaryGenerator {
15    /// Configuration for summary generation
16    config: SummaryConfig,
17}
18
19/// Configuration for lifecycle summary generation
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SummaryConfig {
22    /// Include detailed borrow information
23    pub include_borrow_details: bool,
24    /// Include clone relationship information
25    pub include_clone_details: bool,
26    /// Minimum lifetime threshold for inclusion (in milliseconds)
27    pub min_lifetime_threshold_ms: u64,
28    /// Maximum number of lifecycle events to include per allocation
29    pub max_events_per_allocation: usize,
30}
31
32impl Default for SummaryConfig {
33    fn default() -> Self {
34        Self {
35            include_borrow_details: true,
36            include_clone_details: true,
37            min_lifetime_threshold_ms: 0,
38            max_events_per_allocation: 50,
39        }
40    }
41}
42
43/// Complete lifecycle data for export to lifetime.json
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct LifecycleExportData {
46    /// Lifecycle events for each allocation
47    pub lifecycle_events: Vec<LifecycleEventSummary>,
48    /// Variable groups for organization
49    pub variable_groups: Vec<VariableGroup>,
50    /// Count of user variables (those with meaningful names)
51    pub user_variables_count: usize,
52    /// Whether visualization data is ready
53    pub visualization_ready: bool,
54    /// Export metadata
55    pub metadata: ExportMetadata,
56}
57
58/// Summary of lifecycle events for a single allocation
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct LifecycleEventSummary {
61    /// Allocation pointer
62    pub allocation_ptr: usize,
63    /// Variable name (if available)
64    pub var_name: Option<String>,
65    /// Type name
66    pub type_name: Option<String>,
67    /// Size of the allocation
68    pub size: usize,
69    /// Total lifetime in milliseconds
70    pub lifetime_ms: Option<u64>,
71    /// Lifecycle events
72    pub events: Vec<LifecycleEvent>,
73    /// High-level summary
74    pub summary: AllocationLifecycleSummary,
75}
76
77/// Individual lifecycle event
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct LifecycleEvent {
80    /// Event ID
81    pub id: u64,
82    /// Event type
83    pub event_type: String,
84    /// Timestamp when the event occurred
85    pub timestamp: u64,
86    /// Size involved in the event (if applicable)
87    pub size: Option<usize>,
88    /// Additional event details
89    pub details: Option<String>,
90}
91
92/// High-level summary of an allocation's lifecycle
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct AllocationLifecycleSummary {
95    /// Total lifetime in milliseconds
96    pub lifetime_ms: Option<u64>,
97    /// Borrowing information
98    pub borrow_info: BorrowInfo,
99    /// Cloning information
100    pub clone_info: CloneInfo,
101    /// Whether detailed ownership history is available
102    pub ownership_history_available: bool,
103    /// Lifecycle pattern classification
104    pub lifecycle_pattern: LifecyclePattern,
105    /// Memory efficiency score (0.0 to 1.0)
106    pub efficiency_score: f64,
107}
108
109/// Classification of lifecycle patterns
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum LifecyclePattern {
112    /// Short-lived allocation (< 1ms)
113    Ephemeral,
114    /// Short-term allocation (1ms - 100ms)
115    ShortTerm,
116    /// Medium-term allocation (100ms - 10s)
117    MediumTerm,
118    /// Long-term allocation (> 10s)
119    LongTerm,
120    /// Leaked allocation (never deallocated)
121    Leaked,
122    /// Unknown pattern
123    Unknown,
124}
125
126/// Group of related variables
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct VariableGroup {
129    /// Group name
130    pub name: String,
131    /// Variables in this group
132    pub variables: Vec<String>,
133    /// Total memory used by this group
134    pub total_memory: usize,
135    /// Average lifetime of variables in this group
136    pub average_lifetime_ms: f64,
137}
138
139/// Export metadata
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ExportMetadata {
142    /// Export timestamp
143    pub export_timestamp: u64,
144    /// Total allocations analyzed
145    pub total_allocations: usize,
146    /// Total events processed
147    pub total_events: usize,
148    /// Analysis duration in milliseconds
149    pub analysis_duration_ms: u64,
150}
151
152impl LifecycleSummaryGenerator {
153    /// Create a new lifecycle summary generator
154    pub fn new() -> Self {
155        Self::with_config(SummaryConfig::default())
156    }
157
158    /// Create a new lifecycle summary generator with custom configuration
159    pub fn with_config(config: SummaryConfig) -> Self {
160        Self { config }
161    }
162
163    /// Generate complete lifecycle export data
164    pub fn generate_lifecycle_export(
165        &self,
166        ownership_history: &OwnershipHistoryRecorder,
167        allocations: &[AllocationInfo],
168    ) -> LifecycleExportData {
169        let start_time = std::time::Instant::now();
170
171        // Generate lifecycle event summaries
172        let lifecycle_events = self.generate_lifecycle_events(ownership_history, allocations);
173
174        // Generate variable groups
175        let variable_groups = self.generate_variable_groups(&lifecycle_events);
176
177        // Count user variables (those with meaningful names)
178        let user_variables_count = lifecycle_events
179            .iter()
180            .filter(|event| {
181                event
182                    .var_name
183                    .as_ref()
184                    .map(|name| self.is_user_variable(name))
185                    .unwrap_or(false)
186            })
187            .count();
188
189        let analysis_duration = start_time.elapsed().as_millis() as u64;
190
191        LifecycleExportData {
192            lifecycle_events,
193            variable_groups,
194            user_variables_count,
195            visualization_ready: true,
196            metadata: ExportMetadata {
197                export_timestamp: self.get_current_timestamp(),
198                total_allocations: allocations.len(),
199                total_events: ownership_history.get_statistics().total_events,
200                analysis_duration_ms: analysis_duration,
201            },
202        }
203    }
204
205    /// Generate lifecycle event summaries for all allocations
206    fn generate_lifecycle_events(
207        &self,
208        ownership_history: &OwnershipHistoryRecorder,
209        allocations: &[AllocationInfo],
210    ) -> Vec<LifecycleEventSummary> {
211        let mut summaries = Vec::new();
212
213        for allocation in allocations {
214            // Skip allocations below the minimum lifetime threshold
215            if let Some(lifetime_ms) = allocation.lifetime_ms {
216                if lifetime_ms < self.config.min_lifetime_threshold_ms {
217                    continue;
218                }
219            }
220
221            let summary = self.generate_single_lifecycle_summary(ownership_history, allocation);
222            summaries.push(summary);
223        }
224
225        summaries
226    }
227
228    /// Generate lifecycle summary for a single allocation
229    fn generate_single_lifecycle_summary(
230        &self,
231        ownership_history: &OwnershipHistoryRecorder,
232        allocation: &AllocationInfo,
233    ) -> LifecycleEventSummary {
234        let ptr = allocation.ptr;
235
236        // Get ownership summary if available
237        let ownership_summary = ownership_history.get_summary(ptr);
238
239        // Generate lifecycle events
240        let events = if let Some(ownership_events) = ownership_history.get_events(ptr) {
241            ownership_events
242                .iter()
243                .take(self.config.max_events_per_allocation)
244                .map(|event| LifecycleEvent {
245                    id: event.event_id,
246                    event_type: self.format_event_type(&event.event_type),
247                    timestamp: event.timestamp,
248                    size: Some(allocation.size),
249                    details: event.details.context.clone(),
250                })
251                .collect()
252        } else {
253            // Create basic events from allocation info
254            let mut basic_events = vec![LifecycleEvent {
255                id: 1,
256                event_type: "Allocation".to_string(),
257                timestamp: allocation.timestamp_alloc,
258                size: Some(allocation.size),
259                details: Some("Memory allocated".to_string()),
260            }];
261
262            if let Some(dealloc_time) = allocation.timestamp_dealloc {
263                basic_events.push(LifecycleEvent {
264                    id: 2,
265                    event_type: "Deallocation".to_string(),
266                    timestamp: dealloc_time,
267                    size: Some(allocation.size),
268                    details: Some("Memory deallocated".to_string()),
269                });
270            }
271
272            basic_events
273        };
274
275        // Create allocation lifecycle summary
276        let summary = if let Some(ownership_summary) = ownership_summary {
277            AllocationLifecycleSummary {
278                lifetime_ms: allocation.lifetime_ms,
279                borrow_info: ownership_summary.borrow_info.clone(),
280                clone_info: ownership_summary.clone_info.clone(),
281                ownership_history_available: true,
282                lifecycle_pattern: self.classify_lifecycle_pattern(allocation.lifetime_ms),
283                efficiency_score: self.calculate_efficiency_score(allocation, ownership_summary),
284            }
285        } else {
286            AllocationLifecycleSummary {
287                lifetime_ms: allocation.lifetime_ms,
288                borrow_info: BorrowInfo {
289                    immutable_borrows: 0,
290                    mutable_borrows: 0,
291                    max_concurrent_borrows: 0,
292                    last_borrow_timestamp: None,
293                    active_borrows: Vec::new(),
294                },
295                clone_info: CloneInfo {
296                    clone_count: 0,
297                    is_clone: false,
298                    original_ptr: None,
299                    cloned_ptrs: Vec::new(),
300                },
301                ownership_history_available: false,
302                lifecycle_pattern: self.classify_lifecycle_pattern(allocation.lifetime_ms),
303                efficiency_score: 0.5, // Default score
304            }
305        };
306
307        LifecycleEventSummary {
308            allocation_ptr: ptr,
309            var_name: allocation.var_name.clone(),
310            type_name: allocation.type_name.clone(),
311            size: allocation.size,
312            lifetime_ms: allocation.lifetime_ms,
313            events,
314            summary,
315        }
316    }
317
318    /// Format ownership event type for display
319    fn format_event_type(
320        &self,
321        event_type: &crate::core::ownership_history::OwnershipEventType,
322    ) -> String {
323        match event_type {
324            crate::core::ownership_history::OwnershipEventType::Allocated => {
325                "Allocation".to_string()
326            }
327            crate::core::ownership_history::OwnershipEventType::Cloned { .. } => {
328                "Clone".to_string()
329            }
330            crate::core::ownership_history::OwnershipEventType::Dropped => {
331                "Deallocation".to_string()
332            }
333            crate::core::ownership_history::OwnershipEventType::OwnershipTransferred { .. } => {
334                "OwnershipTransfer".to_string()
335            }
336            crate::core::ownership_history::OwnershipEventType::Borrowed { .. } => {
337                "Borrow".to_string()
338            }
339            crate::core::ownership_history::OwnershipEventType::MutablyBorrowed { .. } => {
340                "MutableBorrow".to_string()
341            }
342            crate::core::ownership_history::OwnershipEventType::BorrowReleased { .. } => {
343                "BorrowRelease".to_string()
344            }
345            crate::core::ownership_history::OwnershipEventType::RefCountChanged { .. } => {
346                "RefCountChange".to_string()
347            }
348        }
349    }
350
351    /// Classify the lifecycle pattern based on lifetime
352    fn classify_lifecycle_pattern(&self, lifetime_ms: Option<u64>) -> LifecyclePattern {
353        match lifetime_ms {
354            None => LifecyclePattern::Leaked,
355            Some(0) => LifecyclePattern::Ephemeral,
356            Some(ms) if ms < 1 => LifecyclePattern::Ephemeral,
357            Some(ms) if ms < 100 => LifecyclePattern::ShortTerm,
358            Some(ms) if ms < 10_000 => LifecyclePattern::MediumTerm,
359            Some(_) => LifecyclePattern::LongTerm,
360        }
361    }
362
363    /// Calculate efficiency score for an allocation
364    fn calculate_efficiency_score(
365        &self,
366        allocation: &AllocationInfo,
367        ownership_summary: &OwnershipSummary,
368    ) -> f64 {
369        let mut score: f64 = 0.5; // Base score
370
371        // Bonus for having a meaningful variable name
372        if allocation
373            .var_name
374            .as_ref()
375            .map(|name| self.is_user_variable(name))
376            .unwrap_or(false)
377        {
378            score += 0.1;
379        }
380
381        // Bonus for appropriate lifetime (not too short, not leaked)
382        match self.classify_lifecycle_pattern(allocation.lifetime_ms) {
383            LifecyclePattern::ShortTerm | LifecyclePattern::MediumTerm => score += 0.2,
384            LifecyclePattern::Ephemeral => score -= 0.1,
385            LifecyclePattern::Leaked => score -= 0.3,
386            _ => {}
387        }
388
389        // Penalty for excessive borrowing
390        if ownership_summary.borrow_info.max_concurrent_borrows > 5 {
391            score -= 0.1;
392        }
393
394        // Bonus for being part of a clone chain (indicates reuse)
395        if ownership_summary.clone_info.clone_count > 0 || ownership_summary.clone_info.is_clone {
396            score += 0.1;
397        }
398
399        // Clamp score between 0.0 and 1.0
400        score.clamp(0.0, 1.0)
401    }
402
403    /// Check if a variable name indicates a user-defined variable
404    fn is_user_variable(&self, name: &str) -> bool {
405        // Filter out system-generated names
406        !name.starts_with("primitive_")
407            && !name.starts_with("struct_")
408            && !name.starts_with("collection_")
409            && !name.starts_with("buffer_")
410            && !name.starts_with("system_")
411            && !name.starts_with("fast_tracked")
412            && name != "unknown"
413    }
414
415    /// Generate variable groups for organization
416    fn generate_variable_groups(
417        &self,
418        lifecycle_events: &[LifecycleEventSummary],
419    ) -> Vec<VariableGroup> {
420        let mut groups: HashMap<String, Vec<&LifecycleEventSummary>> = HashMap::new();
421
422        // Group by type name
423        for event in lifecycle_events {
424            if let Some(ref type_name) = event.type_name {
425                let group_name = self.extract_base_type_name(type_name);
426                groups.entry(group_name).or_default().push(event);
427            }
428        }
429
430        // Convert to VariableGroup structs
431        groups
432            .into_iter()
433            .map(|(name, events)| {
434                let variables: Vec<String> =
435                    events.iter().filter_map(|e| e.var_name.clone()).collect();
436
437                let total_memory: usize = events.iter().map(|e| e.size).sum();
438
439                let average_lifetime_ms = if !events.is_empty() {
440                    let total_lifetime: u64 = events.iter().filter_map(|e| e.lifetime_ms).sum();
441                    let count = events.iter().filter(|e| e.lifetime_ms.is_some()).count();
442                    if count > 0 {
443                        total_lifetime as f64 / count as f64
444                    } else {
445                        0.0
446                    }
447                } else {
448                    0.0
449                };
450
451                VariableGroup {
452                    name,
453                    variables,
454                    total_memory,
455                    average_lifetime_ms,
456                }
457            })
458            .collect()
459    }
460
461    /// Extract base type name for grouping
462    fn extract_base_type_name(&self, type_name: &str) -> String {
463        // Extract the base type from complex type names
464        if let Some(pos) = type_name.find('<') {
465            type_name[..pos].to_string()
466        } else if let Some(pos) = type_name.rfind("::") {
467            type_name[pos + 2..].to_string()
468        } else {
469            type_name.to_string()
470        }
471    }
472
473    /// Get current timestamp in nanoseconds
474    fn get_current_timestamp(&self) -> u64 {
475        std::time::SystemTime::now()
476            .duration_since(std::time::UNIX_EPOCH)
477            .unwrap_or_default()
478            .as_nanos() as u64
479    }
480
481    /// Export lifecycle data to JSON string
482    pub fn export_to_json(&self, export_data: &LifecycleExportData) -> serde_json::Result<String> {
483        serde_json::to_string_pretty(export_data)
484    }
485}
486
487impl Default for LifecycleSummaryGenerator {
488    fn default() -> Self {
489        Self::new()
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use crate::core::ownership_history::{OwnershipEventType, OwnershipHistoryRecorder};
497    use crate::core::types::AllocationInfo;
498
499    fn create_test_allocation(ptr: usize, size: usize, var_name: Option<String>) -> AllocationInfo {
500        AllocationInfo {
501            ptr,
502            size,
503            var_name,
504            type_name: Some("String".to_string()),
505            scope_name: Some("test_scope".to_string()),
506            timestamp_alloc: 1000000,
507            timestamp_dealloc: Some(2000000),
508            thread_id: "test_thread".to_string(),
509            borrow_count: 0,
510            stack_trace: Some(vec!["test_function".to_string()]),
511            is_leaked: false,
512            lifetime_ms: Some(1000),
513            borrow_info: None,
514            clone_info: None,
515            ownership_history_available: false,
516            smart_pointer_info: None,
517            memory_layout: None,
518            generic_info: None,
519            dynamic_type_info: None,
520            runtime_state: None,
521            stack_allocation: None,
522            temporary_object: None,
523            fragmentation_analysis: None,
524            generic_instantiation: None,
525            type_relationships: None,
526            type_usage: None,
527            function_call_tracking: None,
528            lifecycle_tracking: None,
529            access_tracking: None,
530            drop_chain_analysis: None,
531        }
532    }
533
534    #[test]
535    fn test_json_export() {
536        let generator = LifecycleSummaryGenerator::default();
537        let export_data = LifecycleExportData {
538            lifecycle_events: vec![],
539            variable_groups: vec![],
540            user_variables_count: 0,
541            visualization_ready: true,
542            metadata: ExportMetadata {
543                export_timestamp: 0,
544                total_allocations: 0,
545                total_events: 0,
546                analysis_duration_ms: 0,
547            },
548        };
549
550        let json = generator.export_to_json(&export_data).unwrap();
551        assert!(json.contains("lifecycle_events"));
552        assert!(json.contains("variable_groups"));
553    }
554
555    #[test]
556    fn test_generate_single_lifecycle_summary() {
557        let generator = LifecycleSummaryGenerator::default();
558        let mut history = OwnershipHistoryRecorder::default();
559
560        // Create a test allocation with ownership events
561        let ptr = 0x1000;
562        let size = 1024;
563        let alloc = create_test_allocation(ptr, size, Some("test_var".to_string()));
564
565        // Add some ownership events
566        history.record_event(ptr, OwnershipEventType::Allocated, 1);
567
568        let summary = generator.generate_single_lifecycle_summary(&history, &alloc);
569
570        assert_eq!(summary.allocation_ptr, ptr);
571        assert_eq!(summary.size, size);
572        assert_eq!(summary.var_name, Some("test_var".to_string()));
573        assert_eq!(summary.type_name, Some("String".to_string()));
574    }
575
576    #[test]
577    fn test_calculate_efficiency_score() {
578        let generator = LifecycleSummaryGenerator::default();
579        let alloc = create_test_allocation(0x1000, 1024, Some("test_var".to_string()));
580        let mut history = OwnershipHistoryRecorder::default();
581
582        // Add some ownership events
583        history.record_event(0x1000, OwnershipEventType::Allocated, 1);
584
585        let summary = history.get_summary(0x1000).unwrap();
586        let score = generator.calculate_efficiency_score(&alloc, summary);
587        assert!((0.0..=1.0).contains(&score));
588    }
589
590    #[test]
591    fn test_generate_lifecycle_events() {
592        let generator = LifecycleSummaryGenerator::default();
593        let history = OwnershipHistoryRecorder::default();
594
595        // Create test allocations
596        let alloc1 = create_test_allocation(0x1000, 1024, Some("var1".to_string()));
597        let alloc2 = create_test_allocation(0x2000, 2048, Some("var2".to_string()));
598
599        let events = generator.generate_lifecycle_events(&history, &[alloc1, alloc2]);
600        assert_eq!(events.len(), 2);
601    }
602
603    #[test]
604    fn test_format_event_type() {
605        let generator = LifecycleSummaryGenerator::default();
606
607        let allocated = generator.format_event_type(&OwnershipEventType::Allocated);
608        assert_eq!(allocated, "Allocation");
609
610        let borrowed = generator.format_event_type(&OwnershipEventType::Borrowed {
611            borrower_scope: "scope1".to_string(),
612        });
613        assert_eq!(borrowed, "Borrow");
614
615        let cloned =
616            generator.format_event_type(&OwnershipEventType::Cloned { source_ptr: 0x1000 });
617        assert_eq!(cloned, "Clone");
618    }
619
620    #[test]
621    fn test_generate_lifecycle_export() {
622        let generator = LifecycleSummaryGenerator::default();
623        let history = OwnershipHistoryRecorder::default();
624        let alloc = create_test_allocation(0x1000, 1024, Some("test_var".to_string()));
625
626        let export = generator.generate_lifecycle_export(&history, &[alloc]);
627
628        assert_eq!(export.lifecycle_events.len(), 1);
629        assert_eq!(export.user_variables_count, 1);
630        assert!(export.visualization_ready);
631    }
632
633    #[test]
634    fn test_extract_base_type_name() {
635        let generator = LifecycleSummaryGenerator::default();
636
637        assert_eq!(generator.extract_base_type_name("String"), "String");
638        assert_eq!(
639            generator.extract_base_type_name("std::string::String"),
640            "String"
641        );
642        assert_eq!(generator.extract_base_type_name("Vec<u8>"), "Vec");
643        assert_eq!(
644            generator.extract_base_type_name("std::vec::Vec<u8>"),
645            "std::vec::Vec"
646        );
647        assert_eq!(generator.extract_base_type_name("&str"), "&str");
648    }
649
650    #[test]
651    fn test_is_user_variable() {
652        let generator = LifecycleSummaryGenerator::default();
653
654        assert!(generator.is_user_variable("user_var"));
655        assert!(generator.is_user_variable("my_var_123"));
656        assert!(!generator.is_user_variable("primitive_var"));
657        assert!(!generator.is_user_variable("struct_var"));
658        assert!(!generator.is_user_variable("unknown"));
659    }
660
661    #[test]
662    fn test_lifecycle_pattern_classification() {
663        let generator = LifecycleSummaryGenerator::default();
664
665        assert!(matches!(
666            generator.classify_lifecycle_pattern(None),
667            LifecyclePattern::Leaked
668        ));
669        assert!(matches!(
670            generator.classify_lifecycle_pattern(Some(0)),
671            LifecyclePattern::Ephemeral
672        ));
673        assert!(matches!(
674            generator.classify_lifecycle_pattern(Some(50)),
675            LifecyclePattern::ShortTerm
676        ));
677        assert!(matches!(
678            generator.classify_lifecycle_pattern(Some(5000)),
679            LifecyclePattern::MediumTerm
680        ));
681        assert!(matches!(
682            generator.classify_lifecycle_pattern(Some(15000)),
683            LifecyclePattern::LongTerm
684        ));
685    }
686
687    #[test]
688    fn test_variable_groups_generation() {
689        let generator = LifecycleSummaryGenerator::default();
690
691        let events = vec![
692            LifecycleEventSummary {
693                allocation_ptr: 0x1000,
694                var_name: Some("str1".to_string()),
695                type_name: Some("String".to_string()),
696                size: 100,
697                lifetime_ms: Some(1000),
698                events: vec![],
699                summary: AllocationLifecycleSummary {
700                    lifetime_ms: Some(1000),
701                    borrow_info: BorrowInfo {
702                        immutable_borrows: 0,
703                        mutable_borrows: 0,
704                        max_concurrent_borrows: 0,
705                        last_borrow_timestamp: None,
706                        active_borrows: vec![],
707                    },
708                    clone_info: CloneInfo {
709                        clone_count: 0,
710                        is_clone: false,
711                        original_ptr: None,
712                        cloned_ptrs: vec![],
713                    },
714                    ownership_history_available: false,
715                    lifecycle_pattern: LifecyclePattern::ShortTerm,
716                    efficiency_score: 0.5,
717                },
718            },
719            LifecycleEventSummary {
720                allocation_ptr: 0x2000,
721                var_name: Some("str2".to_string()),
722                type_name: Some("String".to_string()),
723                size: 200,
724                lifetime_ms: Some(2000),
725                events: vec![],
726                summary: AllocationLifecycleSummary {
727                    lifetime_ms: Some(2000),
728                    borrow_info: BorrowInfo {
729                        immutable_borrows: 0,
730                        mutable_borrows: 0,
731                        max_concurrent_borrows: 0,
732                        last_borrow_timestamp: None,
733                        active_borrows: vec![],
734                    },
735                    clone_info: CloneInfo {
736                        clone_count: 0,
737                        is_clone: false,
738                        original_ptr: None,
739                        cloned_ptrs: vec![],
740                    },
741                    ownership_history_available: false,
742                    lifecycle_pattern: LifecyclePattern::MediumTerm,
743                    efficiency_score: 0.5,
744                },
745            },
746        ];
747
748        let groups = generator.generate_variable_groups(&events);
749        assert_eq!(groups.len(), 1); // Should group by "String" type
750        assert_eq!(groups[0].name, "String");
751        assert_eq!(groups[0].total_memory, 300);
752        assert_eq!(groups[0].variables.len(), 2);
753    }
754}