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}