tinylfu_cached/cache/stats/
mod.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use crossbeam_utils::CachePadded;
5
6const TOTAL_STATS: usize = 10;
7
8/// Defines various stats that are measured in the cache.
9#[repr(usize)]
10#[non_exhaustive]
11#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
12pub enum StatsType {
13    /// Defines the number of `hits` for the keys
14    CacheHits = 0,
15    /// Defines the number of `misses` for the keys
16    CacheMisses = 1,
17    /// Defines the number of `keys added`
18    KeysAdded = 2,
19    /// Defines the number of `keys deleted`
20    KeysDeleted = 3,
21    /// Defines the number of `keys updated`
22    KeysUpdated = 4,
23    /// Defines the number of `keys rejected`
24    KeysRejected = 5,
25    /// Defines the `total weight added`
26    WeightAdded = 6,
27    /// Defines the `total weight removed`
28    WeightRemoved = 7,
29    /// Defines the total number of `gets registered` in the frequency counter
30    AccessAdded = 8,
31    /// Defines the total number of `gets dropped`
32    AccessDropped = 9,
33}
34
35impl StatsType {
36    const VALUES: [Self; TOTAL_STATS] = [
37        Self::CacheHits,
38        Self::CacheMisses,
39        Self::KeysAdded,
40        Self::KeysDeleted,
41        Self::KeysUpdated,
42        Self::KeysRejected,
43        Self::WeightAdded,
44        Self::WeightRemoved,
45        Self::AccessAdded,
46        Self::AccessDropped
47    ];
48}
49
50/// StatsSummary is view representation of various stats represented by [`StatsType`].
51#[derive(Debug, PartialEq)]
52pub struct StatsSummary {
53    pub stats_by_type: HashMap<StatsType, u64>,
54    pub hit_ratio: f64,
55}
56
57impl StatsSummary {
58    pub(crate) fn new(stats_by_type: HashMap<StatsType, u64>, hit_ratio: f64) -> Self {
59        StatsSummary {
60            stats_by_type,
61            hit_ratio,
62        }
63    }
64
65    /// Returns an Option&lt;u64&gt; counter corresponding to the [`StatsType`].
66    pub fn get(&self, stats_type: &StatsType) -> Option<u64> {
67        self.stats_by_type.get(stats_type).copied()
68    }
69
70    /// Returns an hit ratio as %. Performs `round()`. since v0.0.4.
71    pub fn hit_ratio_as_percentage(&self) -> f64 {
72        (self.hit_ratio * 100.0).round()
73    }
74}
75
76#[repr(transparent)]
77#[derive(Debug)]
78struct Counter(CachePadded<AtomicU64>);
79
80/// ConcurrentStatsCounter measures various stats defined by [`StatsType`].
81/// ConcurrentStatsCounter is represented as an array of entries where each entry is an instance of type [`Counter`].
82/// Each instance of [`Counter`] is a [`crossbeam_utils::CachePadded`] AtomicU64, to avoid false sharing.
83/// ConcurrentStatsCounter provides methods:
84    /// to increase the counters corresponding to various stats and
85    /// to get the values for these stats
86pub(crate) struct ConcurrentStatsCounter {
87    entries: [Counter; TOTAL_STATS],
88}
89
90impl ConcurrentStatsCounter {
91    pub(crate) fn new() -> Self {
92        ConcurrentStatsCounter {
93            entries: (0..TOTAL_STATS)
94                .map(|_index| Counter(CachePadded::new(AtomicU64::new(0))))
95                .collect::<Vec<Counter>>()
96                .try_into().unwrap()
97        }
98    }
99
100    pub(crate) fn found_a_hit(&self) { self.add(StatsType::CacheHits, 1); }
101
102    pub(crate) fn found_a_miss(&self) { self.add(StatsType::CacheMisses, 1); }
103
104    pub(crate) fn add_weight(&self, delta: u64) { self.add(StatsType::WeightAdded, delta); }
105
106    pub(crate) fn remove_weight(&self, delta: u64) { self.add(StatsType::WeightRemoved, delta); }
107
108    pub(crate) fn add_access(&self, delta: u64) { self.add(StatsType::AccessAdded, delta); }
109
110    pub(crate) fn drop_access(&self, delta: u64) { self.add(StatsType::AccessDropped, delta); }
111
112    pub(crate) fn add_key(&self) { self.add(StatsType::KeysAdded, 1); }
113
114    pub(crate) fn reject_key(&self) { self.add(StatsType::KeysRejected, 1); }
115
116    pub(crate) fn delete_key(&self) { self.add(StatsType::KeysDeleted, 1); }
117
118    pub(crate) fn update_key(&self) { self.add(StatsType::KeysUpdated, 1); }
119
120    pub(crate) fn hits(&self) -> u64 {
121        self.get(&StatsType::CacheHits)
122    }
123
124    pub(crate) fn misses(&self) -> u64 {
125        self.get(&StatsType::CacheMisses)
126    }
127
128    pub(crate) fn keys_added(&self) -> u64 {
129        self.get(&StatsType::KeysAdded)
130    }
131
132    pub(crate) fn keys_deleted(&self) -> u64 {
133        self.get(&StatsType::KeysDeleted)
134    }
135
136    pub(crate) fn keys_rejected(&self) -> u64 { self.get(&StatsType::KeysRejected) }
137
138    pub(crate) fn keys_updated(&self) -> u64 { self.get(&StatsType::KeysUpdated) }
139
140    pub(crate) fn weight_added(&self) -> u64 {
141        self.get(&StatsType::WeightAdded)
142    }
143
144    pub(crate) fn weight_removed(&self) -> u64 { self.get(&StatsType::WeightRemoved) }
145
146    pub(crate) fn access_added(&self) -> u64 { self.get(&StatsType::AccessAdded) }
147
148    pub(crate) fn access_dropped(&self) -> u64 { self.get(&StatsType::AccessDropped) }
149
150    pub(crate) fn hit_ratio(&self) -> f64 {
151        let hits = self.hits();
152        let misses = self.misses();
153        if hits == 0 || misses == 0 {
154            return 0.0;
155        }
156        (hits as f64) / (hits + misses) as f64
157    }
158
159    pub(crate) fn clear(&self) {
160        for entry in &self.entries {
161            entry.0.store(0, Ordering::Release);
162        }
163    }
164
165    pub(crate) fn summary(&self) -> StatsSummary {
166        let mut stats_by_type = HashMap::new();
167        for stats_type in StatsType::VALUES.iter().copied() {
168            stats_by_type.insert(stats_type, self.get(&stats_type));
169        }
170        StatsSummary::new(stats_by_type, self.hit_ratio())
171    }
172
173    fn add(&self, stats_type: StatsType, count: u64) {
174        self.entries[stats_type as usize].0.fetch_add(count, Ordering::AcqRel);
175    }
176
177    fn get(&self, stats_type: &StatsType) -> u64 {
178        self.entries[*stats_type as usize].0.load(Ordering::Acquire)
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use std::collections::HashMap;
185
186    use crate::cache::stats::{ConcurrentStatsCounter, StatsType};
187
188    #[test]
189    fn increase_cache_hits() {
190        let stats_counter = ConcurrentStatsCounter::new();
191        stats_counter.found_a_hit();
192        stats_counter.found_a_hit();
193
194        assert_eq!(2, stats_counter.hits());
195    }
196
197    #[test]
198    fn increase_cache_misses() {
199        let stats_counter = ConcurrentStatsCounter::new();
200        stats_counter.found_a_miss();
201        stats_counter.found_a_miss();
202
203        assert_eq!(2, stats_counter.misses());
204    }
205
206    #[test]
207    fn increase_keys_added() {
208        let stats_counter = ConcurrentStatsCounter::new();
209        stats_counter.add_key();
210        stats_counter.add_key();
211
212        assert_eq!(2, stats_counter.keys_added());
213    }
214
215    #[test]
216    fn increase_keys_deleted() {
217        let stats_counter = ConcurrentStatsCounter::new();
218        stats_counter.add(StatsType::KeysDeleted, 1);
219        stats_counter.add(StatsType::KeysDeleted, 1);
220
221        assert_eq!(2, stats_counter.keys_deleted());
222    }
223
224    #[test]
225    fn increase_keys_rejected() {
226        let stats_counter = ConcurrentStatsCounter::new();
227        stats_counter.reject_key();
228        stats_counter.reject_key();
229
230        assert_eq!(2, stats_counter.keys_rejected());
231    }
232
233    #[test]
234    fn increase_keys_updated() {
235        let stats_counter = ConcurrentStatsCounter::new();
236        stats_counter.update_key();
237        stats_counter.update_key();
238
239        assert_eq!(2, stats_counter.keys_updated());
240    }
241
242    #[test]
243    fn hit_ratio_as_zero() {
244        let stats_counter = ConcurrentStatsCounter::new();
245        stats_counter.add(StatsType::CacheMisses, 1);
246
247        assert_eq!(0.0, stats_counter.hit_ratio());
248    }
249
250    #[test]
251    fn hit_ratio() {
252        let stats_counter = ConcurrentStatsCounter::new();
253        stats_counter.add(StatsType::CacheMisses, 3);
254        stats_counter.add(StatsType::CacheHits, 1);
255
256        assert_eq!(0.25, stats_counter.hit_ratio());
257    }
258
259    #[test]
260    fn weight_added() {
261        let stats_counter = ConcurrentStatsCounter::new();
262        stats_counter.add_weight(1);
263        stats_counter.add_weight(1);
264
265        assert_eq!(2, stats_counter.weight_added());
266    }
267
268    #[test]
269    fn weight_removed() {
270        let stats_counter = ConcurrentStatsCounter::new();
271        stats_counter.add_weight(1);
272        stats_counter.add_weight(1);
273
274        stats_counter.remove_weight(1);
275        stats_counter.remove_weight(1);
276
277        assert_eq!(2, stats_counter.weight_removed());
278    }
279
280    #[test]
281    fn access_added() {
282        let stats_counter = ConcurrentStatsCounter::new();
283        stats_counter.add_access(1);
284        stats_counter.add_access(1);
285
286        assert_eq!(2, stats_counter.access_added());
287    }
288
289    #[test]
290    fn access_dropped() {
291        let stats_counter = ConcurrentStatsCounter::new();
292        stats_counter.drop_access(1);
293        stats_counter.drop_access(1);
294
295        assert_eq!(2, stats_counter.access_dropped());
296    }
297
298    #[test]
299    fn clear() {
300        let stats_counter = ConcurrentStatsCounter::new();
301        stats_counter.add_key();
302        stats_counter.add_key();
303        assert_eq!(2, stats_counter.keys_added());
304
305        stats_counter.clear();
306        assert_eq!(0, stats_counter.keys_added());
307    }
308
309    #[test]
310    fn stats_summary_with_all_stats_as_one() {
311        let stats_counter = ConcurrentStatsCounter::new();
312        stats_counter.found_a_hit();
313        stats_counter.found_a_miss();
314        stats_counter.add_key();
315        stats_counter.delete_key();
316        stats_counter.update_key();
317        stats_counter.reject_key();
318        stats_counter.add_weight(1);
319        stats_counter.remove_weight(1);
320        stats_counter.add_access(1);
321        stats_counter.drop_access(1);
322
323        let summary = stats_counter.summary();
324        let mut stats_by_type = HashMap::new();
325        for stats_type in StatsType::VALUES.iter().copied() {
326            stats_by_type.insert(stats_type, 1);
327        }
328
329        assert_eq!(0.5, summary.hit_ratio);
330        assert_eq!(stats_by_type, summary.stats_by_type);
331    }
332
333    #[test]
334    fn stats_summary() {
335        let stats_counter = ConcurrentStatsCounter::new();
336        stats_counter.found_a_hit();
337        stats_counter.found_a_miss();
338        stats_counter.delete_key();
339        stats_counter.update_key();
340        stats_counter.remove_weight(1);
341        stats_counter.add_access(1);
342        stats_counter.drop_access(2);
343
344        let summary = stats_counter.summary();
345        let mut stats_by_type = HashMap::new();
346        stats_by_type.insert(StatsType::CacheHits, 1);
347        stats_by_type.insert(StatsType::CacheMisses, 1);
348        stats_by_type.insert(StatsType::KeysAdded, 0);
349        stats_by_type.insert(StatsType::KeysDeleted, 1);
350        stats_by_type.insert(StatsType::KeysUpdated, 1);
351        stats_by_type.insert(StatsType::KeysRejected, 0);
352        stats_by_type.insert(StatsType::WeightAdded, 0);
353        stats_by_type.insert(StatsType::WeightRemoved, 1);
354        stats_by_type.insert(StatsType::AccessAdded, 1);
355        stats_by_type.insert(StatsType::AccessDropped, 2);
356
357        assert_eq!(0.5, summary.hit_ratio);
358        assert_eq!(stats_by_type, summary.stats_by_type);
359    }
360
361    #[test]
362    fn stats_summary_with_hit_ratio() {
363        let stats_counter = ConcurrentStatsCounter::new();
364        stats_counter.found_a_hit();
365        stats_counter.found_a_miss();
366        stats_counter.found_a_miss();
367
368        let summary = stats_counter.summary();
369        assert_eq!(33.0, summary.hit_ratio_as_percentage());
370    }
371}
372
373#[cfg(test)]
374mod stats_summary_tests {
375    use std::collections::HashMap;
376    use crate::cache::stats::{StatsSummary, StatsType};
377
378    #[test]
379    fn missing_stats() {
380        let summary = StatsSummary::new(HashMap::new(), 0.0);
381        assert_eq!(None, summary.get(&StatsType::CacheHits));
382    }
383
384    #[test]
385    fn stats_value_by_its_type() {
386        let mut stats_by_type = HashMap::new();
387        stats_by_type.insert(StatsType::CacheHits, 1);
388        stats_by_type.insert(StatsType::KeysAdded, 5);
389
390        let summary = StatsSummary::new(stats_by_type, 1.0);
391        assert_eq!(1, summary.get(&StatsType::CacheHits).unwrap());
392        assert_eq!(5, summary.get(&StatsType::KeysAdded).unwrap());
393    }
394}