tinylfu_cached/cache/stats/
mod.rs1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use crossbeam_utils::CachePadded;
5
6const TOTAL_STATS: usize = 10;
7
8#[repr(usize)]
10#[non_exhaustive]
11#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
12pub enum StatsType {
13 CacheHits = 0,
15 CacheMisses = 1,
17 KeysAdded = 2,
19 KeysDeleted = 3,
21 KeysUpdated = 4,
23 KeysRejected = 5,
25 WeightAdded = 6,
27 WeightRemoved = 7,
29 AccessAdded = 8,
31 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#[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 pub fn get(&self, stats_type: &StatsType) -> Option<u64> {
67 self.stats_by_type.get(stats_type).copied()
68 }
69
70 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
80pub(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}