Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ad44988
test: acceptance spec and pending tests for planner-layering Example 1
zzylol Oct 3, 2026
ba5e3ff
test: run the Example 1 acceptance tests against the real stages
zzylol Oct 3, 2026
acd6fdc
test: read Example 1 logical stages as flat DAGs after the export rem…
zzylol Oct 8, 2026
0f8ad79
test: Example 1 acceptance on the composed stages (1 -> 3 -> 3 -> 1)
zzylol Oct 13, 2026
3f5174f
test: port #590's Example 2-4 acceptance specs and tests
zzylol Oct 13, 2026
21c988b
feat(devtools): stage_pipeline examples for #509 Example 3
zzylol Oct 13, 2026
074c51a
test: follow #576's per-consumer charging and exact top-k columns
zzylol Oct 13, 2026
748232f
refactor(devtools): one planning pipeline for stage_pipeline and the …
zzylol Oct 13, 2026
e2187ec
fix(devtools): select over every candidate; --max-displayed limits on…
zzylol Oct 13, 2026
a13361e
docs: Example 2's SQL candidates compose; the SQL test waits only on …
zzylol Oct 13, 2026
1c3b6a2
test: name stages by number and name; say query-language frontends
zzylol Oct 13, 2026
922bb7b
test: charging helper follows plan selection's per-consumer rule
zzylol Oct 13, 2026
9f29897
test: Example 1 picks Q2 candidates by (option, no shared input)
zzylol Oct 13, 2026
dc8a754
docs: Example 3 Pattern B charges each node once per consuming query
zzylol Oct 13, 2026
a04fe81
docs(stage-viewer): rows 2, 3a and 3b describe the planner's current …
zzylol Oct 13, 2026
62d4dd2
docs(stage-viewer): row 2 from the SQL run; 3a ties 32 plans, not 108
zzylol Oct 13, 2026
cad0f7e
test: Example 1 counts only unshared candidates; stage3_ charging-hel…
zzylol Oct 13, 2026
e2afcd0
refactor(devtools): PlanningStagesRun fields carry stage number and name
zzylol Oct 13, 2026
f669b69
test(devtools): unique temp file per stage_pipeline run
zzylol Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

356 changes: 111 additions & 245 deletions crates/devtools/src/bin/stage_pipeline.rs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions crates/devtools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
//! matching front-end crate directly — [`asap_frontend_promql`] (PromQL parser
//! only) or [`asap_frontend_sql`] (DataFusion only) — so it never compiles the
//! other's parser.
//!
//! [`planning_stages`] and [`planner_layering_examples`] hold the one
//! pipeline and the #509 example workloads that `stage_pipeline` and the
//! planner-layering acceptance tests share.

pub mod planner_layering_examples;
pub mod planning_stages;

pub use asap_frontend_promql::{
lower_promql_workload, lower_promql_workload_with_histograms, PromqlError,
Expand Down
233 changes: 233 additions & 0 deletions crates/devtools/src/planner_layering_examples.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//! The workloads of #509 Examples 1, 3 and 4
//! (`docs/design_docs/proposals/planner-layering.md`), shared by
//! `stage_pipeline` and the planner-layering acceptance tests.

use asap_types::types::AccuracyTarget;
use asap_types::workload::{
AccuracyRequirement, BatchEntry, DataArrival, DataDistribution, DataWorkload, DurationMs,
Evidence, EvidenceSource, LatencyRequirement, PlanningWorkload, Predictability, Query,
QueryLanguage, QueryRequirements, QueryTimeScope, QueryWorkload, Rate, RepeatedDemand,
RepeatingEntry, RepetitionInterval, TimeSelection, TimestampMs,
};

pub fn declared<T>(value: T) -> Evidence<T> {
Evidence {
value: Some(value),
source: EvidenceSource::Declared,
..Default::default()
}
}

pub const MINUTE_MS: u64 = 60_000;
/// PromQL's `y` is 365 days.
pub const YEAR_MS: u64 = 365 * 24 * 60 * MINUTE_MS;
/// Pattern A's batch execution time T (2026-01-01T00:00:00Z).
pub const T_MS: u64 = 1_767_225_600_000;

/// The #509 shared data workload with `arrival`.
pub fn shared_data_workload(arrival: DataArrival) -> DataWorkload {
DataWorkload {
data_ingestion_interval: declared(DurationMs(15_000)),
ingestion_volume: Evidence::default(),
// Data at rest has no ingestion (`validate` rejects a positive rate).
ingestion_rate: match arrival {
DataArrival::AtRest => Evidence::default(),
_ => declared(Rate(1_000_000.0 / 15.0)),
},
input_cardinality: declared(1_000_000),
distribution: declared(DataDistribution::Zipf),
arrival,
}
}

pub const EXAMPLE1_Q1: &str = "sum by (job) (rate(http_requests_total[1m]))";
pub const EXAMPLE1_Q2: &str = "topk by (job) (10, sum_over_time(http_requests_total[1m]))";

/// Example 1: two dashboard panels, every 10 s over the last minute.
pub fn example1() -> PlanningWorkload {
let panel = |query: &str, accuracy, response_latency| RepeatingEntry {
query: Query(query.into()),
demand: RepeatedDemand::FixedInterval(RepetitionInterval(10_000)),
requirements: QueryRequirements {
accuracy: AccuracyRequirement::Explicit(accuracy),
response_latency,
},
predictability: Predictability::Predictable { known_at: None },
time_selection: TimeSelection {
scope: QueryTimeScope::RealTime,
lookback: Some(DurationMs(MINUTE_MS)),
as_of: None,
},
};
PlanningWorkload {
query_workload: QueryWorkload {
language: QueryLanguage::PromQL,
query_batch: None,
repeating_queries: Some(vec![
panel(
EXAMPLE1_Q1,
AccuracyTarget::Exact,
LatencyRequirement::Unspecified,
),
panel(
EXAMPLE1_Q2,
AccuracyTarget::EpsilonDelta {
epsilon: 0.01,
delta: 0.001,
},
LatencyRequirement::ExplicitMaxMs(100.0),
),
]),
},
data_workload: Some(shared_data_workload(DataArrival::ContinuouslyIngesting)),
}
}

/// Pattern A's five queries: (PromQL, `lookback`, T − `as_of`).
pub const PATTERN_A: [(&str, u64, u64); 5] = [
("quantile_over_time(0.99, latency_ms[5y])", 5 * YEAR_MS, 0),
("quantile_over_time(0.99, latency_ms[1y])", YEAR_MS, 0),
(
"quantile_over_time(0.99, latency_ms[1y] offset 1y)",
YEAR_MS,
YEAR_MS,
),
(
"quantile_over_time(0.99, latency_ms[1y] offset 2y)",
YEAR_MS,
2 * YEAR_MS,
),
(
"quantile_over_time(0.99, latency_ms[3y] offset 2y)",
3 * YEAR_MS,
2 * YEAR_MS,
),
];

/// How Pattern A's batch recurs (Example 4 varies it).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatternARecurrence {
/// As given: one `ad_hoc` batch run once at T.
OnceAdHoc,
/// Example 4: repeated monthly and `Predictable { known_at }`; each run
/// reads the intervals ending at its own evaluation time.
MonthlyPredictable,
}

fn pattern_a_requirements() -> QueryRequirements {
QueryRequirements {
accuracy: AccuracyRequirement::Explicit(AccuracyTarget::EpsilonDelta {
epsilon: 0.005,
delta: 0.01,
}),
response_latency: LatencyRequirement::Unspecified,
}
}

/// Example 3, Pattern A: p99 over five historical intervals.
pub fn pattern_a(recurrence: PatternARecurrence, arrival: DataArrival) -> PlanningWorkload {
let (batch, repeating) = match recurrence {
PatternARecurrence::OnceAdHoc => (
Some(
PATTERN_A
.iter()
.map(|&(query, lookback, before_t)| BatchEntry {
query: Query(query.into()),
requirements: pattern_a_requirements(),
predictability: Predictability::AdHoc,
invocations: 1,
execute_at: Some(TimestampMs(T_MS)),
time_selection: TimeSelection {
scope: QueryTimeScope::Longitudinal,
lookback: Some(DurationMs(lookback)),
as_of: Some(TimestampMs(T_MS - before_t)),
},
})
.collect(),
),
None,
),
PatternARecurrence::MonthlyPredictable => (
None,
Some(
PATTERN_A
.iter()
.map(|&(query, lookback, _)| RepeatingEntry {
query: Query(query.into()),
demand: RepeatedDemand::FixedInterval(RepetitionInterval(
(30 * 24 * 60 * MINUTE_MS) as u32,
)),
requirements: pattern_a_requirements(),
predictability: Predictability::Predictable {
known_at: Some(TimestampMs(T_MS)),
},
time_selection: TimeSelection {
scope: QueryTimeScope::Longitudinal,
lookback: Some(DurationMs(lookback)),
as_of: None,
},
})
.collect(),
),
),
};
PlanningWorkload {
query_workload: QueryWorkload {
language: QueryLanguage::PromQL,
query_batch: batch,
repeating_queries: repeating,
},
data_workload: Some(shared_data_workload(arrival)),
}
}

pub const PATTERN_B: &str = "quantile_over_time(0.99, latency_ms[5m])";
pub const PATTERN_B_WINDOW_MS: u64 = 5 * MINUTE_MS;
pub const PATTERN_B_INTERVAL_MS: u64 = MINUTE_MS;

/// Example 3, Pattern B: a p99 panel over the last 5 min, every minute.
pub fn pattern_b() -> PlanningWorkload {
PlanningWorkload {
query_workload: QueryWorkload {
language: QueryLanguage::PromQL,
query_batch: None,
repeating_queries: Some(vec![RepeatingEntry {
query: Query(PATTERN_B.into()),
demand: RepeatedDemand::FixedInterval(RepetitionInterval(
PATTERN_B_INTERVAL_MS as u32,
)),
requirements: QueryRequirements {
accuracy: AccuracyRequirement::Explicit(AccuracyTarget::EpsilonDelta {
epsilon: 0.01,
delta: 0.01,
}),
response_latency: LatencyRequirement::ExplicitMaxMs(200.0),
},
predictability: Predictability::Predictable { known_at: None },
time_selection: TimeSelection {
scope: QueryTimeScope::RealTime,
lookback: Some(DurationMs(PATTERN_B_WINDOW_MS)),
as_of: None,
},
}]),
},
data_workload: Some(shared_data_workload(DataArrival::ContinuouslyIngesting)),
}
}

#[cfg(test)]
mod tests {
use super::*;

/// Data at rest has no ingestion rate, so the at-rest shared data
/// workload validates.
#[test]
fn at_rest_shared_data_workload_is_valid() {
shared_data_workload(DataArrival::AtRest)
.validate()
.unwrap();
pattern_a(PatternARecurrence::OnceAdHoc, DataArrival::AtRest)
.validate()
.unwrap();
}
}
Loading
Loading