icydb_core/db/diagnostics/
execution_trace.rs1use crate::db::{
7 access::{AccessPathKind, AccessPlan, AccessPlanDispatch, dispatch_access_plan},
8 direction::Direction,
9 query::plan::OrderDirection,
10};
11#[cfg(test)]
12use std::cell::Cell;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum ExecutionAccessPathVariant {
22 ByKey,
23 ByKeys,
24 KeyRange,
25 IndexPrefix,
26 IndexMultiLookup,
27 IndexRange,
28 FullScan,
29 Union,
30 Intersection,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum ExecutionOptimization {
41 PrimaryKey,
42 PrimaryKeyTopNSeek,
43 SecondaryOrderPushdown,
44 SecondaryOrderTopNSeek,
45 IndexRangeLimitPushdown,
46}
47
48#[expect(clippy::enum_variant_names)]
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub(crate) enum ExecutionOptimizationCounter {
58 BytesPrimaryKeyFastPath,
59 BytesStreamFastPath,
60 CoveringExistsFastPath,
61 CoveringCountFastPath,
62 PrimaryKeyCountFastPath,
63 PrimaryKeyCardinalityCountFastPath,
64 CoveringIndexProjectionFastPath,
65 CoveringConstantProjectionFastPath,
66 NumericFieldStreamingFoldFastPath,
67}
68
69impl ExecutionOptimizationCounter {
70 #[cfg(test)]
71 const CARDINALITY: usize = 9;
72
73 #[cfg(test)]
74 const fn index(self) -> usize {
75 match self {
76 Self::BytesPrimaryKeyFastPath => 0,
77 Self::BytesStreamFastPath => 1,
78 Self::CoveringExistsFastPath => 2,
79 Self::CoveringCountFastPath => 3,
80 Self::PrimaryKeyCountFastPath => 4,
81 Self::PrimaryKeyCardinalityCountFastPath => 5,
82 Self::CoveringIndexProjectionFastPath => 6,
83 Self::CoveringConstantProjectionFastPath => 7,
84 Self::NumericFieldStreamingFoldFastPath => 8,
85 }
86 }
87}
88
89#[cfg(test)]
90thread_local! {
91 static EXECUTION_OPTIMIZATION_HITS: Cell<[u64; ExecutionOptimizationCounter::CARDINALITY]> =
92 const { Cell::new([0; ExecutionOptimizationCounter::CARDINALITY]) };
93}
94
95#[cfg(test)]
96pub(crate) fn take_execution_optimization_hits_for_tests(
97 optimization: ExecutionOptimizationCounter,
98) -> u64 {
99 EXECUTION_OPTIMIZATION_HITS.with(|counter| {
100 let mut hits = counter.get();
101 let index = optimization.index();
102 let value = hits[index];
103 hits[index] = 0;
104 counter.set(hits);
105 value
106 })
107}
108
109#[cfg(test)]
110pub(crate) fn record_execution_optimization_hit_for_tests(
111 optimization: ExecutionOptimizationCounter,
112) {
113 EXECUTION_OPTIMIZATION_HITS.with(|counter| {
114 let mut hits = counter.get();
115 let index = optimization.index();
116 hits[index] = hits[index].saturating_add(1);
117 counter.set(hits);
118 });
119}
120
121#[cfg(not(test))]
122pub(crate) const fn record_execution_optimization_hit_for_tests(
123 _optimization: ExecutionOptimizationCounter,
124) {
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub struct ExecutionTrace {
136 pub(crate) access_path_variant: ExecutionAccessPathVariant,
137 pub(crate) direction: OrderDirection,
138 pub(crate) optimization: Option<ExecutionOptimization>,
139 pub(crate) keys_scanned: u64,
140 pub(crate) rows_materialized: u64,
141 pub(crate) rows_returned: u64,
142 pub(crate) execution_time_micros: u64,
143 pub(crate) index_only: bool,
144 pub(crate) continuation_applied: bool,
145 pub(crate) index_predicate_applied: bool,
146 pub(crate) index_predicate_keys_rejected: u64,
147 pub(crate) distinct_keys_deduped: u64,
148}
149
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct ExecutionMetrics {
159 pub(crate) rows_scanned: u64,
160 pub(crate) rows_materialized: u64,
161 pub(crate) execution_time_micros: u64,
162 pub(crate) index_only: bool,
163}
164
165impl ExecutionTrace {
166 #[must_use]
168 pub(in crate::db) fn new<K>(
169 access: &AccessPlan<K>,
170 direction: Direction,
171 continuation_applied: bool,
172 ) -> Self {
173 Self {
174 access_path_variant: access_path_variant(access),
175 direction: execution_order_direction(direction),
176 optimization: None,
177 keys_scanned: 0,
178 rows_materialized: 0,
179 rows_returned: 0,
180 execution_time_micros: 0,
181 index_only: false,
182 continuation_applied,
183 index_predicate_applied: false,
184 index_predicate_keys_rejected: 0,
185 distinct_keys_deduped: 0,
186 }
187 }
188
189 #[expect(clippy::too_many_arguments)]
191 pub(in crate::db) fn set_path_outcome(
192 &mut self,
193 optimization: Option<ExecutionOptimization>,
194 keys_scanned: usize,
195 rows_materialized: usize,
196 rows_returned: usize,
197 execution_time_micros: u64,
198 index_only: bool,
199 index_predicate_applied: bool,
200 index_predicate_keys_rejected: u64,
201 distinct_keys_deduped: u64,
202 ) {
203 self.optimization = optimization;
204 self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
205 self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
206 self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
207 self.execution_time_micros = execution_time_micros;
208 self.index_only = index_only;
209 self.index_predicate_applied = index_predicate_applied;
210 self.index_predicate_keys_rejected = index_predicate_keys_rejected;
211 self.distinct_keys_deduped = distinct_keys_deduped;
212 debug_assert_eq!(
213 self.keys_scanned,
214 u64::try_from(keys_scanned).unwrap_or(u64::MAX),
215 "execution trace keys_scanned must match rows_scanned metrics input",
216 );
217 }
218
219 #[must_use]
221 pub const fn metrics(&self) -> ExecutionMetrics {
222 ExecutionMetrics {
223 rows_scanned: self.keys_scanned,
224 rows_materialized: self.rows_materialized,
225 execution_time_micros: self.execution_time_micros,
226 index_only: self.index_only,
227 }
228 }
229
230 #[must_use]
232 pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
233 self.access_path_variant
234 }
235
236 #[must_use]
238 pub const fn direction(&self) -> OrderDirection {
239 self.direction
240 }
241
242 #[must_use]
244 pub const fn optimization(&self) -> Option<ExecutionOptimization> {
245 self.optimization
246 }
247
248 #[must_use]
250 pub const fn keys_scanned(&self) -> u64 {
251 self.keys_scanned
252 }
253
254 #[must_use]
256 pub const fn rows_materialized(&self) -> u64 {
257 self.rows_materialized
258 }
259
260 #[must_use]
262 pub const fn rows_returned(&self) -> u64 {
263 self.rows_returned
264 }
265
266 #[must_use]
268 pub const fn execution_time_micros(&self) -> u64 {
269 self.execution_time_micros
270 }
271
272 #[must_use]
274 pub const fn index_only(&self) -> bool {
275 self.index_only
276 }
277
278 #[must_use]
280 pub const fn continuation_applied(&self) -> bool {
281 self.continuation_applied
282 }
283
284 #[must_use]
286 pub const fn index_predicate_applied(&self) -> bool {
287 self.index_predicate_applied
288 }
289
290 #[must_use]
292 pub const fn index_predicate_keys_rejected(&self) -> u64 {
293 self.index_predicate_keys_rejected
294 }
295
296 #[must_use]
298 pub const fn distinct_keys_deduped(&self) -> u64 {
299 self.distinct_keys_deduped
300 }
301}
302
303impl ExecutionMetrics {
304 #[must_use]
306 pub const fn rows_scanned(&self) -> u64 {
307 self.rows_scanned
308 }
309
310 #[must_use]
312 pub const fn rows_materialized(&self) -> u64 {
313 self.rows_materialized
314 }
315
316 #[must_use]
318 pub const fn execution_time_micros(&self) -> u64 {
319 self.execution_time_micros
320 }
321
322 #[must_use]
324 pub const fn index_only(&self) -> bool {
325 self.index_only
326 }
327}
328
329fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
330 match dispatch_access_plan(access) {
331 AccessPlanDispatch::Path(path) => match path.kind() {
332 AccessPathKind::ByKey => ExecutionAccessPathVariant::ByKey,
333 AccessPathKind::ByKeys => ExecutionAccessPathVariant::ByKeys,
334 AccessPathKind::KeyRange => ExecutionAccessPathVariant::KeyRange,
335 AccessPathKind::IndexPrefix => ExecutionAccessPathVariant::IndexPrefix,
336 AccessPathKind::IndexMultiLookup => ExecutionAccessPathVariant::IndexMultiLookup,
337 AccessPathKind::IndexRange => ExecutionAccessPathVariant::IndexRange,
338 AccessPathKind::FullScan => ExecutionAccessPathVariant::FullScan,
339 },
340 AccessPlanDispatch::Union(_) => ExecutionAccessPathVariant::Union,
341 AccessPlanDispatch::Intersection(_) => ExecutionAccessPathVariant::Intersection,
342 }
343}
344
345const fn execution_order_direction(direction: Direction) -> OrderDirection {
346 match direction {
347 Direction::Asc => OrderDirection::Asc,
348 Direction::Desc => OrderDirection::Desc,
349 }
350}
351
352#[cfg(test)]
357mod tests {
358 use crate::db::{
359 access::AccessPlan,
360 diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
361 direction::Direction,
362 };
363
364 #[test]
365 fn execution_trace_metrics_projection_exposes_requested_surface() {
366 let access = AccessPlan::by_key(11u64);
367 let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
368 trace.set_path_outcome(
369 Some(ExecutionOptimization::PrimaryKey),
370 5,
371 3,
372 2,
373 42,
374 true,
375 true,
376 7,
377 9,
378 );
379
380 let metrics = trace.metrics();
381 assert_eq!(
382 metrics,
383 ExecutionMetrics {
384 rows_scanned: 5,
385 rows_materialized: 3,
386 execution_time_micros: 42,
387 index_only: true,
388 },
389 "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
390 );
391 assert_eq!(
392 trace.rows_returned(),
393 2,
394 "trace should preserve returned-row counters independently from materialization counters",
395 );
396 }
397}