use std::sync::atomic::{AtomicU64, Ordering};
use super::QueryStats;
#[derive(Debug, Default)]
pub(crate) struct AtomicQueryStats {
total_queries: AtomicU64,
error_queries: AtomicU64,
cache_hits: AtomicU64,
rows_affected: AtomicU64,
exec_time_us_sum: AtomicU64,
}
impl AtomicQueryStats {
pub(crate) fn record_query(&self) {
self.total_queries.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_error(&self) {
self.error_queries.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_cache_hit(&self) {
self.cache_hits.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_execution(&self, exec_time_us: u64, rows_affected: u64) {
self.exec_time_us_sum
.fetch_add(exec_time_us, Ordering::Relaxed);
self.rows_affected
.fetch_add(rows_affected, Ordering::Relaxed);
}
pub(crate) fn snapshot(&self) -> QueryStats {
let total_queries = self.total_queries.load(Ordering::Relaxed);
let cache_hits = self.cache_hits.load(Ordering::Relaxed);
let exec_time_us_sum = self.exec_time_us_sum.load(Ordering::Relaxed);
let (avg_execution_time_us, cache_hit_ratio) = if total_queries == 0 {
(0, 0.0)
} else {
(
exec_time_us_sum / total_queries,
cache_hits.min(total_queries) as f64 / total_queries as f64,
)
};
QueryStats {
total_queries,
error_queries: self.error_queries.load(Ordering::Relaxed),
avg_execution_time_us,
cache_hit_ratio,
rows_affected: self.rows_affected.load(Ordering::Relaxed),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn empty_snapshot_is_all_zero() {
let snap = AtomicQueryStats::default().snapshot();
assert_eq!(snap.total_queries, 0);
assert_eq!(snap.error_queries, 0);
assert_eq!(snap.rows_affected, 0);
assert_eq!(snap.avg_execution_time_us, 0);
assert_eq!(snap.cache_hit_ratio, 0.0);
}
#[test]
fn derived_values_have_expected_shape() {
let stats = AtomicQueryStats::default();
for _ in 0..4 {
stats.record_query();
}
stats.record_cache_hit();
stats.record_execution(100, 2);
stats.record_execution(300, 3);
let snap = stats.snapshot();
assert_eq!(snap.total_queries, 4);
assert_eq!(snap.rows_affected, 5);
assert_eq!(snap.avg_execution_time_us, 100);
assert!((0.0..=1.0).contains(&snap.cache_hit_ratio));
assert!(snap.cache_hit_ratio > 0.0);
assert!((snap.cache_hit_ratio - 0.25).abs() < f64::EPSILON);
}
#[test]
fn concurrent_increments_are_exact() {
const THREADS: u64 = 16;
const PER_THREAD: u64 = 5_000;
let stats = Arc::new(AtomicQueryStats::default());
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let stats = Arc::clone(&stats);
thread::spawn(move || {
for _ in 0..PER_THREAD {
stats.record_query();
stats.record_error();
stats.record_cache_hit();
stats.record_execution(2, 3);
}
})
})
.collect();
for h in handles {
h.join().expect("worker thread panicked");
}
let snap = stats.snapshot();
let issued = THREADS * PER_THREAD;
assert_eq!(snap.total_queries, issued);
assert_eq!(snap.error_queries, issued);
assert_eq!(snap.rows_affected, issued * 3);
assert_eq!(snap.avg_execution_time_us, 2);
assert!((snap.cache_hit_ratio - 1.0).abs() < f64::EPSILON);
}
}