Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,14 +289,17 @@ jq '.candidates[] | {candidate_index, unavailable_reason}' \

target/debug/examples/compile_workload_artifact \
"$ASAPQUERY_PLANNING_SNAPSHOT" \
--dot target/readme-evidence/selected.dot \
> target/readme-evidence/selected.json
jq '.cost_comparison' target/readme-evidence/selected.json
jq '.install_request.summary_catalog' target/readme-evidence/selected.json
jq '.install_request.precompute_plan | {materializations, executable_dags}' \
jq '.summary_catalog' target/readme-evidence/selected.json
jq '.precompute_plan | {materializations, executable_dags}' \
target/readme-evidence/selected.json
jq '.install_request.query_plan.entries' target/readme-evidence/selected.json
jq '.install_request.precompute_plan.schemas[] | {materialization, schema_id}' \
jq '.query_plan.entries' target/readme-evidence/selected.json
jq '.precompute_plan.schemas[] | {materialization, schema_id}' \
target/readme-evidence/selected.json
dot -Tsvg target/readme-evidence/selected.dot \
-o target/readme-evidence/selected.svg
```

Candidate discovery accepts the checked-in unquoted templates. Deployment and
Expand Down Expand Up @@ -471,7 +474,7 @@ To inspect supported MetricsQL planning independently:
target/debug/examples/compile_workload_artifact \
"$ASAPQUERY_PLANNING_SNAPSHOT" --metricsql \
> target/readme-evidence/victoriametrics/selected.json
jq '.install_request.query_plan.entries' \
jq '.query_plan.entries' \
target/readme-evidence/victoriametrics/selected.json
```

Expand Down Expand Up @@ -522,7 +525,7 @@ python3 - <<'PY'
import json
from pathlib import Path
root = Path('target/readme-evidence')
envelope = json.loads((root / 'selected.json').read_text())['install_request']['precompute_plan']['envelope']
envelope = json.loads((root / 'selected.json').read_text())['precompute_plan']['envelope']
envelope['plan_id'] = 9001
envelope['plan_version'] = 1
workload = {
Expand Down
2 changes: 1 addition & 1 deletion control_plane/docs/candidate-physical-explain.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Physical identity combines the logical/mask alternative with existing materializ

The read-only `/api/v1/physical-plan/cost-manifests` and MetricsQL equivalent retain their default manifest-array response. Add `"explain": true` to the existing request to receive `{ "manifests": [...], "alternatives": [...], "logical_selection": [...] }`. Failed alternatives remain alongside usable manifests. When none can bind or be completely priced, the error retains an `all_infeasible` report and every accumulated alternative rather than only a generic message.

Snapshot compilation exposes the same logical trace on `PhysicalPlan`; `compile_workload_artifact` prints it outside the install request. Compile-and-publish returns the trace without adding it to executable wire DTOs. SQL's existing selection trace gains the same semantic candidate/root identities.
Snapshot compilation exposes the same logical trace on `CompiledPhysicalPlan`; `compile_workload_artifact` serializes it as `planner_selection_trace`. Compile-and-publish returns the trace without adding it to executable wire DTOs. SQL's existing selection trace gains the same semantic candidate/root identities.

These are bounded explanations: they cover the actual Planner search and the existing physical materialization/exact inventory, not every possible placement or resource-constrained cluster assignment. Missing numeric measurements remain missing. The next provider integration must occur before logical commitment and reuse Planner's provider/resource contracts.

Expand Down
51 changes: 15 additions & 36 deletions control_plane/examples/compile_workload_artifact.rs
Original file line number Diff line number Diff line change
@@ -1,52 +1,31 @@
//! Control-plane entry point: cost-select a workload and emit its atomic install request.
use control_plane::physical::compiler::BackendLocalPlanningInput;
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let path = args
.next()
.ok_or("usage: compile_workload_artifact SNAPSHOT.json [--metricsql]")?;
let metricsql = match args.next().as_deref() {
None => false,
Some("--metricsql") => true,
Some(_) => return Err("expected optional --metricsql".into()),
};
if args.next().is_some() {
return Err("unexpected arguments".into());
.ok_or("usage: compile_workload_artifact SNAPSHOT.json [--metricsql] [--dot OUTPUT.dot]")?;
let mut metricsql = false;
let mut dot_path = None;
while let Some(argument) = args.next() {
match argument.as_str() {
"--metricsql" => metricsql = true,
"--dot" => {
dot_path = Some(args.next().ok_or("--dot requires an output path")?);
}
_ => return Err(format!("unexpected argument `{argument}`").into()),
}
}
let snapshot: BackendLocalPlanningInput = serde_json::from_slice(&std::fs::read(path)?)?;
let start = std::time::Instant::now();
let plan = if metricsql {
snapshot.compile_metricsql()?
} else {
snapshot.compile_promql()?
};
let elapsed = start.elapsed().as_nanos();
let comparison = plan
.cost_comparison
.ok_or("missing complete-plan comparison")?;
println!(
"{}",
serde_json::to_string_pretty(&json!({
"schema_version": 1,
"planning_elapsed_ns": elapsed,
"envelope": plan.envelope,
"cost_comparison": comparison,
"logical_selection": plan.planner_selection_trace,
"backend_revision": control_plane::physical::compiler::BACKEND_REVISION,
"planner_revision": control_plane::physical::compiler::PLANNER_REVISION,
"lifecycle_estimates": plan.lifecycle_estimates,
"install_request": {
"summary_catalog": plan.summary_catalog,
"collector_plans": plan.collector_plans,
"precompute_plan": plan.precompute_plan,
"transmission_plan": plan.transmission_plan,
"query_plan": plan.query_plan,
"storage_routing": null,
"adaptation_evidence": []
}
}))?
);
if let Some(path) = dot_path {
std::fs::write(path, control_plane::physical::plan_dot::render(&plan))?;
}
println!("{}", serde_json::to_string_pretty(&plan)?);
Ok(())
}
6 changes: 5 additions & 1 deletion control_plane/src/physical/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,11 @@ pub fn build_transmission_plan(

/// Complete physical projection of one post-ASAP planning decision.
/// All three child plans share the same envelope and are compiled together.
#[derive(Debug, Clone)]
/// Complete selected physical plan, serializable for developer inspection.
///
/// The serialized form is an inspection artifact emitted by
/// `compile_workload_artifact`; it is not an input accepted by the compiler.
#[derive(Debug, Clone, Serialize)]
pub struct CompiledPhysicalPlan {
pub envelope: PlanEnvelope,
pub summary_catalog: super::summary_catalog::SummaryCatalog,
Expand Down
1 change: 1 addition & 0 deletions control_plane/src/physical/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod sketch_catalog;
pub mod summary_catalog;
pub mod workload_cost;

pub mod plan_dot;
pub mod publication;

pub(crate) mod maintained_population;
233 changes: 233 additions & 0 deletions control_plane/src/physical/plan_dot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//! Graphviz inspection rendering for one selected physical plan.
//!
//! This is intentionally a developer-facing view: JSON remains the complete
//! representation, while DOT keeps labels compact enough to follow execution.

use super::compiler::CompiledPhysicalPlan;
use crate::query_plan::QueryPlanNode;
use asap_types::query_plan::residual::ResidualQueryOperator;

/// Render the selected precompute and query execution DAGs as deterministic DOT.
pub fn render(plan: &CompiledPhysicalPlan) -> String {
let mut dot = String::from(
"digraph compiled_physical_plan {\n rankdir=LR;\n node [shape=box, fontname=Helvetica];\n",
);

dot.push_str(" subgraph cluster_precompute {\n label=\"PrecomputePlan\";\n");
for materialization in &plan.precompute_plan.materializations {
let id = materialization.policy_fingerprint();
let node = materialization_node(id.as_u64());
let label = format!(
"materialization\n{}\nmetric={}\nwindow_type={}\nwindow={}s / {}s\nlayout={:?}",
id,
materialization.metric,
materialization.window_type,
materialization.window_size,
materialization.slide_interval,
materialization.window_layout,
);
emit_node(&mut dot, &node, &label, "shape=component");
}
for (dag_index, (query_id, installed)) in
plan.precompute_plan.executable_dags.iter().enumerate()
{
dot.push_str(&format!(
" subgraph cluster_precompute_dag_{dag_index} {{\n label=\"{}\";\n",
escape(query_id)
));
for node in &installed.document.nodes {
let id = precompute_node(dag_index, node.id.0);
let binding = installed
.binding
.nodes
.get(&node.id)
.map(|binding| format!("\n{:?}", binding))
.unwrap_or_default();
emit_node(
&mut dot,
&id,
&format!("{:?} #{}{}", node.operator, node.id.0, binding),
"",
);
}
for edge in &installed.document.edges {
dot.push_str(&format!(
" {} -> {} [label=\"{:?}\"];\n",
precompute_node(dag_index, edge.producer.0),
precompute_node(dag_index, edge.consumer.0),
edge.role
));
}
dot.push_str(" }\n");
}
dot.push_str(" }\n");

for (query_index, (query_key, entry)) in plan.query_plan.entries.iter().enumerate() {
dot.push_str(&format!(
" subgraph cluster_query_{query_index} {{\n label=\"QueryPlan: {}\";\n",
escape(query_key)
));
for (id, node) in &entry.nodes {
let attributes = if *id == entry.root {
"shape=doubleoctagon"
} else {
""
};
emit_node(
&mut dot,
&query_node(query_index, id.0),
&query_node_label(node),
attributes,
);
if let QueryPlanNode::ReadMaterialization { binding } = node {
dot.push_str(&format!(
" {} -> {} [style=dashed, color=gray40, label=\"reads\"];\n",
materialization_node(binding.materialization.as_u64()),
query_node(query_index, id.0),
));
}
}
for (id, node) in &entry.nodes {
for input in node.inputs() {
dot.push_str(&format!(
" {} -> {};\n",
query_node(query_index, input.0),
query_node(query_index, id.0),
));
}
}
dot.push_str(" }\n");
}
dot.push_str("}\n");
dot
}

fn materialization_node(id: u64) -> String {
format!("materialization_{id:016x}")
}

fn precompute_node(dag: usize, id: u32) -> String {
format!("precompute_{dag}_{id}")
}

fn query_node(query: usize, id: u64) -> String {
format!("query_{query}_{id}")
}

fn emit_node(dot: &mut String, id: &str, label: &str, attributes: &str) {
dot.push_str(&format!(" {id} [label=\"{}\"", escape(label)));
if !attributes.is_empty() {
dot.push_str(&format!(", {attributes}"));
}
dot.push_str("];\n");
}

fn escape(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}

fn query_node_label(node: &QueryPlanNode) -> String {
match node {
QueryPlanNode::RelationalJoin { .. } => "RelationalJoin".into(),
QueryPlanNode::Relational { .. } => "Relational".into(),
QueryPlanNode::Logical { operator, .. } => format!("Logical\n{}", residual_label(operator)),
QueryPlanNode::Scalar { value } => format!("Scalar\n{value}"),
QueryPlanNode::Binary { operator, .. } => format!("Binary\n{operator:?}"),
QueryPlanNode::ReduceSum { .. } => "ReduceSum".into(),
QueryPlanNode::ReadMaterialization { binding } => format!(
"ReadMaterialization\n{}\nwindow={}ms\nlookback={:?}",
binding.materialization.fingerprint(),
binding.window_ms,
binding.readout_lookback_ms
),
QueryPlanNode::SummaryEstimate { query, .. } => format!("SummaryEstimate\n{query:?}"),
QueryPlanNode::ExactReadout { readout, .. } => format!("ExactReadout\n{readout:?}"),
QueryPlanNode::SummaryMerge { .. } => "SummaryMerge".into(),
QueryPlanNode::CandidateTopK { k, .. } => format!("CandidateTopK\nk={k}"),
QueryPlanNode::ExternalExact { .. } => "ExternalExact".into(),
QueryPlanNode::ExactFallback { reason } => format!("ExactFallback\n{reason}"),
}
}

fn residual_label(operator: &ResidualQueryOperator) -> &'static str {
match operator {
ResidualQueryOperator::CurrentSeries { .. } => "CurrentSeries",
ResidualQueryOperator::ExactSubquery { .. } => "ExactSubquery",
ResidualQueryOperator::CandidateExactSubquery { .. } => "CandidateExactSubquery",
ResidualQueryOperator::Scan { .. } => "Scan",
ResidualQueryOperator::UnaryNegate => "UnaryNegate",
ResidualQueryOperator::VectorToScalar => "VectorToScalar",
ResidualQueryOperator::Aggregate { .. } => "Aggregate",
ResidualQueryOperator::TopKSelection { .. } => "TopKSelection",
ResidualQueryOperator::Binary { .. } => "Binary",
ResidualQueryOperator::Temporal { .. } => "Temporal",
ResidualQueryOperator::Sort { .. } => "Sort",
ResidualQueryOperator::HistogramQuantile => "HistogramQuantile",
ResidualQueryOperator::Subquery { .. } => "Subquery",
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::physical::compiler::{BackendLocalPlanningInput, QueryFrontend};

fn fixture() -> CompiledPhysicalPlan {
let snapshot: BackendLocalPlanningInput = serde_json::from_str(include_str!(
"../../../docs/examples/asapquery-planning-snapshot.json"
))
.unwrap();
crate::physical::compiler::tests::quoted_snapshot(snapshot, QueryFrontend::PromQl)
.compile_promql()
.unwrap()
}

#[test]
fn render_links_query_reads_to_precompute_materializations() {
let dot = render(&fixture());
assert!(dot.contains("cluster_precompute"), "{dot}");
assert!(dot.contains("cluster_query_"), "{dot}");
assert!(dot.contains("style=dashed"), "{dot}");
assert!(dot.contains("ReadMaterialization"), "{dot}");
assert!(dot.contains("window_type="), "{dot}");
assert!(dot.contains("layout="), "{dot}");
}

#[test]
fn render_uses_graphviz_line_breaks_in_labels() {
let dot = render(&fixture());
assert!(
dot.contains("materialization\\npolicy_fp:"),
"expected a Graphviz line break, not a literal backslash-n: {dot}"
);
assert!(
!dot.contains("materialization\\\\npolicy_fp:"),
"label double-escaped its Graphviz line break: {dot}"
);
}

#[test]
fn compiled_physical_plan_serializes_each_projection() {
let artifact = serde_json::to_value(fixture()).unwrap();
for field in [
"envelope",
"summary_catalog",
"collector_plans",
"precompute_plan",
"transmission_plan",
"query_plan",
"storage_routing",
"lifecycle_estimates",
"cost_comparison",
"planner_selection_trace",
] {
assert!(
artifact.get(field).is_some(),
"missing `{field}`: {artifact}"
);
}
}
}
Loading
Loading