velesdb_core/velesql/query_stats.rs
1//! Runtime statistics for adaptive query planning.
2
3// Reason: u64 → f64 casts are for selectivity ratio normalisation and EMA retrieval;
4// cardinalities and ratios here never approach 2^53 so precision loss is negligible.
5#![allow(clippy::cast_precision_loss)]
6
7use std::sync::atomic::{AtomicU64, Ordering};
8
9/// Statistics for query planning decisions.
10#[derive(Debug, Default)]
11pub struct QueryStats {
12 /// Estimated ratio of nodes matching graph patterns (0.0-1.0).
13 graph_selectivity: AtomicU64,
14 /// Average vector search latency in microseconds.
15 avg_vector_latency_us: AtomicU64,
16 /// Average graph traversal latency in microseconds.
17 avg_graph_latency_us: AtomicU64,
18 /// Number of vector queries executed (for averaging).
19 vector_query_count: AtomicU64,
20 /// Number of graph queries executed (for averaging).
21 graph_query_count: AtomicU64,
22}
23
24impl QueryStats {
25 /// Creates new empty query statistics.
26 #[must_use]
27 pub fn new() -> Self {
28 Self::default()
29 }
30
31 /// Updates graph selectivity estimate.
32 pub fn update_graph_selectivity(&self, matched: u64, total: u64) {
33 if total > 0 {
34 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
35 // Reason: selectivity ratio * 1_000_000 is always in [0, 1_000_000] range,
36 // which fits in u64. Both matched and total are unsigned, so ratio is non-negative.
37 let selectivity = (matched as f64 / total as f64 * 1_000_000.0) as u64;
38 self.graph_selectivity.store(selectivity, Ordering::Relaxed);
39 }
40 }
41
42 /// Gets the current graph selectivity estimate (0.0-1.0).
43 #[must_use]
44 pub fn graph_selectivity(&self) -> f64 {
45 self.graph_selectivity.load(Ordering::Relaxed) as f64 / 1_000_000.0
46 }
47
48 /// Updates average vector search latency using exponential moving average.
49 ///
50 /// Uses EMA with α=0.1 for thread-safe updates without race conditions.
51 /// EMA formula: new_avg = α * latency + (1-α) * old_avg
52 /// This avoids the race condition in running average calculations.
53 pub fn update_vector_latency(&self, latency_us: u64) {
54 self.vector_query_count.fetch_add(1, Ordering::Relaxed);
55 Self::atomic_ema_update(&self.avg_vector_latency_us, latency_us);
56 }
57
58 /// Updates average graph traversal latency using exponential moving average.
59 ///
60 /// Uses EMA with α=0.1 for thread-safe updates without race conditions.
61 /// This ensures accurate statistics for query planning decisions.
62 pub fn update_graph_latency(&self, latency_us: u64) {
63 self.graph_query_count.fetch_add(1, Ordering::Relaxed);
64 Self::atomic_ema_update(&self.avg_graph_latency_us, latency_us);
65 }
66
67 /// Atomically updates an EMA using compare-and-swap loop.
68 ///
69 /// α = 0.1 (10% weight to new value, 90% to historical average)
70 /// This provides smooth averaging while being fully thread-safe.
71 fn atomic_ema_update(avg: &AtomicU64, new_value: u64) {
72 loop {
73 let old_avg = avg.load(Ordering::Relaxed);
74 let new_avg = if old_avg == 0 {
75 // First value: use it directly
76 new_value
77 } else {
78 // EMA: new_avg = 0.1 * new_value + 0.9 * old_avg
79 // Using integer math: (new_value + 9 * old_avg) / 10
80 (new_value + 9 * old_avg) / 10
81 };
82 // CAS loop ensures atomic read-modify-write
83 if avg
84 .compare_exchange_weak(old_avg, new_avg, Ordering::Relaxed, Ordering::Relaxed)
85 .is_ok()
86 {
87 break;
88 }
89 // Retry on contention
90 }
91 }
92
93 /// Gets the average vector latency in microseconds.
94 #[must_use]
95 pub fn avg_vector_latency_us(&self) -> u64 {
96 self.avg_vector_latency_us.load(Ordering::Relaxed)
97 }
98
99 /// Gets the average graph latency in microseconds.
100 #[must_use]
101 pub fn avg_graph_latency_us(&self) -> u64 {
102 self.avg_graph_latency_us.load(Ordering::Relaxed)
103 }
104
105 /// Gets the total number of vector queries.
106 #[must_use]
107 pub fn vector_query_count(&self) -> u64 {
108 self.vector_query_count.load(Ordering::Relaxed)
109 }
110
111 /// Gets the total number of graph queries.
112 #[must_use]
113 pub fn graph_query_count(&self) -> u64 {
114 self.graph_query_count.load(Ordering::Relaxed)
115 }
116}