Skip to main content

oxilean_kernel/hash_cons/
stats.rs

1//! Statistics for the hash-consing arena.
2
3/// Statistics snapshot emitted by [`HashConsArena::stats`].
4///
5/// [`HashConsArena::stats`]: super::arena::HashConsArena::stats
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct HashConsStats {
8    /// Number of `mk_*` calls that found an existing node (cache hit).
9    pub hits: u64,
10    /// Number of `mk_*` calls that inserted a new node (cache miss).
11    pub misses: u64,
12    /// Total number of `mk_*` calls (`hits + misses`).
13    pub total: u64,
14    /// Number of distinct nodes actually stored in the arena.
15    pub unique_nodes: usize,
16}
17
18impl HashConsStats {
19    /// Fraction of calls that were cache hits (`hits / total`).
20    ///
21    /// Returns `0.0` when `total` is zero.
22    pub fn hit_rate(&self) -> f64 {
23        if self.total == 0 {
24            return 0.0;
25        }
26        self.hits as f64 / self.total as f64
27    }
28
29    /// Fraction of calls that resulted in a new allocation (`misses / total`).
30    ///
31    /// Returns `0.0` when `total` is zero.
32    pub fn dedup_ratio(&self) -> f64 {
33        if self.total == 0 {
34            return 0.0;
35        }
36        self.misses as f64 / self.total as f64
37    }
38
39    /// Returns `true` when at least one deduplication hit occurred.
40    pub fn has_dedup(&self) -> bool {
41        self.hits > 0
42    }
43}
44
45impl std::fmt::Display for HashConsStats {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(
48            f,
49            "HashConsStats {{ hits: {}, misses: {}, total: {}, unique_nodes: {}, hit_rate: {:.2}% }}",
50            self.hits,
51            self.misses,
52            self.total,
53            self.unique_nodes,
54            self.hit_rate() * 100.0,
55        )
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_stats_hit_rate_zero_total() {
65        let s = HashConsStats {
66            hits: 0,
67            misses: 0,
68            total: 0,
69            unique_nodes: 0,
70        };
71        assert_eq!(s.hit_rate(), 0.0);
72    }
73
74    #[test]
75    fn test_stats_hit_rate_all_hits() {
76        let s = HashConsStats {
77            hits: 10,
78            misses: 0,
79            total: 10,
80            unique_nodes: 5,
81        };
82        assert!((s.hit_rate() - 1.0).abs() < f64::EPSILON);
83    }
84
85    #[test]
86    fn test_stats_dedup_ratio() {
87        let s = HashConsStats {
88            hits: 7,
89            misses: 3,
90            total: 10,
91            unique_nodes: 3,
92        };
93        assert!((s.dedup_ratio() - 0.3).abs() < 1e-10);
94    }
95
96    #[test]
97    fn test_stats_has_dedup_true() {
98        let s = HashConsStats {
99            hits: 1,
100            misses: 9,
101            total: 10,
102            unique_nodes: 9,
103        };
104        assert!(s.has_dedup());
105    }
106
107    #[test]
108    fn test_stats_has_dedup_false_when_no_hits() {
109        let s = HashConsStats {
110            hits: 0,
111            misses: 5,
112            total: 5,
113            unique_nodes: 5,
114        };
115        assert!(!s.has_dedup());
116    }
117
118    #[test]
119    fn test_stats_display_does_not_panic() {
120        let s = HashConsStats {
121            hits: 3,
122            misses: 2,
123            total: 5,
124            unique_nodes: 2,
125        };
126        let text = format!("{}", s);
127        assert!(text.contains("hits: 3"));
128        assert!(text.contains("misses: 2"));
129    }
130}