use crate::db::{
access::{AccessPath, AccessPlan},
executor::{SharedPreparedExecutionPlan, StructuralProjectionExecutionRoute},
session::query::QueryPlanCacheAttribution,
};
use candid::CandidType;
use serde::Deserialize;
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct AttributedRead<T> {
pub result: T,
pub attribution: OperationReadAttribution,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum ReadAccessRoute {
PrimaryKey,
PrimaryKeySet,
PrimaryKeyRange,
SecondaryIndexPrefix,
SecondaryIndexMultiLookup,
SecondaryIndexBranchSet,
SecondaryIndexRange,
FullScan,
Union,
Intersection,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum ReadExecutionRoute {
Covering,
Streaming,
Materialized,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum ReadPlanCacheOutcome {
Hit,
Miss,
Bypassed,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub struct OperationReadAttribution {
pub total_local_instructions: u64,
pub engine_local_instructions: u64,
pub response_decode_local_instructions: u64,
pub access_route: ReadAccessRoute,
pub execution_route: ReadExecutionRoute,
pub plan_cache: ReadPlanCacheOutcome,
pub rows_scanned: u64,
pub rows_emitted: u64,
}
pub(in crate::db::session) struct OperationReadAttributionBuilder {
access_route: Option<ReadAccessRoute>,
execution_route: Option<ReadExecutionRoute>,
plan_cache: Option<ReadPlanCacheOutcome>,
rows_scanned: u64,
rows_emitted: u64,
}
impl OperationReadAttributionBuilder {
#[must_use]
pub(in crate::db::session) const fn new() -> Self {
Self {
access_route: None,
execution_route: None,
plan_cache: None,
rows_scanned: 0,
rows_emitted: 0,
}
}
pub(in crate::db::session) fn record_plan(
&mut self,
prepared_plan: &SharedPreparedExecutionPlan,
cache: QueryPlanCacheAttribution,
) {
self.access_route = Some(ReadAccessRoute::from_plan(
&prepared_plan.logical_plan().access,
));
self.plan_cache = Some(ReadPlanCacheOutcome::from_cache(cache));
}
pub(in crate::db::session) fn record_execution(
&mut self,
route: StructuralProjectionExecutionRoute,
rows_scanned: usize,
rows_emitted: u32,
) {
self.execution_route = Some(ReadExecutionRoute::from_projection(route));
self.rows_scanned = u64::try_from(rows_scanned).unwrap_or(u64::MAX);
self.rows_emitted = u64::from(rows_emitted);
}
pub(in crate::db::session) fn finish(
self,
engine_local_instructions: u64,
) -> Result<OperationReadAttribution, crate::db::QueryError> {
Ok(OperationReadAttribution {
total_local_instructions: engine_local_instructions,
engine_local_instructions,
response_decode_local_instructions: 0,
access_route: self
.access_route
.ok_or_else(crate::db::QueryError::invariant)?,
execution_route: self
.execution_route
.ok_or_else(crate::db::QueryError::invariant)?,
plan_cache: self
.plan_cache
.ok_or_else(crate::db::QueryError::invariant)?,
rows_scanned: self.rows_scanned,
rows_emitted: self.rows_emitted,
})
}
}
impl ReadAccessRoute {
fn from_plan(plan: &AccessPlan<crate::value::Value>) -> Self {
match plan {
AccessPlan::Path(path) => match path.as_ref() {
AccessPath::ByKey(_) => Self::PrimaryKey,
AccessPath::ByKeys(_) => Self::PrimaryKeySet,
AccessPath::KeyRange { .. } => Self::PrimaryKeyRange,
AccessPath::IndexPrefix { .. } => Self::SecondaryIndexPrefix,
AccessPath::IndexMultiLookup { .. } => Self::SecondaryIndexMultiLookup,
AccessPath::IndexBranchSet { .. } => Self::SecondaryIndexBranchSet,
AccessPath::IndexRange { .. } => Self::SecondaryIndexRange,
AccessPath::FullScan => Self::FullScan,
},
AccessPlan::Union(_) => Self::Union,
AccessPlan::Intersection(_) => Self::Intersection,
}
}
}
impl ReadExecutionRoute {
const fn from_projection(route: StructuralProjectionExecutionRoute) -> Self {
match route {
StructuralProjectionExecutionRoute::Covering => Self::Covering,
StructuralProjectionExecutionRoute::Streaming => Self::Streaming,
StructuralProjectionExecutionRoute::Materialized => Self::Materialized,
}
}
}
impl ReadPlanCacheOutcome {
const fn from_cache(cache: QueryPlanCacheAttribution) -> Self {
if cache.hits > 0 && cache.misses == 0 {
Self::Hit
} else if cache.misses > 0 && cache.hits == 0 {
Self::Miss
} else {
Self::Bypassed
}
}
}
#[must_use]
#[cfg(target_arch = "wasm32")]
pub(in crate::db::session) fn read_operation_local_instruction_counter() -> u64 {
crate::runtime::performance_counter(1)
}
#[must_use]
#[cfg(not(target_arch = "wasm32"))]
pub(in crate::db::session) const fn read_operation_local_instruction_counter() -> u64 {
0
}
#[cfg(test)]
mod tests {
use super::{
OperationReadAttribution, ReadAccessRoute, ReadExecutionRoute, ReadPlanCacheOutcome,
};
#[test]
fn operation_read_attribution_has_a_fixed_small_candid_envelope() {
let attribution = OperationReadAttribution {
total_local_instructions: u64::MAX,
engine_local_instructions: u64::MAX,
response_decode_local_instructions: u64::MAX,
access_route: ReadAccessRoute::SecondaryIndexBranchSet,
execution_route: ReadExecutionRoute::Materialized,
plan_cache: ReadPlanCacheOutcome::Bypassed,
rows_scanned: u64::MAX,
rows_emitted: u64::MAX,
};
let encoded = candid::encode_one(attribution)
.expect("fixed operation-local attribution should encode");
assert_eq!(encoded.len(), 200);
assert!(encoded.len() <= 256);
}
}