vibesql_executor/profiling.rs
1//! Performance profiling and debug utilities for understanding bottlenecks
2//!
3//! This module provides profiling infrastructure that integrates with the
4//! structured debug output system. When `VIBESQL_DEBUG_FORMAT=json`, profiling
5//! output is emitted as JSON for machine parsing.
6//!
7//! # Environment Variables
8//!
9//! ## Umbrella Debug Flag
10//!
11//! `VIBESQL_DEBUG` provides unified control over all semantic debug logging:
12//!
13//! - `VIBESQL_DEBUG=1` - Enable all semantic logging (optimizer decisions)
14//! - `VIBESQL_DEBUG=optimizer` - Enable only optimizer-related logging
15//! - `VIBESQL_DEBUG=scan` - Enable only scan path logging
16//! - `VIBESQL_DEBUG=dml` - Enable only DML-related logging
17//!
18//! ## Individual Flags (for fine-grained control)
19//!
20//! These flags work independently or are auto-enabled by `VIBESQL_DEBUG`:
21//!
22//! - `JOIN_REORDER_VERBOSE` - Join reorder decisions and costs
23//! - `SUBQUERY_TRANSFORM_VERBOSE` - Subquery-to-join transformations
24//! - `TABLE_ELIM_VERBOSE` - Table elimination decisions
25//! - `SCAN_PATH_VERBOSE` - Scan path selection (index vs table scan)
26//! - `INDEX_SELECT_DEBUG` - Index selection decisions
27//!
28//! ## Phase Timing Flags
29//!
30//! - `DELETE_PROFILE` - Delete operation phase timing
31//! - `JOIN_PROFILE` - Join execution phase timing
32//! - `RANGE_SCAN_PROFILE` - Range scan phase timing
33
34use crate::debug_output::{self, Category, DebugEvent};
35use instant::Instant;
36use std::sync::LazyLock;
37
38/// Debug level from VIBESQL_DEBUG environment variable (lazily initialized)
39/// 0 = disabled, 1 = all, 2 = optimizer, 3 = scan, 4 = dml
40static DEBUG_LEVEL: LazyLock<u8> = LazyLock::new(|| {
41 std::env::var("VIBESQL_DEBUG")
42 .ok()
43 .and_then(|v| match v.to_lowercase().as_str() {
44 "1" | "true" | "all" => Some(1),
45 "optimizer" => Some(2),
46 "scan" => Some(3),
47 "dml" => Some(4),
48 _ => v.parse().ok(),
49 })
50 .unwrap_or(0)
51});
52
53/// Profiling enabled flag (lazily initialized from VIBESQL_PROFILE)
54static PROFILING_ENABLED: LazyLock<bool> =
55 LazyLock::new(|| std::env::var("VIBESQL_PROFILE").is_ok());
56
57/// Debug categories enabled via VIBESQL_DEBUG
58#[derive(Clone, Copy, PartialEq, Eq)]
59pub enum DebugCategory {
60 /// All debug logging enabled
61 All,
62 /// Optimizer decisions (join reorder, subquery transform, table elimination)
63 Optimizer,
64 /// Scan path selection (index vs table scan, columnar)
65 Scan,
66 /// DML operations (cost model, delete profiling)
67 Dml,
68 /// Disabled
69 None,
70}
71
72// Thread-local cache for debug checks to avoid repeated env var lookups
73thread_local! {
74 static SCAN_PATH_VERBOSE_ENABLED: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
75}
76
77/// Initialize profiling based on environment variable.
78/// Also initializes the debug output format.
79pub fn init() {
80 // Initialize debug output format first
81 debug_output::init();
82
83 // Force lazy initialization of profiling flag
84 if *PROFILING_ENABLED {
85 debug_output::debug_event(Category::Profile, "init", "PROFILE")
86 .text("Profiling enabled")
87 .field_bool("enabled", true)
88 .emit();
89 }
90}
91
92/// Check if profiling is enabled
93pub fn is_enabled() -> bool {
94 *PROFILING_ENABLED
95}
96
97/// Get the current debug category
98pub fn debug_category() -> DebugCategory {
99 match *DEBUG_LEVEL {
100 0 => DebugCategory::None,
101 1 => DebugCategory::All,
102 2 => DebugCategory::Optimizer,
103 3 => DebugCategory::Scan,
104 4 => DebugCategory::Dml,
105 _ => DebugCategory::None,
106 }
107}
108
109/// Check if a specific debug category is enabled
110pub fn is_debug_enabled(category: DebugCategory) -> bool {
111 let current = debug_category();
112 current == DebugCategory::All || current == category
113}
114
115/// Check if scan path debug logging is enabled
116/// Returns true if VIBESQL_DEBUG=1, VIBESQL_DEBUG=scan, or SCAN_PATH_VERBOSE=1
117#[inline]
118pub fn is_scan_debug_enabled() -> bool {
119 // Use thread-local cache to avoid repeated env var lookups
120 SCAN_PATH_VERBOSE_ENABLED.with(|cache| {
121 if let Some(enabled) = cache.get() {
122 return enabled;
123 }
124
125 let enabled =
126 is_debug_enabled(DebugCategory::Scan) || std::env::var("SCAN_PATH_VERBOSE").is_ok();
127
128 cache.set(Some(enabled));
129 enabled
130 })
131}
132
133/// A profiling timer that logs elapsed time when dropped
134pub struct ProfileTimer {
135 label: &'static str,
136 start: Instant,
137 enabled: bool,
138}
139
140impl ProfileTimer {
141 /// Create a new profiling timer
142 pub fn new(label: &'static str) -> Self {
143 let enabled = is_enabled();
144 Self { label, start: Instant::now(), enabled }
145 }
146}
147
148impl Drop for ProfileTimer {
149 fn drop(&mut self) {
150 if self.enabled {
151 let elapsed = self.start.elapsed();
152 DebugEvent::new(Category::Profile, "timer", "PROFILE")
153 .text(format!(
154 "{} took {:.3}ms ({:.0}µs)",
155 self.label,
156 elapsed.as_secs_f64() * 1000.0,
157 elapsed.as_micros()
158 ))
159 .field_str("label", self.label)
160 .field_duration_ms("duration_ms", elapsed)
161 .field_duration_us("duration_us", elapsed)
162 .emit();
163 }
164 }
165}
166
167/// Macro to create a profiling scope
168#[macro_export]
169macro_rules! profile {
170 ($label:expr) => {
171 let _timer = $crate::profiling::ProfileTimer::new($label);
172 };
173}