Repository navigation
eval: instant RQEs read one scrape, not 1 s - #215
Conversation
A PromQL instant query reads one sample per series, but the synthetic spatial templates (sum by (job), quantile by (job)) looked back 1 s, so their N grew with the data rate (2e5 per job at 200/s, 1e4 at 10/s). They now look back one scrape interval (1 / samples_per_sec_per_series s), so N = series per job = 1e3 at any rate. autosketch_vs_asap scrapes a metric with samples_per_sec_per_series at that rate (record metrics keep 1 s), so a sub-second lookback holds whole scrapes, and writes sub-second windows and slides as fractional seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| // sample per series. Record metrics (http, flows) keep SCRAPE_MS. | ||
| let scrape_ms = workload["e2e"]["metrics"][metric]["samples_per_sec_per_series"] | ||
| .as_u64() | ||
| .map_or(SCRAPE_MS, |per_sec| 1_000 / per_sec); |
There was a problem hiding this comment.
1_000 / per_sec is integer division with no guard on the rate.
- A rate that doesn't divide 1000 truncates. At
--samples-per-sec 3, scrape_ms is 333. The temporal lookbacks (60000) and interval (1000) aren't multiples of 333, so lib.rs validation rejects every synthetic RQE. The same happens at 7, 30 and 300/s. - Above 1000/s, scrape_ms is 0 ("scrape interval is 0").
samples_per_sec_per_series: 0panics with divide-by-zero.
This needs a check that the rate divides 1000, here and in the exporter.
There was a problem hiding this comment.
Fixed in 813204b: the runner asserts that samples_per_sec_per_series is > 0 and divides 1000, so the scrape interval is whole ms. 3/s, 7/s, 0 and anything above 1000/s are rejected with a message naming the metric.
| // A metric with a per-series rate (the synthetic gauge and counter) is | ||
| // scraped at it, so an instant RQE's one-scrape lookback holds one | ||
| // sample per series. Record metrics (http, flows) keep SCRAPE_MS. | ||
| let scrape_ms = workload["e2e"]["metrics"][metric]["samples_per_sec_per_series"] |
There was a problem hiding this comment.
The committed input tables (results/autosketch-vs-asap-hydra/inputs/tables/*.json, e2e-rate10/inputs/tables/*.json) still have the old 1 s instant lookback, but they already carry samples_per_sec_per_series (200/s and 10/s). This line now reads them with a 5 ms / 100 ms scrape.
Instant RQEs then get candidate windows of 5 ms, 10 ms, … up to 1 s instead of only 1 s, and temporal RQEs get sub-second windows too. So the candidate sets, MILP and plans no longer match the committed results built from these same inputs, and nothing flags the mismatch.
Two options: regenerate those tables, or keep SCRAPE_MS when the table's lookback is the old one.
There was a problem hiding this comment.
Measured: on the committed 200/s tables (classic, all, multigroup at shared = 1), main's runner and this PR's give byte-identical plans. The sub-second windows get enumerated, but the reported candidate count is unchanged (420 / 574 / 154) and so is the MILP. What doesn't reproduce is candidate build time (see the other thread). So I'm keeping the finer grain rather than special-casing old tables. Regenerating the committed tables with one-scrape instant RQEs goes with #213's §6.3 rerun. Numbers are in the PR description, "Candidate windows at 200/s".
| cardinality: [(labels.clone(), 0), (LabelSet::new(), 1)].into(), | ||
| labels, | ||
| scrape_interval_ms: SCRAPE_MS, | ||
| scrape_interval_ms: scrape_ms, |
There was a problem hiding this comment.
With a sub-second scrape, temporal RQEs on synthetic streams also change, which contradicts the PR's claim that they don't. At 200/s, a 60 s lookback gives divisors(12000) = 48 candidate windows (5 ms … 60 s) instead of 12, and 24 h gives 220 instead of 96.
- If the MILP picks, say, a 500 ms window/slide, the temporal plan emits a fractional
window_secs, which ASAPQuery may not accept (design decision 4 is still unconfirmed). - The larger candidate set also inflates
candidate_build_secs/milp_solve_secs/planning_secs, which are used to compare AutoSketch with ASAP.
There was a problem hiding this comment.
Agreed, my claim was wrong; the description now says temporal RQEs get scrape-grained candidates. Keeping them deliberately: the engine supports sub-second windows, so that's the real search space, and more candidates can only lower ASAP's cost. Measured at 200/s: the MILP picks no sub-second window for any temporal RQE, solve time is unchanged, and candidate build time grows ~3–4× (classic 0.18 → 0.77 s, all 0.30 → 0.94 s). ASAP's planning time stays under 1 s against AutoSketch's 600 s of benchmarking, so the planning-time comparison doesn't move. This is design decision 6 in the description.
| gauge `data`, increments from the counter `data_total`. | ||
| """ | ||
| rate = SYNTHETIC_SERIES * SYNTHETIC_SAMPLES_PER_SEC # samples/s over all series | ||
| scrape = 1 / SYNTHETIC_SAMPLES_PER_SEC # an instant query's lookback, in s |
There was a problem hiding this comment.
1 / SYNTHETIC_SAMPLES_PER_SEC is float seconds, and nothing checks that it's a whole number of ms. The runner truncates it with (lookback_secs * 1000.0) as Millis, so at --samples-per-sec 3 or 7 the lookback becomes 333 / 142 ms. Neither lines up with the 1000 ms step or the temporal lookbacks, so the table can't be planned.
The exporter should reject rates that don't divide 1000.
There was a problem hiding this comment.
Fixed in 813204b: the exporter rejects a --samples-per-sec that doesn't divide 1000 (parser.error). The new test test_samples_per_sec_must_divide_1000 covers 3, 0 and 2000.
An instant RQE's lookback is one scrape, 1000 / rate ms, and the runner keeps lookbacks and scrape intervals in whole ms. A rate that doesn't divide 1000 (3/s gives 333 ms) truncates, and every synthetic RQE fails validation; above 1000/s the scrape is 0 ms, and 0 divides by zero. The exporter rejects such a --samples-per-sec, and the runner such a samples_per_sec_per_series. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| // A metric with a per-series rate (the synthetic gauge and counter) is | ||
| // scraped at it, so an instant RQE's one-scrape lookback holds one | ||
| // sample per series. Record metrics (http, flows) keep SCRAPE_MS. | ||
| let scrape_ms = workload["e2e"]["metrics"][metric]["samples_per_sec_per_series"] |
There was a problem hiding this comment.
If workload.e2e.metrics[metric] is missing (no e2e block, or a key mismatch: e2e keys swap / for _), scrape_ms silently falls back to 1000. An instant RQE with a 5 ms lookback then gets no candidate windows, so evaluate drops it as unservable before validate_facts runs. Only the dropped count is logged. This should fail loudly when the lookback/interval aren't multiples of the scrape.
There was a problem hiding this comment.
Fixed in 460e290. The runner now asserts that each RQE's lookback and interval are multiples of its stream's scrape, before it builds candidates. Checked by deleting e2e.metrics.data from a 10/s table: it now panics with …/sum_by_job/keys/1s: lookback 100 ms and interval 1000 ms must be multiples of …/job/value's 1000 ms scrape instead of dropping the RQE.
There was a problem hiding this comment.
(line 696) The docstring says the runner does not read the e2e block, but it now reads samples_per_sec_per_series from it for the scrape interval. test_classic_tables_are_main_s_all_tables_byte_for_byte deletes e2e before comparing (and its comment repeats the claim), so dropping or renaming that field wouldn't fail any test.
There was a problem hiding this comment.
Fixed in 460e290. The e2e_block docstring and the test comment now say the runner reads samples_per_sec_per_series. Before the byte-for-byte test drops e2e, it asserts that every RQE metric has samples_per_sec_per_series == 200. Checked: renaming the field fails the test.
There was a problem hiding this comment.
(line 519) as Millis truncates. Lookbacks are now fractional seconds (1/rate), so a value like 0.0049999 would become 4 ms, which isn't a multiple of the 5 ms scrape. Use .round() here (and for interval_ms).
There was a problem hiding this comment.
Fixed in 460e290: lookback_ms and interval_ms are .round()ed. No rate that divides 1000 truncates wrongly today (I checked all of them), but with the new assert a mis-rounded value would now fail loudly anyway.
A metric missing from the e2e block falls back to a 1 s scrape, so a 5 ms instant lookback had no candidate window and was dropped as unservable, with only the dropped count logged. The runner now asserts that each RQE's lookback and interval are multiples of its stream's scrape, and rounds them to ms instead of truncating. The e2e_block docstring and the byte-for-byte test no longer claim the runner ignores the e2e block; the test checks every RQE metric's samples_per_sec_per_series before dropping the block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry (#217) Regenerates rqe-optimizer/results/e2e-rate10 (classic, all, multigroup at shared = 1, 10 samples/s) on main: the denser grid's cost table and curves (#213), instant RQEs reading one scrape (#215), and one RQE per schema query (#216). Tables, --runs 3 results and --runs 1 plans; the README gives the RQE counts, costs, classic deployments and ASAPQuery's --data-ingestion-interval-ms 100. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
A PromQL instant query (
sum by (job) (data),quantile by (job) (0.9, data)) reads one sample per series. The synthetic spatial templates looked back 1 s whatever the data rate, so each per-job sketch held 2e5 items at 200 samples/s and 1e4 at 10/s instead of the 1e3 series in a job. Raising the feed rate made instant RQEs harder, unlike PromQL.export_autosketch_eval_table.py: the 6 classic instant RQEs (and the dashboard's quantile-by-job) look back one scrape interval,1 / samples_per_sec_per_seriess: 5 ms at 200/s, 100 ms at 10/s. N = 1e3 per job at any rate. Their ids still end in/1s(the step), so e2e queries and ids are unchanged.autosketch_vs_asap: the runner computes N from the scrape interval and series count, not the table'smax_N, and scraped every stream once a second. A 100 ms lookback held 0 scrapes and had no candidate windows. A metric withsamples_per_sec_per_series(the syntheticdata/data_total) is now scraped at that rate, with series = the real 1e4. Record metrics (http,flows) keep 1 s. Temporal RQEs see the same N and ingest rate as before, but their candidate windows are now every divisor of the lookback in scrapes, so sub-second windows are candidates too (see "Candidate windows at 200/s").--samples-per-sec(exporter) andsamples_per_sec_per_series(runner) must divide 1000, so a scrape is whole ms. 3/s, 0 and 2000/s are rejected.window_secs/slide_secsare fractional for sub-second deployments (they truncated to 0), and integers otherwise, so existing plans serialize byte for byte.Results: 10 samples/s replan
Classic, multigroup and all at shared = 1 (
--samples-per-sec 10),--runs 1, saturation dirstudy_merged(a local merged study, not the committedautosketch-vs-asap-inputs). The baseline is main's 10/s tables (the 1 s window) on the same inputs and runner, so only the instant window differs.Cheapest plan, no latency bound:
sum_by_job_sum_over_timemove. That RQE rode the instant 1 sexact-sumdeployment and now gets its own 60 s one (+1 deployment, +4% ASAP CPU).quantile_by_jobkeeps DDSketch α = 0.02 at N = 1e3.Candidate windows at 200/s
The full scrape-grained candidate set is kept, not restricted to whole seconds: the engine runs sub-second windows, and more candidates can only lower ASAP's cost. Measured on classic, all and multigroup at shared = 1, 200/s,
study_merged,--runs 1:planning_secswon't reproduce exactly until the tables are rerun.sum_by_job_sum_over_timeno longer shares the instant 1 s deployment (+1 deployment).No result files are committed:
study_mergedisn't in the repo (and differs from #213's inputs), sorqe-optimizer/results/e2e-rate10/stays as #212 left it until we replan on committed inputs.Design decisions (please approve)
1 / samples_per_sec_per_seriess. One sample per series, as PromQL reads it, so N stops depending on the rate. The alternative was keeping 1 s and calling it "everything in the last second" in the paper./1ssuffix. It's the step, so e2e query → RQE mappings and downstream consumers don't change. The lookback lives inlookback_secs.Caveats
Testing
cargo test --release -p rqe-optimizerandcargo clippy -D warningspass.scripts/test_export_autosketch_eval_table.py: 30 passed (a new test rejects rates that don't divide 1000).eval/dense-grid) is clean.🤖 Generated with Claude Code