Repository navigation
refactor(types)!: DataWorkload as arrival plus per-source facts; G bound per source - #728
Merged
Merged
Conversation
DataArrival carries rows_per_second and the PromQL sample_interval; sources maps each PromQL metric or SQL table to SourceFacts (series, metric_type, stream_norms, max_distinct_per_evaluation, one Evidence). Stage 3 binds G and the input row count from the series of the sources a query reads, and pricing from the sources each node reads. The frontends require the sample interval only for a bare instant selector, and no longer take a planning clock. WorkloadAccuracyEvidence, the generic Evidence<T> and the per-field evidence are removed.
…; G adds or branches
- SourceFacts gains sample_interval (was DataArrival's) and renames series
to series_count; DataArrival is AtRest | Ingesting { rows_per_second } |
Mixed { rows_per_second } | Unknown.
- The PromQL and MetricsQL frontends give each bare instant selector its
own metric's interval; MissingSampleInterval fires only when that metric
declares none, whatever the arrival.
- Evidence, is_fresh_at on source facts, EvidenceSource and
DemandEstimate.source are deleted: every fact is declared.
- Stage 3 bounds G and the input row count with a RowBound per node: a
union (PromQL or, SQL UNION, Concat) adds its branches and a constant
vector one row, while a join or fold of one source counts it once.
- price() builds each node's producers once; accuracy_check computes the
bound once per root.
- devtools declare --interval-ms on each metric read (arrival stays
unknown); sketch_coverage takes --sample-interval-ms.
…the run-a-query guide
A selector such as {job="a"} reads every metric, and a
Source::TimeSeries names exactly one, so lowering now rejects it with
"selector without one exact metric name", as the MetricsQL frontend
does, instead of scanning a source named "". The 29 Prometheus
testdata corpus queries with such selectors are counted as expected
unsupported.
SharedArrival only restated DataArrival without the rate. Callers pass a DataArrival with SHARED_ROWS_PER_SECOND, and the new candidate-label test from main declares its source.
The row bound of PromqlVectorFromScalar zeroed the copies of the sources below it, so series_read of vector(scalar(max(rate(x[5m])))) was 1 and the input row count shrank going up the tree. The bound now keeps them, and at_most_one_row gives a vector() root G = 1.
A source with zero series would bound the input row count at zero, while absent(m) over it still returns one row. DataWorkload::validate now rejects it with WorkloadError::ZeroSeriesCount.
No non-test code reads them: max_concurrency had no reader and peak_rate was only validated.
stage_pipeline's --interval-ms becomes --sample-interval-ms, the name the other lowering tools use; the stage viewer's server and the stage-viewer guide pass and document the new flag.
zzylol
force-pushed
the
data-workload-arrival-sources
branch
from
October 16, 2026 13:52
c8bb4bf to
a233d32
Compare
No planner code reads them; validation only checked them. Their types Confidence and ObservationWindow, and the errors EmptyObservationWindow and InvalidConfidence, go with them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
DataWorkloadmixed facts about how data arrives with facts about each source, and gave every field its ownEvidence<T>. One globalinput_cardinalitystood for the series of every source, so a workload over two metrics could not say how many series each has, and Stage 3 bound a query's number of valuesGfrom data the query may not read. One globaldata_ingestion_intervalstood for the sampling cadence of every metric, although metrics can be scraped at different intervals. Per-source facts (stream_norms,max_distinct_per_evaluation,metric_types) were already keyed by source, but in three separate maps.What
DataWorkloadnow has two parts:arrival: DataArrival:AtRest,Ingesting { rows_per_second },Mixed { rows_per_second }orUnknown.sources: BTreeMap<String, SourceFacts>, keyed by PromQL metric or SQL table. Each entry holdsseries_count(was the globalinput_cardinality, and must be positive),metric_type,sample_interval(was the globaldata_ingestion_interval),stream_normsandmax_distinct_per_evaluation.Every fact is declared. The planner has no clock, so freshness evidence is gone: a caller drops facts that no longer hold before planning.
sample_intervalof the metric it reads.{job="a"}, is now rejected withselector without one exact metric name, as MetricsQL already did. Before, it scanned a source named"".Gand the input row count by theseries_countof the sources each query reads. Each branch of anoror union now counts, which fixes an unsoundGforor.Selected plans and costs for Examples 1, 2, 3a, 3b and 4 are unchanged: the
stage_pipelinedocuments are byte-identical tomain(see Verification).How
Mapping.
data_ingestion_interval→sources[metric].sample_interval.ingestion_rate→arrival.{Ingesting,Mixed}.rows_per_second.input_cardinality→sources[source].series_count.stream_norms,max_distinct_per_evaluationandmetric_types→ the matchingSourceFactsfields.Validation.
DataWorkload::validatechecks that the rate is finite and non-negative. For each source, it checks thatsample_intervalandmax_distinct_per_evaluationare positive, and that the stream norms are finite and non-negative. It also rejectsseries_count: 0with the newWorkloadError::ZeroSeriesCount. A source with zero series has nothing to plan for, andabsent(m)over it still returns one row, which the bound would not count.Sample interval per metric (frontends). The PromQL frontend collects the metric names that a query's selectors read. It then looks up each one's interval with
PlanningWorkload::sample_interval_of, andinstant_sourcegives each bare instant selector its own metric's interval. MetricsQL does the same perMetricExpression.WorkloadError::MissingSampleIntervalfires only when a bare instant selector's metric declares no interval, whatever the arrival. Range-only queries need no interval, so Example 4's at-rest Pattern A still plans.lower_metricsql(query, accuracy, sample_interval)keeps its signature: its one interval holds for every metric.Selectors with no metric name (PromQL).
vs_partsrejects a selector that has neither a metric name nor an equality__name__matcher. ASource::TimeSeriesnames exactly one metric, so such a selector has no representation.collect_metric_names, and with itpromql_metric_names, skips the selectors that lowering rejects.no_metric_name) inpromql_corpus, which asserts that there are exactly 29. Examples:sum({idx="0"}),absent_over_time({job="grok"}[20m]),rate({env="1"}[10m]),{job="app-server"} == 80. The testdata floor goes from 1121 to 1097 lowered, which ismain's 1126 minus these 29. The docs corpus and the other three corpora have none.G counts what the root can return (Stage 3, soundness). On
main,Gwas the one globalinput_cardinality. Nowseries_readwalks the root and computes aRowBoundfor each node: copies per source, plus constant rows. It memoizes shared nodes. Per-source counting needs this walk because ofor:rate(x[5m]) or sum without (instance) (rate(x[5m]))returns up toseries(x)+ groups values;rate(x[5m]) or vector(0)returns one more.The
RowBoundof each node:Scancounts its source once.or, SQLUNION,Concat) adds its branches.vector(s)adds one constant row and keeps the copies of the sources below it, so the bound never shrinks going up the tree.at_most_one_rownow gives avector(s)rootG= 1.VALUEScontributes its rows.The result is Σ copies ×
series_count+ constant rows. It isNonewhen the root reads no source, or when a source it reads declares noseries_count. The same bound serves theinput_row_countstatistic.Pricing (Stage 3). On
main, every node was priced with the globalinput_cardinality. Now the newpriced_seriesprices each physical node with the sum ofseries_countover the sources it reads. Undeclared sources use the 1,000 default, as before.price()builds each node's producer list once fromdag.edges, and reuses it for the inputs, the sources andSummaryMerge's states.One bound per candidate.
accuracy_checkcomputesseries_readonce per root, then passes it tovalues_per_result(root, series_read)and tobound_root_guarantee(root, series, data, models).No planning clock. The frontends'
now_msargument,Evidence<T>with itsvalue_at, and Stage 3's freshness filter (value_at(ANY_TIME)) are gone. "Unbound" now means only thatseries_countis not declared, or that no source is read.DemandEstimate::is_fresh_atstays: recurrence and statistics read it with their own planning time.Deleted because no non-test code reads them:
WorkloadAccuracyEvidence(only re-exported).Evidence<T>(withvalue,source,observed_at_ms,valid_for_msandvalue_at).EvidenceSourceandDemandEstimate.source.DemandEstimate.max_concurrency(no reader), andDemandEstimate.peak_rate,confidenceandobservation_window(only validated).ConfidenceandObservationWindow, which only those fields used.WorkloadError::{UnavailableDataIngestionInterval, MissingPromqlDataWorkload, MissingDataIngestionInterval, AtRestWithPositiveIngestionRate}, andEmptyObservationWindowandInvalidConfidence, which checked the deleted fields.declared<T>(value) -> Evidence<T>.devtools.
DataWorkload::sampled_every(metrics, interval)andpromql_metric_names(query)letlower_promql_with_sample_intervalandstage_pipeline --promqldeclare the sample interval on each metric that the queries read.stage_pipeline's--interval-msbecomes--sample-interval-ms, the name thatsketch_coverage,variant_coverage,analyze_corporaandshow_logical_dagnow use (they had--data-ingestion-interval-ms). The stage viewer'stools/dag-viewer/server.pypasses the new flag.Unknown, as onmain.shared_data_workload(arrival, source)gains thesourceparameter. It declares that source'sseries_count(1,000,000) and its 15 ssample_interval. Callers pass the newSHARED_ROWS_PER_SECOND(1,000,000 / 15) in an arrivingDataArrival.pattern_a(recurrence, arrival: DataArrival)keeps its signature. The arrival now carries the rate, so callers passDataArrival::Mixed { rows_per_second: SHARED_ROWS_PER_SECOND }where they passedDataArrival::Mixed.Docs. Edited only where they describe the changed API:
docs/develop_docs/library-api.md: the PromQL lowering row and rules, theDataWorkloadparagraph, its JSON, the defaults table, and the four examples' data workloads.docs/design_docs/architecture/input-output-workflow.md: thedata_workloadrows, the flowchart node, and theDataWorkloadsection.docs/develop_docs/pre-asap-ir.md: theTimeRangeKind::Instantline.docs/design_docs/architecture/updated_interface_with_pluggable_optimization.md: theFrontendInputrow.docs/user_guide_docs/run-a-query.md: every--data-ingestion-interval-msbecomes--sample-interval-ms. The note on the flag now says that it is declared for every metric a query reads.docs/user_guide_docs/stage-viewer.md: the--sample-interval-msrow.Key code interfaces added / modified
crates/types/src/workload.rscrates/frontend-promql/src/lib.rs,crates/frontend-metricsql/src/lib.rs,crates/planner/src/lib.rscrates/asap-aware-mapping/src/stage3_plan_selection.rs(private)asap_aware_mapping::WorkloadAccuracyEvidenceis removed (no non-test reader).crates/devtools/src/planner_layering_examples.rs,crates/devtools/src/lib.rs,crates/devtools/src/bin/stage_pipeline.rsstage_pipeline --sample-interval-ms <u64>(was--interval-ms) is the sample interval of every metric the--promqlqueries read (default 15000).Usage
Example 1, end to end; then a bare instant selector over two metrics; then a selector with no metric name. I ran this against this head as a scratch
asap-devtoolsexample. It printed:If the panel had a whole-result target, Stage 3 would bound its
Gby 1,000,000 +series_count(up), because each branch of theorcounts.Before this PR
{job="a"}lowered to a scan of a metric named"".After this PR
Gand row count come from the sources it reads, and each branch of anorcounts.series_countof zero is rejected.Verification
Unchanged selections and costs. I ran
stage_pipelinefromorigin/main(5da6a64, which includes refactor(planner)!: Pass 1 candidates are exact or a summary; fused computations become the outer target's FusedComputation #729) and from this head (0c248d3) for these runs:--example planner-layering-1,-2,-3aand-3b, each under--cost-model usage-over-time(cost model 1) andprovisioned-capacity(cost model 2);planner-layering-3bunder--cost-model cluster-capacitywith 256 GB and 64 GB of memory and 1 TB of disk, each with thelowest-latency,least-memoryandpareto-frontobjectives;--promql "topk by (job) (10, rate(x[1m]))" --promql "sum(up)" --epsilon 0.01 --delta 0.001run.All 15 documents are byte-identical between the two.
mainwas built in its own target directory, so no artifact was shared. Selected candidates:Tests on the touched crates. I ran
cargo nextest runonasap-types,asap-frontend-promql,asap-frontend-metricsql,asap-aware-mapping,asap-physical-operators,asap-devtools,asap-integration-testsandasap-planner: 1667 passed, 35 skipped.python3 -m unittest test_serverintools/dag-viewerpasses.Checks. These are clean on this head:
cargo fmt --all -- --check;cargo clippy --workspace --all-targets --all-features --locked -- -D warnings;cargo test --doc -p asap-types -p asap-frontend-promql -p asap-frontend-metricsql.tools/verify_metricsql_external_consumer.shpasses, becauselower_metricsql(query, accuracy, Duration)keeps its signature.New or rewritten tests. Each fix test was written first, and failed on the previous head:
values_per_result_adds_the_branches_of_an_or, with x = 10 series:rate(x[5m]) or sum without (instance) (rate(x[5m]))rate(x[5m]) or vector(0)rate(x[5m]) / rate(x[5m])sum without (instance) (rate(x[5m])) * rate(x[5m])rate(x[5m]) + rate(m[5m])a_vector_of_a_scalar_keeps_the_rows_read_below_it(failed withseries_read= 1):series_readofvector(scalar(max(rate(x[5m]))))is 11 (10 series + 1 constant row), and itsGis 1;Gofvector(1)is 1;series_readofrate(x[5m]) or vector(scalar(max(rate(x[5m]))))is 21.stage3_binds_only_the_series_count_of_the_sources_read(wasstage3_binds_only_fresh_input_cardinality) checks that another source'sseries_countleaves the row count unbound.selector_without_metric_name_is_rejected__GAP(failed:{job="a"}lowered to a scan of"") covers{job="a"},rate({job="a"}[5m]),sum by (job) ({job="a"})and ahistogram_quantileover{le!=""}.lowering_is_total_over_the_entire_corpuscounts the 29 queries with no metric name apart, and asserts that count.each_instant_selector_uses_its_own_metrics_sample_interval: insum(data) + sum(other), the selectors look back 1 s and 30 s.instant_selector_needs_its_metrics_sample_intervalcovers every arrival and no data workload, with and without histograms. Its queries have bare, aggregated, subquery and mixed-metric selectors. A declared metric lowers whatever the arrival.workload_lowering_uses_the_declared_sample_interval.series_count_must_be_positive(failed: zero validated);canonical_workloads_have_a_strict_json_round_trip;the_former_data_workload_shape_is_rejected, which also rejectsevidence,seriesand an arrivalsample_interval;arrival_rate_and_sample_interval_are_validated;estimated_demand_validates_its_rate(wasestimated_demand_validates_window_rate_and_confidence): a negativeexpected_rateisInvalidRate;ingesting_payload_without_sample_interval_validates.sample_interval_ms_sets_the_instant_selector_lookback(failed: unknown argument). With--sample-interval-ms 30000,sum(up)looks back 30 s, and--interval-msis rejected.Limitations and follow-ups
docs/design_docs/proposals/still name the old fields (tracked in Update the docs after the G1–G11 consolidation groups land #677).__name__matcher also needs.