Repository navigation
eval: export ASAPQuery random workload manifests - #226
Conversation
--random-manifest writes the eval table of an ASAPQuery random workload (asap-tools random_workloads.py, ProjectASAP/ASAPQuery#879): its common-promql queries as schema RQEs on metric `data`, so a fine sum can serve a coarse one, quantiles per series (avg of per-series quantiles), and top-k heaps below the curves' 1,000 keys read there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
milindsrivastava1997
left a comment
There was a problem hiding this comment.
Code review findings (via Claude Code).
| **DEFAULT_TARGETS, | ||
| } | ||
| qs.append(q) | ||
| e2e.append({"promql": query["promql"], "interval_secs": int(t), |
There was a problem hiding this comment.
Leaving expr to #228, which is stacked on this PR; they should land together.
| by_name = {label["name"]: label for label in labels} | ||
| for name in grouping: | ||
| parent = by_name[name].get("child_of") | ||
| assert parent is None or parent in grouping, (grouping, name) |
There was a problem hiding this comment.
This asserts that a child label's parent is in the grouping instead of adding the parent, so a valid child-only grouping crashes the export. E.g. sum by (label_0) (...), where label_0 is a child of job, raises AssertionError. closed_labels() already adds parents.
There was a problem hiding this comment.
Fixed in 5f1d374: the export now uses closed_labels(), so sum by (label_0) counts 2 × 500 groups (test added).
| TOPK_MIN_CURVE_KEYS = 1_000 | ||
|
|
||
|
|
||
| def random_groups(labels, grouping): |
There was a problem hiding this comment.
random_groups() copies the fan-out logic of closed_labels()/grouping_cardinality() only because those take a SCHEMAS metric name. The copies have already drifted (this one asserts where the original adds parents). Letting closed_labels take a list of labels would remove the copy and fix the crash above.
There was a problem hiding this comment.
Fixed in 5f1d374: closed_labels() takes a label list, labels_cardinality() wraps it for any labels, and random_groups() is gone.
| "alpha_rank": alpha, "alpha_memory": alpha, "tail_class": "", | ||
| "families": SYNTHETIC_FAMILIES["topk" if keys else capability], | ||
| "capability": "topk" if keys else capability, | ||
| "stream": f"data/{RANDOM_STREAMS[capability]}", "metric": "data", |
There was a problem hiding this comment.
Any common-promql capability other than sum, quantile or top-k stops the whole export with a bare KeyError here (and in SYNTHETIC_FAMILIES), and no table is written. Better to skip such queries with a message, or reject them with a clear error.
There was a problem hiding this comment.
Fixed in 5f1d374: queries of other capabilities are left out with a stderr line, and the table is still written (test added).
| continue | ||
| [rqe] = query["rqes"] | ||
| capability = rqe["capability"] | ||
| grouping = names if capability == "quantile" else rqe["grouping_labels"] |
There was a problem hiding this comment.
Every quantile is assumed to be avg by (G) of per-series quantiles; the query's PromQL and grouping_labels are never checked. A cross-series quantile by (job) (0.9, data) would be exported as a per-series RQE with min_N = per_sec * s, so its planned sketch size and error are silently wrong.
There was a problem hiding this comment.
Fixed in 5f1d374: a quantile's PromQL must match avg by (<grouping_labels>) (quantile_over_time(phi, data[W])), the only shape random_workloads.py writes; anything else is refused (test added).
| query_id, keys, assumptions = query["id"], None, [] | ||
| if capability == "topk_by_value": | ||
| query_id = f"topk{rqe['k']}_{query_id}" | ||
| keys = series // groups |
There was a problem hiding this comment.
This reads a top-k's grouping_labels as the heap's grouping (keys = series // groups). #225 says ASAPQuery's QueryRequirements.grouping_labels for a top-k is the per-series labels it returns. Under that reading, topk(4, data) has grouping ['job', 'label_0'], giving 1,000 heaps of 1 key (clamped, with an assumption) instead of one heap over 1,000 keys. Which reading is right?
There was a problem hiding this comment.
In the manifest, a top-k's grouping_labels is its topk by grouping, the heaps' buckets: random_workloads.py writes only topk by (G), with G as grouping_labels. So keys per heap = series / groups is right here. #225's reading (the per-series labels returned) is ASAPQuery's QueryRequirements, a different field; #228's expr maps between the two (grouping per-series, topk_by G).
| labels = manifest["labels"] | ||
| names = [label["name"] for label in labels] | ||
| series = random_groups(labels, names) | ||
| per_sec = manifest["samples_per_sec_per_series"] |
There was a problem hiding this comment.
samples_per_sec_per_series is copied without checking that it's an integer that divides 1000. The runner reads it with as_u64() and asserts 1000 % per_sec == 0, so a rate of 3/s panics only after the table is written, and a rate stored as 10.0 makes as_u64() return None and the runner silently falls back to SCRAPE_MS.
There was a problem hiding this comment.
Fixed in 5f1d374: the rate must be an int dividing 1000, else ValueError before anything is written (3 and 10.0 tested).
| kind = "values" if capability == "quantile" else "keys" | ||
| q = { | ||
| "dataset": dataset, "query_id": query_id, "kind": kind, | ||
| "range": f"{s:g}s", "range_s": s, "step_s": int(t), "theta": theta, "K": keys, |
There was a problem hiding this comment.
f"{s:g}s" keeps only 6 significant digits, and this string is part of the RQE id. E.g. lookback_ms 1_234_567 gives 1234.57s while range_s is 1234.567, and lookbacks that differ below 6 digits can get the same id, with no uniqueness check on the table.
There was a problem hiding this comment.
Fixed in 5f1d374: the range is {ms // 1000}s for whole seconds, else {ms}ms, and duplicate RQE ids are refused. Workload 01's ids are unchanged.
| "covered_min_N": n_items, "max_covered_share": 1 / groups, | ||
| "covered_max_N": n_items, | ||
| "assumptions": assumptions, | ||
| **DEFAULT_TARGETS, |
There was a problem hiding this comment.
Each manifest RQE carries an accuracy_sla (0.95 today), but this always applies DEFAULT_TARGETS. Nothing breaks yet, but a manifest with a stricter SLA would get sketches sized for the wrong accuracy and nothing would record it. Either read it or assert it matches the defaults.
There was a problem hiding this comment.
Fixed in 5f1d374: an accuracy_sla other than 0.95 (DEFAULT_TARGETS, the runner's p95) is refused.
| print(f"Done. {len(tables)} `synthetic` tables and {len(plan)} planned runs in " | ||
| f"{args.out}", file=sys.stderr) | ||
| return 0 | ||
| if args.random_manifest: |
There was a problem hiding this comment.
--random-manifest is checked after --synthetic, so --synthetic --random-manifest m.json writes the synthetic grid and ignores the manifest. --samples-per-sec and --keep-duplicate-queries are accepted with it but have no effect. A mutually exclusive group, or a parser error, would make that visible.
There was a problem hiding this comment.
Fixed in 5f1d374: --random-manifest with --synthetic, --summary, --samples-per-sec or --keep-duplicate-queries is a parser error.
- closed_labels() takes a label list, so random manifests reuse it: a child-only grouping counts its parents' groups instead of failing. - Other capabilities are left out (logged); a quantile that isn't an avg of per-series quantiles, an accuracy_sla other than 0.95, and a rate that doesn't divide 1000 are refused. - RQE ids carry the exact range; duplicate ids are refused. - --random-manifest refuses the flags it would ignore. Workload 01's table is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per-series quantiles that differ only in their avg's grouping read the same per-series RQE; ASAPQuery's --planner saved refuses one leaf naming two RQEs. They now share the first query's id. Workloads 03, 04, 06, 08 and 09 of seed 862 hit this; workload 01's table is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
milindsrivastava1997
left a comment
There was a problem hiding this comment.
Code review: 10 findings, posted inline. The first three are correctness bugs in random_manifest_table.
| ms, t = rqe["lookback_ms"], rqe["interval_ms"] / 1000 | ||
| assert t == int(t), f"{query['id']}: interval {t} s is not whole seconds" | ||
| # Queries reading one RQE (per-series quantiles that differ only in | ||
| # their avg's grouping) share its id. |
There was a problem hiding this comment.
Correctness: the dedup key uses rqe.get("phi"), not the phi in the PromQL. If the RQE has no phi field (the test manifest's q3 has none), quantile_over_time(0.5, data[100s]) and quantile_over_time(0.99, data[100s]) get the same key, and the p99 is served from the p50's RQE. The regex already captures phi; put it in the key.
There was a problem hiding this comment.
Holding: the quantile branch is being reworked (pooled quantiles grouped by G instead of avg of per-series), which changes this check; will fix with that rework.
| e2e.append({"promql": query["promql"], "interval_secs": int(t), | ||
| "rqes": [shared[key]]}) | ||
| continue | ||
| groups = labels_cardinality(labels, grouping) |
There was a problem hiding this comment.
Correctness: RQEs aren't checked against the curves' first N (SCHEMA_MIN_COVERED_N = 1e3), as schema_queries() does. A per-series quantile_over_time(0.9, data[10s]) at 10/s covers only 100 samples, and an instant top-k over 2 jobs covers 500; both are exported, and the planner has to extrapolate to size them.
There was a problem hiding this comment.
Fixed in ba319e0: RQEs under the curves' first N (1e3 items per group per window) are left out with a log line, and their e2e query with them (test added).
| f"{RANDOM_ACCURACY_SLA} (DEFAULT_TARGETS) is exported") | ||
| grouping = rqe["grouping_labels"] | ||
| if capability == "quantile": | ||
| m = AVG_OF_SERIES_QUANTILES.match(query["promql"]) |
There was a problem hiding this comment.
Correctness: the PromQL window data[...] is never compared with lookback_ms, and its phi is never compared with the RQE's phi. A data[5m] query with lookback_ms 100000 passes, so the RQE is sized for 100 s while the e2e query reads 5 m.
There was a problem hiding this comment.
Holding: the quantile branch is being reworked (pooled quantiles grouped by G instead of avg of per-series), which changes this check; will fix with that rework. #228's full PromQL match may cover it too.
| grouping = rqe["grouping_labels"] | ||
| if capability == "quantile": | ||
| m = AVG_OF_SERIES_QUANTILES.match(query["promql"]) | ||
| if not m or m[1].split(", ") != grouping: |
There was a problem hiding this comment.
The grouping check compares m[1].split(", ") as an ordered list, so avg by (job,label_0) or avg by (label_0, job) raises a ValueError even though the shape is supported. Strip each label and compare sets.
There was a problem hiding this comment.
Holding: the quantile branch is being reworked (pooled quantiles grouped by G instead of avg of per-series), which changes this check; will fix with that rework.
| f"{query['promql']!r}") | ||
| grouping = names | ||
| ms, t = rqe["lookback_ms"], rqe["interval_ms"] / 1000 | ||
| assert t == int(t), f"{query['id']}: interval {t} s is not whole seconds" |
There was a problem hiding this comment.
A bare assert is skipped under python -O. With interval_ms 500, int(t) truncates the interval to 0 and a zero query interval is exported. Raise a ValueError like the other input checks.
There was a problem hiding this comment.
Fixed in ba319e0: a non-whole-second interval (e.g. 500 ms) is a ValueError (test added).
| [rqe] = query["rqes"] | ||
| capability = rqe["capability"] | ||
| if capability not in RANDOM_STREAMS: | ||
| print(f"left out {query['id']}: no RQE for capability {capability!r}", |
There was a problem hiding this comment.
No e2e dedup on (promql, interval_secs), unlike e2e_block(). Two manifest queries with the same PromQL and interval produce two identical e2e entries, so the query runs twice and its latency is counted twice.
There was a problem hiding this comment.
Fixed in ba319e0: a repeated (PromQL, interval) is one e2e entry (test added).
| parser.add_argument("--random-manifest", | ||
| help="write the eval table of an ASAPQuery random workload manifest " | ||
| f"(asap-tools random_workloads.py), its {RANDOM_SUPPORT} queries, " | ||
| "to --out instead") |
There was a problem hiding this comment.
--synthetic, --summary, --samples-per-sec and --keep-duplicate-queries are refused with --random-manifest, but --curves, --saturation, --curves-big, --saturation-big and --merge-curves are dropped without a warning, and inputs records only the manifest hash. Refuse those too.
There was a problem hiding this comment.
Fixed in ba319e0: --curves, --curves-big, --saturation, --saturation-big and --merge-curves are refused with --random-manifest too.
| labels = manifest["labels"] | ||
| names = [label["name"] for label in labels] | ||
| series = labels_cardinality(labels, names) | ||
| per_sec = manifest["samples_per_sec_per_series"] |
There was a problem hiding this comment.
isinstance(per_sec, int) accepts a bool: true passes and is exported as rate 1. This divisibility check is also written separately in main()'s --samples-per-sec check; a shared helper would keep the two from drifting.
There was a problem hiding this comment.
Fixed in ba319e0: divides_1000() rejects bools and is shared with --samples-per-sec's check.
| } | ||
| qs.append(q) | ||
| shared[key] = synthetic_entry(q)["id"] | ||
| e2e.append({"promql": query["promql"], "interval_secs": int(t), |
There was a problem hiding this comment.
Cleanup: synthetic_entry(q) runs inside the loop only to get the id, then again for every q after the loop. Keep the entry built in the loop. ids.count() makes the duplicate-id check O(n²); collections.Counter does it in one pass.
There was a problem hiding this comment.
Fixed in ba319e0: each entry is built once in the loop; the duplicate-id check uses collections.Counter.
| with open(args.random_manifest) as f: | ||
| manifest = json.load(f) | ||
| inputs = {os.path.basename(args.random_manifest): sha256(args.random_manifest)} | ||
| table = random_manifest_table(manifest, args.revision, inputs) |
There was a problem hiding this comment.
Cleanup: this copies build()'s table-writing block (makedirs, json.dump with indent=1 and sort_keys, trailing newline). Write the table once after both branches so the two can't drift.
There was a problem hiding this comment.
Fixed in ba319e0: run() writes the table once after the --random-manifest/build() branch.
- RQEs below the curves' first N (1e3 items per group) are left out (logged), as schema_queries() does. - A sub-second interval is a ValueError, not an assert; a bool rate is refused; divides_1000() is shared with --samples-per-sec. - A repeated query is one e2e entry. - The curve flags are refused with --random-manifest. - Each RQE entry is built once; one table writer for both modes. Seed 862's ten tables are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
milindsrivastava1997
left a comment
There was a problem hiding this comment.
Automated code review: 10 findings (7 correctness, 3 cleanup).
| if entry not in e2e: | ||
| e2e.append(entry) | ||
| continue | ||
| groups = labels_cardinality(labels, grouping) |
There was a problem hiding this comment.
The SCHEMA_MIN_COVERED_N check also drops exact sum RQEs, and their e2e queries, even though the exact-sum family reads no accuracy curve. For example, sum by (job,label_0) (sum_over_time(data[60s])) at 10/s has n_items=600 < 1000, so it is removed from both rqes and e2e. Instant sums with a one-scrape lookback are always dropped. Suggest limiting the check to non-exact families.
There was a problem hiding this comment.
Fixed in e62a15e: the check now applies only to sketch families; exact accumulators read no curve (test added).
| if interval_ms <= 0 or interval_ms % 1000: | ||
| raise ValueError(f"{query['id']}: interval {interval_ms} ms is not whole " | ||
| "seconds") | ||
| t = interval_ms // 1000 |
There was a problem hiding this comment.
lookback_ms is never checked against the scrape period (1000/samples_per_sec ms). At 10/s, a lookback of 1_234_567 ms (as in test_ranges_in_rqe_ids_are_exact) or 150 ms exports fine, then from_table() in autosketch_vs_asap.rs panics on lookback_ms.is_multiple_of(scrape_ms). The exporter should reject this input, as it already does for interval_ms.
There was a problem hiding this comment.
Fixed in e62a15e: a lookback that isn't a multiple of the scrape period is a ValueError (test added).
| for query in manifest["queries"]: | ||
| if query["support"] != RANDOM_SUPPORT: | ||
| continue | ||
| [rqe] = query["rqes"] |
There was a problem hiding this comment.
[rqe] = query['rqes'] assumes exactly one RQE per common-promql query. A binary or ratio query (e.g. quantile/quantile) or an empty rqes list aborts with ValueError: too many values to unpack and no query id. Suggest skipping with a log line or raising a clear error that names the query.
There was a problem hiding this comment.
Fixed in e62a15e: a common-promql query with other than one RQE is a ValueError naming the query (test added).
| RANDOM_STREAMS = {"sum": "value", "quantile": "dist", "topk_by_value": "value"} | ||
| # The fewest distinct keys the top-k curves measure; a heap over fewer keys | ||
| # is read there, which overstates its error. | ||
| TOPK_MIN_CURVE_KEYS = 1_000 |
There was a problem hiding this comment.
The AVG_OF_SERIES_QUANTILES regex requires avg by (...) with a non-empty, ', '-joined label list. avg(quantile_over_time(0.9, data[100s])) with grouping_labels=[], or avg by (job,label_0) with no space, raises 'a quantile must be avg by ...' and the whole export fails.
There was a problem hiding this comment.
Holding for the quantile rework (pooled quantiles grouped by G), which replaces this check.
| if capability == "quantile": | ||
| m = AVG_OF_SERIES_QUANTILES.match(query["promql"]) | ||
| if not m or m[1].split(", ") != grouping: | ||
| raise ValueError(f"{query['id']}: a quantile must be avg by " |
There was a problem hiding this comment.
This check validates only the avg grouping. It never checks that the PromQL's phi or range matches the RQE's phi/lookback_ms, yet RQE sharing is keyed on the RQE fields alone. PromQL quantile_over_time(0.99, data[5m]) with an RQE of phi 0.9 and lookback 100000 exports without error and may share another query's RQE id. The e2e query then reads a different window and quantile than the planner sized.
There was a problem hiding this comment.
Holding for the quantile rework, as with the earlier phi/window comment.
| s = ms / 1000 | ||
| n_items = rate * s / groups | ||
| # Accuracy is read off the curves, which start at this N. | ||
| if n_items < SCHEMA_MIN_COVERED_N: |
There was a problem hiding this comment.
For top-k heaps with fewer than 1,000 keys, K is raised to 1,000 while min_N, groups and theta stay the same. Top-k by (job, label_0) has 1 key per heap but is exported with K=1000, so the plan's cost and error for that RQE are off by orders of magnitude. Only an assumptions string records this.
There was a problem hiding this comment.
Intentional: the 500-key heaps of seed 862 read at 1,000 keys is conservative (fewer keys, fewer collisions). Leaving the clamp as is; tiny heaps don't occur in these workloads.
| # --random-manifest: the five-system queries of an ASAPQuery random workload | ||
| # (asap-tools random_workloads.py), and the streams their capabilities read. | ||
| RANDOM_SUPPORT = "common-promql" | ||
| RANDOM_STREAMS = {"sum": "value", "quantile": "dist", "topk_by_value": "value"} |
There was a problem hiding this comment.
sum and topk_by_value both map to the stream data/value, but autosketch_vs_asap builds one MetricFacts per stream and assumes 'A stream is one value of its metric, so one capability'. With both a sum and a topk at the same grouping, the stream's facts (including hydra_dataset) take the first RQE's capability, and data_shape is merged across capabilities.
There was a problem hiding this comment.
Harmless today (metric data has no Hydra dataset, and exact sums give no data shape), but agreed it's cleaner to split: deferring to the quantile rework, which regenerates the archived tables anyway.
| print(f"left out {query['id']}: {n_items:g} items per group per window, " | ||
| f"below the curves' first N ({SCHEMA_MIN_COVERED_N:g})", file=sys.stderr) | ||
| continue | ||
| query_id, keys, assumptions = query["id"], None, [] |
There was a problem hiding this comment.
Cleanup: the TOPK_MIN_CURVE_KEYS clamp and the below-curves skip are special cases in the exporter, and each path behaves differently: schema_queries raises, while this path logs and drops. Handling N or K below the first curve point once, in the curve lookup, would remove both copies and keep them from drifting apart.
There was a problem hiding this comment.
Agreed in principle, but it's a curve-lookup refactor in the optimizer, out of scope here. Deferring.
| rqes.append(synthetic_entry(q)) | ||
| shared[key] = rqes[-1]["id"] | ||
| e2e.append({"promql": query["promql"], "interval_secs": t, "rqes": [shared[key]]}) | ||
| duplicates = sorted(i for i, n in collections.Counter(r["id"] for r in rqes).items() |
There was a problem hiding this comment.
Cleanup: this duplicate-RQE-id check repeats build()'s existing assert len(ids) == len(set(ids)) and can't be reached through the shared path. One shared helper over rqes would do the job in both places.
There was a problem hiding this comment.
Fixed in e62a15e: check_unique_ids() is shared by build() and the random-manifest path.
| # Queries reading one RQE (per-series quantiles that differ only in | ||
| # their avg's grouping) share its id. | ||
| key = (capability, tuple(grouping), ms, t, rqe.get("phi"), rqe.get("k")) | ||
| if key in shared: |
There was a problem hiding this comment.
Nit: entry not in e2e is a linear scan over a list of dicts, so the dedup is quadratic. The shared dict already holds the key for an O(1) check. A set keyed on (promql, interval_secs), or reusing e2e_block's unique, would avoid the scan.
There was a problem hiding this comment.
Fixed in e62a15e: the dedup uses a set of (promql, interval_secs).
- The curves' first-N check skips exact families, which read no curve. - A lookback that isn't whole scrapes, or a query reading other than one RQE, is a ValueError naming the query. - check_unique_ids() is shared with build(); the e2e dedup is a set. Seed 862's ten tables are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for-random-manifest-tables Takes #226's shared-RQE e2e entries and gives every entry, including one reading a shared RQE, its tree (e2e_query()). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gKTCAEz3sJ1Hzde16xxU1
* eval: RQE expression trees in e2e tables Every e2e query gets `expr`, its RQE expression tree: a leaf per RQE it reads (id, metric or table, capability, lookback_ms, grouping, phi/k/value), and a `/` node for template 10. Rate queries, and schema queries merged across several RQEs (--keep-duplicate-queries), get none. Regenerate the e2e-rate10 tables: only `expr` is added, so the RQE ids and plans are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gKTCAEz3sJ1Hzde16xxU1 * eval: RQE expression trees for random-manifest tables --random-manifest e2e queries carry `expr` (#227, ProjectASAP/ASAPQuery#886): avg by (G) of per-series quantile_over_time is an avg over a per-series quantile leaf; topk by (G) of sum_over_time a per-series top-k leaf with `topk_by` G (left out for a global top-k); sum by (G) of sum_over_time and spatial quantile by (G) a leaf at G; SQL cardinality a leaf on its table with `value`. Counter-gated top-k and rate have none. Leaf ids are the table's RQE ids; values come from the manifest's RQE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gKTCAEz3sJ1Hzde16xxU1 * eval: leaf capabilities as a set, renaming only topk Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gKTCAEz3sJ1Hzde16xxU1 * eval: random-manifest trees from RQE fields, not PromQL random_expr() builds each tree from the manifest RQE's capability, grouping_labels, phi and k, as asap-tools' rqe_query() renders them: quantile is avg by G of a per-series quantile leaf, topk_by_value a per-series top-k leaf ranked within G, sum a leaf at G. No PromQL regexes, so no list-order label comparison; the spatial quantile shape (read as a group quantile from a per-series RQE) and the unreachable SQL cardinality branch go. A test builds seed-862 workload-01's table and checks every tree names its query's RQE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gKTCAEz3sJ1Hzde16xxU1 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Adds
--random-manifesttoscripts/export_autosketch_eval_table.py. It writes the eval table for an ASAPQuery random workload manifest (asap-tools random_workloads.py, ProjectASAP/ASAPQuery#879), soautosketch_vs_asapcan plan it and ASAPQuery's--planner savedcan convert the plan.Only the manifest's
common-promqlqueries are exported (the five-system arm). Each is a schema RQE on metricdata, using the manifest's labels:avg by (G)of per-series quantiles, so the RQE is per series: 1,000 samples per 100s window at 10/s.assumptions.The e2e block holds the manifest's PromQL with one RQE per query.
Testing:
python3 scripts/test_export_autosketch_eval_table.py: 33 pass, including a new test for the export.asap/cpuplan rolls the coarse sum up from the fine one, and the plan converts in ASAPQuery (archived in ASAPQuery#879).🤖 Generated with Claude Code