Skip to content

Commit 741ddec

Browse files
feat(precompute): apply spatial filters during ingest routing (#724)
* feat(planner): load profiled atomic costs * feat(precompute): apply spatial filters during ingest routing * fix(precompute): validate planner spatial filters * fix(precompute): use supported aggregation type in routing tests
1 parent d123e46 commit 741ddec

13 files changed

Lines changed: 914 additions & 71 deletions
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Cost-based optimizer decision map
2+
3+
Goal: turn a PromQL workload with query frequencies and dataset-conditioned
4+
sketch measurements into an offline deployment plan, then collect evidence that
5+
the selected plan improves the stated objective without violating accuracy.
6+
7+
## Progress log
8+
9+
- 2026-09-07: Located the existing offline planner entry point:
10+
`asap-optimizer-cli`. `Controller::generate()` still takes the hardcoded
11+
planner path, so production wiring is intentionally not the first milestone.
12+
- 2026-09-07: Verified a wire-contract gap: sketch-bench emits a versioned,
13+
workload-profiled atomic-cost document, while ASAPQuery currently loads a
14+
legacy flat entry array. A profile-aware loader and explicit selector are the
15+
first integration change.
16+
- 2026-09-07: Distinguished grouping labels from sketch keys. Grouping labels
17+
determine how many sketch instances the planner deploys; sketch-key
18+
properties belong to a measured benchmark profile.
19+
- 2026-09-07: Chosen first vertical slice: KLL backing PromQL
20+
`quantile_over_time`. The next unresolved concrete inputs are a source
21+
metric, KLL input value/key column, trace time slice, and grouping labels.
22+
- 2026-09-07: Candidate external workload specs already present in
23+
`sketch-bench`: Alibaba microservices CPU (`cpu_utilization`, grouped by
24+
`msname`, one-minute slice); Google task CPU (`cpu_rate`, grouped by
25+
`machine_id`, three-minute slice); and Datadog/BOOM (`target`, scalar,
26+
one-minute slice). Existing ASAPQuery Prometheus replay configurations also
27+
exist for Alibaba node CPU and Google CPU. Recommended first case: Google
28+
CPU, because it has both a sketch-bench workload spec and a matching
29+
Prometheus replay configuration with explicit labels.
30+
- 2026-09-07: Verified the selected Google trace is available locally:
31+
`google-cluster-data/ClusterData2011/clusterdata-2011-2/task_usage/part-00262-of-00500.csv.gz`
32+
(92 MB). The external loader supports numeric f64 grouped workloads, and the
33+
selected spec uses `cpu_rate`, grouped by `machine_id`, over a three-minute
34+
time slice. `kll-percall` is the correct benchmark variant because the
35+
ASAPQuery runtime invokes KLL `quantile()` per query. Remaining prerequisites:
36+
(1) a matching `quantile_over_time` workload and series-inventory CSV,
37+
(2) profile-aware atomic-cost loading/selection in ASAPQuery, and
38+
(3) explicit accuracy, arrival-rate, and exact-baseline assumptions.
39+
- 2026-09-07: Inspected sketch-bench PR #124. It is the correct producer-side
40+
contract: a versioned document, one profile per exact external workload,
41+
required `query_accuracy`, and no cross-profile merging. Decision: do not
42+
add an ASAPQuery-specific `logical_metric` field to PR #124. Its
43+
`value_column` identifies the physical trace column measured; a Prometheus
44+
metric name is a query-engine identity that may rename or transform that
45+
column. ASAPQuery owns the explicit mapping from a queried metric to a
46+
benchmark profile. Do not add KLL key columns for this slice: KLL consumes
47+
numeric values and keyed external workloads are intentionally unsupported.
48+
ASAPQuery should mirror the versioned types, select exactly one external
49+
profile from an explicit selector, and pass only its entries to the existing
50+
candidate resolver.
51+
- 2026-09-07: Corrected an earlier naming example after inspecting collector
52+
code. `google_mean_cpu_usage_rate_0` is an older experiment-specific name;
53+
the existing evaluation mapper emits `google_cluster_2019_cpu_rate` from
54+
raw `cpu_rate`, and creates a `_q_kll` alias only to route a second copy of
55+
the same samples through KLL. These are naming/routing conventions, not
56+
distinct source values. For the first slice, use one canonical raw-stream
57+
identity throughout (recommended Prometheus-safe name:
58+
`google_task_usage_cpu_rate`), and do not require a separate logical-metric
59+
field in the sketch-bench document.
60+
- 2026-09-07: Validated the PR-123/PR-124 dependency analysis. PR 123 changes
61+
the underlying benchmark-record schema; PR 124 is the atomic-cost consumer
62+
break and must be handled via its independent document schema version. Two
63+
implementation corrections: `query_accuracy` is a required map of named
64+
metrics, not one `f64`; and workload selection can happen once at the
65+
ASAPQuery load boundary, returning the selected profile's existing flat
66+
`AtomicCostTable`. This avoids threading workload identity through every
67+
candidate resolver while still rejecting zero or ambiguous matches before
68+
any candidate lookup.
69+
- 2026-09-07: Implemented the ASAPQuery consumer seam on branch
70+
`feat/profiled-atomic-cost-loader`. It mirrors PR 124's versioned document,
71+
external workload, required accuracy map, strict schema validation, and
72+
exact single-profile selection. The two offline optimizer CLIs now require a
73+
JSON `profiles[].workload` selector whenever `--atomic-costs` is supplied;
74+
selected entries preserve the existing flat resolver interface. Focused
75+
loader and full planner tests pass (216 tests plus doc-tests); the latter was
76+
run outside the sandbox because its existing ClickHouse mock opens a local
77+
listener.
78+
- 2026-09-07: Review follow-up: centralized CLI profile loading behind one
79+
optimizer module interface and added explicit zero-match coverage. Synthetic
80+
profile descriptions remain opaque JSON because sketch-bench's data-generator
81+
schema is independently versioned; document, profile, entry, and external
82+
workload fields remain strict at this consumer boundary.
83+
84+
## #1: What identifies a benchmark cost profile?
85+
86+
Type: Discuss
87+
88+
### Question
89+
90+
Which dataset properties are expected to change insert, merge, query, memory,
91+
or accuracy enough that they must select a distinct atomic-cost profile? Decide
92+
the first paper-scale profile matrix and the semantics for choosing a profile at
93+
planning time.
94+
95+
### Answer
96+
97+
Open. `sketch-bench` already emits `AtomicCostDocument { schema_version,
98+
profiles: [{ workload, entries }] }`; the optimizer currently reads the older
99+
flat entry array and therefore has no profile-selection rule. The available
100+
observability corpus is under `../benchmarks/metrics_observability/data` from
101+
the workspace root (Datadog/BOOM and Alibaba traces). Candidate framing from
102+
the user: `(dataset_name, metric_name, keying/aggregated-label names,
103+
time-range)`. The remaining decision is whether labels and range identify an
104+
atomic measurement or instead parameterize the structural cost formula.
105+
106+
Clarification: **grouping labels** are the PromQL `GROUP BY` labels and
107+
partition the metric stream into separate sketch instances. A **sketch key**
108+
is the value or label tuple inserted into keyed sketches such as HLL, CMS, and
109+
Hydra. Grouping labels and their observed distinct-group count are planner
110+
context; the sketch-key distribution/cardinality (and, where relevant, encoded
111+
key size) belong in the benchmark profile.
112+
113+
For the first vertical slice, scope this decision to one KLL
114+
`quantile_over_time` workload. Do not define the full cross-dataset matrix yet.
115+
116+
## #2: What is the minimum credible empirical planning loop?
117+
118+
Blocked by: #1
119+
Type: Prototype
120+
121+
### Question
122+
123+
What end-to-end experiment should prove that measured costs, rather than
124+
hand-tuned constants, change a planner decision appropriately for a fixed
125+
PromQL workload and series inventory?
126+
127+
### Answer
128+
129+
Open. The existing offline `asap-optimizer-cli` is the intended harness after
130+
the document-loader/profile-selection gap is closed.
131+
132+
## #3: What feasibility evidence constrains optimization?
133+
134+
Blocked by: #1, #2
135+
Type: Discuss
136+
137+
### Question
138+
139+
For each query/sketch/config/dataset case, which accuracy metric and threshold
140+
make a candidate eligible, and how will exact-query cost and arrival rate be
141+
measured rather than assumed?
142+
143+
### Answer
144+
145+
Open. `sketch-bench` already retains capability-specific `query_accuracy`, but
146+
the ASAPQuery greedy optimizer does not use it; arrival rate (`rho`) and exact
147+
query cost are currently placeholders.

‎.design_docs/optimizer-v1-implementation-plan.md‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,8 @@ cargo run -p asap_planner --bin asap-optimizer-cli -- \
322322
--dataset <path/to/series-inventory.csv> \
323323
--data-ingestion-interval-ms 60000 \
324324
[--rho 1.0] \
325-
[--atomic-costs <path/to/atomic_costs.json>]
325+
[--atomic-costs <path/to/atomic_costs.json> \
326+
--atomic-cost-workload <path/to/workload-selector.json>]
326327
```
327328

328329
Takes the same `ControllerConfig` YAML format as `asap-planner --input_config`.
@@ -332,7 +333,11 @@ group count; no live Prometheus connection is needed. A `metrics:` hints block,
332333
present, is checked against the dataset and mismatches fail loudly.
333334
Prints deployed streaming configs and query configs to stdout. `--rho` is the
334335
placeholder arrival rate (see TODOs below — not real yet). `--atomic-costs` is
335-
optional; omit it and ordinary unbenchmarked candidates use the flat stub, while
336+
optional; when supplied it requires `--atomic-cost-workload`, a JSON file
337+
containing the exact `profiles[].workload` value from that benchmark artifact.
338+
The loader validates the document schema and rejects a selector that matches
339+
zero or multiple profiles; it never mixes entries across workloads. Omit both
340+
flags and ordinary unbenchmarked candidates use the flat stub, while
336341
CMS-with-heap candidates warn and are dropped until a matching reference row is available.
337342

338343
### Running with real sketch-bench costs
@@ -352,13 +357,15 @@ CMS-with-heap candidates warn and are dropped until a matching reference row is
352357
# See what each candidate would cost, before selection:
353358
cargo run -p asap_planner --bin candidate-gen-dump -- \
354359
--input_config workload.yaml --data-ingestion-interval-ms 60000 \
355-
--atomic-costs path/to/atomic_costs.json
360+
--atomic-costs path/to/atomic_costs.json \
361+
--atomic-cost-workload path/to/workload-selector.json
356362

357363
# Run the actual optimizer:
358364
cargo run -p asap_planner --bin asap-optimizer-cli -- \
359365
--input_config workload.yaml --data-ingestion-interval-ms 60000 \
360366
--dataset path/to/series-inventory.csv \
361-
--atomic-costs path/to/atomic_costs.json
367+
--atomic-costs path/to/atomic_costs.json \
368+
--atomic-cost-workload path/to/workload-selector.json
362369
```
363370

364371
`candidate-gen-dump`'s output labels each resolved params row `[real]` or `[stub]`; candidates

‎CONTEXT.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ASAPQuery
2+
3+
Terms used by the cost-based sketch optimizer and its benchmarking pipeline.
4+
5+
## Language
6+
7+
**Grouping labels**:
8+
PromQL `GROUP BY` labels that partition a metric stream into label groups; a
9+
non-subpopulation sketch is instantiated once per distinct group.
10+
_Avoid_: keying labels
11+
12+
**Sketch key**:
13+
The value or label tuple inserted into and queried from a keyed sketch such as
14+
HLL, CMS, or Hydra. Its distribution, cardinality, and encoded size may affect
15+
the measured atomic cost and accuracy.
16+
_Avoid_: grouping label, partition key
17+
18+
**Benchmark profile**:
19+
A reproducible description of the input trace slice used to measure an
20+
atomic-cost table for a sketch family and configuration.
21+
22+
**Planner context**:
23+
Query- and deployment-specific structural inputs, such as label-group count
24+
and retained-window count, that scale atomic costs into a plan cost.

‎asap-planner-rs/src/bin/candidate_gen_dump.rs‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use std::path::PathBuf;
66

77
use asap_planner::{
88
optimizer::{
9-
enumerate_candidates, extract_aqes, load_atomic_cost_table, resolve_atomic_costs,
10-
AtomicCostTable, AtomicCosts, CandidateConfig, RQE,
9+
enumerate_candidates, extract_aqes, load_optional_selected_atomic_cost_table,
10+
resolve_atomic_costs, AtomicCostTable, AtomicCosts, CandidateConfig, RQE,
1111
},
1212
ControllerConfig,
1313
};
@@ -28,21 +28,26 @@ struct Args {
2828
#[arg(long = "data-ingestion-interval-ms")]
2929
scrape_interval_ms: u64,
3030

31-
/// Path to sketch-bench's exported atomic-cost table (see ASAPQuery#524).
31+
/// Path to sketch-bench's versioned atomic-cost document.
3232
/// When given, each params row also prints its resolved AtomicCosts --
3333
/// real (from the table) or the flat stub (unbenchmarked family, or this
3434
/// exact param point missing from the table) -- labeled which.
3535
#[arg(long = "atomic-costs")]
3636
atomic_costs: Option<PathBuf>,
37+
38+
/// JSON `profiles[].workload` value selecting exactly one measured profile.
39+
#[arg(long = "atomic-cost-workload", requires = "atomic_costs")]
40+
atomic_cost_workload: Option<PathBuf>,
3741
}
3842

3943
fn main() -> anyhow::Result<()> {
4044
let args = Args::parse();
4145

42-
let atomic_cost_table = match &args.atomic_costs {
43-
Some(path) => load_atomic_cost_table(path)?,
44-
None => AtomicCostTable::default(),
45-
};
46+
let atomic_cost_table = load_optional_selected_atomic_cost_table(
47+
args.atomic_costs.as_deref(),
48+
args.atomic_cost_workload.as_deref(),
49+
)?
50+
.unwrap_or_default();
4651

4752
let yaml_str = std::fs::read_to_string(&args.input_config)?;
4853
let config: ControllerConfig = serde_yaml::from_str(&yaml_str)?;

‎asap-planner-rs/src/bin/optimizer_cli.rs‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use std::path::PathBuf;
88

99
use asap_planner::optimizer::{
10-
load_atomic_cost_table, run_greedy_pipeline, AtomicCostTable, SeriesDataset,
10+
load_optional_selected_atomic_cost_table, run_greedy_pipeline, AtomicCostTable, SeriesDataset,
1111
};
1212
use asap_planner::ControllerConfig;
1313
use clap::Parser;
@@ -35,14 +35,21 @@ struct Args {
3535
#[arg(long = "rho", default_value = "1.0", value_parser = parse_positive_finite)]
3636
rho: f64,
3737

38-
/// Path to the atomic-cost table sketch-bench's `atomic-costs` subcommand
39-
/// exports (see ASAPQuery#524, sketch-bench#30). Omitted: every
38+
/// Path to the versioned atomic-cost document sketch-bench's `atomic-costs`
39+
/// subcommand exports. Requires --atomic-cost-workload to select exactly
40+
/// one measured workload profile. Omitted: every
4041
/// benchmarked-family candidate (CMS/HLL/KLL) is dropped, since there is
4142
/// no data to cost it at — only trivial accumulators and EXACT remain
4243
/// selectable.
4344
#[arg(long = "atomic-costs")]
4445
atomic_costs: Option<PathBuf>,
4546

47+
/// JSON `profiles[].workload` value copied from the sketch-bench atomic-cost
48+
/// document. This makes the empirical workload profile explicit and avoids
49+
/// mixing costs from different traces or time windows.
50+
#[arg(long = "atomic-cost-workload", requires = "atomic_costs")]
51+
atomic_cost_workload: Option<PathBuf>,
52+
4653
#[arg(short, long, action = clap::ArgAction::Count)]
4754
verbose: u8,
4855
}
@@ -70,8 +77,11 @@ fn main() -> anyhow::Result<()> {
7077
let config: ControllerConfig = serde_yaml::from_str(&yaml_str)?;
7178
let dataset = SeriesDataset::from_path(&args.dataset)?;
7279

73-
let atomic_cost_table = match &args.atomic_costs {
74-
Some(path) => load_atomic_cost_table(path)?,
80+
let atomic_cost_table = match load_optional_selected_atomic_cost_table(
81+
args.atomic_costs.as_deref(),
82+
args.atomic_cost_workload.as_deref(),
83+
)? {
84+
Some(table) => table,
7585
None => {
7686
tracing::warn!(
7787
"no --atomic-costs supplied; CMS/HLL/KLL candidates will never be selected"

0 commit comments

Comments
 (0)