spg_engine/
query_stats.rs1use alloc::collections::{BTreeMap, VecDeque};
15use alloc::string::String;
16use alloc::vec::Vec;
17
18pub(crate) const QUERY_STATS_MAX: usize = 1024;
22
23#[derive(Debug, Clone, Default)]
24pub struct QueryStat {
25 pub exec_count: u64,
26 pub total_us: u64,
27 pub max_us: u64,
28 pub last_seen_us: u64,
29}
30
31#[derive(Debug, Clone, Default)]
32pub struct QueryStats {
33 entries: BTreeMap<String, QueryStat>,
37 lru: VecDeque<String>,
40}
41
42impl QueryStats {
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn len(&self) -> usize {
48 self.entries.len()
49 }
50
51 pub fn is_empty(&self) -> bool {
52 self.entries.is_empty()
53 }
54
55 pub fn get(&self, sql: &str) -> Option<&QueryStat> {
58 self.entries.get(sql)
59 }
60
61 pub fn iter(&self) -> impl Iterator<Item = (&String, &QueryStat)> {
64 self.entries.iter()
65 }
66
67 pub fn record(&mut self, sql: &str, elapsed_us: u64, now_us: u64) {
71 if let Some(stat) = self.entries.get_mut(sql) {
72 stat.exec_count = stat.exec_count.saturating_add(1);
73 stat.total_us = stat.total_us.saturating_add(elapsed_us);
74 stat.max_us = stat.max_us.max(elapsed_us);
75 stat.last_seen_us = now_us;
76 if let Some(idx) = self.lru.iter().position(|k| k == sql) {
78 let key = self.lru.remove(idx).expect("idx from position");
79 self.lru.push_back(key);
80 }
81 return;
82 }
83 if self.entries.len() >= QUERY_STATS_MAX
85 && let Some(oldest) = self.lru.pop_front()
86 {
87 self.entries.remove(&oldest);
88 }
89 self.entries.insert(
90 String::from(sql),
91 QueryStat {
92 exec_count: 1,
93 total_us: elapsed_us,
94 max_us: elapsed_us,
95 last_seen_us: now_us,
96 },
97 );
98 self.lru.push_back(String::from(sql));
99 }
100
101 pub fn clear(&mut self) {
103 self.entries.clear();
104 self.lru.clear();
105 }
106
107 pub fn cap(&self) -> usize {
108 QUERY_STATS_MAX
109 }
110
111 pub fn snapshot(&self) -> Vec<(String, QueryStat)> {
114 self.lru
115 .iter()
116 .filter_map(|sql| self.entries.get(sql).map(|s| (sql.clone(), s.clone())))
117 .collect()
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use alloc::string::ToString;
125
126 #[test]
127 fn record_increments_counters() {
128 let mut qs = QueryStats::new();
129 qs.record("SELECT 1", 100, 1000);
130 qs.record("SELECT 1", 200, 2000);
131 let s = qs.get("SELECT 1").expect("present");
132 assert_eq!(s.exec_count, 2);
133 assert_eq!(s.total_us, 300);
134 assert_eq!(s.max_us, 200);
135 assert_eq!(s.last_seen_us, 2000);
136 }
137
138 #[test]
139 fn distinct_sql_yields_separate_entries() {
140 let mut qs = QueryStats::new();
141 qs.record("SELECT 1", 10, 100);
142 qs.record("SELECT 2", 20, 200);
143 assert_eq!(qs.len(), 2);
144 }
145
146 #[test]
147 fn lru_evicts_oldest_at_cap() {
148 let mut qs = QueryStats::new();
149 for i in 0..QUERY_STATS_MAX {
150 qs.record(&alloc::format!("SELECT {i}"), 1, i as u64);
151 }
152 assert_eq!(qs.len(), QUERY_STATS_MAX);
153 qs.record("SELECT new", 1, QUERY_STATS_MAX as u64);
155 assert_eq!(qs.len(), QUERY_STATS_MAX);
156 assert!(qs.get("SELECT 0").is_none(), "oldest evicted");
157 assert!(qs.get("SELECT new").is_some());
158 }
159
160 #[test]
161 fn re_recording_an_entry_promotes_lru() {
162 let mut qs = QueryStats::new();
163 qs.record("a", 1, 1);
164 qs.record("b", 1, 2);
165 qs.record("c", 1, 3);
166 qs.record("a", 1, 4);
168 for i in 0..(QUERY_STATS_MAX - 3) {
170 qs.record(&alloc::format!("filler{i}"), 1, 100 + i as u64);
171 }
172 qs.record("trigger", 1, 9999);
173 assert!(qs.get("a").is_some(), "a was MRU; should survive");
174 assert!(qs.get("b").is_none(), "b should be evicted");
175 }
176
177 #[test]
178 fn clear_drops_everything() {
179 let mut qs = QueryStats::new();
180 qs.record("a", 1, 1);
181 qs.record("b", 1, 2);
182 qs.clear();
183 assert!(qs.is_empty());
184 }
185
186 #[test]
187 fn snapshot_returns_lru_order_oldest_first() {
188 let mut qs = QueryStats::new();
189 qs.record("a", 1, 100);
190 qs.record("b", 1, 200);
191 qs.record("c", 1, 300);
192 let snap = qs.snapshot();
193 let keys: Vec<String> = snap.iter().map(|(k, _)| k.clone()).collect();
194 assert_eq!(
195 keys,
196 alloc::vec!["a".to_string(), "b".to_string(), "c".to_string()]
197 );
198 }
199}