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}