use serde::{Deserialize, Serialize};
use super::{CompiledPipeline, ResidentLifetime};
use crate::{
error::{QueryError, QueryErrorCode, Result},
plan::QueryPlan,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "precision", rename_all = "snake_case")]
pub enum ShadowBytes {
Estimated {
bytes: u64,
model: String,
},
Unknown {
reason: String,
},
}
impl ShadowBytes {
pub fn estimated(bytes: u64, model: impl Into<String>) -> Self {
Self::Estimated { bytes, model: model.into() }
}
pub fn unknown(reason: impl Into<String>) -> Self {
Self::Unknown { reason: reason.into() }
}
pub const fn value(&self) -> Option<u64> {
match self {
Self::Estimated { bytes, .. } => Some(*bytes),
Self::Unknown { .. } => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShadowMemoryModels {
pub task_workspace: Vec<ShadowBytes>,
pub resources: Vec<ShadowResourceModel>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShadowResourceModel {
pub borrowed: bool,
pub bytes: ShadowBytes,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShadowResourceLifetime {
pub(crate) resource: usize,
pub(crate) producer: usize,
pub(crate) consumers: Vec<usize>,
pub(crate) sink_output: bool,
pub model: ShadowResourceModel,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShadowTaskEnvelope {
pub task: usize,
pub plan_node: Option<usize>,
pub live_resources: Vec<usize>,
pub overlapping_workspaces: Vec<usize>,
pub query_owned_bytes: ShadowBytes,
pub borrowed_resident_bytes: ShadowBytes,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShadowPipelineEstimate {
pub(crate) schema: String,
pub(crate) execution_scope: String,
pub query_owned_peak: ShadowBytes,
pub borrowed_resident_peak: ShadowBytes,
pub task_workspace: Vec<ShadowBytes>,
pub resources: Vec<ShadowResourceLifetime>,
pub envelopes: Vec<ShadowTaskEnvelope>,
}
impl CompiledPipeline {
pub fn shadow_memory_estimate(
&self,
plan: &QueryPlan,
models: ShadowMemoryModels,
) -> Result<ShadowPipelineEstimate> {
self.validate()?;
if models.task_workspace.len() != self.tasks().len() || models.resources.len() != self.resources().len() {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
"pipeline.shadow_memory_estimate",
"shadow models must use the compiled task and resource index spaces",
));
}
let count = self.tasks().len();
let mut precedes = vec![vec![false; count]; count];
for edge in self.edges() {
precedes[edge.from().index()][edge.to().index()] = true;
}
for through in 0..count {
let through_row = precedes[through].clone();
for row in &mut precedes {
if row[through] {
for (reaches, through_reaches) in row.iter_mut().zip(&through_row) {
*reaches |= *through_reaches;
}
}
}
}
let mut envelopes = Vec::with_capacity(count);
for task in self.tasks() {
let at = task.id().index();
let live_resources = self
.resources()
.iter()
.filter(|resource| {
let producer = resource.producer().index();
if precedes[at][producer] {
return false;
}
match resource.lifetime() {
ResidentLifetime::TaskLocal { task } => {
let owner = task.index();
owner == at || (!precedes[owner][at] && !precedes[at][owner])
},
ResidentLifetime::BetweenTasks { .. } => {
resource.consumers().iter().any(|consumer| !precedes[consumer.index()][at])
},
ResidentLifetime::SinkOutput { .. } => true,
}
})
.map(|resource| resource.id().index())
.collect::<Vec<_>>();
let overlapping_workspaces = (0..count)
.filter(|other| *other == at || (!precedes[*other][at] && !precedes[at][*other]))
.collect::<Vec<_>>();
let owned = live_resources
.iter()
.filter(|id| !models.resources[**id].borrowed)
.map(|id| &models.resources[*id].bytes)
.chain(overlapping_workspaces.iter().map(|id| &models.task_workspace[*id]));
let borrowed = live_resources
.iter()
.filter(|id| models.resources[**id].borrowed)
.map(|id| &models.resources[*id].bytes);
envelopes.push(ShadowTaskEnvelope {
task: at,
plan_node: task.plan_node().map(|node| node.index()),
query_owned_bytes: sum(owned),
borrowed_resident_bytes: sum(borrowed),
live_resources,
overlapping_workspaces,
});
}
Ok(ShadowPipelineEstimate {
schema: "algeon.shadow_pipeline_estimate".to_owned(),
execution_scope: format!("blake3:{}", plan.execution_fingerprint()),
query_owned_peak: maximum(envelopes.iter().map(|envelope| &envelope.query_owned_bytes)),
borrowed_resident_peak: maximum(envelopes.iter().map(|envelope| &envelope.borrowed_resident_bytes)),
task_workspace: models.task_workspace,
resources: self
.resources()
.iter()
.zip(models.resources)
.map(|(resource, model)| ShadowResourceLifetime {
resource: resource.id().index(),
producer: resource.producer().index(),
consumers: resource.consumers().iter().map(|task| task.index()).collect(),
sink_output: matches!(resource.lifetime(), ResidentLifetime::SinkOutput { .. }),
model,
})
.collect(),
envelopes,
})
}
}
fn sum<'a>(values: impl Iterator<Item = &'a ShadowBytes>) -> ShadowBytes {
let mut total = 0u64;
for value in values {
let Some(bytes) = value.value() else {
return ShadowBytes::unknown("component_cost_unavailable");
};
let Some(sum) = total.checked_add(bytes) else {
return ShadowBytes::unknown("arithmetic_overflow");
};
total = sum;
}
ShadowBytes::estimated(total, "compiled_dependency_overlap_envelope")
}
fn maximum<'a>(values: impl Iterator<Item = &'a ShadowBytes>) -> ShadowBytes {
let mut peak = 0u64;
for value in values {
let Some(bytes) = value.value() else {
return ShadowBytes::unknown("incomplete_overlap_envelope");
};
peak = peak.max(bytes);
}
ShadowBytes::estimated(peak, "compiled_dependency_overlap_envelope")
}