Skip to main content

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}