Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 227 additions & 16 deletions scripts/export_autosketch_eval_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"""

import argparse
import collections
import csv
import hashlib
import itertools
Expand Down Expand Up @@ -307,6 +308,20 @@ def rqe_entry(q, curves, curves_big, sat, sat_big, merge):
KEEP_DUPLICATE_QUERIES = False


def check_unique_ids(rqes):
duplicates = sorted(i for i, n in collections.Counter(r["id"] for r in rqes).items()
if n > 1)
if duplicates:
raise ValueError(f"duplicate RQE ids: {duplicates}")


def divides_1000(rate):
"""Whether series scraped `rate` times a second are scraped every whole
number of ms, as the runner requires."""
return (isinstance(rate, int) and not isinstance(rate, bool) and rate > 0
and 1000 % rate == 0)


def set_samples_per_sec(rate, skip_below_curves):
"""Scrapes each series `rate` times a second (SYNTHETIC_SAMPLES_PER_SEC)
and scales the schema metrics' rates with it; with `skip_below_curves`,
Expand Down Expand Up @@ -343,10 +358,10 @@ def all_subsets(metric, names=None):
]


def closed_labels(metric, grouping):
def closed_labels(schema_labels, grouping):
"""`grouping`'s labels with every child's ancestors (a child value names
its parent), in schema order."""
labels = {label["name"]: label for label in SCHEMAS[metric]["labels"]}
its parent), in the order of `schema_labels` (SCHEMAS' format)."""
labels = {label["name"]: label for label in schema_labels}
closed, todo = set(), list(grouping)
while todo:
name = todo.pop()
Expand All @@ -359,7 +374,12 @@ def closed_labels(metric, grouping):
def grouping_cardinality(metric, grouping):
"""Groups of `grouping` on `metric`: the product of its labels' fan-outs,
with every child's ancestors included."""
return math.prod(label["cardinality"] for label in closed_labels(metric, grouping))
return labels_cardinality(SCHEMAS[metric]["labels"], grouping)


def labels_cardinality(schema_labels, grouping):
"""grouping_cardinality() on `schema_labels` (SCHEMAS' format)."""
return math.prod(label["cardinality"] for label in closed_labels(schema_labels, grouping))


def label_shares(metric, label):
Expand All @@ -381,7 +401,7 @@ def group_coverage(metric, grouping, covers_share):
`covers_share`, plus always the largest group (None: every group). A group's share is the product of
its labels' shares: labels are drawn independently given their parents."""
marginals = [sorted(label_shares(metric, label), reverse=True)
for label in closed_labels(metric, grouping)]
for label in closed_labels(SCHEMAS[metric]["labels"], grouping)]
largest = math.prod(m[0] for m in marginals)
if covers_share is None:
return (grouping_cardinality(metric, grouping), math.prod(m[-1] for m in marginals),
Expand Down Expand Up @@ -429,17 +449,19 @@ def point_id(point):
return base if metrics == 1 else f"{base}/metrics={metrics}"


def rqe_leaf(rqe, metric, capability, lookback_s, grouping, phi=None, k=None, value=None):
def rqe_leaf(rqe, metric, capability, lookback_s, grouping, phi=None, k=None, value=None,
topk_by=None):
"""An expression-tree leaf reading RQE `rqe`, whose output is grouped by
`grouping`; None for a capability no leaf reads (rate)."""
`grouping` (a top-k ranks within each `topk_by` group, else over all);
None for a capability no leaf reads (rate)."""
if capability not in LEAF_CAPABILITIES:
return None
if capability == "topk":
capability = "topk_by_value"
leaf = {"id": rqe, "metric": metric, "capability": capability,
"lookback_ms": round(lookback_s * 1000), "grouping": list(grouping)}
leaf.update((key, v) for key, v in [("phi", phi), ("k", k), ("value", value)]
if v is not None)
leaf.update((key, v) for key, v in [("phi", phi), ("k", k), ("value", value),
("topk_by", topk_by)] if v is not None)
return {"rqe": leaf}


Expand Down Expand Up @@ -809,6 +831,179 @@ def e2e_block(qs, queries):
return {"queries": list(unique.values()), "metrics": metrics}


# --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"}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

# 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding for the quantile rework (pooled quantiles grouped by G), which replaces this check.

# DEFAULT_TARGETS' accuracy level (the runner's p95), the only SLA exported.
RANDOM_ACCURACY_SLA = 0.95
# The one quantile shape random_workloads.py writes: an avg of per-series
# quantiles, grouped by the RQE's grouping_labels.
AVG_OF_SERIES_QUANTILES = re.compile(
r"^avg by \(([\w, ]+)\) \(quantile_over_time\([\d.]+, data\[\w+\]\)\)$")


def random_expr(rqe, rqe_id, series_labels):
"""The RQE expression tree of a manifest query whose one RQE is `rqe`
(the table's `rqe_id` on `data`), built from the RQE's fields as
asap-tools' rqe_query() renders them, or None for another capability. A leaf's
grouping is its output labels; G is the RQE's grouping_labels:
- quantile, `avg by (G) (quantile_over_time(phi, data[W]))`: avg by G of
a per-series quantile leaf;
- sum, `sum by (G) (sum_over_time(data[W]))`: a leaf grouped by G;
- topk_by_value, `topk [by (G)] (k, sum_over_time(data[W]))`: a
per-series top-k leaf ranked within each G (`topk_by`, left out for a
global top-k)."""
capability, grouping = rqe["capability"], rqe["grouping_labels"]

def leaf(capability, grouping, **extra):
return rqe_leaf(rqe_id, "data", capability, rqe["lookback_ms"] / 1000, grouping, **extra)

if capability == "quantile":
return {"agg": "avg", "by": grouping,
"of": leaf("quantile", series_labels, phi=rqe["phi"])}
if capability == "topk_by_value":
return leaf("topk", series_labels, k=rqe["k"], topk_by=grouping or None)
if capability == "sum":
return leaf("sum", grouping)
return None


def e2e_query(query, rqe, t, rqe_id, series_labels):
"""The e2e entry of manifest `query` (its RQE `rqe`) reading the table's
`rqe_id`, possibly shared with other queries, with its tree when it has one."""
entry = {"promql": query["promql"], "interval_secs": t, "rqes": [rqe_id]}
expr = random_expr(rqe, rqe_id, series_labels)
return {**entry, "expr": expr} if expr else entry


def random_manifest_table(manifest, revision, inputs):
"""The eval table of `manifest`'s RANDOM_SUPPORT queries: one RQE each
on the schema metric `data`, its labels the manifest's. Every series is
scraped at the same rate, so a grouping's groups hold equal shares. A
sum RQE is at its query's grouping, and a top-k's heaps at its `topk by`
grouping, on one stream, so a fine sum can serve a coarse one; a
quantile is `avg by` of per-series quantiles, so its RQE is per series.
Queries of other capabilities are left out (logged). The e2e block holds
the PromQL and the query's RQE expression tree (random_expr())."""
labels = manifest["labels"]
names = [label["name"] for label in labels]
series = labels_cardinality(labels, names)
per_sec = manifest["samples_per_sec_per_series"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5f1d374: the rate must be an int dividing 1000, else ValueError before anything is written (3 and 10.0 tested).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ba319e0: divides_1000() rejects bools and is shared with --samples-per-sec's check.

if not divides_1000(per_sec):
raise ValueError(f"samples_per_sec_per_series {per_sec!r} must be an "
"integer dividing 1000")
rate = series * per_sec
facts = manifest["data_facts"]
theta = facts["series"]["ranking"]["exponent"]
alpha = facts["metrics"]["data"]["pareto_shape"]
dataset = f"random/seed={manifest['seed']}/{manifest['id']}"
scrape_ms = 1000 // per_sec
rqes, e2e, shared, e2e_keys = [], [], {}, set()
for query in manifest["queries"]:
if query["support"] != RANDOM_SUPPORT:
continue
if len(query["rqes"]) != 1:
raise ValueError(f"{query['id']}: reads {len(query['rqes'])} RQEs; only "
"single-RQE queries are exported")
[rqe] = query["rqes"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e62a15e: a common-promql query with other than one RQE is a ValueError naming the query (test added).

capability = rqe["capability"]
if capability not in RANDOM_STREAMS:
print(f"left out {query['id']}: no RQE for capability {capability!r}",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ba319e0: a repeated (PromQL, interval) is one e2e entry (test added).

file=sys.stderr)
continue
if rqe["accuracy_sla"] != RANDOM_ACCURACY_SLA:
raise ValueError(f"{query['id']}: accuracy_sla {rqe['accuracy_sla']}; only "
f"{RANDOM_ACCURACY_SLA} (DEFAULT_TARGETS) is exported")
grouping = rqe["grouping_labels"]
if capability == "quantile":
m = AVG_OF_SERIES_QUANTILES.match(query["promql"])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

if not m or m[1].split(", ") != grouping:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

raise ValueError(f"{query['id']}: a quantile must be avg by "

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding for the quantile rework, as with the earlier phi/window comment.

f"({', '.join(grouping)}) of per-series quantiles: "
f"{query['promql']!r}")
grouping = names
ms, interval_ms = rqe["lookback_ms"], rqe["interval_ms"]
if interval_ms <= 0 or interval_ms % 1000:
raise ValueError(f"{query['id']}: interval {interval_ms} ms is not whole "
"seconds")
# The optimizer's windows are whole scrapes.
if ms <= 0 or ms % scrape_ms:
raise ValueError(f"{query['id']}: lookback {ms} ms is not a multiple of the "
f"{scrape_ms} ms scrape")
t = interval_ms // 1000

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e62a15e: a lookback that isn't a multiple of the scrape period is a ValueError (test added).

# Queries reading one RQE (per-series quantiles that differ only in
# their avg's grouping) share its id.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

key = (capability, tuple(grouping), ms, t, rqe.get("phi"), rqe.get("k"))
if key in shared:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e62a15e: the dedup uses a set of (promql, interval_secs).

if (query["promql"], t) not in e2e_keys:
e2e_keys.add((query["promql"], t))
e2e.append(e2e_query(query, rqe, t, shared[key], names))
continue
groups = labels_cardinality(labels, grouping)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e62a15e: the check now applies only to sketch families; exact accumulators read no curve (test added).

s = ms / 1000
n_items = rate * s / groups
families = SYNTHETIC_FAMILIES["topk" if capability == "topk_by_value" else capability]
# A sketch's accuracy is read off the curves, which start at this N;
# exact accumulators read none.
exact = all(family in EXACT_SKETCH for family in families)
if not exact and n_items < SCHEMA_MIN_COVERED_N:
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, []

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed in principle, but it's a curve-lookup refactor in the optimizer, out of scope here. Deferring.

if capability == "topk_by_value":
query_id = f"topk{rqe['k']}_{query_id}"
keys = series // groups

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

if keys < TOPK_MIN_CURVE_KEYS:
assumptions.append(f"{keys} keys per heap read at the curves' "
f"{TOPK_MIN_CURVE_KEYS}")
keys = TOPK_MIN_CURVE_KEYS
kind = "values" if capability == "quantile" else "keys"
q = {
"dataset": dataset, "query_id": query_id, "kind": kind,
# Part of the RQE id, so exact.
"range": f"{ms // 1000}s" if ms % 1000 == 0 else f"{ms}ms",
"range_s": s, "step_s": t, "theta": theta, "K": keys,
"min_N": n_items, "max_N": n_items,
"alpha_rank": alpha, "alpha_memory": alpha, "tail_class": "",
"families": families,
"capability": "topk" if keys else capability,
"stream": f"data/{RANDOM_STREAMS[capability]}", "metric": "data",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5f1d374: queries of other capabilities are left out with a stderr line, and the table is still written (test added).

"groups": groups, "arrival_rate": rate,
"grouping": grouping, "covers_share": None,
"covered_groups": groups, "min_covered_share": 1 / groups,
"covered_min_N": n_items, "max_covered_share": 1 / groups,
"covered_max_N": n_items,
"assumptions": assumptions,
**DEFAULT_TARGETS,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5f1d374: an accuracy_sla other than 0.95 (DEFAULT_TARGETS, the runner's p95) is refused.

}
rqes.append(synthetic_entry(q))
shared[key] = rqes[-1]["id"]
e2e_keys.add((query["promql"], t))
e2e.append(e2e_query(query, rqe, t, shared[key], names))
check_unique_ids(rqes)
metrics = {"data": {
"type": "gauge", "labels": labels, "samples_per_sec_per_series": per_sec,
"values": {"value": {"zipf": theta, "keys": series}, "dist": {"pareto": alpha}},
}}
return {
"schema_version": 1,
"plan": "ProjectASAP/ASAPQuery#879",
"inputs": inputs,
"sketch_bench_revision": revision,
"workloads": [{
"dataset": dataset,
"rqes": rqes,
"e2e": {"queries": e2e, "metrics": metrics},
"schemas": {"data": {"labels": labels}},
}],
}


def queries_per_instance(q):
"""Queries one evaluation issues per instance: a `traces` key query reads
every key (K point queries); an exact group value, a top-k list and a
Expand Down Expand Up @@ -874,8 +1069,7 @@ def build(args):
continue
entry = rqe_entry(q, curves, curves_big, sat, sat_big, merge)
workloads.setdefault(q["dataset"], []).append(entry)
ids = [r["id"] for rqes in workloads.values() for r in rqes]
assert len(ids) == len(set(ids)), "RQE ids must be unique"
check_unique_ids(r for rqes in workloads.values() for r in rqes)
inputs = [p for p in [args.summary, args.curves, args.curves_big, args.saturation,
args.saturation_big, *(args.merge_curves or [])] if p]
return {
Expand Down Expand Up @@ -912,12 +1106,22 @@ def main(argv=None):
help="--synthetic: an RQE for every template asking a query, not "
"just the strictest (the section 6.3 tables). Without it, a strictest "
"copy below the curves' first N fails the export")
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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ba319e0: --curves, --curves-big, --saturation, --saturation-big and --merge-curves are refused with --random-manifest too.

parser.add_argument("--out", required=True)
args = parser.parse_args(argv)
ignored = ["synthetic", "summary", "samples_per_sec", "keep_duplicate_queries",
"curves", "curves_big", "saturation", "saturation_big", "merge_curves"]
if args.random_manifest and any(getattr(args, name) not in (None, False)
for name in ignored):
parser.error("--random-manifest takes its rate and queries from the manifest and "
"reads no curves: drop "
+ ", ".join("--" + name.replace("_", "-") for name in ignored))
# An instant query reads one scrape, 1000 / rate ms, which the runner keeps
# in whole ms.
rate = args.samples_per_sec
if rate is not None and (rate <= 0 or 1000 % rate):
if args.samples_per_sec is not None and not divides_1000(args.samples_per_sec):
parser.error("--samples-per-sec must divide 1000")
global KEEP_DUPLICATE_QUERIES
KEEP_DUPLICATE_QUERIES = args.keep_duplicate_queries
Expand Down Expand Up @@ -951,9 +1155,16 @@ def run(args, parser):
print(f"Done. {len(tables)} `synthetic` tables and {len(plan)} planned runs in "
f"{args.out}", file=sys.stderr)
return 0
if not (args.summary and args.curves and args.saturation):
parser.error("--summary, --curves and --saturation are required without --synthetic")
table = build(args)
if args.random_manifest:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5f1d374: --random-manifest with --synthetic, --summary, --samples-per-sec or --keep-duplicate-queries is a parser error.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ba319e0: run() writes the table once after the --random-manifest/build() branch.

else:
if not (args.summary and args.curves and args.saturation):
parser.error("--summary, --curves and --saturation are required without "
"--synthetic or --random-manifest")
table = build(args)
os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
with open(args.out, "w") as f:
json.dump(table, f, indent=1, sort_keys=True)
Expand Down
Loading
Loading