icydb_core/db/session/query/
attribution.rs1use crate::db::{
7 access::{AccessPath, AccessPlan},
8 executor::{SharedPreparedExecutionPlan, StructuralProjectionExecutionRoute},
9 session::query::QueryPlanCacheAttribution,
10};
11use candid::CandidType;
12use serde::Deserialize;
13
14#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
16pub struct AttributedRead<T> {
17 pub result: T,
19 pub attribution: OperationReadAttribution,
21}
22
23#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
28pub enum ReadAccessRoute {
29 PrimaryKey,
31 PrimaryKeySet,
33 PrimaryKeyRange,
35 SecondaryIndexPrefix,
37 SecondaryIndexMultiLookup,
39 SecondaryIndexBranchSet,
41 SecondaryIndexRange,
43 FullScan,
45 Union,
47 Intersection,
49}
50
51#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
53pub enum ReadExecutionRoute {
54 Covering,
56 Streaming,
58 Materialized,
60}
61
62#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
64pub enum ReadPlanCacheOutcome {
65 Hit,
67 Miss,
69 Bypassed,
71}
72
73#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
75pub struct OperationReadAttribution {
76 pub total_local_instructions: u64,
78 pub engine_local_instructions: u64,
80 pub response_decode_local_instructions: u64,
83 pub access_route: ReadAccessRoute,
85 pub execution_route: ReadExecutionRoute,
87 pub plan_cache: ReadPlanCacheOutcome,
89 pub rows_scanned: u64,
91 pub rows_emitted: u64,
93}
94
95pub(in crate::db::session) struct OperationReadAttributionBuilder {
96 access_route: Option<ReadAccessRoute>,
97 execution_route: Option<ReadExecutionRoute>,
98 plan_cache: Option<ReadPlanCacheOutcome>,
99 rows_scanned: u64,
100 rows_emitted: u64,
101}
102
103impl OperationReadAttributionBuilder {
104 #[must_use]
105 pub(in crate::db::session) const fn new() -> Self {
106 Self {
107 access_route: None,
108 execution_route: None,
109 plan_cache: None,
110 rows_scanned: 0,
111 rows_emitted: 0,
112 }
113 }
114
115 pub(in crate::db::session) fn record_plan(
116 &mut self,
117 prepared_plan: &SharedPreparedExecutionPlan,
118 cache: QueryPlanCacheAttribution,
119 ) {
120 self.access_route = Some(ReadAccessRoute::from_plan(
121 &prepared_plan.logical_plan().access,
122 ));
123 self.plan_cache = Some(ReadPlanCacheOutcome::from_cache(cache));
124 }
125
126 pub(in crate::db::session) fn record_execution(
127 &mut self,
128 route: StructuralProjectionExecutionRoute,
129 rows_scanned: usize,
130 rows_emitted: u32,
131 ) {
132 self.execution_route = Some(ReadExecutionRoute::from_projection(route));
133 self.rows_scanned = u64::try_from(rows_scanned).unwrap_or(u64::MAX);
134 self.rows_emitted = u64::from(rows_emitted);
135 }
136
137 pub(in crate::db::session) fn finish(
138 self,
139 engine_local_instructions: u64,
140 ) -> Result<OperationReadAttribution, crate::db::QueryError> {
141 Ok(OperationReadAttribution {
142 total_local_instructions: engine_local_instructions,
143 engine_local_instructions,
144 response_decode_local_instructions: 0,
145 access_route: self
146 .access_route
147 .ok_or_else(crate::db::QueryError::invariant)?,
148 execution_route: self
149 .execution_route
150 .ok_or_else(crate::db::QueryError::invariant)?,
151 plan_cache: self
152 .plan_cache
153 .ok_or_else(crate::db::QueryError::invariant)?,
154 rows_scanned: self.rows_scanned,
155 rows_emitted: self.rows_emitted,
156 })
157 }
158}
159
160impl ReadAccessRoute {
161 fn from_plan(plan: &AccessPlan<crate::value::Value>) -> Self {
162 match plan {
163 AccessPlan::Path(path) => match path.as_ref() {
164 AccessPath::ByKey(_) => Self::PrimaryKey,
165 AccessPath::ByKeys(_) => Self::PrimaryKeySet,
166 AccessPath::KeyRange { .. } => Self::PrimaryKeyRange,
167 AccessPath::IndexPrefix { .. } => Self::SecondaryIndexPrefix,
168 AccessPath::IndexMultiLookup { .. } => Self::SecondaryIndexMultiLookup,
169 AccessPath::IndexBranchSet { .. } => Self::SecondaryIndexBranchSet,
170 AccessPath::IndexRange { .. } => Self::SecondaryIndexRange,
171 AccessPath::FullScan => Self::FullScan,
172 },
173 AccessPlan::Union(_) => Self::Union,
174 AccessPlan::Intersection(_) => Self::Intersection,
175 }
176 }
177}
178
179impl ReadExecutionRoute {
180 const fn from_projection(route: StructuralProjectionExecutionRoute) -> Self {
181 match route {
182 StructuralProjectionExecutionRoute::Covering => Self::Covering,
183 StructuralProjectionExecutionRoute::Streaming => Self::Streaming,
184 StructuralProjectionExecutionRoute::Materialized => Self::Materialized,
185 }
186 }
187}
188
189impl ReadPlanCacheOutcome {
190 const fn from_cache(cache: QueryPlanCacheAttribution) -> Self {
191 if cache.hits > 0 && cache.misses == 0 {
192 Self::Hit
193 } else if cache.misses > 0 && cache.hits == 0 {
194 Self::Miss
195 } else {
196 Self::Bypassed
197 }
198 }
199}
200
201#[must_use]
202#[cfg(target_arch = "wasm32")]
203pub(in crate::db::session) fn read_operation_local_instruction_counter() -> u64 {
204 crate::runtime::performance_counter(1)
205}
206
207#[must_use]
208#[cfg(not(target_arch = "wasm32"))]
209pub(in crate::db::session) const fn read_operation_local_instruction_counter() -> u64 {
210 0
211}
212
213#[cfg(test)]
214mod tests {
215 use super::{
216 OperationReadAttribution, ReadAccessRoute, ReadExecutionRoute, ReadPlanCacheOutcome,
217 };
218
219 #[test]
220 fn operation_read_attribution_has_a_fixed_small_candid_envelope() {
221 let attribution = OperationReadAttribution {
222 total_local_instructions: u64::MAX,
223 engine_local_instructions: u64::MAX,
224 response_decode_local_instructions: u64::MAX,
225 access_route: ReadAccessRoute::SecondaryIndexBranchSet,
226 execution_route: ReadExecutionRoute::Materialized,
227 plan_cache: ReadPlanCacheOutcome::Bypassed,
228 rows_scanned: u64::MAX,
229 rows_emitted: u64::MAX,
230 };
231 let encoded = candid::encode_one(attribution)
232 .expect("fixed operation-local attribution should encode");
233
234 assert_eq!(encoded.len(), 200);
235 assert!(encoded.len() <= 256);
236 }
237}