query_flow/
tracer.rs

1//! Tracer trait for observing query-flow execution.
2//!
3//! This module defines the [`Tracer`] trait and related types for observing
4//! query execution. The default [`NoopTracer`] provides zero-cost when tracing
5//! is not needed.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use query_flow::{QueryRuntime, Tracer, SpanId};
11//!
12//! // Custom tracer implementation
13//! struct MyTracer;
14//!
15//! impl Tracer for MyTracer {
16//!     fn new_span_id(&self) -> SpanId {
17//!         SpanId(1)
18//!     }
19//!
20//!     fn on_query_start(&self, span_id: SpanId, query: TracerQueryKey) {
21//!         println!("Query started: {:?}", query);
22//!     }
23//! }
24//!
25//! let runtime = QueryRuntime::with_tracer(MyTracer);
26//! ```
27
28use serde::{Deserialize, Serialize};
29use std::sync::atomic::{AtomicU64, Ordering};
30
31use crate::key::FullCacheKey;
32
33/// Unique identifier for a query execution span.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub struct SpanId(pub u64);
36
37/// Represents a query key in a type-erased manner for tracing.
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39pub struct TracerQueryKey {
40    /// The query type name (e.g., "calc::ParseExpr")
41    pub query_type: &'static str,
42    /// Debug representation of the cache key (e.g., "(\"main.txt\",)")
43    pub cache_key_debug: String,
44}
45
46impl TracerQueryKey {
47    /// Create a new tracer query key.
48    #[inline]
49    pub fn new(query_type: &'static str, cache_key_debug: impl Into<String>) -> Self {
50        Self {
51            query_type,
52            cache_key_debug: cache_key_debug.into(),
53        }
54    }
55}
56
57/// Represents an asset key in a type-erased manner for tracing.
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub struct TracerAssetKey {
60    /// The asset type name (e.g., "calc::SourceFile")
61    pub asset_type: &'static str,
62    /// Debug representation of the key (e.g., "SourceFile(\"main.txt\")")
63    pub key_debug: String,
64}
65
66impl TracerAssetKey {
67    /// Create a new tracer asset key.
68    #[inline]
69    pub fn new(asset_type: &'static str, key_debug: impl Into<String>) -> Self {
70        Self {
71            asset_type,
72            key_debug: key_debug.into(),
73        }
74    }
75}
76
77/// Query execution result classification.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum ExecutionResult {
80    /// Query computed a new value (output changed).
81    Changed,
82    /// Query completed but output unchanged (early cutoff applied).
83    Unchanged,
84    /// Query returned cached value without execution.
85    CacheHit,
86    /// Query suspended waiting for async loading.
87    Suspended,
88    /// Query detected a dependency cycle.
89    CycleDetected,
90    /// Query failed with an error.
91    Error { message: String },
92}
93
94/// Asset loading state for tracing.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum TracerAssetState {
97    /// Asset is currently loading.
98    Loading,
99    /// Asset is ready with a value.
100    Ready,
101    /// Asset was not found.
102    NotFound,
103}
104
105/// Reason for cache invalidation.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum InvalidationReason {
108    /// A dependency query changed its output.
109    DependencyChanged { dep: TracerQueryKey },
110    /// An asset dependency was updated.
111    AssetChanged { asset: TracerAssetKey },
112    /// Manual invalidation was triggered.
113    ManualInvalidation,
114    /// An asset was removed.
115    AssetRemoved { asset: TracerAssetKey },
116}
117
118/// Tracer trait for observing query-flow execution.
119///
120/// Implementations can collect events for testing, forward to the `tracing` crate,
121/// or provide custom observability.
122///
123/// All methods have default empty implementations, so you only need to override
124/// the events you're interested in. The [`NoopTracer`] uses all defaults for
125/// zero-cost when tracing is disabled.
126///
127/// # Thread Safety
128///
129/// Implementations must be `Send + Sync` as the tracer may be called from
130/// multiple threads concurrently.
131pub trait Tracer: Send + Sync + 'static {
132    /// Generate a new unique span ID.
133    ///
134    /// This is the only required method. Called at the start of each query execution.
135    fn new_span_id(&self) -> SpanId;
136
137    /// Called when a query execution starts.
138    #[inline]
139    fn on_query_start(&self, _span_id: SpanId, _query: TracerQueryKey) {}
140
141    /// Called when cache validity is checked.
142    #[inline]
143    fn on_cache_check(&self, _span_id: SpanId, _query: TracerQueryKey, _valid: bool) {}
144
145    /// Called when a query execution ends.
146    #[inline]
147    fn on_query_end(&self, _span_id: SpanId, _query: TracerQueryKey, _result: ExecutionResult) {}
148
149    /// Called when a query dependency is registered during execution.
150    #[inline]
151    fn on_dependency_registered(
152        &self,
153        _span_id: SpanId,
154        _parent: TracerQueryKey,
155        _dependency: TracerQueryKey,
156    ) {
157    }
158
159    /// Called when an asset dependency is registered during execution.
160    #[inline]
161    fn on_asset_dependency_registered(
162        &self,
163        _span_id: SpanId,
164        _parent: TracerQueryKey,
165        _asset: TracerAssetKey,
166    ) {
167    }
168
169    /// Called when early cutoff comparison is performed.
170    #[inline]
171    fn on_early_cutoff_check(
172        &self,
173        _span_id: SpanId,
174        _query: TracerQueryKey,
175        _output_changed: bool,
176    ) {
177    }
178
179    /// Called when an asset is requested.
180    #[inline]
181    fn on_asset_requested(&self, _asset: TracerAssetKey, _state: TracerAssetState) {}
182
183    /// Called when an asset is resolved with a value.
184    #[inline]
185    fn on_asset_resolved(&self, _asset: TracerAssetKey, _changed: bool) {}
186
187    /// Called when an asset is invalidated.
188    #[inline]
189    fn on_asset_invalidated(&self, _asset: TracerAssetKey) {}
190
191    /// Called when a query is invalidated.
192    #[inline]
193    fn on_query_invalidated(&self, _query: TracerQueryKey, _reason: InvalidationReason) {}
194
195    /// Called when a dependency cycle is detected.
196    #[inline]
197    fn on_cycle_detected(&self, _path: Vec<TracerQueryKey>) {}
198
199    /// Called when a missing dependency error occurs.
200    #[inline]
201    fn on_missing_dependency(&self, _query: TracerQueryKey, _dependency_description: String) {}
202
203    /// Called when a query is accessed, providing the [`FullCacheKey`] for GC tracking.
204    ///
205    /// This is called at the start of each query execution, before `on_query_start`.
206    /// Use this to track access times or reference counts for garbage collection.
207    ///
208    /// # Example
209    ///
210    /// ```ignore
211    /// use query_flow::{FullCacheKey, Tracer, SpanId};
212    /// use std::collections::HashMap;
213    /// use std::sync::Mutex;
214    /// use std::time::Instant;
215    ///
216    /// struct GcTracer {
217    ///     access_times: Mutex<HashMap<FullCacheKey, Instant>>,
218    /// }
219    ///
220    /// impl Tracer for GcTracer {
221    ///     fn new_span_id(&self) -> SpanId { SpanId(0) }
222    ///
223    ///     fn on_query_key(&self, full_key: &FullCacheKey) {
224    ///         self.access_times.lock().unwrap()
225    ///             .insert(full_key.clone(), Instant::now());
226    ///     }
227    /// }
228    /// ```
229    #[inline]
230    fn on_query_key(&self, _full_key: &FullCacheKey) {}
231}
232
233/// Zero-cost tracer that discards all events.
234///
235/// This is the default tracer for [`QueryRuntime`](crate::QueryRuntime).
236pub struct NoopTracer;
237
238/// Global span counter for NoopTracer.
239static NOOP_SPAN_COUNTER: AtomicU64 = AtomicU64::new(1);
240
241impl Tracer for NoopTracer {
242    #[inline(always)]
243    fn new_span_id(&self) -> SpanId {
244        SpanId(NOOP_SPAN_COUNTER.fetch_add(1, Ordering::Relaxed))
245    }
246    // All other methods use the default empty implementations
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use std::sync::atomic::AtomicUsize;
253    use std::sync::Arc;
254
255    struct CountingTracer {
256        start_count: AtomicUsize,
257        end_count: AtomicUsize,
258    }
259
260    impl CountingTracer {
261        fn new() -> Self {
262            Self {
263                start_count: AtomicUsize::new(0),
264                end_count: AtomicUsize::new(0),
265            }
266        }
267    }
268
269    impl Tracer for CountingTracer {
270        fn new_span_id(&self) -> SpanId {
271            SpanId(1)
272        }
273
274        fn on_query_start(&self, _span_id: SpanId, _query: TracerQueryKey) {
275            self.start_count.fetch_add(1, Ordering::Relaxed);
276        }
277
278        fn on_query_end(&self, _span_id: SpanId, _query: TracerQueryKey, _result: ExecutionResult) {
279            self.end_count.fetch_add(1, Ordering::Relaxed);
280        }
281    }
282
283    #[test]
284    fn test_noop_tracer_span_id() {
285        let tracer = NoopTracer;
286        let id1 = tracer.new_span_id();
287        let id2 = tracer.new_span_id();
288        assert_ne!(id1, id2);
289    }
290
291    #[test]
292    fn test_counting_tracer() {
293        let tracer = CountingTracer::new();
294        let key = TracerQueryKey::new("TestQuery", "()");
295
296        tracer.on_query_start(SpanId(1), key.clone());
297        tracer.on_query_start(SpanId(2), key.clone());
298        tracer.on_query_end(SpanId(1), key, ExecutionResult::Changed);
299
300        assert_eq!(tracer.start_count.load(Ordering::Relaxed), 2);
301        assert_eq!(tracer.end_count.load(Ordering::Relaxed), 1);
302    }
303
304    #[test]
305    fn test_tracer_is_send_sync() {
306        fn assert_send_sync<T: Send + Sync>() {}
307        assert_send_sync::<NoopTracer>();
308        assert_send_sync::<Arc<CountingTracer>>();
309    }
310}