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
12#[cfg_attr(
13 doc,
14 doc = "ExecutionAccessPathVariant\n\nCoarse access path shape recorded in execution traces."
15)]
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum ExecutionAccessPathVariant {
18 ByKey,
19 ByKeys,
20 KeyRange,
21 IndexPrefix,
22 IndexMultiLookup,
23 IndexRange,
24 FullScan,
25 Union,
26 Intersection,
27}
28
29#[cfg_attr(
30 doc,
31 doc = "ExecutionOptimization\n\nLoad optimization selected at execution time, if any."
32)]
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum ExecutionOptimization {
35 PrimaryKey,
36 PrimaryKeyTopNSeek,
37 SecondaryOrderPushdown,
38 SecondaryOrderTopNSeek,
39 IndexRangeLimitPushdown,
40}
41
42#[cfg_attr(
43 doc,
44 doc = "ExecutionTrace\n\nStructured execution trace snapshot for one load path.\nCaptures plan shape and counters without affecting behavior."
45)]
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct ExecutionTrace {
48 pub(crate) access_path_variant: ExecutionAccessPathVariant,
49 pub(crate) direction: OrderDirection,
50 pub(crate) optimization: Option<ExecutionOptimization>,
51 pub(crate) keys_scanned: u64,
52 pub(crate) rows_materialized: u64,
53 pub(crate) rows_returned: u64,
54 pub(crate) execution_time_micros: u64,
55 pub(crate) index_only: bool,
56 pub(crate) continuation_applied: bool,
57 pub(crate) index_predicate_applied: bool,
58 pub(crate) index_predicate_keys_rejected: u64,
59 pub(crate) distinct_keys_deduped: u64,
60}
61
62#[cfg_attr(
63 doc,
64 doc = "ExecutionMetrics\n\nCompact metrics view derived from one `ExecutionTrace`.\nKept small for lightweight observability surfaces."
65)]
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub struct ExecutionMetrics {
68 pub(crate) rows_scanned: u64,
69 pub(crate) rows_materialized: u64,
70 pub(crate) execution_time_micros: u64,
71 pub(crate) index_only: bool,
72}
73
74impl ExecutionTrace {
75 #[must_use]
77 pub(in crate::db) fn new<K>(
78 access: &AccessPlan<K>,
79 direction: Direction,
80 continuation_applied: bool,
81 ) -> Self {
82 Self {
83 access_path_variant: access_path_variant(access),
84 direction: execution_order_direction(direction),
85 optimization: None,
86 keys_scanned: 0,
87 rows_materialized: 0,
88 rows_returned: 0,
89 execution_time_micros: 0,
90 index_only: false,
91 continuation_applied,
92 index_predicate_applied: false,
93 index_predicate_keys_rejected: 0,
94 distinct_keys_deduped: 0,
95 }
96 }
97
98 #[expect(clippy::too_many_arguments)]
100 pub(in crate::db) fn set_path_outcome(
101 &mut self,
102 optimization: Option<ExecutionOptimization>,
103 keys_scanned: usize,
104 rows_materialized: usize,
105 rows_returned: usize,
106 execution_time_micros: u64,
107 index_only: bool,
108 index_predicate_applied: bool,
109 index_predicate_keys_rejected: u64,
110 distinct_keys_deduped: u64,
111 ) {
112 self.optimization = optimization;
113 self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
114 self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
115 self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
116 self.execution_time_micros = execution_time_micros;
117 self.index_only = index_only;
118 self.index_predicate_applied = index_predicate_applied;
119 self.index_predicate_keys_rejected = index_predicate_keys_rejected;
120 self.distinct_keys_deduped = distinct_keys_deduped;
121 debug_assert_eq!(
122 self.keys_scanned,
123 u64::try_from(keys_scanned).unwrap_or(u64::MAX),
124 "execution trace keys_scanned must match rows_scanned metrics input",
125 );
126 }
127
128 #[must_use]
130 pub const fn metrics(&self) -> ExecutionMetrics {
131 ExecutionMetrics {
132 rows_scanned: self.keys_scanned,
133 rows_materialized: self.rows_materialized,
134 execution_time_micros: self.execution_time_micros,
135 index_only: self.index_only,
136 }
137 }
138
139 #[must_use]
141 pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
142 self.access_path_variant
143 }
144
145 #[must_use]
147 pub const fn direction(&self) -> OrderDirection {
148 self.direction
149 }
150
151 #[must_use]
153 pub const fn optimization(&self) -> Option<ExecutionOptimization> {
154 self.optimization
155 }
156
157 #[must_use]
159 pub const fn keys_scanned(&self) -> u64 {
160 self.keys_scanned
161 }
162
163 #[must_use]
165 pub const fn rows_materialized(&self) -> u64 {
166 self.rows_materialized
167 }
168
169 #[must_use]
171 pub const fn rows_returned(&self) -> u64 {
172 self.rows_returned
173 }
174
175 #[must_use]
177 pub const fn execution_time_micros(&self) -> u64 {
178 self.execution_time_micros
179 }
180
181 #[must_use]
183 pub const fn index_only(&self) -> bool {
184 self.index_only
185 }
186
187 #[must_use]
189 pub const fn continuation_applied(&self) -> bool {
190 self.continuation_applied
191 }
192
193 #[must_use]
195 pub const fn index_predicate_applied(&self) -> bool {
196 self.index_predicate_applied
197 }
198
199 #[must_use]
201 pub const fn index_predicate_keys_rejected(&self) -> u64 {
202 self.index_predicate_keys_rejected
203 }
204
205 #[must_use]
207 pub const fn distinct_keys_deduped(&self) -> u64 {
208 self.distinct_keys_deduped
209 }
210}
211
212impl ExecutionMetrics {
213 #[must_use]
215 pub const fn rows_scanned(&self) -> u64 {
216 self.rows_scanned
217 }
218
219 #[must_use]
221 pub const fn rows_materialized(&self) -> u64 {
222 self.rows_materialized
223 }
224
225 #[must_use]
227 pub const fn execution_time_micros(&self) -> u64 {
228 self.execution_time_micros
229 }
230
231 #[must_use]
233 pub const fn index_only(&self) -> bool {
234 self.index_only
235 }
236}
237
238fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
239 match dispatch_access_plan(access) {
240 AccessPlanDispatch::Path(path) => match path.kind() {
241 AccessPathKind::ByKey => ExecutionAccessPathVariant::ByKey,
242 AccessPathKind::ByKeys => ExecutionAccessPathVariant::ByKeys,
243 AccessPathKind::KeyRange => ExecutionAccessPathVariant::KeyRange,
244 AccessPathKind::IndexPrefix => ExecutionAccessPathVariant::IndexPrefix,
245 AccessPathKind::IndexMultiLookup => ExecutionAccessPathVariant::IndexMultiLookup,
246 AccessPathKind::IndexRange => ExecutionAccessPathVariant::IndexRange,
247 AccessPathKind::FullScan => ExecutionAccessPathVariant::FullScan,
248 },
249 AccessPlanDispatch::Union(_) => ExecutionAccessPathVariant::Union,
250 AccessPlanDispatch::Intersection(_) => ExecutionAccessPathVariant::Intersection,
251 }
252}
253
254const fn execution_order_direction(direction: Direction) -> OrderDirection {
255 match direction {
256 Direction::Asc => OrderDirection::Asc,
257 Direction::Desc => OrderDirection::Desc,
258 }
259}
260
261#[cfg(test)]
266mod tests {
267 use crate::db::{
268 access::AccessPlan,
269 diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
270 direction::Direction,
271 };
272
273 #[test]
274 fn execution_trace_metrics_projection_exposes_requested_surface() {
275 let access = AccessPlan::by_key(11u64);
276 let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
277 trace.set_path_outcome(
278 Some(ExecutionOptimization::PrimaryKey),
279 5,
280 3,
281 2,
282 42,
283 true,
284 true,
285 7,
286 9,
287 );
288
289 let metrics = trace.metrics();
290 assert_eq!(
291 metrics,
292 ExecutionMetrics {
293 rows_scanned: 5,
294 rows_materialized: 3,
295 execution_time_micros: 42,
296 index_only: true,
297 },
298 "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
299 );
300 assert_eq!(
301 trace.rows_returned(),
302 2,
303 "trace should preserve returned-row counters independently from materialization counters",
304 );
305 }
306}