memscope_rs/core/
bounded_memory_stats.rs

1//! Bounded memory statistics to prevent infinite growth
2//!
3//! This module provides memory statistics structures that use bounded containers
4//! to prevent memory leaks during long-running applications.
5
6use crate::core::types::{
7    AllocationInfo, ConcurrencyAnalysis, FragmentationAnalysis, ScopeLifecycleMetrics,
8    SystemLibraryStats,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::VecDeque;
12
13/// Configuration for bounded memory statistics
14#[derive(Debug, Clone)]
15pub struct BoundedStatsConfig {
16    /// Maximum number of recent allocations to keep in memory
17    pub max_recent_allocations: usize,
18    /// Maximum number of historical summaries to keep
19    pub max_historical_summaries: usize,
20    /// Enable automatic cleanup when limits are reached
21    pub enable_auto_cleanup: bool,
22    /// Cleanup threshold (percentage of max before cleanup)
23    pub cleanup_threshold: f32,
24}
25
26impl Default for BoundedStatsConfig {
27    fn default() -> Self {
28        Self {
29            max_recent_allocations: 10_000,  // Keep last 10k allocations
30            max_historical_summaries: 1_000, // Keep 1k historical summaries
31            enable_auto_cleanup: true,
32            cleanup_threshold: 0.9, // Cleanup when 90% full
33        }
34    }
35}
36
37/// Lightweight allocation summary for bounded storage
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct AllocationSummary {
40    pub ptr: usize,
41    pub size: usize,
42    pub timestamp_alloc: u64,
43    pub timestamp_dealloc: Option<u64>,
44    pub type_name: Option<String>,
45    pub var_name: Option<String>,
46    pub is_leaked: bool,
47    pub lifetime_ms: Option<u64>,
48}
49
50impl From<&AllocationInfo> for AllocationSummary {
51    fn from(alloc: &AllocationInfo) -> Self {
52        Self {
53            ptr: alloc.ptr,
54            size: alloc.size,
55            timestamp_alloc: alloc.timestamp_alloc,
56            timestamp_dealloc: alloc.timestamp_dealloc,
57            type_name: alloc.type_name.clone(),
58            var_name: alloc.var_name.clone(),
59            is_leaked: alloc.is_leaked,
60            lifetime_ms: alloc.lifetime_ms,
61        }
62    }
63}
64
65/// Bounded memory statistics that prevent infinite growth
66#[derive(Debug, Clone, Serialize)]
67pub struct BoundedMemoryStats {
68    /// Configuration for this instance
69    #[serde(skip)]
70    pub config: BoundedStatsConfig,
71
72    /// Basic statistics (these don't grow infinitely)
73    pub total_allocations: usize,
74    pub total_allocated: usize,
75    pub active_allocations: usize,
76    pub active_memory: usize,
77    pub peak_allocations: usize,
78    pub peak_memory: usize,
79    pub total_deallocations: usize,
80    pub total_deallocated: usize,
81    pub leaked_allocations: usize,
82    pub leaked_memory: usize,
83
84    /// Analysis data (bounded)
85    pub fragmentation_analysis: FragmentationAnalysis,
86    pub lifecycle_stats: ScopeLifecycleMetrics,
87    pub system_library_stats: SystemLibraryStats,
88    pub concurrency_analysis: ConcurrencyAnalysis,
89
90    /// Bounded containers for detailed data
91    pub recent_allocations: VecDeque<AllocationSummary>,
92    pub historical_summaries: VecDeque<HistoricalSummary>,
93
94    /// Cleanup statistics
95    pub cleanup_count: u32,
96    pub last_cleanup_timestamp: Option<u64>,
97    pub total_allocations_processed: u64,
98}
99
100/// Historical summary for long-term trends
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct HistoricalSummary {
103    pub timestamp: u64,
104    pub total_allocations: usize,
105    pub total_memory: usize,
106    pub active_allocations: usize,
107    pub active_memory: usize,
108    pub allocation_rate: f64,    // allocations per second
109    pub memory_growth_rate: f64, // bytes per second
110}
111
112impl BoundedMemoryStats {
113    /// Create new bounded memory statistics with default configuration
114    pub fn new() -> Self {
115        Self::with_config(BoundedStatsConfig::default())
116    }
117
118    /// Create new bounded memory statistics with custom configuration
119    pub fn with_config(config: BoundedStatsConfig) -> Self {
120        Self {
121            config,
122            total_allocations: 0,
123            total_allocated: 0,
124            active_allocations: 0,
125            active_memory: 0,
126            peak_allocations: 0,
127            peak_memory: 0,
128            total_deallocations: 0,
129            total_deallocated: 0,
130            leaked_allocations: 0,
131            leaked_memory: 0,
132            fragmentation_analysis: FragmentationAnalysis::default(),
133            lifecycle_stats: ScopeLifecycleMetrics {
134                scope_name: "global".to_string(),
135                variable_count: 0,
136                average_lifetime_ms: 0.0,
137                total_memory_usage: 0,
138                peak_memory_usage: 0,
139                allocation_frequency: 0.0,
140                deallocation_efficiency: 0.0,
141                completed_allocations: 0,
142                memory_growth_events: 0,
143                peak_concurrent_variables: 0,
144                memory_efficiency_ratio: 0.0,
145                ownership_transfer_events: 0,
146                fragmentation_score: 0.0,
147                instant_allocations: 0,
148                short_term_allocations: 0,
149                medium_term_allocations: 0,
150                long_term_allocations: 0,
151                suspected_leaks: 0,
152                risk_distribution: crate::core::types::RiskDistribution::default(),
153                scope_metrics: Vec::new(),
154                type_lifecycle_patterns: Vec::new(),
155            },
156            system_library_stats: SystemLibraryStats::default(),
157            concurrency_analysis: ConcurrencyAnalysis::default(),
158            recent_allocations: VecDeque::with_capacity(1000),
159            historical_summaries: VecDeque::with_capacity(100),
160            cleanup_count: 0,
161            last_cleanup_timestamp: None,
162            total_allocations_processed: 0,
163        }
164    }
165
166    /// Add a new allocation, automatically managing bounds
167    pub fn add_allocation(&mut self, alloc: &AllocationInfo) {
168        // CRITICAL FIX: Track all allocations initially, but only count user variables in active_allocations
169        // This allows the two-phase tracking: track_allocation() -> associate_var() to work correctly
170
171        // Always update total statistics for all allocations
172        self.total_allocations += 1;
173        self.total_allocated += alloc.size;
174        self.total_allocations_processed += 1;
175
176        // Only count allocations with variable names as "active" user allocations
177        // This prevents system allocations from inflating the active count
178        if alloc.var_name.is_some() {
179            self.active_allocations += 1;
180            self.active_memory += alloc.size;
181
182            // Update peaks only for user variables
183            if self.active_allocations > self.peak_allocations {
184                self.peak_allocations = self.active_allocations;
185            }
186            if self.active_memory > self.peak_memory {
187                self.peak_memory = self.active_memory;
188            }
189
190            // Add to recent allocations with bounds checking (only user variables)
191            let summary = AllocationSummary::from(alloc);
192            self.add_allocation_summary(summary);
193        }
194
195        // Check if cleanup is needed
196        if self.config.enable_auto_cleanup {
197            self.check_and_cleanup();
198        }
199    }
200
201    /// Update the status of an active allocation, especially its var_name status.
202    /// This is used when a var_name is associated with an already tracked allocation.
203    pub fn update_active_allocation_status(
204        &mut self,
205        alloc: &AllocationInfo,
206        old_var_name_is_none: bool,
207    ) {
208        // If the var_name was None and is now Some, increment active_allocations
209        // This handles the case where an allocation was initially tracked but not counted as "active"
210        // because it didn't have a var_name (user variable) yet.
211        if old_var_name_is_none && alloc.var_name.is_some() {
212            self.active_allocations += 1;
213            self.active_memory += alloc.size;
214
215            // Update peaks as well
216            if self.active_allocations > self.peak_allocations {
217                self.peak_allocations = self.active_allocations;
218            }
219            if self.active_memory > self.peak_memory {
220                self.peak_memory = self.active_memory;
221            }
222
223            // Add to recent allocations with bounds checking (only for user variables)
224            let summary = AllocationSummary::from(alloc);
225            self.add_allocation_summary(summary);
226        } else if alloc.var_name.is_some() {
227            // Update existing summary in recent_allocations if found
228            if let Some(summary) = self
229                .recent_allocations
230                .iter_mut()
231                .find(|s| s.ptr == alloc.ptr)
232            {
233                summary.var_name = alloc.var_name.clone();
234                summary.type_name = alloc.type_name.clone();
235                summary.size = alloc.size; // Size might have been estimated initially
236            }
237        }
238
239        // Check if cleanup is needed
240        if self.config.enable_auto_cleanup {
241            self.check_and_cleanup();
242        }
243    }
244
245    /// Record a deallocation
246    pub fn record_deallocation(&mut self, ptr: usize, size: usize) {
247        self.total_deallocations += 1;
248        self.total_deallocated += size;
249
250        // Only decrease active_allocations if this was a user variable (has var_name)
251        // Check if this allocation is in recent_allocations (which only contains user variables)
252        let current_timestamp = self.get_current_timestamp();
253        if let Some(summary) = self.recent_allocations.iter_mut().find(|s| s.ptr == ptr) {
254            // This is a user variable, decrease active counts
255            self.active_allocations = self.active_allocations.saturating_sub(1);
256            self.active_memory = self.active_memory.saturating_sub(size);
257
258            // Update deallocation timestamp
259            summary.timestamp_dealloc = Some(current_timestamp);
260            if let Some(alloc_time) = summary.timestamp_dealloc {
261                summary.lifetime_ms = Some((alloc_time - summary.timestamp_alloc) / 1_000_000);
262            }
263        }
264        // If not found in recent_allocations, it was a system allocation - don't decrease active counts
265    }
266
267    /// Record a memory leak
268    pub fn record_leak(&mut self, size: usize) {
269        self.leaked_allocations += 1;
270        self.leaked_memory += size;
271    }
272
273    /// Add allocation summary with bounds management
274    fn add_allocation_summary(&mut self, summary: AllocationSummary) {
275        // Check if we need to make room
276        if self.recent_allocations.len() >= self.config.max_recent_allocations {
277            // Remove oldest allocation
278            if let Some(old_summary) = self.recent_allocations.pop_front() {
279                // Optionally create historical summary from removed data
280                self.maybe_create_historical_summary(&old_summary);
281            }
282        }
283
284        self.recent_allocations.push_back(summary);
285    }
286
287    /// Create historical summary from removed allocation data
288    fn maybe_create_historical_summary(&mut self, _removed_summary: &AllocationSummary) {
289        // Create historical summary periodically (e.g., every 1000 allocations)
290        if self.total_allocations % 1000 == 0 {
291            let summary = HistoricalSummary {
292                timestamp: self.get_current_timestamp(),
293                total_allocations: self.total_allocations,
294                total_memory: self.total_allocated,
295                active_allocations: self.active_allocations,
296                active_memory: self.active_memory,
297                allocation_rate: self.calculate_allocation_rate(),
298                memory_growth_rate: self.calculate_memory_growth_rate(),
299            };
300
301            // Add to historical summaries with bounds checking
302            if self.historical_summaries.len() >= self.config.max_historical_summaries {
303                self.historical_summaries.pop_front();
304            }
305            self.historical_summaries.push_back(summary);
306        }
307    }
308
309    /// Check if cleanup is needed and perform it
310    fn check_and_cleanup(&mut self) {
311        let recent_threshold =
312            (self.config.max_recent_allocations as f32 * self.config.cleanup_threshold) as usize;
313        let historical_threshold =
314            (self.config.max_historical_summaries as f32 * self.config.cleanup_threshold) as usize;
315
316        let mut cleaned = false;
317
318        // Cleanup recent allocations if needed
319        if self.recent_allocations.len() >= recent_threshold {
320            let remove_count = self.recent_allocations.len() / 4; // Remove 25%
321            for _ in 0..remove_count {
322                if let Some(old_summary) = self.recent_allocations.pop_front() {
323                    self.maybe_create_historical_summary(&old_summary);
324                }
325            }
326            cleaned = true;
327        }
328
329        // Cleanup historical summaries if needed
330        if self.historical_summaries.len() >= historical_threshold {
331            let remove_count = self.historical_summaries.len() / 4; // Remove 25%
332            for _ in 0..remove_count {
333                self.historical_summaries.pop_front();
334            }
335            cleaned = true;
336        }
337
338        if cleaned {
339            self.cleanup_count += 1;
340            self.last_cleanup_timestamp = Some(self.get_current_timestamp());
341        }
342    }
343
344    /// Get current timestamp in nanoseconds
345    fn get_current_timestamp(&self) -> u64 {
346        std::time::SystemTime::now()
347            .duration_since(std::time::UNIX_EPOCH)
348            .unwrap_or_default()
349            .as_nanos() as u64
350    }
351
352    /// Calculate allocation rate (allocations per second)
353    fn calculate_allocation_rate(&self) -> f64 {
354        if let Some(oldest) = self.recent_allocations.front() {
355            let time_span = self.get_current_timestamp() - oldest.timestamp_alloc;
356            if time_span > 0 {
357                let seconds = time_span as f64 / 1_000_000_000.0;
358                return self.recent_allocations.len() as f64 / seconds;
359            }
360        }
361        0.0
362    }
363
364    /// Calculate memory growth rate (bytes per second)
365    fn calculate_memory_growth_rate(&self) -> f64 {
366        if let Some(oldest) = self.historical_summaries.front() {
367            let time_span = self.get_current_timestamp() - oldest.timestamp;
368            if time_span > 0 {
369                let seconds = time_span as f64 / 1_000_000_000.0;
370                let memory_growth = self.active_memory as i64 - oldest.active_memory as i64;
371                return memory_growth as f64 / seconds;
372            }
373        }
374        0.0
375    }
376
377    /// Get memory usage statistics for this stats instance
378    pub fn get_memory_usage(&self) -> MemoryUsageStats {
379        let recent_allocations_size =
380            self.recent_allocations.len() * std::mem::size_of::<AllocationSummary>();
381        let historical_summaries_size =
382            self.historical_summaries.len() * std::mem::size_of::<HistoricalSummary>();
383        let base_size = std::mem::size_of::<Self>();
384
385        MemoryUsageStats {
386            total_size: base_size + recent_allocations_size + historical_summaries_size,
387            recent_allocations_size,
388            historical_summaries_size,
389            base_size,
390            recent_allocations_count: self.recent_allocations.len(),
391            historical_summaries_count: self.historical_summaries.len(),
392        }
393    }
394
395    /// Force cleanup of old data
396    pub fn force_cleanup(&mut self) {
397        let old_cleanup_threshold = self.config.cleanup_threshold;
398        self.config.cleanup_threshold = 0.5; // Force more aggressive cleanup
399        self.check_and_cleanup();
400        self.config.cleanup_threshold = old_cleanup_threshold;
401    }
402
403    /// Get all allocations as a Vec (for compatibility with existing code)
404    pub fn get_all_allocations(&self) -> Vec<AllocationInfo> {
405        self.recent_allocations
406            .iter()
407            .map(|summary| {
408                // Convert summary back to AllocationInfo for compatibility
409                AllocationInfo {
410                    ptr: summary.ptr,
411                    size: summary.size,
412                    var_name: summary.var_name.clone(),
413                    type_name: summary.type_name.clone(),
414                    scope_name: Some("tracked".to_string()),
415                    timestamp_alloc: summary.timestamp_alloc,
416                    timestamp_dealloc: summary.timestamp_dealloc,
417                    thread_id: "main".to_string(), // Default thread ID
418                    borrow_count: 0,
419                    stack_trace: None,
420                    is_leaked: summary.is_leaked,
421                    lifetime_ms: summary.lifetime_ms,
422                    borrow_info: None,
423                    clone_info: None,
424                    ownership_history_available: false,
425                    // Set other fields to default/None for compatibility
426                    smart_pointer_info: None,
427                    memory_layout: None,
428                    generic_info: None,
429                    dynamic_type_info: None,
430                    runtime_state: None,
431                    stack_allocation: None,
432                    temporary_object: None,
433                    fragmentation_analysis: None,
434                    generic_instantiation: None,
435                    type_relationships: None,
436                    type_usage: None,
437                    function_call_tracking: None,
438                    lifecycle_tracking: None,
439                    access_tracking: None,
440                    drop_chain_analysis: None,
441                }
442            })
443            .collect()
444    }
445}
446
447impl Default for BoundedMemoryStats {
448    fn default() -> Self {
449        Self::new()
450    }
451}
452
453/// Memory usage statistics for the stats instance itself
454#[derive(Debug, Clone, Serialize)]
455pub struct MemoryUsageStats {
456    pub total_size: usize,
457    pub recent_allocations_size: usize,
458    pub historical_summaries_size: usize,
459    pub base_size: usize,
460    pub recent_allocations_count: usize,
461    pub historical_summaries_count: usize,
462}
463
464/// Allocation history manager for separate storage of detailed history
465pub struct AllocationHistoryManager {
466    /// Configuration
467    config: BoundedStatsConfig,
468    /// Detailed allocation history (bounded)
469    history: VecDeque<AllocationInfo>,
470    /// History cleanup statistics
471    cleanup_count: u32,
472    last_cleanup_timestamp: Option<u64>,
473}
474
475impl AllocationHistoryManager {
476    /// Create new history manager
477    pub fn new() -> Self {
478        Self::with_config(BoundedStatsConfig::default())
479    }
480
481    /// Create new history manager with custom configuration
482    pub fn with_config(config: BoundedStatsConfig) -> Self {
483        Self {
484            config,
485            history: VecDeque::with_capacity(1000),
486            cleanup_count: 0,
487            last_cleanup_timestamp: None,
488        }
489    }
490
491    /// Add allocation to history
492    pub fn add_allocation(&mut self, alloc: AllocationInfo) {
493        // Check bounds and cleanup if needed
494        if self.history.len() >= self.config.max_recent_allocations {
495            let remove_count = self.history.len() / 4; // Remove 25%
496            for _ in 0..remove_count {
497                self.history.pop_front();
498            }
499            self.cleanup_count += 1;
500            self.last_cleanup_timestamp = Some(
501                std::time::SystemTime::now()
502                    .duration_since(std::time::UNIX_EPOCH)
503                    .unwrap_or_default()
504                    .as_nanos() as u64,
505            );
506        }
507
508        self.history.push_back(alloc);
509    }
510
511    /// Get all history entries
512    pub fn get_history(&self) -> &VecDeque<AllocationInfo> {
513        &self.history
514    }
515
516    /// Get history as Vec for compatibility
517    pub fn get_history_vec(&self) -> Vec<AllocationInfo> {
518        self.history.iter().cloned().collect()
519    }
520
521    /// Clear all history
522    pub fn clear(&mut self) {
523        self.history.clear();
524    }
525
526    /// Get memory usage of this history manager
527    pub fn get_memory_usage(&self) -> usize {
528        std::mem::size_of::<Self>() + self.history.len() * std::mem::size_of::<AllocationInfo>()
529    }
530}
531
532impl Default for AllocationHistoryManager {
533    fn default() -> Self {
534        Self::new()
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::core::types::AllocationInfo;
542
543    fn create_test_allocation(id: usize) -> AllocationInfo {
544        AllocationInfo {
545            ptr: 0x1000 + id,
546            size: 64 + (id % 100),
547            var_name: Some(format!("var_{id}")),
548            type_name: Some("TestType".to_string()),
549            scope_name: Some("test".to_string()),
550            timestamp_alloc: id as u64 * 1000,
551            timestamp_dealloc: None,
552            thread_id: "main".to_string(),
553            borrow_count: 0,
554            stack_trace: None,
555            is_leaked: false,
556            lifetime_ms: None,
557            borrow_info: None,
558            clone_info: None,
559            ownership_history_available: false,
560            smart_pointer_info: None,
561            memory_layout: None,
562            generic_info: None,
563            dynamic_type_info: None,
564            runtime_state: None,
565            stack_allocation: None,
566            temporary_object: None,
567            fragmentation_analysis: None,
568            generic_instantiation: None,
569            type_relationships: None,
570            type_usage: None,
571            function_call_tracking: None,
572            lifecycle_tracking: None,
573            access_tracking: None,
574            drop_chain_analysis: None,
575        }
576    }
577
578    #[test]
579    fn test_bounded_memory_stats_no_overflow() {
580        let mut stats = BoundedMemoryStats::with_config(BoundedStatsConfig {
581            max_recent_allocations: 100,
582            max_historical_summaries: 10,
583            enable_auto_cleanup: true,
584            cleanup_threshold: 0.9,
585        });
586
587        // Add 150 allocations
588        for i in 0..150 {
589            let alloc = create_test_allocation(i);
590            stats.add_allocation(&alloc);
591        }
592
593        // Verify bounds are respected
594        assert!(stats.recent_allocations.len() <= 100);
595        assert_eq!(stats.total_allocations, 150);
596        assert!(stats.cleanup_count > 0);
597    }
598
599    #[test]
600    fn test_allocation_history_manager_bounds() {
601        let mut manager = AllocationHistoryManager::with_config(BoundedStatsConfig {
602            max_recent_allocations: 50,
603            ..Default::default()
604        });
605
606        // Add 100 allocations
607        for i in 0..100 {
608            let alloc = create_test_allocation(i);
609            manager.add_allocation(alloc);
610        }
611
612        // Verify bounds are respected
613        assert!(manager.history.len() <= 50);
614        assert!(manager.cleanup_count > 0);
615    }
616
617    #[test]
618    fn test_memory_usage_calculation() {
619        let stats = BoundedMemoryStats::new();
620        let usage = stats.get_memory_usage();
621
622        assert!(usage.total_size > 0);
623        assert_eq!(usage.recent_allocations_count, 0);
624        assert_eq!(usage.historical_summaries_count, 0);
625    }
626
627    #[test]
628    fn test_deallocation_tracking() {
629        let mut stats = BoundedMemoryStats::new();
630        let alloc = create_test_allocation(1);
631
632        stats.add_allocation(&alloc);
633        assert_eq!(stats.active_allocations, 1);
634        assert_eq!(stats.active_memory, alloc.size);
635
636        stats.record_deallocation(alloc.ptr, alloc.size);
637        assert_eq!(stats.active_allocations, 0);
638        assert_eq!(stats.active_memory, 0);
639        assert_eq!(stats.total_deallocations, 1);
640    }
641
642    #[test]
643    fn test_leak_tracking() {
644        let mut stats = BoundedMemoryStats::new();
645
646        stats.record_leak(1024);
647        assert_eq!(stats.leaked_allocations, 1);
648        assert_eq!(stats.leaked_memory, 1024);
649    }
650}