Skip to main content

laminar_core/compiler/
compilation_metrics.rs

1//! Compilation metrics and observability for the JIT pipeline.
2//!
3//! [`CompilationMetrics`] tracks atomic counters for compiled, fallback, and
4//! errored queries along with cumulative compilation time. [`CacheSnapshot`]
5//! provides a point-in-time view of the compiler cache state.
6//!
7//! All counters use `Relaxed` ordering — these are advisory/observability
8//! values, not synchronization primitives.
9
10use std::sync::atomic::{AtomicU64, Ordering};
11
12/// Global metrics for the JIT compilation pipeline.
13///
14/// All counters are atomic and use `Relaxed` ordering for minimal overhead
15/// (< 5ns per increment). Intended for dashboards and diagnostics, not
16/// for correctness-critical decisions.
17#[derive(Debug)]
18pub struct CompilationMetrics {
19    /// Queries successfully compiled via JIT.
20    queries_compiled: AtomicU64,
21    /// Queries that fell back to `DataFusion` interpreted execution.
22    queries_fallback: AtomicU64,
23    /// Queries where compilation failed with an error.
24    queries_error: AtomicU64,
25    /// Total compilation time (nanoseconds).
26    compile_time_total_ns: AtomicU64,
27}
28
29impl CompilationMetrics {
30    /// Creates a new metrics instance with all counters at zero.
31    #[must_use]
32    pub fn new() -> Self {
33        Self {
34            queries_compiled: AtomicU64::new(0),
35            queries_fallback: AtomicU64::new(0),
36            queries_error: AtomicU64::new(0),
37            compile_time_total_ns: AtomicU64::new(0),
38        }
39    }
40
41    /// Records a successful JIT compilation.
42    pub fn record_compiled(&self, compile_time_ns: u64) {
43        self.queries_compiled.fetch_add(1, Ordering::Relaxed);
44        self.compile_time_total_ns
45            .fetch_add(compile_time_ns, Ordering::Relaxed);
46    }
47
48    /// Records a fallback to interpreted execution (plan not compilable).
49    pub fn record_fallback(&self) {
50        self.queries_fallback.fetch_add(1, Ordering::Relaxed);
51    }
52
53    /// Records a compilation error.
54    pub fn record_error(&self) {
55        self.queries_error.fetch_add(1, Ordering::Relaxed);
56    }
57
58    /// Returns the number of successfully compiled queries.
59    #[must_use]
60    pub fn compiled_count(&self) -> u64 {
61        self.queries_compiled.load(Ordering::Relaxed)
62    }
63
64    /// Returns the number of fallback queries.
65    #[must_use]
66    pub fn fallback_count(&self) -> u64 {
67        self.queries_fallback.load(Ordering::Relaxed)
68    }
69
70    /// Returns the number of errored queries.
71    #[must_use]
72    pub fn error_count(&self) -> u64 {
73        self.queries_error.load(Ordering::Relaxed)
74    }
75
76    /// Returns total compilation time in nanoseconds.
77    #[must_use]
78    pub fn compile_time_total_ns(&self) -> u64 {
79        self.compile_time_total_ns.load(Ordering::Relaxed)
80    }
81
82    /// Returns total number of queries (compiled + fallback + error).
83    #[must_use]
84    pub fn total_queries(&self) -> u64 {
85        self.compiled_count() + self.fallback_count() + self.error_count()
86    }
87
88    /// Takes a snapshot of all metrics.
89    #[must_use]
90    pub fn snapshot(&self) -> MetricsSnapshot {
91        MetricsSnapshot {
92            queries_compiled: self.compiled_count(),
93            queries_fallback: self.fallback_count(),
94            queries_error: self.error_count(),
95            compile_time_total_ns: self.compile_time_total_ns(),
96        }
97    }
98}
99
100impl Default for CompilationMetrics {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106/// Point-in-time snapshot of [`CompilationMetrics`].
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct MetricsSnapshot {
109    /// Queries successfully compiled.
110    pub queries_compiled: u64,
111    /// Queries that fell back to interpreted.
112    pub queries_fallback: u64,
113    /// Queries where compilation errored.
114    pub queries_error: u64,
115    /// Total compilation time (nanoseconds).
116    pub compile_time_total_ns: u64,
117}
118
119impl MetricsSnapshot {
120    /// Returns total queries across all outcomes.
121    #[must_use]
122    pub fn total_queries(&self) -> u64 {
123        self.queries_compiled + self.queries_fallback + self.queries_error
124    }
125
126    /// Returns the JIT compilation rate as a fraction (0.0–1.0).
127    ///
128    /// Returns 0.0 if no queries have been processed.
129    #[must_use]
130    #[allow(clippy::cast_precision_loss)]
131    pub fn compilation_rate(&self) -> f64 {
132        let total = self.total_queries();
133        if total == 0 {
134            return 0.0;
135        }
136        self.queries_compiled as f64 / total as f64
137    }
138}
139
140/// Point-in-time snapshot of the compiler cache state.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub struct CacheSnapshot {
143    /// Number of cached entries.
144    pub entries: usize,
145    /// Maximum capacity.
146    pub capacity: usize,
147    /// Cache hits.
148    pub hits: u64,
149    /// Cache misses (triggered compilation).
150    pub misses: u64,
151    /// Cache evictions.
152    pub evictions: u64,
153}
154
155impl CacheSnapshot {
156    /// Returns the hit rate as a fraction (0.0–1.0).
157    ///
158    /// Returns 0.0 if no lookups have occurred.
159    #[must_use]
160    #[allow(clippy::cast_precision_loss)]
161    pub fn hit_rate(&self) -> f64 {
162        let total = self.hits + self.misses;
163        if total == 0 {
164            return 0.0;
165        }
166        self.hits as f64 / total as f64
167    }
168
169    /// Returns the fill ratio (entries / capacity).
170    #[must_use]
171    #[allow(clippy::cast_precision_loss)]
172    pub fn fill_ratio(&self) -> f64 {
173        if self.capacity == 0 {
174            return 0.0;
175        }
176        self.entries as f64 / self.capacity as f64
177    }
178}
179
180#[cfg(test)]
181#[allow(clippy::float_cmp)]
182mod tests {
183    use super::*;
184
185    // ── CompilationMetrics tests ─────────────────────────────────
186
187    #[test]
188    fn metrics_initial_zero() {
189        let m = CompilationMetrics::new();
190        assert_eq!(m.compiled_count(), 0);
191        assert_eq!(m.fallback_count(), 0);
192        assert_eq!(m.error_count(), 0);
193        assert_eq!(m.compile_time_total_ns(), 0);
194        assert_eq!(m.total_queries(), 0);
195    }
196
197    #[test]
198    fn metrics_record_compiled() {
199        let m = CompilationMetrics::new();
200        m.record_compiled(1_000_000);
201        m.record_compiled(2_000_000);
202        assert_eq!(m.compiled_count(), 2);
203        assert_eq!(m.compile_time_total_ns(), 3_000_000);
204    }
205
206    #[test]
207    fn metrics_record_fallback() {
208        let m = CompilationMetrics::new();
209        m.record_fallback();
210        m.record_fallback();
211        m.record_fallback();
212        assert_eq!(m.fallback_count(), 3);
213    }
214
215    #[test]
216    fn metrics_record_error() {
217        let m = CompilationMetrics::new();
218        m.record_error();
219        assert_eq!(m.error_count(), 1);
220    }
221
222    #[test]
223    fn metrics_total_queries() {
224        let m = CompilationMetrics::new();
225        m.record_compiled(100);
226        m.record_compiled(200);
227        m.record_fallback();
228        m.record_error();
229        assert_eq!(m.total_queries(), 4);
230    }
231
232    #[test]
233    fn metrics_default() {
234        let m = CompilationMetrics::default();
235        assert_eq!(m.compiled_count(), 0);
236    }
237
238    // ── MetricsSnapshot tests ────────────────────────────────────
239
240    #[test]
241    fn snapshot_captures_state() {
242        let m = CompilationMetrics::new();
243        m.record_compiled(500);
244        m.record_fallback();
245
246        let snap = m.snapshot();
247        assert_eq!(snap.queries_compiled, 1);
248        assert_eq!(snap.queries_fallback, 1);
249        assert_eq!(snap.queries_error, 0);
250        assert_eq!(snap.compile_time_total_ns, 500);
251        assert_eq!(snap.total_queries(), 2);
252    }
253
254    #[test]
255    fn snapshot_compilation_rate() {
256        let snap = MetricsSnapshot {
257            queries_compiled: 3,
258            queries_fallback: 1,
259            queries_error: 0,
260            compile_time_total_ns: 0,
261        };
262        let rate = snap.compilation_rate();
263        assert!((rate - 0.75).abs() < f64::EPSILON);
264    }
265
266    #[test]
267    fn snapshot_compilation_rate_zero() {
268        let snap = MetricsSnapshot {
269            queries_compiled: 0,
270            queries_fallback: 0,
271            queries_error: 0,
272            compile_time_total_ns: 0,
273        };
274        assert_eq!(snap.compilation_rate(), 0.0);
275    }
276
277    // ── CacheSnapshot tests ──────────────────────────────────────
278
279    #[test]
280    fn cache_snapshot_hit_rate() {
281        let snap = CacheSnapshot {
282            entries: 10,
283            capacity: 64,
284            hits: 80,
285            misses: 20,
286            evictions: 5,
287        };
288        assert!((snap.hit_rate() - 0.8).abs() < f64::EPSILON);
289    }
290
291    #[test]
292    fn cache_snapshot_hit_rate_zero() {
293        let snap = CacheSnapshot {
294            entries: 0,
295            capacity: 64,
296            hits: 0,
297            misses: 0,
298            evictions: 0,
299        };
300        assert_eq!(snap.hit_rate(), 0.0);
301    }
302
303    #[test]
304    fn cache_snapshot_fill_ratio() {
305        let snap = CacheSnapshot {
306            entries: 32,
307            capacity: 64,
308            hits: 0,
309            misses: 0,
310            evictions: 0,
311        };
312        assert!((snap.fill_ratio() - 0.5).abs() < f64::EPSILON);
313    }
314
315    #[test]
316    fn cache_snapshot_fill_ratio_zero_capacity() {
317        let snap = CacheSnapshot {
318            entries: 0,
319            capacity: 0,
320            hits: 0,
321            misses: 0,
322            evictions: 0,
323        };
324        assert_eq!(snap.fill_ratio(), 0.0);
325    }
326
327    #[test]
328    fn cache_snapshot_debug() {
329        let snap = CacheSnapshot {
330            entries: 5,
331            capacity: 64,
332            hits: 10,
333            misses: 2,
334            evictions: 1,
335        };
336        let s = format!("{snap:?}");
337        assert!(s.contains("CacheSnapshot"));
338        assert!(s.contains("entries: 5"));
339    }
340}