memscope_rs/stack_trace/
cache.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicUsize, Ordering};
3
4/// Statistics for stack trace cache performance
5#[derive(Debug, Clone)]
6pub struct CacheStats {
7    /// Total cache lookups performed
8    pub total_lookups: usize,
9    /// Number of cache hits
10    pub cache_hits: usize,
11    /// Number of cache misses
12    pub cache_misses: usize,
13    /// Current cache size (number of entries)
14    pub cache_size: usize,
15    /// Cache hit ratio (0.0 to 1.0)
16    pub hit_ratio: f64,
17}
18
19/// High-performance cache for stack trace data
20pub struct StackTraceCache<T> {
21    /// Internal cache storage
22    cache: HashMap<u64, T>,
23    /// Total number of lookups
24    lookup_count: AtomicUsize,
25    /// Number of successful cache hits
26    hit_count: AtomicUsize,
27    /// Maximum cache size before eviction
28    max_size: usize,
29}
30
31impl<T> StackTraceCache<T> {
32    /// Create new cache with specified maximum size
33    pub fn new(max_size: usize) -> Self {
34        Self {
35            cache: HashMap::new(),
36            lookup_count: AtomicUsize::new(0),
37            hit_count: AtomicUsize::new(0),
38            max_size,
39        }
40    }
41
42    /// Get value from cache by key
43    pub fn get(&mut self, key: u64) -> Option<&T> {
44        self.lookup_count.fetch_add(1, Ordering::Relaxed);
45
46        if let Some(value) = self.cache.get(&key) {
47            self.hit_count.fetch_add(1, Ordering::Relaxed);
48            Some(value)
49        } else {
50            None
51        }
52    }
53
54    /// Insert value into cache
55    pub fn insert(&mut self, key: u64, value: T) {
56        // Evict oldest entries if cache is full
57        if self.cache.len() >= self.max_size {
58            self.evict_oldest();
59        }
60
61        self.cache.insert(key, value);
62    }
63
64    /// Clear all cache entries
65    pub fn clear(&mut self) {
66        self.cache.clear();
67        self.lookup_count.store(0, Ordering::Relaxed);
68        self.hit_count.store(0, Ordering::Relaxed);
69    }
70
71    /// Get current cache statistics
72    pub fn stats(&self) -> CacheStats {
73        let lookups = self.lookup_count.load(Ordering::Relaxed);
74        let hits = self.hit_count.load(Ordering::Relaxed);
75        let misses = lookups.saturating_sub(hits);
76        let hit_ratio = if lookups > 0 {
77            hits as f64 / lookups as f64
78        } else {
79            0.0
80        };
81
82        CacheStats {
83            total_lookups: lookups,
84            cache_hits: hits,
85            cache_misses: misses,
86            cache_size: self.cache.len(),
87            hit_ratio,
88        }
89    }
90
91    /// Get current cache size
92    pub fn size(&self) -> usize {
93        self.cache.len()
94    }
95
96    /// Check if cache is empty
97    pub fn is_empty(&self) -> bool {
98        self.cache.is_empty()
99    }
100
101    /// Simple eviction strategy - remove first entry
102    fn evict_oldest(&mut self) {
103        if let Some(key) = self.cache.keys().next().copied() {
104            self.cache.remove(&key);
105        }
106    }
107}
108
109impl<T> Default for StackTraceCache<T> {
110    fn default() -> Self {
111        Self::new(1000) // Default cache size
112    }
113}
114
115impl CacheStats {
116    /// Check if cache performance is good
117    pub fn is_performing_well(&self) -> bool {
118        self.hit_ratio >= 0.8 && self.total_lookups > 10
119    }
120
121    /// Get cache efficiency description
122    pub fn efficiency_description(&self) -> &'static str {
123        match self.hit_ratio {
124            x if x >= 0.9 => "Excellent",
125            x if x >= 0.8 => "Good",
126            x if x >= 0.6 => "Fair",
127            x if x >= 0.4 => "Poor",
128            _ => "Very Poor",
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_basic_cache_operations() {
139        let mut cache = StackTraceCache::new(3);
140
141        assert!(cache.is_empty());
142        assert_eq!(cache.size(), 0);
143
144        cache.insert(1, "first");
145        cache.insert(2, "second");
146
147        assert_eq!(cache.size(), 2);
148        assert!(!cache.is_empty());
149
150        assert_eq!(cache.get(1), Some(&"first"));
151        assert_eq!(cache.get(2), Some(&"second"));
152        assert_eq!(cache.get(3), None);
153    }
154
155    #[test]
156    fn test_cache_eviction() {
157        let mut cache = StackTraceCache::new(2);
158
159        cache.insert(1, "first");
160        cache.insert(2, "second");
161        assert_eq!(cache.size(), 2);
162
163        // Adding third item should evict one
164        cache.insert(3, "third");
165        assert_eq!(cache.size(), 2);
166
167        // One item should be evicted (could be any due to HashMap ordering)
168        let get1 = cache.get(1).is_some();
169        let get2 = cache.get(2).is_some();
170        let get3 = cache.get(3).is_some();
171        let remaining_count = [get1, get2, get3].iter().filter(|&&x| x).count();
172        assert_eq!(remaining_count, 2); // Only 2 items should remain
173    }
174
175    #[test]
176    fn test_cache_stats() {
177        let mut cache = StackTraceCache::new(10);
178
179        cache.insert(1, "value1");
180        cache.insert(2, "value2");
181
182        // Perform some lookups
183        cache.get(1); // hit
184        cache.get(2); // hit
185        cache.get(3); // miss
186        cache.get(1); // hit
187
188        let stats = cache.stats();
189        assert_eq!(stats.total_lookups, 4);
190        assert_eq!(stats.cache_hits, 3);
191        assert_eq!(stats.cache_misses, 1);
192        assert_eq!(stats.cache_size, 2);
193        assert!((stats.hit_ratio - 0.75).abs() < 0.01);
194    }
195
196    #[test]
197    fn test_cache_clear() {
198        let mut cache = StackTraceCache::new(10);
199
200        cache.insert(1, "value");
201        cache.get(1);
202
203        assert!(!cache.is_empty());
204        assert!(cache.stats().total_lookups > 0);
205
206        cache.clear();
207
208        assert!(cache.is_empty());
209        assert_eq!(cache.stats().total_lookups, 0);
210        assert_eq!(cache.stats().cache_hits, 0);
211    }
212
213    #[test]
214    fn test_efficiency_description() {
215        let excellent = CacheStats {
216            hit_ratio: 0.95,
217            total_lookups: 100,
218            cache_hits: 95,
219            cache_misses: 5,
220            cache_size: 50,
221        };
222        assert_eq!(excellent.efficiency_description(), "Excellent");
223
224        let poor = CacheStats {
225            hit_ratio: 0.45, // Changed to fall in "Poor" range (0.4-0.6)
226            total_lookups: 100,
227            cache_hits: 45,
228            cache_misses: 55,
229            cache_size: 20,
230        };
231        assert_eq!(poor.efficiency_description(), "Poor");
232    }
233}