Skip to main content

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}