memscope_rs/memory/
bounded_history.rs1use std::collections::VecDeque;
8use std::sync::{Arc, Mutex, RwLock};
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone)]
13pub struct BoundedHistoryConfig {
14 pub max_entries: usize,
16 pub max_age: Duration,
18 pub total_memory_limit: usize,
20 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), total_memory_limit: 50 * 1024 * 1024, cleanup_threshold: 0.8, }
32 }
33}
34
35pub struct BoundedHistory<T> {
37 config: BoundedHistoryConfig,
39 entries: Arc<Mutex<VecDeque<TimestampedEntry<T>>>>,
41 current_memory_usage: Arc<Mutex<usize>>,
43 stats: Arc<RwLock<BoundedHistoryStats>>,
45 #[allow(dead_code)]
47 last_cleanup: Arc<Mutex<Instant>>,
48}
49
50#[derive(Debug, Clone)]
52pub struct TimestampedEntry<T> {
53 pub data: T,
55 pub timestamp: Instant,
57 pub estimated_size: usize,
59}
60
61#[derive(Debug, Clone, Default)]
63pub struct BoundedHistoryStats {
64 pub total_entries_added: u64,
66 pub entries_expired: u64,
68 pub entries_evicted: u64,
70 pub cleanup_operations: u64,
72 pub current_memory_usage: usize,
74 pub peak_memory_usage: usize,
76}
77
78impl<T> TimestampedEntry<T> {
79 pub fn new(data: T, estimated_size: usize) -> Self {
81 Self {
82 data,
83 timestamp: Instant::now(),
84 estimated_size,
85 }
86 }
87
88 pub fn is_expired(&self, max_age: Duration) -> bool {
90 self.timestamp.elapsed() > max_age
91 }
92
93 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 pub fn new() -> Self {
114 Self::with_config(BoundedHistoryConfig::default())
115 }
116
117 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 pub fn push(&self, data: T) -> bool {
130 let estimated_size = std::mem::size_of::<T>() + 64; 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 if *usage + estimated_size > self.config.total_memory_limit {
138 self.evict_oldest_entries(estimated_size);
139 }
140
141 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 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 }
292}