memscope_rs/memory/
bounded_history.rs

1//! Bounded History Implementation
2//!
3//! This module provides memory-bounded history tracking with automatic cleanup
4//! and age-based expiration, addressing the unlimited memory growth issue
5//! identified in the improvement plan.
6
7use std::collections::VecDeque;
8use std::sync::{Arc, Mutex, RwLock};
9use std::time::{Duration, Instant};
10
11/// Configuration for bounded history behavior
12#[derive(Debug, Clone)]
13pub struct BoundedHistoryConfig {
14    /// Maximum number of entries to keep
15    pub max_entries: usize,
16    /// Maximum age of entries before expiration
17    pub max_age: Duration,
18    /// Total memory limit in bytes
19    pub total_memory_limit: usize,
20    /// Cleanup threshold (percentage of max_entries)
21    pub cleanup_threshold: f32,
22}
23
24impl Default for BoundedHistoryConfig {
25    fn default() -> Self {
26        Self {
27            max_entries: 10_000,
28            max_age: Duration::from_secs(3600),   // 1 hour
29            total_memory_limit: 50 * 1024 * 1024, // 50MB
30            cleanup_threshold: 0.8,               // Cleanup when 80% full
31        }
32    }
33}
34
35/// Thread-safe bounded history with automatic cleanup
36pub struct BoundedHistory<T> {
37    /// Configuration parameters
38    config: BoundedHistoryConfig,
39    /// The actual history entries
40    entries: Arc<Mutex<VecDeque<TimestampedEntry<T>>>>,
41    /// Current estimated memory usage
42    current_memory_usage: Arc<Mutex<usize>>,
43    /// Operation statistics
44    stats: Arc<RwLock<BoundedHistoryStats>>,
45    /// Last cleanup timestamp
46    #[allow(dead_code)]
47    last_cleanup: Arc<Mutex<Instant>>,
48}
49
50/// Timestamped entry wrapper for history tracking
51#[derive(Debug, Clone)]
52pub struct TimestampedEntry<T> {
53    /// The actual data being stored
54    pub data: T,
55    /// Timestamp when this entry was created
56    pub timestamp: Instant,
57    /// Estimated memory size of this entry
58    pub estimated_size: usize,
59}
60
61/// Statistics about bounded history operation
62#[derive(Debug, Clone, Default)]
63pub struct BoundedHistoryStats {
64    /// Total entries added
65    pub total_entries_added: u64,
66    /// Total entries removed due to age
67    pub entries_expired: u64,
68    /// Total entries removed due to capacity
69    pub entries_evicted: u64,
70    /// Total cleanup operations performed
71    pub cleanup_operations: u64,
72    /// Current memory usage estimate
73    pub current_memory_usage: usize,
74    /// Peak memory usage observed
75    pub peak_memory_usage: usize,
76}
77
78impl<T> TimestampedEntry<T> {
79    /// Create a new timestamped entry
80    pub fn new(data: T, estimated_size: usize) -> Self {
81        Self {
82            data,
83            timestamp: Instant::now(),
84            estimated_size,
85        }
86    }
87
88    /// Check if this entry has expired based on max age
89    pub fn is_expired(&self, max_age: Duration) -> bool {
90        self.timestamp.elapsed() > max_age
91    }
92
93    /// Get the age of this entry
94    pub fn age(&self) -> Duration {
95        self.timestamp.elapsed()
96    }
97}
98
99impl<T> Default for BoundedHistory<T>
100where
101    T: Clone + Send + Sync + 'static,
102{
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl<T> BoundedHistory<T>
109where
110    T: Clone + Send + Sync + 'static,
111{
112    /// Create a new bounded history with default configuration
113    pub fn new() -> Self {
114        Self::with_config(BoundedHistoryConfig::default())
115    }
116
117    /// Create a new bounded history with custom configuration
118    pub fn with_config(config: BoundedHistoryConfig) -> Self {
119        Self {
120            config,
121            entries: Arc::new(Mutex::new(VecDeque::new())),
122            current_memory_usage: Arc::new(Mutex::new(0)),
123            stats: Arc::new(RwLock::new(BoundedHistoryStats::default())),
124            last_cleanup: Arc::new(Mutex::new(Instant::now())),
125        }
126    }
127
128    /// Add a new entry to the history
129    pub fn push(&self, data: T) -> bool {
130        let estimated_size = std::mem::size_of::<T>() + 64; // Basic estimation
131        let entry = TimestampedEntry::new(data, estimated_size);
132
133        if let (Ok(mut entries), Ok(mut usage)) =
134            (self.entries.lock(), self.current_memory_usage.lock())
135        {
136            // Check memory limit
137            if *usage + estimated_size > self.config.total_memory_limit {
138                self.evict_oldest_entries(estimated_size);
139            }
140
141            // Check entry count limit
142            if entries.len() >= self.config.max_entries {
143                if let Some(removed) = entries.pop_front() {
144                    *usage = usage.saturating_sub(removed.estimated_size);
145                }
146            }
147
148            entries.push_back(entry);
149            *usage += estimated_size;
150
151            // Update stats
152            if let Ok(mut stats) = self.stats.write() {
153                stats.total_entries_added += 1;
154                stats.current_memory_usage = *usage;
155                if *usage > stats.peak_memory_usage {
156                    stats.peak_memory_usage = *usage;
157                }
158            }
159
160            true
161        } else {
162            false
163        }
164    }
165
166    pub fn entries(&self) -> Vec<T> {
167        if let Ok(entries) = self.entries.lock() {
168            entries.iter().map(|entry| entry.data.clone()).collect()
169        } else {
170            Vec::new()
171        }
172    }
173
174    pub fn clear(&self) {
175        if let Ok(mut entries) = self.entries.lock() {
176            entries.clear();
177        }
178        if let Ok(mut usage) = self.current_memory_usage.lock() {
179            *usage = 0;
180        }
181    }
182
183    pub fn len(&self) -> usize {
184        self.entries.lock().map(|e| e.len()).unwrap_or(0)
185    }
186
187    pub fn is_empty(&self) -> bool {
188        self.entries.lock().map(|e| e.is_empty()).unwrap_or(true)
189    }
190
191    pub fn get_memory_usage_stats(&self) -> BoundedHistoryStats {
192        if let (Ok(entries), Ok(usage)) = (self.entries.lock(), self.current_memory_usage.lock()) {
193            let _memory_usage_mb = *usage as f64 / (1024.0 * 1024.0);
194            let _oldest_entry_age_secs = entries
195                .front()
196                .map(|entry| entry.timestamp.elapsed().as_secs_f64());
197            let _average_entry_size = if entries.is_empty() {
198                0.0
199            } else {
200                *usage as f64 / entries.len() as f64
201            };
202
203            if let Ok(stats) = self.stats.read() {
204                stats.clone()
205            } else {
206                BoundedHistoryStats::default()
207            }
208        } else {
209            BoundedHistoryStats::default()
210        }
211    }
212
213    pub fn cleanup_expired(&self) -> usize {
214        let cutoff = Instant::now() - self.config.max_age;
215        let mut removed_count = 0;
216
217        if let (Ok(mut entries), Ok(mut usage)) =
218            (self.entries.lock(), self.current_memory_usage.lock())
219        {
220            while let Some(entry) = entries.front() {
221                if entry.timestamp < cutoff {
222                    if let Some(removed) = entries.pop_front() {
223                        *usage = usage.saturating_sub(removed.estimated_size);
224                        removed_count += 1;
225                    }
226                } else {
227                    break;
228                }
229            }
230        }
231        removed_count
232    }
233
234    fn evict_oldest_entries(&self, needed_space: usize) {
235        if let (Ok(mut entries), Ok(mut usage)) =
236            (self.entries.lock(), self.current_memory_usage.lock())
237        {
238            let mut freed_space = 0;
239            while freed_space < needed_space && !entries.is_empty() {
240                if let Some(entry) = entries.pop_front() {
241                    freed_space += entry.estimated_size;
242                    *usage = usage.saturating_sub(entry.estimated_size);
243                }
244            }
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_basic_functionality() {
255        let config = BoundedHistoryConfig {
256            max_entries: 3,
257            max_age: Duration::from_secs(60),
258            total_memory_limit: 1024 * 1024,
259            cleanup_threshold: 0.8,
260        };
261        let history = BoundedHistory::with_config(config);
262
263        assert!(history.push(1));
264        assert!(history.push(2));
265        assert!(history.push(3));
266        assert_eq!(history.len(), 3);
267
268        assert!(history.push(4));
269        assert_eq!(history.len(), 3);
270
271        let values = history.entries();
272        assert_eq!(values.len(), 3);
273    }
274
275    #[test]
276    fn test_memory_stats() {
277        let config = BoundedHistoryConfig {
278            max_entries: 100,
279            max_age: Duration::from_secs(60),
280            total_memory_limit: 10 * 1024 * 1024,
281            cleanup_threshold: 0.8,
282        };
283        let history = BoundedHistory::with_config(config);
284
285        for i in 0..50 {
286            history.push(i);
287        }
288
289        let _stats = history.get_memory_usage_stats();
290        // Just test that it doesn't crash - no assertion needed for unsigned value
291    }
292}