iqdb_cache/stats.rs
1//! Cache hit/miss accounting.
2
3/// A point-in-time snapshot of a [`CachedIndex`](crate::CachedIndex)'s cache.
4///
5/// Returned by [`CachedIndex::cache_stats`](crate::CachedIndex::cache_stats).
6/// `hits` and `misses` are monotonic counters over the cache's lifetime;
7/// `len` and `capacity` describe its current occupancy. Use
8/// [`hit_rate`](CacheStats::hit_rate) to turn the counters into a ratio for
9/// tuning.
10///
11/// # Examples
12///
13/// ```
14/// use iqdb_cache::CacheStats;
15///
16/// let stats = CacheStats {
17/// hits: 75,
18/// misses: 25,
19/// evictions: 8,
20/// len: 64,
21/// capacity: 128,
22/// };
23/// assert_eq!(stats.lookups(), 100);
24/// assert!((stats.hit_rate() - 0.75).abs() < f64::EPSILON);
25/// ```
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct CacheStats {
29 /// Lookups served from the cache.
30 pub hits: u64,
31 /// Lookups that missed and fell through to the wrapped index.
32 pub misses: u64,
33 /// Entries discarded by the eviction policy to make room over the cache's
34 /// lifetime.
35 pub evictions: u64,
36 /// Entries currently held.
37 pub len: usize,
38 /// Maximum entries the cache will hold; `0` means caching is disabled.
39 pub capacity: usize,
40}
41
42impl CacheStats {
43 /// Total lookups observed: `hits + misses` (saturating).
44 ///
45 /// # Examples
46 ///
47 /// ```
48 /// use iqdb_cache::CacheStats;
49 ///
50 /// let stats = CacheStats { hits: 3, misses: 1, evictions: 0, len: 4, capacity: 8 };
51 /// assert_eq!(stats.lookups(), 4);
52 /// ```
53 #[inline]
54 #[must_use]
55 pub fn lookups(&self) -> u64 {
56 self.hits.saturating_add(self.misses)
57 }
58
59 /// The fraction of lookups served from cache, in `0.0..=1.0`.
60 ///
61 /// Returns `0.0` when there have been no lookups, so the result is always
62 /// finite and safe to display.
63 ///
64 /// # Examples
65 ///
66 /// ```
67 /// use iqdb_cache::CacheStats;
68 ///
69 /// let warm = CacheStats { hits: 9, misses: 1, evictions: 0, len: 10, capacity: 16 };
70 /// assert!((warm.hit_rate() - 0.9).abs() < 1e-9);
71 ///
72 /// let cold = CacheStats { hits: 0, misses: 0, evictions: 0, len: 0, capacity: 16 };
73 /// assert_eq!(cold.hit_rate(), 0.0);
74 /// ```
75 #[inline]
76 #[must_use]
77 pub fn hit_rate(&self) -> f64 {
78 let total = self.lookups();
79 if total == 0 {
80 0.0
81 } else {
82 self.hits as f64 / total as f64
83 }
84 }
85}