1use std::fmt::{Debug, Display};
13use std::sync::Mutex;
14
15use tracing::{Level, span, subscriber};
16use tracing_core::{Interest, Metadata};
17use tracing_subscriber::{field, layer};
18
19use smallvec::SmallVec;
20
21#[allow(missing_debug_implementations)]
23pub struct PlanTrace<T> {
24 filter: Option<SmallVec<[&'static str; 4]>>,
28 path: Mutex<String>,
31 start: Mutex<Option<std::time::Instant>>,
33 times: Mutex<Vec<std::time::Instant>>,
37 entries: Mutex<Vec<TraceEntry<T>>>,
39}
40
41#[derive(Clone, Debug)]
43pub struct TraceEntry<T> {
44 pub instant: std::time::Instant,
49 pub span_duration: std::time::Duration,
51 pub full_duration: std::time::Duration,
53 pub path: String,
55 pub plan: T,
57}
58
59pub fn trace_plan<T: Clone + 'static>(plan: &T) {
107 tracing::Span::current().with_subscriber(|(_id, subscriber)| {
108 if let Some(trace) = subscriber.downcast_ref::<PlanTrace<T>>() {
109 trace.push(plan)
110 }
111 });
112}
113
114pub fn dbg_plan<S: Display, T: Clone + 'static>(segment: S, plan: &T) {
121 span!(target: "optimizer", Level::DEBUG, "segment", path.segment = %segment).in_scope(|| {
122 trace_plan(plan);
123 });
124}
125
126pub fn dbg_misc<S: Display, T: Display>(segment: S, misc: T) {
133 span!(target: "optimizer", Level::DEBUG, "segment", path.segment = %segment).in_scope(|| {
134 trace_plan(&misc.to_string());
135 });
136}
137
138#[allow(missing_debug_implementations)]
144pub struct ContextHash(u64);
145
146impl ContextHash {
147 pub fn of<T: std::hash::Hash>(t: T) -> Self {
148 use std::collections::hash_map::DefaultHasher;
149 use std::hash::Hasher;
150
151 let mut h = DefaultHasher::new();
152 t.hash(&mut h);
153 ContextHash(h.finish())
154 }
155}
156
157impl Display for ContextHash {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 write!(f, "{:x}", self.0 & 0xFFFFFFFu64) }
161}
162
163impl<S, T> layer::Layer<S> for PlanTrace<T>
169where
170 S: subscriber::Subscriber,
171 T: 'static,
172{
173 fn on_new_span(
174 &self,
175 attrs: &span::Attributes<'_>,
176 _id: &span::Id,
177 _ctx: layer::Context<'_, S>,
178 ) {
179 let mut path = self.path.lock().expect("path shouldn't be poisoned");
181 let segment = attrs.get_str("path.segment");
182 let segment = segment.unwrap_or_else(|| attrs.metadata().name().to_string());
183 if !path.is_empty() {
184 path.push('/');
185 }
186 path.push_str(segment.as_str());
187 }
188
189 fn on_enter(&self, _id: &span::Id, _ctx: layer::Context<'_, S>) {
190 let now = std::time::Instant::now();
191 let mut start = self.start.lock().expect("start shouldn't be poisoned");
193 start.get_or_insert(now);
194 let mut times = self.times.lock().expect("times shouldn't be poisoned");
196 times.push(now);
197 }
198
199 fn on_exit(&self, _id: &span::Id, _ctx: layer::Context<'_, S>) {
200 let mut path = self.path.lock().expect("path shouldn't be poisoned");
202 let new_len = path.rfind('/').unwrap_or(0);
203 path.truncate(new_len);
204 let mut times = self.times.lock().expect("times shouldn't be poisoned");
206 times.pop();
207 }
208}
209
210impl<S, T> layer::Filter<S> for PlanTrace<T>
211where
212 S: subscriber::Subscriber,
213 T: 'static + Clone,
214{
215 fn enabled(&self, meta: &Metadata<'_>, _cx: &layer::Context<'_, S>) -> bool {
216 self.is_enabled(meta)
217 }
218
219 fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
220 if self.is_enabled(meta) {
221 Interest::always()
222 } else {
223 Interest::never()
224 }
225 }
226}
227
228impl<T: 'static + Clone> PlanTrace<T> {
229 pub fn new(filter: Option<SmallVec<[&'static str; 4]>>) -> Self {
232 Self {
233 filter,
234 path: Mutex::new(String::with_capacity(256)),
235 start: Mutex::new(None),
236 times: Mutex::new(Default::default()),
237 entries: Mutex::new(Default::default()),
238 }
239 }
240
241 fn is_enabled(&self, meta: &Metadata<'_>) -> bool {
244 meta.is_span() && meta.target() == "optimizer"
245 }
246
247 pub fn drain_as_vec(&self) -> Vec<TraceEntry<T>> {
253 let mut entries = self.entries.lock().expect("entries shouldn't be poisoned");
254 entries.split_off(0)
255 }
256
257 pub fn collect_as_vec(&self) -> Vec<TraceEntry<T>> {
259 let entries = self.entries.lock().expect("entries shouldn't be poisoned");
260 (*entries).clone()
261 }
262
263 pub fn find(&self, path: &str) -> Option<TraceEntry<T>>
265 where
266 T: Clone,
267 {
268 let entries = self.entries.lock().expect("entries shouldn't be poisoned");
269 entries.iter().find(|entry| entry.path == path).cloned()
270 }
271
272 fn push(&self, plan: &T)
278 where
279 T: Clone,
280 {
281 if let Some(current_path) = self.current_path() {
282 let times = self.times.lock().expect("times shouldn't be poisoned");
283 let start = self.start.lock().expect("start shouldn't is poisoned");
284 if let (Some(full_start), Some(span_start)) = (start.as_ref(), times.last()) {
285 let mut entries = self.entries.lock().expect("entries shouldn't be poisoned");
286 let time = std::time::Instant::now();
287 entries.push(TraceEntry {
288 instant: time,
289 span_duration: time.duration_since(*span_start),
290 full_duration: time.duration_since(*full_start),
291 path: current_path,
292 plan: plan.clone(),
293 });
294 }
295 }
296 }
297
298 fn current_path(&self) -> Option<String> {
303 let path = self.path.lock().expect("path shouldn't be poisoned");
304 let path = path.as_str();
305 match self.filter.as_ref() {
306 Some(named_paths) => {
307 if named_paths.contains(&path) {
308 Some(path.to_owned())
309 } else {
310 None
311 }
312 }
313 None => Some(path.to_owned()),
314 }
315 }
316}
317
318trait GetStr {
320 fn get_str(&self, key: &'static str) -> Option<String>;
321}
322
323impl<'a> GetStr for span::Attributes<'a> {
324 fn get_str(&self, key: &'static str) -> Option<String> {
325 let mut extract_str = ExtractStr::new(key);
326 self.record(&mut extract_str);
327 extract_str.val()
328 }
329}
330
331struct ExtractStr {
334 key: &'static str,
335 val: Option<String>,
336}
337
338impl ExtractStr {
339 fn new(key: &'static str) -> Self {
340 Self { key, val: None }
341 }
342
343 fn val(self) -> Option<String> {
344 self.val
345 }
346}
347
348impl field::Visit for ExtractStr {
349 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
350 if field.name() == self.key {
351 self.val = Some(value.to_string())
352 }
353 }
354
355 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
356 if field.name() == self.key {
357 self.val = Some(format!("{value:?}"))
358 }
359 }
360}
361
362#[cfg(test)]
363mod test {
364 use mz_ore::instrument;
365 use tracing::dispatcher;
366 use tracing_subscriber::prelude::*;
367
368 use super::{PlanTrace, trace_plan};
369
370 #[mz_ore::test]
371 fn test_optimizer_trace() {
372 let subscriber = tracing_subscriber::registry().with(Some(PlanTrace::<String>::new(None)));
373 let dispatch = dispatcher::Dispatch::new(subscriber);
374
375 dispatcher::with_default(&dispatch, || {
376 optimize();
377 });
378
379 if let Some(trace) = dispatch.downcast_ref::<PlanTrace<String>>() {
380 let trace = trace.drain_as_vec();
381 assert_eq!(trace.len(), 5);
382 for (i, entry) in trace.into_iter().enumerate() {
383 let path = entry.path;
384 match i {
385 0 => {
386 assert_eq!(path, "optimize");
387 }
388 1 => {
389 assert_eq!(path, "optimize/logical/my_optimization");
390 }
391 2 => {
392 assert_eq!(path, "optimize/logical");
393 }
394 3 => {
395 assert_eq!(path, "optimize/physical");
396 }
397 4 => {
398 assert_eq!(path, "optimize");
399 }
400 _ => (),
401 }
402 }
403 }
404 }
405
406 #[instrument(level = "info")]
407 fn optimize() {
408 let mut plan = constant_plan(42);
409 trace_plan(&plan);
410 logical_optimizer(&mut plan);
411 physical_optimizer(&mut plan);
412 trace_plan(&plan);
413 }
414
415 #[instrument(level = "info", name = "logical")]
416 fn logical_optimizer(plan: &mut String) {
417 some_optimization(plan);
418 *plan = plan.replace("RawPlan", "LogicalPlan");
419 trace_plan(plan);
420 }
421
422 #[instrument(level = "info", name = "physical")]
423 fn physical_optimizer(plan: &mut String) {
424 *plan = plan.replace("LogicalPlan", "PhysicalPlan");
425 trace_plan(plan);
426 }
427
428 #[mz_ore::instrument(level = "debug", fields(path.segment ="my_optimization"))]
429 fn some_optimization(plan: &mut String) {
430 *plan = plan.replace("42", "47");
431 trace_plan(plan);
432 }
433
434 fn constant_plan(i: usize) -> String {
435 format!("RawPlan(#{})", i)
436 }
437}