diff --git a/crates/devtools/src/bin/dag_export.rs b/crates/devtools/src/bin/dag_export.rs index 08df63be..354f1962 100644 --- a/crates/devtools/src/bin/dag_export.rs +++ b/crates/devtools/src/bin/dag_export.rs @@ -18,6 +18,10 @@ // cargo run -p asap-lower --bin dag_export -- \ // --epsilon 0.01 --sql "SELECT quantile(0.99, latency) FROM metrics" --name p99 // +// `--post-asap` needs one of two cost sources to rank with, and does +// nothing without either (see `--default-cost` and `--planner-cost-json` +// below). +// // `--post-asap` is optional and off by default. When passed, this binary // additionally runs `asap_aware_mapping::replacement::search_workload` (this // binary took no strategies of its own — `default_strategies()` already @@ -46,8 +50,26 @@ // `post_graph` is `None`, both skipped from the JSON entirely in that // case). E.g.: // cargo run -p asap-lower --bin dag_export -- \ -// --post-asap --epsilon 0.01 \ +// --post-asap --default-cost --epsilon 0.01 \ // --sql "SELECT quantile(0.95, latency) FROM metrics" --name q1 +// +// `--default-cost` and `--planner-cost-json` are the two mutually exclusive +// ways to give `--post-asap` a cost model, and they differ in what the +// export is allowed to claim: +// +// - `--planner-cost-json ` supplies complete deployment-owned +// physical-plan evidence. It both ranks the candidates and is exported: +// every decision carries a calibrated `CostUnits` annotation. +// - `--default-cost` ranks with `asap_aware_mapping::cost_model:: +// DefaultCostModel` — structural node counts, owning no deployment +// evidence. The structure of the result is real (which replacements the +// search found, which one won per group, what the merged post-ASAP DAG +// looks like); the numbers are not exported at all. Every decision's +// cost is `CostSource::Unavailable` with `value: None`, which the viewer +// renders as "Not estimated". Use it to see what ASAPPlanner does with a +// workload before there is a deployment to measure. +// +// Absent both, `--post-asap` exports the raw plan only. use std::cell::RefCell; use std::collections::HashMap; @@ -58,7 +80,6 @@ use asap_aware_mapping::analytical_cost::{ cache_hit_ratios, AnalyticalCostError, EvidenceBackedPhysicalDag as PhysicalDag, PhysicalNodeEvidence, ResourceCalibration, ANALYTICAL_COST_MODEL_VERSION, }; -#[cfg(test)] use asap_aware_mapping::cost_model::DefaultCostModel; use asap_aware_mapping::cost_model::{Cost, CostModel}; use asap_aware_mapping::physical_operator_statistics::ComparisonScope; @@ -765,6 +786,11 @@ struct ParsedArgs { progress: bool, table_schemas: Vec, planner_cost: Option, + /// `--default-cost`: rank the `--post-asap` search with + /// [`DefaultCostModel`] and export no cost at all. Mutually exclusive + /// with `planner_cost`, which both ranks *and* is exported — see this + /// file's top-of-file usage doc for why the two can't be combined. + default_cost: bool, topk_margin: Option, } @@ -814,6 +840,10 @@ impl AccuracyEvidenceProvider for TopKMarginEvidence { } fn parse_args() -> ParsedArgs { + parse_args_from(std::env::args().skip(1)) +} + +fn parse_args_from(argv: impl Iterator) -> ParsedArgs { let mut entries: Vec<(String, Lang, String)> = Vec::new(); let mut pending: Option<(Lang, String)> = None; let mut accuracy = AccuracyTarget::Exact; @@ -821,8 +851,9 @@ fn parse_args() -> ParsedArgs { let mut progress = false; let mut table_schemas = Vec::new(); let mut planner_cost_json = None; + let mut default_cost = false; let mut topk_margin_json = None; - let mut args = std::env::args().skip(1); + let mut args = argv; fn flush(entries: &mut Vec<(String, Lang, String)>, pending: &mut Option<(Lang, String)>) { if let Some((lang, query)) = pending.take() { @@ -871,6 +902,9 @@ fn parse_args() -> ParsedArgs { .expect("planner cost evidence requires a JSON document"), ); } + "--default-cost" => { + default_cost = true; + } "--topk-margin-json" => { topk_margin_json = Some( args.next() @@ -881,6 +915,17 @@ fn parse_args() -> ParsedArgs { } } flush(&mut entries, &mut pending); + // Rejected rather than given a precedence order: the two flags disagree + // about what the export may claim, not just about which model ranks, so + // silently preferring one would make the exported costs depend on an + // argument order the caller never stated. + if default_cost && planner_cost_json.is_some() { + panic!( + "--default-cost and --planner-cost-json are mutually exclusive: --default-cost ranks \ + with structural node counts and exports no cost, --planner-cost-json exports \ + deployment-owned costs" + ); + } let planner_cost = planner_cost_json .map(|raw| parse_planner_cost_document(&raw).unwrap_or_else(|error| panic!("{error}"))); let topk_margin = topk_margin_json.map(|raw| { @@ -898,6 +943,7 @@ fn parse_args() -> ParsedArgs { progress, table_schemas, planner_cost, + default_cost, topk_margin, } } @@ -1420,13 +1466,14 @@ async fn main() { progress, table_schemas, planner_cost, + default_cost, topk_margin, } = parse_args(); let sql_catalog = catalog(&table_schemas); let planner_started = Instant::now(); if entries.is_empty() { eprintln!( - "usage: dag_export --sql \"\" [--name