diff --git a/crates/cost/src/ranking.rs b/crates/cost/src/ranking.rs index 05adcb630..2c1308b6c 100644 --- a/crates/cost/src/ranking.rs +++ b/crates/cost/src/ranking.rs @@ -1,6 +1,5 @@ //! How a cost model orders valid plans ([`PlanRanking`]): Stage 3 ranks its -//! candidates with it, and the facade picks the best of its tumbling-window-width -//! rounds' selections with it (#1047). +//! candidates with it. use std::cmp::Ordering; use crate::{ClusterObjective, CostModel, PlanCost, QueryLatency, ResourceUsage}; diff --git a/crates/devtools/src/bin/sketch_coverage.rs b/crates/devtools/src/bin/sketch_coverage.rs index d537e23c5..f511b3497 100644 --- a/crates/devtools/src/bin/sketch_coverage.rs +++ b/crates/devtools/src/bin/sketch_coverage.rs @@ -67,7 +67,6 @@ fn analyze_corpus( &NoAccuracyEvidence, &DataWorkload::default(), None, - None, ) else { rejected += 1; continue; diff --git a/crates/devtools/src/bin/stage_pipeline.rs b/crates/devtools/src/bin/stage_pipeline.rs index 90d637f51..03c2809ae 100644 --- a/crates/devtools/src/bin/stage_pipeline.rs +++ b/crates/devtools/src/bin/stage_pipeline.rs @@ -24,11 +24,10 @@ // Writes an `asap-stage-pipeline/v1` document (tools/dag-viewer) with the // four planner stages as the facade runs them (`asap_planner::run_planning`). // The default document keeps the selected plan; `--exhaustive` adds the -// reference enumeration: the selected round's memo choices, each implemented +// reference enumeration: Pass 2's memo choices, each implemented // by Stage 2 with every materialization assignment, all priced by the // reference pricer (`asap_plan_selection::reference_pricer`), under -// `reference_pricing`. `--milp-time-limit-s` bounds planning; a timeout keeps -// the best plan found, never an optimality claim for unexplored candidates. +// `reference_pricing`. // Unlike `plan_installable`'s default settings, Stage 3 ignores accuracy // guarantees unless `--enforce-accuracy-guarantees` is given (below); // `accuracy_guarantees` is `"ignored"` or `"enforced"`. @@ -49,7 +48,7 @@ // built it, then under `--exhaustive` each enumerated choice's physical // plans `E-`; an enumerated choice Stage 2 cannot implement is // listed under `pruned` with the reason. `pruned_candidates` lists the -// physical candidates Stage 2 pruned in the selected round +// physical candidates Stage 2 pruned // (`PhysicalASAPMemoCandidates::pruned_candidates`): each with its memo // `group`, `operator_node`, `operator`, `implementation_root`, `timing`, // `materialization` and `reason` (`PruneReason`: @@ -61,7 +60,7 @@ // rejected ones taking each cause in turn; // - stage3_plan_selection: Stage 3's `Selection`: `selected_dag` // (`DagOfSelectedCandidates`: `candidate_of_node`, the candidate id of -// the selected round's Stage 2 output each node runs, one node per run; +// Stage 2's output each node runs, one node per run; // `edges`, each `producer` node feeding input `input` of a `consumer` // node; and `node_of_output_reference`), // `nodes_with_proven_non_negative_weights`, the plan's `cost` (its @@ -97,9 +96,8 @@ // and the plan's peak memory when over 10^12 bytes and the cost model // declares no memory capacity (`large_peak_memory_warning`); absent when // there are none; -// - planning_report: the facade's `PlanningReport`: Pass 1's time and, per -// tumbling-window-width round, Pass 2's and Stage 2's counts and each stage's time, -// with Stage 3's search report or error; +// - planning_report: the facade's `PlanningReport`: each stage's time, +// Pass 2's and Stage 2's counts, and Stage 3's search report; // - reference_pricing (with `--exhaustive`): the reference pricer's // selection among the enumerated plans, named as Stage 3's `Selection` // names its fields: the shown candidates' `cost_per_candidate` and @@ -114,7 +112,7 @@ // `pruned_candidates_not_enumerated` (rejections naming no enumerated // candidate) and `pruned_candidate_count_by_cause`. // `memo` (unless `--no-memo`) is the memo the search built, from the -// planning round whose plan was selected: `nodes`, one node +// planning run: `nodes`, one node // table for the whole section (`MemoNodes`: each distinct node once, matched // by its serialized JSON, children as indices into the table and before // their parents); `steps`, one per @@ -315,7 +313,7 @@ const USAGE: &str = [--name ]... [--table ...] [--max-error --failure-probability --accuracy-metric ] [--sample-interval-ms ] [--series-count ] [--rows-per-second ] [--rows-held ])) \ [--cost-model usage-over-time|aws-fargate-us-east-1|provisioned-capacity|cluster-capacity [--cluster-memory-bytes \ --cluster-disk-bytes [--cluster-cpu-cores ] --cluster-objective lowest-latency|least-memory]] \ -[--cost-inputs ] [--enforce-accuracy-guarantees] [--deployment-capabilities ] [--exhaustive [--max-combinations ]] [--milp-time-limit-s ] [--max-displayed ] \ +[--cost-inputs ] [--enforce-accuracy-guarantees] [--deployment-capabilities ] [--exhaustive [--max-combinations ]] [--max-displayed ] \ [--no-memo] [--max-memo-candidates ] [--memo-enumeration-limit ] [--max-enumerated ] [--price-enumerated ] --out "; /// Default evaluator-assignment budget for an exhaustive archive run. @@ -404,9 +402,6 @@ fn run(args: Vec) -> Result<(), Failure> { let mut cost_inputs_path: Option = None; let mut enforce_accuracy_guarantees = false; let mut exhaustive = false; - // `--milp-time-limit-s` bounds the facade's planning budget (Pass 2 and - // its rounds); Stage 3's MILP always runs to optimality (#1185). - let mut milp_time_limit_ms: Option = None; let mut max_combinations_given = false; let mut write_memo = true; let mut memo_caps = MemoCaps { @@ -489,12 +484,6 @@ fn run(args: Vec) -> Result<(), Failure> { "--price-enumerated" => { memo_caps.choices_to_price = value()?.parse().map_err(|e| format!("{e}"))? } - "--milp-time-limit-s" => { - let seconds: f64 = value()? - .parse() - .map_err(|e| format!("--milp-time-limit-s: {e}"))?; - milp_time_limit_ms = Some((seconds * 1e3) as u64); - } other => return Err(format!("unknown argument {other}").into()), } } @@ -746,10 +735,7 @@ fn run(args: Vec) -> Result<(), Failure> { &data, models, // The document shows every stage's memo. - PlanSearchSettings { - time_limit_ms: milp_time_limit_ms, - inspect: true, - }, + PlanSearchSettings { inspect: true }, exhaustive.then_some(max_combinations), ); // A rate, increase or irate over a declared gauge, or an aggregation of @@ -1407,7 +1393,7 @@ fn accepted_json(queries: &[AcceptedAccuracy]) -> Value { } /// Stage 3's selection ([`Selection`]): the selected DAG over the selected -/// round's Stage 2 candidates (one node per run of a candidate), the nodes +/// Stage 2 candidates (one node per run of a candidate), the nodes /// whose weights it proved non-negative, the plan's cost, the accuracy accepted per query, the /// candidates it pruned and its search report; `error` when the facade /// selected no plan. diff --git a/crates/devtools/src/memo_pricing.rs b/crates/devtools/src/memo_pricing.rs index 3e7e688ff..469e2513e 100644 --- a/crates/devtools/src/memo_pricing.rs +++ b/crates/devtools/src/memo_pricing.rs @@ -254,7 +254,7 @@ pub struct EnumeratedChoice { } /// `stage_pipeline --exhaustive`: the first `max_choices` choices of the -/// selected round's Pass 2 memo (`run.logical_asap_candidates`), each composed, +/// Pass 2 memo (`run.logical_asap_candidates`), each composed, /// implemented by Stage 2 with every materialization assignment, and all /// their physical plans priced together by the reference pricer. #[derive(Debug)] diff --git a/crates/devtools/src/planning_stages.rs b/crates/devtools/src/planning_stages.rs index 47618fdd6..a0f6a7f6a 100644 --- a/crates/devtools/src/planning_stages.rs +++ b/crates/devtools/src/planning_stages.rs @@ -21,9 +21,9 @@ pub const DEFAULT_REFERENCE_CHOICES: usize = 10_000; pub struct PlanningStagesRun { /// The query-language frontends' roots, one per workload entry. pub stage0_query_language_frontends: Vec, - /// The facade's run: Pass 1, the tumbling-window-width rounds and the selection. + /// The facade's run: every stage and the selection. pub run: PlanningRun, - /// `--exhaustive`: the selected round's memo choices, each implemented + /// `--exhaustive`: Pass 2's memo choices, each implemented /// by Stage 2 and all priced by the reference pricer. pub reference: Option, } @@ -188,11 +188,8 @@ pub fn run_planning_stages_over( entries, data, models, - // The reference enumeration reads the selected round's memo. - PlanSearchSettings { - inspect: true, - ..PlanSearchSettings::default() - }, + // The reference enumeration reads Pass 2's memo. + PlanSearchSettings { inspect: true }, Some(max_combinations.unwrap_or(DEFAULT_REFERENCE_CHOICES)), ) } diff --git a/crates/devtools/tests/corpus_snapshots.rs b/crates/devtools/tests/corpus_snapshots.rs index 119ba4dfb..3f347a364 100644 --- a/crates/devtools/tests/corpus_snapshots.rs +++ b/crates/devtools/tests/corpus_snapshots.rs @@ -15,12 +15,8 @@ //! and accepted accuracy per query, the materialized views, tumbling-window retention, //! and whether the search proved the plan optimal; for a query that does not //! plan, the error. Candidate ids, round and evaluation counters, timings, the -//! Stage 1 and Stage 2 candidate lists and the memo vary from run to run -//! (round budgets are wall-clock), so they are left out. Floats are rounded -//! to 12 significant digits. A run that the time limit stopped fails instead -//! of being recorded, unless the query has a `time_limit_reason` in the -//! manifest (its plan is then recorded and compared as usual); a query with a -//! `skip_reason` is skipped. +//! Stage 1 and Stage 2 candidate lists and the memo are left out. Floats are +//! rounded to 12 significant digits. A query with a `skip_reason` is skipped. //! //! The same binary holds the sketch plan snapshots (`sketch_plan_snapshots.rs`), //! which reuse these helpers on multi-query workloads with accuracy targets. @@ -33,10 +29,6 @@ use serde_json::{json, Map, Value}; #[path = "sketch_plan_snapshots.rs"] mod sketch_plan_snapshots; -/// Per query. The slowest kept queries take about 60 s on one core, so this -/// leaves room for a slower CI runner while still failing a runaway search. -const TIME_LIMIT_S: &str = "240"; - /// Held for the whole of each planner snapshot test, so the corpus and /// sketch plan tests do not run at once (`--include-ignored` would otherwise /// run them in parallel) and slow each other's searches. @@ -69,14 +61,13 @@ fn plan(workload: &Value, args: &[String], scratch: &Path) -> Value { stage_pipeline(&input_args, scratch) } -/// Runs `stage_pipeline` (without the memo section, under the -/// [`TIME_LIMIT_S`] budget) with `args`, which name its input; panics (a -/// planner crash) on failure. +/// Runs `stage_pipeline` (without the memo section) with `args`, which name +/// its input; panics (a planner crash) on failure. fn stage_pipeline(args: &[String], scratch: &Path) -> Value { let output = scratch.join("document.json"); let mut command = Command::new(env!("CARGO_BIN_EXE_stage_pipeline")); command - .args(["--milp-time-limit-s", TIME_LIMIT_S, "--no-memo"]) + .arg("--no-memo") .args(args) .arg("--out") .arg(&output); @@ -89,40 +80,14 @@ fn stage_pipeline(args: &[String], scratch: &Path) -> Value { serde_json::from_str(&std::fs::read_to_string(output).unwrap()).unwrap() } -/// Whether the wall-clock budget ended the search: planning (Pass 1 and -/// every tumbling-window-width round) used the whole budget. A round cut short by its -/// own budget is not visible in the report. -fn hit_time_limit(document: &Value) -> bool { - let report = &document["planning_report"]; - let Some(limit) = report["time_limit_ms"].as_f64() else { - return false; - }; - let rounds: f64 = report["rounds"] - .as_array() - .into_iter() - .flatten() - .map(|round| { - ["pass2_ms", "stage2_ms", "stage3_ms"] - .iter() - .filter_map(|key| round[*key].as_f64()) - .sum::() - }) - .sum(); - report["pass1_ms"].as_f64().unwrap_or(0.0) + rounds >= limit -} - -/// The selected plan's search status (`optimal`; `deadline_reached` in -/// documents from before the MILP ran without a time limit), or null -/// without a plan. +/// The selected plan's search status (`optimal`), or null without a plan. fn search_status(document: &Value) -> &Value { &document["stage3_plan_selection"]["search_report"]["status"] } -/// Whether the plan is proven optimal: its search says so and every legal -/// tumbling-window width was searched. +/// Whether the plan is proven optimal: its search says so. fn proven_optimal(document: &Value) -> bool { search_status(document) == "optimal" - && document["planning_report"]["tumbling_window_widths_pending"] != json!(true) } /// `value` with every non-integer number rounded to 12 significant digits. @@ -301,15 +266,6 @@ fn corpus_plans_match_snapshots() { &args, &scratch, ); - // A `time_limit_reason` accepts a run the time limit ended; the - // plan and cost are still compared with the snapshot. - if hit_time_limit(&document) && query.get("time_limit_reason").is_none() { - failures.push(format!( - "{name}/{id}: the {TIME_LIMIT_S} s time limit ended the search, \ - so its plan depends on machine speed" - )); - continue; - } let actual = serde_json::to_string_pretty(&snapshot_of(&document)).unwrap() + "\n"; let path = dir.join(format!("{id}.json")); if update { @@ -361,7 +317,7 @@ fn snapshot_keeps_only_the_plan() { "accepted_accuracy_per_query": [], "materialized_views": [], "tumbling_window_retention_ms": {"1": 60000}, - "search_report": {"status": "deadline_reached", "solver_ms": 1}, + "search_report": {"solver_ms": 1}, }, }); assert_eq!( @@ -404,36 +360,13 @@ fn snapshot_of_a_failed_query() { ); } -/// Planning that used its whole wall-clock budget is detected; planning -/// within it, or with no report, is not. -#[test] -fn time_limit_detection() { - let report = |pass1: f64, stage3: f64| { - json!({"planning_report": { - "pass1_ms": pass1, "time_limit_ms": 1000, - "rounds": [{"pass2_ms": 1.0, "stage2_ms": 1.0, "stage3_ms": stage3}], - }}) - }; - assert!(hit_time_limit(&report(10.0, 990.0))); - assert!(!hit_time_limit(&report(10.0, 12.0))); - assert!(!hit_time_limit( - &json!({"stage3_plan_selection": {"error": "x"}}) - )); -} - -/// An optimal search proves the plan optimal only when no legal tumbling-window width -/// was left unsearched. +/// Only an `optimal` search status proves the plan optimal. #[test] -fn proven_optimal_needs_every_tumbling_window_width() { - let document = |status: &str, pending: bool| { - json!({ - "stage3_plan_selection": {"search_report": {"status": status}}, - "planning_report": {"tumbling_window_widths_pending": pending}, - }) - }; - assert!(proven_optimal(&document("optimal", false))); - assert!(!proven_optimal(&document("optimal", true))); - assert!(!proven_optimal(&document("deadline_reached", false))); +fn proven_optimal_needs_an_optimal_status() { + let document = + |status: Value| json!({"stage3_plan_selection": {"search_report": {"status": status}}}); + assert!(proven_optimal(&document(json!("optimal")))); + assert!(!proven_optimal(&document(Value::Null))); } /// The failure message names the first differing JSON path. diff --git a/crates/devtools/tests/corpus_snapshots/o11y_bench/corpus.json b/crates/devtools/tests/corpus_snapshots/o11y_bench/corpus.json index 7284fa7bf..1b55ea4d7 100644 --- a/crates/devtools/tests/corpus_snapshots/o11y_bench/corpus.json +++ b/crates/devtools/tests/corpus_snapshots/o11y_bench/corpus.json @@ -78,8 +78,6 @@ { "id": "q05", "query": "max_over_time((sum(rate(http_requests_total{job=\"order-service\",status=~\"5..\"}[5m])) / sum(rate(http_requests_total{job=\"order-service\"}[5m])))[6h:1m])" -, - "time_limit_reason": "searching every pane-width round to Complete runs past the 240 s limit; the plan and cost found before it are the same as when planning stopped after 16 s" }, { "id": "q06", diff --git a/crates/devtools/tests/memo_pricing.rs b/crates/devtools/tests/memo_pricing.rs index 66826b145..68b85028d 100644 --- a/crates/devtools/tests/memo_pricing.rs +++ b/crates/devtools/tests/memo_pricing.rs @@ -215,7 +215,6 @@ fn assert_no_listed_choice_is_cheaper(document: &Value) -> Vec { /// Q2's summary reads miss its 100 ms latency bound, and no valid DAG is /// cheaper than the selection. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn example1_no_listed_choice_is_cheaper_than_the_selected_plan() { let document = priced_document(&["--example", "planner-layering-1"]); let choices = assert_no_listed_choice_is_cheaper(&document); @@ -233,7 +232,6 @@ fn example1_no_listed_choice_is_cheaper_than_the_selected_plan() { /// has a guarantee through `/` and `+` over a KLL rank error or a DDSketch /// relative one, so every listed DAG is exact and valid. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn ratio_of_quantiles_no_listed_choice_is_cheaper_than_the_selected_plan() { let ignored = assert_no_listed_choice_is_cheaper(&priced_document(&[ "--workload", diff --git a/crates/devtools/tests/planner_fuzz.rs b/crates/devtools/tests/planner_fuzz.rs index 2b9b78515..73df7cc00 100644 --- a/crates/devtools/tests/planner_fuzz.rs +++ b/crates/devtools/tests/planner_fuzz.rs @@ -140,7 +140,6 @@ fn exact_duplicate_of_a_shared_query_holds() { /// A bounded worker campaign writes the summary with the design-doc coverage /// table and one draft per finding, including cases stopped by its time limit. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn campaign_writes_summary_coverage_and_drafts() { use asap_devtools::planner_fuzz::report::{run_campaign, Campaign}; let out = Path::new(env!("CARGO_TARGET_TMPDIR")) diff --git a/crates/devtools/tests/sketch_plan_snapshots.rs b/crates/devtools/tests/sketch_plan_snapshots.rs index c9047256c..1196f1e1f 100644 --- a/crates/devtools/tests/sketch_plan_snapshots.rs +++ b/crates/devtools/tests/sketch_plan_snapshots.rs @@ -18,24 +18,16 @@ //! Regenerate with //! `UPDATE_SNAPSHOTS=1 cargo test --release -p asap-devtools --test corpus_snapshots -- --ignored sketch_plans_match_snapshots`. //! -//! The search's seed and tumbling-window-width rounds have wall-clock budgets (5 s -//! each), so a search that stops at a budget, of time or of evaluations, can -//! select a different plan on a slower machine. Only a search that proves its -//! plan optimal is independent of speed, so every workload must end with -//! status `optimal` and no tumbling-window width left unsearched; any other ending -//! fails instead of being recorded. Each kept workload gave the same plan in three runs and on one core. -//! Workloads whose search stops at a budget (the raw-retained memo examples, -//! full random-mixed workloads, three or more quantile queries over 10,000 -//! series, the `or` and `COUNT(DISTINCT)` workloads, single quantiles under -//! the default and raw-retained deployments) are left out. +//! Planning has no time limit, so every workload must end with status +//! `optimal`; any other ending fails instead of being recorded. use std::path::PathBuf; use serde_json::{json, Value}; use super::{ - first_difference, first_differing_path, hit_time_limit, plan_snapshot, proven_optimal, rounded, - search_status, stage_pipeline, + first_difference, first_differing_path, plan_snapshot, proven_optimal, rounded, search_status, + stage_pipeline, }; fn repository_root() -> PathBuf { @@ -134,21 +126,10 @@ fn sketch_plans_match_snapshots() { for entry in entries { let id = entry["id"].as_str().unwrap(); let document = stage_pipeline(&arguments(&manifest, entry), &scratch); - if hit_time_limit(&document) { - failures.push(format!( - "{id}: the {} s time limit ended the search, so its plan depends on \ - machine speed", - super::TIME_LIMIT_S - )); - continue; - } if !proven_optimal(&document) { failures.push(format!( - "{id}: the plan is not proven optimal (status {}, tumbling-window widths \ - pending {}), so a round's budget may have chosen it; drop the \ - workload or make it smaller", - search_status(&document), - document["planning_report"]["tumbling_window_widths_pending"] + "{id}: the plan is not proven optimal (status {})", + search_status(&document) )); continue; } @@ -197,7 +178,6 @@ fn sketch_plans_match_snapshots() { /// Each manifest entry names a known deployment and an existing input, and /// builds its arguments. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn manifest_entries_build_their_arguments() { let manifest: Value = serde_json::from_str( &std::fs::read_to_string(snapshots_dir().join("manifest.json")).unwrap(), diff --git a/crates/devtools/tests/stage_pipeline.rs b/crates/devtools/tests/stage_pipeline.rs index fdc39e876..df58ba3e1 100644 --- a/crates/devtools/tests/stage_pipeline.rs +++ b/crates/devtools/tests/stage_pipeline.rs @@ -97,7 +97,6 @@ fn validated_dag(value: &Value, queries: usize) { /// Stage 2 candidate once and costs exactly the valid ones; the committed /// fixture's workload is current. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn example1_document_preserves_workload_and_validates_archived_plans() { // Example 1's story is about which plans miss their accuracy target; // archive enough evaluated plans to check the cross-stage linkage. @@ -176,7 +175,6 @@ fn example1_document_preserves_workload_and_validates_archived_plans() { /// `--max-displayed` cuts only what the document shows, says so, and keeps /// the selection made over all candidates. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn display_cap_is_recorded_and_keeps_the_selection() { // Instant quantiles provide several implementations without expanding a // tumbling-window-width domain. Exhaustive mode ensures the display cap is exercised. @@ -248,7 +246,6 @@ fn pass1_pruned(document: &serde_json::Value) -> Vec<(String, String)> { /// is pruned in Pass 1 (`TopKHeapOverOneRowPerItem`), listed under /// `pruned_candidates`, and every candidate left composes. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn a_topk_heap_over_one_row_per_item_is_pruned_with_its_reason() { let document = document_of(&[ "--promql", @@ -280,7 +277,6 @@ fn a_topk_heap_over_one_row_per_item_is_pruned_with_its_reason() { /// display cap does not change the selection, and the selected plan is /// displayed with its cost. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn example3a_archive_and_viewer_select_consistently() { // Candidate IDs are local to one time-bounded search. Validate the viewer // references within its own archive instead of comparing independent runs. @@ -305,7 +301,6 @@ fn example3a_archive_and_viewer_select_consistently() { /// Example 2 plans its three SQL queries with the default search and /// selects a plan. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn example2_plans_the_sql_workload() { // The default search: its SQL windows get tumbling windows, so an // exhaustive run has over 10^4 Stage 2 candidates. The integration test @@ -322,7 +317,6 @@ fn example2_plans_the_sql_workload() { /// SQL queries over tables declared with `--table` are planned through the /// same stages, and the document records each query's language. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn sql_queries_over_declared_tables_are_planned() { let out = std::env::temp_dir().join(format!("stage_pipeline_{}_sql.json", std::process::id())); let status = Command::new(env!("CARGO_BIN_EXE_stage_pipeline")) @@ -357,7 +351,6 @@ fn sql_queries_over_declared_tables_are_planned() { /// One run mixes SQL (over declared tables) and PromQL queries, in the /// order given, each named with `--name`. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn mixed_sql_and_promql_queries_are_planned_with_their_names() { let out = std::env::temp_dir().join(format!("stage_pipeline_{}_mixed.json", std::process::id())); @@ -419,7 +412,6 @@ fn failure(args: &[&str]) -> (Option, String) { /// Exhaustive mode bounds evaluated assignments and archives the examined prefix. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn workload_above_the_candidate_limit_reports_partial_search() { let document = generate(&["--max-combinations", "3", "--max-displayed", "100"]); // The memo has more than three choices: the reference enumeration @@ -457,7 +449,6 @@ fn nine_quantile_queries() -> Vec { /// enumerating the combinations. The document holds the selected plan and /// the search's report. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn default_search_plans_nine_queries_without_cartesian_enumeration() { let args = nine_quantile_queries(); let args: Vec<&str> = args.iter().map(String::as_str).collect(); @@ -475,7 +466,6 @@ fn default_search_plans_nine_queries_without_cartesian_enumeration() { /// `--max-combinations` limits only the exhaustive search. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn max_combinations_needs_exhaustive() { let (status, stderr) = failure(&["--promql", "sum(m)", "--max-combinations", "10"]); assert_eq!(status, Some(2)); @@ -488,7 +478,6 @@ fn max_combinations_needs_exhaustive() { /// enforced. Timed searches may choose different incumbents and archive IDs; /// the API contract is output coverage, accuracy, and executable installation. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn default_search_and_installation_validate_the_nine_query_workload() { const WORKLOAD: &str = "../../tools/dag-viewer/workloads/random-mixed-seed-862/workload-01.json"; @@ -565,7 +554,6 @@ fn default_search_and_installation_validate_the_nine_query_workload() { /// queries read, as in the other lowering tools: `sum(up)`'s bare selector /// looks back 30 s. The former `--interval-ms` is unknown. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn sample_interval_ms_sets_the_instant_selector_lookback() { let document = document_of(&["--promql", "sum(up)", "--sample-interval-ms", "30000"]); let ranges: Vec<&Value> = document["stage0_query_language_frontends"]["dag"]["nodes"] @@ -585,7 +573,6 @@ fn sample_interval_ms_sets_the_instant_selector_lookback() { } #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn table_declared_twice_is_rejected() { let table = r#"{"name": "metrics", "columns": [{"name": "ts", "type": "timestamp"}], "time_index": 0}"#; @@ -606,7 +593,6 @@ fn table_declared_twice_is_rejected() { /// A defaulted name that collides says it is the default. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn duplicate_default_name_says_it_is_the_default() { let (_, stderr) = failure(&["--promql", "sum(a)", "--name", "q2", "--promql", "sum(b)"]); assert!( @@ -649,11 +635,9 @@ fn document_of(args: &[&str]) -> Value { document } -/// CLI planning includes local candidate generation in the same budget/report -/// as the production planner, rather than bypassing the timer via from_pass1. +/// CLI planning reports Pass 1's time as the production planner does. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] -fn cli_accounts_for_pass1_generation_in_the_planning_budget() { +fn cli_reports_pass1_generation_time() { let document = document_of(&["--promql", "sum(data)"]); let selection = &document["stage3_plan_selection"]; assert!(selects_a_plan(selection), "{selection}"); @@ -666,7 +650,6 @@ fn cli_accounts_for_pass1_generation_in_the_planning_budget() { /// When Pass 1 fails, the document still carries the Stage 0 DAG, with the /// error under Stage 1 and no selection, so the viewer can show both. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn pass1_failure_still_writes_stage0_and_the_error() { let document = document_of(&[ "--promql", @@ -711,7 +694,6 @@ const SCALAR_QUERIES_ARGS: &[&str] = &[ ]; #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn scalar_queries_keep_their_positions_in_scalar_roots() { let document = document_of(SCALAR_QUERIES_ARGS); assert!(selects_a_plan(&document["stage3_plan_selection"])); @@ -733,7 +715,6 @@ fn scalar_queries_keep_their_positions_in_scalar_roots() { /// `time_index` must be a column index; anything else is an error rather /// than a table without a time column. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn table_time_index_must_be_a_column_index() { for index in [r#""0""#, "-1", "1.5", "3"] { let table = format!( @@ -754,7 +735,6 @@ fn table_time_index_must_be_a_column_index() { /// naturally stated in (the default-metric table, display only): a top-k's /// is the L1-scaled absolute error; an exact query shows none. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn a_target_without_a_metric_shows_the_query_default() { let document = generate(&[]); let queries = &document["workload"]["queries"]; @@ -773,7 +753,6 @@ fn a_target_without_a_metric_shows_the_query_default() { /// accuracy is the target's own JSON form. Guarantees are enforced, since /// the test is about which guarantees meet the target. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn a_quantile_target_without_a_metric_accepts_rank_or_relative() { let query = |metric: Option<&str>| { let mut accuracy = serde_json::json!({"max_error": 0.01}); @@ -861,7 +840,7 @@ fn a_quantile_target_without_a_metric_accepts_rank_or_relative() { /// usage. Cost model 3 is run with guarantees enforced: ignoring them, a /// candidate without a guarantee fits the cluster better. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] +#[ignore = "fails on main too: the selection MILP does not support cost models 2 and 3 (UnsupportedCostModel); not a wall-clock issue; #888"] fn cost_model_flag_chooses_how_candidates_are_compared() { let selected = |extra: &[&str]| { let document = generate_example("planner-layering-3b", extra); @@ -899,7 +878,6 @@ fn cost_model_flag_chooses_how_candidates_are_compared() { /// samples). A costlier candidate's reason shows /// such small costs in scientific notation. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn aws_fargate_preset_prices_example_4_in_dollars() { // Pricing acceptance exercises the production bounded planner; exhaustive // archive traversal is tested separately on small catalogs. @@ -926,7 +904,6 @@ fn aws_fargate_preset_prices_example_4_in_dollars() { /// (`P3-m1`) is selected when guarantees are enforced. A deployment with an /// unknown field is refused. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn deployment_capabilities_flag_sets_the_capabilities_and_shows_them() { let default = generate_example("planner-layering-3b", &[]); assert_eq!( @@ -1004,7 +981,6 @@ fn deployment_capabilities_flag_sets_the_capabilities_and_shows_them() { /// Cluster limits need cost model 3, and cost model 3 needs them. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn cluster_flags_and_cost_model_3_go_together() { let (code, stderr) = failure(&[ "--exhaustive", @@ -1033,7 +1009,6 @@ fn cluster_flags_and_cost_model_3_go_together() { /// range between its own pair of queries: every logical candidate's label /// names which queries share each node, so no two labels are the same. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn logical_candidate_labels_name_each_query() { let document = document_of(&[ "--exhaustive", @@ -1066,7 +1041,6 @@ fn logical_candidate_labels_name_each_query() { /// charge, here the query-time top-k tumbling windows at about 308 times the /// selected plan) used to replace round 1's, reporting a gap of 3.09e-4. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn milp_final_bound_keeps_the_bound_an_earlier_round_proved() { let workload = serde_json::json!({ "query_workload": { @@ -1131,7 +1105,6 @@ fn milp_final_bound_keeps_the_bound_an_earlier_round_proved() { /// (`P1`, 1,270.52), and the document reports their retention, 1 h + 5 min. /// The workload section shows the range. An invalid range is refused. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn workload_file_with_a_range_evaluation_is_planned() { let workload = serde_json::json!({ "query_workload": { @@ -1261,7 +1234,7 @@ fn workload_file_with_a_range_evaluation_is_planned() { /// `--cost-model provisioned-capacity` selects for Example 4 (P1). The file /// replaces the cost-model flags, so combining them is refused. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] +#[ignore = "fails on main too: the selection MILP does not support cost models 2 and 3 (UnsupportedCostModel); not a wall-clock issue; #888"] fn plan_selection_settings_flag_loads_the_settings_from_json() { let path = std::env::temp_dir().join(format!( "stage_pipeline_{}_plan_selection_settings.json", @@ -1302,7 +1275,6 @@ fn plan_selection_settings_flag_loads_the_settings_from_json() { /// have rejected it. The document says which mode it /// planned under; there is no `--ignore-accuracy-guarantees`. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn accuracy_guarantees_are_ignored_by_default_and_enforced_on_request() { let args = [ "--exhaustive", @@ -1431,7 +1403,7 @@ fn workload_set_index_and_files_validate() { /// materialized views as `plan_installable`'s plan derives them, each mapped /// to a node of the selected Stage 2 candidate. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] +#[ignore = "slow without a planning budget: over 30 min in a debug build; #1197"] fn workload_examples_preserve_data_and_valid_materialized_views() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tools/dag-viewer"); let examples: Value = @@ -1444,8 +1416,8 @@ fn workload_examples_preserve_data_and_valid_materialized_views() { .filter(|e| e.get("workload").is_some()) .collect(); assert_eq!(workload_examples.len(), 10); - // Each production search has a wall-clock budget. Bound fixture - // concurrency so this acceptance test does not starve its own solvers. + // Bound fixture concurrency so this acceptance test does not starve its + // own solvers. for batch in workload_examples.chunks(2) { std::thread::scope(|scope| { for example in batch { @@ -1558,9 +1530,8 @@ fn check_candidate_example(document: Value, _committed: Value, example: &Value) example["id"] ); let report = &document["planning_report"]; - let round = &report["rounds"][report["selected_round"].as_u64().unwrap() as usize]; assert!( - round["physical_candidates"].as_u64().unwrap() > 0, + report["physical_candidates"].as_u64().unwrap() > 0, "{report}" ); } @@ -1568,7 +1539,6 @@ fn check_candidate_example(document: Value, _committed: Value, example: &Value) /// The `--promql` path's data workload is what it plans with: every metric /// the queries read, sampled every `--sample-interval-ms`. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn promql_queries_carry_their_sampled_data_workload() { // `document_of` declares 1,000 series and 60,000 rows held per source. let document = document_of(&[ @@ -1596,7 +1566,6 @@ fn promql_queries_carry_their_sampled_data_workload() { /// rejected ones, each cause in turn; `pruned_candidate_count_by_cause` counts every /// rejection. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn display_cap_shows_the_selected_then_valid_cheapest_then_each_cause() { let document = generate_example("planner-layering-3a", &["--max-displayed", "6"]); let selection = &document["reference_pricing"]; @@ -1624,7 +1593,6 @@ fn display_cap_shows_the_selected_then_valid_cheapest_then_each_cause() { /// families, as `plan_installable` does. CountSketch cannot answer a total, so /// it is neither generated for count nor reported as deployment-pruned. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn undeclared_supported_summaries_default_to_the_executors_families() { let document = document_of(&[ "--exhaustive", @@ -1680,7 +1648,6 @@ const QUANTILE_BY_WORKLOAD: &str = r#"{"query_workload": {"language": "promql", /// the workload does not declare), for its accuracy; none fails to /// execute. Accepted plans answer Q3 and Q4 exactly. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn exact_grouped_quantile_plans_compile_and_are_compared() { let path = std::env::temp_dir().join(format!( "stage_pipeline_{}_quantile_by_workload.json", @@ -1736,7 +1703,6 @@ fn exact_grouped_quantile_plans_compile_and_are_compared() { /// once (≈0.1364/s) instead of once per reader (0.2203/s), and beats the /// separate scans (0.1530/s). #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn a_scan_two_queries_share_is_offered_built_once_per_evaluation() { let query = |q: &str| { serde_json::json!({ @@ -1887,7 +1853,6 @@ fn widened_scans_keep_memo_groups_and_stage2_candidates_acyclic() { } #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn scans_projecting_different_labels_are_shared() { let query = |q: &str| { serde_json::json!({ @@ -2042,7 +2007,6 @@ fn scans_projecting_different_labels_are_shared() { /// The default search is the candidate-connection search; its report has /// no legacy route. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn default_search_reports_candidate_connections() { let document = document_of(&["--promql", "sum(m)"]); assert_eq!(document["search"], "candidate-connections"); @@ -2055,7 +2019,6 @@ fn default_search_reports_candidate_connections() { /// There is one search: `--search` is not an option, so no engine name, /// current or retired, is accepted. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn search_flag_is_rejected() { for engine in ["candidate-connections", "legacy", "memo-milp"] { let (status, stderr) = failure(&["--promql", "sum(m)", "--search", engine]); @@ -2064,36 +2027,11 @@ fn search_flag_is_rejected() { } } -/// A zero planning budget is accepted for the default engine; it must not claim optimality. -#[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] -fn milp_time_limit_is_reported() { - let document = document_of(&["--promql", "sum(m)", "--milp-time-limit-s", "0"]); - let selection = &document["stage3_plan_selection"]; - assert!(!selects_a_plan(selection), "{selection}"); - // The facade's report keeps the budget and selects no round. - let report = &document["planning_report"]; - assert!( - report.is_object(), - "budget stop retains its report: {selection}" - ); - assert_eq!(report["time_limit_ms"], 0); - assert!(report["selected_round"].is_null(), "{report}"); - assert!( - selection["error"] - .as_str() - .unwrap() - .contains("planning budget"), - "{selection}" - ); -} - /// The memo search's document names its selected plan's logical candidate /// under the id its physical candidates read (`from_logical`), as the /// viewer's validator requires: the brute force numbers its logical plans, /// so the selected one need not be the first. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn the_memo_search_document_names_the_selected_logical_candidate() { let document = document_of(&[ "--promql", @@ -2128,7 +2066,6 @@ fn the_memo_search_document_names_the_selected_logical_candidate() { /// enough to compare a plan's predicted CPU with a runtime's measurement /// without `plan_installable`. Report-only: prices are unchanged. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn per_node_costs_show_operator_timing_and_predicted_cpu() { // The selected plan supplies the serialization evidence; exhaustive // candidate coverage is tested separately by the archive tests. @@ -2170,7 +2107,6 @@ fn per_node_costs_show_operator_timing_and_predicted_cpu() { /// (which the viewer shows) naming the query, the function and the metric. /// A counter gets none. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn a_counter_function_over_a_declared_gauge_plans_with_a_warning() { let workload = |metric_type: &str| { serde_json::json!({ @@ -2281,7 +2217,6 @@ const FLOWS_TABLE: &str = r#"{"name": "flows", "columns": [{"name": "ts", "type" /// file, q2 from the SQL file) and its data workload is the union of the /// files' sources, with their arrival rates added (1000 + 500 rows/s). #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn several_workload_files_in_different_languages_are_planned_together() { let (promql, sql) = mixed_language_workload_files( serde_json::json!({"ingesting": {"rows_per_second": 500.0}}), @@ -2331,7 +2266,6 @@ fn several_workload_files_in_different_languages_are_planned_together() { /// sources, so `flows` (10 groups, no sample interval) would be priced at /// 1500 × 10 / 15010 ≈ 1 row/s instead of its file's 500. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn workload_files_that_disagree_on_the_data_are_refused() { let (promql, sql) = mixed_language_workload_files( serde_json::json!({"ingesting": {"rows_per_second": 500.0}}), @@ -2410,7 +2344,6 @@ fn workload_files_that_disagree_on_the_data_are_refused() { /// enforced gets a selected plan the executor compiles, as its bucket rates /// alone (the control) already did. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn a_classic_histogram_quantile_gets_a_selected_plan() { let buckets = r#"sum by (le) (rate(http_request_duration_seconds_bucket{service="customer-orders-service"}[5m]))"#; for query in [ @@ -2437,7 +2370,6 @@ const MEMO_RATIO_OF_QUANTILES: &str = /// The `memo` section: the memo after each step, the selected choice and /// the enumeration, on the issue's example. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn memo_section_lists_each_step_the_selected_choice_and_the_enumeration() { let out = std::env::temp_dir().join(format!( "stage_pipeline_{}_memo_ratio.json", @@ -2576,7 +2508,7 @@ fn document_with_milp_search(args: &[&str]) -> Value { /// #1095 C5: with the group selection MILP, example 1 and the RangeHeavy T5 /// q0.5 + q0.9 workload select a plan through the real pipeline, solved to -/// optimality or stopped at the deadline, whose `plan_cost` equals the MILP's objective exactly (the +/// optimality, whose `plan_cost` equals the MILP's objective exactly (the /// search errors on a difference and reports `inexact_objective` when the /// model encoded a term inexactly) and which passes every query's accuracy /// check. @@ -2689,10 +2621,11 @@ fn milp_search_selects_checked_plans_on_ratio_and_rollupheavy() { /// #1095 C5: on RangeHeavy T8+T5 (`q0.9 / q0.5` with both operands also /// queried alone), the group selection MILP selects a DDSketch plan that passes every /// query's accuracy check (the A1 rows admitted KLL there, which the check -/// rejects). Which chains it shares depends on the deadline; the weights +/// rejects). The weights /// that let one chain serve all three queries are checked in /// `groups::tests::one_pane_chain_serves_a_ratio_and_its_operands`. #[test] +#[ignore = "slow without a planning budget: over 30 min in a debug build; #1197"] fn milp_search_selects_a_checked_ddsketch_plan_for_a_ratio_and_its_operands() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../tools/dag-viewer/memo-examples/promql/rangeheavy-t8-t5-1h.json"); diff --git a/crates/executor/src/error.rs b/crates/executor/src/error.rs index 26351b9d2..afee33d80 100644 --- a/crates/executor/src/error.rs +++ b/crates/executor/src/error.rs @@ -1,8 +1,6 @@ use crate::plan::NodeId; #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum Error { - #[error("physical compilation deadline exceeded")] - CompilationDeadlineExceeded, #[error("invalid DAG: {0}")] Invalid(String), #[error("operator failed: {0}")] diff --git a/crates/executor/src/physical_planner/logical.rs b/crates/executor/src/physical_planner/logical.rs index c6830a077..fc4f78c91 100644 --- a/crates/executor/src/physical_planner/logical.rs +++ b/crates/executor/src/physical_planner/logical.rs @@ -3,11 +3,7 @@ use super::*; use planner_types::ir::{ASAPOp, Operator as LogicalOperator, OperatorNode}; use std::rc::Rc; -pub(super) fn restore_before( - dag: &PhysicalASAPDAG, - deadline: Option, -) -> Result>, Error> { - check_compile_deadline(deadline)?; +pub(super) fn restore(dag: &PhysicalASAPDAG) -> Result>, Error> { dag.validate().map_err(|e| invalid(e.to_string()))?; let mut done: BTreeMap> = BTreeMap::new(); let mut remaining: Vec<_> = dag.nodes.iter().collect(); @@ -15,7 +11,6 @@ pub(super) fn restore_before( let before = remaining.len(); let mut next = Vec::new(); for node in remaining { - check_compile_deadline(deadline)?; if node .payload .children() diff --git a/crates/executor/src/physical_planner/mod.rs b/crates/executor/src/physical_planner/mod.rs index 56f194fd1..c78dec714 100644 --- a/crates/executor/src/physical_planner/mod.rs +++ b/crates/executor/src/physical_planner/mod.rs @@ -61,29 +61,7 @@ pub fn compile( views: &[MaterializedView], roots: &[NodeId], ) -> Result { - compile_before(dag, views, roots, None) -} - -/// Compile with cooperative cancellation between planning operations. The -/// ordinary runtime compiler delegates here without a deadline. -pub fn compile_before( - dag: &PhysicalASAPDAG, - views: &[MaterializedView], - roots: &[NodeId], - deadline: Option, -) -> Result { - check_compile_deadline(deadline)?; - let compiled = compile_internal(dag, views, roots, deadline)?; - check_compile_deadline(deadline)?; - Ok(compiled) -} - -fn check_compile_deadline(deadline: Option) -> Result<(), Error> { - if deadline.is_some_and(|limit| std::time::Instant::now() >= limit) { - Err(Error::CompilationDeadlineExceeded) - } else { - Ok(()) - } + compile_internal(dag, views, roots) } #[cfg(test)] @@ -107,9 +85,8 @@ fn compile_internal( dag: &PhysicalASAPDAG, views: &[MaterializedView], roots: &[NodeId], - deadline: Option, ) -> Result { - preflight_depth(dag, deadline)?; + preflight_depth(dag)?; // A tumbling-window chain stands for its tumbling windows; the deployment binds each tumbling window's // raw input, so it runs only once expanded. if let Some(node) = dag.nodes.iter().find(|n| { @@ -128,7 +105,7 @@ fn compile_internal( if let Some(node) = dag.nodes.iter().find(|n| n.id as u64 > u64::from(u32::MAX)) { return Err(invalid(format!("Planner node ID {} exceeds u32", node.id))); } - let restored = logical::restore_before(dag, deadline)?; + let restored = logical::restore(dag)?; dag.validate().map_err(|e| invalid(e.to_string()))?; let nodes = dag .nodes @@ -150,7 +127,6 @@ fn compile_internal( ) }); for edge in edges { - check_compile_deadline(deadline)?; dependencies .entry(edge.consumer as u64) .or_default() @@ -170,7 +146,6 @@ fn compile_internal( let mut seen = BTreeSet::new(); let mut pending = roots.iter().map(|&id| (id, false)).collect::>(); while let Some((id, expanded)) = pending.pop() { - check_compile_deadline(deadline)?; if expanded { ordered.push(id); continue; @@ -201,7 +176,6 @@ fn compile_internal( let compile_roots: BTreeSet = roots.iter().copied().collect(); let mut physical_dag = CompiledPhysicalDAG::new(roots.to_vec()); for id in ordered { - check_compile_deadline(deadline)?; let node = nodes[&id]; let auxiliary = helper_id(id, 0)?; let output = Arc::new(node.output_schema.clone()); @@ -1494,10 +1468,7 @@ fn expression( } // Bound recursion before invoking the upstream recursive provenance validator. -fn preflight_depth( - dag: &PhysicalASAPDAG, - deadline: Option, -) -> Result<(), Error> { +fn preflight_depth(dag: &PhysicalASAPDAG) -> Result<(), Error> { let mut remaining = dag .nodes .iter() @@ -1508,7 +1479,6 @@ fn preflight_depth( } let mut consumers = BTreeMap::<_, Vec<_>>::new(); for edge in &dag.edges { - check_compile_deadline(deadline)?; if !remaining.contains_key(&edge.producer) { return Err(invalid("missing Planner edge producer")); } @@ -1528,7 +1498,6 @@ fn preflight_depth( let mut depths = BTreeMap::new(); let mut visited = 0; while let Some(id) = ready.pop_front() { - check_compile_deadline(deadline)?; visited += 1; let depth = *depths.get(&id).unwrap_or(&1usize); if depth > 128 { @@ -1608,7 +1577,7 @@ fn local_scalar(expr: &ScalarExpr) -> Result, expected: &[(&str, f64)], query: // for φ outside [0, 1], NaN for a NaN φ. The 0.8, 0.2 and NaN rows are Prometheus's own expected // values for `quantile without(point)` over the same series. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn exact_grouped_quantile_matches_prometheus() { let (inf, nan) = (f64::INFINITY, f64::NAN); for (query, expected) in [ @@ -417,7 +413,6 @@ fn exact_grouped_quantile_matches_prometheus() { // Prometheus; a group of one series returns that series' value for any φ in // [0, 1]. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn exact_grouped_quantile_of_no_series_is_empty() { for query in ["quantile by (job) (0.5, data)", "quantile(0.5, data)"] { let rows = run(&exact_dag(query), &[], 60_000).unwrap(); @@ -455,7 +450,6 @@ fn with_kind( // A comparison Binary over grouped values keeps the groups whose comparison // holds, with their value, on either side of the literal; `bool` yields 1 or 0. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn grouped_comparisons_filter_or_return_bool() { for (query, expected) in [ ( @@ -478,7 +472,6 @@ fn grouped_comparisons_filter_or_return_bool() { // A `bool` comparison Binary over per-series evaluations matches one-to-one and // drops the metric name; a filter keeps the surviving left value. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn per_series_comparisons_filter_or_return_bool() { use planner_types::ir::operator::BinaryOpKind::*; use planner_types::ir::scalar::CompareOpKind::*; @@ -555,7 +548,6 @@ fn counter( // drops the metric name, as Prometheus does. An overflowing stored sum fails // the checked division instead of returning +Inf. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn per_series_average_divides_stored_sum_by_count() { let dag = exact_dag("avg_over_time(m[5m])"); assert_eq!( @@ -578,7 +570,6 @@ fn per_series_average_divides_stored_sum_by_count() { // Unmatched series are dropped; x/0 is +Inf and 0/0 is NaN; an empty side // yields an empty vector. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn per_series_rate_ratio_matches_prometheus() { // Window (0, 300s]: first sample at 60s, extrapolated 60s to the start // (durationToZero is exactly 60s too), so rate = (last - first) * 1.25 / 300. @@ -612,7 +603,6 @@ fn per_series_rate_ratio_matches_prometheus() { // A literal operand applies to every stored per-series value, on either side, // and drops the metric name. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn per_series_scalar_arithmetic_applies_to_stored_evaluations() { let samples = counter("m", "api", 10., 10.).collect::>(); // rate = 40 * 1.25 / 300 = 1/6. @@ -633,7 +623,6 @@ fn per_series_scalar_arithmetic_applies_to_stored_evaluations() { // A literal operand drops the metric name; series that then share a label set // are an error, as in Prometheus, rather than duplicate output series. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn per_series_scalar_arithmetic_rejects_label_sets_equal_without_the_name() { let dag = exact_dag("sum_over_time(m[5m]) * 2"); let samples = counter("m", "api", 10., 10.) @@ -673,7 +662,6 @@ fn with_vector_match( // `on` and `ignoring` reduce each side to the matching labels, which become // the result's labels; a duplicate match group on either side is an error. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn per_series_vector_matching_follows_on_and_ignoring() { use planner_types::ir::operator::VectorMatchKind; let samples = counter("a", "api", 10., 10.) diff --git a/crates/executor/tests/physical_dag.rs b/crates/executor/tests/physical_dag.rs index d53398f3f..bd22b97d5 100644 --- a/crates/executor/tests/physical_dag.rs +++ b/crates/executor/tests/physical_dag.rs @@ -1213,7 +1213,6 @@ fn grouped_temporal_schema_compiles_and_executes_topk() { // A certified candidate set must have authoritative values for every key, including after recovery. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn certified_pruning_rejects_missing_authoritative_values_after_recovery() { use asap_executor::physical_planner::{compile_node, CompiledPhysicalDAG}; use planner_types::ir::properties::*; diff --git a/crates/executor/tests/planspace_series_identity_heap.rs b/crates/executor/tests/planspace_series_identity_heap.rs index c128135a3..18919c26c 100644 --- a/crates/executor/tests/planspace_series_identity_heap.rs +++ b/crates/executor/tests/planspace_series_identity_heap.rs @@ -59,7 +59,6 @@ fn carries_identity(root: &Rc) -> bool { const CURRENT_SERIES_TOPK: &str = "topk by(job)(1, m)"; #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn selection_never_commits_a_series_identity_heap() { let accuracy = AccuracyTarget::max_error(0.1); let root = lower(CURRENT_SERIES_TOPK, &accuracy); diff --git a/crates/executor/tests/precompute_candidates.rs b/crates/executor/tests/precompute_candidates.rs index 6b4b98b61..b540f8110 100644 --- a/crates/executor/tests/precompute_candidates.rs +++ b/crates/executor/tests/precompute_candidates.rs @@ -113,7 +113,6 @@ fn answer( /// Rate state. Moving Sum before Rate would answer differently across /// counter resets. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn grouped_rate_can_be_materialized_before_or_after_grouped_sum() { let dag = grouped_rate(); let state = dag diff --git a/crates/frontend-promql/tests/count_planning.rs b/crates/frontend-promql/tests/count_planning.rs index 1dfe2ab78..b208f7ae9 100644 --- a/crates/frontend-promql/tests/count_planning.rs +++ b/crates/frontend-promql/tests/count_planning.rs @@ -40,7 +40,6 @@ fn grouped_count_offers_hydra_but_selection_keeps_per_group_state() { // Exact series and temporal counts must select a count accumulator, not distinct or sum. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn exact_counts_select_count_accumulators() { for query in ["count(up)", "count by(job)(up)", "count_over_time(up[5m])"] { let root = lower_promql(query, AccuracyTarget::exact()).unwrap(); diff --git a/crates/frontend-promql/tests/observability/metrics_observability.rs b/crates/frontend-promql/tests/observability/metrics_observability.rs index a0234112a..39815ca2e 100644 --- a/crates/frontend-promql/tests/observability/metrics_observability.rs +++ b/crates/frontend-promql/tests/observability/metrics_observability.rs @@ -84,7 +84,6 @@ fn queries(corpus: &str) -> impl Iterator { } #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn benchmark_corpora_are_total_and_keep_their_coverage() { for (name, corpus, ratchet) in CORPORA { let mut total = 0; @@ -151,7 +150,6 @@ fn benchmark_corpora_are_total_and_keep_their_coverage() { /// sum a string). The ranked-row TopK contract now exposes its numeric value /// and retained identity columns (#672). #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn kubernetes_mixin_aggregates_over_topk_compose() { for query in [ r#"sum without (id, metrics_path, name, image, endpoint, job, node) ( topk by (cluster, namespace, pod, container, instance) (1, increase( container_cpu_cfs_throttled_periods_total{container!="", job="cadvisor", } [5m]) ) ) / on (cluster, namespace, pod, container, instance) group_left sum without (id, metrics_path, name, image, endpoint, job, node) ( topk by (cluster, namespace, pod, container, instance) (1, increase( container_cpu_cfs_periods_total{job="cadvisor", } [5m]) ) ) > ( 25 / 100 )"#, diff --git a/crates/frontend-promql/tests/observability/promql_corpus.rs b/crates/frontend-promql/tests/observability/promql_corpus.rs index de255c07e..cd0aab26c 100644 --- a/crates/frontend-promql/tests/observability/promql_corpus.rs +++ b/crates/frontend-promql/tests/observability/promql_corpus.rs @@ -99,7 +99,6 @@ fn bind_tally(corpus: &str, accuracy: AccuracyTarget) -> BindTally { } #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn binding_is_total_over_the_entire_corpus() { let accuracy = AccuracyTarget::max_error(0.01); let docs = bind_tally(DOCS, accuracy.clone()); diff --git a/crates/frontend-promql/tests/univmon_candidates.rs b/crates/frontend-promql/tests/univmon_candidates.rs index fc30cb04c..de94f5e2c 100644 --- a/crates/frontend-promql/tests/univmon_candidates.rs +++ b/crates/frontend-promql/tests/univmon_candidates.rs @@ -31,7 +31,6 @@ fn candidate(query: &str, accuracy: AccuracyTarget) -> Rc { } #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn four_evaluations_size_their_value_frequency_states_and_keep_honest_guarantees() { // Pass 1 sizes UnivMon per readout: the distinct count and L2 get their // own shapes, while the total and entropy keep the baseline shape and so @@ -119,7 +118,6 @@ fn four_evaluations_size_their_value_frequency_states_and_keep_honest_guarantees } #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn uncalibrated_frequency_evaluations_do_not_bypass_accuracy_targets() { // An unmeasured heuristic remains inspectable but is never certified or // automatically selected for a caller-visible bounded-error result. diff --git a/crates/frontend-sql/tests/pearson_corr.rs b/crates/frontend-sql/tests/pearson_corr.rs index 48cb21822..c6f31079a 100644 --- a/crates/frontend-sql/tests/pearson_corr.rs +++ b/crates/frontend-sql/tests/pearson_corr.rs @@ -147,7 +147,6 @@ async fn corr_filter_is_a_measure_filter() { // The plan the planner pipeline selects retains the complete typed query and // compiles to a physical ASAP DAG. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn corr_survives_exact_plan_compilation() { let query = lower("SELECT corr(x, y) AS r FROM a").await; // `a` holds 60,000 rows: the query scans all of them. diff --git a/crates/integration-tests/src/lib.rs b/crates/integration-tests/src/lib.rs index 3e136714a..49534547c 100644 --- a/crates/integration-tests/src/lib.rs +++ b/crates/integration-tests/src/lib.rs @@ -345,7 +345,6 @@ pub mod planning { use asap_logical_asap_optimization::test_support::{ add_local_window_composition, try_populate_pass1, }; - use asap_logical_asap_optimization::TumblingWindowWidthDomain; let initial = asap_logical_asap_optimization::generate_dag_of_logical_memo_groups(&roots); let inventory = generate_per_computation_candidates( &asap_logical_asap_optimization::generate_dag_of_logical_memo_groups(&roots), @@ -353,11 +352,8 @@ pub mod planning { .unwrap(); let mut memo = try_populate_pass1(initial, &inventory).unwrap(); add_local_window_composition(&mut memo, entries); - asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups( - memo, - TumblingWindowWidthDomain::SeedGrids, - ) - .unwrap() + asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups(memo) + .unwrap() } /// One plan of `memo`'s first `queries` queries (`memo::enumeration::build_logical_asap_dags_from_chosen_candidates`): diff --git a/crates/integration-tests/tests/crate_graph.rs b/crates/integration-tests/tests/crate_graph.rs index 4e1877956..2eca8fe81 100644 --- a/crates/integration-tests/tests/crate_graph.rs +++ b/crates/integration-tests/tests/crate_graph.rs @@ -168,7 +168,6 @@ fn crate_graph_violations(metadata: &Value) -> Vec { } #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn workspace_crate_graph_follows_the_target_graph() { let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); let manifest = concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.toml"); diff --git a/crates/integration-tests/tests/filtered_aggregates.rs b/crates/integration-tests/tests/filtered_aggregates.rs index 813d010dc..47dfd4efd 100644 --- a/crates/integration-tests/tests/filtered_aggregates.rs +++ b/crates/integration-tests/tests/filtered_aggregates.rs @@ -128,7 +128,6 @@ async fn exact_plan(sql: &str) -> Rc { /// `COUNT(*) FILTER (WHERE x > 0) GROUP BY g` counts the matching rows per /// group, and a group without one reports 0. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn exact_filtered_count_keeps_groups_without_matches() { let root = exact_plan("SELECT g, COUNT(*) FILTER (WHERE x > 0) AS c FROM events GROUP BY g").await; @@ -138,7 +137,6 @@ async fn exact_filtered_count_keeps_groups_without_matches() { /// `SUM(x) FILTER (WHERE x > 0) GROUP BY g` sums the matching rows; a group /// without one reports NULL, as SQL's SUM over no rows does. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn exact_filtered_sum_is_null_for_groups_without_matches() { let root = exact_plan("SELECT g, SUM(x) FILTER (WHERE x > 0) AS s FROM events GROUP BY g").await; @@ -148,7 +146,6 @@ async fn exact_filtered_sum_is_null_for_groups_without_matches() { /// `COUNT(y)` over a nullable `y` is lowered as a count filtered by /// `y IS NOT NULL`, so it executes the same way. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn exact_count_of_nullable_column_skips_nulls() { let root = exact_plan("SELECT g, COUNT(y) AS c FROM events GROUP BY g").await; assert_eq!(execute(&root), ["a 1", "b 0", "c 1"]); diff --git a/crates/integration-tests/tests/finer_aggregate_reuse.rs b/crates/integration-tests/tests/finer_aggregate_reuse.rs index 50981d30e..ed8a1b06e 100644 --- a/crates/integration-tests/tests/finer_aggregate_reuse.rs +++ b/crates/integration-tests/tests/finer_aggregate_reuse.rs @@ -4,9 +4,8 @@ use asap_devtools::planner_fuzz::{read_seed_file, FuzzSettings, FuzzWorkload, Pl use asap_devtools::planning_stages::run_search_over; use asap_executor::values::Value; use asap_logical_asap_optimization::generate_dag_of_logical_memo_groups; -use asap_logical_asap_optimization::{ - test_support::{generate_per_computation_candidates, try_populate_pass1}, - TumblingWindowWidthDomain, +use asap_logical_asap_optimization::test_support::{ + generate_per_computation_candidates, try_populate_pass1, }; use asap_types::ir::{ASAPOp, NonASAPOp, OperatorNode}; use asap_types::stages::{CandidateForMemoGroup, Memo, OriginOfCandidateForMemoGroup}; @@ -55,11 +54,8 @@ fn expanded(queries: &[String]) -> Memo { ) .unwrap(); let memo = try_populate_pass1(initial, &inventory).unwrap(); - asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups( - memo, - TumblingWindowWidthDomain::SeedGrids, - ) - .unwrap() + asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups(memo) + .unwrap() } /// The aggregate group of query `query` alone and its rollup candidates. @@ -314,10 +310,7 @@ fn cost(queries: &[String]) -> f64 { &entries, &w.data, models, - PlanSearchSettings { - time_limit_ms: Some(30_000), - inspect: false, - }, + PlanSearchSettings::default(), None, ) .unwrap(); @@ -330,7 +323,6 @@ fn cost(queries: &[String]) -> f64 { /// Example B end to end: adding the filtered sum to the finer sums costs /// only its read of the finer result, not a second scan and aggregation. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn filtered_sum_costs_little_beyond_the_finer_sums() { let finer = format!("SELECT city, host, SUM(value) FROM metrics WHERE {WINDOW} GROUP BY city, host"); @@ -350,10 +342,7 @@ fn filtered_sum_costs_little_beyond_the_finer_sums() { /// Example C end to end: the joint plan costs no more than planning each /// query alone, and no more than on main before the state path (0.2285/s). /// The joint optimum is 0.1963/s: q1 keeps the NYC and SF states of a KLL -/// by city built over q0's query-time rows. The search reaches the separate -/// plans' cost because the first round evaluates them combined when the joint -/// search ends above them; reaching 0.1963/s within the budget needs -/// candidate-level cuts for rejected proposals. +/// by city built over q0's query-time rows. #[test] #[ignore = "needs #1095's engine: since #1047 P7 Stage 3 has no per-query seed, and the joint search ends at 0.3321/s, above the separate plans' 0.2285/s"] fn filtered_kll_states_cost_no_more_than_separate_plans() { diff --git a/crates/integration-tests/tests/having_shared_aggregate.rs b/crates/integration-tests/tests/having_shared_aggregate.rs index ff1cc86f6..554e63d38 100644 --- a/crates/integration-tests/tests/having_shared_aggregate.rs +++ b/crates/integration-tests/tests/having_shared_aggregate.rs @@ -4,9 +4,8 @@ use asap_devtools::planner_fuzz::{read_seed_file, FuzzSettings, FuzzWorkload, Planner}; use asap_executor::values::Value; use asap_logical_asap_optimization::generate_dag_of_logical_memo_groups; -use asap_logical_asap_optimization::{ - test_support::{generate_per_computation_candidates, try_populate_pass1}, - TumblingWindowWidthDomain, +use asap_logical_asap_optimization::test_support::{ + generate_per_computation_candidates, try_populate_pass1, }; use asap_types::ir::NonASAPOp; use asap_types::stages::Memo; @@ -53,11 +52,8 @@ fn expanded(queries: &[String]) -> Memo { let dag = generate_dag_of_logical_memo_groups(&roots); let inventory = generate_per_computation_candidates(&dag).unwrap(); let memo = try_populate_pass1(dag, &inventory).unwrap(); - asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups( - memo, - TumblingWindowWidthDomain::SeedGrids, - ) - .unwrap() + asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups(memo) + .unwrap() } fn row(city: &str, host: &str, value: f64) -> Vec { diff --git a/crates/integration-tests/tests/identical_exact_queries.rs b/crates/integration-tests/tests/identical_exact_queries.rs index 17ec2a164..55fb53010 100644 --- a/crates/integration-tests/tests/identical_exact_queries.rs +++ b/crates/integration-tests/tests/identical_exact_queries.rs @@ -21,10 +21,6 @@ fn workload() -> FuzzWorkload { } fn cost(w: &FuzzWorkload) -> f64 { - // Each regression has a wall-clock planning budget. Concurrent solver - // runs in this test binary must not consume one another's CPU budget. - static PLANNING: std::sync::Mutex<()> = std::sync::Mutex::new(()); - let _planning = PLANNING.lock().unwrap(); let roots = Planner::new(FuzzSettings::default()).lower(w).unwrap(); let planning = w.planning_workload(); let entries: Vec<_> = planning.query_workload.entries().collect(); @@ -38,10 +34,7 @@ fn cost(w: &FuzzWorkload) -> f64 { &entries, &w.data, models, - PlanSearchSettings { - time_limit_ms: Some(30_000), - inspect: false, - }, + PlanSearchSettings::default(), None, ) .unwrap(); @@ -53,7 +46,6 @@ fn cost(w: &FuzzWorkload) -> f64 { /// #922: two identical exact percentile queries pay for their result once. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn identical_exact_percentiles_share_work() { let related = workload(); let mut base = related.clone(); @@ -68,7 +60,6 @@ fn identical_exact_percentiles_share_work() { /// Different ranks share the source window but still compute their own values. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn different_exact_percentiles_share_the_input_window() { let mut related = workload(); related.queries[1].entry.query.0 = related.queries[1].entry.query.0.replace("0.99", "0.5"); diff --git a/crates/integration-tests/tests/independent_plan_seed.rs b/crates/integration-tests/tests/independent_plan_seed.rs index 6785d3532..7dcee170e 100644 --- a/crates/integration-tests/tests/independent_plan_seed.rs +++ b/crates/integration-tests/tests/independent_plan_seed.rs @@ -50,10 +50,7 @@ fn cost(queries: &[String]) -> f64 { &entries, &w.data, models, - PlanSearchSettings { - time_limit_ms: Some(30_000), - inspect: false, - }, + PlanSearchSettings::default(), None, ) .unwrap(); @@ -82,7 +79,6 @@ fn assert_no_costlier_than_separate(queries: &[String]) -> (f64, f64) { /// chain; together the search used to stop at a query-time plan about twice /// as costly. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn per_city_percentiles_cost_no_more_than_separate_plans() { assert_no_costlier_than_separate(&[ format!("SELECT {P90} FROM metrics WHERE city = 'NYC' AND {W5}"), @@ -95,7 +91,6 @@ fn per_city_percentiles_cost_no_more_than_separate_plans() { /// ingestion-time state; together the search used to stop at a query-time /// plan almost three times as costly. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn percentile_and_max_over_one_scan_cost_no_more_than_separate_plans() { assert_no_costlier_than_separate(&[ format!("SELECT {P90} FROM metrics WHERE {W5}"), @@ -105,7 +100,6 @@ fn percentile_and_max_over_one_scan_cost_no_more_than_separate_plans() { /// The same with a grouped count over the shared scan. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn percentile_and_grouped_count_over_one_scan_cost_no_more_than_separate_plans() { assert_no_costlier_than_separate(&[ format!("SELECT {P90} FROM metrics WHERE {W5}"), diff --git a/crates/integration-tests/tests/joint_search_equivalence.rs b/crates/integration-tests/tests/joint_search_equivalence.rs index 42cbc9c2b..edd0dd2a3 100644 --- a/crates/integration-tests/tests/joint_search_equivalence.rs +++ b/crates/integration-tests/tests/joint_search_equivalence.rs @@ -74,17 +74,13 @@ fn run( data: &DataWorkload, models: PlanningModels<'_>, _exhaustive: bool, - budget_ms: u64, ) -> PlanningRun { run_planning( roots.to_vec(), entries, data, models, - PlanSearchSettings { - time_limit_ms: Some(budget_ms), - inspect: false, - }, + PlanSearchSettings::default(), ) .unwrap() } @@ -130,17 +126,13 @@ fn run_workload( .with_cost_inputs(settings) .with_deployment_capabilities(deployment), false, - 10_000, ); assert_valid(&run, entries.len()); run } -// Bounded runs can stop at different progressive tumbling-window-width domains. Compare -// validated output semantics here, not inventory sizes from different catalogs. // Fixed-catalog terminal/producer scaling is tested in candidates::scopes. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn repeated_queries_preserve_all_outputs_and_exact_guarantees() { let settings = CostInputs::default(); let deployment = DeploymentCapabilities::default(); @@ -164,7 +156,6 @@ fn repeated_queries_preserve_all_outputs_and_exact_guarantees() { // Different windows over the same source remain legal with retained raw data // disabled; their selected ingestion state must cover both output contracts. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn different_windows_and_raw_retention_policies_produce_valid_plans() { let workload = workload_of( &[ @@ -187,7 +178,6 @@ fn different_windows_and_raw_retention_policies_produce_valid_plans() { // Factoring a fused TopK candidate must not absorb the separately requested sum. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn fused_top_k_preserves_its_independently_requested_inner_sum() { let workload = workload_of( &[ @@ -205,7 +195,6 @@ fn fused_top_k_preserves_its_independently_requested_inner_sum() { // Grouped rollups coexist with temporal quantiles without merging distinct outputs. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn sketch_rollup_coexists_with_other_quantile_outputs() { let workload = workload_of( &[ @@ -224,7 +213,6 @@ fn sketch_rollup_coexists_with_other_quantile_outputs() { // Local sketch contracts do not replace composed output accuracy validation. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn composed_quantile_and_sum_outputs_are_validated_together() { let workload = workload_of( &[ @@ -243,7 +231,6 @@ fn composed_quantile_and_sum_outputs_are_validated_together() { // A custom accuracy policy still participates in final evaluation and prevents // builtin-only permanent pruning assumptions from being applied to its outputs. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn custom_accuracy_policy_is_respected() { struct CustomAccuracy(std::cell::Cell); impl AccuracyModel for CustomAccuracy { @@ -262,7 +249,6 @@ fn custom_accuracy_policy_is_respected() { workload.data_workload.as_ref().unwrap(), PlanningModels::builtin().with_accuracy(&policy), false, - 10_000, ); assert_valid(&result, 1); assert!( @@ -271,29 +257,8 @@ fn custom_accuracy_policy_is_respected() { ); } -// Exhausting the entire budget before planning yields an explicit diagnostic, -// never an unvalidated plan advertised as a warm-start incumbent. -#[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] -fn zero_budget_returns_no_unvalidated_plan() { - let workload = workload_of(&[("m", 60_000)], AccuracyTarget::exact()); - let roots = lower_promql_roots(&workload).unwrap(); - let entries: Vec<_> = workload.query_workload.entries().collect(); - let run = run( - &roots, - &entries, - workload.data_workload.as_ref().unwrap(), - PlanningModels::builtin(), - false, - 0, - ); - assert!(run.selected_physical_plan.is_none()); - assert!(run.selection.unwrap_err().to_string().contains("budget")); -} - // The SQL frontend keeps repeated and different-window COUNT DISTINCT outputs. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn repeated_sql_queries_preserve_separate_output_references() { let (mut workload, catalog) = planner_layering_common::example2(); let entries = workload.query_workload.repeating_queries.as_mut().unwrap(); @@ -309,7 +274,6 @@ fn repeated_sql_queries_preserve_separate_output_references() { workload.data_workload.as_ref().unwrap(), PlanningModels::builtin(), false, - 10_000, ); assert_valid(&run, 3); } @@ -317,7 +281,6 @@ fn repeated_sql_queries_preserve_separate_output_references() { // Request work on literal outputs still consumes CPU and latency budgets even // when there is no operator node to which a fee could be attributed. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn rootless_request_overhead_respects_capacity_and_latency() { let mut workload = workload_of(&[("1", 1)], AccuracyTarget::exact()); let roots = lower_promql_roots(&workload).unwrap(); @@ -343,7 +306,6 @@ fn rootless_request_overhead_respects_capacity_and_latency() { workload.data_workload.as_ref().unwrap(), PlanningModels::builtin().with_cost_inputs(&settings), false, - 10_000, ); assert!(result.selection.is_err()); assert!(result.selected_physical_plan.is_none()); @@ -353,7 +315,6 @@ fn rootless_request_overhead_respects_capacity_and_latency() { // Unsupported exact operators cannot become accepted merely because no summary // candidate exists; failure must leave the exported physical plan absent. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn unsupported_exact_operation_never_returns_an_unvalidated_plan() { let workload = workload_of( &[("stddev_over_time(m[5m])", 60_000)], @@ -367,7 +328,6 @@ fn unsupported_exact_operation_never_returns_an_unvalidated_plan() { workload.data_workload.as_ref().unwrap(), PlanningModels::builtin(), false, - 10_000, ); assert!(result.selection.is_err()); assert!(result.selected_physical_plan.is_none()); @@ -376,7 +336,6 @@ fn unsupported_exact_operation_never_returns_an_unvalidated_plan() { // Deterministic small mixed workloads exercise exact temporal operators, counter // semantics, different sources and windows without claiming timeout optimality. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn seeded_small_mixed_workloads_return_validated_plans() { let queries = [ "sum_over_time(m[1m])", diff --git a/crates/integration-tests/tests/materialization_e2e.rs b/crates/integration-tests/tests/materialization_e2e.rs index c28f4d4bf..1751d3e95 100644 --- a/crates/integration-tests/tests/materialization_e2e.rs +++ b/crates/integration-tests/tests/materialization_e2e.rs @@ -127,9 +127,7 @@ fn execute( evaluation_time_ms: i64, ) -> (Vec, Vec) { let mut dag = dag.clone(); - let views = dag - .set_retention(&HashMap::new(), &HashMap::new(), None) - .unwrap(); + let views = dag.set_retention(&HashMap::new(), &HashMap::new()).unwrap(); let raw = |dag: &PhysicalASAPDAG| { physical_common::raw_data_store(dag, |source, schema| { let Source::TimeSeries { metric } = source else { @@ -292,7 +290,6 @@ const INGESTION_WINDOW: Scope = Scope::Ingestion { /// selectors keeps only label sets present on both sides (none when /// disjoint), at either placement, and never becomes aligned maintenance. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn arithmetic_over_different_selectors_matches_prometheus() { // The selected root reads the outer `sum`'s exact state through its // finalize. diff --git a/crates/integration-tests/tests/materialized_views.rs b/crates/integration-tests/tests/materialized_views.rs index 02a762b41..c6fad2f29 100644 --- a/crates/integration-tests/tests/materialized_views.rs +++ b/crates/integration-tests/tests/materialized_views.rs @@ -85,7 +85,6 @@ fn stage3_raw_lookback_ms(dag: &PhysicalASAPDAG) -> Vec<(Source, Option)> { /// same plan. With no raw data kept, both run from views refreshed at /// ingestion and no source is read at query time. #[tokio::test(flavor = "multi_thread")] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn raw_data_retention_is_the_lookback_stage3_prices() { let workload = repeating(&["rate(m[5m])", "rate(m[5m] offset 1h)"]); let input = || UserInput::new(&workload, FrontendInput::Promql, PlanningModels::builtin()); diff --git a/crates/integration-tests/tests/memo_enumeration.rs b/crates/integration-tests/tests/memo_enumeration.rs index 9ae2cdf63..83df2cf2d 100644 --- a/crates/integration-tests/tests/memo_enumeration.rs +++ b/crates/integration-tests/tests/memo_enumeration.rs @@ -9,15 +9,6 @@ //! the production run's `PlanningRun::per_computation_candidates` (after //! Pass 1) and `PlanningRun::logical_asap_candidates` (after Pass 2). //! -//! **Which round's memo.** The default search plans in rounds of growing -//! tumbling-window-width domains and records the memo of the round whose plan it -//! selected, so its last step depends on the time budget (the seed-grid -//! round when a debug build runs out of it; round 5 of the release run that -//! made the doc's document, 354 groups and 1738 candidates). The ratio test -//! runs one round over the complete tumbling-window-width domain (a search with fewer -//! than 4 evaluations has no progressive rounds), so its last step is the -//! same on every machine. Stage 0 and Pass 1 are the same in every round. -//! //! **The memo and the whole-workload plans.** After Pass 1 the memo's //! choices whose candidates are all sized for the whole accuracy budget //! (`accuracy_budget_parts` 1) and the plans `compose_logical_candidate` builds from @@ -92,20 +83,13 @@ fn production_roots(workload: &PlanningWorkload) -> Vec { } /// The production run of Stages 1 to 3 over `workload`. -fn production_run(workload: &PlanningWorkload, search: PlanSearchSettings) -> PlanningRun { - production_run_with(workload, search, PlanningModels::builtin()) +fn production_run(workload: &PlanningWorkload) -> PlanningRun { + production_run_with(workload, PlanningModels::builtin()) } /// [`production_run`] with `models`, keeping every stage's memo. -fn production_run_with( - workload: &PlanningWorkload, - search: PlanSearchSettings, - models: PlanningModels<'_>, -) -> PlanningRun { - let search = PlanSearchSettings { - inspect: true, - ..search - }; +fn production_run_with(workload: &PlanningWorkload, models: PlanningModels<'_>) -> PlanningRun { + let search = PlanSearchSettings { inspect: true }; let entries: Vec<_> = workload.query_workload.entries().collect(); let data = workload.data_workload.clone().unwrap_or_default(); let roots = production_roots(workload); @@ -114,7 +98,7 @@ fn production_run_with( /// A deployment model that may accept an output whatever its guarantee, so /// Pass 1 prunes no summary for its target, and accepts none: Stage 3 -/// validates no plan, and the run records its last round's memo. +/// validates no plan, and the run still records its memo. struct AcceptsNoOutput; impl asap_accuracy::AccuracyModel for AcceptsNoOutput { @@ -127,22 +111,11 @@ impl asap_accuracy::AccuracyModel for AcceptsNoOutput { } } -/// Rounds up to the complete tumbling-window-width domain, with a time limit a slow -/// machine's Pass 2 does not reach. Stage 3 may use the whole budget: when -/// it validates no plan, it searches until the deadline; the memo steps are -/// recorded either way. -fn complete_domain_round() -> PlanSearchSettings { - PlanSearchSettings { - time_limit_ms: Some(90_000), - ..PlanSearchSettings::default() - } -} - -/// The memo after Pass 2: the selected (or last) round's. +/// The memo after Pass 2. fn pass2_memo(run: &PlanningRun) -> &Memo { &run.logical_asap_candidates .as_ref() - .expect("a planning round recorded its memo within the time budget") + .expect("the run recorded its Pass 2 memo") .memo } @@ -340,17 +313,16 @@ fn assert_pass1_memo_and_whole_workload_plans( /// The example's memo after each production step, and the `accuracy_budget_parts` /// of the state-configuration rule's rewrites (one test: one planning run). #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn ratio_of_quantiles_production_memo() { // With the built-in model, Pass 1 prunes every summary of the quantile // groups: no query has a guarantee through `/` and `+` over a KLL rank // error or a DDSketch relative one. The memo below is planned with a // model that prunes none and accepts no output, so its summaries stay - // and its last round, over the complete tumbling-window-width domain, is recorded. - let enforced = production_run(&ratio_of_quantiles(), PlanSearchSettings::default()); + // and its Pass 2 memo is recorded. + let enforced = production_run(&ratio_of_quantiles()); assert_eq!(candidate_count(pass1_memo(&enforced)), 7); let models = PlanningModels::builtin().with_accuracy(&AcceptsNoOutput); - let run = production_run_with(&ratio_of_quantiles(), complete_domain_round(), models); + let run = production_run_with(&ratio_of_quantiles(), models); // Stage 0 (Pass 1's groups): series identity makes the two // `quantile 0.5` one group. // Pass 1: the two quantile groups have 5 candidates each, 3 for the @@ -430,10 +402,8 @@ fn ratio_of_quantiles_production_memo() { /// On the three-query workload, the same relation: the memo's 3 × 3 /// whole-budget DAGs and the inventory's 3 × 3 assignments share the 2 × 2 /// that take no KLL (the memo's is sized for the whole result, the -/// inventory's per value). Pass 1 is the same in every round, so the -/// default search will do. +/// inventory's per value), with the default search. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn shared_quantiles_pass1_memo_and_whole_workload_plans() { let workload = with_queries( &[ @@ -443,7 +413,7 @@ fn shared_quantiles_pass1_memo_and_whole_workload_plans() { ], 60_000, ); - let run = production_run(&workload, PlanSearchSettings::default()); + let run = production_run(&workload); assert_eq!( assert_pass1_memo_and_whole_workload_plans( &run, diff --git a/crates/integration-tests/tests/operator_design_examples.rs b/crates/integration-tests/tests/operator_design_examples.rs index 015529018..88b19291d 100644 --- a/crates/integration-tests/tests/operator_design_examples.rs +++ b/crates/integration-tests/tests/operator_design_examples.rs @@ -244,7 +244,6 @@ async fn sql_window_and_filtered_aggregate_types() { /// finalized sum above it) is the cheapest plan, so both roots read one /// state. No replacement dag is constructed by the test. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn batch_planning_replaces_summary_operators() { use asap_executor::values::Value; use asap_planner::PlanningModels; diff --git a/crates/integration-tests/tests/planner_layering_common/mod.rs b/crates/integration-tests/tests/planner_layering_common/mod.rs index 45e59b423..6227dfea7 100644 --- a/crates/integration-tests/tests/planner_layering_common/mod.rs +++ b/crates/integration-tests/tests/planner_layering_common/mod.rs @@ -289,7 +289,7 @@ fn run_stages_over( ) -> Run { let entries: Vec<_> = workload.query_workload.entries().collect(); let data = workload.data_workload.clone().unwrap_or_default(); - // The facade's run, with the reference pricer over the selected round's + // The facade's run, with the reference pricer over the Stage 2 // memo choices: the whole-plan costs and rejections these tests read. let run = run_planning_stages_over(roots, &entries, &data, None, models) .expect("planning stages run"); diff --git a/crates/integration-tests/tests/planner_layering_example1.rs b/crates/integration-tests/tests/planner_layering_example1.rs index bc12132e0..ca983c655 100644 --- a/crates/integration-tests/tests/planner_layering_example1.rs +++ b/crates/integration-tests/tests/planner_layering_example1.rs @@ -651,7 +651,7 @@ fn compile_in_runtime(p: &Physical) -> Result<(), String> { use asap_executor::physical_planner::compile; let mut dag = p.dag().clone(); let views = dag - .set_retention(&Default::default(), &Default::default(), None) + .set_retention(&Default::default(), &Default::default()) .map_err(|e| format!("{}: {e}", p.id))?; let roots: Vec = dag.roots.iter().map(|&id| id as u64).collect(); compile(&dag, &views, &roots) diff --git a/crates/integration-tests/tests/planner_layering_example2.rs b/crates/integration-tests/tests/planner_layering_example2.rs index a71a9c7e6..9c09da9fd 100644 --- a/crates/integration-tests/tests/planner_layering_example2.rs +++ b/crates/integration-tests/tests/planner_layering_example2.rs @@ -251,7 +251,6 @@ fn rule_fixture_computations( } #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn production_memo_selects_a_validated_example2_plan() { let workload = standin_workload(); let run = run_stages(&workload, lower_promql(&workload)); diff --git a/crates/integration-tests/tests/production_rollup_workloads.rs b/crates/integration-tests/tests/production_rollup_workloads.rs index 1dd0e5cfa..fa20b73b6 100644 --- a/crates/integration-tests/tests/production_rollup_workloads.rs +++ b/crates/integration-tests/tests/production_rollup_workloads.rs @@ -79,10 +79,7 @@ fn production( entries, data, PlanningModels::builtin().with_cost_inputs(&settings), - PlanSearchSettings { - time_limit_ms: Some(30_000), - inspect: false, - }, + PlanSearchSettings::default(), ) .unwrap() } @@ -165,7 +162,6 @@ async fn sql(queries: &[String], accuracy: AccuracyTarget) -> PlanningRun { } #[tokio::test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] async fn production_three_sql_groupings_share_kll() { let accuracy = AccuracyTarget::max_error(0.05) .failure_probability(0.05) @@ -176,7 +172,6 @@ async fn production_three_sql_groupings_share_kll() { } #[tokio::test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] async fn production_sql_distinct_region_service_rollup() { let accuracy = AccuracyTarget::max_error(0.05) .failure_probability(0.05) @@ -187,7 +182,6 @@ async fn production_sql_distinct_region_service_rollup() { } #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] // The earlier controlled fixture omitted raw retention. This production // workload explicitly holds one current sample for each of its 10,000 series. fn production_promql_compares_rollup_with_exact() { @@ -280,7 +274,6 @@ fn independent_kll_cost( // Exercise multi-input operators through the production route under more than // three heterogeneous query schedules, not just the local pricing kernel. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn production_binary_queries_use_joint_prices_with_mixed_schedules() { use asap_types::workload::{DemandEstimate, Rate}; let accuracy = AccuracyTarget::exact(); diff --git a/crates/integration-tests/tests/range_evaluation.rs b/crates/integration-tests/tests/range_evaluation.rs index 2bc02a751..2f97eeb72 100644 --- a/crates/integration-tests/tests/range_evaluation.rs +++ b/crates/integration-tests/tests/range_evaluation.rs @@ -263,7 +263,6 @@ fn explicit_tumbling_window_candidates_follow_the_evaluation_step_grid() { /// 3,900,000 ms, so the next refresh's earliest step finds its tumbling windows. /// Without the range it is the window, 300,000 ms. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn stage3_plan_selection_reports_the_retention_of_kept_tumbling_windows() { let deployment = keeping_query_time_results(); let query = "max_over_time(m[5m])"; @@ -340,7 +339,6 @@ fn no_range_evaluation_leaves_every_price_unchanged() { /// of tumbling windows (460.0410), and B1 472.2760: the kept tumbling windows are selected, and without /// query-time retention, the ingestion-time tumbling windows. #[test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] fn stage3_plan_selection_prefers_kept_tumbling_windows_once_the_range_is_declared() { let query = "max_over_time(m[5m])"; let deployment = keeping_query_time_results(); diff --git a/crates/integration-tests/tests/rollup_controlled.rs b/crates/integration-tests/tests/rollup_controlled.rs index e4a5dc9b8..3d955b45e 100644 --- a/crates/integration-tests/tests/rollup_controlled.rs +++ b/crates/integration-tests/tests/rollup_controlled.rs @@ -164,7 +164,6 @@ fn controlled_distinct_rollup_prices_the_original_workload() { // The production entrypoint must find a sketch rollup under the viewer budget. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn production_distinct_rollup_finds_shared_kmv() { let (workload, roots) = fixture(); let entries: Vec<_> = workload.query_workload.entries().collect(); @@ -178,10 +177,7 @@ fn production_distinct_rollup_finds_shared_kmv() { &entries, data, PlanningModels::builtin().with_cost_inputs(&settings), - asap_planner::PlanSearchSettings { - time_limit_ms: Some(30_000), - inspect: false, - }, + asap_planner::PlanSearchSettings::default(), ) .unwrap(); let selection = run diff --git a/crates/integration-tests/tests/runtime_handoff.rs b/crates/integration-tests/tests/runtime_handoff.rs index 349a3b07f..cadfe8788 100644 --- a/crates/integration-tests/tests/runtime_handoff.rs +++ b/crates/integration-tests/tests/runtime_handoff.rs @@ -73,7 +73,6 @@ fn materialized_count(dag: &PhysicalASAPDAG) -> usize { /// export carries that selection rather than resetting every node to /// query time. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn export_carries_the_selected_materialization() { let capabilities = without_raw_data(); let output = plan(&example4b(), &capabilities).await; @@ -127,7 +126,6 @@ fn tumbling_window_chain_count(dag: &PhysicalASAPDAG) -> usize { /// the executor compiles it. The stage export keeps the chain for costing /// and display. Its one query is answered by the DAG's one root. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn installable_plan_has_its_tumbling_window_chains_expanded() { let workload = example4b(); let (installed, report) = installable_with_report(&workload).await; @@ -195,7 +193,6 @@ fn summary_builds(dag: &PhysicalASAPDAG) -> Vec { /// with the same parameters, every node at query time and nothing materialized, /// with the same queries bound to its roots. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn reference_plan_runs_the_installable_plan_at_query_time() { let plan = installable(&example4b()).await; let reference = query_time_reference(&plan).expect("re-times"); @@ -241,7 +238,6 @@ fn repeating(queries: &[&str]) -> PlanningWorkload { /// p99 and p50 over the same 60 minutes share one chain of tumbling windows: the /// installable plan materializes one shared set of tumbling windows. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn two_queries_on_one_chain_store_its_tumbling_windows_once() { let plan = installable(&repeating(&[ "quantile_over_time(0.99, latency_ms[60m])", @@ -285,7 +281,7 @@ async fn installable_at_query_time(query: &str) -> Result BTreeSet FuzzWorkload { } fn cost(w: &FuzzWorkload) -> f64 { - // Each regression has a wall-clock planning budget. Concurrent solver - // runs in this test binary must not consume one another's CPU budget. - static PLANNING: std::sync::Mutex<()> = std::sync::Mutex::new(()); - let _planning = PLANNING.lock().unwrap(); let roots = Planner::new(FuzzSettings::default()).lower(w).unwrap(); let planning = w.planning_workload(); let entries: Vec<_> = planning.query_workload.entries().collect(); @@ -51,10 +47,7 @@ fn cost(w: &FuzzWorkload) -> f64 { &entries, &w.data, models, - PlanSearchSettings { - time_limit_ms: Some(30_000), - inspect: false, - }, + PlanSearchSettings::default(), None, ) .unwrap(); @@ -83,7 +76,6 @@ fn exact_coarse_sum_reuses_fine_sum() { /// #924: the 1m sum reads the newest tumbling window of the existing 2m chain. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn nested_exact_sum_reuses_newest_tumbling_windows() { let related = workload(924); let mut base = related.clone(); @@ -98,7 +90,6 @@ fn nested_exact_sum_reuses_newest_tumbling_windows() { /// #925: the slower copy reads the same maintained sum. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn slower_repeat_reuses_faster_sum() { let related = workload(925); let mut base = related.clone(); @@ -113,7 +104,6 @@ fn slower_repeat_reuses_faster_sum() { /// #902: the filtered reader avoids its independent scan and aggregation. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn grouped_filtered_quantile_plans() { assert!( cost(&workload(902)) < 0.25, @@ -205,9 +195,8 @@ fn sql_count_topk_has_a_fused_raw_row_heap() { /// prevent an exact sum from rolling up the finer sum's output. #[test] fn exact_rollup_catalog_crosses_identical_per_series_average() { - use asap_logical_asap_optimization::{ - test_support::{generate_per_computation_candidates, try_populate_pass1}, - TumblingWindowWidthDomain, + use asap_logical_asap_optimization::test_support::{ + generate_per_computation_candidates, try_populate_pass1, }; use asap_types::stages::OriginOfCandidateForMemoGroup; let roots: Vec<_> = Planner::new(FuzzSettings::default()) @@ -222,11 +211,8 @@ fn exact_rollup_catalog_crosses_identical_per_series_average() { .unwrap(); let memo = try_populate_pass1(initial, &inventory).unwrap(); let expanded = - asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups( - memo, - TumblingWindowWidthDomain::SeedGrids, - ) - .unwrap(); + asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups(memo) + .unwrap(); let owner = expanded.query_roots[1].as_operator().unwrap().0 as usize; assert!( expanded.candidates[owner] @@ -243,9 +229,8 @@ use crate::physical_common; #[test] fn filtered_group_preserves_sql_empty_and_null_semantics() { use asap_executor::values::Value; - use asap_logical_asap_optimization::{ - test_support::{generate_per_computation_candidates, try_populate_pass1}, - TumblingWindowWidthDomain, + use asap_logical_asap_optimization::test_support::{ + generate_per_computation_candidates, try_populate_pass1, }; use asap_types::ir::{operator::AggIntent, NonASAPOp}; use asap_types::stages::OriginOfCandidateForMemoGroup; @@ -261,11 +246,8 @@ fn filtered_group_preserves_sql_empty_and_null_semantics() { .unwrap(); let memo = try_populate_pass1(initial, &inventory).unwrap(); let expanded = - asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups( - memo, - TumblingWindowWidthDomain::SeedGrids, - ) - .unwrap(); + asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups(memo) + .unwrap(); let (owner, candidate) = expanded.candidates.iter().enumerate().flat_map(|(owner, candidates)| candidates.iter().map(move |c| (owner,c))) .find(|(_, c)| c.origin == OriginOfCandidateForMemoGroup::Rollup && matches!(c.root_node.non_asap(), Some(NonASAPOp::Aggregate { measures, child, .. }) if matches!(measures.as_slice(), [AggIntent::Min { .. }]) && matches!(child.non_asap(), Some(NonASAPOp::Filter { .. })))) @@ -401,9 +383,8 @@ fn filtered_group_preserves_sql_empty_and_null_semantics() { #[test] fn sql_count_rollup_executes_with_integer_results() { use asap_executor::values::Value; - use asap_logical_asap_optimization::{ - test_support::{generate_per_computation_candidates, try_populate_pass1}, - TumblingWindowWidthDomain, + use asap_logical_asap_optimization::test_support::{ + generate_per_computation_candidates, try_populate_pass1, }; use asap_types::ir::ASAPOp; use asap_types::stages::OriginOfCandidateForMemoGroup; @@ -430,7 +411,6 @@ fn sql_count_rollup_executes_with_integer_results() { let expanded = asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups( memo, - TumblingWindowWidthDomain::SeedGrids, ) .unwrap(); let (owner, candidate) = expanded @@ -489,7 +469,6 @@ fn sql_count_rollup_executes_with_integer_results() { /// #922: duplicate exact TopK readers share their result and inner window work. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn duplicate_exact_topk_shares_work() { let mut w = workload(924); for query in &mut w.queries { @@ -504,7 +483,6 @@ fn duplicate_exact_topk_shares_work() { /// #924/#925 SQL variants: nested windows and slower readers reuse one sum. #[test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] fn sql_sum_shares_nested_windows_and_slower_readers() { let mut w = workload(922); w.queries.truncate(1); @@ -531,9 +509,8 @@ fn sql_sum_shares_nested_windows_and_slower_readers() { /// #923: sketches over an inner range function expose the same finer rollup. #[test] fn sketch_rollup_crosses_inner_range_function() { - use asap_logical_asap_optimization::{ - test_support::{generate_per_computation_candidates, try_populate_pass1}, - TumblingWindowWidthDomain, + use asap_logical_asap_optimization::test_support::{ + generate_per_computation_candidates, try_populate_pass1, }; use asap_types::stages::OriginOfCandidateForMemoGroup; let mut w = workload(923); @@ -554,11 +531,8 @@ fn sketch_rollup_crosses_inner_range_function() { .unwrap(); let memo = try_populate_pass1(initial, &inventory).unwrap(); let expanded = - asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups( - memo, - TumblingWindowWidthDomain::SeedGrids, - ) - .unwrap(); + asap_logical_asap_optimization::test_support::sharing_rules_and_shared_state_groups(memo) + .unwrap(); let owner = expanded.query_roots[1].as_operator().unwrap().0 as usize; assert!(expanded.candidates[owner] .iter() diff --git a/crates/integration-tests/tests/sql_to_physical.rs b/crates/integration-tests/tests/sql_to_physical.rs index 3563e464f..258821b74 100644 --- a/crates/integration-tests/tests/sql_to_physical.rs +++ b/crates/integration-tests/tests/sql_to_physical.rs @@ -19,7 +19,6 @@ use std::sync::Arc; /// SQL filtering and grouped aggregation survive logical/physical lowering; /// rebinding the compiled DAG runs against new data rather than cached results. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn sql_filter_grouped_sum_executes_and_rebinds() { let catalog = SqlCatalog::new().with_table( "metrics", diff --git a/crates/integration-tests/tests/sql_trailing_window.rs b/crates/integration-tests/tests/sql_trailing_window.rs index 9c3d57ddf..81a229329 100644 --- a/crates/integration-tests/tests/sql_trailing_window.rs +++ b/crates/integration-tests/tests/sql_trailing_window.rs @@ -164,7 +164,6 @@ async fn run(deployment: &DeploymentCapabilities) -> Run { /// Pass 2 offers each query's distinct-count sketch in tumbling windows, and /// Stage 2 maintains a tumbling-window chain at ingestion time, as for `m[5m]`. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn sql_windows_get_tumbling_windows_and_ingestion_time_builds() { let run = run(&DeploymentCapabilities::default()).await; for (query, window_ms) in WINDOWS_MS.iter().enumerate() { diff --git a/crates/logical-asap-optimization/src/lib.rs b/crates/logical-asap-optimization/src/lib.rs index 0b80579b1..bee42d27d 100644 --- a/crates/logical-asap-optimization/src/lib.rs +++ b/crates/logical-asap-optimization/src/lib.rs @@ -87,15 +87,9 @@ pub use pass1::{ generate_per_computation_logical_memo_candidates, PerComputationCandidateError, PerComputationLogicalMemoCandidatesError, }; -// Pass 2: its entry, the state it keeps between tumbling-window-width rounds, and the -// tumbling-window-width domain of a round. -pub use pass2::logical_memo_candidates::{ - generate_logical_asap_memo_candidates, LogicalASAPMemoCandidatesError, Pass2RoundState, -}; +// Pass 2: its entry and its error. +pub use pass2::logical_memo_candidates::generate_logical_asap_memo_candidates; pub use pass2::memo::candidates::CandidatePopulationError; -pub use pass2::memo::shared_configurations::{ - TumblingWindowWidthDomain, TumblingWindowWidthFrontier, -}; pub use pass2::memo::state_groups::Pass2RuleError; // The CandidateLogicalASAPDAGs a memo stands for (developer tools' // reference pricing). diff --git a/crates/logical-asap-optimization/src/pass1/logical_memo_candidates.rs b/crates/logical-asap-optimization/src/pass1/logical_memo_candidates.rs index 7af6eb03d..f045fa778 100644 --- a/crates/logical-asap-optimization/src/pass1/logical_memo_candidates.rs +++ b/crates/logical-asap-optimization/src/pass1/logical_memo_candidates.rs @@ -3,7 +3,6 @@ //! ([`PerComputationLogicalMemoCandidates`]), which Pass 2 extends. use std::rc::Rc; -use std::time::Instant; use asap_types::ir::properties::AccuracyTarget; use asap_types::ir::{QueryRoot, SchemaDerivationError}; @@ -19,11 +18,9 @@ use crate::pass1::per_computation_candidates::{ generate_per_computation_candidates, PerComputationCandidateError, }; use crate::pass2::memo::candidates::CandidatePopulationError; -use crate::pass2::memo::check_generation_deadline; use crate::pass2::memo::groups::try_populate_pass1; use asap_types::stages::{ - CandidateGenerationTimeout, DagOfLogicalMemoGroups, Memo, PerComputationLogicalMemoCandidates, - PrunedCandidate, + DagOfLogicalMemoGroups, Memo, PerComputationLogicalMemoCandidates, PrunedCandidate, }; /// Why [`generate_per_computation_logical_memo_candidates`] failed. @@ -36,9 +33,6 @@ pub enum PerComputationLogicalMemoCandidatesError { /// The targets do not belong to the DAG of logical memo groups. #[error(transparent)] Population(#[from] CandidatePopulationError), - /// The deadline passed while sizing the candidates. - #[error(transparent)] - Timeout(#[from] CandidateGenerationTimeout), /// A candidate's summaries sized for its readers' failure probability /// did not rebuild. #[error(transparent)] @@ -57,9 +51,7 @@ pub enum PerComputationLogicalMemoCandidatesError { /// candidates, each with its summaries sized for its readers' failure /// probabilities (`size_for_failure_probability`): one sized candidate /// per accuracy-budget split. Every pruned way to compute a measure is in -/// the output's `pruned_candidates`. Stops with -/// [`PerComputationLogicalMemoCandidatesError::Timeout`] once `deadline` -/// passes while sizing. +/// the output's `pruned_candidates`. pub fn generate_per_computation_logical_memo_candidates( dag_of_logical_memo_groups: DagOfLogicalMemoGroups, accuracy_targets: &[AccuracyTarget], @@ -67,7 +59,6 @@ pub fn generate_per_computation_logical_memo_candidates( evidence: &dyn AccuracyEvidenceProvider, data: &DataWorkload, supported_summaries: Option<&[SupportedSummary]>, - deadline: Option, ) -> Result { let mut per_computation = generate_per_computation_candidates(&dag_of_logical_memo_groups)? .without_unsupported_summaries(supported_summaries); @@ -76,13 +67,7 @@ pub fn generate_per_computation_logical_memo_candidates( per_computation.without_summaries_no_query_can_use(accuracy_targets, data, accuracy); } let mut memo = try_populate_pass1(dag_of_logical_memo_groups, &per_computation)?; - size_for_failure_probability( - &mut memo, - &per_computation.roots, - accuracy_targets, - data, - deadline, - )?; + size_for_failure_probability(&mut memo, &per_computation.roots, accuracy_targets, data)?; let pruned_candidates = per_computation .pruned_ways_to_compute .into_iter() @@ -115,11 +100,9 @@ fn size_for_failure_probability( roots: &[QueryRoot], accuracy_targets: &[AccuracyTarget], data: &DataWorkload, - deadline: Option, ) -> Result<(), PerComputationLogicalMemoCandidatesError> { for (group, candidates) in memo.groups.iter().zip(&mut memo.candidates) { for candidate in candidates.iter_mut() { - check_generation_deadline(deadline)?; let Some(failure_probability) = group .reached_by_queries() .iter() @@ -152,7 +135,6 @@ fn size_for_failure_probability( #[cfg(test)] mod tests { use super::*; - use crate::pass2::memo::shared_configurations::TumblingWindowWidthDomain; use crate::stage0::generate_dag_of_logical_memo_groups; use crate::test_support::{ lower_promql, shared_summary_state_groups, sharing_rules_and_shared_state_groups, @@ -201,7 +183,6 @@ mod tests { &NoAccuracyEvidence, data, None, - None, ) .unwrap() .memo @@ -313,9 +294,7 @@ mod tests { ], &data(Some(10_000)), ); - let memo = - sharing_rules_and_shared_state_groups(memo, TumblingWindowWidthDomain::SeedGrids) - .unwrap(); + let memo = sharing_rules_and_shared_state_groups(memo).unwrap(); let found = kll_failure_probabilities( shared_summary_state_groups(&memo) .iter() diff --git a/crates/logical-asap-optimization/src/pass2/logical_memo_candidates.rs b/crates/logical-asap-optimization/src/pass2/logical_memo_candidates.rs index de83ebac0..834b451a6 100644 --- a/crates/logical-asap-optimization/src/pass2/logical_memo_candidates.rs +++ b/crates/logical-asap-optimization/src/pass2/logical_memo_candidates.rs @@ -2,110 +2,53 @@ //! candidates and groups that share work across computations //! ([`asap_types::stages::LogicalASAPMemoCandidates`]), which Stage 2 implements. -use std::time::Instant; - -use crate::pass2::memo::check_generation_deadline; use crate::pass2::memo::rules::{ finer_aggregate, rollup, scan_labels, shared_configuration_readers, summary_inputs, summary_results, topk, }; -use crate::pass2::memo::shared_configurations::TumblingWindowWidthDomain; use crate::pass2::memo::state_groups::{ add_shared_summary_state_groups, check_unexpanded_memo, Pass2RuleError, }; -use crate::pass2::memo::window_candidates::extend_local_window_composition; +use crate::pass2::memo::window_candidates::add_local_window_composition; use asap_types::stages::Memo; -use asap_types::stages::PerComputationLogicalMemoCandidates; -use asap_types::stages::{CandidateGenerationTimeout, LogicalASAPMemoCandidates}; +use asap_types::stages::{LogicalASAPMemoCandidates, PerComputationLogicalMemoCandidates}; use asap_types::workload::QueryWorkloadEntry; -/// What Pass 2 keeps between tumbling-window-width rounds: the memo after local -/// window composition and before the other rules, and the tumbling-window widths it -/// already holds. Each round adds only the newly exposed widths to it, -/// then reruns the other rules on a copy. -#[derive(Debug, Clone)] -pub struct Pass2RoundState { - unexpanded_memo: Memo, - previous_domain: Option, -} - -impl Pass2RoundState { - /// The state before the first round: Pass 1's memo, no tumbling-window widths. - pub fn new(pass1: PerComputationLogicalMemoCandidates) -> Self { - Self { - unexpanded_memo: pass1.memo, - previous_domain: None, - } - } -} - -/// Why [`generate_logical_asap_memo_candidates`] failed. -#[derive(Debug, thiserror::Error)] -pub enum LogicalASAPMemoCandidatesError { - /// The deadline passed during local window composition. The - /// [`Pass2RoundState`] may hold partial work: discard it. - #[error(transparent)] - LocalWindowComposition(CandidateGenerationTimeout), - /// A later rule or the shared summary-state groups failed. The - /// [`Pass2RoundState`] already holds this round's tumbling-window widths and stays - /// usable for the next round. - #[error(transparent)] - Expansion(#[from] Pass2RuleError), -} - -/// Stage 1, Pass 2 for one tumbling-window-width round: adds to `round`'s memo the -/// local tumbling-window candidates for the widths of `domain` that earlier rounds did -/// not generate (rule 1), records `domain` in `round`, then runs rules 2–9 -/// and `add_shared_summary_state_groups` on a copy and returns it. -/// `entries[i]` is query `i`'s workload entry. Stops once `deadline` passes. +/// Stage 1, Pass 2: adds to Pass 1's memo the local tumbling-window +/// candidates for every legal tumbling-window width (rule 1), then runs +/// rules 2–9 and `add_shared_summary_state_groups`. +/// `entries[i]` is query `i`'s workload entry. pub fn generate_logical_asap_memo_candidates( - round: &mut Pass2RoundState, + pass1: PerComputationLogicalMemoCandidates, entries: &[QueryWorkloadEntry], - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result { +) -> Result { + let mut memo = pass1.memo; // 1. Local window composition. - extend_local_window_composition( - &mut round.unexpanded_memo, - entries, - round.previous_domain, - domain, - deadline, - ) - .map_err(LogicalASAPMemoCandidatesError::LocalWindowComposition)?; - round.previous_domain = Some(domain); - let mut memo = round.unexpanded_memo.clone(); - apply_sharing_rules(&mut memo, domain, deadline)?; + add_local_window_composition(&mut memo, entries); + apply_sharing_rules(&mut memo)?; // 10. One group per shared summary build, read by its reader groups. - add_shared_summary_state_groups(&mut memo, deadline)?; + add_shared_summary_state_groups(&mut memo)?; Ok(LogicalASAPMemoCandidates { memo }) } /// Pass 2's rules 2–9, in order, on a memo after local window composition. -pub(crate) fn apply_sharing_rules( - memo: &mut Memo, - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result<(), Pass2RuleError> { - check_generation_deadline(deadline)?; +pub(crate) fn apply_sharing_rules(memo: &mut Memo) -> Result<(), Pass2RuleError> { check_unexpanded_memo(memo)?; // 2. Scan widening. - scan_labels::add_scan_label_candidates(memo, deadline)?; + scan_labels::add_scan_label_candidates(memo)?; // 3. Summary inputs. - summary_inputs::factor_summary_inputs(memo, deadline)?; + summary_inputs::factor_summary_inputs(memo)?; // 4. Top-k reuse. - topk::add_topk_candidates(memo, deadline)?; + topk::add_topk_candidates(memo)?; // 5. Finer aggregate. - finer_aggregate::add_finer_aggregate_candidates(memo, deadline)?; + finer_aggregate::add_finer_aggregate_candidates(memo)?; // 6. Rollup. - rollup::add_rollup_candidates(memo, deadline)?; + rollup::add_rollup_candidates(memo)?; // 7. Window rollup. - rollup::add_window_rollup_candidates(memo, deadline)?; + rollup::add_window_rollup_candidates(memo)?; // 8. Summary results. - summary_results::factor_summary_results(memo, deadline)?; + summary_results::factor_summary_results(memo)?; // 9. Shared configurations. - shared_configuration_readers::add_state_configuration_candidates_in_domain( - memo, domain, deadline, - )?; + shared_configuration_readers::add_state_configuration_candidates(memo)?; Ok(()) } diff --git a/crates/logical-asap-optimization/src/pass2/memo.rs b/crates/logical-asap-optimization/src/pass2/memo.rs index 2a0296768..f41be0ddf 100644 --- a/crates/logical-asap-optimization/src/pass2/memo.rs +++ b/crates/logical-asap-optimization/src/pass2/memo.rs @@ -14,17 +14,3 @@ pub(crate) mod shared_configurations; pub(crate) mod state_groups; /// Local tumbling-window candidates with selected-reader grid configurations. pub(crate) mod window_candidates; - -use std::time::Instant; - -use asap_types::stages::CandidateGenerationTimeout; - -pub(crate) fn check_generation_deadline( - deadline: Option, -) -> Result<(), CandidateGenerationTimeout> { - if deadline.is_some_and(|deadline| Instant::now() >= deadline) { - Err(CandidateGenerationTimeout) - } else { - Ok(()) - } -} diff --git a/crates/logical-asap-optimization/src/pass2/memo/rules/finer_aggregate.rs b/crates/logical-asap-optimization/src/pass2/memo/rules/finer_aggregate.rs index 3785a8996..78aee9654 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/rules/finer_aggregate.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/rules/finer_aggregate.rs @@ -3,14 +3,12 @@ //! rows, then merge them into the query's groups with each measure's rollup. use super::reuse::append_reader; use crate::pass2::finer_aggregate::match_finer; -use crate::pass2::memo::check_generation_deadline; use crate::pass2::memo::state_groups::Pass2RuleError; use asap_types::ir::operator::{AggIntent, Reduction}; use asap_types::ir::{NonASAPOp, Operator, OperatorNode, OperatorResultKind}; use asap_types::stages::{Memo, OriginOfCandidateForMemoGroup}; use std::collections::HashMap; use std::rc::Rc; -use std::time::Instant; /// A memo group's SQL aggregate: no `without`, measure filter or `HAVING`. struct Aggregate<'a> { @@ -70,10 +68,7 @@ fn value_rollup(measure: &AggIntent, col: usize, global: bool) -> Option, -) -> Result { +pub(crate) fn add_finer_aggregate_candidates(memo: &mut Memo) -> Result { // Only aggregates of equal measures can match; bucket by them (the // first levels of the paper's filter tree) instead of trying all pairs. let mut buckets: HashMap> = HashMap::new(); @@ -89,7 +84,6 @@ pub(crate) fn add_finer_aggregate_candidates( for groups in buckets.values() { for &owner in groups { for &finer in groups { - check_generation_deadline(deadline)?; if owner != finer { added += add_read(memo, owner, finer)?; } diff --git a/crates/logical-asap-optimization/src/pass2/memo/rules/rollup.rs b/crates/logical-asap-optimization/src/pass2/memo/rules/rollup.rs index 76cd7bf01..af01314ef 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/rules/rollup.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/rules/rollup.rs @@ -3,7 +3,6 @@ use super::reuse::{ append_producer, append_reader, combined_inputs, eligible, replace, restore_inputs, }; use crate::pass2::asap_aware_cse::{rollup_candidate_for, rollup_pass2_candidates}; -use crate::pass2::memo::check_generation_deadline; use crate::pass2::memo::state_groups::{simple_build, Pass2RuleError}; use asap_types::ir::schema::FieldDataType; use asap_types::ir::{ @@ -12,18 +11,11 @@ use asap_types::ir::{ use asap_types::stages::{Memo, OriginOfCandidateForMemoGroup}; use std::collections::HashMap; use std::rc::Rc; -use std::time::Instant; -pub(crate) fn add_rollup_candidates( - memo: &mut Memo, - deadline: Option, -) -> Result { - check_generation_deadline(deadline)?; +pub(crate) fn add_rollup_candidates(memo: &mut Memo) -> Result { let mut candidates = Vec::new(); for (group, entries) in memo.candidates.iter().enumerate() { - check_generation_deadline(deadline)?; for candidate in entries { - check_generation_deadline(deadline)?; if eligible(candidate) && matches!( candidate.origin, @@ -39,9 +31,7 @@ pub(crate) fn add_rollup_candidates( } let mut added = 0; for (coarse_group, coarse, coarse_build) in &candidates { - check_generation_deadline(deadline)?; for (fine_group, fine, _) in &candidates { - check_generation_deadline(deadline)?; if coarse_group == fine_group { continue; } @@ -219,23 +209,17 @@ pub(crate) fn add_rollup_candidates( } } } - check_generation_deadline(deadline)?; Ok(added) } /// Existing tumbling window rollup rules applied to producer pairs on already generated /// legal local grids. No whole-workload choice combinations are constructed. -pub(crate) fn add_window_rollup_candidates( - memo: &mut Memo, - deadline: Option, -) -> Result { - check_generation_deadline(deadline)?; +pub(crate) fn add_window_rollup_candidates(memo: &mut Memo) -> Result { use crate::pass2::asap_aware_cse::{ tumbling_window_candidates, window_composition_shared_chain_candidates, }; let mut grids = vec![std::collections::BTreeSet::new(); memo.groups.len()]; for (group, candidates) in memo.candidates.iter().enumerate() { - check_generation_deadline(deadline)?; for candidate in candidates.iter().filter(|candidate| { candidate.origin == OriginOfCandidateForMemoGroup::TumblingWindows && eligible(candidate) @@ -266,14 +250,11 @@ pub(crate) fn add_window_rollup_candidates( .collect(); let mut added = 0; for (coarse_group, coarse) in &candidates { - check_generation_deadline(deadline)?; for (fine_group, fine) in &candidates { - check_generation_deadline(deadline)?; if coarse_group == fine_group || grids[*fine_group].is_empty() { continue; } for grid in &grids[*coarse_group] { - check_generation_deadline(deadline)?; let roots = [ QueryRoot::Operator(Rc::clone(&coarse.root_node)), QueryRoot::Operator(Rc::clone(&fine.root_node)), @@ -286,7 +267,6 @@ pub(crate) fn add_window_rollup_candidates( for rewritten in window_composition_shared_chain_candidates(&roots, &tumbling_windows) { - check_generation_deadline(deadline)?; if rewritten.shared_chains.is_empty() { continue; } @@ -328,7 +308,6 @@ pub(crate) fn add_window_rollup_candidates( } } } - check_generation_deadline(deadline)?; Ok(added) } @@ -336,7 +315,6 @@ pub(crate) fn add_window_rollup_candidates( mod tests { use crate::pass1::per_computation_candidates::generate_per_computation_candidates; use crate::pass2::memo::groups::try_populate_pass1; - use crate::pass2::memo::shared_configurations::TumblingWindowWidthDomain; use crate::stage0::generate_dag_of_logical_memo_groups; use crate::test_support::{lower_promql, sharing_rules_and_shared_state_groups}; use asap_types::ir::properties::AccuracyTarget; @@ -357,11 +335,9 @@ mod tests { let inventory = generate_per_computation_candidates(&generate_dag_of_logical_memo_groups(&roots)) .unwrap(); - let memo = sharing_rules_and_shared_state_groups( - try_populate_pass1(initial, &inventory).unwrap(), - TumblingWindowWidthDomain::Complete, - ) - .unwrap(); + let memo = + sharing_rules_and_shared_state_groups(try_populate_pass1(initial, &inventory).unwrap()) + .unwrap(); let root_of = |query: usize| memo.query_roots[query].as_operator().unwrap().0 as usize; let (coarse, fine) = (root_of(0), root_of(1)); assert!( diff --git a/crates/logical-asap-optimization/src/pass2/memo/rules/scan_labels.rs b/crates/logical-asap-optimization/src/pass2/memo/rules/scan_labels.rs index d7319b103..1410c426a 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/rules/scan_labels.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/rules/scan_labels.rs @@ -1,9 +1,7 @@ //! Scan-label widening for explicit memo state sharing. Reuses the existing //! Pass 2 widening rule; only its source-chain boundaries are interned here. -use crate::pass2::memo::check_generation_deadline; use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; use asap_accuracy::propagation::recompose_guarantees; use asap_types::ir::operator::Reduction; @@ -15,17 +13,11 @@ use asap_types::stages::{MemoGroup, MemoGroupId}; /// Preserve local implementations and append readers over union-label inputs. /// Parent/query reachability is rebuilt by the enclosing state-group expansion. -pub(crate) fn add_scan_label_candidates( - memo: &mut Memo, - deadline: Option, -) -> Result { - check_generation_deadline(deadline)?; +pub(crate) fn add_scan_label_candidates(memo: &mut Memo) -> Result { let mut owners = Vec::new(); let mut roots = Vec::new(); for (group, candidates) in memo.candidates.iter().enumerate() { - check_generation_deadline(deadline)?; for (index, candidate) in candidates.iter().enumerate() { - check_generation_deadline(deadline)?; // Source-group candidates expose scan reuse to every downstream // operator, including exact/non-summary computations. Their public // schemas stay narrow through the existing projection rewrite. @@ -81,7 +73,6 @@ pub(crate) fn add_scan_label_candidates( let mut added = 0; let mut canonical = HashMap::new(); for (((group, index), root), unwidened) in owners.into_iter().zip(widened).zip(unwidened) { - check_generation_deadline(deadline)?; let (QueryRoot::Operator(node), QueryRoot::Operator(unwidened)) = (root, unwidened) else { unreachable!("operator readers") }; @@ -138,7 +129,6 @@ pub(crate) fn add_scan_label_candidates( memo.candidates[group].push(reader); added += 1; } - check_generation_deadline(deadline)?; Ok(added) } diff --git a/crates/logical-asap-optimization/src/pass2/memo/rules/shared_configuration_readers.rs b/crates/logical-asap-optimization/src/pass2/memo/rules/shared_configuration_readers.rs index 04b682f58..50a8aff50 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/rules/shared_configuration_readers.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/rules/shared_configuration_readers.rs @@ -1,9 +1,7 @@ //! Expand compatible readers at finite shared sizes and tumbling-window horizons before //! interning concrete states. Local candidates remain available. -use crate::pass2::memo::check_generation_deadline; use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; use asap_types::ir::flat::{flatten, FlatDag}; use asap_types::ir::schema::{sketch::SketchKind, FieldDataType}; @@ -14,8 +12,8 @@ use super::shared_producer::{ }; use crate::pass2::asap_aware_cse::{with_resized_build, with_tumbling_window_chain}; use crate::pass2::memo::shared_configurations::{ - shared_size, state_size_configurations, tumbling_window_configurations_before, - TumblingWindowConfiguration, TumblingWindowRequirement, TumblingWindowWidthDomain, + shared_size, state_size_configurations, tumbling_window_configurations, + TumblingWindowConfiguration, TumblingWindowRequirement, }; use crate::pass2::memo::state_groups::{same_range_input, simple_build, Pass2RuleError}; use asap_types::stages::Memo; @@ -95,7 +93,6 @@ enum StateCovering { /// later ([`super::super::state_groups`]). struct SummaryStates { deduplicate_requests: bool, - domain: TumblingWindowWidthDomain, /// Every candidate's graph signature by group, so no rewrite repeats one. known: HashMap<(usize, String), Vec>>, rewrite_count: usize, @@ -103,28 +100,21 @@ struct SummaryStates { requests: HashSet<(usize, usize)>, } -pub(crate) fn add_state_configuration_candidates_in_domain( - memo: &mut Memo, - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result<(), Pass2RuleError> { - expand_configurations(memo, true, domain, deadline).map(|_| ()) +pub(crate) fn add_state_configuration_candidates(memo: &mut Memo) -> Result<(), Pass2RuleError> { + expand_configurations(memo, true).map(|_| ()) } fn expand_configurations( memo: &mut Memo, deduplicate_requests: bool, - domain: TumblingWindowWidthDomain, - deadline: Option, ) -> Result { let mut states = SummaryStates { deduplicate_requests, - domain, known: HashMap::new(), rewrite_count: 0, requests: HashSet::new(), }; - add_shared_producer_candidates(memo, &mut states, deadline)?; + add_shared_producer_candidates(memo, &mut states)?; Ok(states.rewrite_count) } @@ -132,16 +122,10 @@ impl SharedProducer for SummaryStates { type Need = StateNeed; type Covering = StateCovering; - fn sets( - &mut self, - memo: &Memo, - deadline: Option, - ) -> Result>>, Pass2RuleError> { + fn sets(&mut self, memo: &Memo) -> Result>>, Pass2RuleError> { let mut sets: Vec>> = Vec::new(); for (group, candidates) in memo.candidates.iter().enumerate() { - check_generation_deadline(deadline)?; for (candidate, entry) in candidates.iter().enumerate() { - check_generation_deadline(deadline)?; let Some((build, _)) = simple_build(memo, entry) else { continue; }; @@ -198,9 +182,7 @@ impl SharedProducer for SummaryStates { } } for (group, candidates) in memo.candidates.iter().enumerate() { - check_generation_deadline(deadline)?; for candidate in candidates { - check_generation_deadline(deadline)?; self.known .entry((group, graph_signature(&candidate.root_node)?)) .or_default() @@ -210,17 +192,13 @@ impl SharedProducer for SummaryStates { let mut layouts: Vec<(usize, usize, FlatDag)> = Vec::new(); for set in &mut sets { if spans_groups(set) { - assign_normalized_layouts(memo, set, &mut layouts, deadline)?; + assign_normalized_layouts(memo, set, &mut layouts)?; } } Ok(sets) } - fn coverings( - &self, - set: &[Reader], - deadline: Option, - ) -> Result, Pass2RuleError> { + fn coverings(&self, set: &[Reader]) -> Result, Pass2RuleError> { let tumbling_window_requirements: Option> = set .iter() .map(|reader| tumbling_window_requirement(&reader.need)) @@ -234,7 +212,7 @@ impl SharedProducer for SummaryStates { }) .collect(); let first = &set[0].need.family; - return Ok(state_size_configurations(requirements, deadline)? + return Ok(state_size_configurations(requirements) .into_iter() .map(|configuration| { let family = match (first, configuration) { @@ -253,19 +231,17 @@ impl SharedProducer for SummaryStates { }) .collect()); }; - Ok( - tumbling_window_configurations_before(&requirements, self.domain, deadline)? - .into_iter() - .enumerate() - .map( - |(configuration_index, configuration)| StateCovering::TumblingWindows { - configuration_index, - accuracy_budget_parts: covered_parts(&configuration.family, set), - configuration, - }, - ) - .collect(), - ) + Ok(tumbling_window_configurations(&requirements) + .into_iter() + .enumerate() + .map( + |(configuration_index, configuration)| StateCovering::TumblingWindows { + configuration_index, + accuracy_budget_parts: covered_parts(&configuration.family, set), + configuration, + }, + ) + .collect()) } fn derive( @@ -375,7 +351,6 @@ fn assign_normalized_layouts( memo: &Memo, set: &mut [Reader], layouts: &mut Vec<(usize, usize, FlatDag)>, - deadline: Option, ) -> Result<(), Pass2RuleError> { let Some(requirements) = set .iter() @@ -386,7 +361,6 @@ fn assign_normalized_layouts( }; let mut layout_keys: HashMap<(usize, String), Vec> = HashMap::new(); for (reader, requirement) in set.iter_mut().zip(&requirements) { - check_generation_deadline(deadline)?; let Ok(count) = u32::try_from(requirement.window_ms) else { // Normalization must not narrow the legal long-window domain. continue; @@ -564,16 +538,8 @@ mod tests { ]; crate::pass2::memo::window_candidates::add_local_window_composition(&mut memo, &entries); let mut unoptimized = memo.clone(); - let optimized_rewrites = - expand_configurations(&mut memo, true, TumblingWindowWidthDomain::Complete, None) - .unwrap(); - let exhaustive_rewrites = expand_configurations( - &mut unoptimized, - false, - TumblingWindowWidthDomain::Complete, - None, - ) - .unwrap(); + let optimized_rewrites = expand_configurations(&mut memo, true).unwrap(); + let exhaustive_rewrites = expand_configurations(&mut unoptimized, false).unwrap(); assert!( optimized_rewrites < exhaustive_rewrites, "{optimized_rewrites} versus {exhaustive_rewrites}" @@ -659,7 +625,7 @@ mod tests { "{parts_of_size:?}" ); let before = memo.candidates.iter().map(Vec::len).sum::(); - expand_configurations(&mut memo, true, TumblingWindowWidthDomain::SeedGrids, None).unwrap(); + expand_configurations(&mut memo, true).unwrap(); assert!(memo.candidates.iter().map(Vec::len).sum::() > before); for candidate in memo.candidates.iter().flatten() { for k in kll_sizes(candidate) { diff --git a/crates/logical-asap-optimization/src/pass2/memo/rules/shared_producer.rs b/crates/logical-asap-optimization/src/pass2/memo/rules/shared_producer.rs index dbf4635f1..ea01f1956 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/rules/shared_producer.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/rules/shared_producer.rs @@ -4,10 +4,8 @@ //! from it as one more candidate; its own candidates stay, so plan selection //! decides. Identical expressions are the trivial case, shared by memo group //! identity, and add no candidates here. -use crate::pass2::memo::check_generation_deadline; use crate::pass2::memo::state_groups::Pass2RuleError; use asap_types::stages::Memo; -use std::time::Instant; /// One reader of a kind of shared producer: its memo group and its need. pub(in crate::pass2::memo) struct Reader { @@ -23,17 +21,9 @@ pub(in crate::pass2::memo) trait SharedProducer { type Covering; /// The memo's readers of this kind, in sets whose members can all read /// one producer; in memo order. - fn sets( - &mut self, - memo: &Memo, - deadline: Option, - ) -> Result>>, Pass2RuleError>; + fn sets(&mut self, memo: &Memo) -> Result>>, Pass2RuleError>; /// The coverings to offer the readers of `set`. - fn coverings( - &self, - set: &[Reader], - deadline: Option, - ) -> Result, Pass2RuleError>; + fn coverings(&self, set: &[Reader]) -> Result, Pass2RuleError>; /// Adds `reader`'s derivation from the producer at `covering`, if that /// producer serves it; the number of candidates added. fn derive( @@ -55,23 +45,18 @@ pub(in crate::pass2::memo) fn spans_groups(set: &[Reader]) -> bool { pub(in crate::pass2::memo) fn add_shared_producer_candidates( memo: &mut Memo, producer: &mut P, - deadline: Option, ) -> Result { - check_generation_deadline(deadline)?; let mut added = 0; - for set in producer.sets(memo, deadline)? { - check_generation_deadline(deadline)?; + for set in producer.sets(memo)? { if !spans_groups(&set) { continue; } - let coverings = producer.coverings(&set, deadline)?; + let coverings = producer.coverings(&set)?; for reader in &set { for covering in &coverings { - check_generation_deadline(deadline)?; added += producer.derive(memo, reader, covering)?; } } } - check_generation_deadline(deadline)?; Ok(added) } diff --git a/crates/logical-asap-optimization/src/pass2/memo/rules/summary_inputs.rs b/crates/logical-asap-optimization/src/pass2/memo/rules/summary_inputs.rs index dcab1cd59..06e1cbd04 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/rules/summary_inputs.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/rules/summary_inputs.rs @@ -2,19 +2,13 @@ //! This preserves their full expressions and schema, including opaque series //! identity, rather than assuming that a projection is an identity operation. use super::reuse::{append_producer, append_reader, eligible, replace}; -use crate::pass2::memo::check_generation_deadline; use crate::pass2::memo::state_groups::Pass2RuleError; use asap_types::ir::{ASAPOp, NonASAPOp, OperatorNode}; use asap_types::stages::{Memo, OriginOfCandidateForMemoGroup}; use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; -pub(crate) fn factor_summary_inputs( - memo: &mut Memo, - deadline: Option, -) -> Result<(), Pass2RuleError> { - check_generation_deadline(deadline)?; +pub(crate) fn factor_summary_inputs(memo: &mut Memo) -> Result<(), Pass2RuleError> { let originals: Vec<_> = memo .candidates .iter() @@ -27,7 +21,6 @@ pub(crate) fn factor_summary_inputs( }) .collect(); for (owner, original) in originals { - check_generation_deadline(deadline)?; let boundaries: HashSet<_> = original .input_groups .iter() @@ -37,7 +30,6 @@ pub(crate) fn factor_summary_inputs( let mut seen = HashSet::new(); let mut inputs = Vec::new(); while let Some(node) = pending.pop() { - check_generation_deadline(deadline)?; if boundaries.contains(&Rc::as_ptr(&node)) || !seen.insert(Rc::as_ptr(&node)) { continue; } @@ -46,7 +38,6 @@ pub(crate) fn factor_summary_inputs( Some(ASAPOp::SummaryAgg { .. } | ASAPOp::SummaryTumblingWindowChain { .. }) ) { for child in node.children() { - check_generation_deadline(deadline)?; if !boundaries.contains(&Rc::as_ptr(child)) { inputs.push(Rc::clone(child)); } @@ -57,7 +48,6 @@ pub(crate) fn factor_summary_inputs( let queries = memo.groups[owner].queries.clone(); let mut replacements = HashMap::new(); for input in inputs { - check_generation_deadline(deadline)?; canonical_preprocessing(memo, &input, &boundaries, &queries, &mut replacements)?; } if !replacements.is_empty() { @@ -65,7 +55,6 @@ pub(crate) fn factor_summary_inputs( append_reader(memo, owner, &original, reader, original.origin.clone())?; } } - check_generation_deadline(deadline)?; Ok(()) } diff --git a/crates/logical-asap-optimization/src/pass2/memo/rules/summary_results.rs b/crates/logical-asap-optimization/src/pass2/memo/rules/summary_results.rs index 3d2cfbc12..58137290e 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/rules/summary_results.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/rules/summary_results.rs @@ -2,19 +2,13 @@ //! choose a state configuration independently, without enumerating combinations //! of all private states in the enclosing query. use super::reuse::{append_producer, append_reader, eligible, replace}; -use crate::pass2::memo::check_generation_deadline; use crate::pass2::memo::state_groups::{direct_input_builds, Pass2RuleError}; use asap_types::ir::{ASAPOp, OperatorNode}; use asap_types::stages::{Memo, OriginOfCandidateForMemoGroup}; use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; -pub(crate) fn factor_summary_results( - memo: &mut Memo, - deadline: Option, -) -> Result { - check_generation_deadline(deadline)?; +pub(crate) fn factor_summary_results(memo: &mut Memo) -> Result { let originals: Vec<_> = memo .candidates .iter() @@ -28,7 +22,6 @@ pub(crate) fn factor_summary_results( .collect(); let mut added = 0; for (owner, original) in originals { - check_generation_deadline(deadline)?; let builds = direct_input_builds(memo, &original); if builds.len() < 2 { continue; @@ -44,7 +37,6 @@ pub(crate) fn factor_summary_results( let mut seen = HashSet::new(); let mut readouts = Vec::new(); while let Some(node) = pending.pop() { - check_generation_deadline(deadline)?; if boundaries.contains(&Rc::as_ptr(&node)) || !seen.insert(Rc::as_ptr(&node)) { continue; } @@ -71,7 +63,6 @@ pub(crate) fn factor_summary_results( let queries = memo.groups[owner].queries.clone(); let mut replacements = HashMap::new(); for readout in readouts { - check_generation_deadline(deadline)?; let group = append_producer( memo, Rc::clone(&readout), @@ -95,6 +86,5 @@ pub(crate) fn factor_summary_results( OriginOfCandidateForMemoGroup::SummaryResult, )?); } - check_generation_deadline(deadline)?; Ok(added) } diff --git a/crates/logical-asap-optimization/src/pass2/memo/rules/topk.rs b/crates/logical-asap-optimization/src/pass2/memo/rules/topk.rs index 681f5de28..0664b82ef 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/rules/topk.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/rules/topk.rs @@ -3,7 +3,6 @@ use super::reuse::{append_producer, append_reader, eligible, replace}; use super::shared_producer::{add_shared_producer_candidates, Reader, SharedProducer}; use crate::pass2::asap_aware_cse::topk_reuse_candidates; -use crate::pass2::memo::check_generation_deadline; use crate::pass2::memo::state_groups::Pass2RuleError; use asap_types::ir::operator::{AggIntent, Reduction}; use asap_types::ir::properties::AccuracyGuarantee; @@ -13,18 +12,13 @@ use asap_types::stages::MemoGroupId; use asap_types::stages::{Memo, OriginOfCandidateForMemoGroup}; use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; -pub(crate) fn add_topk_candidates( - memo: &mut Memo, - deadline: Option, -) -> Result { +pub(crate) fn add_topk_candidates(memo: &mut Memo) -> Result { add_shared_producer_candidates( memo, &mut TopKRankings { exact_groups: HashMap::new(), }, - deadline, ) } @@ -50,15 +44,10 @@ impl SharedProducer for TopKRankings { type Need = Rc; type Covering = SharedRanking; - fn sets( - &mut self, - memo: &Memo, - deadline: Option, - ) -> Result>>>, Pass2RuleError> { + fn sets(&mut self, memo: &Memo) -> Result>>>, Pass2RuleError> { let mut owners = HashMap::new(); let mut roots = Vec::new(); for (group, entry) in memo.groups.iter().enumerate() { - check_generation_deadline(deadline)?; let [input] = entry.logical_node.children()[..] else { continue; }; @@ -96,7 +85,6 @@ impl SharedProducer for TopKRankings { fn coverings( &self, set: &[Reader>], - _deadline: Option, ) -> Result, Pass2RuleError> { // The largest `k` holds every smaller top-k, so it serves the set. Ok(vec![SharedRanking { diff --git a/crates/logical-asap-optimization/src/pass2/memo/shared_configurations.rs b/crates/logical-asap-optimization/src/pass2/memo/shared_configurations.rs index 2a2240572..aa5682be1 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/shared_configurations.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/shared_configurations.rs @@ -7,155 +7,15 @@ use asap_types::ir::schema::{ use asap_types::workload::greatest_common_divisor; use std::collections::BTreeSet; -/// Width domain used by the same candidate rules. Seed grids are only a -/// feasible-plan heuristic; optimality must use the complete domain. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum TumblingWindowWidthDomain { - #[default] - Complete, - SeedGrids, - /// Seed grids plus widths obtained by dividing each grid into at most - /// `max_parts` equal pieces. Increasing the limit monotonically exposes - /// every legal divisor; only `Complete` certifies full-domain search. - Expanding { - max_parts: u64, - }, -} - -#[cfg(test)] -pub(crate) fn tumbling_window_width_domain( - grids: impl IntoIterator, - domain: TumblingWindowWidthDomain, -) -> Vec { - tumbling_window_width_domain_before(grids, domain, None) - .expect("generation without a deadline cannot time out") -} - -pub(crate) fn tumbling_window_width_domain_before( - grids: impl IntoIterator, - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result, asap_types::stages::CandidateGenerationTimeout> { - super::check_generation_deadline(deadline)?; +/// Every tumbling-window width that divides one of `grids` (zero grids +/// ignored), in ascending order. +pub(crate) fn tumbling_window_width_domain(grids: impl IntoIterator) -> Vec { + let grids: BTreeSet = grids.into_iter().filter(|&grid| grid > 0).collect(); let mut widths = BTreeSet::new(); - for width in grids { - super::check_generation_deadline(deadline)?; - if width > 0 { - widths.insert(width); - } - } - if domain == TumblingWindowWidthDomain::Complete { - let mut complete = BTreeSet::new(); - for grid in widths { - complete.extend(tumbling_window_width_divisors_before(grid, deadline)?); - } - return Ok(complete.into_iter().collect()); - } - let grids = widths.clone(); - loop { - let mut additional = BTreeSet::new(); - for &a in &widths { - for &b in &widths { - super::check_generation_deadline(deadline)?; - let width = greatest_common_divisor(a, b); - if !widths.contains(&width) { - additional.insert(width); - } - } - } - if additional.is_empty() { - break; - } - widths.extend(additional); - } - if let TumblingWindowWidthDomain::Expanding { max_parts } = domain { - // Work is proportional to the requested frontier, not the complete - // divisor domain. Do not introduce the finest reciprocal width early. - for grid in grids { - for parts in 2..=max_parts.min(grid) { - if parts % 1024 == 2 { - super::check_generation_deadline(deadline)?; - } - if grid.is_multiple_of(parts) { - widths.insert(grid / parts); - } - } - } - } - super::check_generation_deadline(deadline)?; - Ok(widths.into_iter().collect()) -} - -/// Integer-domain frontier, independent of expensive candidate graph expansion. -/// Complete divisor lists are computed once under the caller's deadline. -pub struct TumblingWindowWidthFrontier { - grids: Vec, - complete: Vec>, - previous: Vec>, - max_parts: u64, - finished: bool, -} - -impl TumblingWindowWidthFrontier { - pub fn new( - grids: impl IntoIterator, - deadline: Option, - ) -> Result { - let grids: Vec<_> = grids - .into_iter() - .filter(|grid| *grid > 0) - .collect::>() - .into_iter() - .collect(); - let complete = grids - .iter() - .map(|&grid| tumbling_window_width_divisors_before(grid, deadline)) - .collect::, _>>()?; - let previous = grids.iter().map(|&grid| vec![grid]).collect(); - Ok(Self { - grids, - complete, - previous, - max_parts: 4, - finished: false, - }) - } - - /// Skip frontiers which expose no new width for any individual reader grid. - /// Comparing per-grid sets also protects group-specific producer choices. - pub fn next_domain( - &mut self, - deadline: Option, - ) -> Result, asap_types::stages::CandidateGenerationTimeout> - { - while !self.finished { - super::check_generation_deadline(deadline)?; - let current: Vec> = self - .grids - .iter() - .zip(&self.complete) - .map(|(&grid, widths)| { - widths - .iter() - .copied() - .filter(|width| grid / width <= self.max_parts) - .collect() - }) - .collect(); - self.max_parts = self.max_parts.saturating_mul(2); - if current == self.complete { - self.finished = true; - return Ok(Some(TumblingWindowWidthDomain::Complete)); - } - if current != self.previous { - self.previous = current; - return Ok(Some(TumblingWindowWidthDomain::Expanding { - max_parts: self.max_parts / 2, - })); - } - } - Ok(None) + for grid in grids { + widths.extend(tumbling_window_width_divisors(grid)); } + widths.into_iter().collect() } #[derive(Debug, Clone)] @@ -190,74 +50,36 @@ impl TumblingWindowConfiguration { /// Every positive integer-millisecond divisor of a grid, in ascending order. /// The time representation fixes the search resolution; no representative-width /// sampling or arbitrary candidate cap changes this domain. -#[cfg(test)] pub(crate) fn tumbling_window_width_divisors(grid_ms: u64) -> Vec { - tumbling_window_width_divisors_before(grid_ms, None) - .expect("generation without a deadline cannot time out") -} - -fn tumbling_window_width_divisors_before( - grid_ms: u64, - deadline: Option, -) -> Result, asap_types::stages::CandidateGenerationTimeout> { - super::check_generation_deadline(deadline)?; let mut widths = BTreeSet::new(); let mut divisor = 1; while divisor <= grid_ms / divisor { - // A large user-provided grid must not monopolize the planning budget. - if divisor % 1024 == 1 { - super::check_generation_deadline(deadline)?; - } if grid_ms.is_multiple_of(divisor) { widths.insert(divisor); widths.insert(grid_ms / divisor); } divisor += 1; } - super::check_generation_deadline(deadline)?; - Ok(widths.into_iter().collect()) + widths.into_iter().collect() } /// Complete divisor grids, distinct required horizons, and joined state sizes. /// This enumerates configurations of one compatible producer, not workload plans. -#[cfg(test)] -fn tumbling_window_configurations( - readers: &[TumblingWindowRequirement], -) -> Vec { - tumbling_window_configurations_in_domain(readers, TumblingWindowWidthDomain::Complete) -} - -#[cfg(test)] -fn tumbling_window_configurations_in_domain( +pub(crate) fn tumbling_window_configurations( readers: &[TumblingWindowRequirement], - domain: TumblingWindowWidthDomain, ) -> Vec { - tumbling_window_configurations_before(readers, domain, None) - .expect("generation without a deadline cannot time out") -} - -pub(crate) fn tumbling_window_configurations_before( - readers: &[TumblingWindowRequirement], - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result, asap_types::stages::CandidateGenerationTimeout> { - super::check_generation_deadline(deadline)?; if readers .iter() .any(|r| r.tumbling_window_width_ms == 0 || r.window_ms == 0) { - return Ok(Vec::new()); + return Vec::new(); } - let grids = tumbling_window_width_domain_before( - readers.iter().map(|reader| { + let grids = + tumbling_window_width_domain(readers.iter().map(|reader| { greatest_common_divisor(reader.tumbling_window_width_ms, reader.window_ms) - }), - domain, - deadline, - )?; + })); let mut configurations = Vec::new(); for &tumbling_window_width_ms in &grids { - super::check_generation_deadline(deadline)?; let eligible: Vec<&TumblingWindowRequirement> = readers .iter() .filter(|r| { @@ -270,16 +92,13 @@ pub(crate) fn tumbling_window_configurations_before( // The sizes: the readers' own, closed under the shared size. let mut sizes: Vec = Vec::new(); for reader in &eligible { - super::check_generation_deadline(deadline)?; if !sizes.iter().any(|known| same_family(known, &reader.family)) { sizes.push(reader.family.clone()); } } let mut i = 0; while i < sizes.len() { - super::check_generation_deadline(deadline)?; for j in 0..i { - super::check_generation_deadline(deadline)?; if let Some(joined) = shared_size(&sizes[i], &sizes[j]) { if !sizes.iter().any(|known| same_family(known, &joined)) { sizes.push(joined); @@ -289,9 +108,7 @@ pub(crate) fn tumbling_window_configurations_before( i += 1; } for &longest_window_ms in &windows { - super::check_generation_deadline(deadline)?; for family in &sizes { - super::check_generation_deadline(deadline)?; let configuration = TumblingWindowConfiguration { tumbling_window_width_ms, longest_window_ms, @@ -318,33 +135,27 @@ pub(crate) fn tumbling_window_configurations_before( } } } - super::check_generation_deadline(deadline)?; - Ok(configurations) + configurations } /// All distinct joined reader requirements, retaining recorded error contracts. /// This is configuration generation only; sharing is represented by state groups. pub(crate) fn state_size_configurations( requirements: Vec>, - deadline: Option, -) -> Result>, asap_types::stages::CandidateGenerationTimeout> { - super::check_generation_deadline(deadline)?; +) -> Vec> { if requirements.iter().any(Option::is_none) { - return Ok(vec![None]); + return vec![None]; } let requirements: Vec = requirements.into_iter().flatten().collect(); let mut joins: Vec = Vec::new(); for requirement in &requirements { - super::check_generation_deadline(deadline)?; let mut grown = vec![requirement.clone()]; for join in &joins { - super::check_generation_deadline(deadline)?; if let Some(joined) = size_for_all_readers(&[join, requirement]) { grown.push(joined); } } for join in grown { - super::check_generation_deadline(deadline)?; if !joins.iter().any(|known| same_size(known, &join)) { joins.push(join); } @@ -352,7 +163,6 @@ pub(crate) fn state_size_configurations( } let mut configurations = Vec::new(); for configuration in joins { - super::check_generation_deadline(deadline)?; let admitted: Vec<_> = requirements .iter() .filter(|required| { @@ -365,7 +175,7 @@ pub(crate) fn state_size_configurations( configurations.push(Some(configuration)); } } - Ok(configurations) + configurations } fn same_size(a: &SketchParams, b: &SketchParams) -> bool { @@ -424,22 +234,15 @@ mod tests { k: 200, failure_probability: None, }; - let configurations = state_size_configurations( - vec![ - Some(small.clone()), - Some(large.clone()), - Some(small.clone()), - ], - None, - ) - .unwrap(); + let configurations = state_size_configurations(vec![ + Some(small.clone()), + Some(large.clone()), + Some(small.clone()), + ]); assert_eq!(configurations.len(), 2); assert!(configurations.contains(&Some(small))); assert!(configurations.contains(&Some(large))); - assert_eq!( - state_size_configurations(vec![None, None], None).unwrap(), - vec![None] - ); + assert_eq!(state_size_configurations(vec![None, None]), vec![None]); } // Equal capacity does not erase the stricter recorded failure probability. @@ -453,8 +256,7 @@ mod tests { k: 331, failure_probability: Some(0.001), }; - let configurations = - state_size_configurations(vec![Some(weak), Some(strict.clone())], None).unwrap(); + let configurations = state_size_configurations(vec![Some(weak), Some(strict.clone())]); assert!(configurations .iter() .flatten() @@ -493,71 +295,6 @@ mod tests { // Every integer-millisecond width that divides the grid is offered, not // just the coarsest width or GCDs of the readers' original configurations. - #[test] - fn width_frontier_skips_empty_rounds_and_detects_actual_completion() { - let mut frontier = TumblingWindowWidthFrontier::new([97], None).unwrap(); - assert_eq!( - frontier.next_domain(None).unwrap(), - Some(TumblingWindowWidthDomain::Complete) - ); - assert_eq!(frontier.next_domain(None).unwrap(), None); - let mut frontier = TumblingWindowWidthFrontier::new([12], None).unwrap(); - assert_eq!( - frontier.next_domain(None).unwrap(), - Some(TumblingWindowWidthDomain::Expanding { max_parts: 4 }) - ); - assert_eq!( - frontier.next_domain(None).unwrap(), - Some(TumblingWindowWidthDomain::Expanding { max_parts: 8 }) - ); - assert_eq!( - frontier.next_domain(None).unwrap(), - Some(TumblingWindowWidthDomain::Complete) - ); - assert!( - TumblingWindowWidthFrontier::new([u64::MAX], Some(std::time::Instant::now())).is_err() - ); - } - - #[test] - fn expanding_widths_are_nested_and_eventually_complete() { - let grids = [60, 90]; - let mut previous = - tumbling_window_width_domain(grids, TumblingWindowWidthDomain::SeedGrids); - for max_parts in [2, 4, 8, 16, 32, 64, 128] { - let next = tumbling_window_width_domain( - grids, - TumblingWindowWidthDomain::Expanding { max_parts }, - ); - assert!(previous.iter().all(|width| next.contains(width))); - assert!(next - .iter() - .all(|width| grids.iter().any(|grid| grid % width == 0))); - previous = next; - } - assert_eq!( - previous, - tumbling_window_width_domain(grids, TumblingWindowWidthDomain::Complete) - ); - } - - #[test] - fn expanding_domain_defers_fine_tumbling_windows_and_obeys_deadline() { - let widths = tumbling_window_width_domain( - [60_000], - TumblingWindowWidthDomain::Expanding { max_parts: 4 }, - ); - assert_eq!(widths, vec![15_000, 20_000, 30_000, 60_000]); - assert!(tumbling_window_width_domain_before( - [u64::MAX], - TumblingWindowWidthDomain::Expanding { - max_parts: u64::MAX - }, - Some(std::time::Instant::now()), - ) - .is_err()); - } - #[test] fn tumbling_window_width_domain_contains_every_legal_divisor() { let reader = TumblingWindowRequirement { @@ -583,20 +320,6 @@ mod tests { } } - // Cancellation is checked during divisor enumeration, not only between - // configurations: this grid cannot be fully factored within the budget. - #[test] - #[ignore = "flaky: depends on a wall-clock deadline (#1067)"] - fn large_grid_enumeration_honors_deadline() { - let deadline = std::time::Instant::now() + std::time::Duration::from_millis(2); - assert!(tumbling_window_width_domain_before( - [u64::MAX], - TumblingWindowWidthDomain::Complete, - Some(deadline), - ) - .is_err()); - } - // Public finite enumeration rejects invalid grids instead of dividing by zero. #[test] fn invalid_tumbling_window_grid_has_no_configurations() { diff --git a/crates/logical-asap-optimization/src/pass2/memo/state_groups.rs b/crates/logical-asap-optimization/src/pass2/memo/state_groups.rs index 960693bf0..bb945bd6c 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/state_groups.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/state_groups.rs @@ -5,10 +5,8 @@ //! Concrete state identity includes capacity, guarantee, update/filter semantics, //! and materialization. Supported private projections/filters gain explicit input groups. -use super::check_generation_deadline; use std::collections::{BTreeMap, HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; use asap_types::ir::{ASAPOp, NonASAPOp, OperatorNode}; @@ -19,8 +17,6 @@ use asap_types::stages::{MemoGroup, MemoGroupId}; /// (2–9), or the shared summary-state groups (10). #[derive(Debug, thiserror::Error)] pub enum Pass2RuleError { - #[error(transparent)] - DeadlineExceeded(#[from] asap_types::stages::CandidateGenerationTimeout), #[error("memo group and candidate vectors have different lengths")] MismatchedGroups, #[error("memo contains an out-of-range group reference")] @@ -72,28 +68,21 @@ pub(crate) fn check_unexpanded_memo(memo: &Memo) -> Result<(), Pass2RuleError> { /// (unsupported candidate shapes unchanged). Then rebuilds every group's /// parents and the queries it may serve. On error, discard `memo`: it may /// hold partial work. -pub(crate) fn add_shared_summary_state_groups( - memo: &mut Memo, - deadline: Option, -) -> Result<(), Pass2RuleError> { - add_shared_summary_state_groups_with_index(memo, deadline, true) +pub(crate) fn add_shared_summary_state_groups(memo: &mut Memo) -> Result<(), Pass2RuleError> { + add_shared_summary_state_groups_with_index(memo, true) } /// `indexed` looks builds up by fingerprint; otherwise by a linear scan /// (the reference the index is tested against). fn add_shared_summary_state_groups_with_index( memo: &mut Memo, - deadline: Option, indexed: bool, ) -> Result<(), Pass2RuleError> { let mut builds: Vec = Vec::new(); let mut build_index: HashMap, Vec> = HashMap::new(); for (group, candidates) in memo.candidates.iter().enumerate() { - check_generation_deadline(deadline)?; for (index, candidate) in candidates.iter().enumerate() { - check_generation_deadline(deadline)?; for (build, children) in direct_input_builds(memo, candidate) { - check_generation_deadline(deadline)?; let fingerprint = build_fingerprint(&build); let bucket = build_index.entry(fingerprint).or_default(); let matching = if indexed { @@ -123,7 +112,6 @@ fn add_shared_summary_state_groups_with_index( let mut replacements: BTreeMap<(usize, usize), HashMap<*const OperatorNode, Rc>> = BTreeMap::new(); for build in builds { - check_generation_deadline(deadline)?; // Repeated private builds within one result are reusable too. The // reader list records concrete build occurrences, not just owners. if build.readers.len() < 2 { @@ -161,7 +149,6 @@ fn add_shared_summary_state_groups_with_index( accuracy_budget_parts, }]); for (group, index, own_build) in build.readers { - check_generation_deadline(deadline)?; replacements .entry((group, index)) .or_default() @@ -170,7 +157,6 @@ fn add_shared_summary_state_groups_with_index( state_groups.push(state_group); } for ((group, index), replacements) in replacements { - check_generation_deadline(deadline)?; // Factor every available producer boundary together. A producer used by // only one selected reader is still legal; no state-subset enumeration // is required. Retain the original entirely local implementation too. @@ -191,11 +177,9 @@ fn add_shared_summary_state_groups_with_index( } // Parents describe every possible group/candidate dependency, not a selected plan. for group in &mut memo.groups { - check_generation_deadline(deadline)?; group.parents.clear(); } for group in 0..memo.groups.len() { - check_generation_deadline(deadline)?; let dependencies: HashSet<_> = memo.groups[group] .child_groups .iter() @@ -207,7 +191,6 @@ fn add_shared_summary_state_groups_with_index( .copied() .collect(); for child in dependencies { - check_generation_deadline(deadline)?; memo.groups[child.0 as usize] .parents .push(MemoGroupId(group as u32)); @@ -236,13 +219,11 @@ fn add_shared_summary_state_groups_with_index( .copied() .collect(); for child in children { - check_generation_deadline(deadline)?; memo.groups[child.0 as usize].queries.extend(&queries); memo.groups[child.0 as usize].queries.sort_unstable(); memo.groups[child.0 as usize].queries.dedup(); } } - check_generation_deadline(deadline)?; Ok(()) } @@ -479,10 +460,7 @@ mod tests { /// Pass 2 after local window composition, over the complete tumbling-window-width /// domain, as the Pass 2 entry runs it. fn expand(memo: Memo) -> Result { - let memo = crate::test_support::sharing_rules_and_shared_state_groups( - memo, - super::super::shared_configurations::TumblingWindowWidthDomain::Complete, - )?; + let memo = crate::test_support::sharing_rules_and_shared_state_groups(memo)?; Ok(expanded(memo)) } @@ -515,14 +493,12 @@ mod tests { ]), &[6, 6, 6], ); - let domain = super::super::shared_configurations::TumblingWindowWidthDomain::Complete; let mut memo = memo; - crate::pass2::logical_memo_candidates::apply_sharing_rules(&mut memo, domain, None) - .unwrap(); + crate::pass2::logical_memo_candidates::apply_sharing_rules(&mut memo).unwrap(); let mut linear = memo.clone(); let mut indexed = memo; - add_shared_summary_state_groups_with_index(&mut indexed, None, true).unwrap(); - add_shared_summary_state_groups_with_index(&mut linear, None, false).unwrap(); + add_shared_summary_state_groups_with_index(&mut indexed, true).unwrap(); + add_shared_summary_state_groups_with_index(&mut linear, false).unwrap(); let (indexed, linear) = (expanded(indexed), expanded(linear)); assert_eq!(indexed.state_groups, linear.state_groups); assert_eq!(indexed.memo.groups.len(), linear.memo.groups.len()); @@ -554,21 +530,6 @@ mod tests { } } - // State expansion returns a typed timeout, not a partial success that a - // solver could incorrectly report as optimal over the complete catalog. - #[test] - fn expired_state_expansion_returns_timeout() { - let mut memo = workload(&[("quantile_over_time(0.95, latency[5m])", 0.01)]); - assert!(matches!( - crate::pass2::logical_memo_candidates::apply_sharing_rules( - &mut memo, - super::super::shared_configurations::TumblingWindowWidthDomain::Complete, - Some(Instant::now()) - ), - Err(Pass2RuleError::DeadlineExceeded(_)) - )); - } - fn workload(queries: &[(&str, f64)]) -> Memo { workload_with_identity(queries, false) } @@ -929,12 +890,7 @@ mod tests { ], ] { let mut memo = workload_with_identity(&queries, true); - crate::pass2::logical_memo_candidates::apply_sharing_rules( - &mut memo, - super::super::shared_configurations::TumblingWindowWidthDomain::Complete, - None, - ) - .unwrap(); + crate::pass2::logical_memo_candidates::apply_sharing_rules(&mut memo).unwrap(); assert!( memo.candidates.iter().flatten().any( |candidate| candidate.origin == OriginOfCandidateForMemoGroup::WidenedScans @@ -1453,7 +1409,7 @@ mod tests { .filter(|node| is_univmon(node)) .collect(); assert!(!before.is_empty(), "real generation must offer UnivMon"); - crate::pass2::memo::rules::summary_inputs::factor_summary_inputs(&mut memo, None).unwrap(); + crate::pass2::memo::rules::summary_inputs::factor_summary_inputs(&mut memo).unwrap(); assert!(memo .candidates .iter() diff --git a/crates/logical-asap-optimization/src/pass2/memo/window_candidates.rs b/crates/logical-asap-optimization/src/pass2/memo/window_candidates.rs index f1aca5835..6f981102a 100644 --- a/crates/logical-asap-optimization/src/pass2/memo/window_candidates.rs +++ b/crates/logical-asap-optimization/src/pass2/memo/window_candidates.rs @@ -2,10 +2,7 @@ //! connections, not the union of potential consumers, determine eligibility. #[cfg(test)] use super::shared_configurations::tumbling_window_width_divisors; -use super::shared_configurations::{ - tumbling_window_width_domain_before, TumblingWindowWidthDomain, -}; -use super::{check_generation_deadline, CandidateGenerationTimeout}; +use super::shared_configurations::tumbling_window_width_domain; use crate::pass2::asap_aware_cse::{ tumbling_window_candidates, window_composition_pass2_candidates, }; @@ -18,62 +15,22 @@ use asap_types::workload::QueryWorkloadEntry; use std::collections::BTreeSet; use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; -/// Offer each supported reader-grid configuration. Stage 2 checks the actual -/// chosen consumers, including ingestion alignment and range evaluation steps. -#[cfg(any(test, feature = "test-support"))] +/// Offer each supported reader-grid configuration, for every legal +/// tumbling-window width. Stage 2 checks the actual chosen consumers, +/// including ingestion alignment and range evaluation steps. pub fn add_local_window_composition(memo: &mut Memo, entries: &[QueryWorkloadEntry]) { - add_local_window_composition_in_domain(memo, entries, TumblingWindowWidthDomain::Complete); -} - -#[cfg(any(test, feature = "test-support"))] -pub fn add_local_window_composition_in_domain( - memo: &mut Memo, - entries: &[QueryWorkloadEntry], - domain: TumblingWindowWidthDomain, -) { - try_add_local_window_composition_in_domain(memo, entries, domain, None) - .expect("generation without a deadline cannot time out"); -} - -/// On timeout the caller must discard this memo, which may contain partial work. -#[cfg(any(test, feature = "test-support"))] -pub(crate) fn try_add_local_window_composition_in_domain( - memo: &mut Memo, - entries: &[QueryWorkloadEntry], - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result<(), CandidateGenerationTimeout> { - extend_local_window_composition(memo, entries, None, domain, deadline) -} - -/// Extend a pre-state-expansion memo with only newly exposed tumbling-window widths. -/// `previous` must describe widths already generated for this memo. -pub(crate) fn extend_local_window_composition( - memo: &mut Memo, - entries: &[QueryWorkloadEntry], - previous: Option, - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result<(), CandidateGenerationTimeout> { - check_generation_deadline(deadline)?; // Flatten each existing/generated graph once. Repeatedly flattening the // growing candidate list makes a complete tumbling-window-width domain quadratic. let mut known: HashMap<(usize, String), Vec> = HashMap::new(); for (group, candidates) in memo.candidates.iter().enumerate() { - check_generation_deadline(deadline)?; for (index, candidate) in candidates.iter().enumerate() { - check_generation_deadline(deadline)?; if let Ok(signature) = candidate_signature(candidate) { known.entry((group, signature)).or_default().push(index); } } } - for (group, candidate) in - local_tumbling_window_candidates_delta(memo, entries, previous, domain, deadline)? - { - check_generation_deadline(deadline)?; + for (group, candidate) in local_tumbling_window_candidates(memo, entries) { let Ok(signature) = candidate_signature(&candidate) else { memo.candidates[group].push(candidate); continue; @@ -91,75 +48,24 @@ pub(crate) fn extend_local_window_composition( memo.candidates[group].push(candidate); } } - check_generation_deadline(deadline)?; - Ok(()) } /// Expand legal tumbling-window widths while preserving canonical memo inputs. -#[cfg(test)] fn local_tumbling_window_candidates( memo: &Memo, entries: &[QueryWorkloadEntry], ) -> Vec<(usize, CandidateForMemoGroup)> { - local_tumbling_window_candidates_before( - memo, - entries, - TumblingWindowWidthDomain::Complete, - None, - ) - .expect("generation without a deadline cannot time out") -} - -#[cfg(test)] -fn local_tumbling_window_candidates_before( - memo: &Memo, - entries: &[QueryWorkloadEntry], - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result, CandidateGenerationTimeout> { - local_tumbling_window_candidates_delta(memo, entries, None, domain, deadline) -} - -fn local_tumbling_window_candidates_delta( - memo: &Memo, - entries: &[QueryWorkloadEntry], - previous: Option, - domain: TumblingWindowWidthDomain, - deadline: Option, -) -> Result, CandidateGenerationTimeout> { - check_generation_deadline(deadline)?; let group_of = group_of_node(&memo.groups); let mut own: Vec<(usize, CandidateForMemoGroup)> = Vec::new(); for (g, group) in memo.groups.iter().enumerate() { - check_generation_deadline(deadline)?; - let grids = tumbling_window_width_domain_before( + let grids = tumbling_window_width_domain( group .queries .iter() .filter_map(|&query| entries.get(query)?.evaluation_grid_ms()), - domain, - deadline, - )?; - let previous_widths = previous - .map(|previous| { - tumbling_window_width_domain_before( - group - .queries - .iter() - .filter_map(|&query| entries.get(query)?.evaluation_grid_ms()), - previous, - deadline, - ) - }) - .transpose()? - .unwrap_or_default(); + ); for grid in grids { - if previous_widths.binary_search(&grid).is_ok() { - continue; - } - check_generation_deadline(deadline)?; for candidate in &memo.candidates[g] { - check_generation_deadline(deadline)?; if !matches!( candidate.origin, OriginOfCandidateForMemoGroup::Pass1 @@ -187,7 +93,6 @@ fn local_tumbling_window_candidates_delta( continue; }; for pass2_candidate in pass2_candidates { - check_generation_deadline(deadline)?; let Ok(rewritten) = pass2_candidate.roots else { continue; }; @@ -200,7 +105,6 @@ fn local_tumbling_window_candidates_delta( // semantics. Otherwise the tumbling window's canonical input disappears. let mut restored = HashMap::new(); for rewritten in OperatorNode::reachable(node) { - check_generation_deadline(deadline)?; let mut matches = canonical_scans.iter().filter(|original| { original.operator == rewritten.operator && original.schema == rewritten.schema @@ -229,8 +133,7 @@ fn local_tumbling_window_candidates_delta( } } } - check_generation_deadline(deadline)?; - Ok(own) + own } fn restore_scan_boundaries( @@ -274,7 +177,6 @@ fn compatible_reader_grids(group: &MemoGroup, entries: &[QueryWorkloadEntry]) -> .queries .iter() .filter_map(|&query| entries.get(query)?.evaluation_grid_ms()), - TumblingWindowWidthDomain::Complete, ) } @@ -357,65 +259,6 @@ mod tests { use asap_types::ir::properties::AccuracyTarget; use asap_types::workload::{QueryRecurrence, RepeatedDemand, RepetitionInterval}; - // An expired generation budget reports an incomplete catalog explicitly; - // callers must never interpret it as an empty, fully explored candidate set. - #[test] - fn expired_window_generation_returns_timeout() { - let (mut memo, entries) = fixture(&[repeated(60_000)]); - let original_sizes: Vec<_> = memo.candidates.iter().map(Vec::len).collect(); - assert!(try_add_local_window_composition_in_domain( - &mut memo, - &entries, - TumblingWindowWidthDomain::Complete, - Some(Instant::now()) - ) - .is_err()); - assert_eq!( - memo.candidates.iter().map(Vec::len).collect::>(), - original_sizes - ); - } - - #[test] - fn incremental_local_widths_match_one_complete_pass() { - let (base, entries) = fixture(&[repeated(60)]); - let mut incremental = base.clone(); - let mut previous = None; - for domain in [ - TumblingWindowWidthDomain::SeedGrids, - TumblingWindowWidthDomain::Expanding { max_parts: 4 }, - TumblingWindowWidthDomain::Expanding { max_parts: 8 }, - TumblingWindowWidthDomain::Complete, - ] { - extend_local_window_composition(&mut incremental, &entries, previous, domain, None) - .unwrap(); - assert!(local_tumbling_window_candidates_delta( - &incremental, - &entries, - Some(domain), - domain, - None - ) - .unwrap() - .is_empty()); - previous = Some(domain); - } - let mut complete = base; - add_local_window_composition(&mut complete, &entries); - let signatures = |memo: &Memo| { - memo.candidates - .iter() - .enumerate() - .flat_map(|(group, candidates)| { - candidates - .iter() - .map(move |candidate| (group, candidate_signature(candidate).unwrap())) - }) - .collect::>() - }; - assert_eq!(signatures(&incremental), signatures(&complete)); - } - fn repeated(interval: u32) -> QueryRecurrence { QueryRecurrence::Repeated(RepeatedDemand::FixedInterval(RepetitionInterval(interval))) } diff --git a/crates/logical-asap-optimization/src/test_support.rs b/crates/logical-asap-optimization/src/test_support.rs index d9cdffc3d..eb97ce0b8 100644 --- a/crates/logical-asap-optimization/src/test_support.rs +++ b/crates/logical-asap-optimization/src/test_support.rs @@ -16,9 +16,7 @@ pub use crate::pass2::asap_aware_cse::{ with_union_scan_labels, }; pub use crate::pass2::memo::groups::try_populate_pass1; -pub use crate::pass2::memo::window_candidates::{ - add_local_window_composition, add_local_window_composition_in_domain, -}; +pub use crate::pass2::memo::window_candidates::add_local_window_composition; pub use crate::stage0::cse::share_common_sub_dags; use asap_types::ir::properties::AccuracyTarget; @@ -29,16 +27,15 @@ use asap_types::workload::{ }; /// Pass 2 after local window composition, on a memo the test composed -/// itself (or not at all): rules 2–9 for `domain`, then +/// itself (or not at all): rules 2–9, then /// `add_shared_summary_state_groups`. /// Production runs these through /// [`generate_logical_asap_memo_candidates`](crate::pass2::logical_memo_candidates::generate_logical_asap_memo_candidates). pub fn sharing_rules_and_shared_state_groups( mut memo: asap_types::stages::Memo, - domain: crate::pass2::memo::shared_configurations::TumblingWindowWidthDomain, ) -> Result { - crate::pass2::logical_memo_candidates::apply_sharing_rules(&mut memo, domain, None)?; - crate::pass2::memo::state_groups::add_shared_summary_state_groups(&mut memo, None)?; + crate::pass2::logical_memo_candidates::apply_sharing_rules(&mut memo)?; + crate::pass2::memo::state_groups::add_shared_summary_state_groups(&mut memo)?; Ok(memo) } diff --git a/crates/physical-asap-optimization/src/catalog/configurations.rs b/crates/physical-asap-optimization/src/catalog/configurations.rs index 3bc3cca9b..c5b8ade0a 100644 --- a/crates/physical-asap-optimization/src/catalog/configurations.rs +++ b/crates/physical-asap-optimization/src/catalog/configurations.rs @@ -59,8 +59,6 @@ pub fn may_keep_across_evaluations_per_sole_reader_query( #[derive(Debug, thiserror::Error)] pub enum ConfigurationError { - #[error(transparent)] - DeadlineExceeded(#[from] asap_types::stages::CandidateGenerationTimeout), #[error("invalid source candidate inventory: {0}")] InvalidInventory(String), #[error("candidate {candidate} already has timing or materialization assigned")] @@ -70,16 +68,14 @@ pub enum ConfigurationError { /// Each operator node of `source` with every timing and materialization /// Stage 2 allows, one candidate each. `query_roots` are the memo's; their /// group references are `source`'s output references. -pub fn enumerate_candidate_configurations_before( +pub fn enumerate_candidate_configurations( source: &CandidateInventory, query_roots: &[QueryRoot], entries: &[QueryWorkloadEntry], data: &DataWorkload, query_time_results_retained: bool, raw_data_retained: bool, - deadline: Option, ) -> Result { - super::check_deadline(deadline)?; let invalid = |message: &str| ConfigurationError::InvalidInventory(message.into()); source.validate().map_err(|error| invalid(&error))?; let output_queries: Vec = output_references(query_roots) @@ -104,14 +100,12 @@ pub fn enumerate_candidate_configurations_before( let mut potential = vec![vec![false; entries.len()]; source_count]; let mut may_produce_query_output = vec![false; source_count]; for (output, &query) in output_queries.iter().enumerate() { - super::check_deadline(deadline)?; query_outputs[query].push(output); let mut pending = source.query_output_nodes[output].clone(); for root in &pending { may_produce_query_output[root.0] = true; } while let Some(candidate) = pending.pop() { - super::check_deadline(deadline)?; if !potential[candidate.0][query] { potential[candidate.0][query] = true; pending.extend( @@ -127,7 +121,6 @@ pub fn enumerate_candidate_configurations_before( let mut pruned_candidates = Vec::new(); let mut source_configurations = vec![Vec::new(); source_count]; for source_id in 0..source_count { - super::check_deadline(deadline)?; let node = &source.operator_nodes_of_candidates_for_memo_groups[source_id].physical_node; if node.timing.is_some() || node.materialization != Materialization::NotMaterialized { return Err(ConfigurationError::AssignedExecution { @@ -324,7 +317,6 @@ pub fn enumerate_candidate_configurations_before( // index; reader eligibility remains expressed by separate graph constraints. let mut timing_index = Vec::with_capacity(source_configurations.len()); for configurations in &source_configurations { - super::check_deadline(deadline)?; let mut by_timing = [ProducerCandidates::default(), ProducerCandidates::default()]; for (timing_index, timing) in [ExecutionTiming::QueryTime, ExecutionTiming::IngestionTime] .into_iter() @@ -332,7 +324,6 @@ pub fn enumerate_candidate_configurations_before( { let mut compatible = Vec::new(); for &producer in configurations { - super::check_deadline(deadline)?; if compatible_timing(timing, &candidates[producer.0]) { compatible.push(producer); } @@ -344,7 +335,6 @@ pub fn enumerate_candidate_configurations_before( let mut input_cache: HashMap<(ProducerCandidates, ExecutionTiming), ProducerCandidates> = HashMap::new(); for candidate in &mut candidates { - super::check_deadline(deadline)?; let mut requirements = Vec::new(); for input in &source.producer_nodes_per_input[candidate.operator_node.0] { let key = (input.clone(), candidate.timing); @@ -357,7 +347,6 @@ pub fn enumerate_candidate_configurations_before( }; let mut compatible = Vec::new(); for producer in input { - super::check_deadline(deadline)?; compatible.extend(timing_index[producer.0][timing].iter().copied()); } let producer_candidates = producer_sets.intern(compatible); @@ -376,7 +365,6 @@ pub fn enumerate_candidate_configurations_before( ); result.pruned_candidates = pruned_candidates; super::validate_candidates(&result).map_err(|error| invalid(&error))?; - super::check_deadline(deadline)?; Ok(result) } diff --git a/crates/physical-asap-optimization/src/catalog/factoring.rs b/crates/physical-asap-optimization/src/catalog/factoring.rs index 967e3adb7..4ee9e4b68 100644 --- a/crates/physical-asap-optimization/src/catalog/factoring.rs +++ b/crates/physical-asap-optimization/src/catalog/factoring.rs @@ -87,7 +87,6 @@ impl MemoGroupOutputToCompatibleProducersCache { /// `accuracy_budget_parts` the largest of theirs. pub fn factor_terminal_readers( inventory: &mut super::inventory::CandidateInventory, - deadline: Option, ) -> Result { use super::inventory::CandidateInventoryError; use asap_types::ir::ASAPOp; @@ -95,7 +94,6 @@ pub fn factor_terminal_readers( let count = inventory.operator_nodes_of_candidates_for_memo_groups.len(); let mut internal = vec![false; count]; for inputs in &inventory.producer_nodes_per_input { - super::check_deadline(deadline)?; for input in inputs { for producer in input { internal[producer.0] = true; @@ -112,7 +110,6 @@ pub fn factor_terminal_readers( let mut representative: Vec<_> = (0..count).collect(); let mut eligible = vec![false; count]; for id in 0..count { - super::check_deadline(deadline)?; let node = &inventory.operator_nodes_of_candidates_for_memo_groups[id]; if internal[id] || outputs[id].is_empty() @@ -171,7 +168,6 @@ pub fn factor_terminal_readers( let mut candidates_for_memo_group = HashMap::>::new(); let mut accuracy_budget_parts = HashMap::::new(); for (id, &rep) in representative.iter().enumerate() { - super::check_deadline(deadline)?; if eligible[id] { let producer_candidates = unions.entry(rep).or_default(); let known = known.entry(rep).or_default(); @@ -210,7 +206,6 @@ pub fn factor_terminal_readers( .iter() .enumerate() { - super::check_deadline(deadline)?; if representative[id] != id { continue; } diff --git a/crates/physical-asap-optimization/src/catalog/inventory.rs b/crates/physical-asap-optimization/src/catalog/inventory.rs index f0ecfd67a..35177beff 100644 --- a/crates/physical-asap-optimization/src/catalog/inventory.rs +++ b/crates/physical-asap-optimization/src/catalog/inventory.rs @@ -66,8 +66,6 @@ impl CandidateInventory { #[derive(Debug, thiserror::Error)] pub enum CandidateInventoryError { - #[error(transparent)] - DeadlineExceeded(#[from] asap_types::stages::CandidateGenerationTimeout), #[error("invalid candidate memo: {0}")] InvalidMemo(String), #[error("unsupported candidate {candidate} in group {group}: {reason}")] @@ -88,28 +86,24 @@ pub enum CandidateInventoryError { /// The inventory with identical terminal readers factored into one node /// (whose `candidates_for_memo_group` lists every candidate it can implement). -pub fn build_candidate_inventory_before( +pub fn build_candidate_inventory( memo: &Memo, output_groups: &[MemoGroupId], - deadline: Option, ) -> Result { let mut inventory = build_with_compatible_producers_cache( memo, output_groups, - deadline, &mut MemoGroupOutputToCompatibleProducersCache::default(), )?; - super::factoring::factor_terminal_readers(&mut inventory, deadline)?; + super::factoring::factor_terminal_readers(&mut inventory)?; Ok(inventory) } pub fn build_with_compatible_producers_cache( memo: &Memo, output_groups: &[MemoGroupId], - deadline: Option, compatible_producers_cache: &mut MemoGroupOutputToCompatibleProducersCache, ) -> Result { - super::check_deadline(deadline)?; let invalid = |message: &str| CandidateInventoryError::InvalidMemo(message.into()); if memo.groups.len() != memo.candidates.len() { return Err(invalid("group and candidate vector lengths differ")); @@ -127,9 +121,7 @@ pub fn build_with_compatible_producers_cache( implementation_roots_of_group: vec![Vec::new(); memo.groups.len()], }; for (group, candidates) in memo.candidates.iter().enumerate() { - super::check_deadline(deadline)?; for (index, candidate) in candidates.iter().enumerate() { - super::check_deadline(deadline)?; let unsupported = |reason| CandidateInventoryError::Unsupported { group, candidate: index, @@ -200,9 +192,7 @@ pub fn build_with_compatible_producers_cache( } } for (group, candidates) in memo.candidates.iter().enumerate() { - super::check_deadline(deadline)?; for (index, candidate) in candidates.iter().enumerate() { - super::check_deadline(deadline)?; let mut boundaries = HashMap::new(); for &child in &candidate.input_groups { let pointer = Rc::as_ptr(&memo.groups[child.0 as usize].logical_node); @@ -231,7 +221,6 @@ pub fn build_with_compatible_producers_cache( index, }, owner_accuracy_budget_parts: candidate.accuracy_budget_parts, - deadline, }; let root_id = builder.inventory.implementation_roots_of_group[group][index]; let implemented = Rc::clone( @@ -265,7 +254,6 @@ pub fn build_with_compatible_producers_cache( inventory.query_output_nodes.push(compatible_candidates); } inventory.validate().map_err(|error| invalid(&error))?; - super::check_deadline(deadline)?; Ok(inventory) } @@ -279,7 +267,6 @@ struct ImmediateCandidateBuilder<'a> { owner: CandidateForMemoGroupId, /// The owner's `accuracy_budget_parts`, which its private nodes carry. owner_accuracy_budget_parts: usize, - deadline: Option, } impl ImmediateCandidateBuilder<'_> { @@ -288,7 +275,6 @@ impl ImmediateCandidateBuilder<'_> { node: &Rc, reserved: Option, ) -> Result { - super::check_deadline(self.deadline)?; if let Some(&existing) = self.private_nodes.get(&Rc::as_ptr(node)) { return Ok(existing); } @@ -317,7 +303,6 @@ impl ImmediateCandidateBuilder<'_> { for &candidate in &self.inventory.implementation_roots_of_group[group.0 as usize] { - super::check_deadline(self.deadline)?; let template = &self.inventory.operator_nodes_of_candidates_for_memo_groups [candidate.0] .physical_node; diff --git a/crates/physical-asap-optimization/src/catalog/mod.rs b/crates/physical-asap-optimization/src/catalog/mod.rs index d0566e0ee..70079d0a2 100644 --- a/crates/physical-asap-optimization/src/catalog/mod.rs +++ b/crates/physical-asap-optimization/src/catalog/mod.rs @@ -35,17 +35,6 @@ pub enum ErrorGeneratingPhysicalASAPMemoCandidates { Pruning(String), } -impl ErrorGeneratingPhysicalASAPMemoCandidates { - /// Whether generation stopped at `deadline`, leaving no usable output. - pub fn is_deadline_exceeded(&self) -> bool { - matches!( - self, - Self::Inventory(CandidateInventoryError::DeadlineExceeded(_)) - | Self::Configuration(ConfigurationError::DeadlineExceeded(_)) - ) - } -} - /// Stage 2: implement every operator node of every candidate in `logical`'s /// memo, enumerate the timings and materializations the data and deployment /// allow (`query_time_results_retained`; no query-time raw scans unless @@ -60,22 +49,20 @@ pub fn generate_physical_asap_memo_candidates( data: &DataWorkload, query_time_results_retained: bool, raw_data_retained: bool, - deadline: Option, ) -> Result { let memo = &logical.memo; let output_groups: Vec<_> = asap_types::stages::output_references(&memo.query_roots) .into_iter() .map(|(_, group)| group) .collect(); - let inventory = inventory::build_candidate_inventory_before(memo, &output_groups, deadline)?; - let configured = configurations::enumerate_candidate_configurations_before( + let inventory = inventory::build_candidate_inventory(memo, &output_groups)?; + let configured = configurations::enumerate_candidate_configurations( &inventory, &memo.query_roots, entries, data, query_time_results_retained, raw_data_retained, - deadline, )?; pruning::prune(configured).map_err(ErrorGeneratingPhysicalASAPMemoCandidates::Pruning) } @@ -153,30 +140,19 @@ fn validate_candidates(physical: &PhysicalASAPMemoCandidates) -> Result<(), Stri Ok(()) } -/// Discard partially built catalogs on timeout; they cannot certify coverage. -fn check_deadline( - deadline: Option, -) -> Result<(), asap_types::stages::CandidateGenerationTimeout> { - if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) { - Err(asap_types::stages::CandidateGenerationTimeout) - } else { - Ok(()) - } -} - /// Catalog internals that plan selection's unit tests exercise together with /// its solver. #[cfg(feature = "test-support")] pub mod test_support { pub use super::configurations::{ - compatible_timing, enumerate_candidate_configurations_before, + compatible_timing, enumerate_candidate_configurations, may_keep_across_evaluations_per_sole_reader_query, }; pub use super::factoring::{ factor_terminal_readers, MemoGroupOutputKey, MemoGroupOutputToCompatibleProducersCache, }; pub use super::inventory::{ - build_candidate_inventory_before, build_with_compatible_producers_cache, CandidateInventory, + build_candidate_inventory, build_with_compatible_producers_cache, CandidateInventory, }; pub use super::pruning::{prune, usable_candidates}; pub use super::realize::{ diff --git a/crates/physical-asap-optimization/src/catalog/realize.rs b/crates/physical-asap-optimization/src/catalog/realize.rs index 81d7c39a0..c784723bc 100644 --- a/crates/physical-asap-optimization/src/catalog/realize.rs +++ b/crates/physical-asap-optimization/src/catalog/realize.rs @@ -243,7 +243,6 @@ mod tests { use asap_logical_asap_optimization::test_support::{ entry, lower_promql, sharing_rules_and_shared_state_groups, }; - use asap_logical_asap_optimization::TumblingWindowWidthDomain; use asap_types::ir::properties::AccuracyTarget; use asap_types::stages::{ LogicalASAPMemoCandidates, PhysicalCandidateId, SelectedEdge, SelectedNodeId, @@ -299,13 +298,12 @@ mod tests { .unwrap(); let memo = try_populate_pass1(initial, &per_computation).unwrap(); let logical = LogicalASAPMemoCandidates { - memo: sharing_rules_and_shared_state_groups(memo, TumblingWindowWidthDomain::Complete) - .unwrap(), + memo: sharing_rules_and_shared_state_groups(memo).unwrap(), }; let entries = vec![entry(QueryRecurrence::Unknown)]; let data = DataWorkload::default(); let physical = crate::catalog::generate_physical_asap_memo_candidates( - &logical, &entries, &data, false, true, None, + &logical, &entries, &data, false, true, ) .unwrap(); let selected = physical diff --git a/crates/plan-selection/src/groups/tests.rs b/crates/plan-selection/src/groups/tests.rs index 1ae32f30b..f9c2ce4ee 100644 --- a/crates/plan-selection/src/groups/tests.rs +++ b/crates/plan-selection/src/groups/tests.rs @@ -1,7 +1,6 @@ use std::rc::Rc; use super::*; -use asap_logical_asap_optimization::TumblingWindowWidthDomain; use asap_types::ir::properties::{AccuracyMetric, BoundExpr, ProbabilityExpr}; use asap_types::ir::scalar::expr_ir::ScalarValue; use asap_types::ir::schema::SketchAlgorithm; @@ -547,20 +546,12 @@ struct ThroughStages1And2 { /// through Stage 1 and Stage 2 as the facade runs them, over the complete pane-width domain, with raw data and /// query-time results retained. fn through_stages_1_and_2(queries: &[&str]) -> ThroughStages1And2 { - through_stages_1_and_2_over(queries, TumblingWindowWidthDomain::Complete) -} - -/// [`through_stages_1_and_2`] over the pane-width domain `domain`. -fn through_stages_1_and_2_over( - queries: &[&str], - domain: TumblingWindowWidthDomain, -) -> ThroughStages1And2 { use asap_logical_asap_optimization::test_support::lower_promql; let roots: Vec = queries .iter() .map(|query| QueryRoot::Operator(lower_promql(query, target()))) .collect(); - through_stages_1_and_2_of(roots, domain) + through_stages_1_and_2_of(roots) } /// The accuracy target of [`through_stages_1_and_2`]'s queries. @@ -568,14 +559,11 @@ fn target() -> asap_types::ir::properties::AccuracyTarget { asap_types::ir::properties::AccuracyTarget::max_error(0.01).failure_probability(0.01) } -/// [`through_stages_1_and_2_over`] for query roots `roots`. -fn through_stages_1_and_2_of( - roots: Vec, - domain: TumblingWindowWidthDomain, -) -> ThroughStages1And2 { +/// [`through_stages_1_and_2`] for query roots `roots`. +fn through_stages_1_and_2_of(roots: Vec) -> ThroughStages1And2 { + use asap_logical_asap_optimization::generate_logical_asap_memo_candidates; use asap_logical_asap_optimization::generate_per_computation_logical_memo_candidates; use asap_logical_asap_optimization::test_support::entry_with; - use asap_logical_asap_optimization::{generate_logical_asap_memo_candidates, Pass2RoundState}; use asap_types::workload::{ DurationMs, Predictability, QueryRecurrence, RepeatedDemand, RepetitionInterval, }; @@ -601,14 +589,11 @@ fn through_stages_1_and_2_of( &asap_accuracy::NoAccuracyEvidence, &data, None, - None, ) .unwrap(); - let mut round = Pass2RoundState::new(pass1); - let logical = - generate_logical_asap_memo_candidates(&mut round, &entries, domain, None).unwrap(); + let logical = generate_logical_asap_memo_candidates(pass1, &entries).unwrap(); let physical = asap_physical_asap_optimization::generate_physical_asap_memo_candidates( - &logical, &entries, &data, true, true, None, + &logical, &entries, &data, true, true, ) .unwrap(); // Stage 2 copies each node's `accuracy_budget_parts` from its memo @@ -783,14 +768,11 @@ fn ratio_weights_come_from_the_composition() { // for the whole budget (`α = ε`) never serves the quotient's denominator. #[test] fn one_pane_chain_serves_a_ratio_and_its_operands() { - let stages = through_stages_1_and_2_over( - &[ - "quantile_over_time(0.9, m[5m]) / quantile_over_time(0.5, m[5m])", - "quantile_over_time(0.9, m[5m])", - "quantile_over_time(0.5, m[5m])", - ], - TumblingWindowWidthDomain::SeedGrids, - ); + let stages = through_stages_1_and_2(&[ + "quantile_over_time(0.9, m[5m]) / quantile_over_time(0.5, m[5m])", + "quantile_over_time(0.9, m[5m])", + "quantile_over_time(0.5, m[5m])", + ]); let inputs = prepare_milp_inputs( &stages.physical, &stages.entries, @@ -894,7 +876,7 @@ fn kept_builds_match_fresh_builds(queries: &[&str]) -> MilpInputs { /// [`kept_builds_match_fresh_builds`] for query roots `roots`. fn kept_builds_match_fresh_builds_of(roots: Vec) -> MilpInputs { - let stages = through_stages_1_and_2_of(roots, TumblingWindowWidthDomain::SeedGrids); + let stages = through_stages_1_and_2_of(roots); let prepare = |fresh: bool| { accuracy_positions::FRESH_BUILDS.with(|cell| cell.set(fresh)); let inputs = prepare_milp_inputs( diff --git a/crates/plan-selection/src/milp/model/tests.rs b/crates/plan-selection/src/milp/model/tests.rs index e6809472d..a8ec9f4a0 100644 --- a/crates/plan-selection/src/milp/model/tests.rs +++ b/crates/plan-selection/src/milp/model/tests.rs @@ -85,10 +85,9 @@ fn through_stages_over( ingesting: bool, values_non_negative: bool, ) -> Fixture { + use asap_logical_asap_optimization::generate_logical_asap_memo_candidates; use asap_logical_asap_optimization::generate_per_computation_logical_memo_candidates; use asap_logical_asap_optimization::test_support::{entry_with, lower_promql}; - use asap_logical_asap_optimization::TumblingWindowWidthDomain; - use asap_logical_asap_optimization::{generate_logical_asap_memo_candidates, Pass2RoundState}; use asap_types::ir::properties::AccuracyTarget; use asap_types::workload::{ Predictability, QueryRecurrence, RepeatedDemand, RepetitionInterval, @@ -138,24 +137,15 @@ fn through_stages_over( &asap_accuracy::NoAccuracyEvidence, &data, None, - None, - ) - .unwrap(); - let mut round = Pass2RoundState::new(pass1); - let logical = generate_logical_asap_memo_candidates( - &mut round, - &entries, - TumblingWindowWidthDomain::Complete, - None, ) .unwrap(); + let logical = generate_logical_asap_memo_candidates(pass1, &entries).unwrap(); let physical = asap_physical_asap_optimization::generate_physical_asap_memo_candidates( &logical, &entries, &data, false, raw_data_retained, - None, ) .unwrap(); Fixture { diff --git a/crates/plan-selection/src/report.rs b/crates/plan-selection/src/report.rs index 77fb843ae..e2617b580 100644 --- a/crates/plan-selection/src/report.rs +++ b/crates/plan-selection/src/report.rs @@ -291,9 +291,6 @@ pub enum SearchStatus { /// Why Stage 3 selected no plan. #[derive(Debug, Error)] pub enum ErrorSelectingPlan { - /// Evaluation stopped without proving feasibility or infeasibility. - #[error("candidate evaluation deadline exceeded")] - EvaluationDeadlineExceeded, /// Bad input: the deployment's cost inputs /// ([`CostInputs::validate`](asap_cost::CostInputs::validate)). #[error("cost inputs: {0}")] diff --git a/crates/plan-selection/src/selector.rs b/crates/plan-selection/src/selector.rs index 21a417487..5270385cd 100644 --- a/crates/plan-selection/src/selector.rs +++ b/crates/plan-selection/src/selector.rs @@ -213,23 +213,6 @@ impl<'a> PlanSelector<'a> { /// it if it can still be selected. #[cfg(any(test, feature = "reference-pricer"))] pub fn offer(&mut self, candidate: &PhysicalWorkloadPlan) -> Result<(), ErrorSelectingPlan> { - self.offer_before(candidate, None) - } - - /// A stopped compiler is not evidence that the candidate is invalid. - pub fn offer_before( - &mut self, - candidate: &PhysicalWorkloadPlan, - deadline: Option, - ) -> Result<(), ErrorSelectingPlan> { - let check_deadline = || { - if deadline.is_some_and(|limit| std::time::Instant::now() >= limit) { - Err(ErrorSelectingPlan::EvaluationDeadlineExceeded) - } else { - Ok(()) - } - }; - check_deadline()?; if candidate.roots.len() != self.entries.len() { return Err(ErrorSelectingPlan::EntryCount { candidate: candidate.id.clone(), @@ -284,7 +267,6 @@ impl<'a> PlanSelector<'a> { return Ok(()); } } - check_deadline()?; if !self.keep_every_candidate { for kept in self.kept.drain(..) { self.costs.remove(&kept.id); diff --git a/crates/planner/src/installable.rs b/crates/planner/src/installable.rs index 0ba1079fb..63a89482c 100644 --- a/crates/planner/src/installable.rs +++ b/crates/planner/src/installable.rs @@ -367,11 +367,8 @@ impl InstallablePlan { tumbling_window_retention_ms_by_node: &HashMap, reader_span_ms_by_node: &HashMap, ) -> Result { - let views = dag.set_retention( - tumbling_window_retention_ms_by_node, - reader_span_ms_by_node, - None, - )?; + let views = + dag.set_retention(tumbling_window_retention_ms_by_node, reader_span_ms_by_node)?; check_queries(&dag, &query_texts)?; executor_compiles(&dag, &views)?; let plan = InstallablePlan { diff --git a/crates/planner/src/lib.rs b/crates/planner/src/lib.rs index b6b8eb230..4811832cc 100644 --- a/crates/planner/src/lib.rs +++ b/crates/planner/src/lib.rs @@ -6,7 +6,7 @@ //! ``` //! //! The default pass, [`Stage1ToStage3Pass`], orchestrates the stages -//! ([`run_planning`]): Stage 0 and Pass 1, then per tumbling-window-width round Pass 2, +//! ([`run_planning`]): Stage 0, Pass 1, Pass 2 over every tumbling-window width, //! Stage 2 (`generate_physical_asap_memo_candidates`) and Stage 3 //! (`asap_plan_selection::select_plan`), and builds the selected plan through //! Stage 2's realizer. The stages never call each other. @@ -32,8 +32,8 @@ pub use pass::{ PassNameConflict, PassRegistry, PlanOutput, PlanningModels, QueryPlan, }; pub use run::{ - run_planning, selected_chosen_candidates, PlanSearchSettings, PlanningReport, - PlanningRoundReport, PlanningRun, SelectionError, Stage1ToStage3Pass, + run_planning, selected_chosen_candidates, PlanSearchSettings, PlanningReport, PlanningRun, + SelectionError, Stage1ToStage3Pass, }; mod installable; diff --git a/crates/planner/src/run.rs b/crates/planner/src/run.rs index e2568b7e0..ec86b39a4 100644 --- a/crates/planner/src/run.rs +++ b/crates/planner/src/run.rs @@ -1,33 +1,27 @@ //! The facade's orchestration of the planning stages (#1047): Stage 0 -//! (the memo of logical groups), Pass 1, then per tumbling-window-width round Pass 2 → -//! Stage 2 → Stage 3, and finally the selected plan built through Stage 2's -//! realizer. No stage calls another; the facade owns the deadline and splits -//! it per round (#1048). +//! (the memo of logical groups), Pass 1, Pass 2, Stage 2, Stage 3, and +//! finally the selected plan built through Stage 2's realizer. No stage +//! calls another, and none has a deadline: each runs to completion. use asap_logical_asap_optimization::generate_dag_of_logical_memo_groups; use asap_types::stages::PerComputationLogicalMemoCandidates; use std::collections::HashSet; use std::rc::Rc; -use std::time::{Duration, Instant}; +use std::time::Instant; -use asap_cost::PlanRanking; +use asap_logical_asap_optimization::generate_logical_asap_memo_candidates; use asap_logical_asap_optimization::ChosenCandidatePerMemoGroup; use asap_logical_asap_optimization::DeploymentCapabilitiesError; -use asap_logical_asap_optimization::{ - generate_logical_asap_memo_candidates, LogicalASAPMemoCandidatesError, Pass2RoundState, -}; use asap_logical_asap_optimization::{ generate_per_computation_logical_memo_candidates, PerComputationCandidateError, PerComputationLogicalMemoCandidatesError, }; -use asap_logical_asap_optimization::{TumblingWindowWidthDomain, TumblingWindowWidthFrontier}; use asap_physical_asap_optimization::{ generate_physical_asap_memo_candidates, Stage2AssignmentRealizer, }; use asap_plan_selection::{select_plan, ErrorSelectingPlan, SearchReport, Selection}; use asap_types::ir::schema::schema_support::with_promql_series_identity; -use asap_types::ir::{ASAPOp, NonASAPOp, OperatorNode, QueryRoot, TimeRangeKind}; -use asap_types::stages::DagOfLogicalMemoGroups; +use asap_types::ir::{OperatorNode, QueryRoot}; use asap_types::stages::{ AssignmentRealizer, CandidateForMemoGroupId, DagOfSelectedCandidates, LogicalASAPMemoCandidates, Memo, MemoGroupId, PhysicalASAPMemoCandidates, @@ -40,24 +34,9 @@ use crate::pass::{ OptimizationInput, OptimizationPass, OptimizeError, PlanOutput, PlanningModels, QueryPlan, }; -/// The planning budget when [`PlanSearchSettings::time_limit_ms`] is unset. -const DEFAULT_TIME_LIMIT_MS: u64 = 30_000; -/// The seed-width round's share of the remaining budget, and its cap. -const SEED_ROUND_SHARE: u32 = 4; -const SEED_ROUND_MAX: Duration = Duration::from_secs(5); -/// An intermediate tumbling-window-width round's budget cap. -const INTERMEDIATE_ROUND_MAX: Duration = Duration::from_secs(5); -/// The least budget left after Pass 1 for the seed round to run first. -const SEED_ROUND_MIN_REMAINING: Duration = Duration::from_secs(1); - /// How the facade searches. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct PlanSearchSettings { - /// The whole planning budget, Pass 1 included, in milliseconds; default - /// 30 s. The facade splits it over the tumbling-window-width rounds, and each - /// stage stops at its round's deadline (an evaluation in progress may - /// overrun it). - pub time_limit_ms: Option, /// Keep Pass 1's output for inspection (`stage_pipeline`, the DAG /// viewer): [`PlanningRun::per_computation_candidates`]. Default off: /// production planning copies no memo. @@ -72,10 +51,10 @@ pub enum SelectionError { /// workload. #[error("deployment capabilities: {0}")] InvalidDeploymentCapabilities(DeploymentCapabilitiesError), - /// Pass 1's memo candidates or a round's Pass 2 failed. + /// Pass 1's memo candidates or Pass 2 failed. #[error("logical ASAP-aware optimization: {0}")] LogicalASAPCandidates(String), - /// A round's Stage 2 failed. + /// Stage 2 failed. #[error("physical ASAP-aware optimization: {0}")] PhysicalASAPCandidates(String), /// Stage 3 selected no plan. @@ -84,16 +63,13 @@ pub enum SelectionError { /// Stage 2 could not build the plan Stage 3 selected. #[error("building the selected plan: {0}")] Realization(PhysicalCandidateError), - /// The budget ended before any round selected a plan. - #[error("the planning budget ended before a plan was selected")] - DeadlineReached, } -/// One tumbling-window-width round, as the facade ran it. -#[derive(Debug, Clone, PartialEq, serde::Serialize)] -pub struct PlanningRoundReport { - /// The round's tumbling-window-width domain (`SeedGrids`, `Complete`, ...). - pub tumbling_window_width_domain: String, +/// What the facade did: each stage's counts and time. +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)] +pub struct PlanningReport { + /// Stage 0 and Pass 1, in milliseconds. + pub pass1_ms: f64, /// Pass 2's output: memo groups and their candidates. pub memo_groups: usize, pub logical_candidates: usize, @@ -110,23 +86,6 @@ pub struct PlanningRoundReport { pub stage3_ms: f64, /// Stage 3's search report, when it selected a plan. pub search_report: Option, - /// Why the round selected no plan. - pub error: Option, -} - -/// What the facade did: per stage and round, its counts and timings. -#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)] -pub struct PlanningReport { - /// Stage 0 and Pass 1, in milliseconds. - pub pass1_ms: f64, - pub rounds: Vec, - /// The round whose selection is the plan (index into `rounds`). - pub selected_round: Option, - /// Some legal tumbling-window widths were not searched: the plan is the best over - /// the widths searched, not over all of them. - pub tumbling_window_widths_pending: bool, - /// The whole budget, in milliseconds. - pub time_limit_ms: u64, } /// Every stage's output for one workload ([`run_planning`]). @@ -136,11 +95,10 @@ pub struct PlanningRun { /// candidates and the pruned candidates. /// `None` when Pass 1 failed or without [`PlanSearchSettings::inspect`]. pub per_computation_candidates: Option, - /// The selected round's Pass 2 output (the last round's when none is - /// selected). + /// Pass 2's output, when it ran. pub logical_asap_candidates: Option, - /// The selected round's Stage 2 output (the last round's when none is - /// selected): the catalog `selection`'s candidate ids index. + /// Stage 2's output, when it ran: the catalog `selection`'s candidate + /// ids index. pub physical_asap_candidates: Option, /// Stage 3's selection. pub selection: Result, @@ -152,14 +110,6 @@ pub struct PlanningRun { pub report: PlanningReport, } -/// One round's outputs. -struct Round { - index: usize, - logical: LogicalASAPMemoCandidates, - physical: PhysicalASAPMemoCandidates, - selection: Result, -} - /// Run the planning stages over the frontends' `roots` (query `i` is /// `roots[i]`, with workload entry `entries[i]`). Only a Pass 1 failure is /// an error; a failed selection is reported in the run. @@ -171,15 +121,11 @@ pub fn run_planning( settings: PlanSearchSettings, ) -> Result { let started = Instant::now(); - let time_limit_ms = settings.time_limit_ms.unwrap_or(DEFAULT_TIME_LIMIT_MS); - let global_deadline = started.checked_add(Duration::from_millis(time_limit_ms)); for root in &roots { root.validate_structure()?; } // Stage 0: the memo of logical groups. let initial = generate_dag_of_logical_memo_groups(&roots); - let has_splittable_window = has_splittable_window(&initial); - let has_groups = !initial.groups.is_empty(); // Pass 1: the per-computation candidates the deployment can run and // some query can use. let accuracy_targets: Vec<_> = entries @@ -197,7 +143,6 @@ pub fn run_planning( .deployment_capabilities .supported_summaries .as_deref(), - global_deadline, ); // A query Pass 1 cannot plan is an input error. let pass1 = match pass1 { @@ -205,411 +150,110 @@ pub fn run_planning( pass1 => pass1, }; let mut report = PlanningReport { - time_limit_ms, + pass1_ms: elapsed_ms(started), ..PlanningReport::default() }; - let mut run = PlanningRun { - per_computation_candidates: pass1.as_ref().ok().filter(|_| settings.inspect).cloned(), - logical_asap_candidates: None, - physical_asap_candidates: None, - selection: Err(SelectionError::DeadlineReached), - selected_physical_plan: None, - selected_plan: None, - report: PlanningReport::default(), - }; - if let Err(error) = models.deployment_capabilities.validate(data) { - run.selection = Err(SelectionError::InvalidDeploymentCapabilities(error)); - run.report = report; - return Ok(run); - } - let pass1 = match pass1 { - Ok(pass1) => pass1, - Err(error) => { - // A Pass 1 deadline is the planning budget running out. - run.selection = Err(match error { - PerComputationLogicalMemoCandidatesError::Timeout(_) => { - SelectionError::DeadlineReached - } - error => SelectionError::LogicalASAPCandidates(error.to_string()), - }); - report.pass1_ms = elapsed_ms(started); - run.report = report; - return Ok(run); - } - }; - report.pass1_ms = elapsed_ms(started); - let mut state = Some(Pass2RoundState::new(pass1)); - + let per_computation_candidates = pass1.as_ref().ok().filter(|_| settings.inspect).cloned(); let realizer = Stage2AssignmentRealizer { original_roots: &roots, entries, data, query_time_results_retained: models.deployment_capabilities.query_time_results_retained, }; - let ranking = PlanRanking::of(&models.cost_inputs.cost_model); - let mut best: Option = None; - let mut last: Option = None; - let mut final_error: Option = None; - let mut completed_full_domain = false; - let mut run_round = |domain: TumblingWindowWidthDomain, - deadline: Option, - state: &mut Option, - report: &mut PlanningReport| - -> RoundEnd { - let index = report.rounds.len(); - let outcome = plan_round( - state, domain, deadline, entries, data, &models, &realizer, index, + let mut logical_asap_candidates = None; + let mut physical_asap_candidates = None; + let mut selected_physical_plan = None; + let mut selected_plan = None; + let selection = (|| { + models + .deployment_capabilities + .validate(data) + .map_err(SelectionError::InvalidDeploymentCapabilities)?; + let pass1 = + pass1.map_err(|error| SelectionError::LogicalASAPCandidates(error.to_string()))?; + // Pass 2: local window composition over every legal + // tumbling-window width, then the sharing rules. + let started = Instant::now(); + let logical = generate_logical_asap_memo_candidates(pass1, entries); + report.pass2_ms = elapsed_ms(started); + let logical = logical_asap_candidates.insert( + logical.map_err(|error| SelectionError::LogicalASAPCandidates(error.to_string()))?, ); - report.rounds.push(outcome.report); - let Some(round) = outcome.round else { - return outcome.end; - }; - match &round.selection { - Ok(selection) => { - let replaces = best.as_ref().is_none_or(|best| { - let best = best.selection.as_ref().expect("best round selected"); - !ranking.better(&best.cost, &selection.cost) - }); - if replaces { - best = Some(round); - } else { - last = Some(round); - } - } - Err(_) => last = Some(round), - } - outcome.end - }; - - let remaining = || { - global_deadline.map_or(Duration::MAX, |deadline| { - deadline.saturating_duration_since(Instant::now()) - }) - }; - let seed_enabled = has_splittable_window - && calibration_is_frozen(models.cost_inputs) - && remaining() >= SEED_ROUND_MIN_REMAINING; - if !seed_enabled { - let end = run_round( - TumblingWindowWidthDomain::Complete, - global_deadline, - &mut state, - &mut report, + report.memo_groups = logical.memo.groups.len(); + report.logical_candidates = logical.memo.candidates.iter().map(Vec::len).sum(); + // Stage 2. + let started = Instant::now(); + let physical = generate_physical_asap_memo_candidates( + logical, + entries, + data, + models.deployment_capabilities.query_time_results_retained, + models.deployment_capabilities.raw_data_retained, ); - completed_full_domain = end.completed; - if let Some(error) = end.fatal { - final_error = Some(error); - } - } else { - let seed_deadline = - Instant::now().checked_add((remaining() / SEED_ROUND_SHARE).min(SEED_ROUND_MAX)); - let end = run_round( - TumblingWindowWidthDomain::SeedGrids, - seed_deadline, - &mut state, - &mut report, + report.stage2_ms = elapsed_ms(started); + let physical = physical_asap_candidates.insert( + physical.map_err(|error| SelectionError::PhysicalASAPCandidates(error.to_string()))?, ); - if let Some(error) = end.fatal { - final_error = Some(error); - } - // A literal-only workload has no computation in which tumbling-window rules - // can fire. - let grids = entries + report.operator_nodes = physical.operator_nodes_of_candidates_for_memo_groups.len(); + report.physical_candidates = physical.candidates.len(); + report.physical_producer_links = physical + .candidates .iter() - .filter_map(|entry| entry.evaluation_grid_ms()) - .filter(|_| has_groups); - let mut frontier = TumblingWindowWidthFrontier::new(grids, global_deadline); - while final_error.is_none() && state.is_some() && !remaining().is_zero() { - let domain = match frontier - .as_mut() - .map(|frontier| frontier.next_domain(global_deadline)) - { - Ok(Ok(Some(domain))) => domain, - Ok(Ok(None)) | Ok(Err(_)) | Err(_) => break, - }; - let complete = domain == TumblingWindowWidthDomain::Complete; - let deadline = if complete { - global_deadline - } else { - Instant::now().checked_add(remaining().min(INTERMEDIATE_ROUND_MAX)) - }; - let end = run_round(domain, deadline, &mut state, &mut report); - if let Some(error) = end.fatal { - final_error = Some(error); - } - if complete { - completed_full_domain = end.completed; - break; - } - } - } - report.tumbling_window_widths_pending = !completed_full_domain; - - let (chosen, selected) = match (final_error, best) { - (Some(error), best) => { - run.selection = Err(error); - (last.or(best), false) - } - (None, Some(best)) => (Some(best), true), - (None, None) => { - let mut last = last; - run.selection = Err(last - .as_mut() - .and_then(|round| { - std::mem::replace(&mut round.selection, Err(SelectionError::DeadlineReached)) - .err() - }) - .unwrap_or(SelectionError::DeadlineReached)); - (last, false) - } - }; - let Some(round) = chosen else { - run.report = report; - return Ok(run); - }; - if let (true, Ok(selection)) = (selected, round.selection) { - report.selected_round = Some(round.index); - match realizer.realize(&round.physical, &selection.selected_dag) { - Ok(realized) => { - let proven: HashSet<*const OperatorNode> = selection - .nodes_with_proven_non_negative_weights - .iter() - .filter_map(|node| realized.operator_node_of_selected_node.get(node.0)) - .map(Rc::as_ptr) - .collect(); - run.selected_plan = Some(with_confirmed_weights_at( - &realized.plan.roots, - data, - &|node| proven.contains(&Rc::as_ptr(node)), - )); - run.selected_physical_plan = Some(realized); - run.selection = Ok(selection); - } - Err(error) => run.selection = Err(SelectionError::Realization(error)), - } - } - run.logical_asap_candidates = Some(round.logical); - run.physical_asap_candidates = Some(round.physical); - run.report = report; - Ok(run) -} - -/// How a round ended, for the round loop. -struct RoundEnd { - /// The round ran Pass 2, Stage 2 and Stage 3 to the end (with or - /// without a plan). - completed: bool, - /// An error that ends planning: the complete catalog is infeasible, or - /// a stage failed for a reason other than its deadline. - fatal: Option, -} - -struct RoundOutcome { - round: Option, - report: PlanningRoundReport, - end: RoundEnd, -} - -/// One tumbling-window-width round: Pass 2 over `state` for `domain`, Stage 2, then -/// Stage 3, each stopping at `deadline`. A Pass 2 timeout in local window -/// composition leaves `state` unusable, so it is dropped. -#[allow(clippy::too_many_arguments)] -fn plan_round( - state: &mut Option, - domain: TumblingWindowWidthDomain, - deadline: Option, - entries: &[QueryWorkloadEntry], - data: &DataWorkload, - models: &PlanningModels<'_>, - realizer: &dyn AssignmentRealizer, - index: usize, -) -> RoundOutcome { - let mut report = PlanningRoundReport { - tumbling_window_width_domain: format!("{domain:?}"), - memo_groups: 0, - logical_candidates: 0, - operator_nodes: 0, - physical_candidates: 0, - physical_producer_links: 0, - stage2_pruned_candidates: 0, - pass2_ms: 0.0, - stage2_ms: 0.0, - stage3_ms: 0.0, - search_report: None, - error: None, - }; - let stopped = |report: PlanningRoundReport, fatal: Option| RoundOutcome { - round: None, - report, - end: RoundEnd { - completed: false, - fatal, - }, - }; - let Some(round_state) = state.as_mut() else { - report.error = Some("Pass 2's round state was dropped".into()); - return stopped(report, None); - }; - let started = Instant::now(); - let logical = generate_logical_asap_memo_candidates(round_state, entries, domain, deadline); - report.pass2_ms = elapsed_ms(started); - let logical = match logical { - Ok(logical) => logical, - Err(error) => { - report.error = Some(error.to_string()); - if matches!( - error, - LogicalASAPMemoCandidatesError::LocalWindowComposition(_) - ) { - *state = None; - } - let fatal = - (!past(deadline)).then(|| SelectionError::LogicalASAPCandidates(error.to_string())); - return stopped(report, fatal); - } - }; - report.memo_groups = logical.memo.groups.len(); - report.logical_candidates = logical.memo.candidates.iter().map(Vec::len).sum(); - let started = Instant::now(); - let physical = generate_physical_asap_memo_candidates( - &logical, - entries, - data, - models.deployment_capabilities.query_time_results_retained, - models.deployment_capabilities.raw_data_retained, - deadline, - ); - report.stage2_ms = elapsed_ms(started); - let physical = match physical { - Ok(physical) => physical, - Err(error) => { - report.error = Some(error.to_string()); - let fatal = (!error.is_deadline_exceeded()) - .then(|| SelectionError::PhysicalASAPCandidates(error.to_string())); - return stopped(report, fatal); - } - }; - report.operator_nodes = physical.operator_nodes_of_candidates_for_memo_groups.len(); - report.physical_candidates = physical.candidates.len(); - report.physical_producer_links = physical - .candidates - .iter() - .flat_map(|candidate| &candidate.producer_candidates_per_input) - .map(|producers| producers.len()) - .sum(); - report.stage2_pruned_candidates = physical.pruned_candidates.len(); - let started = Instant::now(); - let selection = select_plan( - &physical, - entries, - data, - &models.accuracy_check_inputs(), - models.cost_inputs, - realizer, - ); - report.stage3_ms = elapsed_ms(started); - let fatal = match &selection { - Ok(selection) => { - report.search_report = Some(selection.search_report.clone()); - None - } - Err(error) => { - report.error = Some(error.to_string()); - match error { - ErrorSelectingPlan::EvaluationDeadlineExceeded => None, - // An infeasible restricted catalog does not make the complete - // one infeasible. - ErrorSelectingPlan::NoValidCandidate(_) - if domain != TumblingWindowWidthDomain::Complete => - { - None - } - error => Some(SelectionError::Selection(clone_error(error))), - } - } - }; - RoundOutcome { - round: Some(Round { - index, - logical, + .flat_map(|candidate| &candidate.producer_candidates_per_input) + .map(|producers| producers.len()) + .sum(); + report.stage2_pruned_candidates = physical.pruned_candidates.len(); + // Stage 3. + let started = Instant::now(); + let selection = select_plan( physical, - selection: selection.map_err(SelectionError::Selection), - }), + entries, + data, + &models.accuracy_check_inputs(), + models.cost_inputs, + &realizer, + ); + report.stage3_ms = elapsed_ms(started); + let selection = selection.map_err(SelectionError::Selection)?; + report.search_report = Some(selection.search_report.clone()); + // The selected plan, as Stage 2 builds it. + let realized = realizer + .realize(physical, &selection.selected_dag) + .map_err(SelectionError::Realization)?; + let proven: HashSet<*const OperatorNode> = selection + .nodes_with_proven_non_negative_weights + .iter() + .filter_map(|node| realized.operator_node_of_selected_node.get(node.0)) + .map(Rc::as_ptr) + .collect(); + selected_plan = Some(with_confirmed_weights_at( + &realized.plan.roots, + data, + &|node| proven.contains(&Rc::as_ptr(node)), + )); + selected_physical_plan = Some(realized); + Ok(selection) + })(); + Ok(PlanningRun { + per_computation_candidates, + logical_asap_candidates, + physical_asap_candidates, + selection, + selected_physical_plan, + selected_plan, report, - end: RoundEnd { - completed: true, - fatal, - }, - } -} - -/// `error` again, for the fatal-error record (the round keeps the original). -fn clone_error(error: &ErrorSelectingPlan) -> ErrorSelectingPlan { - match error { - ErrorSelectingPlan::NoValidCandidate(rejected) => { - ErrorSelectingPlan::NoValidCandidate(rejected.clone()) - } - ErrorSelectingPlan::RealizerRejected(reason) => { - ErrorSelectingPlan::RealizerRejected(reason.clone()) - } - ErrorSelectingPlan::UnsupportedCostModel(model) => { - ErrorSelectingPlan::UnsupportedCostModel(model.clone()) - } - &ErrorSelectingPlan::ObjectiveDiffersFromPlanCost { - objective_per_second, - plan_cost_per_second, - } => ErrorSelectingPlan::ObjectiveDiffersFromPlanCost { - objective_per_second, - plan_cost_per_second, - }, - ErrorSelectingPlan::SelectedPlanFailedCheck { cause, reason } => { - ErrorSelectingPlan::SelectedPlanFailedCheck { - cause: *cause, - reason: reason.clone(), - } - } - error => ErrorSelectingPlan::Solver(error.to_string()), - } -} - -fn past(deadline: Option) -> bool { - deadline.is_some_and(|deadline| Instant::now() >= deadline) + }) } fn elapsed_ms(started: Instant) -> f64 { started.elapsed().as_secs_f64() * 1000.0 } -/// Whether Stage 3 prices with one fixed calibration factor: a frozen factor -/// is given, or no operator is measured. Only then is the seed-width round -/// comparable with later rounds. -fn calibration_is_frozen(cost_inputs: &asap_cost::CostInputs) -> bool { - cost_inputs.frozen_calibration_factor.is_some() - || cost_inputs.measured_operation_costs.operators.is_empty() -} - -/// Tumbling-window rules require a temporal range below a summary build: a positive -/// `Range` window, or an existing tumbling-window chain. Instant lookback selectors -/// have no range to split into tumbling windows. -fn has_splittable_window(initial: &DagOfLogicalMemoGroups) -> bool { - initial.groups.iter().any(|group| { - matches!( - group.logical_node.non_asap(), - Some(NonASAPOp::TimeRange { - kind: TimeRangeKind::Range, range, .. - }) if !range.is_zero() - ) || matches!( - group.logical_node.asap(), - Some(ASAPOp::SummaryTumblingWindowChain { .. }) - ) - }) -} - /// The selected plan's memo candidate for each group it uses: the memo /// candidate whose root each selected node's candidate is. Factoring may give several /// memo candidates of a group one root (identical terminal readers of /// different inputs); the one reading the selected producers' groups is taken. -/// `memo` and `physical` are the selected round's +/// `memo` and `physical` are the run's /// ([`PlanningRun::logical_asap_candidates`]'s memo and /// [`PlanningRun::physical_asap_candidates`]); `selected` is the selection's /// `selected_dag`. An error says why the plan is not one candidate per group. diff --git a/crates/planner/tests/columnar_batches.rs b/crates/planner/tests/columnar_batches.rs index 83a378807..658dee91b 100644 --- a/crates/planner/tests/columnar_batches.rs +++ b/crates/planner/tests/columnar_batches.rs @@ -30,7 +30,6 @@ use time_series::{series_identity, RawDataSample}; const EVALUATION_MS: i64 = 3_600_000; #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_tumbling_window_is_built_from_arrow_columns_with_one_label_entry_per_series() { // What the user writes: p99 of `latency_ms` over the last hour, every // 10 minutes, at rank error 0.01, over 1,000 series sampled every second. diff --git a/crates/planner/tests/e2e_plan.rs b/crates/planner/tests/e2e_plan.rs index 4ea34e373..67fa65135 100644 --- a/crates/planner/tests/e2e_plan.rs +++ b/crates/planner/tests/e2e_plan.rs @@ -86,7 +86,6 @@ fn sql_workload( /// `QueryWorkload::entries()` order, without the caller touching any stage's /// candidates. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn plans_every_query_in_entry_order() { let workload = sql_workload( vec![ @@ -110,7 +109,6 @@ async fn plans_every_query_in_entry_order() { /// The facade selects what the planning stages select over the frontends' /// roots ([`run_planning`]), and reports that selection. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn facade_plans_match_run_planning() { let workload = sql_workload( vec![ @@ -181,7 +179,6 @@ async fn facade_plans_match_run_planning() { /// `query_batch` alone, so driving the frontend through it would drop the /// repeating entries. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn lowers_repeating_sql_entries_too() { let workload = sql_workload( vec![batch("SELECT COUNT(DISTINCT l_orderkey) FROM lineitem")], @@ -300,7 +297,6 @@ async fn harness_rejects_a_pass_that_mislabels_entry_indices() { /// A frontend input that cannot lower the workload's language fails before any /// query is parsed. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn rejects_a_frontend_that_does_not_match_the_workload_language() { let workload = sql_workload( vec![batch("SELECT COUNT(DISTINCT l_orderkey) FROM lineitem")], @@ -317,7 +313,6 @@ async fn rejects_a_frontend_that_does_not_match_the_workload_language() { /// A repeating PromQL query yields one plan carrying its selected DAG root. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn each_plan_carries_its_selected_root() { let workload = PlanningWorkload { query_workload: QueryWorkload { @@ -351,7 +346,6 @@ async fn each_plan_carries_its_selected_root() { /// Scalar-only and mixed workloads preserve entry bindings without wrapper nodes. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn scalar_roots_survive_planning_in_workload_order() { for queries in [ vec!["2", "time()"], diff --git a/crates/planner/tests/per_sample_updater.rs b/crates/planner/tests/per_sample_updater.rs index 2350741f1..b8f83f1e5 100644 --- a/crates/planner/tests/per_sample_updater.rs +++ b/crates/planner/tests/per_sample_updater.rs @@ -24,7 +24,6 @@ use time_series::{series_identity, series_labels, time_series_tables, RawDataSam const EVALUATION_MS: i64 = 3_600_000; #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn per_sample_updates_build_the_materialized_tumbling_window_the_plan_refreshes() { // What the user writes: the 5-minute rate of a counter, every minute, // exactly, over 1,000 series sampled every 15 s. @@ -249,7 +248,6 @@ async fn answers( /// from them: the same answers as the plan's query-time reference over the /// raw data samples. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn tumbling_windows_installed_from_per_sample_states_answer_the_query() { use asap_executor::arrow::array::{DictionaryArray, TimestampMillisecondArray, UInt32Array}; use asap_executor::arrow::datatypes::UInt32Type; diff --git a/crates/planner/tests/runtime_install.rs b/crates/planner/tests/runtime_install.rs index 4c09a1b99..7b952acb2 100644 --- a/crates/planner/tests/runtime_install.rs +++ b/crates/planner/tests/runtime_install.rs @@ -614,7 +614,6 @@ fn tumbling_window_sketch(plan: &InstallablePlan, id: usize) -> SketchAlgorithm /// the one a run of its view alone builds, and both queries answer as the /// query-time reference. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn tumbling_windows_of_one_refresh_window_read_each_raw_window_once() { let entry = |query: &str, metric: &str| { format!( @@ -710,7 +709,6 @@ async fn tumbling_windows_of_one_refresh_window_read_each_raw_window_once() { /// in the query run. One p99 per series, read from the hour's oldest tumbling window, /// equal to the query-time reference; the other metric is not read. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_runtime_installs_and_runs_the_plan_from_one_facade_call() { let plan_json = plan_json(&["quantile_over_time(0.99, latency_ms[60m])"]).await; let run = run_data_plane( @@ -735,7 +733,6 @@ async fn a_runtime_installs_and_runs_the_plan_from_one_facade_call() { /// add up to Stage 3's selection cost and their CPU to the plan's, with /// one latency per query, each node naming the installed nodes that run it. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn the_plan_carries_stage3s_predicted_usage() { let (plan, selection) = planned_with_report(&["quantile_over_time(0.99, latency_ms[60m])"]).await; @@ -801,7 +798,6 @@ async fn the_plan_carries_stage3s_predicted_usage() { /// The query's label matcher selects series `a` only: the runtime feeds /// every sample, and the window's selector applies the plan's `Scan`. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn label_matchers_select_the_series_a_tumbling_window_reads() { let plan_json = plan_json(&[r#"quantile_over_time(0.99, latency_ms{instance="a"}[60m])"#]).await; @@ -823,7 +819,6 @@ async fn label_matchers_select_the_series_a_tumbling_window_reads() { /// state key once, independent of the optimizer's tumbling-window widths. Both answers /// equal the reference. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn an_hour_and_its_offset_twin_both_equal_the_reference() { let plan_json = plan_json(&[ "quantile_over_time(0.99, latency_ms[60m])", @@ -858,7 +853,6 @@ async fn an_hour_and_its_offset_twin_both_equal_the_reference() { /// 20 s tumbling windows end only on the first's, so the second would read /// `(t − 605 s, t − 5 s]` instead of `(t − 600 s, t]`. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn queries_at_two_phases_read_tumbling_windows_on_both_schedules() { assert_phased_copies_answer_as_reference(&[0, 5_000]).await; } @@ -867,7 +861,6 @@ async fn queries_at_two_phases_read_tumbling_windows_on_both_schedules() { /// epoch and the plan carries no tumbling-window origin, so 20 s tumbling windows would end 5 s /// before every evaluation: the tumbling windows must also end on the phase itself. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn queries_at_one_nonzero_phase_read_tumbling_windows_anchored_at_the_epoch() { assert_phased_copies_answer_as_reference(&[5_000, 5_000]).await; } @@ -956,7 +949,6 @@ async fn assert_phased_copies_answer_as_reference(phases_ms: &[i64]) { /// every evaluation without raw data. `quantile_over_time(0.99, m[5m])` /// every 5 minutes at rank error 0.01 (a KLL per series) had no plan. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_window_equal_to_its_interval_is_kept_at_ingestion_time() { assert_answers_as_reference_without_raw_data( "quantile_over_time(0.99, m[5m])", @@ -978,7 +970,6 @@ async fn a_window_equal_to_its_interval_is_kept_at_ingestion_time() { /// epoch grid would end 5 s before every evaluation, so the plan must not /// read one; it still answers every evaluation as the reference. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_window_equal_to_its_interval_at_an_offset_phase_answers_as_reference() { assert_answers_as_reference_without_raw_data( "sum by (job) (sum_over_time(m[20s]))", @@ -1079,7 +1070,6 @@ async fn assert_answers_as_reference_without_raw_data( /// (#1173). Every evaluation of both copies over one minute answers as the /// query-time reference. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn queries_at_two_intervals_read_chains_on_their_own_grids() { for query in [ "sum by (job) (rate(m[1m]))", @@ -1169,7 +1159,6 @@ async fn assert_copies_at_intervals_answer_as_reference(query: &str, intervals_m /// A plan file of another DAG wire version is refused, and so is the /// reference of a tampered plan file, with an error rather than a panic. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_plan_file_of_another_version_or_tampered_is_refused() { let plan_json = plan_json(&["quantile_over_time(0.99, latency_ms[60m])"]).await; let mut other_version: InstallablePlan = serde_json::from_str(&plan_json).unwrap(); @@ -1196,7 +1185,6 @@ async fn a_plan_file_of_another_version_or_tampered_is_refused() { /// sample in its 1 s lookback counts. Series `a` has samples at T-900 ms (1) /// and T-100 ms (10), series `b` at T-200 ms (100): the sum is 10 + 100. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn an_instant_selector_reads_the_latest_sample_per_series() { let workload = workload(&["sum(latency_ms)"]); let plan = plan_installable(UserInput::new( @@ -1241,7 +1229,6 @@ async fn an_instant_selector_reads_the_latest_sample_per_series() { /// evaluation's answers (each series' newest-tumbling-window maximum) equal the /// reference. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn query_time_results_kept_across_evaluations_are_built_in_query_runs() { let capabilities = DeploymentCapabilities { query_time_results_retained: true, @@ -1317,7 +1304,6 @@ async fn query_time_results_kept_across_evaluations_are_built_in_query_runs() { /// is not read. Per series, p99 interpolates between its two largest /// samples (rank 0.99 × 9 = 8.91): 9.91 for `a`, 99.1 for `b`, 5 for `c`. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn an_exact_per_series_quantile_runs_from_raw_data_samples() { let workload: PlanningWorkload = serde_json::from_str( r#"{"query_workload": {"language": "promql", "repeating_queries": [{ @@ -1442,7 +1428,6 @@ fn promql_quantile(q: f64, values: &mut [f64]) -> f64 { /// raw data at query time. Run on data, each series' ratio is within 0.05 /// of the exact ratio of its hour's samples. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_ratio_of_two_quantiles_runs_from_ddsketches_within_its_relative_target() { let query = "quantile_over_time(0.9, latency_ms[60m]) / quantile_over_time(0.5, latency_ms[60m])"; @@ -1493,7 +1478,7 @@ async fn a_ratio_of_two_quantiles_runs_from_ddsketches_within_its_relative_targe /// for its strictest reader, the ratio's share; the plan still reads no raw /// data samples. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] +#[ignore = "slow without a planning budget: over 30 min in a debug build; #1197"] async fn a_quantile_shared_with_a_ratio_is_sized_for_the_ratio() { let capabilities = DeploymentCapabilities { raw_data_retained: false, @@ -1696,7 +1681,6 @@ fn relative_time(from_ms: i64, to_ms: i64) -> Vec { /// in place of the maximum rolls up from the fine sum instead: /// [`a_coarse_sum_rolls_up_from_the_stored_fine_sum`].) #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn group_outputs_from_one_stored_per_series_sum_are_listed_and_built() { let workload = exact_workload( &[ @@ -1755,7 +1739,6 @@ async fn group_outputs_from_one_stored_per_series_sum_are_listed_and_built() { /// stores two outputs, the second reading only the first. Built in that /// order, they answer as the query-time reference. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_coarse_sum_rolls_up_from_the_stored_fine_sum() { let workload = exact_workload( &[ @@ -1794,7 +1777,6 @@ async fn a_coarse_sum_rolls_up_from_the_stored_fine_sum() { /// stored per-series maxima, which the per-series query reads too: listed /// after them with their time selection, under a definition of its own. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_summary_across_series_from_per_series_summaries_is_listed_and_built() { let workload = exact_workload( &[ @@ -1845,7 +1827,6 @@ async fn a_summary_across_series_from_per_series_summaries_is_listed_and_built() /// from the stored tumbling windows it answers as the reference, and a minute later /// only the newest tumbling window and the new merge are refreshed. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_kept_sliding_window_merge_is_listed_and_built() { let capabilities = DeploymentCapabilities { query_time_results_retained: true, @@ -1913,7 +1894,6 @@ async fn a_kept_sliding_window_merge_is_listed_and_built() { /// each with its own time selection; each reads its tumbling window of /// raw data samples and nothing stored. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn tumbling_windows_of_an_hour_share_one_definition_with_distinct_times() { let plan: InstallablePlan = serde_json::from_str(&plan_json(&["quantile_over_time(0.99, latency_ms[60m])"]).await) @@ -1957,7 +1937,6 @@ async fn tumbling_windows_of_an_hour_share_one_definition_with_distinct_times() /// Series `e1`..`e3` are in `east` with sums 6, 60 and 30; `w1`, `w2` in /// `west` with sums 9 and 3. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn topk_answers_carry_the_chosen_series_labels() { let workload: PlanningWorkload = serde_json::from_str( r#"{"query_workload": {"language": "promql", "repeating_queries": [{ @@ -2040,7 +2019,6 @@ fn assert_full_series_answers(run: &DataPlaneRun, expected: usize) { /// rates, answers whole input series: `topk(2, ...)` the two of five, /// `by (region)` per region, `bottomk` likewise. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn every_topk_form_answers_whole_series() { for (query, expected) in [ ("topk(2, sum_over_time(data[1m]))", 2), @@ -2114,7 +2092,6 @@ async fn plan_p99_avg( /// selected, its answers equal the query-time reference's and are within /// 5% of the exact averages. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_non_negative_gauges_merged_ddsketch_tumbling_windows_carry_the_relative_bound_through_avg( ) { let (plan, selection) = plan_p99_avg(true).await.expect("a valid plan"); @@ -2235,7 +2212,6 @@ async fn a_non_negative_gauges_merged_ddsketch_tumbling_windows_carry_the_relati /// reason (`NoErrorBoundForQueryTarget`) and Stage 2 the exact candidates, /// which scan raw data at query time. #[tokio::test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] async fn an_undeclared_gauges_merged_ddsketch_tumbling_windows_get_no_relative_bound_through_avg() { let error = plan_p99_avg(false).await.expect_err("no valid plan"); assert_plan_selection_selected_none(&error); @@ -2283,10 +2259,7 @@ fn pass1_pruned_for_no_error_bound( &entries, &data, models, - PlanSearchSettings { - time_limit_ms: Some(5_000), - inspect: true, - }, + PlanSearchSettings { inspect: true }, ) .unwrap(); run.per_computation_candidates @@ -2301,14 +2274,11 @@ fn pass1_pruned_for_no_error_bound( .collect() } -/// `error` says plan selection found no valid plan: no candidate is valid, -/// or none was validated by the deadline. Not a solver, realizer or input -/// failure. +/// `error` says plan selection found no valid plan: no candidate is valid. +/// Not a solver, realizer or input failure. fn assert_plan_selection_selected_none(error: &str) { assert!( - error.contains("plan selection: no valid candidate") - || error.contains("plan selection: no plan by the deadline") - || error.contains("the planning budget ended before a plan was selected"), + error.contains("plan selection: no valid candidate"), "{error}" ); } @@ -2361,7 +2331,6 @@ fn grouped_samples() -> Vec { /// reference. Series of `data` carry `region`, `service` and `job`; values /// are positive. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn quantile_queries_with_different_groupings_share_one_per_series_sketch() { let plan_json = jointly_planned( &[ @@ -2455,7 +2424,6 @@ fn exact_average_of_p99s(samples: &[RawDataSample]) -> BTreeMap { /// a guarantee, and it runs: equal to its query-time reference and within a /// loose tolerance of the exact answer. #[tokio::test] -#[ignore = "flaky: depends on wall-clock search budgets (#1067)"] async fn ignoring_accuracy_guarantees_selects_a_sketch_plan_that_runs() { let workload = average_of_p99s_workload(); let capabilities = DeploymentCapabilities { @@ -2561,7 +2529,6 @@ async fn ignoring_accuracy_guarantees_selects_a_sketch_plan_that_runs() { /// `ignore_accuracy_guarantees`, and the report marks its output as /// accepted without a guarantee. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn ignoring_accuracy_guarantees_plans_the_undeclared_p99_query() { let capabilities = DeploymentCapabilities { raw_data_retained: false, @@ -2714,7 +2681,6 @@ fn assert_p99s_within_alpha( /// equals the query-time reference's and is within the relative error 0.05 /// of the exact average of PromQL's interpolated p99s. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_ddsketch_over_a_gauge_reporting_zero_runs_within_alpha() { let (plan, _) = plan_p99("avg", true, false).await; let views = plan.materialized_views().unwrap(); @@ -2740,7 +2706,6 @@ async fn a_ddsketch_over_a_gauge_reporting_zero_runs_within_alpha() { /// within 0.05 of the exact value, including series whose samples are all /// negative. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_ddsketch_over_signed_samples_runs_within_alpha() { let (plan, report) = plan_p99("max", false, true).await; let views = plan.materialized_views().unwrap(); @@ -2821,7 +2786,6 @@ fn counter_raw_data_samples() -> Vec { /// executor compiles; it runs, equal to its query-time reference and to the /// exact top 10 jobs (jobs 3..=12, each the sum of its two series). #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn ignoring_guarantees_counter_topk_plans_compile_and_answer_exactly() { let capabilities = DeploymentCapabilities { raw_data_retained: false, @@ -2895,7 +2859,6 @@ async fn ignoring_guarantees_counter_topk_plans_compile_and_answer_exactly() { /// refuses: the counter top-k plan with its answer column's type changed /// from Float64 to Utf8 does not compile, with the executor's reason. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn check_compiles_reports_the_executors_error() { let capabilities = DeploymentCapabilities { raw_data_retained: false, @@ -2988,7 +2951,6 @@ fn stored_definitions(plan: &InstallablePlan, family: &str) -> BTreeSet /// two queries' scans read), so every stored Sum has one definition, and /// both answer as the query-time reference. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn fine_and_coarse_sums_store_one_sum() { let plan_json = jointly_planned( &[ @@ -3018,7 +2980,6 @@ async fn fine_and_coarse_sums_store_one_sum() { /// per-series sums keeps each chosen series' labels, and per-series /// DDSketch quantiles keep every series' labels. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn per_series_answers_beside_a_grouping_share_one_state_and_answer_as_the_reference() { for (queries, family, accuracy, answers) in [ ( @@ -3305,7 +3266,7 @@ async fn assert_forced_heap_answers( /// labels, within its bound of their exact sums. (A Count-Min heap needs /// the weights proven non-negative.) #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] +#[ignore = "fails on main too: Stage 3 rejects the forced heap plan (SelectedPlanFailedCheck, AccuracyTargetMissed); not a wall-clock issue; #1015"] async fn a_forced_heap_topk_sum_plan_compiles_and_answers_within_its_bound() { let workload = heap_topk_workload( "topk by (region) (3, sum_over_time(data[1m]))", @@ -3353,7 +3314,7 @@ fn skewed_count_samples() -> (Vec, BTreeMap Vec { /// the true top 3 jobs, with their `job` labels, within its bound of their /// exact sums. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] +#[ignore = "fails on main too: Stage 3 rejects the forced heap plan (SelectedPlanFailedCheck, AccuracyTargetMissed); not a wall-clock issue; #1015"] async fn a_forced_heap_counter_topk_plan_compiles_and_answers_within_its_bound() { let samples = skewed_counter_raw_data_samples(); for (function, seconds) in [("increase", 300.0), ("rate", 1.0)] { @@ -3482,7 +3443,7 @@ fn skewed_region_raw_data_samples() -> ( /// A Count-Min heap over sample values needs the gauge declared /// non-negative; a Count Sketch heap takes any gauge. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] +#[ignore = "fails on main too: Stage 3 rejects the forced heap plan (SelectedPlanFailedCheck, AccuracyTargetMissed); not a wall-clock issue; #1015"] async fn a_forced_heap_grouped_topk_sum_plan_compiles_and_answers_within_its_bound() { let (samples, sums, counts) = skewed_region_raw_data_samples(); let gauge = r#""metric_type": "gauge""#; @@ -3649,7 +3610,6 @@ async fn bucket_plan_json(queries: &[&str]) -> String { /// 0.98. The result has no labels: `sum by (le)` keeps only `le`, which /// the quantile drops. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn classic_histogram_quantiles_run_with_prometheus_interpolation() { let selector = format!(r#"rate({BUCKET_METRIC}{{service="orders"}}[5m])"#); let queries: Vec = [0.5, 0.95, 0.99] @@ -3683,7 +3643,6 @@ async fn classic_histogram_quantiles_run_with_prometheus_interpolation() { /// empty result. Over one series' buckets, without `sum`, the result keeps /// that series' labels but `le` (and the name `rate` dropped). #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_classic_histogram_quantile_keeps_labels_and_handles_inf_and_empty_input() { let by_job = format!("histogram_quantile(0.99, sum by (job, le) (rate({BUCKET_METRIC}[5m])))"); let alert = format!("{by_job} > 0.5"); @@ -3770,7 +3729,6 @@ fn m_sample(labels: &[(&str, &str)], ago_ms: i64, value: f64) -> RawDataSample { /// (0 included). A series whose newest sample is the stale marker has /// ended, so `absent` answers 1 again. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn absent_answers_as_prometheus_through_the_installed_plan() { let query = r#"absent(m{job="a",instance!="3",env!="prod"})"#; let one = vec![(BTreeMap::from([("job".to_string(), "a".to_string())]), 1.0)]; @@ -3803,7 +3761,6 @@ async fn absent_answers_as_prometheus_through_the_installed_plan() { /// `absent(m)` (no matcher) synthesizes a label-less sample; the missing-job /// alert shape (several equality matchers) keeps every matcher's label. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn absent_labels_come_from_the_equality_matchers_only() { assert_eq!( exact_answers("absent(m)", &[]).await, diff --git a/crates/planner/tests/sql_per_group_topk.rs b/crates/planner/tests/sql_per_group_topk.rs index d749556f5..d4ea3a199 100644 --- a/crates/planner/tests/sql_per_group_topk.rs +++ b/crates/planner/tests/sql_per_group_topk.rs @@ -302,7 +302,6 @@ where /// The exact per-city top 5 compiles and answers each city's top hosts /// with their counts, `sf` with its two. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn an_exact_sql_per_city_top_5_runs() { let plan = plan_sql(&[TOP_5_HOSTS_PER_CITY], EXACT).await; plan.check_compiles().unwrap(); @@ -313,7 +312,6 @@ async fn an_exact_sql_per_city_top_5_runs() { /// SQL's `ROW_NUMBER` per-city top 5 and PromQL's `topk by (city)` answer /// the same rows over the same data. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn sql_and_promql_per_city_top_5_answer_alike() { let sql = ranked_rows( &run_sql(&plan_sql(&[TOP_5_HOSTS_PER_CITY], EXACT).await, 0), @@ -329,7 +327,6 @@ async fn sql_and_promql_per_city_top_5_answer_alike() { /// The global top 5 answers its hosts, not only their counts. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn an_exact_sql_top_5_answers_its_hosts() { let plan = plan_sql(&[TOP_5_HOSTS], EXACT).await; let answered: BTreeSet<(String, i64)> = ranked_rows(&run_sql(&plan, 0), "topk_5") @@ -345,7 +342,6 @@ async fn an_exact_sql_top_5_answers_its_hosts() { /// installs, so the executor refreshes every materialized view it keeps and /// compiles its query run. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn the_selected_approximate_plan_installs_and_compiles() { let plan = plan_sql(&[TOP_5_HOSTS, TOP_5_HOSTS_PER_CITY], APPROXIMATE).await; plan.check_compiles().unwrap(); @@ -357,7 +353,6 @@ async fn the_selected_approximate_plan_installs_and_compiles() { /// (each view's refresh, then the query run) and refuses it with the /// executor's reason rather than returning a plan a runtime cannot run. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_plan_the_executor_cannot_run_is_refused_at_installation() { let with_rn = TOP_5_HOSTS_PER_CITY.replace("SELECT city, host, c FROM", "SELECT city, host, c, rn FROM"); @@ -392,7 +387,6 @@ async fn a_plan_the_executor_cannot_run_is_refused_at_installation() { /// the count then the host (a tie-breaking second key), and with a SELECT /// list that drops the partition column. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn generic_per_city_top_5_forms_run_exactly() { let tie_broken = TOP_5_HOSTS_PER_CITY.replace("COUNT(*) DESC)", "COUNT(*) DESC, host)"); let plan = plan_sql(&[&tie_broken], EXACT).await; diff --git a/crates/planner/tests/sql_table_scans.rs b/crates/planner/tests/sql_table_scans.rs index eb601dc96..69dd65cf4 100644 --- a/crates/planner/tests/sql_table_scans.rs +++ b/crates/planner/tests/sql_table_scans.rs @@ -259,7 +259,6 @@ fn expected_distinct_users() -> BTreeMap { } #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_distinct_count_over_an_int64_id_in_a_now_window_runs_from_a_sketch() { let plan = plan(DISTINCT_USERS_LAST_5_MINUTES, APPROXIMATE).await; let selected = format!("{:?}", plan.dag.nodes); @@ -278,7 +277,6 @@ async fn a_distinct_count_over_an_int64_id_in_a_now_window_runs_from_a_sketch() } #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn an_exact_distinct_count_over_an_int64_id_in_a_now_window_runs() { let plan = plan(DISTINCT_USERS_LAST_5_MINUTES, EXACT).await; let (fields, rows) = run(&plan); @@ -291,7 +289,6 @@ async fn an_exact_distinct_count_over_an_int64_id_in_a_now_window_runs() { } #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn now_is_the_evaluation_time() { let plan = plan( "SELECT service, COUNT(*) AS value FROM http \ @@ -317,7 +314,6 @@ fn percentile_cont(q: f64, mut values: Vec) -> f64 { } #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn an_exact_grouped_sql_percentile_runs() { let plan = plan( "SELECT service, approx_percentile_cont(latency, 0.99) AS value FROM http \ @@ -352,7 +348,6 @@ async fn an_exact_grouped_sql_percentile_runs() { /// A global percentile over an empty window is NULL (SQL), and the plan /// types it nullable. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn an_exact_global_sql_percentile_of_no_row_is_null() { let plan = plan( "SELECT approx_percentile_cont(latency, 0.5) AS value FROM http \ @@ -375,7 +370,6 @@ async fn an_exact_global_sql_percentile_of_no_row_is_null() { /// which the executor runs as filters on `ts`. Both answer exactly (the /// sketch is exact below its capacity). #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn distinct_counts_over_5_and_15_minute_windows_run_from_tumbling_windows() { let fifteen = DISTINCT_USERS_LAST_5_MINUTES.replace("5 minutes", "15 minutes"); let plan = plan_all(&[DISTINCT_USERS_LAST_5_MINUTES, &fifteen], APPROXIMATE).await; @@ -399,7 +393,6 @@ async fn distinct_counts_over_5_and_15_minute_windows_run_from_tumbling_windows( /// `T - 15m` exactly once: 300 per service (the row at `T - 15m` is not /// read), and 100 in the last 5 minutes. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn row_counts_over_5_and_15_minute_windows_read_each_row_once() { let five = "SELECT service, COUNT(*) AS value FROM http \ WHERE ts >= now() - INTERVAL '5 minutes' GROUP BY service"; @@ -558,7 +551,6 @@ fn run_data_plane(plan: &InstallablePlan) -> Vec>> { /// window, and both queries answer exactly from the tumbling windows (the sketch is /// exact below its capacity), as the query-time reference does. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn ingestion_time_tumbling_windows_built_from_table_rows_answer_exactly() { let fifteen = DISTINCT_USERS_LAST_5_MINUTES.replace("5 minutes", "15 minutes"); let plan = plan_without_raw_data(&[DISTINCT_USERS_LAST_5_MINUTES, &fifteen], APPROXIMATE).await; @@ -587,7 +579,6 @@ async fn ingestion_time_tumbling_windows_built_from_table_rows_answer_exactly() /// Exact row counts over 5 and 15 minutes from ingestion-time state: each /// window's table rows are read once, 100 and 300 per service. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn ingestion_time_row_counts_read_each_table_row_once() { let five = "SELECT service, COUNT(*) AS value FROM http \ WHERE ts >= now() - INTERVAL '5 minutes' GROUP BY service"; @@ -612,7 +603,6 @@ async fn ingestion_time_row_counts_read_each_table_row_once() { /// one tumbling window built at ingestion time from the table rows, read /// no raw data at query time, and answers exactly. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_window_equal_to_its_interval_is_built_at_ingestion_time_from_table_rows() { let five_minutes_ms = 5 * MINUTE_MS as u64; let plan = plan_without_raw_data_every( @@ -688,7 +678,6 @@ impl DataStore for RecordingStore { // asked for `(T - 5m, T]` at evaluation time `T`; it returns all 20 minutes // of rows, and the answer is the one over the window. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn a_data_store_is_asked_for_the_query_window() { // The user's query, planned exactly over the `http` table. let plan = plan( diff --git a/crates/planner/tests/summary_sharing.rs b/crates/planner/tests/summary_sharing.rs index 1a321d198..8ea453196 100644 --- a/crates/planner/tests/summary_sharing.rs +++ b/crates/planner/tests/summary_sharing.rs @@ -155,8 +155,8 @@ fn composed_summary_candidates( (candidates, selection) } -/// Construct a legal shared-KLL candidate explicitly. Sharing capability must -/// not depend on which prefix a time-limited optimization search visits. +/// Construct a legal shared-KLL candidate explicitly, independent of the +/// plan the search selects. fn shared_kll_candidate(workload: &PlanningWorkload) -> asap_types::physical::PhysicalWorkloadPlan { use asap_logical_asap_optimization::test_support::{ compose_logical_candidate, generate_per_computation_candidates, @@ -488,7 +488,6 @@ async fn cross_series_p50_and_p99_share_one_producer() { /// Two identical ungrouped queries: their identical producers (rate, then /// sum) are shared. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn identical_ungrouped_queries_share_their_producers() { let query = ("sum(rate(x[5m]))", 0.01); let output = plan_promql(&[query, query]).await; @@ -499,7 +498,6 @@ async fn identical_ungrouped_queries_share_their_producers() { /// The SQL frontend reaches the same sharing for two copies of one filtered /// percentile. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn identical_sql_percentiles_share_one_producer() { let query = "SELECT approx_percentile_cont(l_extendedprice, 0.5) FROM lineitem WHERE l_orderkey > 10"; @@ -561,7 +559,6 @@ async fn sql_p50_and_p99_share_one_producer() { /// operator root (#511 §2.3): the scalar root is rebuilt over its selected /// sub-DAG, and entries keep their order around a literal scalar query. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn scalar_queries_read_planned_sub_dags() { let output = plan_promql(&[ ("1", 0.01), @@ -642,7 +639,6 @@ fn combined_scalar_references_keep_summary_candidates() { /// output-level evidence (plan selection, #576), so it is not selected /// automatically: the exact plan is. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn combined_vector_quantiles_keep_unselected_summary_candidates() { for query in [ "quantile(0.99, rate(x[5m])) - quantile(0.5, rate(x[5m]))", @@ -681,7 +677,6 @@ async fn combined_vector_quantiles_keep_unselected_summary_candidates() { /// The selected graph may reuse a producer or choose separate physical /// candidates. Every distinct selected reference must be exported exactly once. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn identical_scalar_references_satisfy_the_plan_contract() { for (query, distinct) in [ ("scalar(rate(x[5m])) - scalar(rate(x[5m]))", 1), @@ -716,7 +711,6 @@ async fn identical_scalar_references_satisfy_the_plan_contract() { /// is read through `FinalizeExactAccumulator`, so its output is plain values, /// not accumulator state. #[tokio::test] -#[ignore = "nondeterministic: plans under a wall-clock budget; revisit after Stage 3 design (#1175)"] async fn exact_state_roots_are_finalized() { for query in [ "sum_over_time(m[1m])", diff --git a/crates/types/src/ir/flat.rs b/crates/types/src/ir/flat.rs index cfc631f55..ac56e5b5f 100644 --- a/crates/types/src/ir/flat.rs +++ b/crates/types/src/ir/flat.rs @@ -46,36 +46,10 @@ pub struct FlatDag { /// becomes one node, so sharing is kept. Also returns the original node for /// each id. pub fn flatten(roots: &[QueryRoot]) -> (FlatDag, Vec>) { - flatten_before(roots, None).expect("flatten without a deadline cannot time out") -} - -#[derive(Debug, thiserror::Error)] -#[error("planning deadline exceeded flattening the physical DAG")] -pub(crate) struct FlattenDeadlineExceeded; - -/// The same pointer-preserving export, cooperatively bounded while visiting -/// nodes. A timeout returns no partially flattened DAG. -pub(crate) fn flatten_before( - roots: &[QueryRoot], - deadline: Option, -) -> Result<(FlatDag, Vec>), FlattenDeadlineExceeded> { let mut ids = HashMap::new(); let mut nodes = Vec::new(); let mut originals = Vec::new(); - check_deadline(deadline)?; - let mut error = None; - let mut id_of = |node: &Rc| { - if error.is_some() { - return 0; - } - match visit(node, &mut ids, &mut nodes, &mut originals, deadline) { - Ok(id) => id, - Err(failed) => { - error = Some(failed); - 0 - } - } - }; + let mut id_of = |node: &Rc| visit(node, &mut ids, &mut nodes, &mut originals); let roots = roots .iter() .map(|root| match root { @@ -83,19 +57,7 @@ pub(crate) fn flatten_before( QueryRoot::Scalar(expr) => QueryRoot::Scalar(expr.map_operator_refs(&mut id_of)), }) .collect(); - if let Some(error) = error { - return Err(error); - } - check_deadline(deadline)?; - Ok((FlatDag { nodes, roots }, originals)) -} - -fn check_deadline(deadline: Option) -> Result<(), FlattenDeadlineExceeded> { - if deadline.is_some_and(|limit| std::time::Instant::now() >= limit) { - Err(FlattenDeadlineExceeded) - } else { - Ok(()) - } + (FlatDag { nodes, roots }, originals) } fn visit( @@ -103,29 +65,13 @@ fn visit( ids: &mut HashMap<*const OperatorNode, NodeId>, nodes: &mut Vec, originals: &mut Vec>, - deadline: Option, -) -> Result { - check_deadline(deadline)?; +) -> NodeId { if let Some(&id) = ids.get(&Rc::as_ptr(node)) { - return Ok(id); + return id; } - let mut error = None; - let operator = node.operator.map_children(|child| { - if error.is_some() { - return 0; - } - match visit(child, ids, nodes, originals, deadline) { - Ok(id) => id, - Err(failed) => { - error = Some(failed); - 0 - } - } - }); - if let Some(error) = error { - return Err(error); - } - check_deadline(deadline)?; + let operator = node + .operator + .map_children(|child| visit(child, ids, nodes, originals)); let id = nodes.len(); nodes.push(FlatNode { operator, @@ -137,7 +83,7 @@ fn visit( }); originals.push(Rc::clone(node)); ids.insert(Rc::as_ptr(node), id); - Ok(id) + id } #[cfg(test)] @@ -163,30 +109,6 @@ mod tests { .unwrap() } - #[test] - #[ignore = "flaky: depends on a wall-clock deadline (#1067)"] - fn bounded_flatten_preserves_identity_and_abandons_partial_export() { - use std::time::{Duration, Instant}; - let shared = values(); - let roots = vec![ - QueryRoot::Operator(filter(Rc::clone(&shared))), - QueryRoot::Operator(filter(Rc::clone(&shared))), - ]; - let (expected, originals) = flatten(&roots); - let (bounded, bounded_originals) = - flatten_before(&roots, Some(Instant::now() + Duration::from_secs(1))).unwrap(); - assert_eq!(bounded, expected); - assert!(originals - .iter() - .zip(&bounded_originals) - .all(|(a, b)| Rc::ptr_eq(a, b))); - assert!(flatten_before(&roots, Some(Instant::now())).is_err()); - let large: Vec<_> = (0..100_000) - .map(|_| QueryRoot::Operator(values())) - .collect(); - assert!(flatten_before(&large, Some(Instant::now() + Duration::from_millis(1))).is_err()); - } - #[test] fn a_shared_node_is_flattened_once() { let shared = values(); diff --git a/crates/types/src/ir/materialized_view.rs b/crates/types/src/ir/materialized_view.rs index bbbd1a32f..6a3c2de29 100644 --- a/crates/types/src/ir/materialized_view.rs +++ b/crates/types/src/ir/materialized_view.rs @@ -65,7 +65,6 @@ use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -389,16 +388,6 @@ pub enum InstallableDagError { /// for less than its readers' window reaches back. #[error("query-time scan {node_id} is kept for less than its window reaches back")] ScanRetentionTooShort { node_id: PhysicalASAPNodeId }, - /// The caller's deadline passed while deriving the views or retention. - #[error("deadline exceeded deriving materialized views")] - DeadlineExceeded, -} - -fn check_deadline(deadline: Option) -> Result<(), InstallableDagError> { - match deadline { - Some(limit) if Instant::now() >= limit => Err(InstallableDagError::DeadlineExceeded), - _ => Ok(()), - } } impl PhysicalASAPDAG { @@ -418,7 +407,7 @@ impl PhysicalASAPDAG { } self.validate()?; let views: Vec = self - .views_with_node_retention(None)? + .views_with_node_retention()? .into_iter() .map(|(view, _)| view) .collect(); @@ -578,7 +567,7 @@ impl PhysicalASAPDAG { /// Sets `retention_ms` on every materialized view and every query-time /// `Scan` of an installed DAG (tumbling-window chains expanded), and returns the - /// views ([`Self::materialized_views`]); `Err` when `deadline` passes. + /// views ([`Self::materialized_views`]). /// /// A view takes `tumbling_window_retention_ms_by_node`'s value (Stage 3's retention /// of a tumbling window's chain: its offset plus its window plus the longest @@ -606,12 +595,9 @@ impl PhysicalASAPDAG { &mut self, tumbling_window_retention_ms_by_node: &HashMap, reader_span_ms_by_node: &HashMap, - deadline: Option, ) -> Result, InstallableDagError> { - let (mut views, scans_read): (Vec, Vec>) = self - .views_with_node_retention(deadline)? - .into_iter() - .unzip(); + let (mut views, scans_read): (Vec, Vec>) = + self.views_with_node_retention()?.into_iter().unzip(); let mut reach_ms_by_node = HashMap::new(); let mut position_of = HashMap::new(); for (position, view) in views.iter_mut().enumerate() { @@ -633,7 +619,6 @@ impl PhysicalASAPDAG { // Views before their inputs: an input stays while every view built // from it may still be refreshed. for index in (0..views.len()).rev() { - check_deadline(deadline)?; let view = &views[index]; let needed_ms = match view.refresh { MaterializedViewRefresh::OnFirstRead => view.retention_ms, @@ -661,7 +646,6 @@ impl PhysicalASAPDAG { } let scan_windows = self.query_time_scan_windows(); for node in self.nodes.iter().filter(|node| is_query_time_scan(node)) { - check_deadline(deadline)?; let window_ms = scan_windows[&node.id].lookback_ms(); let retention_ms = window_ms.map(|window_ms| { let span_ms = reader_span_ms_by_node.get(&node.id).copied().unwrap_or(0); @@ -740,9 +724,7 @@ impl PhysicalASAPDAG { /// `Scan`s its refresh reads through its raw-data windows. fn views_with_node_retention( &self, - deadline: Option, ) -> Result)>, InstallableDagError> { - check_deadline(deadline)?; let index = ViewIndex::new(self); index.check_raw_data_windows()?; let views: HashSet = self @@ -763,7 +745,6 @@ impl PhysicalASAPDAG { // its raw-data windows and those of the views it reads, unioned. let mut window_by_view: HashMap = HashMap::new(); for &id in index.children_first.iter().filter(|id| views.contains(id)) { - check_deadline(deadline)?; let node = index.nodes[&id]; let (inputs, scans) = index.inputs_of_view(id, &views)?; let read_windows: Vec = inputs diff --git a/crates/types/src/ir/operator/tumbling_window_chain.rs b/crates/types/src/ir/operator/tumbling_window_chain.rs index 1df8b6a7e..8416fa701 100644 --- a/crates/types/src/ir/operator/tumbling_window_chain.rs +++ b/crates/types/src/ir/operator/tumbling_window_chain.rs @@ -13,7 +13,7 @@ //! ends at the same time and is shorter. use std::collections::HashMap; use std::rc::Rc; -use std::time::{Duration, Instant}; +use std::time::Duration; use super::asap::ASAPOp; use super::node::{Operator, OperatorNode}; @@ -224,24 +224,6 @@ fn build_over( Ok(build) } -/// Expansion failure distinguishes invalid IR from an interrupted valid plan. -#[derive(Debug, thiserror::Error)] -pub enum TumblingWindowChainExpansionError { - #[error(transparent)] - Invalid(#[from] SchemaDerivationError), - #[error("planning deadline exceeded expanding tumbling-window chains")] - DeadlineExceeded, -} - -fn without_deadline(error: TumblingWindowChainExpansionError) -> SchemaDerivationError { - match error { - TumblingWindowChainExpansionError::Invalid(error) => error, - TumblingWindowChainExpansionError::DeadlineExceeded => { - unreachable!("no expansion deadline was supplied") - } - } -} - /// `root` with every `SummaryTumblingWindowChain` expanded into its tumbling windows: a `SummaryMerge` /// reads each tumbling window of each chain it read, newest first, as plain /// `SummaryAgg` nodes the executor runs; a merge of the newest `n` tumbling windows @@ -256,7 +238,7 @@ fn without_deadline(error: TumblingWindowChainExpansionError) -> SchemaDerivatio pub fn expand_tumbling_window_chains( root: &Rc, ) -> Result, SchemaDerivationError> { - expand(root, &mut NodeToExpandedNodeCache::default()).map_err(without_deadline) + expand(root, &mut NodeToExpandedNodeCache::default()) } /// A workload with its tumbling-window chains expanded @@ -295,19 +277,7 @@ pub struct ExpandedTumblingWindowChain { pub fn expand_tumbling_window_chains_in_workload( roots: &[QueryRoot], ) -> Result { - expand_tumbling_window_chains_in_workload_before(roots, None).map_err(without_deadline) -} - -/// Expand the same legal tumbling-window domain, abandoning partial expansion on timeout. -pub(crate) fn expand_tumbling_window_chains_in_workload_before( - roots: &[QueryRoot], - deadline: Option, -) -> Result { - let mut cache = NodeToExpandedNodeCache { - deadline, - ..Default::default() - }; - cache.check_deadline()?; + let mut cache = NodeToExpandedNodeCache::default(); let roots = roots .iter() .map(|root| match root { @@ -352,10 +322,6 @@ pub(crate) fn expand_tumbling_window_chains_in_workload_before( } }) .collect(); - // Catalog assembly follows expansion and is part of the same budget. - if deadline.is_some_and(|deadline| Instant::now() >= deadline) { - return Err(TumblingWindowChainExpansionError::DeadlineExceeded); - } Ok(TumblingWindowChainExpansion { roots, chains, @@ -384,7 +350,6 @@ fn timed_like(node: &Rc, timing: ExecutionTiming) -> OperatorNode /// merges read expands into one set of tumbling windows. #[derive(Default)] struct NodeToExpandedNodeCache { - deadline: Option, expanded_node_of: HashMap<*const OperatorNode, Rc>, tumbling_window_of: HashMap<(*const OperatorNode, u32), Rc>, /// Each chain expanded, in the order first reached. @@ -393,32 +358,18 @@ struct NodeToExpandedNodeCache { expanded: Vec<(Rc, Rc)>, } -impl NodeToExpandedNodeCache { - fn check_deadline(&self) -> Result<(), TumblingWindowChainExpansionError> { - if self - .deadline - .is_some_and(|deadline| Instant::now() >= deadline) - { - Err(TumblingWindowChainExpansionError::DeadlineExceeded) - } else { - Ok(()) - } - } -} - fn expand( node: &Rc, cache: &mut NodeToExpandedNodeCache, -) -> Result, TumblingWindowChainExpansionError> { - cache.check_deadline()?; +) -> Result, SchemaDerivationError> { if let Some(done) = cache.expanded_node_of.get(&Rc::as_ptr(node)) { return Ok(Rc::clone(done)); } let out = match &node.operator { Operator::ASAP(ASAPOp::SummaryTumblingWindowChain { .. }) => { - return Err( - invalid("a tumbling-window chain is read only through a summary merge").into(), - ) + return Err(invalid( + "a tumbling-window chain is read only through a summary merge", + )) } Operator::ASAP(ASAPOp::SummaryMerge { children, @@ -433,7 +384,6 @@ fn expand( .. }) => { for index in 0..newest_tumbling_windows.unwrap_or(*tumbling_window_count) { - cache.check_deadline()?; let key = (Rc::as_ptr(child), index); if let Some(done) = cache.tumbling_window_of.get(&key) { inputs.push(Rc::clone(done)); diff --git a/crates/types/src/ir/physical_export.rs b/crates/types/src/ir/physical_export.rs index b508d81ea..ccb07e256 100644 --- a/crates/types/src/ir/physical_export.rs +++ b/crates/types/src/ir/physical_export.rs @@ -13,7 +13,7 @@ use std::rc::Rc; use serde::{Deserialize, Serialize}; use thiserror::Error; -use super::flat::{flatten_before, NodeId}; +use super::flat::{flatten, NodeId}; use super::scalar::ScalarExpr; use crate::ir::operator::asap::ASAPOp; use crate::ir::operator::node::{Operator, OperatorNode}; @@ -855,38 +855,10 @@ pub fn compile_physical_asap_workload( Ok(compile_physical_asap_workload_with_node_ids(roots)?.dag) } -#[derive(Debug, Error)] -pub enum PhysicalASAPCompilationError { - #[error(transparent)] - Invalid(#[from] ExecutionDataStateError), - #[error("planning deadline exceeded exporting the physical DAG")] - DeadlineExceeded, -} - pub fn compile_physical_asap_workload_with_node_ids( roots: &[QueryRoot], ) -> Result { - compile_physical_asap_workload_with_node_ids_before(roots, None).map_err(|error| match error { - PhysicalASAPCompilationError::Invalid(error) => error, - PhysicalASAPCompilationError::DeadlineExceeded => { - unreachable!("no export deadline supplied") - } - }) -} - -pub(crate) fn compile_physical_asap_workload_with_node_ids_before( - roots: &[QueryRoot], - deadline: Option, -) -> Result { - let check_deadline = || { - if deadline.is_some_and(|limit| std::time::Instant::now() >= limit) { - Err(PhysicalASAPCompilationError::DeadlineExceeded) - } else { - Ok(()) - } - }; - let (flat, nodes_by_id) = flatten_before(roots, deadline) - .map_err(|_| PhysicalASAPCompilationError::DeadlineExceeded)?; + let (flat, nodes_by_id) = flatten(roots); let mut scalar_roots = Vec::new(); let roots = flat .roots @@ -903,14 +875,12 @@ pub(crate) fn compile_physical_asap_workload_with_node_ids_before( let mut nodes: Vec = Vec::with_capacity(flat.nodes.len()); let mut edges = Vec::new(); for (id, flat_node) in flat.nodes.into_iter().enumerate() { - check_deadline()?; let node = &nodes_by_id[id]; let output_state = data_state(node).ok_or(ExecutionDataStateError::UntimedNode { operator: node.operator.kind_name(), })?; // Children come before their parents, so every producer is already in `nodes`. for (producer, role) in edge_roles(&flat_node.operator) { - check_deadline()?; let producer_state = nodes[producer].output_state; let ingestion_dependency = producer_state.timing == ExecutionTiming::IngestionTime && output_state.timing == ExecutionTiming::IngestionTime; @@ -944,9 +914,7 @@ pub(crate) fn compile_physical_asap_workload_with_node_ids_before( roots, scalar_roots, }; - check_deadline()?; dag.validate().map_err(ExecutionDataStateError::from)?; - check_deadline()?; Ok(PhysicalASAPDAGCompilation { dag, node_ids: PhysicalASAPNodeIdentityMap { nodes_by_id }, diff --git a/crates/types/src/stages/memo.rs b/crates/types/src/stages/memo.rs index 080af2621..765ef09ca 100644 --- a/crates/types/src/stages/memo.rs +++ b/crates/types/src/stages/memo.rs @@ -5,12 +5,6 @@ use std::rc::Rc; use crate::ir::{OperatorNode, QueryRoot}; -/// Candidate generation stopped before its finite catalog was complete. -/// Callers must discard the partial catalog and cannot certify optimality from it. -#[derive(Debug, Clone, Copy, thiserror::Error)] -#[error("candidate generation deadline exceeded")] -pub struct CandidateGenerationTimeout; - /// A group, by index in the memo's groups. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct MemoGroupId(pub u32); diff --git a/crates/types/src/stages/mod.rs b/crates/types/src/stages/mod.rs index 9675682d1..109ce200e 100644 --- a/crates/types/src/stages/mod.rs +++ b/crates/types/src/stages/mod.rs @@ -18,10 +18,7 @@ mod realizer; pub use candidates::{CandidateForMemoGroup, Memo, OriginOfCandidateForMemoGroup}; pub use logical_memo_candidates::LogicalASAPMemoCandidates; -pub use memo::{ - queries_rooted_at, root_groups, CandidateGenerationTimeout, DagOfLogicalMemoGroups, MemoGroup, - MemoGroupId, -}; +pub use memo::{queries_rooted_at, root_groups, DagOfLogicalMemoGroups, MemoGroup, MemoGroupId}; pub use per_computation_logical_memo_candidates::{ ComputationTarget, FusedComputation, HavingEntry, HavingPlacement, HowToComputeAggregateMeasure, PerComputationLogicalMemoCandidates, diff --git a/crates/types/src/test_support.rs b/crates/types/src/test_support.rs index 14b13add4..d267ee3eb 100644 --- a/crates/types/src/test_support.rs +++ b/crates/types/src/test_support.rs @@ -3,21 +3,17 @@ //! public API stays as narrow as its production users need. Each forwards //! to the crate-private function of the same name. use std::rc::Rc; -use std::time::Instant; use crate::ir::materialized_view::RawDataWindow; use crate::ir::operator::maintained_population::CurrentSeriesInput; use crate::ir::operator::node::Operator; -use crate::ir::operator::tumbling_window_chain::{ - TumblingWindowChainExpansion, TumblingWindowChainExpansionError, -}; -use crate::ir::physical_export::{PhysicalASAPCompilationError, PhysicalASAPDAGCompilation}; +use crate::ir::physical_export::PhysicalASAPDAGCompilation; use crate::ir::properties::summary_coverage::SummaryCoverage; use crate::ir::properties::timing::NodeToTimedNodeCache; use crate::ir::properties::ExecutionDataStateError; use crate::ir::schema::Schema; use crate::ir::{ASAPOp, NonASAPOp}; -use crate::ir::{OperatorNode, QueryRoot, SchemaDerivationError}; +use crate::ir::{OperatorNode, SchemaDerivationError}; use crate::physical::Materialization; /// `SummaryCoverage::definition_id`. @@ -38,16 +34,6 @@ pub fn tumbling_window( crate::ir::operator::tumbling_window_chain::tumbling_window(chain, index) } -/// `tumbling_window_chain::expand_tumbling_window_chains_in_workload_before`. -pub fn expand_tumbling_window_chains_in_workload_before( - roots: &[QueryRoot], - deadline: Option, -) -> Result { - crate::ir::operator::tumbling_window_chain::expand_tumbling_window_chains_in_workload_before( - roots, deadline, - ) -} - /// `physical_export::compile_physical_asap_dag_with_node_ids`. pub fn compile_physical_asap_dag_with_node_ids( root: &Rc, @@ -55,14 +41,6 @@ pub fn compile_physical_asap_dag_with_node_ids( crate::ir::physical_export::compile_physical_asap_dag_with_node_ids(root) } -/// `physical_export::compile_physical_asap_workload_with_node_ids_before`. -pub fn compile_physical_asap_workload_with_node_ids_before( - roots: &[QueryRoot], - deadline: Option, -) -> Result { - crate::ir::physical_export::compile_physical_asap_workload_with_node_ids_before(roots, deadline) -} - /// `Operator::output_schema`. pub fn output_schema(operator: &Operator) -> Result { operator.output_schema() diff --git a/crates/types/tests/materialized_view.rs b/crates/types/tests/materialized_view.rs index ea6f0a664..0576ca187 100644 --- a/crates/types/tests/materialized_view.rs +++ b/crates/types/tests/materialized_view.rs @@ -130,7 +130,7 @@ fn a_view_without_retention_is_refused() { fn retention_propagates_from_a_view_refreshed_on_first_read_to_its_inputs() { let (mut dag, merge_id) = tumbling_windows_and_kept_merge(); let spans = HashMap::from([(merge_id, 3_600_000)]); - dag.set_retention(&HashMap::new(), &spans, None).unwrap(); + dag.set_retention(&HashMap::new(), &spans).unwrap(); let views = dag.materialized_views().unwrap(); let (merge, tumbling_windows) = views.split_last().unwrap(); assert_eq!(merge.node_id, merge_id); @@ -158,9 +158,7 @@ fn retention_propagates_from_a_view_refreshed_on_first_read_to_its_inputs() { #[test] fn a_view_kept_shorter_than_its_reach_is_refused() { let (mut dag, _) = tumbling_windows_and_kept_merge(); - let views = dag - .set_retention(&HashMap::new(), &HashMap::new(), None) - .unwrap(); + let views = dag.set_retention(&HashMap::new(), &HashMap::new()).unwrap(); let older = views .iter() .find(|v| { @@ -183,20 +181,6 @@ fn a_view_kept_shorter_than_its_reach_is_refused() { ); } -/// An expired deadline stops the derivation. -#[test] -fn the_derivation_stops_at_its_deadline() { - let (mut dag, _) = tumbling_windows_and_kept_merge(); - assert_eq!( - dag.set_retention( - &HashMap::new(), - &HashMap::new(), - Some(std::time::Instant::now()) - ), - Err(InstallableDagError::DeadlineExceeded) - ); -} - /// A quantile sketch of `algorithm` (KLL k = 200 or DDSketch α = 0.01). fn quantile_sketch(algorithm: SketchAlgorithm) -> FieldDataType { let params = match algorithm { @@ -265,7 +249,7 @@ fn three_chains_over_one_scan() -> (PhysicalASAPDAG, PhysicalASAPDAG) { let expanded = expand_tumbling_window_chains_in_workload(&timed).unwrap(); let mut installed = compile_physical_asap_workload(&expanded.roots).unwrap(); installed - .set_retention(&HashMap::new(), &HashMap::new(), None) + .set_retention(&HashMap::new(), &HashMap::new()) .unwrap(); (unexpanded, installed) } @@ -395,8 +379,7 @@ fn a_shifted_tumbling_window_has_its_own_phase() { .collect(); let timed = apply_materialization_timings_to_roots(&roots, &assignment).unwrap(); let mut dag = compile_physical_asap_workload(&timed).unwrap(); - dag.set_retention(&HashMap::new(), &HashMap::new(), None) - .unwrap(); + dag.set_retention(&HashMap::new(), &HashMap::new()).unwrap(); let mut windows: Vec<(u64, u64)> = dag .ingestion_refresh_groups() .unwrap() @@ -438,8 +421,7 @@ fn views_of_unbounded_windows_are_groups_of_their_own() { .collect(); let timed = apply_materialization_timings_to_roots(&roots, &assignment).unwrap(); let mut dag = compile_physical_asap_workload(&timed).unwrap(); - dag.set_retention(&HashMap::new(), &HashMap::new(), None) - .unwrap(); + dag.set_retention(&HashMap::new(), &HashMap::new()).unwrap(); let groups = dag.ingestion_refresh_groups().unwrap(); assert_eq!(groups.len(), 2); for group in &groups { @@ -471,8 +453,7 @@ fn a_view_reading_a_view_of_its_group_follows_it() { ) .unwrap(); let mut dag = compile_physical_asap_workload(&timed).unwrap(); - dag.set_retention(&HashMap::new(), &HashMap::new(), None) - .unwrap(); + dag.set_retention(&HashMap::new(), &HashMap::new()).unwrap(); let groups = dag.ingestion_refresh_groups().unwrap(); assert_eq!(groups.len(), 1); let [tumbling_window_view, rollup_view] = groups[0].views.as_slice() else { diff --git a/crates/types/tests/physical_export.rs b/crates/types/tests/physical_export.rs index d331e4845..649cb714a 100644 --- a/crates/types/tests/physical_export.rs +++ b/crates/types/tests/physical_export.rs @@ -767,27 +767,15 @@ fn high_fan_in_edges_preserve_multiplicity_without_order_dependence() { )); } +/// A workload export of one timed root is that root's export, and an +/// untimed root is invalid. #[test] -#[ignore = "flaky: depends on a wall-clock deadline (#1067)"] -fn physical_export_deadline_is_typed_and_preserves_unbounded_behavior() { - use asap_types::ir::physical_export::PhysicalASAPCompilationError; - use asap_types::test_support::compile_physical_asap_workload_with_node_ids_before; - use std::time::Instant; +fn workload_export_matches_the_single_root_export() { + use asap_types::ir::physical_export::compile_physical_asap_workload_with_node_ids; let timed = timed_at_ingestion(); let roots = [QueryRoot::Operator(Rc::clone(&timed))]; - let bounded = compile_physical_asap_workload_with_node_ids_before( - &roots, - Some(Instant::now() + std::time::Duration::from_secs(1)), - ) - .unwrap(); - assert_eq!(bounded.dag, compile_physical_asap_dag(&timed).unwrap()); - assert!(matches!( - compile_physical_asap_workload_with_node_ids_before(&roots, Some(Instant::now())), - Err(PhysicalASAPCompilationError::DeadlineExceeded) - )); + let exported = compile_physical_asap_workload_with_node_ids(&roots).unwrap(); + assert_eq!(exported.dag, compile_physical_asap_dag(&timed).unwrap()); let untimed = [QueryRoot::Operator(plan())]; - assert!(matches!( - compile_physical_asap_workload_with_node_ids_before(&untimed, None), - Err(PhysicalASAPCompilationError::Invalid(_)) - )); + assert!(compile_physical_asap_workload_with_node_ids(&untimed).is_err()); } diff --git a/crates/types/tests/tumbling_window_chain.rs b/crates/types/tests/tumbling_window_chain.rs index 164774b64..b37d77385 100644 --- a/crates/types/tests/tumbling_window_chain.rs +++ b/crates/types/tests/tumbling_window_chain.rs @@ -443,54 +443,18 @@ fn a_rollup_of_the_newest_tumbling_windows_covers_their_window() { ); } -/// A fine-width chain remains a legal candidate; interrupted expansion is a -/// typed budget stop, never a malformed chain or a partially successful plan. +/// Expansion keeps every tumbling window and the identity of a chain two +/// readers share. #[test] -#[ignore = "flaky: depends on a wall-clock deadline (#1067)"] -fn fine_tumbling_window_expansion_stops_at_the_planning_deadline() { - use asap_types::ir::operator::tumbling_window_chain::TumblingWindowChainExpansionError; - use asap_types::ir::QueryRoot; - use asap_types::test_support::expand_tumbling_window_chains_in_workload_before; - use std::time::Instant; - let mut op = chain(scan()).operator.clone(); - let Operator::ASAP(ASAPOp::SummaryTumblingWindowChain { - tumbling_window_width_ms, - tumbling_window_count, - .. - }) = &mut op - else { - unreachable!() - }; - *tumbling_window_width_ms = 1; - *tumbling_window_count = 1_000_000; - let roots = [QueryRoot::Operator(merge(vec![OperatorNode::new_shared( - op, - ) - .unwrap()]))]; - let deadline = Instant::now() + Duration::from_millis(1); - assert!(matches!( - expand_tumbling_window_chains_in_workload_before(&roots, Some(deadline)), - Err(TumblingWindowChainExpansionError::DeadlineExceeded) - )); -} - -/// Supplying ample budget preserves complete expansion and shared tumbling window identity. -#[test] -#[ignore = "flaky: depends on a wall-clock deadline (#1067)"] -fn budgeted_expansion_preserves_every_tumbling_window_and_shared_reader() { +fn expansion_preserves_every_tumbling_window_and_shared_reader() { + use asap_types::ir::operator::tumbling_window_chain::expand_tumbling_window_chains_in_workload; use asap_types::ir::QueryRoot; - use asap_types::test_support::expand_tumbling_window_chains_in_workload_before; - use std::time::Instant; let shared = chain(scan()); let roots = [ QueryRoot::Operator(merge(vec![shared.clone()])), QueryRoot::Operator(merge(vec![shared])), ]; - let result = expand_tumbling_window_chains_in_workload_before( - &roots, - Some(Instant::now() + Duration::from_secs(10)), - ) - .unwrap(); + let result = expand_tumbling_window_chains_in_workload(&roots).unwrap(); assert_eq!(result.chains.len(), 1); assert_eq!(result.chains[0].tumbling_windows.len(), 5); let QueryRoot::Operator(first) = &result.roots[0] else { diff --git a/docs/design_docs/architecture/updated_interface_with_pluggable_optimization.md b/docs/design_docs/architecture/updated_interface_with_pluggable_optimization.md index 0ba5593fc..d1c0d2453 100644 --- a/docs/design_docs/architecture/updated_interface_with_pluggable_optimization.md +++ b/docs/design_docs/architecture/updated_interface_with_pluggable_optimization.md @@ -125,14 +125,13 @@ tracked in #580. | Step | Call | |---|---| | Prepare roots | Entries in `entries()` order, the accuracy target from each entry's `requirements`; PromQL operator roots carry the series identity (`with_promql_series_identity`) where it has a realization | -| Stage 1: logical ASAP-aware optimization | `generate_per_computation_logical_memo_candidates` (Pass 1, once), then per tumbling-window-width round `generate_logical_asap_memo_candidates` (Pass 2) | -| Stage 2: physical ASAP-aware optimization | `generate_physical_asap_memo_candidates` per round | -| Stage 3: plan selection | `select_plan` per round: check accuracy and latency, price under the cost model, select one plan; the facade keeps the best round's and builds it with Stage 2's `AssignmentRealizer::realize` | +| Stage 1: logical ASAP-aware optimization | `generate_per_computation_logical_memo_candidates` (Pass 1), then `generate_logical_asap_memo_candidates` (Pass 2) over every tumbling-window width | +| Stage 2: physical ASAP-aware optimization | `generate_physical_asap_memo_candidates` | +| Stage 3: plan selection | `select_plan`: check accuracy and latency, price under the cost model, select one plan; the facade builds it with Stage 2's `AssignmentRealizer::realize` | `run_planning` runs these steps over the query-language frontends' roots -(Stage 0) and returns `PlanningRun`: Pass 1's candidates, the selected -round's Pass 2 and Stage 2 outputs, the selection, the selected plan and a -report per round; the `stage_pipeline` devtool shows them. +(Stage 0) and returns `PlanningRun`: Pass 1's candidates, the Pass 2 and +Stage 2 outputs, the selection, the selected plan and a report; the `stage_pipeline` devtool shows them. `ReplacementStrategy` remains a concept of the legacy candidate search, which no pass uses any more. diff --git a/docs/design_docs/proposals/planner-layering.md b/docs/design_docs/proposals/planner-layering.md index 200b27a90..9f34e78a5 100644 --- a/docs/design_docs/proposals/planner-layering.md +++ b/docs/design_docs/proposals/planner-layering.md @@ -425,9 +425,8 @@ with accuracy guarantees enforced and the `aws-fargate-us-east-1` cost model). `crates/integration-tests/tests/memo_enumeration.rs` lowers the query as production does (with the series-identity column) and reads the memo from the production run. It asserts the Stage 0 and Pass 1 numbers above. The last -step is the memo of the planning round whose plan was selected, so it depends -on the time budget; the test runs one round over the complete tumbling-window-width -domain, which gives 438 groups and 2158 candidates. +step is Pass 2's memo over the complete tumbling-window-width domain, which +gives 438 groups and 2158 candidates. #### Algorithm: memo to candidate DAGs @@ -509,12 +508,9 @@ candidate per memo group and execution context, with each shareable candidate charged once, and is solved once; it prices cost model 1 only (cost models 2 and 3 are an `UnsupportedCostModel` error, #888). Stage 3 realizes the chosen plan through Stage 2's `AssignmentRealizer`, which the facade builds and passes in, -so Stage 3 does not depend on the Stage 2 crate. The facade first finds an -incumbent (the best validated plan so far) in a round over seed tumbling-window grids, -then exposes finer tumbling-window widths in later rounds, each round running Pass 2, -Stage 2 and Stage 3; only a completed round over the full tumbling-window-width domain -bounds the declared search space, and otherwise the result is the best -validated plan found. Stage 3 checks that `plan_cost` of the realized plan +so Stage 3 does not depend on the Stage 2 crate. The facade runs Pass 2, +Stage 2 and Stage 3 once, over the full tumbling-window-width domain, with no +time limit. Stage 3 checks that `plan_cost` of the realized plan equals the MILP's objective, and that every query's accuracy and latency hold. #### Memo code interface @@ -530,9 +526,9 @@ candidates is in `crates/plan-selection/src/` (`select.rs`, `groups/`, `milp/`). | Stage 0 | `generate_dag_of_logical_memo_groups` → `DagOfLogicalMemoGroups`, `MemoGroup`, `MemoGroupId` | `stage0.rs` (the types: `asap_types::stages`); hash-consing is `share_common_sub_dags` in `stage0/cse.rs` | | Pass 1 | `generate_per_computation_logical_memo_candidates` → `PerComputationLogicalMemoCandidates` (its `Memo` and its `PrunedCandidate`s; `ComputationTarget` is Pass 1's working type, in `asap_types::stages`), the one entry: inside it, the computation targets (walking the memo groups), the summaries the deployment does not support and those no query can use pruned, the candidates attached to the groups (`try_populate_pass1`), then the accuracy-scoped sizes | `pass1/logical_memo_candidates.rs`; `pass1/per_computation_candidates.rs`; `pass2/memo/groups.rs` | | Memo types | `DagOfLogicalMemoGroups`, `PerComputationLogicalMemoCandidates`, `HowToComputeAggregateMeasure`, Pass 1's working types (`ComputationTarget`, `FusedComputation`, `HavingEntry`, `HavingPlacement`), `Memo`, `CandidateForMemoGroup`, `OriginOfCandidateForMemoGroup`, `LogicalASAPMemoCandidates`, `CandidateGenerationTimeout`, `root_groups`, `queries_rooted_at` | `crates/types/src/stages/` (`asap_types::stages`) | -| Pass 2 | `generate_logical_asap_memo_candidates` → `LogicalASAPMemoCandidates`, one tumbling-window-width round over a `Pass2RoundState` (kept between rounds: the memo after step 1 and before the rules, and the widths already generated). It calls in this order: 1. window composition for the round's new widths (`extend_local_window_composition`); 2–9. the rules in `pass2/memo/rules/`: scan widening, summary inputs, top-k reuse, finer aggregate, rollup, window rollup, summary results, state configurations; 10. `add_shared_summary_state_groups`, the shared builds | `pass2/logical_memo_candidates.rs` (`LogicalASAPMemoCandidates` itself: `asap_types::stages`); `pass2/memo/window_candidates.rs`, `pass2/memo/rules/`, `pass2/memo/state_groups.rs`, `pass2/memo/shared_configurations.rs` | +| Pass 2 | `generate_logical_asap_memo_candidates` → `LogicalASAPMemoCandidates`, from Pass 1's output. It calls in this order: 1. window composition for every legal tumbling-window width (`add_local_window_composition`); 2–9. the rules in `pass2/memo/rules/`: scan widening, summary inputs, top-k reuse, finer aggregate, rollup, window rollup, summary results, state configurations; 10. `add_shared_summary_state_groups`, the shared builds | `pass2/logical_memo_candidates.rs` (`LogicalASAPMemoCandidates` itself: `asap_types::stages`); `pass2/memo/window_candidates.rs`, `pass2/memo/rules/`, `pass2/memo/state_groups.rs`, `pass2/memo/shared_configurations.rs` | | Memo to candidate DAGs | `enumerate`, `build_logical_asap_dags_from_chosen_candidates`, `ChosenCandidatePerMemoGroup`, `AllChosenCandidatesPerMemoGroup`, `ErrorBuildingLogicalASAPDagsFromChosenCandidates` | `pass2/memo/enumeration.rs` (re-exported at the crate root) | -| Production memo build and search | The facade's `run_planning` builds the DAG of logical memo groups and runs Pass 1 (`generate_per_computation_logical_memo_candidates`) once; then, for each tumbling-window-width round, `generate_logical_asap_memo_candidates` (the `Pass2RoundState` is kept between rounds), `generate_physical_asap_memo_candidates` and Stage 3's `select_plan`. It keeps the best round's selection (`asap_cost::PlanRanking`) and builds that plan with Stage 2's `AssignmentRealizer::realize`. Behind `select_plan`: the group selection MILP (#1095), `groups/` and `milp/` | `crates/planner/src/run.rs`; `crates/plan-selection/src/select.rs` | +| Production memo build and search | The facade's `run_planning` builds the DAG of logical memo groups and runs Pass 1 (`generate_per_computation_logical_memo_candidates`) once; then `generate_logical_asap_memo_candidates`, `generate_physical_asap_memo_candidates` and Stage 3's `select_plan`, once each, and builds the selected plan with Stage 2's `AssignmentRealizer::realize`. Behind `select_plan`: the group selection MILP (#1095), `groups/` and `milp/` | `crates/planner/src/run.rs`; `crates/plan-selection/src/select.rs` | ## ASAPPlanner Detailed Design: Planning Stages and their decisions/Strategies @@ -605,7 +601,7 @@ the memo path's entry points. | 1, Pass 2 | One Pass 1 choice per computation → that choice rewritten by each sharing rule, every subset of sharing choices (no longer in the code) | `Memo`; the entries' recurrence and range steps → the same memo with each rule's candidates added to the groups they implement | the memo rules (`pass2/memo/rules/`), `pass2::memo::window_candidates` | | 1, concrete summary-state groups | (part of Pass 2) | `Memo` → the memo with concrete summary-state groups: one group per shared size or tumbling-window layout that readers can read | `add_shared_summary_state_groups`, Pass 2's last step; the Pass 2 output is `LogicalASAPMemoCandidates` (`generate_logical_asap_memo_candidates`) | | 2 | One whole-workload logical candidate; entries (recurrence, predictability), `DataWorkload`, query-time retention → its physical candidates, one per materialization assignment | `LogicalASAPMemoCandidates`; entries, `DataWorkload`, query-time and raw-data retention → `PhysicalASAPMemoCandidates`: every operator node of every candidate with each allowed timing and materialization (one physical candidate each, no copies), the producers of each input, reader constraints (each applied to every selected run of its candidate), and the pruned candidates (`PrunedCandidate`) | `generate_physical_asap_memo_candidates` (`asap_physical_asap_optimization`) | -| 3 | Every physical candidate; accuracy targets, accuracy model and evidence, cost model → one plan | `PhysicalASAPMemoCandidates`; entries, `DataWorkload`, `AccuracyCheckInputs`, `CostInputs`, Stage 2's realizer → `Selection`: the selected plan (`selected_dag`, a `DagOfSelectedCandidates`: one node per run of a candidate, so a candidate recomputed by two readers or shared as two instances over different inputs is two nodes, with an edge per input), its cost, the accepted accuracy per query and the search report | `select_plan` (`asap_plan_selection`); the facade (`run_planning`, output `PlanningRun`) runs it per tumbling-window-width round and builds the selected `PhysicalWorkloadPlan` with `AssignmentRealizer::realize`, one plan node per selected node | +| 3 | Every physical candidate; accuracy targets, accuracy model and evidence, cost model → one plan | `PhysicalASAPMemoCandidates`; entries, `DataWorkload`, `AccuracyCheckInputs`, `CostInputs`, Stage 2's realizer → `Selection`: the selected plan (`selected_dag`, a `DagOfSelectedCandidates`: one node per run of a candidate, so a candidate recomputed by two readers or shared as two instances over different inputs is two nodes, with an edge per input), its cost, the accepted accuracy per query and the search report | `select_plan` (`asap_plan_selection`); the facade (`run_planning`, output `PlanningRun`) runs it once and builds the selected `PhysicalWorkloadPlan` with `AssignmentRealizer::realize`, one plan node per selected node | | 4. Execution | The selected `PhysicalASAPDAG` → query results | Same: an `InstallablePlan` (`{dag, query_texts}`) → query results | The deployment runtime installs the `InstallablePlan` | What changed with the memo: diff --git a/docs/develop_docs/asap-aware-mapping-architecture.md b/docs/develop_docs/asap-aware-mapping-architecture.md index ac453ecc0..fa83723aa 100644 --- a/docs/develop_docs/asap-aware-mapping-architecture.md +++ b/docs/develop_docs/asap-aware-mapping-architecture.md @@ -58,9 +58,9 @@ PhysicalASAPMemoCandidates (operator nodes per timing/materialization + pruned_c Selection (selected_dag, cost, accepted_accuracy_per_query, search_report) ``` -`asap_planner::run_planning` runs all of it over lowered roots: Pass 1 once, -then Pass 2, Stage 2 and Stage 3 per tumbling-window-width round, keeping the best -round's selection and building that plan with `AssignmentRealizer::realize`. +`asap_planner::run_planning` runs all of it over lowered roots: Pass 1, then +Pass 2 over every tumbling-window width, Stage 2 and Stage 3, and builds the +selected plan with `AssignmentRealizer::realize`. It returns every stage's output (`PlanningRun`); `Stage1ToStage3Pass` is the same pipeline behind the `OptimizationPass` trait. diff --git a/docs/develop_docs/joint-candidate-workload-evaluation.md b/docs/develop_docs/joint-candidate-workload-evaluation.md index de11594e9..ae9fb7725 100644 --- a/docs/develop_docs/joint-candidate-workload-evaluation.md +++ b/docs/develop_docs/joint-candidate-workload-evaluation.md @@ -1,5 +1,7 @@ # Joint candidate search: ASAPQuery workload evaluation +> Historical: the results below predate #1185, which removed the planning deadlines and the tumbling-window-width rounds. + Developer evaluation, 2026-10-24. These are exploratory optimizer measurements and modeled execution costs, not measured query-runtime performance. Single debug-build runs on a shared host do not establish a speedup. ## Current production-path validation @@ -138,7 +140,7 @@ CARGO_BUILD_JOBS=4 cargo build -p asap-devtools --bin stage_pipeline /usr/bin/time -v -o run.time.txt timeout 120s \ /mydata/target-joint-candidate-connections/debug/stage_pipeline \ --workload tools/dag-viewer/workloads/random-mixed-seed-862/workload-01.json \ - --search candidate-connections --milp-time-limit-s 30 \ + --search candidate-connections \ --cost-model aws-fargate-us-east-1 --enforce-accuracy-guarantees \ --deployment-capabilities '{"raw_data_retained":false,"query_time_results_retained":false}' \ --out run.json diff --git a/docs/develop_docs/library-api.md b/docs/develop_docs/library-api.md index 3b2267408..d1001c98c 100644 --- a/docs/develop_docs/library-api.md +++ b/docs/develop_docs/library-api.md @@ -166,8 +166,7 @@ example, see `lineitem_catalog` in [the planner's end-to-end tests](../../crates `asap_planner::e2e_plan` runs the whole planner: the query-language frontends (Stage 0), then the default optimization pass `Stage1ToStage3Pass`, which runs logical ASAP-aware optimization (Stage 1), physical ASAP-aware optimization -(Stage 2) and plan selection (Stage 3): Pass 1 once, then Pass 2, Stage 2 -and Stage 3 per tumbling-window-width round, keeping the best round's plan. +(Stage 2) and plan selection (Stage 3), each once, with no time limit. ```text asap_planner::e2e_plan(input: UserInput<'_>) -> Result // async @@ -181,10 +180,10 @@ asap_planner::run_planning( entries: &[QueryWorkloadEntry], // one per root: accuracy, latency bound, recurrence data: &DataWorkload, models: PlanningModels<'_>, - settings: PlanSearchSettings, // time_limit_ms, default 30 s; inspect, default off + settings: PlanSearchSettings, // inspect, default off ) -> Result -// Stage 3 alone, over one round's Stage 2 output: +// Stage 3 alone, over Stage 2's output: asap_plan_selection::select_plan( physical: &PhysicalASAPMemoCandidates, entries: &[QueryWorkloadEntry], @@ -207,7 +206,7 @@ asap_plan_selection::select_plan( | `PlanOutput.plans` | One `QueryPlan { entry_index, root }` per query, in `QueryWorkload::entries()` order | | `PlanOutput.selection` | `Some` with Stage 3's `Selection` under the default pass: `selected_dag`, `nodes_with_proven_non_negative_weights`, `cost` (the selected plan's `PlanCost` with its `ResourceUsage` and `cost_per_second`, `None` under cost model 3), `accepted_accuracy_per_query`, `pruned_candidates` and `search_report` | | `PlanOutput.tumbling_window_retention_ms` | For each tumbling-window chain the selected plan keeps beyond one evaluation, by node id, how long its tumbling windows are kept (`RealizedPlan::tumbling_window_retention_ms`) | -| `PlanningRun` | `per_computation_candidates`, the selected round's `logical_asap_candidates` and `physical_asap_candidates`, `selection`, `selected_physical_plan` (`RealizedPlan`), `selected_plan` (the roots to run) and `report` (per round: counts and Pass 2, Stage 2 and Stage 3 times); `per_computation_candidates` is kept only with `PlanSearchSettings::inspect`; `asap_planner::selected_chosen_candidates` reads the selected plan as one candidate per group of `logical_asap_candidates.memo` | +| `PlanningRun` | `per_computation_candidates`, `logical_asap_candidates`, `physical_asap_candidates`, `selection`, `selected_physical_plan` (`RealizedPlan`), `selected_plan` (the roots to run) and `report` (Pass 2's and Stage 2's counts and each stage's time); `per_computation_candidates` is kept only with `PlanSearchSettings::inspect`; `asap_planner::selected_chosen_candidates` reads the selected plan as one candidate per group of `logical_asap_candidates.memo` | | `PlanOutput::physical_asap_dag()` | The planned queries as one `PhysicalASAPDAG`, timed with the default `MaterializationAssignment` (see [Export](#export)) | Each candidate's `ResourceUsage` holds its usage without prices: diff --git a/docs/develop_docs/tumbling-window-search-compression-evaluation.md b/docs/develop_docs/tumbling-window-search-compression-evaluation.md index 9d88e65e2..2419cf97e 100644 --- a/docs/develop_docs/tumbling-window-search-compression-evaluation.md +++ b/docs/develop_docs/tumbling-window-search-compression-evaluation.md @@ -1,5 +1,7 @@ # Tumbling-window-search compression evaluation +> Historical: this report predates #1185, which removed the planning budget and the tumbling-window-width rounds; the planner now searches every width in one pass. + This report is for planner developers. It separates three changes: storing repeated producer lists once, factoring equivalent candidate connections, and spending a bounded search budget across progressively larger tumbling-window-width domains. Storage compression alone does not reduce the number of MILP variables. Searching fewer widths before a timeout does not prove equivalence to the complete domain. ## Reproduction diff --git a/docs/user_guide_docs/stage-pipeline.md b/docs/user_guide_docs/stage-pipeline.md index 8bdd1b05d..5f79f103a 100644 --- a/docs/user_guide_docs/stage-pipeline.md +++ b/docs/user_guide_docs/stage-pipeline.md @@ -25,13 +25,12 @@ cargo run --release -p asap-devtools --bin stage_pipeline -- \ --promql 'sum_over_time(requests[5m])' \ --series-count 1000 --sample-interval-ms 15000 \ --enforce-accuracy-guarantees \ - --milp-time-limit-s 30 \ --out plan.json ``` The `--` separates Cargo options from planner options. `--out` is required. Release builds are recommended for planning-performance measurements. Initial -compilation can take several minutes and is separate from the planning budget. +compilation can take several minutes and is separate from planning time. Alternatively, build once and run the binary directly: @@ -54,7 +53,6 @@ cargo run --release -p asap-devtools --bin stage_pipeline -- \ --deployment-capabilities '{"raw_data_retained":true,"query_time_results_retained":false}' \ --cost-model aws-fargate-us-east-1 \ --enforce-accuracy-guarantees \ - --milp-time-limit-s 280 \ --out rangeheavy-plan.json ``` @@ -76,10 +74,9 @@ for workload, source-fact, and SQL examples. | Option | Meaning | | --- | --- | | `--enforce-accuracy-guarantees` | Enforce the queries' accuracy requirements. Without it, this inspection tool ignores accuracy guarantees, although executability checks still apply. | -| `--milp-time-limit-s 280` | Set the planning time budget in seconds. A timeout may return a validated incumbent or no selected plan; it does not establish optimality. | | `--cost-model aws-fargate-us-east-1` | Compare plans using the configured AWS Fargate pricing model. Other supported names are `usage-over-time`, `provisioned-capacity`, and `cluster-capacity`. | | `--max-displayed 64` | Limit candidates shown in the document; this is not a search budget. | -| `--exhaustive --max-combinations 10000` | Enumerate up to this many memo choices of the selected round, implement each with Stage 2 and price every resulting plan with the whole-workload reference pricer; its selection, costs and rejections are under `reference_pricing`. It is a reference, not Stage 3's search. | +| `--exhaustive --max-combinations 10000` | Enumerate up to this many memo choices of Pass 2's memo, implement each with Stage 2 and price every resulting plan with the whole-workload reference pricer; its selection, costs and rejections are under `reference_pricing`. It is a reference, not Stage 3's search. | | `--cost-inputs cost_inputs.json` | Load the cost inputs (`CostInputs` JSON) instead of `--cost-model`, cluster flags, or `--measured-operation-costs`. | | `--no-memo` | Leave out the `memo` section (the memo after each step and the enumerated candidate DAGs), for a smaller document. | | `--price-enumerated 50` | Cost the first 50 enumerated candidate DAGs through Stage 2 and Stage 3, and flag any cheaper than the selected plan. Off by default. | @@ -107,10 +104,10 @@ PY The document format is `asap-stage-pipeline/v1`. Check `selected_dag`, `cost`, `search_report` and any `error` in `stage3_plan_selection`, and the -per-round counts and times in `planning_report`. A written JSON document +counts and times in `planning_report`. A written JSON document alone does not mean planning succeeded: `selected_dag` may be null. -A selected plan is not necessarily a proven optimum; inspect the search -status (`search_report.status`) and the lower bound. Costs are model predictions, not measured +The search status (`search_report.status`) of a selected plan is `optimal`: +the plan is optimal over the candidates Stage 2 generated. Costs are model predictions, not measured execution times or an actual cloud bill. ## Troubleshooting diff --git a/tools/benchmark_tumbling_window_search.py b/tools/benchmark_tumbling_window_search.py index 4feb482ff..d8e0cc93d 100644 --- a/tools/benchmark_tumbling_window_search.py +++ b/tools/benchmark_tumbling_window_search.py @@ -3,6 +3,10 @@ Linux /usr/bin/time measures whole-process peak RSS. Search counters describe reported domains and must not be confused with a complete-catalog optimum. + +Historical: since #1185 the planner has no planning budget and searches every +tumbling-window width in one pass, so `--budget` only labels a run and the +domain check passes trivially for binaries built after #1185. """ import argparse import concurrent.futures @@ -135,7 +139,6 @@ def run_one(entry, args): command = ["/usr/bin/time", "-v", "-o", str(output / (name + ".time")), "timeout", f"{args.timeout}s", str(binary), "--workload", str(VIEWER / entry["workload"]), - "--milp-time-limit-s", str(args.budget), "--cost-model", entry["cost_model"], "--enforce-accuracy-guarantees", "--deployment-capabilities", json.dumps(entry["deployment_capabilities"]), "--out", str(artifact)] diff --git a/tools/dag-viewer/README.md b/tools/dag-viewer/README.md index d86637799..083600cbe 100644 --- a/tools/dag-viewer/README.md +++ b/tools/dag-viewer/README.md @@ -262,11 +262,10 @@ are ignored, `stage_pipeline`'s default. "retention_ms": 600000, "stage2_node_id": 1, "installed_position": 0, "installed_count": 30 }] }, - "planning_report": { "pass1_ms": 3.1, "selected_round": 1, "tumbling_window_widths_pending": false, "time_limit_ms": 30000, - "rounds": [{ "tumbling_window_width_domain": "SeedGrids", "memo_groups": 12, "logical_candidates": 30, - "operator_nodes": 40, "physical_candidates": 96, "stage2_pruned_candidates": 4, - "pass2_ms": 1.2, "stage2_ms": 3.4, "stage3_ms": 150.0, - "search_report": { "...": "..." }, "error": null }] }, + "planning_report": { "pass1_ms": 3.1, "memo_groups": 12, "logical_candidates": 30, + "operator_nodes": 40, "physical_candidates": 96, "physical_producer_links": 120, + "stage2_pruned_candidates": 4, "pass2_ms": 1.2, "stage2_ms": 3.4, "stage3_ms": 150.0, + "search_report": { "...": "..." } }, "reference_pricing": { "cost_per_candidate": { "E1-0": { "...": "as stage3_plan_selection.cost" } }, "accepted_accuracy_per_candidate": { "E1-0": [{ "...": "as accepted_accuracy_per_query" }] }, @@ -291,7 +290,7 @@ are ignored, `stage_pipeline`'s default. `requirements.default_metric`, the metric the query's result is naturally stated in (null for a quantile), for display only. - `stage3_plan_selection` is Stage 3's `Selection`: the selected DAG - (`selected_dag`: `candidate_of_node`, the selected round's Stage 2 + (`selected_dag`: `candidate_of_node`, the Stage 2 candidate each node runs, one node per run; `edges`, node to node, per `input`; `node_of_output_reference`), the nodes whose count-min weights it proved non-negative, the selected plan's `cost`, the @@ -303,10 +302,8 @@ are ignored, `stage_pipeline`'s default. run's cost model (`--cost-model`): null under cost model 3, which has no prices; a node's is null under cost models 2 and 3, which price or limit the whole plan's peaks. -- `planning_report` is the facade's: Pass 1's time, then per tumbling-window-width - round its domain, Pass 2's and Stage 2's counts, each stage's time, and - Stage 3's search report or error; `selected_round` and whether some tumbling window - widths were not searched (`tumbling_window_widths_pending`). +- `planning_report` is the facade's: each stage's time, Pass 2's and + Stage 2's counts, and Stage 3's search report. - `reference_pricing` (only with `--exhaustive`) is the reference pricer over the enumerated memo choices (`E` logical, `E-` physical), with keys named as Stage 3's `Selection` names its fields: per shown @@ -351,8 +348,8 @@ are ignored, `stage_pipeline`'s default. with the default `CostInputs`). The Stage 3 header shows it; an older document without it is still valid, any other value is not. - `search` is `candidate-connections`, or `exhaustive` when the document - also holds the reference enumeration (`reference_pricing`): the selected - round's memo choices, each implemented by Stage 2 with every + also holds the reference enumeration (`reference_pricing`): Pass 2's + memo choices, each implemented by Stage 2 with every materialization assignment and all priced by the reference pricer. `--max-combinations` bounds the choices enumerated. - `stage3_plan_selection.search_report` reports how the search ended @@ -396,8 +393,8 @@ are ignored, `stage_pipeline`'s default. selects no plan, `stage3_plan_selection` has `selected_dag: null` and `error`; when the reference pricer selects none, its `selected_candidate` is null, with the `pruned_candidates` or `error`. -- `memo` (left out with `--no-memo`) is the memo the search built, from - the planning round whose plan was selected: `nodes`, one node table for +- `memo` (left out with `--no-memo`) is the memo the search built: + `nodes`, one node table for the whole section (each distinct logical node once, as in a flat logical DAG: children are indices into the table and come before their parents; `memoNodeDag` in `stages.js` rebuilds the flat DAG below any of them); diff --git a/tools/dag-viewer/stages.js b/tools/dag-viewer/stages.js index 1470eba23..5d60b5ca4 100644 --- a/tools/dag-viewer/stages.js +++ b/tools/dag-viewer/stages.js @@ -5,8 +5,7 @@ const STAGE_PIPELINE_FORMAT = 'asap-stage-pipeline/v1'; // Documents written before #1163 name the tumbling-window chain `PaneChain`, -// its fields `pane_*`, the planning report's `pane_width_domain` and -// `pane_widths_pending`, and the memo origin `panes` (`Panes` in older MILP +// its fields `pane_*`, and the memo origin `panes` (`Panes` in older MILP // search reports); renames them in place to today's names, keeping each // object's key order, so the rest of the viewer reads one set of names. // Remove once no document the viewer must open predates #1163. @@ -16,8 +15,6 @@ const TUMBLING_WINDOW_RENAMES = Object.freeze({ pane_count: 'tumbling_window_count', newest_panes: 'newest_tumbling_windows', pane_retention_ms: 'tumbling_window_retention_ms', - pane_width_domain: 'tumbling_window_width_domain', - pane_widths_pending: 'tumbling_window_widths_pending', }); const ORIGIN_RENAMES = Object.freeze({ panes: 'tumbling_windows', Panes: 'TumblingWindows' }); const has = (object, key) => Object.prototype.hasOwnProperty.call(object, key); @@ -636,11 +633,6 @@ function searchText(doc) { if (typeof report.cost_lower_bound_per_second === 'number') parts.push(`cost lower bound ${Number(report.cost_lower_bound_per_second.toPrecision(4))}`); parts.push(`MILP ${report.milp_variables} variables, ${report.milp_constraints} constraints · model ${report.model_construction_ms} ms, solve ${report.solver_ms} ms, validation ${report.validation_ms} ms`); } - const planning = doc.planning_report; - if (planning && Array.isArray(planning.rounds)) { - parts.push(`${planning.rounds.length} tumbling-window-width round${planning.rounds.length === 1 ? '' : 's'} (${planning.rounds.map((r) => r.tumbling_window_width_domain).join(', ')})`); - if (planning.tumbling_window_widths_pending) parts.push('some tumbling-window widths not searched'); - } const counts = selectionTable(doc).pruned_candidate_count_by_cause; if (doc.search === 'exhaustive' && counts && Object.keys(counts).length) { parts.push(`pruned: ${Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([c, n]) => `${cause(c)} ×${n}`).join(', ')}`); diff --git a/tools/dag-viewer/test_viewer.py b/tools/dag-viewer/test_viewer.py index 4837ed785..6ba868453 100644 --- a/tools/dag-viewer/test_viewer.py +++ b/tools/dag-viewer/test_viewer.py @@ -489,7 +489,7 @@ def test_header_names_the_search_and_its_report(self): report = self.doc["stage3_plan_selection"]["search_report"] self.assertIn(f"search {report['status'].replace('_', ' ')}", text) self.assertIn(f"MILP {report['milp_variables']} variables", text) - self.assertIn("tumbling-window-width round", text) + self.assertNotIn("round", text) outputs = self.doc["stage3_plan_selection"]["selected_dag"]["node_of_output_reference"] self.assertEqual(len(outputs), 2) # Archived documents (before Stage 3's `Selection`) still render diff --git a/tools/validate_joint_workloads.py b/tools/validate_joint_workloads.py index 3e3e5b9f7..94903071d 100644 --- a/tools/validate_joint_workloads.py +++ b/tools/validate_joint_workloads.py @@ -3,7 +3,7 @@ Build stage_pipeline, copy it to a fixed path, then pass that path here. The binary SHA records exactly what ran even if another build updates the workspace. -Planning uses a cooperative 30s budget and an external 90s process deadline. +Each planning process is stopped after 90 s. """ import argparse import concurrent.futures @@ -50,7 +50,6 @@ def run(entry): '/usr/bin/time', '-v', '-o', str(timing), 'timeout', '90s', str(binary), '--workload', str(repo / 'tools/dag-viewer' / entry['workload']), - '--milp-time-limit-s', '30', '--cost-model', entry['cost_model'], '--enforce-accuracy-guarantees', '--deployment-capabilities', json.dumps(entry['deployment_capabilities']), '--out', str(plan_path),