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 query is accessed, providing the [`FullCacheKey`] for GC tracking.
200    ///
201    /// This is called at the start of each query execution, before `on_query_start`.
202    /// Use this to track access times or reference counts for garbage collection.
203    ///
204    /// # Example
205    ///
206    /// ```ignore
207    /// use query_flow::{FullCacheKey, Tracer, SpanId};
208    /// use std::collections::HashMap;
209    /// use std::sync::Mutex;
210    /// use std::time::Instant;
211    ///
212    /// struct GcTracer {
213    ///     access_times: Mutex<HashMap<FullCacheKey, Instant>>,
214    /// }
215    ///
216    /// impl Tracer for GcTracer {
217    ///     fn new_span_id(&self) -> SpanId { SpanId(0) }
218    ///
219    ///     fn on_query_key(&self, full_key: &FullCacheKey) {
220    ///         self.access_times.lock().unwrap()
221    ///             .insert(full_key.clone(), Instant::now());
222    ///     }
223    /// }
224    /// ```
225    #[inline]
226    fn on_query_key(&self, _full_key: &FullCacheKey) {}
227}
228
229/// Zero-cost tracer that discards all events.
230///
231/// This is the default tracer for [`QueryRuntime`](crate::QueryRuntime).
232pub struct NoopTracer;
233
234/// Global span counter for NoopTracer.
235static NOOP_SPAN_COUNTER: AtomicU64 = AtomicU64::new(1);
236
237impl Tracer for NoopTracer {
238    #[inline(always)]
239    fn new_span_id(&self) -> SpanId {
240        SpanId(NOOP_SPAN_COUNTER.fetch_add(1, Ordering::Relaxed))
241    }
242    // All other methods use the default empty implementations
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use std::sync::atomic::AtomicUsize;
249    use std::sync::Arc;
250
251    struct CountingTracer {
252        start_count: AtomicUsize,
253        end_count: AtomicUsize,
254    }
255
256    impl CountingTracer {
257        fn new() -> Self {
258            Self {
259                start_count: AtomicUsize::new(0),
260                end_count: AtomicUsize::new(0),
261            }
262        }
263    }
264
265    impl Tracer for CountingTracer {
266        fn new_span_id(&self) -> SpanId {
267            SpanId(1)
268        }
269
270        fn on_query_start(&self, _span_id: SpanId, _query: TracerQueryKey) {
271            self.start_count.fetch_add(1, Ordering::Relaxed);
272        }
273
274        fn on_query_end(&self, _span_id: SpanId, _query: TracerQueryKey, _result: ExecutionResult) {
275            self.end_count.fetch_add(1, Ordering::Relaxed);
276        }
277    }
278
279    #[test]
280    fn test_noop_tracer_span_id() {
281        let tracer = NoopTracer;
282        let id1 = tracer.new_span_id();
283        let id2 = tracer.new_span_id();
284        assert_ne!(id1, id2);
285    }
286
287    #[test]
288    fn test_counting_tracer() {
289        let tracer = CountingTracer::new();
290        let key = TracerQueryKey::new("TestQuery", "()");
291
292        tracer.on_query_start(SpanId(1), key.clone());
293        tracer.on_query_start(SpanId(2), key.clone());
294        tracer.on_query_end(SpanId(1), key, ExecutionResult::Changed);
295
296        assert_eq!(tracer.start_count.load(Ordering::Relaxed), 2);
297        assert_eq!(tracer.end_count.load(Ordering::Relaxed), 1);
298    }
299
300    #[test]
301    fn test_tracer_is_send_sync() {
302        fn assert_send_sync<T: Send + Sync>() {}
303        assert_send_sync::<NoopTracer>();
304        assert_send_sync::<Arc<CountingTracer>>();
305    }
306}