rustlavel_model_cache/
stats.rs1use std::collections::BTreeMap;
8use std::sync::Mutex;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
13pub struct Counts {
14 pub entity_hits: u64,
15 pub entity_misses: u64,
16 pub query_hits: u64,
17 pub query_misses: u64,
18 pub too_large: u64,
20 pub unsupported: u64,
24 pub invalidations: u64,
26}
27
28impl Counts {
29 pub fn hit_rate(&self) -> Option<f64> {
34 let hits = self.entity_hits + self.query_hits;
35 let total = hits + self.entity_misses + self.query_misses;
36 (total > 0).then(|| hits as f64 / total as f64)
37 }
38}
39
40#[derive(Default)]
41struct Tally {
42 entity_hits: AtomicU64,
43 entity_misses: AtomicU64,
44 query_hits: AtomicU64,
45 query_misses: AtomicU64,
46 too_large: AtomicU64,
47 unsupported: AtomicU64,
48 invalidations: AtomicU64,
49}
50
51impl Tally {
52 fn read(&self) -> Counts {
53 Counts {
54 entity_hits: self.entity_hits.load(Ordering::Relaxed),
55 entity_misses: self.entity_misses.load(Ordering::Relaxed),
56 query_hits: self.query_hits.load(Ordering::Relaxed),
57 query_misses: self.query_misses.load(Ordering::Relaxed),
58 too_large: self.too_large.load(Ordering::Relaxed),
59 unsupported: self.unsupported.load(Ordering::Relaxed),
60 invalidations: self.invalidations.load(Ordering::Relaxed),
61 }
62 }
63}
64
65#[derive(Default)]
67pub struct Stats {
68 tables: Mutex<BTreeMap<&'static str, Tally>>,
69}
70
71#[derive(Debug, Clone, Copy)]
73pub enum Event {
74 EntityHit,
75 EntityMiss,
76 QueryHit,
77 QueryMiss,
78 TooLarge,
79 Unsupported,
80 Invalidated,
81}
82
83impl Stats {
84 pub fn record(&self, table: &'static str, event: Event) {
85 let mut tables = self.tables.lock().unwrap_or_else(|e| e.into_inner());
89 let tally = tables.entry(table).or_default();
90 let counter = match event {
91 Event::EntityHit => &tally.entity_hits,
92 Event::EntityMiss => &tally.entity_misses,
93 Event::QueryHit => &tally.query_hits,
94 Event::QueryMiss => &tally.query_misses,
95 Event::TooLarge => &tally.too_large,
96 Event::Unsupported => &tally.unsupported,
97 Event::Invalidated => &tally.invalidations,
98 };
99 counter.fetch_add(1, Ordering::Relaxed);
100 }
101
102 pub fn all(&self) -> BTreeMap<&'static str, Counts> {
104 let tables = self.tables.lock().unwrap_or_else(|e| e.into_inner());
105 tables.iter().map(|(name, tally)| (*name, tally.read())).collect()
106 }
107
108 pub fn for_table(&self, table: &str) -> Counts {
109 let tables = self.tables.lock().unwrap_or_else(|e| e.into_inner());
110 tables.get(table).map(Tally::read).unwrap_or_default()
111 }
112
113 pub fn total(&self) -> Counts {
115 self.all().values().fold(Counts::default(), |mut sum, counts| {
116 sum.entity_hits += counts.entity_hits;
117 sum.entity_misses += counts.entity_misses;
118 sum.query_hits += counts.query_hits;
119 sum.query_misses += counts.query_misses;
120 sum.too_large += counts.too_large;
121 sum.unsupported += counts.unsupported;
122 sum.invalidations += counts.invalidations;
123 sum
124 })
125 }
126
127 pub fn reset(&self) {
128 self.tables.lock().unwrap_or_else(|e| e.into_inner()).clear();
129 }
130}
131
132impl std::fmt::Debug for Stats {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.debug_struct("Stats").field("tables", &self.all()).finish()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn a_cache_nobody_has_asked_anything_of_has_no_hit_rate() {
144 assert_eq!(Counts::default().hit_rate(), None);
145
146 let counts = Counts { entity_hits: 3, entity_misses: 1, ..Default::default() };
147 assert_eq!(counts.hit_rate(), Some(0.75));
148 }
149
150 #[test]
151 fn counters_are_kept_per_table_and_summed_on_request() {
152 let stats = Stats::default();
153 stats.record("users", Event::EntityHit);
154 stats.record("users", Event::EntityHit);
155 stats.record("users", Event::QueryMiss);
156 stats.record("posts", Event::EntityMiss);
157
158 assert_eq!(stats.for_table("users").entity_hits, 2);
159 assert_eq!(stats.for_table("posts").entity_misses, 1);
160 assert_eq!(stats.for_table("nothing"), Counts::default());
161
162 let total = stats.total();
163 assert_eq!(total.entity_hits, 2);
164 assert_eq!(total.entity_misses, 1);
165 assert_eq!(total.query_misses, 1);
166
167 stats.reset();
168 assert_eq!(stats.total(), Counts::default());
169 }
170}