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 BytesByCoveringIndexFastPath,
61 BytesByCoveringConstantFastPath,
62 CoveringExistsFastPath,
63 CoveringCountFastPath,
64 PrimaryKeyCountFastPath,
65 PrimaryKeyCardinalityCountFastPath,
66 CoveringIndexProjectionFastPath,
67 CoveringConstantProjectionFastPath,
68 NumericFieldStreamingFoldFastPath,
69}
70
71impl ExecutionOptimizationCounter {
72 #[cfg(test)]
73 const CARDINALITY: usize = 11;
74
75 #[cfg(test)]
76 const fn index(self) -> usize {
77 match self {
78 Self::BytesPrimaryKeyFastPath => 0,
79 Self::BytesStreamFastPath => 1,
80 Self::BytesByCoveringIndexFastPath => 2,
81 Self::BytesByCoveringConstantFastPath => 3,
82 Self::CoveringExistsFastPath => 4,
83 Self::CoveringCountFastPath => 5,
84 Self::PrimaryKeyCountFastPath => 6,
85 Self::PrimaryKeyCardinalityCountFastPath => 7,
86 Self::CoveringIndexProjectionFastPath => 8,
87 Self::CoveringConstantProjectionFastPath => 9,
88 Self::NumericFieldStreamingFoldFastPath => 10,
89 }
90 }
91}
92
93#[cfg(test)]
94thread_local! {
95 static EXECUTION_OPTIMIZATION_HITS: Cell<[u64; ExecutionOptimizationCounter::CARDINALITY]> =
96 const { Cell::new([0; ExecutionOptimizationCounter::CARDINALITY]) };
97}
98
99#[cfg(test)]
100pub(crate) fn take_execution_optimization_hits_for_tests(
101 optimization: ExecutionOptimizationCounter,
102) -> u64 {
103 EXECUTION_OPTIMIZATION_HITS.with(|counter| {
104 let mut hits = counter.get();
105 let index = optimization.index();
106 let value = hits[index];
107 hits[index] = 0;
108 counter.set(hits);
109 value
110 })
111}
112
113#[cfg(test)]
114pub(crate) fn record_execution_optimization_hit_for_tests(
115 optimization: ExecutionOptimizationCounter,
116) {
117 EXECUTION_OPTIMIZATION_HITS.with(|counter| {
118 let mut hits = counter.get();
119 let index = optimization.index();
120 hits[index] = hits[index].saturating_add(1);
121 counter.set(hits);
122 });
123}
124
125#[cfg(not(test))]
126pub(crate) const fn record_execution_optimization_hit_for_tests(
127 _optimization: ExecutionOptimizationCounter,
128) {
129}
130
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub struct ExecutionTrace {
140 pub(crate) access_path_variant: ExecutionAccessPathVariant,
141 pub(crate) direction: OrderDirection,
142 pub(crate) optimization: Option<ExecutionOptimization>,
143 pub(crate) keys_scanned: u64,
144 pub(crate) rows_materialized: u64,
145 pub(crate) rows_returned: u64,
146 pub(crate) execution_time_micros: u64,
147 pub(crate) index_only: bool,
148 pub(crate) continuation_applied: bool,
149 pub(crate) index_predicate_applied: bool,
150 pub(crate) index_predicate_keys_rejected: u64,
151 pub(crate) distinct_keys_deduped: u64,
152}
153
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub struct ExecutionMetrics {
163 pub(crate) rows_scanned: u64,
164 pub(crate) rows_materialized: u64,
165 pub(crate) execution_time_micros: u64,
166 pub(crate) index_only: bool,
167}
168
169impl ExecutionTrace {
170 #[must_use]
172 pub(in crate::db) fn new<K>(
173 access: &AccessPlan<K>,
174 direction: Direction,
175 continuation_applied: bool,
176 ) -> Self {
177 Self {
178 access_path_variant: access_path_variant(access),
179 direction: execution_order_direction(direction),
180 optimization: None,
181 keys_scanned: 0,
182 rows_materialized: 0,
183 rows_returned: 0,
184 execution_time_micros: 0,
185 index_only: false,
186 continuation_applied,
187 index_predicate_applied: false,
188 index_predicate_keys_rejected: 0,
189 distinct_keys_deduped: 0,
190 }
191 }
192
193 #[expect(clippy::too_many_arguments)]
195 pub(in crate::db) fn set_path_outcome(
196 &mut self,
197 optimization: Option<ExecutionOptimization>,
198 keys_scanned: usize,
199 rows_materialized: usize,
200 rows_returned: usize,
201 execution_time_micros: u64,
202 index_only: bool,
203 index_predicate_applied: bool,
204 index_predicate_keys_rejected: u64,
205 distinct_keys_deduped: u64,
206 ) {
207 self.optimization = optimization;
208 self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
209 self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
210 self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
211 self.execution_time_micros = execution_time_micros;
212 self.index_only = index_only;
213 self.index_predicate_applied = index_predicate_applied;
214 self.index_predicate_keys_rejected = index_predicate_keys_rejected;
215 self.distinct_keys_deduped = distinct_keys_deduped;
216 debug_assert_eq!(
217 self.keys_scanned,
218 u64::try_from(keys_scanned).unwrap_or(u64::MAX),
219 "execution trace keys_scanned must match rows_scanned metrics input",
220 );
221 }
222
223 #[must_use]
225 pub const fn metrics(&self) -> ExecutionMetrics {
226 ExecutionMetrics {
227 rows_scanned: self.keys_scanned,
228 rows_materialized: self.rows_materialized,
229 execution_time_micros: self.execution_time_micros,
230 index_only: self.index_only,
231 }
232 }
233
234 #[must_use]
236 pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
237 self.access_path_variant
238 }
239
240 #[must_use]
242 pub const fn direction(&self) -> OrderDirection {
243 self.direction
244 }
245
246 #[must_use]
248 pub const fn optimization(&self) -> Option<ExecutionOptimization> {
249 self.optimization
250 }
251
252 #[must_use]
254 pub const fn keys_scanned(&self) -> u64 {
255 self.keys_scanned
256 }
257
258 #[must_use]
260 pub const fn rows_materialized(&self) -> u64 {
261 self.rows_materialized
262 }
263
264 #[must_use]
266 pub const fn rows_returned(&self) -> u64 {
267 self.rows_returned
268 }
269
270 #[must_use]
272 pub const fn execution_time_micros(&self) -> u64 {
273 self.execution_time_micros
274 }
275
276 #[must_use]
278 pub const fn index_only(&self) -> bool {
279 self.index_only
280 }
281
282 #[must_use]
284 pub const fn continuation_applied(&self) -> bool {
285 self.continuation_applied
286 }
287
288 #[must_use]
290 pub const fn index_predicate_applied(&self) -> bool {
291 self.index_predicate_applied
292 }
293
294 #[must_use]
296 pub const fn index_predicate_keys_rejected(&self) -> u64 {
297 self.index_predicate_keys_rejected
298 }
299
300 #[must_use]
302 pub const fn distinct_keys_deduped(&self) -> u64 {
303 self.distinct_keys_deduped
304 }
305}
306
307impl ExecutionMetrics {
308 #[must_use]
310 pub const fn rows_scanned(&self) -> u64 {
311 self.rows_scanned
312 }
313
314 #[must_use]
316 pub const fn rows_materialized(&self) -> u64 {
317 self.rows_materialized
318 }
319
320 #[must_use]
322 pub const fn execution_time_micros(&self) -> u64 {
323 self.execution_time_micros
324 }
325
326 #[must_use]
328 pub const fn index_only(&self) -> bool {
329 self.index_only
330 }
331}
332
333fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
334 match dispatch_access_plan(access) {
335 AccessPlanDispatch::Path(path) => match path.kind() {
336 AccessPathKind::ByKey => ExecutionAccessPathVariant::ByKey,
337 AccessPathKind::ByKeys => ExecutionAccessPathVariant::ByKeys,
338 AccessPathKind::KeyRange => ExecutionAccessPathVariant::KeyRange,
339 AccessPathKind::IndexPrefix => ExecutionAccessPathVariant::IndexPrefix,
340 AccessPathKind::IndexMultiLookup => ExecutionAccessPathVariant::IndexMultiLookup,
341 AccessPathKind::IndexRange => ExecutionAccessPathVariant::IndexRange,
342 AccessPathKind::FullScan => ExecutionAccessPathVariant::FullScan,
343 },
344 AccessPlanDispatch::Union(_) => ExecutionAccessPathVariant::Union,
345 AccessPlanDispatch::Intersection(_) => ExecutionAccessPathVariant::Intersection,
346 }
347}
348
349const fn execution_order_direction(direction: Direction) -> OrderDirection {
350 match direction {
351 Direction::Asc => OrderDirection::Asc,
352 Direction::Desc => OrderDirection::Desc,
353 }
354}
355
356#[cfg(test)]
361mod tests {
362 use crate::db::{
363 access::AccessPlan,
364 diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
365 direction::Direction,
366 };
367
368 #[test]
369 fn execution_trace_metrics_projection_exposes_requested_surface() {
370 let access = AccessPlan::by_key(11u64);
371 let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
372 trace.set_path_outcome(
373 Some(ExecutionOptimization::PrimaryKey),
374 5,
375 3,
376 2,
377 42,
378 true,
379 true,
380 7,
381 9,
382 );
383
384 let metrics = trace.metrics();
385 assert_eq!(
386 metrics,
387 ExecutionMetrics {
388 rows_scanned: 5,
389 rows_materialized: 3,
390 execution_time_micros: 42,
391 index_only: true,
392 },
393 "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
394 );
395 assert_eq!(
396 trace.rows_returned(),
397 2,
398 "trace should preserve returned-row counters independently from materialization counters",
399 );
400 }
401}