diff --git a/asap-planner-rs/src/optimizer/aqe_extractor.rs b/asap-planner-rs/src/optimizer/aqe_extractor.rs index e4df49a..ec84f87 100644 --- a/asap-planner-rs/src/optimizer/aqe_extractor.rs +++ b/asap-planner-rs/src/optimizer/aqe_extractor.rs @@ -222,7 +222,7 @@ mod tests { ]; let aqes = extract_aqes(&rqes, &empty_schema()); assert_eq!(aqes.len(), 1); - // f_a = sum of rates (total query load for the MIP objective) + // query_frequency_hz = sum of rates (total query load for the MIP objective) let expected_freq = 1.0 / 60.0 + 1.0 / 30.0; assert!((aqes[0].query_frequency_hz - expected_freq).abs() < 1e-9); // min_t and gcd_t used for windowing constraints diff --git a/asap-planner-rs/src/optimizer/cost_model.rs b/asap-planner-rs/src/optimizer/cost_model.rs index 27c25ad..00ba0c9 100644 --- a/asap-planner-rs/src/optimizer/cost_model.rs +++ b/asap-planner-rs/src/optimizer/cost_model.rs @@ -64,72 +64,81 @@ impl Default for CostWeights { /// IngestCost(g): steady-state cost rate of keeping `candidate` deployed, /// independent of which AQEs query it (facility-location requirement). -/// `rho_g` is the arrival rate (items/sec) for this config's metric+filter. +/// `arrival_rate_hz` is the arrival rate (items/sec) for this config's metric+filter. pub fn ingest_cost( candidate: &CandidateConfig, - rho_g: f64, + arrival_rate_hz: f64, costs: &AtomicCosts, weights: &CostWeights, ) -> f64 { - let Some(g) = &candidate.config else { + let Some(agg_config) = &candidate.config else { return 0.0; // EXACT: no streaming config deployed. }; - // N(s,g) = 1 if subpopulation_aware else N_g (distinct label-group count). - // N_g isn't profiled yet (needs Prometheus series-count data) — use 1 as a - // placeholder; both branches collapse to the same value until that lands. - let n = 1.0_f64; + // Subpopulation count: 1 if subpopulation_aware else the distinct + // label-group count for this config. The label-group count isn't profiled + // yet (needs Prometheus series-count data) — use 1 as a placeholder; both + // branches collapse to the same value until that lands. + let subpopulation_count = 1.0_f64; // Defensive floor: slide_interval is a plain u64 on a widely-shared struct; // guard against div-by-zero producing `inf` and poisoning cost comparisons. - let n_concurrent = match g.window_type { + let n_concurrent = match agg_config.window_type { WindowType::Tumbling => 1.0, - WindowType::Sliding => (g.window_size as f64 / g.slide_interval.max(1) as f64).ceil(), + WindowType::Sliding => { + (agg_config.window_size as f64 / agg_config.slide_interval.max(1) as f64).ceil() + } }; - let mem_active = n_concurrent * n * costs.mem_bytes_per_instance; - let mem_retain = match g.window_type { - WindowType::Tumbling => candidate.n_windows as f64 * n * costs.mem_bytes_per_instance, + let mem_active = n_concurrent * subpopulation_count * costs.mem_bytes_per_instance; + let mem_retain = match agg_config.window_type { + WindowType::Tumbling => { + candidate.n_windows as f64 * subpopulation_count * costs.mem_bytes_per_instance + } WindowType::Sliding => 0.0, // already counted in mem_active's concurrent windows }; - let cpu_ingest = match g.window_type { - WindowType::Tumbling => rho_g * costs.insert_cpu_secs, - WindowType::Sliding => rho_g * n_concurrent * costs.insert_cpu_secs, + let cpu_ingest = match agg_config.window_type { + WindowType::Tumbling => arrival_rate_hz * costs.insert_cpu_secs, + WindowType::Sliding => arrival_rate_hz * n_concurrent * costs.insert_cpu_secs, }; weights.ingest_mem * (mem_active + mem_retain) + weights.ingest_cpu * cpu_ingest } -/// QueryCost(a,g): cost of answering one query for `a` from `candidate`. +/// QueryCost(a,g): cost of answering one query for `aqe` from `candidate`. pub fn query_cost( - _a: &AQE, + _aqe: &AQE, candidate: &CandidateConfig, costs: &AtomicCosts, weights: &CostWeights, ) -> f64 { - let Some(g) = &candidate.config else { + let Some(agg_config) = &candidate.config else { return costs.exact_query_cpu_secs * weights.query_cpu; // EXACT: raw query at query time. }; - let n = 1.0_f64; // N(s,g); see ingest_cost comment. - let props = sketch_properties(g.aggregation_type); + // Subpopulation count; see ingest_cost comment. + let subpopulation_count = 1.0_f64; + let props = sketch_properties(agg_config.aggregation_type); let (cpu, mem) = match &candidate.query_method { - QueryMethod::Direct => (n * costs.query_cpu_secs, n * costs.mem_bytes_per_instance), + QueryMethod::Direct => ( + subpopulation_count * costs.query_cpu_secs, + subpopulation_count * costs.mem_bytes_per_instance, + ), QueryMethod::Merge { num_windows } => { debug_assert!(props.mergeable); let merges = (*num_windows).saturating_sub(1) as f64; ( - n * (merges * costs.merge_cpu_secs + costs.query_cpu_secs), - *num_windows as f64 * n * costs.mem_bytes_per_instance, + subpopulation_count * (merges * costs.merge_cpu_secs + costs.query_cpu_secs), + *num_windows as f64 * subpopulation_count * costs.mem_bytes_per_instance, ) } QueryMethod::Subtract => { debug_assert!(props.subtractable); ( - n * (costs.subtract_cpu_secs + costs.query_cpu_secs), - 2.0 * n * costs.mem_bytes_per_instance, + subpopulation_count * (costs.subtract_cpu_secs + costs.query_cpu_secs), + 2.0 * subpopulation_count * costs.mem_bytes_per_instance, ) } // candidate_gen only ever pairs Exact with config=None, already handled above. @@ -141,18 +150,18 @@ pub fn query_cost( weights.query_cpu * cpu + weights.query_mem * mem } -/// Total cost rate contributed by assigning AQE `a` (with frequency `f_a` = -/// `a.query_frequency_hz`) to `candidate`: IngestCost(g) + f_a * QueryCost(a,g). +/// Total cost rate contributed by assigning AQE `aqe` (with frequency +/// `aqe.query_frequency_hz`) to `candidate`: IngestCost(g) + frequency * QueryCost(a,g). /// This is the per-(a,g) term the greedy/MIP solver minimizes. pub fn total_cost_rate( - a: &AQE, + aqe: &AQE, candidate: &CandidateConfig, - rho_g: f64, + arrival_rate_hz: f64, costs: &AtomicCosts, weights: &CostWeights, ) -> f64 { - ingest_cost(candidate, rho_g, costs, weights) - + a.query_frequency_hz * query_cost(a, candidate, costs, weights) + ingest_cost(candidate, arrival_rate_hz, costs, weights) + + aqe.query_frequency_hz * query_cost(aqe, candidate, costs, weights) } #[cfg(test)] diff --git a/asap-planner-rs/src/optimizer/greedy.rs b/asap-planner-rs/src/optimizer/greedy.rs index ffb0fe5..cd5a968 100644 --- a/asap-planner-rs/src/optimizer/greedy.rs +++ b/asap-planner-rs/src/optimizer/greedy.rs @@ -13,13 +13,13 @@ use super::solution::{AQEAssignment, OptimizerSolution, AQE}; /// two AQEs could share one. The Phase 3 MIP finds sharing opportunities; this /// is the v1 baseline. /// -/// `rho_g` is the per-item arrival rate used for every candidate's IngestCost. +/// `arrival_rate_hz` is the per-item arrival rate used for every candidate's IngestCost. /// Real per-config rates need Prometheus scrape-rate × series-count data, /// which isn't wired up yet — a single placeholder value is applied uniformly. pub fn greedy_assign( aqes: Vec, scrape_interval_secs: u64, - rho_g: f64, + arrival_rate_hz: f64, costs: &AtomicCosts, weights: &CostWeights, ) -> OptimizerSolution { @@ -35,7 +35,7 @@ pub fn greedy_assign( let best = candidates .into_iter() .map(|c| { - let cost = total_cost_rate(&aqe, &c, rho_g, costs, weights); + let cost = total_cost_rate(&aqe, &c, arrival_rate_hz, costs, weights); (c, cost) }) // total_cmp (not partial_cmp().unwrap()) so a stray NaN cost can't panic. @@ -43,7 +43,7 @@ pub fn greedy_assign( .map(|(c, _)| c) .expect("enumerate_candidates always returns at least the EXACT fallback"); - let ingest = ingest_cost(&best, rho_g, costs, weights); + let ingest = ingest_cost(&best, arrival_rate_hz, costs, weights); let query_rate = aqe.query_frequency_hz * query_cost(&aqe, &best, costs, weights); let query_method = best.query_method.clone(); diff --git a/asap-planner-rs/src/optimizer/pipeline.rs b/asap-planner-rs/src/optimizer/pipeline.rs index ff4a106..854bb5f 100644 --- a/asap-planner-rs/src/optimizer/pipeline.rs +++ b/asap-planner-rs/src/optimizer/pipeline.rs @@ -57,20 +57,20 @@ pub fn run_all_exact_pipeline( /// No cross-AQE sharing — every deployed sketch serves exactly one AQE, even /// if two AQEs could share one. The Phase 3 MIP finds sharing opportunities. /// -/// `rho_g` is a placeholder arrival rate applied uniformly to every candidate's -/// IngestCost; real per-config rates need Prometheus scrape-rate × series-count -/// data, which isn't wired up yet (see implementation plan TODOs). +/// `arrival_rate_hz` is a placeholder arrival rate applied uniformly to every +/// candidate's IngestCost; real per-config rates need Prometheus scrape-rate × +/// series-count data, which isn't wired up yet (see implementation plan TODOs). pub fn run_greedy_pipeline( config: &ControllerConfig, schema: &PromQLSchema, scrape_interval_secs: u64, - rho_g: f64, + arrival_rate_hz: f64, ) -> (StreamingConfig, InferenceConfig) { run_pipeline(config, schema, "greedy", |aqes| { greedy_assign( aqes, scrape_interval_secs, - rho_g, + arrival_rate_hz, &AtomicCosts::default(), &CostWeights::default(), ) diff --git a/asap-planner-rs/src/optimizer/solution.rs b/asap-planner-rs/src/optimizer/solution.rs index 5e0924c..5ca974c 100644 --- a/asap-planner-rs/src/optimizer/solution.rs +++ b/asap-planner-rs/src/optimizer/solution.rs @@ -14,7 +14,7 @@ pub struct AQE { /// Preserved for use by the translator when building InferenceConfig. pub query_strings: Vec, - /// Query frequency in Hz: f_a (this field) = Σ_{r ∈ R_a} 1/T_r. + /// Query frequency in Hz: this field = Σ_{r ∈ R_a} 1/T_r. /// Used in the MIP objective to convert per-query QueryCost into a cost /// rate (cost/sec) commensurate with the continuously-accruing IngestCost. /// Represents the total query load from all dashboards independently @@ -70,8 +70,7 @@ pub struct AQEAssignment { /// How this AQE's answer is derived from the assigned config. pub query_method: QueryMethod, - /// Estimated cost rate for this assignment: f_a * QueryCost(a, g), where - /// f_a is `aqe.query_frequency_hz`. + /// Estimated cost rate for this assignment: QueryCost(a, g) * `aqe.query_frequency_hz`. /// Zero for Exact assignments (IngestCost is also zero). pub estimated_query_cost_per_sec: f64, }