use alloc::collections::{BTreeMap, VecDeque};
use alloc::string::String;
use alloc::vec::Vec;
pub(crate) const QUERY_STATS_MAX: usize = 1024;
#[derive(Debug, Clone, Default)]
pub struct QueryStat {
pub exec_count: u64,
pub total_us: u64,
pub max_us: u64,
pub last_seen_us: u64,
}
#[derive(Debug, Clone, Default)]
pub struct QueryStats {
entries: BTreeMap<String, QueryStat>,
lru: VecDeque<String>,
}
impl QueryStats {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get(&self, sql: &str) -> Option<&QueryStat> {
self.entries.get(sql)
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &QueryStat)> {
self.entries.iter()
}
pub fn record(&mut self, sql: &str, elapsed_us: u64, now_us: u64) {
if let Some(stat) = self.entries.get_mut(sql) {
stat.exec_count = stat.exec_count.saturating_add(1);
stat.total_us = stat.total_us.saturating_add(elapsed_us);
stat.max_us = stat.max_us.max(elapsed_us);
stat.last_seen_us = now_us;
if let Some(idx) = self.lru.iter().position(|k| k == sql) {
let key = self.lru.remove(idx).expect("idx from position");
self.lru.push_back(key);
}
return;
}
if self.entries.len() >= QUERY_STATS_MAX
&& let Some(oldest) = self.lru.pop_front()
{
self.entries.remove(&oldest);
}
self.entries.insert(
String::from(sql),
QueryStat {
exec_count: 1,
total_us: elapsed_us,
max_us: elapsed_us,
last_seen_us: now_us,
},
);
self.lru.push_back(String::from(sql));
}
pub fn clear(&mut self) {
self.entries.clear();
self.lru.clear();
}
pub fn cap(&self) -> usize {
QUERY_STATS_MAX
}
pub fn snapshot(&self) -> Vec<(String, QueryStat)> {
self.lru
.iter()
.filter_map(|sql| self.entries.get(sql).map(|s| (sql.clone(), s.clone())))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn record_increments_counters() {
let mut qs = QueryStats::new();
qs.record("SELECT 1", 100, 1000);
qs.record("SELECT 1", 200, 2000);
let s = qs.get("SELECT 1").expect("present");
assert_eq!(s.exec_count, 2);
assert_eq!(s.total_us, 300);
assert_eq!(s.max_us, 200);
assert_eq!(s.last_seen_us, 2000);
}
#[test]
fn distinct_sql_yields_separate_entries() {
let mut qs = QueryStats::new();
qs.record("SELECT 1", 10, 100);
qs.record("SELECT 2", 20, 200);
assert_eq!(qs.len(), 2);
}
#[test]
fn lru_evicts_oldest_at_cap() {
let mut qs = QueryStats::new();
for i in 0..QUERY_STATS_MAX {
qs.record(&alloc::format!("SELECT {i}"), 1, i as u64);
}
assert_eq!(qs.len(), QUERY_STATS_MAX);
qs.record("SELECT new", 1, QUERY_STATS_MAX as u64);
assert_eq!(qs.len(), QUERY_STATS_MAX);
assert!(qs.get("SELECT 0").is_none(), "oldest evicted");
assert!(qs.get("SELECT new").is_some());
}
#[test]
fn re_recording_an_entry_promotes_lru() {
let mut qs = QueryStats::new();
qs.record("a", 1, 1);
qs.record("b", 1, 2);
qs.record("c", 1, 3);
qs.record("a", 1, 4);
for i in 0..(QUERY_STATS_MAX - 3) {
qs.record(&alloc::format!("filler{i}"), 1, 100 + i as u64);
}
qs.record("trigger", 1, 9999);
assert!(qs.get("a").is_some(), "a was MRU; should survive");
assert!(qs.get("b").is_none(), "b should be evicted");
}
#[test]
fn clear_drops_everything() {
let mut qs = QueryStats::new();
qs.record("a", 1, 1);
qs.record("b", 1, 2);
qs.clear();
assert!(qs.is_empty());
}
#[test]
fn snapshot_returns_lru_order_oldest_first() {
let mut qs = QueryStats::new();
qs.record("a", 1, 100);
qs.record("b", 1, 200);
qs.record("c", 1, 300);
let snap = qs.snapshot();
let keys: Vec<String> = snap.iter().map(|(k, _)| k.clone()).collect();
assert_eq!(
keys,
alloc::vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
}
}