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