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/// len: 64,
20/// capacity: 128,
21/// };
22/// assert_eq!(stats.lookups(), 100);
23/// assert!((stats.hit_rate() - 0.75).abs() < f64::EPSILON);
24/// ```
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct CacheStats {
28 /// Lookups served from the cache.
29 pub hits: u64,
30 /// Lookups that missed and fell through to the wrapped index.
31 pub misses: u64,
32 /// Entries currently held.
33 pub len: usize,
34 /// Maximum entries the cache will hold; `0` means caching is disabled.
35 pub capacity: usize,
36}
37
38impl CacheStats {
39 /// Total lookups observed: `hits + misses` (saturating).
40 ///
41 /// # Examples
42 ///
43 /// ```
44 /// use iqdb_cache::CacheStats;
45 ///
46 /// let stats = CacheStats { hits: 3, misses: 1, len: 4, capacity: 8 };
47 /// assert_eq!(stats.lookups(), 4);
48 /// ```
49 #[inline]
50 #[must_use]
51 pub fn lookups(&self) -> u64 {
52 self.hits.saturating_add(self.misses)
53 }
54
55 /// The fraction of lookups served from cache, in `0.0..=1.0`.
56 ///
57 /// Returns `0.0` when there have been no lookups, so the result is always
58 /// finite and safe to display.
59 ///
60 /// # Examples
61 ///
62 /// ```
63 /// use iqdb_cache::CacheStats;
64 ///
65 /// let warm = CacheStats { hits: 9, misses: 1, len: 10, capacity: 16 };
66 /// assert!((warm.hit_rate() - 0.9).abs() < 1e-9);
67 ///
68 /// let cold = CacheStats { hits: 0, misses: 0, len: 0, capacity: 16 };
69 /// assert_eq!(cold.hit_rate(), 0.0);
70 /// ```
71 #[inline]
72 #[must_use]
73 pub fn hit_rate(&self) -> f64 {
74 let total = self.lookups();
75 if total == 0 {
76 0.0
77 } else {
78 self.hits as f64 / total as f64
79 }
80 }
81}