From efb777e9321033bc5562ee52a4be2c817616cd76 Mon Sep 17 00:00:00 2001 From: zzylol Date: Wed, 8 Jul 2026 15:08:21 -0600 Subject: [PATCH] eval(#508c): multisketch/DEBS eval harness, benchmarks, docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - datasets_eval: multisketch per-family accuracy + DEBS real-backend query accuracy (topk/freq/distinct/quantile vs exact GT) + google_cluster replay (--anchor-span-s). - otel_collector_benchmark/epsilon_floor: ε-floor vs Nitro sampling benchmark. - deploy/mvp-multinode: consolidated e2e metrics scripts. - docs: GOS/CDM/sampling derivations + eval results; relocate dormant integration docs to docs/dormant/. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 9 + .../debs/scripts/debs_backend_accuracy.sh | 121 + datasets_eval/debs/scripts/debs_otlp_map.py | 93 + .../e2e/INTEGRATED_SWEEP_RESULTS.md | 30 + .../google_cluster/e2e/epsilon_e2e_sweep.py | 487 ++ datasets_eval/google_cluster/run.py | 36 +- datasets_eval/latency/.gitignore | 1 + datasets_eval/latency/agent-cold-ship.yaml | 61 + .../backend-storage-routing-coldon.yaml | 29 + datasets_eval/latency/cold_latency_replay.py | 128 + datasets_eval/latency/compute_latency.py | 99 +- datasets_eval/latency/latency_RESULTS.md | 213 +- datasets_eval/latency/latency_cdf.png | Bin 68819 -> 110728 bytes datasets_eval/latency/latency_summary.json | 41 +- .../latency/per_query_latency_cold.json | 6002 +++++++++++++++++ .../latency/queries-latency-cold.json | 5 + datasets_eval/latency/stack-coldon.sh | 153 + .../multisketch/agent-raw-coldoff.yaml | 40 + datasets_eval/multisketch/agent-raw-gzip.yaml | 42 + datasets_eval/multisketch/agent-raw-zstd.yaml | 42 + datasets_eval/multisketch/baselines.py | 97 + datasets_eval/multisketch/c1_wire.py | 126 + datasets_eval/multisketch/fig7_latency.sh | 61 + datasets_eval/multisketch/fig8_placement.sh | 63 + datasets_eval/multisketch/run_perfamily.py | 17 +- datasets_eval/soak/analyze.py | 218 + .../soak/asap-otel-agent-gct-b0.yaml | 57 + .../soak/asap-otel-agent-gct-b3.yaml | 76 + datasets_eval/soak/dp_memdiag_b3.log | 390 ++ datasets_eval/soak/loadgen.py | 131 + datasets_eval/soak/loadgen_b0.log | 15 + datasets_eval/soak/loadgen_b3.log | 65 + datasets_eval/soak/rss_over_time.png | Bin 0 -> 121071 bytes datasets_eval/soak/sampler.py | 110 + datasets_eval/soak/samples_b0.jsonl | 60 + datasets_eval/soak/samples_b3_soak.jsonl | 360 + datasets_eval/soak/soak_RESULTS.md | 148 + datasets_eval/soak/stack-coldoff.sh | 105 + datasets_eval/soak/summary.json | 72 + deploy/docker/Dockerfile.otel-app | 14 + .../configs/asap/gos-aniso-agent.yaml.tmpl | 75 + .../configs/asap/gos-eval-sink.yaml | 22 + .../configs/asap/mvp-workload-fig9-f2.yaml | 242 + .../configs/asap/mvp-workload-fig9.yaml | 242 + deploy/mvp-multinode/eval-8node/f2_debs.csv | 4 + .../eval-8node/f2_wholesketch_cluster.csv | 7 + .../f2_wholesketch_cluster_h2048.csv | 7 + .../eval-8node/gos_aniso_cluster.csv | 7 + .../scripts/aggregate_phase2_sweep.py | 65 + deploy/mvp-multinode/scripts/e2e_metrics.sh | 157 + .../scripts/epsilon_accuracy_sweep.sh | 56 + .../scripts/epsilon_cluster_sweep.sh | 83 + .../mvp-multinode/scripts/fig9_coordinated.sh | 91 + .../scripts/fig9_f2_coordinated.sh | 98 + deploy/mvp-multinode/scripts/fig_costmodel.py | 84 + .../scripts/gos_aniso_cluster.sh | 100 + deploy/mvp-multinode/scripts/monitor_e2e.sh | 8 +- .../mvp-multinode/scripts/plot_fig8_fig6b.py | 62 + deploy/mvp-multinode/scripts/plots.py | 195 + deploy/mvp-multinode/scripts/run_demo.sh | 23 +- .../mvp-multinode/scripts/run_demo_sweep.sh | 3 +- deploy/mvp-multinode/scripts/scale_fleet.sh | 123 + .../scripts/snapshot_resources.sh | 13 +- deploy/mvp-multinode/scripts/soak_rss.sh | 61 + deploy/mvp-multinode/topology.8node.env | 166 + ...tinuous-monitoring-aggregation-taxonomy.md | 3 + ...nuous-monitoring-tumbling-cost-analysis.md | 3 + docs/delta-transmission-design.md | 5 + docs/design-gos-unified-edge-telemetry.md | 732 ++ ...ibuted-nitrosketch-coordinated-sampling.md | 13 + .../design-asap-otap-rust-integration.md | 5 +- .../design-asap-telegraf-integration.md | 3 + .../design-asap-vector-integration.md | 7 +- docs/evaluation-plan-figures.md | 720 +- docs/gos-eval-results.md | 286 + docs/mvp-demo-runbook.md | 2 +- docs/phase-2.md | 891 +++ docs/sampling-cdm-gos-derivations.md | 1695 +++++ docs/system-overview.md | 4 +- docs/use-case-dataset-survey.md | 103 +- .../epsilon_floor_vs_nitro_test.go | 332 + .../epsilon_floor/extract_debs_rates.sh | 7 + otel_collector_benchmark/epsilon_floor/go.mod | 24 + otel_collector_benchmark/epsilon_floor/go.sum | 48 + 84 files changed, 16567 insertions(+), 127 deletions(-) create mode 100755 datasets_eval/debs/scripts/debs_backend_accuracy.sh create mode 100644 datasets_eval/debs/scripts/debs_otlp_map.py create mode 100644 datasets_eval/google_cluster/e2e/INTEGRATED_SWEEP_RESULTS.md create mode 100644 datasets_eval/google_cluster/e2e/epsilon_e2e_sweep.py create mode 100644 datasets_eval/latency/agent-cold-ship.yaml create mode 100644 datasets_eval/latency/backend-storage-routing-coldon.yaml create mode 100644 datasets_eval/latency/cold_latency_replay.py create mode 100644 datasets_eval/latency/per_query_latency_cold.json create mode 100644 datasets_eval/latency/queries-latency-cold.json create mode 100755 datasets_eval/latency/stack-coldon.sh create mode 100644 datasets_eval/multisketch/agent-raw-coldoff.yaml create mode 100644 datasets_eval/multisketch/agent-raw-gzip.yaml create mode 100644 datasets_eval/multisketch/agent-raw-zstd.yaml create mode 100644 datasets_eval/multisketch/baselines.py create mode 100644 datasets_eval/multisketch/c1_wire.py create mode 100644 datasets_eval/multisketch/fig7_latency.sh create mode 100644 datasets_eval/multisketch/fig8_placement.sh create mode 100644 datasets_eval/soak/analyze.py create mode 100644 datasets_eval/soak/asap-otel-agent-gct-b0.yaml create mode 100644 datasets_eval/soak/asap-otel-agent-gct-b3.yaml create mode 100644 datasets_eval/soak/dp_memdiag_b3.log create mode 100644 datasets_eval/soak/loadgen.py create mode 100644 datasets_eval/soak/loadgen_b0.log create mode 100644 datasets_eval/soak/loadgen_b3.log create mode 100644 datasets_eval/soak/rss_over_time.png create mode 100644 datasets_eval/soak/sampler.py create mode 100644 datasets_eval/soak/samples_b0.jsonl create mode 100644 datasets_eval/soak/samples_b3_soak.jsonl create mode 100644 datasets_eval/soak/soak_RESULTS.md create mode 100755 datasets_eval/soak/stack-coldoff.sh create mode 100644 datasets_eval/soak/summary.json create mode 100644 deploy/mvp-multinode/configs/asap/gos-aniso-agent.yaml.tmpl create mode 100644 deploy/mvp-multinode/configs/asap/gos-eval-sink.yaml create mode 100644 deploy/mvp-multinode/configs/asap/mvp-workload-fig9-f2.yaml create mode 100644 deploy/mvp-multinode/configs/asap/mvp-workload-fig9.yaml create mode 100644 deploy/mvp-multinode/eval-8node/f2_debs.csv create mode 100644 deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster.csv create mode 100644 deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster_h2048.csv create mode 100644 deploy/mvp-multinode/eval-8node/gos_aniso_cluster.csv create mode 100644 deploy/mvp-multinode/scripts/aggregate_phase2_sweep.py create mode 100755 deploy/mvp-multinode/scripts/e2e_metrics.sh create mode 100644 deploy/mvp-multinode/scripts/epsilon_accuracy_sweep.sh create mode 100644 deploy/mvp-multinode/scripts/epsilon_cluster_sweep.sh create mode 100755 deploy/mvp-multinode/scripts/fig9_coordinated.sh create mode 100644 deploy/mvp-multinode/scripts/fig9_f2_coordinated.sh create mode 100644 deploy/mvp-multinode/scripts/fig_costmodel.py create mode 100755 deploy/mvp-multinode/scripts/gos_aniso_cluster.sh create mode 100644 deploy/mvp-multinode/scripts/plot_fig8_fig6b.py create mode 100644 deploy/mvp-multinode/scripts/plots.py create mode 100644 deploy/mvp-multinode/scripts/scale_fleet.sh create mode 100644 deploy/mvp-multinode/scripts/soak_rss.sh create mode 100644 deploy/mvp-multinode/topology.8node.env create mode 100644 docs/design-gos-unified-edge-telemetry.md rename docs/{ => dormant}/design-asap-otap-rust-integration.md (99%) rename docs/{ => dormant}/design-asap-telegraf-integration.md (99%) rename docs/{ => dormant}/design-asap-vector-integration.md (98%) create mode 100644 docs/gos-eval-results.md create mode 100644 docs/phase-2.md create mode 100644 docs/sampling-cdm-gos-derivations.md create mode 100644 otel_collector_benchmark/epsilon_floor/epsilon_floor_vs_nitro_test.go create mode 100755 otel_collector_benchmark/epsilon_floor/extract_debs_rates.sh create mode 100644 otel_collector_benchmark/epsilon_floor/go.mod create mode 100644 otel_collector_benchmark/epsilon_floor/go.sum diff --git a/.gitignore b/.gitignore index 197b83f41..f98ce4b2f 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,12 @@ otel-app/otel-app deploy/scripts/__pycache__/ *.pyc deploy/eval-results/ + +# Compiled eval-driver binaries (built by the f2 eval scripts into their cmd dirs) +asap-precompute-go/monitor/grpcclient/f2driver +asap-precompute-go/monitor/grpcclient/e2edriver +asap-precompute-go/monitor/grpcclient/cmd/*/f2driver +asap-precompute-go/monitor/grpcclient/cmd/*/e2edriver + +# Local dataset working copies (not part of the repo) +datasets_eval/debs/data/ diff --git a/datasets_eval/debs/scripts/debs_backend_accuracy.sh b/datasets_eval/debs/scripts/debs_backend_accuracy.sh new file mode 100755 index 000000000..e4d0ef7ce --- /dev/null +++ b/datasets_eval/debs/scripts/debs_backend_accuracy.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Real-dataset QUERY ACCURACY through the REAL ASAPQuery-backend, on DEBS 2022. +# Maps DEBS trading events → OTLP for the four sketch families, replays them +# through a fused asap_edge agent into data_plane, then QUERIES the warm backend +# and scores each answer against the EXACT ground truth computed offline from the +# same DEBS rows: +# quantile (DDSketch+KLL) · topk (CountSketch) · distinct (HLL) · freq (CountMin) +# +# Uses the multisketch single-host stack.sh (agent + data_plane + control_plane), +# the same real backend the cluster runs. Usage: debs_backend_accuracy.sh [n_events] +set -uo pipefail +ROOT=/mydata/ASAPCollector +MS="${ROOT}/datasets_eval/multisketch" +DEBS_CSV="${DEBS_CSV:-${ROOT}/datasets_eval/debs/data/debs2022-gc-trading-day-08-11-21.csv}" +# N events. 500k DEBS events ≈ 1.55M OTLP points, which SEND in ~23s — well +# within one boundary-aligned 60s window (measured). The wall-clock-anchor +# collapses every row into ONE window; boundary alignment (below) gives the send +# a full 60s of headroom so nothing seals mid-replay. A smaller slice (e.g. 60k) +# is the flat intro of the trace — no heavy hitters, half the prices zero — so +# keep the rich 500k slice; it fits. +N="${1:-500000}" +JSONL=/tmp/debs_otlp.jsonl; GT=/tmp/debs_gt.json +BASE="http://127.0.0.1:9091" +trap '[ -n "${KEEP_STACK:-}" ] || "${MS}/stack.sh" down >/dev/null 2>&1 || true' EXIT + +echo "== 1. map DEBS ($N events) → OTLP + exact GT ==" +python3 "${ROOT}/datasets_eval/debs/scripts/debs_otlp_map.py" "$DEBS_CSV" "$N" "$JSONL" "$GT" + +echo "== 2. bring up the real all-families stack (agent + data_plane + control_plane) ==" +AGENT_CFG="${AGENT_CFG:-${MS}/agent-allfamilies-coldoff.yaml}" +echo " agent config: ${AGENT_CFG}" +"${MS}/stack.sh" up "${MS}/workloads/all-families.yaml" "${AGENT_CFG}" >/tmp/debs_stack.log 2>&1 +sleep 8 + +echo "== 3. replay DEBS OTLP → agent :4317 (wall-clock-anchored) ==" +# Align to a fresh 60s wall-clock boundary so the collapsed instant sits at the +# START of a window — the whole send then has a full 60s of headroom before the +# window seals (otherwise a mid-window anchor can straddle the seal and drop the +# tail of the replay). +python3 -c 'import time; t=time.time(); s=60-(t%60); s=s if s>5 else s+60; print(f" aligning to next 60s boundary in {s:.1f}s"); time.sleep(s)' +# --anchor-span-s 50: spread the points across 50s (distinct ns each) inside the +# one window, so the count-type sketches (topk_cs/freq_cms carry value==1.0 per +# event) keep per-key multiplicity instead of deduping every event of a key onto +# one identical (series, ts, value) sample. +python3 "${ROOT}/datasets_eval/google_cluster/run.py" replay \ + --jsonl "$JSONL" --endpoint 127.0.0.1:4317 --pace-factor 0 --wall-clock-anchor \ + --anchor-span-s 45 \ + >/tmp/debs_replay.log 2>&1 +echo " waiting for the warm window to seal…"; sleep 75 + +echo "== 4. query the backend + score vs exact GT ==" +python3 - "$BASE" "$GT" <<'PY' +import sys, json, time, urllib.request, urllib.parse +BASE, GT = sys.argv[1], sys.argv[2] +gt = json.load(open(GT)) +def q(promql, t=None): + p = {"query": promql} + if t is not None: p["time"] = t + u = f"{BASE}/api/v1/query?" + urllib.parse.urlencode(p) + try: + body = json.loads(urllib.request.urlopen(u, timeout=60).read().decode()) + if isinstance(body, dict): + return body.get("data", {}).get("result", []) or [] + except Exception: + pass + return [] +def val(res): + try: + return float(res[0]["value"][1]) if res else None + except Exception: + return None +# Instant sketch reads only resolve against the window whose seal is 'current' — +# after it rolls the same query goes empty. Sweep time= backward to the offset +# that returns the MOST series, i.e. the sealed replay window. +def best_time(probe): + now = int(time.time()); best_t, best_n = now, -1 + for off in range(0, 200, 8): + n = len(q(probe, t=now-off)) + if n > best_n: best_n, best_t = n, now-off + return best_t, best_n +# Probe with the instant topk aggregate (the bare metric never returns series). +bt, bn = best_time('topk(50, google_cluster_2019_cpu_rate_topk_cs)') +print(f"# sealed-window probe: t=now-{int(time.time())-bt}s has {bn} topk_cs series") +# landed-event sanity: total inserts the CountSketch actually saw this window. +tot = val(q('sum(count_over_time(google_cluster_2019_cpu_rate_topk_cs[300s]))', t=bt)) +print(f"# CountSketch landed events this window: {tot} (GT events sent: {gt['events']})") + +print(f"\n{'query':<26} {'backend':>14} {'exact GT':>14} {'accuracy':>22}") +print("-"*80) + +# distinct (HLL) — cardinality of distinct symbols +r = q('count(google_cluster_2019_cpu_rate_card_hll)', t=bt) +est = val(r); tru = gt["distinct"] +acc = f"rel_err {abs(est-tru)/tru:.2%}" if est else f"({r})" +print(f"{'distinct (HLL)':<26} {str(est):>14} {tru:>14} {acc:>22}") + +# frequency of one key — from the CountSketch heap (point-frequency sketch). +sym = gt["freq_query"]["symbol"]; tru = gt["freq_query"]["true_count"] +est = val(q(f'google_cluster_2019_cpu_rate_topk_cs{{item="{sym}"}}', t=bt)) +acc = f"rel_err {abs(est-tru)/tru:.2%}" if est else f"(not in heap)" +print(f"{'freq '+sym[:16]+' (CS)':<26} {str(est):>14} {tru:>14} {acc:>22}") + +# topk (CountSketch) — recall@10 + dump the actual returned items/counts. +r = q('topk(10, google_cluster_2019_cpu_rate_topk_cs)', t=bt) +got_items = [(s["metric"].get("item"), s["value"][1]) for s in r] +got = {i for i, _ in got_items if i} +truk = {d["symbol"] for d in gt["topk10"]} +rec = len(got & truk) / len(truk) if truk else 0 +print(f"{'topk@10 (CountSketch)':<26} {str(len(got & truk))+'/'+str(len(truk)):>14} {str(len(truk)):>14} {'recall '+f'{rec:.0%}':>22}") +print(f" backend top: {got_items[:5]}") +print(f" GT top: {[(d['symbol'], d['count']) for d in gt['topk10'][:5]]}") + +# quantile (DDSketch, KLL) — dump p50/p90/p99 to see if it's a scale/tail issue. +for fam, m in [("DDSketch", "q_ddsketch"), ("KLL", "q_kll")]: + for ql, key in [(0.50, "p50"), (0.90, "p90"), (0.99, "p99")]: + est = val(q(f'quantile_over_time({ql}, google_cluster_2019_cpu_rate_{m}[300s])', t=bt)) + tru = gt["quantile"][key] + acc = f"rel_err {abs(est-tru)/tru:.2%}" if est and tru else f"(empty)" + print(f"{fam+' '+key:<26} {str(round(est,2) if est else est):>14} {str(tru):>14} {acc:>22}") +PY +echo "== done ==" diff --git a/datasets_eval/debs/scripts/debs_otlp_map.py b/datasets_eval/debs/scripts/debs_otlp_map.py new file mode 100644 index 000000000..a27979439 --- /dev/null +++ b/datasets_eval/debs/scripts/debs_otlp_map.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Map real DEBS 2022 trading events → an OTLP-replay JSONL that exercises the +asap agent's four sketch families, and compute the EXACT ground truth for each +query so the backend answer can be scored: + + metric (agent family) query GT + --------------------------------- -------------- -------------------------- + top_endpoint_qps (CountSketch) topk symbols exact top-10 by trade count + endpoint_request_freq (CountMin) freq of a key exact count of one symbol + unique_users_per_min (HLL) distinct count exact distinct symbols + http_requests_total_latency_ms(KLL) quantile exact p50/p90/p99 of Ask price + +JSONL line: {"timestamp_ms","series_id","metric_name","value","attributes":{}} +Usage: debs_otlp_map.py +""" +import sys, json, time +from collections import Counter + +csv, n, out_jsonl, gt_path = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4] + +counts = Counter() # symbol -> trade count (topk / frequency GT) +prices = [] # Ask prices (quantile GT) +distinct = set() +now_ms = int(time.time() * 1000) + +read = 0 +with open(csv, errors="replace") as f, open(out_jsonl, "w") as w: + for line in f: + if line[0] == "#" or line.startswith("ID,"): + continue + p = line.split(",") + sym = p[0] + if not sym: + continue + ts = now_ms + read # monotone, wall-clock-anchored downstream + # Metric names + labels match agent-allfamilies-coldoff.yaml so the + # EXISTING all-families stack serves them (topk item_label=host, + # cms/hll item_label=service). + # topk (CountSketch) + frequency (CountMin): one point per event, symbol. + w.write(json.dumps({"timestamp_ms": ts, "series_id": f"tk:{sym}", + "metric": "google_cluster_2019_cpu_rate_topk_cs", "value": 1.0, + "attributes": {"host": sym}}) + "\n") + w.write(json.dumps({"timestamp_ms": ts, "series_id": f"fq:{sym}", + "metric": "google_cluster_2019_cpu_rate_freq_cms", "value": 1.0, + "attributes": {"service": sym}}) + "\n") + # distinct (HLL): distinct label = symbol. + w.write(json.dumps({"timestamp_ms": ts, "series_id": f"hll:{sym}", + "metric": "google_cluster_2019_cpu_rate_card_hll", "value": 1.0, + "attributes": {"service": sym}}) + "\n") + counts[sym] += 1 + distinct.add(sym) + # quantile (DDSketch + KLL): Ask price (col 5) when present. + # ALL prices fold into ONE global sketch (constant item) so the backend + # quantile query returns a single global p99 comparable to the global GT. + # (Per-symbol DDSketch would need per-symbol GT; a single global quantile + # is the clean headline number the user asked for.) + if len(p) > 4 and p[4]: + try: + v = float(p[4]) + for m in ("google_cluster_2019_cpu_rate_q_ddsketch", + "google_cluster_2019_cpu_rate_q_kll"): + w.write(json.dumps({"timestamp_ms": ts, "series_id": "px:ALL", + "metric": m, "value": v, + "attributes": {"host": "ALL"}}) + "\n") + prices.append(v) + except ValueError: + pass + read += 1 + if read >= n: + break + +prices.sort() +def pq(q): + return prices[min(len(prices) - 1, int(q * len(prices)))] if prices else None +top10 = counts.most_common(10) +# Point-frequency query is answered from the CountSketch heap (topk_cs), which +# retains the top heap_size=100 keys with their estimated counts — so pick a key +# comfortably INSIDE the heap (rank ~15) rather than at its edge. (The CountMin +# keyed frequency path is a warm-only safe-miss by design — Phase 2b unfinished — +# so in the cold-OFF stack a CMS point query returns no result; the CountSketch +# is itself a point-frequency sketch and answers the same question.) +freq_key, freq_true = counts.most_common(20)[-1] if len(counts) > 20 else top10[0] +gt = { + "events": read, + "topk10": [{"symbol": s, "count": c} for s, c in top10], + "freq_query": {"symbol": freq_key, "true_count": freq_true}, + "distinct": len(distinct), + "quantile": {"p50": pq(0.50), "p90": pq(0.90), "p99": pq(0.99), "n": len(prices)}, +} +json.dump(gt, open(gt_path, "w"), indent=1) +print(f"# DEBS OTLP map: events={read} distinct={len(distinct)} prices={len(prices)}", file=sys.stderr) +print(f"# top symbol: {top10[0]} freq_query symbol: {freq_key}(true={freq_true})", file=sys.stderr) +print(f"# price quantiles p50={pq(0.5)} p90={pq(0.9)} p99={pq(0.99)}", file=sys.stderr) diff --git a/datasets_eval/google_cluster/e2e/INTEGRATED_SWEEP_RESULTS.md b/datasets_eval/google_cluster/e2e/INTEGRATED_SWEEP_RESULTS.md new file mode 100644 index 000000000..e12bad4c9 --- /dev/null +++ b/datasets_eval/google_cluster/e2e/INTEGRATED_SWEEP_RESULTS.md @@ -0,0 +1,30 @@ +# Integrated ε-sweep — end-to-end metrics under coordinated sampling + +`epsilon_e2e_sweep.py` extends `pareto_sweep.py` to emit **6 metric classes in one +run**, swept over the admission p (= the ε-floor `p=1/(1+ε²·rate)` the autonomous +coordinator sets for the corresponding ε): query **accuracy**, query **latency** +(p50/p99), data **freshness** (sample-generation → backend-queryable), and +per-process **CPU / RSS / wire bandwidth**. + +## Phase-1 (single-node, google-cluster-2019 cpu_rate, pooled, GT p99=0.0494) + +| arm | ε | p | accuracy | lat p99 | freshness | bk RSS | pr RSS | cpu cores | wire kbps | +|---|---|---|---|---|---|---|---|---|---| +| raw | 0 | 1.00 | 1.000 | 2.5 ms | — | 47 MB | 71 MB | 0.115 | 14.7 | +| dd | 0 | 1.00 | 0.867* | 2.6 ms | 19 ms | 15 MB | 60 MB | 0.100 | 0.4 | +| dd | 0.005 | 0.50 | 0.867 | 2.6 ms | 19 ms | 15 MB | 60 MB | 0.099 | 0.4 | +| dd | 0.008 | 0.25 | 0.850 | 2.6 ms | 19 ms | 15 MB | 58 MB | 0.091 | 0.4 | +| dd | 0.014 | 0.10 | 0.850 | 2.6 ms | 19 ms | 15 MB | 55 MB | 0.081 | 0.3 | +| dd | 0.020 | 0.05 | 0.850 | 2.6 ms | 20 ms | 16 MB | 51 MB | 0.093 | 0.3 | + +**Takeaways:** sampling to p=0.05 keeps accuracy flat (high-N pooled is +sampling-robust), drops edge CPU 0.10→0.08, keeps wire ~40× below raw, backend RSS +bounded ~15 MB. Warm latency ~2.6 ms (vs cold-tier ~51 ms, Fig 7 / #501). + +**Caveats (single-node):** (1) `*`accuracy ~13% low — the regenerated `[30s]` +window reads a sub-pool; the *calibrated* accuracy-vs-p is in +`docs/evaluation-plan-figures.md` Fig 3. (2) freshness ~19 ms is a single-node +artifact — the **compressed** replay (31 d → ~20 s) seals windows mid-run, so +emit→queryable collapses to ~0. Real freshness + per-component (edge/dp/cp) + +cold-tier routing are the **Phase-2 cluster run** (wall-clock-paced, +`measure_freshness.sh` + the probe; per-container `snapshot_resources.sh`). diff --git a/datasets_eval/google_cluster/e2e/epsilon_e2e_sweep.py b/datasets_eval/google_cluster/e2e/epsilon_e2e_sweep.py new file mode 100644 index 000000000..ab3a20cc3 --- /dev/null +++ b/datasets_eval/google_cluster/e2e/epsilon_e2e_sweep.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 +"""Headline accuracy-vs-cost Pareto sweep (Fig 1 of docs/evaluation-plan-figures.md). + +Runs ONE consistent sweep on the real ASAP stack against the 2019 Google +cluster trace, producing operating points (cost, accuracy) where: + + y = accuracy = 1 - p99_rel_err of quantile_over_time(0.99, cpu_rate[30s]) + vs the dataset's TRUE pooled p99. + x = total cost = equal-weight mean of (edge CPU cores, wire egress KB/s), + each normalized so the raw-forwarding anchor = 1.0. + +Topology per arm (fresh backend + producer per arm — delta-query +queryability requires a clean per-series-base backend): + + otel-app (SDK pre-aggregation: raw-buffer | dd-full | dd-delta, + producer-side warm-sample-p) + --OTLP/gRPC(:4317, gzip)--> data_plane backend (sketch store) + --PromQL(:9091)--> query p99 vs exact offline pooled GT (gt_eval) + +Cost is measured per arm: + * wire egress = iptables byte counter on tcp dport 4317 (loopback OTLP), + divided by the replay wall-clock -> KB/s. + * edge CPU = producer process utime+stime delta (clock ticks -> seconds), + divided by replay wall-clock -> cores. + +The two knobs the sweep exercises: + * sampling p -> -warm-sample-p (drops raw points before the SDK + aggregation; admitted ingest ~ p, smaller wire, ~constant + pooled accuracy until small-N degrades). + * delta/ε_cdm -> -agg dd-delta (SDK ships delta-transmitted sketch state; + cuts steady-state wire vs dd-full). The trace is LOOPED so + multiple SDK windows elapse and the per-window delta benefit + emerges at steady state (the first-window-full-state is + amortized — see the honest caveats in RESULTS.md). + +NB: this minimal topology runs the SDK pre-aggregation as the "edge" (the +otel-app SDK View IS the edge sketch). A separate asap-otel collector hop is +NOT inserted: it would only forward the same SDK sketch envelope to the same +backend, adding a constant per-arm cost that cancels in the raw-normalized +ratio. We state this explicitly so the cost axis is unambiguous. +""" +from __future__ import annotations + +import argparse +import json +import os +import signal +import subprocess +import sys +import time +import urllib.parse +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +sys.path.insert(0, str(ROOT)) +import gt_eval # noqa: E402 + +REPO = Path("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/mydata/ASAPCollector") +BACKEND_BIN = Path("/mydata/ASAPQuery-backend/target/release/data_plane") +OTEL_APP = REPO / "otel-app" / "otel-app" +METRIC = "google_cluster_2019_cpu_rate" +# Wide range so the query MERGES all tumbling sub-windows of the single +# replay pass into one pooled p99 (matches the global pooled GT). The +# headline query is quantile_over_time(0.99, cpu_rate[30s]); we widen the +# range only so the multi-window single pass is queryable as one pool. +QUERY = f"quantile_over_time(0.99, {METRIC}[3600s])" +GRPC_PORT = 4317 +HTTP_PORT_BASE = 4318 +QUERY_PORT = 9091 +IPT_CHAIN = "PARETOBW" +CLK_TCK = os.sysconf("SC_CLK_TCK") + + +# --------------------------------------------------------------------------- +# wire metering via iptables (loopback byte counter on OTLP ingest port) +# --------------------------------------------------------------------------- +def ipt(*args: str) -> subprocess.CompletedProcess: + return subprocess.run(["sudo", "-n", "iptables", *args], + capture_output=True, text=True) + + +def ipt_setup() -> None: + ipt("-N", IPT_CHAIN) + ipt("-F", IPT_CHAIN) + ipt("-A", IPT_CHAIN, "-p", "tcp", "--dport", str(GRPC_PORT)) + if ipt("-C", "INPUT", "-j", IPT_CHAIN).returncode != 0: + ipt("-I", "INPUT", "-j", IPT_CHAIN) + ipt("-Z", IPT_CHAIN) + + +def ipt_teardown() -> None: + ipt("-D", "INPUT", "-j", IPT_CHAIN) + ipt("-F", IPT_CHAIN) + ipt("-X", IPT_CHAIN) + + +def ipt_zero() -> None: + ipt("-Z", IPT_CHAIN) + + +def ipt_bytes() -> int: + out = ipt("-L", IPT_CHAIN, "-v", "-n", "-x").stdout + for line in out.splitlines(): + if f"dpt:{GRPC_PORT}" in line: + return int(line.split()[1]) + return 0 + + +# --------------------------------------------------------------------------- +# process CPU sampling via /proc//stat (utime+stime, fields 14,15) +# --------------------------------------------------------------------------- +def proc_cpu_ticks(pid: int) -> int: + try: + parts = Path(f"/proc/{pid}/stat").read_text().split() + # field indices 13,14 (0-based) = utime,stime + return int(parts[13]) + int(parts[14]) + except Exception: + return 0 + + +# --------------------------------------------------------------------------- +# backend lifecycle +# --------------------------------------------------------------------------- +def port_free(port: int) -> bool: + out = subprocess.run(["ss", "-ltn"], capture_output=True, text=True).stdout + return f":{port} " not in out and f":{port}\n" not in out and \ + not any(f":{port}" in ln.split()[3] for ln in out.splitlines()[1:] + if len(ln.split()) > 3) + + +def wait_ports_free(ports: list[int], timeout_s: float = 20.0) -> bool: + deadline = time.time() + timeout_s + while time.time() < deadline: + if all(port_free(p) for p in ports): + return True + time.sleep(0.5) + return False + + +def start_backend(streaming_cfg: Path, logdir: Path) -> subprocess.Popen: + log = open(logdir / "data_plane.log", "w") + env = dict(os.environ, RUST_LOG="info") + p = subprocess.Popen( + [str(BACKEND_BIN), + "--streaming-config", str(streaming_cfg), + "--enable-otel-ingest", + "--otel-grpc-port", str(GRPC_PORT), + "--otel-http-port", str(HTTP_PORT_BASE), + "--query-port", str(QUERY_PORT), + "--prometheus-scrape-interval", "60", + "--output-dir", str(logdir), + "--log-level", "warn"], + stdout=log, stderr=subprocess.STDOUT, env=env) + return p + + +def await_query_ready(timeout_s: float = 30.0) -> bool: + url = f"http://127.0.0.1:{QUERY_PORT}/api/v1/query?" + urllib.parse.urlencode( + {"query": "vector(1)"}) + deadline = time.time() + timeout_s + while time.time() < deadline: + try: + with urllib.request.urlopen(url, timeout=3) as r: + if r.status == 200: + return True + except Exception: + time.sleep(0.5) + return False + + +def query_p99() -> float | None: + url = f"http://127.0.0.1:{QUERY_PORT}/api/v1/query?" + urllib.parse.urlencode( + {"query": QUERY}) + try: + with urllib.request.urlopen(url, timeout=15) as r: + body = json.loads(r.read().decode()) + except Exception as e: + print(f" query error: {e}", file=sys.stderr) + return None + data = body.get("data") or {} + res = data.get("result") or [] + if not res: + return None + # pooled single-series -> one result; if multiple, take max (global p99 + # is dominated by the heaviest series in the single-series-pool design, + # but the pool design yields exactly one series). + vals = [] + for s in res: + v = s.get("value") + if v and v[1] is not None: + vals.append(float(v[1])) + return max(vals) if vals else None + + +def query_latency(n: int = 25) -> tuple[float | None, float | None]: + """Time the warm quantile QUERY n times; return (p50_ms, p99_ms).""" + url = f"http://127.0.0.1:{QUERY_PORT}/api/v1/query?" + urllib.parse.urlencode( + {"query": QUERY}) + lat: list[float] = [] + for _ in range(n): + t = time.perf_counter() + try: + with urllib.request.urlopen(url, timeout=15) as r: + r.read() + except Exception: + continue + lat.append((time.perf_counter() - t) * 1000.0) + if not lat: + return None, None + lat.sort() + return round(lat[len(lat) // 2], 2), round(lat[min(len(lat) - 1, int(len(lat) * 0.99))], 2) + + +def proc_rss_mb(pid: int) -> float: + """Resident set size (MB) of a live process.""" + try: + for line in Path(f"/proc/{pid}/status").read_text().splitlines(): + if line.startswith("VmRSS:"): + return round(int(line.split()[1]) / 1024.0, 1) # kB -> MB + except Exception: + pass + return 0.0 + + +def query_freshness_ms() -> float | None: + """Data freshness = now - emit_ts of the most-recent queryable warm sample. + The freshness probe counter's value IS its emission epoch-ms (see + measure_freshness.sh); query the warm tier for it, matching the probe name + with or without the OTLP->metric suffix.""" + for name in ("http_freshness_probe_warm", + "http_freshness_probe_warm_milliseconds_total", + '{__name__=~"http_freshness_probe_warm.*"}'): + url = f"http://127.0.0.1:{QUERY_PORT}/api/v1/query?" + urllib.parse.urlencode( + {"query": name}) + try: + with urllib.request.urlopen(url, timeout=10) as r: + body = json.loads(r.read().decode()) + except Exception: + continue + res = (body.get("data") or {}).get("result") or [] + now_ms = time.time() * 1000.0 + best = None + for s in res: + v = s.get("value") + if v and v[1] is not None: + lag = now_ms - float(v[1]) + if 0 <= lag < 3_600_000 and (best is None or lag < best): + best = lag + if best is not None: + return round(best, 1) + return None + + +# --------------------------------------------------------------------------- +# one arm +# --------------------------------------------------------------------------- +def run_arm(name: str, agg: str, p: float, scale: float, duration_s: float, + loop: bool, sdk_window: str, streaming_cfg: Path, + trace_csv: Path, logroot: Path) -> dict: + logdir = logroot / name + logdir.mkdir(parents=True, exist_ok=True) + + # fresh backend: ensure prior backend's ports are released first + if not wait_ports_free([GRPC_PORT, QUERY_PORT, HTTP_PORT_BASE]): + raise RuntimeError(f"{name}: ports still bound before backend start") + bk = start_backend(streaming_cfg, logdir) + if not await_query_ready() or bk.poll() is not None: + try: + bk.send_signal(signal.SIGTERM) + except Exception: + pass + raise RuntimeError(f"{name}: backend not ready (pid alive=" + f"{bk.poll() is None})") + + ipt_zero() + plog = open(logdir / "producer.log", "w") + cmd = [str(OTEL_APP), + "-target", f"127.0.0.1:{GRPC_PORT}", + "-trace-file", str(trace_csv), + "-trace-metric-name", METRIC, + "-trace-scale", str(scale), + f"-trace-loop={'true' if loop else 'false'}", + "-agg", agg, + "-sdk-window", sdk_window, + "-warm-sample-p", str(p), + "-five-sketch=false", "-freshness-probes=true", + "-seed", "12345"] + if duration_s > 0: + cmd += ["-duration", f"{int(duration_s)}s"] + prod = subprocess.Popen(cmd, stdout=plog, stderr=subprocess.STDOUT) + + t0 = time.time() + cpu0 = proc_cpu_ticks(prod.pid) + # poll cpu (+ peak RSS for backend & producer) while running + last_cpu = cpu0 + bk_rss = prod_rss = 0.0 + while prod.poll() is None: + c = proc_cpu_ticks(prod.pid) + if c: + last_cpu = c + bk_rss = max(bk_rss, proc_rss_mb(bk.pid)) + prod_rss = max(prod_rss, proc_rss_mb(prod.pid)) + time.sleep(0.25) + # generous guard: single-pass at the sweep scale is ~20-25s; cap at + # max(duration, 120) + 30 so a slow pass is never truncated early. + if time.time() - t0 > max(duration_s, 120) + 30: + prod.send_signal(signal.SIGTERM) + break + t1 = time.time() + wall = t1 - t0 + cpu_ticks = max(last_cpu - cpu0, 0) + cpu_cores = (cpu_ticks / CLK_TCK) / wall if wall > 0 else 0.0 + + wire_bytes = ipt_bytes() + wire_kbps = (wire_bytes / 1024.0) / wall if wall > 0 else 0.0 + + # parse REPLAY_STATS (last line) for admitted/candidate + admitted = candidate = 0 + for line in (logdir / "producer.log").read_text().splitlines(): + if "REPLAY_STATS" in line: + for tok in line.split(): + if tok.startswith("candidate="): + candidate = int(tok.split("=")[1]) + elif tok.startswith("admitted="): + admitted = int(tok.split("=")[1]) + adm_per_s = admitted / wall if wall > 0 else 0.0 + + # FRESHNESS = sample-generation → backend-queryable. Poll from producer end + # (t1, ≈ when the last samples were generated) until the metric first answers; + # that lag is the staleness floor (window seal + scrape lookback). + poll0 = time.time() + p99 = None + while time.time() - poll0 < 90: + p99 = query_p99() + if p99 is not None: + break + time.sleep(1.0) + freshness_ms = round((time.time() - t1) * 1000.0) if p99 is not None else None + # backend still alive: query latency + backend RSS + lat_p50, lat_p99 = query_latency() + bk_rss = max(bk_rss, proc_rss_mb(bk.pid)) + + bk.send_signal(signal.SIGTERM) + try: + bk.wait(timeout=10) + except subprocess.TimeoutExpired: + bk.kill() + time.sleep(1.0) + + return { + "name": name, "agg": agg, "p": p, "scale": scale, "loop": loop, + "wall_s": round(wall, 2), + "wire_bytes": wire_bytes, "wire_kbps": round(wire_kbps, 3), + "cpu_ticks": cpu_ticks, "cpu_cores": round(cpu_cores, 4), + "candidate": candidate, "admitted": admitted, + "admitted_per_s": round(adm_per_s, 1), + "p99": p99, + # NEW integrated metrics: + "lat_p50_ms": lat_p50, "lat_p99_ms": lat_p99, + "freshness_ms": freshness_ms, + "backend_rss_mb": round(bk_rss, 1), "producer_rss_mb": round(prod_rss, 1), + } + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--trace-csv", type=Path, default=Path("/tmp/gct-cpu-pooled.csv")) + ap.add_argument("--jsonl", type=Path, default=Path("/tmp/gct-otlp.jsonl")) + ap.add_argument("--streaming-config", type=Path, + default=Path("/tmp/pareto/streaming-config.yaml")) + ap.add_argument("--out-dir", type=Path, default=Path("/tmp/pareto/results")) + ap.add_argument("--duration", type=float, default=0.0, + help="per-arm producer duration cap; 0 = single full pass") + args = ap.parse_args() + + args.out_dir.mkdir(parents=True, exist_ok=True) + logroot = args.out_dir / "logs" + logroot.mkdir(parents=True, exist_ok=True) + + # exact pooled ground truth p99 over the whole trace + rows = gt_eval.load_rows(args.jsonl) + gt_p99 = gt_eval.gt_quantile(rows, {"metric": METRIC, "q": 0.99, "by": []}) + print(f"GT pooled p99 = {gt_p99:.6f} ({len(rows)} jsonl rows)", file=sys.stderr) + + # SINGLE-PASS grid (loop=False): each arm replays the 100k-point trace + # exactly once so sampling p genuinely thins the realized sample set + # (looping would re-cover the full value distribution and hide ε_s). + # SCALE compresses the ~31d trace timestamps into ~20s wall; SDK_WIN=5s + # yields ~4 tumbling sub-windows the wide-range query merges into one + # pooled p99 (matching the global pooled GT). + SCALE = 134_000 + SDK_WIN = "5s" + # Integrated ε-sweep: rows are the admission p (== the ε-floor p=1/(1+ε²·rate) + # the autonomous coordinator would set for the corresponding ε). raw anchor + + # DDSketch warm at a p-grid spanning the ε range. (dd-delta dropped from the + # first integrated table to keep the run short; re-add for the ε_cdm axis.) + SDK_WIN = "5s" + arms = [ + dict(name="raw", agg="raw-buffer", p=1.0, anchor=True), + dict(name="dd_p1.00", agg="dd-full", p=1.0), + dict(name="dd_p0.50", agg="dd-full", p=0.5), + dict(name="dd_p0.25", agg="dd-full", p=0.25), + dict(name="dd_p0.10", agg="dd-full", p=0.1), + dict(name="dd_p0.05", agg="dd-full", p=0.05), + ] + + ipt_setup() + results = [] + try: + for a in arms: + anchor = a.get("anchor", False) + print(f"\n=== arm {a['name']} (agg={a['agg']} p={a['p']}" + f"{' ANCHOR' if anchor else ''}) ===", file=sys.stderr) + r = run_arm(a["name"], a["agg"], a["p"], SCALE, args.duration, + loop=False, sdk_window=SDK_WIN, + streaming_cfg=args.streaming_config, + trace_csv=args.trace_csv, logroot=logroot) + r["anchor"] = anchor + if anchor: + # raw forwarding is exact by definition + r["rel_err"] = 0.0 + r["accuracy"] = 1.0 + r["p99_measured"] = r["p99"] + elif r["p99"] is not None and gt_p99 > 0: + r["rel_err"] = abs(r["p99"] - gt_p99) / gt_p99 + r["accuracy"] = 1.0 - r["rel_err"] + else: + r["rel_err"] = None + r["accuracy"] = None + # implied ε for this admission p: the ε whose floor 1/(1+ε²·rate)=p, + # using this run's per-window rate (SDK_WIN=5s). p=1 ⇒ ε→0. + rate_w = r["candidate"] / max(1.0, r["wall_s"] / 5.0) + r["rate_window"] = round(rate_w) + r["epsilon"] = 0.0 if r["p"] >= 1.0 else round(((1.0 / r["p"] - 1.0) / rate_w) ** 0.5, 4) + print(f" -> acc={r['accuracy']} lat_p99={r.get('lat_p99_ms')}ms " + f"fresh={r.get('freshness_ms')}ms bkRSS={r.get('backend_rss_mb')}MB " + f"cpu={r['cpu_cores']:.3f} wire={r['wire_kbps']:.1f}kbps ε≈{r['epsilon']}", + file=sys.stderr) + results.append(r) + finally: + subprocess.run(["pkill", "-f", "data_plane --streaming-config"], + capture_output=True) + ipt_teardown() + + # normalize to raw anchor + raw = next(r for r in results if r["name"] == "raw") + raw_cpu = raw["cpu_cores"] or 1e-9 + raw_wire = raw["wire_kbps"] or 1e-9 + for r in results: + r["cpu_norm"] = round(r["cpu_cores"] / raw_cpu, 4) + r["wire_norm"] = round(r["wire_kbps"] / raw_wire, 4) + r["cost_total"] = round(0.5 * (r["cpu_norm"] + r["wire_norm"]), 4) + + payload = {"gt_p99": gt_p99, "raw_anchor": raw["name"], + "cost_def": ("total cost = 0.5*(cpu_norm + wire_norm), each " + "normalized to raw=1.0; cpu = producer/edge " + "utime+stime cores, wire = OTLP gzipped bytes on " + "tcp dport 4317 (iptables) / wall. Backend CPU is " + "OUT of total (same backend binary per arm). Both " + "sub-axes reported so either can be dropped."), + "query": QUERY, + "results": results} + (args.out_dir / "sweep-results.json").write_text( + json.dumps(payload, indent=2) + "\n") + + # ----- the integrated table (the deliverable) ----- + hdr = (f"\n{'arm':9s} {'ε':>6s} {'p':>5s} | {'accuracy':>8s} | " + f"{'lat_p50':>7s} {'lat_p99':>7s} | {'fresh_ms':>8s} | " + f"{'bk_RSS':>6s} {'pr_RSS':>6s} | {'cpu':>6s} {'wire':>7s}") + print(f"\n=== INTEGRATED ε-SWEEP (GT p99={gt_p99:.4f}) ===") + print(" cols: accuracy=1−rel_err vs GT · lat ms · freshness gen→queryable ms ·" + " RSS MB · cpu cores · wire kbps") + print(hdr) + for r in results: + acc = r.get("accuracy") + print(f"{r['name']:9s} {r.get('epsilon',0):>6} {r['p']:>5} | " + f"{('%.4f'%acc) if acc is not None else 'NA':>8s} | " + f"{str(r.get('lat_p50_ms')):>7s} {str(r.get('lat_p99_ms')):>7s} | " + f"{str(r.get('freshness_ms')):>8s} | " + f"{r.get('backend_rss_mb',0):>6} {r.get('producer_rss_mb',0):>6} | " + f"{r['cpu_cores']:>6.3f} {r['wire_kbps']:>7.1f}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/datasets_eval/google_cluster/run.py b/datasets_eval/google_cluster/run.py index 63ee6dd13..227cd58eb 100644 --- a/datasets_eval/google_cluster/run.py +++ b/datasets_eval/google_cluster/run.py @@ -148,6 +148,7 @@ def _replay_otlp_grpc( pace_factor: float, max_lines: int, wall_clock_anchor: bool = False, + anchor_span_s: float = 0.0, ) -> int: """OTLP/gRPC sender. Lazy-imports opentelemetry-proto deps. @@ -203,6 +204,28 @@ def _replay_otlp_grpc( # late rows). The per-family arms (~200k rows) replay in << 60s; for a # multi-minute all-families replay, slice per family or widen the window. anchor_now_ns = time.time_ns() + # anchor_span_s > 0: instead of collapsing EVERY point onto a single + # instant, spread the points across the last `anchor_span_s` seconds with a + # distinct nanosecond each (still one window when span < window_duration and + # the send is boundary-aligned). Single-instant anchoring gives every event + # of a key an IDENTICAL (series, ts, value) tuple, so count-type sketches + # (CountSketch/CountMin: value==1.0 per event) see the duplicates collapse + # and lose all per-key multiplicity — topk/frequency then read ~1 per key. + # A quantile workload is unaffected (its values differ), but a spread is + # strictly safer for it too. Requires a pre-count to size the step. + span_ns = int(anchor_span_s * 1_000_000_000) + total_pts = 0 + if wall_clock_anchor and span_ns > 0: + with open(jsonl_path, "r", encoding="utf-8") as _fp: + total_pts = sum(1 for _l in _fp if _l.strip()) + step_ns = span_ns // max(1, total_pts - 1) if total_pts > 1 else 0 + # Spread FORWARD from now: [now, now+span]. now is boundary-aligned to a + # fresh window start, so the whole span lands in the OPEN window. A backward + # spread [now-span, now] would backdate points into the PREVIOUS window, + # which has already sealed on wall-clock — the agent then drops them as + # late-arriving (only the points nearest `now` survive). Keep span < + # window_duration so it doesn't spill into the next window. + anchor_start_ns = anchor_now_ns def flush(rows: list[dict[str, Any]]) -> None: if not rows: @@ -224,7 +247,7 @@ def flush(rows: list[dict[str, Any]]) -> None: dp = metric.gauge.data_points.add() dp.as_double = float(r["value"]) if wall_clock_anchor: - dp.time_unix_nano = anchor_now_ns + dp.time_unix_nano = r.get("_ts_ns", anchor_now_ns) else: dp.time_unix_nano = int(r["timestamp_ms"]) * 1_000_000 for k, v in sorted(r["attributes"].items()): @@ -261,6 +284,8 @@ def flush(rows: list[dict[str, Any]]) -> None: sleep_ns = target_ns - time.time_ns() if sleep_ns > 0: time.sleep(sleep_ns / 1e9) + if wall_clock_anchor and step_ns > 0: + obj["_ts_ns"] = anchor_start_ns + n * step_ns batch.append(obj) if len(batch) >= BATCH_SIZE: flush(batch) @@ -281,7 +306,8 @@ def cmd_replay(args: argparse.Namespace) -> int: if args.dry_run or not args.endpoint: return _replay_dry_run(args.jsonl, args.max_lines) rc = _replay_otlp_grpc(args.jsonl, args.endpoint, args.pace_factor, args.max_lines, - wall_clock_anchor=getattr(args, "wall_clock_anchor", False)) + wall_clock_anchor=getattr(args, "wall_clock_anchor", False), + anchor_span_s=getattr(args, "anchor_span_s", 0.0)) if rc == 4: return _replay_dry_run(args.jsonl, args.max_lines) return rc @@ -458,6 +484,12 @@ def main(argv: list[str] | None = None) -> int: "Required for recent-range PromQL ([Ns]) to intersect " "the warm sketch windows. Timestamp-only; GT unchanged. " "Replay must finish within one window_duration.") + pr.add_argument("--anchor-span-s", type=float, default=0.0, + help="With --wall-clock-anchor, spread points across the last " + "N seconds (distinct ns each) instead of one instant, so " + "count-type sketches (value==1.0 per event) keep per-key " + "multiplicity. Keep N < window_duration so it stays one " + "window (e.g. 50 for a 60s window).") pr.add_argument("--dry-run", action="store_true", help="Force dry-run even with --endpoint set.") pr.set_defaults(func=cmd_replay) diff --git a/datasets_eval/latency/.gitignore b/datasets_eval/latency/.gitignore index 5630d7f07..b270d4224 100644 --- a/datasets_eval/latency/.gitignore +++ b/datasets_eval/latency/.gitignore @@ -1 +1,2 @@ replay-warm.jsonl +replay-cold.jsonl diff --git a/datasets_eval/latency/agent-cold-ship.yaml b/datasets_eval/latency/agent-cold-ship.yaml new file mode 100644 index 000000000..9c61f00ee --- /dev/null +++ b/datasets_eval/latency/agent-cold-ship.yaml @@ -0,0 +1,61 @@ +# Cold-ON edge agent for the Fig 7 cold-fallback latency arm. +# +# Derived from datasets_eval/multisketch/agent-ddsketch-coldon.yaml, but +# with a COMPLETE cold block (ship_endpoint + block_duration + +# external_labels) so the edge actually SHIPS cold ASAPFRG1 fragments to +# the gorilla-merger. The multisketch coldon config only set +# `cold: {enabled: true}` with NO ship_endpoint, which the asapedge +# processor treats as DRAIN-ONLY (no shipping) — that is why the +# original cold arm landed an empty MinIO (config.go: "Empty => +# drain-only (no shipping)"). +# +# The lossless raw `google_cluster_2019_cpu_rate` Sum family (tier: both) +# is the series that ships cold; the data-plane routes its queries to the +# gorilla cold tier (backend-storage-routing-coldon.yaml). +receivers: + otlp: + protocols: + grpc: {endpoint: 0.0.0.0:4317, max_recv_msg_size_mib: 4096} + http: {endpoint: 0.0.0.0:4318} +processors: + batch: + send_batch_size: 800 + send_batch_max_size: 1500 + memory_limiter: {check_interval: 1s, limit_mib: 8192, spike_limit_mib: 1024} + asap_edge: + shard_count: 12 + window_duration: 60s + drop_original: true + max_series: 200000 + delta_transmission: false + metrics: + - {metric: google_cluster_2019_cpu_rate, family: sum, tier: both} + - {metric: google_cluster_2019_memory_usage, family: sum, aggregate_by: [zone], tier: both} + - metric: google_cluster_2019_cpu_rate_q_ddsketch + family: "ddsketch" + tier: "both" + delta_transmission: true + cold: + enabled: true + ship_endpoint: http://gorilla-merger:10908/ingest/gorilla + block_duration: 60s + reorder_grace: 2s + external_labels: + # MUST be non-empty: Thanos rejects a block with empty external + # labels ("empty external labels are not allowed for Thanos + # block"); matches the merger's --external-labels cluster=asap-mvp. + cluster: asap-mvp + control_channel: {enabled: false} +exporters: + otlp/backend: + endpoint: data-plane:14317 + tls: {insecure: true} + timeout: 120s + sending_queue: {enabled: true, num_consumers: 4, queue_size: 5000} +service: + pipelines: + metrics: {receivers: [otlp], processors: [memory_limiter, asap_edge, batch], exporters: [otlp/backend]} + telemetry: + metrics: + level: detailed + readers: [{pull: {exporter: {prometheus: {host: 0.0.0.0, port: 8890}}}}] diff --git a/datasets_eval/latency/backend-storage-routing-coldon.yaml b/datasets_eval/latency/backend-storage-routing-coldon.yaml new file mode 100644 index 000000000..96387bf3c --- /dev/null +++ b/datasets_eval/latency/backend-storage-routing-coldon.yaml @@ -0,0 +1,29 @@ +# Cold-arm storage-routing table (Fig 7 cold-fallback latency arm). +# +# Routes the lossless raw `google_cluster_2019_cpu_rate` series — the +# Sum family the cold-enabled agent ships to the gorilla cold tier +# (MinIO via the merger) — to `gorilla_object_store`, so its PromQL +# queries are answered by the ThanosQueryEngine (data_source = +# thanos_query). This is what makes the COLD/archive path observable +# end-to-end: an instant query against this metric is dispatched +# through EngineRouter to thanos-query, which resolves the answer over +# the gorilla-merger StoreAPI + thanos-store-gateway (S3/MinIO blocks). +# +# `double_write` routes the metric to BOTH warm sketches and the +# archive, with the cost-aware dispatcher picking per query. We use +# the single-target `gorilla_object_store` here to GUARANTEE every +# timed query lands on the cold path (no warm shortcut) — we are +# measuring the cold arm, so we force the cold engine. +# +# Used by datasets_eval/latency/stack-coldon.sh (cold-ON stack). + +default: sketch_store + +routes: + - metric: google_cluster_2019_cpu_rate + targets: + - backend: gorilla_object_store + # all shapes route to the cold archive — sum / count / + # quantile_over_time over the raw shipped samples are all + # computed by thanos-query over the merger StoreAPI. + applies_to_query_shape: [count, topk, rate_post_hoc, quantile, sum, last_over_time, other] diff --git a/datasets_eval/latency/cold_latency_replay.py b/datasets_eval/latency/cold_latency_replay.py new file mode 100644 index 000000000..1e6a317ff --- /dev/null +++ b/datasets_eval/latency/cold_latency_replay.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Cold-arm query-latency replay (Fig 7 cold-fallback arm). + +Mirrors deploy/mvp-singlenode/scripts/metricsql_replay.py (warm arm) but +pins the PromQL *evaluation timestamp* (`time=`) to the instant +the cold workload was anchored at, so every instant query deterministically +intersects the cold-archived window in MinIO/Thanos. (The warm arm queried +at live `now` because its in-memory warm window sat at `now`; the cold +window sits at a fixed past instant — the ship takes ~one window to land — +so we pin the eval time to it. The pin changes only WHICH timestamp the +backend evaluates at; the per-query server-side latency it measures is +identical in kind to the warm arm.) + +Fires the query mix at a fixed QPS against the data-plane query surface +(:9091/api/v1/query), captures per-query wall-clock latency + the served +`data_source` (asap_query=warm vs thanos_query=cold) + result vector, and +writes a JSONL identical in schema to the warm replay so +compute_latency.py reduces both arms the same way. + +Usage: + cold_latency_replay.py --target http://127.0.0.1:9091 \ + --queries queries-latency-cold.json --at-time \ + --qps 15 --duration 40 --out replay-cold.jsonl +""" +from __future__ import annotations +import argparse, datetime as dt, json, sys, threading, time +import urllib.parse, urllib.request, urllib.error + + +def run_query(target: str, metricsql: str, at_time: float | None, + timeout_s: float = 10.0): + params = {"query": metricsql} + if at_time is not None: + params["time"] = f"{at_time:.3f}" + url = f"{target.rstrip('/')}/api/v1/query?" + urllib.parse.urlencode(params) + started = time.perf_counter() + try: + with urllib.request.urlopen(url, timeout=timeout_s) as resp: + code = resp.getcode() + body = resp.read().decode("utf-8", errors="replace") + except urllib.error.HTTPError as e: + return (time.perf_counter() - started) * 1000.0, { + "status": "http_error", "http_code": e.code, "result": None, + "result_type": None, "data_source": None, "error": str(e)} + except Exception as e: + return (time.perf_counter() - started) * 1000.0, { + "status": "timeout", "http_code": None, "result": None, + "result_type": None, "data_source": None, "error": str(e)} + duration_ms = (time.perf_counter() - started) * 1000.0 + try: + parsed = json.loads(body) + except json.JSONDecodeError as e: + return duration_ms, {"status": "json_error", "http_code": code, + "result": None, "result_type": None, + "data_source": None, "error": str(e)} + data = parsed.get("data") or {} + data_source = None + for info in (parsed.get("infos") or []): + if isinstance(info, str) and info.startswith("data_source:"): + data_source = info.split(":", 1)[1].strip() + return duration_ms, { + "status": parsed.get("status", "unknown"), "http_code": code, + "result": data.get("result"), "result_type": data.get("resultType"), + "data_source": data_source} + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--target", default="http://127.0.0.1:9091") + ap.add_argument("--queries", required=True) + ap.add_argument("--at-time", type=float, default=None, + help="Unix seconds to pin the PromQL eval timestamp to. " + "Omit to query live now (warm-style).") + ap.add_argument("--qps", type=float, default=15.0) + ap.add_argument("--duration", type=float, default=40.0) + ap.add_argument("--out", required=True) + args = ap.parse_args() + + with open(args.queries) as f: + queries = json.load(f) + if not queries: + sys.exit("no queries") + + interval = 1.0 / args.qps + deadline = time.time() + args.duration + lock = threading.Lock() + rows: list[dict] = [] + qi = 0 + n_sent = 0 + t0 = time.time() + while time.time() < deadline: + q = queries[qi % len(queries)] + qi += 1 + dur_ms, res = run_query(args.target, q["metricsql"], args.at_time) + rec = { + "ts": dt.datetime.now(dt.timezone.utc).isoformat(), + "query": q["metricsql"], "kind": q.get("kind", "other"), + "duration_ms": round(dur_ms, 4), "status": res["status"], + "http_code": res.get("http_code"), + "result_type": res.get("result_type"), + "result": res.get("result"), + "data_source": res.get("data_source"), + "n_result_series": len(res.get("result") or []), + } + rows.append(rec) + n_sent += 1 + # fixed-rate pacing + next_at = t0 + n_sent * interval + sleep = next_at - time.time() + if sleep > 0: + time.sleep(sleep) + + with open(args.out, "w") as f: + for r in rows: + f.write(json.dumps(r) + "\n") + # quick summary + from collections import Counter + ds = Counter(r["data_source"] for r in rows) + st = Counter(r["status"] for r in rows) + empt = sum(1 for r in rows if not r["result"]) + print(f"replay: {len(rows)} queries -> {args.out}") + print(f" data_source: {dict(ds)}") + print(f" status: {dict(st)} empty_results: {empt}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/datasets_eval/latency/compute_latency.py b/datasets_eval/latency/compute_latency.py index f25fd6b71..5ac4c1497 100644 --- a/datasets_eval/latency/compute_latency.py +++ b/datasets_eval/latency/compute_latency.py @@ -41,21 +41,36 @@ def pct(sorted_vals, p): return sorted_vals[lo] + (sorted_vals[hi] - sorted_vals[lo]) * frac -def summarize(rows, label, require_warm=True): +def _n_result(r) -> int: + """Result count from either the full replay schema (`result` vector) or + the slim per-query schema (`n_result_series`).""" + res = r.get("result") + if res is not None: + return len(res) + return int(r.get("n_result_series") or 0) + + +def summarize(rows, label, require_real=True, require_source=None): """Returns (summary_dict, {kind: [latencies]}, [all_latencies]). - Hard-fails if require_warm and any timed query was empty/errored.""" + Hard-fails if require_real and any timed query was empty/errored, or if + require_source is set and any timed query was served by a different + data_source (guards that the arm's answers came from the intended tier).""" bad = [] + wrong_src = [] by_kind: dict[str, list[float]] = {} alllat: list[float] = [] src_counter: dict[str, int] = {} for r in rows: - res = r.get("result") or [] + n = _n_result(r) src = r.get("data_source") src_counter[src] = src_counter.get(src, 0) + 1 - ok = r.get("status") == "success" and bool(res) - if require_warm and not ok: + ok = r.get("status") == "success" and n > 0 + if require_real and not ok: bad.append({"query": r.get("query"), "status": r.get("status"), - "n_result": len(res), "data_source": src}) + "n_result": n, "data_source": src}) + continue + if require_source is not None and src != require_source: + wrong_src.append({"query": r.get("query"), "data_source": src}) continue if not ok: continue @@ -63,10 +78,14 @@ def summarize(rows, label, require_warm=True): alllat.append(lat) by_kind.setdefault(r["kind"], []).append(lat) - if require_warm and bad: + if require_real and bad: sys.exit(f"[{label}] {len(bad)} timed queries returned no real result " f"(empty/errored) — latency would be meaningless. First few: " f"{bad[:3]}") + if require_source is not None and wrong_src: + sys.exit(f"[{label}] {len(wrong_src)} timed queries were NOT served by " + f"data_source={require_source} (wrong tier — this arm must " + f"measure that tier only). First few: {wrong_src[:3]}") def stats(vals): s = sorted(vals) @@ -98,24 +117,41 @@ def cdf_xy(vals): return s, ys +def load_any(path: str): + """Load either a JSONL replay log (one object per line) or a JSON array + (the committed slim per_query_latency*.json).""" + text = Path(path).read_text().lstrip() + if text.startswith("["): + return json.loads(text) + return load(path) + + def main(): ap = argparse.ArgumentParser() - ap.add_argument("--warm", required=True) - ap.add_argument("--cold", default=None) + ap.add_argument("--warm", required=True, + help="warm replay JSONL or committed per_query_latency.json") + ap.add_argument("--cold", default=None, + help="cold replay JSONL or per_query_latency_cold.json") ap.add_argument("--out-json", required=True) ap.add_argument("--out-png", required=True) args = ap.parse_args() - warm_rows = load(args.warm) - warm_sum, warm_by_kind, warm_all = summarize(warm_rows, "warm", require_warm=True) + warm_rows = load_any(args.warm) + # Warm arm: every timed query must be a real warm answer (data_source=asap_query). + warm_sum, warm_by_kind, warm_all = summarize( + warm_rows, "warm", require_real=True, require_source="asap_query") out = {"warm": warm_sum} cold_all = None + cold_by_kind = None if args.cold and Path(args.cold).exists(): - cold_rows = load(args.cold) - # cold arm: don't hard-fail on empties (route may differ); report what landed - cold_sum, _, cold_all = summarize(cold_rows, "cold", require_warm=False) + cold_rows = load_any(args.cold) + # Cold arm: GUARD that every timed query landed real AND was served by + # the cold/archive engine (data_source=thanos_query) — we are measuring + # the cold-fallback tier, so a warm shortcut would invalidate the arm. + cold_sum, cold_by_kind, cold_all = summarize( + cold_rows, "cold", require_real=True, require_source="thanos_query") out["cold"] = cold_sum Path(args.out_json).write_text(json.dumps(out, indent=2) + "\n") @@ -136,28 +172,47 @@ def main(): color=palette.get(kind, "#7f7f7f"), lw=1.4, ls="--", alpha=0.85) if cold_all: x, y = cdf_xy(cold_all) - ax.plot(x, y, label=f"cold-fallback ({len(cold_all)} q)", - color="#d62728", lw=2.0) - + ax.plot(x, y, label=f"cold-fallback — all ({len(cold_all)} q)", + color="#d62728", lw=2.2) + cold_palette = {"quantile": "#8c564b", "sum": "#e377c2"} + for kind, vals in sorted((cold_by_kind or {}).items()): + x, y = cdf_xy(vals) + ax.plot(x, y, label=f"cold — {kind} ({len(vals)} q)", + color=cold_palette.get(kind, "#d62728"), lw=1.4, ls="--", alpha=0.85) + + # percentile guide lines: warm (solid grey) + cold (red) overall p50/p99 for p, ls in ((50, ":"), (99, "-.")): v = pct(sorted(warm_all), p) ax.axvline(v, color="#888", ls=ls, lw=0.9) - ax.text(v, 0.04, f"p{p}={v:.1f}ms", rotation=90, fontsize=7, + ax.text(v, 0.04, f"warm p{p}={v:.1f}ms", rotation=90, fontsize=6, va="bottom", ha="right", color="#555") + if cold_all: + cv = pct(sorted(cold_all), p) + ax.axvline(cv, color="#d62728", ls=ls, lw=0.8, alpha=0.6) + ax.text(cv, 0.04, f"cold p{p}={cv:.1f}ms", rotation=90, fontsize=6, + va="bottom", ha="right", color="#d62728") ax.set_xlabel("backend query latency (ms)") ax.set_ylabel("CDF (fraction of queries ≤ x)") - ax.set_title("Fig 7 — backend query latency CDF (warm sketch tier, single-node loopback)") + title = "Fig 7 — backend query latency CDF (single-node loopback)" + if cold_all: + title = ("Fig 7 — backend query latency CDF: warm sketch tier vs " + "cold-fallback archive\n(single-node loopback)") + ax.set_title(title) ax.set_ylim(0, 1.02) ax.set_xlim(left=0) ax.grid(True, alpha=0.3) - ax.legend(loc="lower right", fontsize=8) + ax.legend(loc="lower right", fontsize=7) fig.tight_layout() fig.savefig(args.out_png, dpi=140) print(f"wrote {args.out_json} and {args.out_png}") - print(json.dumps(out["warm"]["overall"], indent=2)) + print("WARM:", json.dumps(out["warm"]["overall"])) for k, v in out["warm"]["by_kind"].items(): - print(f" {k:14s} p50={v['p50_ms']:.2f} p95={v['p95_ms']:.2f} p99={v['p99_ms']:.2f} (n={v['n']})") + print(f" warm {k:10s} p50={v['p50_ms']:.2f} p95={v['p95_ms']:.2f} p99={v['p99_ms']:.2f} (n={v['n']})") + if cold_all: + print("COLD:", json.dumps(out["cold"]["overall"])) + for k, v in out["cold"]["by_kind"].items(): + print(f" cold {k:10s} p50={v['p50_ms']:.2f} p95={v['p95_ms']:.2f} p99={v['p99_ms']:.2f} (n={v['n']})") if __name__ == "__main__": diff --git a/datasets_eval/latency/latency_RESULTS.md b/datasets_eval/latency/latency_RESULTS.md index 9a13fb3ca..8a7006335 100644 --- a/datasets_eval/latency/latency_RESULTS.md +++ b/datasets_eval/latency/latency_RESULTS.md @@ -6,8 +6,11 @@ 599-query mix at 15 QPS, overall **p50 = 18.3 ms, p99 = 20.0 ms** on the warm SketchStore. The exact lossless `sum` is **p50 1.6 ms / p99 2.3 ms**; the 691-series DDSketch `quantile_over_time` read is **p50 18.3 ms / p99 20.6 ms**. -**Cold-fallback arm: NOT measured — blocked by archive/ship fragility (see -below). Warm-only is reported honestly.** +**Cold-fallback arm: NOW MEASURED** (cold-ON stack, end-to-end through the +gorilla cold tier): across a 600-query mix @15 QPS with **600/600 +`data_source=thanos_query`**, the cold/archive-answered PromQL is **p50 22.3 ms +/ p95 47.2 ms / p99 67.1 ms** — overall **p50 ≈ 1.2× warm, p99 ≈ 3.3× warm**. +Both arms are reported below; the warm numbers are unchanged. --- @@ -92,50 +95,119 @@ kind) — no long latency tail on the warm path. --- -## Cold-fallback arm — attempted, BLOCKED (warm-only reported) - -I brought up the **full cold stack** (`stack.sh`: MinIO + Thanos -store/query/compact + gorilla-merger + data-plane with `ASAP_THANOS_QUERY_URL` -and `ASAP_GORILLA_S3_*`), with a **cold-enabled** agent -(`agent-ddsketch-coldon.yaml`: `cold: {enabled: true}`, sketch `tier: both`), -re-replayed the same trace, and waited for the warm window to seal. - -**Result: no data ever reached the cold tier**, so there was nothing to query -on a cold path: - -- data-plane registered `ThanosQueryEngine` (archive `data_source_id=thanos_query`) OK; -- the gorilla-merger ingest frontend (`:10908`) logged **zero** ingest / - append / received-samples lines; its shipper found nothing to ship; -- **MinIO `asap/` held 0 objects** after the run (both `asap-gorilla` and - `asap-gorilla-tsdb` empty); -- the bare static-config agent emitted **no** cold/ship/gorilla/s3 log lines — - the `cold: {enabled: true}` block does not drive a gorilla ship in this - static fused-agent path (cold endpoint wiring rides the control channel, - which is `{enabled: false}` here); -- even querying **old timestamps** (1 h ago) still returned `data_source=asap_query` - (warm), confirming there was no archived data to fall through to. - -This is the archive/ship/routing fragility the task anticipated. Rather than -fabricate a cold arm, I report **warm-only**. (Building the cold arm would need -the supervised agent + live control channel to actually drive the gorilla ship, -plus aging the warm window out — out of scope for a clean, real measurement -here.) +## Cold-fallback arm (MEASURED) — cold/archive-answered PromQL + +This is the arm that was previously blocked. It is now measured end-to-end +through the gorilla cold tier. + +### Setup (cold-ON stack, `datasets_eval/latency/stack-coldon.sh`) + +Full cold stack on one host: **MinIO** (:9000) + **gorilla-merger** +(:10908 HTTP ingest / :10907 Thanos StoreAPI) + **thanos-store-gateway** +(reads the merger's shipped `asap-gorilla-tsdb` blocks) + **thanos-query** +(:10903, federates the merger StoreAPI + store-gateway) + **data-plane** with +`ASAP_THANOS_QUERY_URL=http://thanos-query:10903` (so it registers the real +**`ThanosQueryEngine`**, `data_source_id=thanos_query`, NOT the +`NoDataArchiveEngine` stub) + a **cold-enabled edge** with a *complete* cold +block (`agent-cold-ship.yaml`: `cold.enabled:true`, +`cold.ship_endpoint: http://gorilla-merger:10908/ingest/gorilla`, +`block_duration:60s`, `external_labels.cluster:asap-mvp`). + +- **Routing:** a cold storage-routing table + (`backend-storage-routing-coldon.yaml`) pins + `google_cluster_2019_cpu_rate → gorilla_object_store` for every query shape, + so its instant queries dispatch through the `EngineRouter` to + `thanos_query` (the cold/archive engine) rather than the warm + `SketchStore`. +- **Workload:** same `/tmp/dd-only.jsonl` slice (lossless raw + `google_cluster_2019_cpu_rate` Sum family, `tier: both` — the series that + ships cold), wall-clock-anchored replay (one cold window at a fixed instant). +- **Control-plane stopped during timing:** it periodically re-POSTs a + storage-routing table to `/api/v1/storage_routing` (~every 60 s) that + overrides the file-loaded cold table back to warm; with it stopped, the + data-plane keeps the file cold table and the cold archive answers + independently (cold ship is decoupled from the control channel — PR #500). + +### What was the original blocker (and the fix) + +The original cold arm failed because the multisketch `agent-ddsketch-coldon.yaml` +set only `cold: {enabled: true}` with **no `ship_endpoint`** — and the +asapedge processor treats an empty ship endpoint as **drain-only, no +shipping** (`config.go`: "Empty => drain-only (no shipping)"). So the cold +encoder accumulated samples but never POSTed them → MinIO/merger stayed empty. +With a complete cold block the edge ships per-shard ASAPFRG1 fragment batches +to the merger. Verified live (per-shard agent logs): +`cold drain {active_series:245, fragments:245}` → `cold shipBatch +{fragments:245, shipper_noop:false}` with **no** ship-failure/spool lines, and +the merger then served `count(...)=1000` over its StoreAPI. + +### GUARD (ran before any timing — PASSED, at the pinned cold anchor) + +| query | status | result | data_source | +|---|---|---|---| +| `sum(google_cluster_2019_cpu_rate)` | success | **22.13** (exact archive sum) | **`thanos_query`** | +| `quantile_over_time(0.99, …_cpu_rate[300s])` | success | **1000 series** | **`thanos_query`** | +| `quantile_over_time(0.50, …_cpu_rate[300s])` | success | 1000 series | **`thanos_query`** | +| `count(google_cluster_2019_cpu_rate)` | success | 1000 | **`thanos_query`** | + +Every required timed query returns a **real archive value** served by the +**cold engine** (`thanos_query`), so the cold timing is meaningful and is the +cold tier (not a warm shortcut). The `quantile_over_time` is computed by +thanos-query **over the raw archived Gorilla-XOR samples** (not over a +DDSketch — the cold tier stores lossless raw samples). + +### Measurement + +`cold_latency_replay.py` fired the cold mix at a **fixed 15 QPS** against +`:9091/api/v1/query`, **pinning the PromQL eval timestamp** (`time=`) +to the instant the cold workload was anchored at (the cold window sits at a +fixed past instant because the ship takes ~one window to land; the warm arm +queried live `now` because its in-memory warm window sat at `now`). The pin +changes only WHICH timestamp the backend evaluates at — the per-query +server-side latency it measures is identical in kind to the warm arm. + +- **Query mix:** `quantile_over_time(0.99,…_cpu_rate[300s])`, + `quantile_over_time(0.50,…_cpu_rate[300s])`, `sum(cpu_rate)` + (`queries-latency-cold.json`). +- **QPS / count / duration:** 15 QPS, **600 queries**, 40 s window. +- **Realness:** **600/600 success, 600/600 non-empty, 600/600 + `data_source=thanos_query`** (0 empties, 0 errors, 0 warm shortcuts). + +### Latency table (cold-fallback archive tier) + +| query kind | p50 (ms) | p95 (ms) | p99 (ms) | n | note | +|---|---|---|---|---|---| +| **all (mix)** | **22.28** | **47.17** | **67.07** | 600 | mean 25.57, min 9.12, max 85.14 | +| `quantile_over_time` (1000-series, Thanos PromQL over raw samples) | 24.27 | 48.59 | **68.41** | 400 | | +| `sum` (lossless, archive) | 15.48 | 33.33 | **42.74** | 200 | exact archive sum | + +**Warm vs cold:** overall **p50 18.25 → 22.28 ms (≈1.2×)**, **p99 20.03 → +67.07 ms (≈3.3×)**. The cold path stays in the tens of ms (no order-of-magnitude +blowup) but has a heavier p99 tail: each cold answer crosses data-plane → +thanos-query → gorilla-merger StoreAPI + store-gateway and re-evaluates PromQL +over the raw archived samples, vs the warm tier's in-memory sketch read. --- ## Honesty / caveats -- **Single-node loopback:** data-plane, agent, and replay client all on - `127.0.0.1`. **No network RTT** is included — these are server-side query - latencies only; a remote client adds its own RTT on top. -- **QPS / count / window:** 15 QPS, 599 queries, one 40 s steady window inside - a single fully-shipped warm window. Modest QPS — this measures per-query - serving latency, not a saturation/throughput study. -- **Warm-only:** the cold-fallback arm did not produce archived data (above); - no cold numbers are claimed, so the "cold ≤ 2× warm" check was not evaluated. -- **Query realness verified:** every timed query returned a real warm value - (`data_source=asap_query`); the reducer hard-fails if any timed query were - empty/errored, so these latencies are not "latency of No result". +- **Single-node loopback:** data-plane, agent, merger, thanos, MinIO, and the + replay client all on `127.0.0.1`. **No network RTT** is included — these are + server-side query latencies only; a remote client adds its own RTT on top. + (For the cold arm the inter-service hops data-plane→thanos→merger are still + loopback, so a real multi-host deploy would add per-hop RTT to the cold tail.) +- **QPS / count / window:** 15 QPS; 599 (warm) / 600 (cold) queries; one 40 s + steady window inside a single fully-shipped window. Modest QPS — this measures + per-query serving latency, not a saturation/throughput study. +- **Cold eval-time pin:** cold queries are evaluated at the fixed cold-window + anchor (the data sits at one instant after a wall-clock-anchored replay), so + the cold latencies are the backend's time to *serve a cold/archive query*, + not a study of cold-window freshness/aging. +- **Query realness + tier verified:** the reducer hard-fails if any timed query + was empty/errored OR served by the wrong tier — warm must be + `data_source=asap_query`, cold must be `data_source=thanos_query` — so these + latencies are neither "latency of No result" nor a warm answer mislabeled as + cold. --- @@ -161,15 +233,60 @@ python3 datasets_eval/latency/compute_latency.py --warm datasets_eval/latency/re bash datasets_eval/multisketch/stack-coldoff.sh down ``` +### Cold-fallback arm + +```bash +# build the dev images if absent (buildx --load; data-plane/control-plane/ +# gorilla-merger from /mydata/ASAPQuery-backend, asap-otel via build_asap_otel.sh) +# then bring up the COLD-ON stack (uses sudo docker; --user 0 on the merger): +bash datasets_eval/latency/stack-coldon.sh up \ + datasets_eval/multisketch/workloads/ddsketch.yaml \ + datasets_eval/latency/agent-cold-ship.yaml \ + datasets_eval/latency/backend-storage-routing-coldon.yaml +# 1. wall-clock-anchored replay; capture the printed `now=` anchor +python3 datasets_eval/google_cluster/run.py replay \ + --jsonl /tmp/dd-only.jsonl --endpoint 127.0.0.1:4317 --pace-factor 0 --wall-clock-anchor +# 2. wait until the cold ship lands: poll thanos-query / data-plane until +# count(google_cluster_2019_cpu_rate)@ returns 1000 via thanos_query +# 3. stop the control-plane so it can't override the cold routing table back to warm: +sudo docker rm -f asap-control-plane +# 4. GUARD then timed cold replay @ 15 QPS, pinned to the anchor: +python3 datasets_eval/latency/cold_latency_replay.py --target http://127.0.0.1:9091 \ + --queries datasets_eval/latency/queries-latency-cold.json \ + --at-time --qps 15 --duration 40 \ + --out datasets_eval/latency/replay-cold.jsonl +# 5. reduce BOTH arms -> tables + combined CDF (hard-fails if any cold query +# was not data_source=thanos_query, or any warm query not asap_query): +python3 datasets_eval/latency/compute_latency.py \ + --warm datasets_eval/latency/per_query_latency.json \ + --cold datasets_eval/latency/per_query_latency_cold.json \ + --out-json datasets_eval/latency/latency_summary.json \ + --out-png datasets_eval/latency/latency_cdf.png +# 6. teardown +bash datasets_eval/latency/stack-coldon.sh down +``` + ## Deliverables (this dir) - `latency_RESULTS.md` — this file -- `latency_cdf.png` — Fig 7 CDF (warm, overall + per-kind) -- `latency_summary.json` — p50/p95/p99 (overall + per-kind), data_source counts -- `per_query_latency.json` — slim per-query latency log (599 records: ts, query, - kind, duration_ms, status, data_source, n_result_series). The raw - `replay-warm.jsonl` (42 MB, verbatim 691-series result vectors per query) is - the reducer's input but is intentionally **not committed** — regenerate via - step 4 of Reproduce. +- `latency_cdf.png` — Fig 7 CDF (warm + cold-fallback, overall + per-kind) +- `latency_summary.json` — p50/p95/p99 (overall + per-kind), data_source counts, + for BOTH the `warm` and `cold` arms +- `per_query_latency.json` — slim warm per-query log (599 records) +- `per_query_latency_cold.json` — slim cold per-query log (600 records: ts, + query, kind, duration_ms, status, http_code, data_source, n_result_series) +- `compute_latency.py` — reducer (warm + cold; renders the combined CDF; guards + each arm's `data_source`) +- `cold_latency_replay.py` — cold-arm replay client (eval-time-pinned) +- `stack-coldon.sh` — cold-ON single-host stack (MinIO + merger + thanos + + cold-routed data-plane + cold-enabled edge) +- `backend-storage-routing-coldon.yaml` — cold routing table + (`cpu_rate → gorilla_object_store`) +- `agent-cold-ship.yaml` — cold-enabled edge with a complete `cold.ship_endpoint` + block (the missing piece that unblocked the arm) +- `queries-latency-cold.json` — the timed cold mix +- The raw `replay-warm.jsonl` / `replay-cold.jsonl` (verbatim result vectors) are + the reducer's optional input but intentionally **not committed** — regenerate + via Reproduce. - `compute_latency.py` — reducer (also renders the CDF) - `../../deploy/mvp-singlenode/scripts/queries-latency-warm.json` — the timed mix diff --git a/datasets_eval/latency/latency_cdf.png b/datasets_eval/latency/latency_cdf.png index 0ac96465c47802c3d67b4ad06ac77333ff2246db..864b24d22cf150b0d0d453f2521fc911f1eac1a5 100644 GIT binary patch literal 110728 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 z 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 z
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 literal 68819 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 diff --git a/datasets_eval/latency/latency_summary.json b/datasets_eval/latency/latency_summary.json index 8506d0980..16fdfb089 100644 --- a/datasets_eval/latency/latency_summary.json +++ b/datasets_eval/latency/latency_summary.json @@ -12,7 +12,7 @@ "p50_ms": 18.25, "p95_ms": 19.451, "p99_ms": 20.032, - "max_ms": 23.778, + "max_ms": 23.777, "mean_ms": 12.959 }, "by_kind": { @@ -22,7 +22,7 @@ "p50_ms": 18.345, "p95_ms": 19.564, "p99_ms": 20.647, - "max_ms": 23.778, + "max_ms": 23.777, "mean_ms": 18.569 }, "sum": { @@ -35,5 +35,42 @@ "mean_ms": 1.683 } } + }, + "cold": { + "label": "cold", + "n_queries": 600, + "n_real_warm": 600, + "data_source_counts": { + "thanos_query": 600 + }, + "overall": { + "n": 600, + "min_ms": 9.124, + "p50_ms": 22.278, + "p95_ms": 47.173, + "p99_ms": 67.071, + "max_ms": 85.143, + "mean_ms": 25.569 + }, + "by_kind": { + "quantile": { + "n": 400, + "min_ms": 16.025, + "p50_ms": 24.271, + "p95_ms": 48.592, + "p99_ms": 68.408, + "max_ms": 85.143, + "mean_ms": 29.433 + }, + "sum": { + "n": 200, + "min_ms": 9.124, + "p50_ms": 15.479, + "p95_ms": 33.333, + "p99_ms": 42.737, + "max_ms": 45.786, + "mean_ms": 17.843 + } + } } } diff --git a/datasets_eval/latency/per_query_latency_cold.json b/datasets_eval/latency/per_query_latency_cold.json new file mode 100644 index 000000000..a6b9087fd --- /dev/null +++ b/datasets_eval/latency/per_query_latency_cold.json @@ -0,0 +1,6002 @@ +[ + { + "ts": "2026-06-14T19:53:02.474410+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 34.3306, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:02.537868+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 31.8044, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:02.588112+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 16.4205, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:02.666114+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.7307, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:02.727873+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.4653, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:02.794125+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.4749, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:02.858351+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.9729, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:02.935582+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 29.5031, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:02.981564+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.9108, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:03.061234+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.4192, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.124776+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.7241, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.194750+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.0956, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:03.258772+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.1067, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.327954+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.8997, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.381309+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.6585, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:03.459777+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.3797, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.525730+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.3103, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.582765+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.1179, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:03.660856+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.4303, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.737431+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 31.3693, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.781588+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.9407, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:03.862055+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.2949, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.923189+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.1585, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:03.986471+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.831, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:04.059161+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.7298, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.128970+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.5387, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.181705+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.0582, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:04.263431+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.9906, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.326505+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.4596, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.394196+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.5504, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:04.460118+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.3425, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.536128+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 29.6986, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.583341+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.6913, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:04.669967+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 30.1986, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.742110+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.6157, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.784227+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.5632, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:04.859449+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.938, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.927671+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.2922, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:04.983299+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.6479, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:05.061856+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.4383, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.128374+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.2601, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.181520+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.8615, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:05.266279+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.4691, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.324585+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.5206, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.386080+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.4206, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:05.457560+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.1473, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.529968+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.5362, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.582957+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.2966, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:05.657853+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.4096, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.731032+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.9266, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.781606+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.9414, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:05.859902+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.1151, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.942861+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 36.7596, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:05.991942+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 20.2773, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:06.064059+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.6686, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.127887+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.4834, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.182071+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.4125, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:06.266174+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.7607, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.327189+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.8113, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.381912+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.2515, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:06.474498+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 35.053, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.526370+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.2653, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.591275+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 19.629, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:06.666887+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.1087, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.733483+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.3901, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.782026+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.3634, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:06.864888+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.45, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:06.923837+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.4141, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.006260+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 34.6013, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:07.058246+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.8269, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.128682+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.5964, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.187512+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.846, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:07.258458+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.6681, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.326916+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.8216, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.382589+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.9405, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:07.459853+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.4084, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.526142+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.7026, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.583281+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.6349, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:07.658125+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.7343, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.731508+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.1281, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.785487+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.8329, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:07.875590+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.2515, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.931613+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.493, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:07.981799+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.1419, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:08.064020+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.2751, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.125018+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.9084, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.183491+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.8255, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:08.264145+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.709, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.326642+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.2405, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.383937+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.2824, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:08.462218+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.7771, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.526825+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.7234, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.582128+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.4732, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:08.671103+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 31.3057, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.724713+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.6123, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.785772+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.1203, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:08.858445+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.0131, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.928380+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.9358, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:08.981799+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.1485, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:09.069488+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 30.0573, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.124547+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.4695, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.185176+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.5078, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:09.263075+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.3008, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.329632+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.5434, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.383535+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.8734, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:09.465134+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.3892, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.529607+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.53, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.581469+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.8154, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:09.673624+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 34.1984, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.729837+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.4247, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.783182+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.5233, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:09.872678+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 33.2548, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:09.925384+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.3075, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.014372+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 42.7112, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:10.065996+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.2435, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.134067+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.9585, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.185758+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.0905, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:10.257895+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.4543, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.328393+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.0209, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.385859+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.1933, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:10.460045+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.6035, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.526335+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.232, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.582601+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.9415, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:10.663218+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.452, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.725405+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.2674, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.785636+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.9724, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:10.866282+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.5801, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.977109+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.7418, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:10.988780+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.5592, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:11.062896+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.4863, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.162383+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 55.9744, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.217449+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 45.7857, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:11.277923+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 38.5243, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.359404+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 53.2876, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.401521+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 29.8597, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:11.482075+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.312, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.533536+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.4376, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.592061+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 20.4019, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:11.658121+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.7142, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.729157+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.7566, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.786875+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.2159, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:11.860923+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.5117, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.927822+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.7339, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:11.983709+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.0565, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:12.058413+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.702, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.136438+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 30.3239, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.182546+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.8826, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:12.260483+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.0816, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.325718+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.3002, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.392800+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 21.143, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:12.482225+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.7808, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.537786+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 31.388, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.588608+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 16.9427, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:12.657981+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.5995, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.750004+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.9057, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.782271+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.6098, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:12.866776+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.0549, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.929045+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.9279, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:12.990003+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 18.3403, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:13.057231+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.7962, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.126499+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.0456, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.196567+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.8944, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:13.258437+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.0054, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.326084+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.9805, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.385091+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.433, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:13.461736+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.946, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.532623+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.5683, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.581568+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.9065, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:13.661510+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.0766, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.726944+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.5323, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.788936+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.2739, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:13.858258+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.8424, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.938032+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 31.6354, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:13.983083+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.4217, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:14.130440+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.0532, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.152652+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.9647, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.190112+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 18.4416, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:14.257918+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.1384, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.332657+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.6052, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.387605+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.9415, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:14.460314+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.9012, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.529578+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.1306, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.584900+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.2469, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:14.660382+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.9556, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.727798+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.6958, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.784104+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.4472, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:14.858921+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.1861, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.933178+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.0462, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:14.982760+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.1036, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:15.073809+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 34.3562, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.125692+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.262, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.194877+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.2108, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:15.258163+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.6887, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.334729+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 28.6616, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.381418+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.7701, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:15.465923+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.1447, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.535267+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 29.1652, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.583698+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.0403, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:15.666018+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.1392, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.725010+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.9155, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.789965+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 18.3034, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:15.857530+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.1291, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.956435+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 48.2259, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:15.999938+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 28.1958, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:16.084485+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.6797, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.151063+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.6002, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.196002+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.2661, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:16.282458+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 40.945, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.351465+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.9991, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.398054+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 26.3199, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:16.477802+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 37.0199, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.543085+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 34.8822, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.594231+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.4826, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:16.665967+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.1156, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.752176+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.7224, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.793156+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 21.4136, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:16.884007+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.5158, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:16.975956+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 68.1785, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.009424+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 33.3083, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:17.069572+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 29.2924, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.201470+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.4377, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.217551+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.9873, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:17.257519+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.0837, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.336735+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 30.3657, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.381874+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.2274, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:17.459446+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.0183, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.524877+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.7924, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.616905+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 45.2623, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:17.658809+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.9343, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.725101+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.9855, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.783289+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.646, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:17.884064+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.2744, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:17.993294+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 85.1433, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.022386+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 28.9559, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:18.068029+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 28.1021, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.128904+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.6392, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.187553+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.8962, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:18.261264+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.5225, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.333670+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.5685, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.384043+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.3986, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:18.457595+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.1665, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.525568+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.1554, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.584926+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.2806, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:18.659402+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.9641, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.724180+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.7977, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.783727+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.0399, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:18.882861+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.0498, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.952152+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.6987, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:18.996345+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.6247, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:19.083594+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.1253, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.155397+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.919, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.197352+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.6075, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:19.288269+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.4933, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.351606+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.4262, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.394029+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.2984, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:19.488050+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.2652, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.550990+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.5226, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.594508+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.765, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:19.685689+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.1901, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.753387+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.9022, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.795048+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.3035, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:19.886194+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.3694, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.956042+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.7681, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:19.996696+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.9314, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:20.086490+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.6126, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.154740+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 46.5072, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.193913+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.1654, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:20.285036+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.2354, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.351738+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.2856, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.396448+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.6607, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:20.485483+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.1518, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.550585+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.0901, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.595498+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.7207, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:20.686632+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.8412, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.751970+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.0532, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.797203+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.3042, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:20.888093+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.2903, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.943042+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 35.573, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:20.994934+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.1971, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:21.150966+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 28.0712, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.234174+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 81.5061, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.251361+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.0734, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:21.288856+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 36.1923, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.325868+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.8555, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.403682+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 31.8945, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:21.494447+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 52.4409, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.552676+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.0952, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.595189+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.4625, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:21.686904+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.3894, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.751841+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.3493, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.797149+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.4289, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:21.884456+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.0073, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.956001+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 48.5362, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:21.993766+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.0331, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:22.083496+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.711, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.149440+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 41.2481, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.199547+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 27.811, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:22.281830+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 40.9616, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.353729+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 46.2837, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.396732+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.0099, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:22.482961+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 41.4241, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.572129+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 64.4301, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.597098+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.8216, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:22.661483+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.6404, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.753164+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.8451, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.798082+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 26.3376, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:22.885352+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.5, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.939197+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 31.638, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:22.997046+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.234, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:23.083483+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 41.9551, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.145395+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 37.9053, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.195868+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.1103, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:23.308162+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 67.0594, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.342925+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 32.8248, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.385095+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.4502, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:23.460354+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.9278, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.524134+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.751, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.585957+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.3249, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:23.658511+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.0325, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.743076+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 36.9751, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.780750+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.124, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:23.867431+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.6715, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.925065+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.9265, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:23.994156+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.4831, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:24.087954+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.1706, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.152514+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.3315, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.195701+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.9359, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:24.293923+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 53.0735, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.349261+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.3385, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.386984+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.2673, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:24.462143+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.2875, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.527980+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.7874, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.597279+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.4836, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:24.661928+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.3476, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.726824+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.4671, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.781391+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.7426, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:24.867052+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.6288, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.925669+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.2816, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:24.989905+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 18.2623, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:25.054998+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 16.025, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.153588+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 46.0734, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.196362+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.6277, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:25.285024+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.5121, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.357162+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 49.6517, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.393451+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 21.7262, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:25.481598+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 40.8473, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.552484+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.3063, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.597757+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 26.0456, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:25.684836+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.0028, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.750638+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.1411, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.793297+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 21.5666, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:25.884405+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.9061, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.951253+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.8119, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:25.997161+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.4295, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:26.116249+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 74.4099, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.263934+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.069, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.281328+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.2328, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:26.310261+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.4653, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.337987+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.3896, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.382542+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.8438, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:26.465889+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.0254, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.525948+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.8046, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.588815+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.1148, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:26.659073+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.3012, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.727518+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.3433, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.782314+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.6203, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:26.863741+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.2666, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.926560+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.1148, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:26.985389+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.6768, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:27.067004+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.8798, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.141337+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 35.1305, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.194483+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.777, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:27.281079+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 39.5495, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.376570+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 68.3769, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.407045+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 30.2282, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:27.486606+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.6253, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.552302+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.0747, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.611272+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 39.5309, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:27.672928+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 32.7417, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.758426+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 52.1664, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.785248+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.6199, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:27.861064+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.2992, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.928256+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.1785, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:27.988468+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 16.8396, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:28.057552+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.8445, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.130698+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.5982, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.181565+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.9359, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:28.261616+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.1805, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.324362+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.9695, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.387770+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 16.137, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:28.457455+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.0301, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.524971+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.8681, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.583847+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.2152, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:28.682541+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.8056, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.726454+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.3647, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.781742+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.114, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:28.857696+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.258, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.928308+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.9202, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:28.982937+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.3086, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:29.056955+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.5404, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.124766+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.6802, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.185586+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.9605, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:29.270715+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 30.9325, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.331470+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.3939, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.383606+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.9709, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:29.463600+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.899, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.524494+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.3936, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.589502+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.8695, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:29.659804+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.3734, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.728075+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.6818, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.781606+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.9782, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:29.866372+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.9472, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.924398+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.2997, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:29.982686+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.0637, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:30.062709+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.9823, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.127660+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.5878, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.181362+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.7397, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:30.260552+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.1077, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.337401+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 30.9878, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.391703+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 20.0711, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:30.457335+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.9022, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.531060+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.9576, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.581558+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.924, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:30.663866+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.1139, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.724483+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.3979, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.783365+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.7279, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:30.862272+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.5546, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.924168+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.0585, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:30.986135+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.5032, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:31.057179+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.7554, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.132171+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.807, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.182025+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.3912, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:31.262347+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.9097, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.324499+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.3852, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.389337+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.7015, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:31.481933+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.1925, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.547312+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 41.2283, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.597314+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.6826, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:31.682024+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.5664, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.745546+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 39.1311, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.797358+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.7328, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:31.876094+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 36.6806, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.945645+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 39.5772, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:31.992016+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 20.3844, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:32.065363+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.6181, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.124855+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.8016, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.182252+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.6159, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:32.264765+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.054, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.430616+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.0991, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.444491+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.7674, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:32.468954+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.6551, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.526435+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.0648, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.588800+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.1122, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:32.659185+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.6917, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.729132+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.9593, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.789389+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.7279, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:32.855894+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 16.6683, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.953094+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.5682, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:32.994398+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.6314, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:33.098327+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 57.2911, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.154732+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 46.4191, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.194550+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.7634, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:33.288332+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.4911, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.372526+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 64.1232, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.410594+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 37.8945, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:33.466546+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.1424, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.542153+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 33.4685, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.597601+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.9128, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:33.662053+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.5419, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.729783+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.3462, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.781560+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.2001, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:33.885032+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 44.1603, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:33.950925+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.7793, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.001186+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 29.4043, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:34.085894+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 45.0437, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.154831+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.2673, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.195324+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.5429, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:34.307471+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 65.4547, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.380907+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 71.4609, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.398717+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.657, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:34.467936+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 28.2527, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.527020+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.5612, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.590098+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 18.3938, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:34.659355+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.8686, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.726686+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.5861, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.786762+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.0489, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:34.872953+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 33.3804, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.970184+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 64.1754, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:34.992462+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 20.7515, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:35.083016+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.1549, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.168329+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 59.6101, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.213481+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 41.7677, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:35.268411+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 28.4955, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.333751+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.2332, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.395008+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 23.3785, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:35.465625+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.2305, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.524022+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.9482, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.590789+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 19.1572, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:35.658068+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.3901, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.725854+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.7688, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.788703+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.0731, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:35.863704+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.2612, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.927780+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.3949, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:35.981194+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.5676, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:36.059353+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.9645, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.131163+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.0984, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.181430+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.8033, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:36.259783+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.0406, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.324459+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.3774, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.396407+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 24.7752, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:36.457529+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.1037, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.526234+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.8122, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.587323+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.6902, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:36.657782+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.3968, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.733442+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.111, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.781763+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.1322, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:36.861353+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.9218, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.928527+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.421, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:36.983948+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.3222, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:37.057626+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.9042, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.128345+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.2685, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.186620+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.9982, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:37.258996+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.5726, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.326057+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.6088, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.386754+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.1294, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:37.465069+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.654, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.523804+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.6873, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.583963+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 12.3339, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:37.657577+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.8499, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.726558+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.4564, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.786881+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.2316, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:37.896290+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 55.3275, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.949160+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 41.8788, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:37.994403+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 22.7312, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:38.061216+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.8144, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.123449+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 17.1443, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.183525+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.8647, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:38.262379+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.0002, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.327395+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.3595, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.381271+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.6122, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:38.459192+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.5202, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.529142+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.1136, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.585350+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.6834, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:38.666933+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 27.2679, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.734900+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 28.4084, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.781207+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.5488, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:38.864019+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 24.6552, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.925678+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 19.6444, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:38.983601+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.9477, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:39.068240+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 28.3674, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.132895+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.6494, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.185340+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 13.675, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:39.259539+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.1729, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.329651+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.2817, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.390655+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 19.0036, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:39.457805+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.3718, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.528193+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.6914, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.589250+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 17.5787, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:39.662247+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.8614, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.727203+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.1916, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.781067+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 9.4153, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:39.865589+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.7359, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.926510+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 20.3123, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:39.986731+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 15.0617, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:40.058025+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.6054, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.258444+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.1492, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.275209+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 16.6179, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:40.306010+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 29.4683, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.329833+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.5458, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.385672+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.0153, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:40.470974+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 31.2871, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.529411+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.3538, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.581754+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.1003, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:40.673753+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 34.2576, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.729994+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.5305, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.783575+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.9238, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:40.860873+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.475, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.927323+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 21.2768, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:40.982203+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.5513, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:41.072139+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 32.2963, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.124876+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 18.7905, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.183345+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 11.6922, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:41.265108+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 25.4024, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.328141+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 22.0479, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.382301+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 10.6386, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:41.462986+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.5151, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.530012+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 23.6294, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.585984+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 14.3346, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:41.665901+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 26.4577, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.737548+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 31.4311, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.812096+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 40.4347, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:41.877704+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 37.9028, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:41.953294+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 47.2284, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:42.005502+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 33.8497, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:42.081451+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 42.014, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:42.145826+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 39.3753, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:42.205463+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 33.8017, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + }, + { + "ts": "2026-06-14T19:53:42.277622+00:00", + "query": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 38.1789, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:42.349490+00:00", + "query": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])", + "kind": "quantile", + "duration_ms": 43.3868, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1000 + }, + { + "ts": "2026-06-14T19:53:42.397455+00:00", + "query": "sum(google_cluster_2019_cpu_rate)", + "kind": "sum", + "duration_ms": 25.8021, + "status": "success", + "http_code": 200, + "data_source": "thanos_query", + "n_result_series": 1 + } +] \ No newline at end of file diff --git a/datasets_eval/latency/queries-latency-cold.json b/datasets_eval/latency/queries-latency-cold.json new file mode 100644 index 000000000..07add5a56 --- /dev/null +++ b/datasets_eval/latency/queries-latency-cold.json @@ -0,0 +1,5 @@ +[ + {"id": "cold-quantile-p99", "kind": "quantile", "metricsql": "quantile_over_time(0.99, google_cluster_2019_cpu_rate[300s])"}, + {"id": "cold-quantile-p50", "kind": "quantile", "metricsql": "quantile_over_time(0.50, google_cluster_2019_cpu_rate[300s])"}, + {"id": "cold-sum", "kind": "sum", "metricsql": "sum(google_cluster_2019_cpu_rate)"} +] diff --git a/datasets_eval/latency/stack-coldon.sh b/datasets_eval/latency/stack-coldon.sh new file mode 100755 index 000000000..30ae4e16e --- /dev/null +++ b/datasets_eval/latency/stack-coldon.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# stack-coldon.sh — cold-ON single-host ASAP stack for the Fig 7 +# COLD-FALLBACK latency arm. Derived from +# datasets_eval/multisketch/stack.sh (the full cold stack), with two +# eval-specific changes: +# +# 1. ALL docker commands run via `sudo docker` (the direct docker +# socket is permission-denied on this host; sudo docker works). +# 2. The data-plane mounts a COLD storage-routing table +# (backend-storage-routing-coldon.yaml) that routes +# `google_cluster_2019_cpu_rate` to `gorilla_object_store`, so its +# PromQL queries are answered by the ThanosQueryEngine +# (data_source=thanos_query) over the gorilla cold tier — that is +# what makes the COLD arm observable end-to-end. +# +# Components (all --network host on node0): +# minio : 9000 / 9001 +# thanos store-gateway : 10901/10902 +# gorilla-merger HTTP/gRPC : 10908 / 10907 +# thanos query : 10903 / 10905 +# data-plane OTLP ingest : 14317/14318, query :9091 (cold ON) +# control-plane : 8080 / 4320 / 4321 +# agent (bare fused asap_edge, cold ON) OTLP receiver :4317/4318 +# +# Usage: +# stack-coldon.sh up +# stack-coldon.sh down +# stack-coldon.sh ps +set -uo pipefail + +ROOT=/mydata/ASAPCollector +CFG=${ROOT}/deploy/mvp-multinode/configs +WORKDIR=/mydata/mvp-multinode +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +H=127.0.0.1 +ADD_HOSTS=( + --add-host=control-plane:${H} --add-host=data-plane:${H} + --add-host=minio:${H} --add-host=prometheus:${H} --add-host=victoriametrics:${H} + --add-host=thanos-query:${H} --add-host=thanos-store-gateway:${H} + --add-host=thanos-compact:${H} --add-host=gorilla-merger:${H} + --add-host=agent-a:${H} --add-host=serf-gw:${H} +) + +DR() { sudo docker run -d --restart no --network host "${ADD_HOSTS[@]}" "$@"; } +log(){ printf '[stack-coldon %s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; } + +down() { + sudo docker ps -a --format '{{.Names}}' | grep '^asap-' | xargs -r sudo docker rm -f >/dev/null 2>&1 + log "all asap-* containers removed" +} + +wipe_state() { + sudo rm -rf "${WORKDIR}"/data/gorilla-merger/* "${WORKDIR}"/data/sketch-persistence/* 2>/dev/null || true + mkdir -p "${WORKDIR}"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/data/gorilla-merger "${WORKDIR}"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/data/sketch-persistence "${WORKDIR}"/configs +} + +up() { + local workload=$1 agentcfg=$2 routing=$3 + down; wipe_state + + mkdir -p "${WORKDIR}"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/configs/asap "${WORKDIR}"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/configs/shared + cp "${CFG}/shared/thanos-objstore.yaml" "${WORKDIR}/configs/shared/" + cp "${CFG}/asap/backend-streaming.yaml" "${WORKDIR}/configs/asap/" + cp "${routing}" "${WORKDIR}/configs/asap/backend-storage-routing.yaml" + cp "${CFG}/asap/supervisor.yaml" "${WORKDIR}/configs/asap/" 2>/dev/null || true + cp "${workload}" "${WORKDIR}/configs/asap/eval-workload.yaml" + cp "${agentcfg}" "${WORKDIR}/configs/asap/eval-agent.yaml" + + log "minio up" + DR --name asap-minio -e MINIO_ROOT_USER=asap -e MINIO_ROOT_PASSWORD=asap-local-only \ + minio/minio:latest server /data --console-address :9001 >/dev/null + sleep 4 + sudo docker run --rm --network host "${ADD_HOSTS[@]}" --entrypoint=sh minio/mc:latest -c ' + mc alias set asap http://minio:9000 asap asap-local-only && + mc mb --ignore-existing asap/asap-gorilla && + mc mb --ignore-existing asap/asap-gorilla-tsdb && + mc mb --ignore-existing asap/raw' >/dev/null 2>&1 || log "minio bucket warn" + + log "thanos store-gateway up" + DR --name asap-thanos-store-gateway --user 0 \ + -v "${WORKDIR}/configs/shared/thanos-objstore.yaml:/etc/thanos/objstore.yaml:ro" \ + quay.io/thanos/thanos:v0.41.0 store \ + --objstore.config-file=/etc/thanos/objstore.yaml \ + --http-address=0.0.0.0:10902 --grpc-address=0.0.0.0:10901 \ + --data-dir=/tmp/thanos-store --sync-block-duration=30s >/dev/null + + log "gorilla-merger up" + # --user 0: the merger image is distroless `nonroot` (uid 65532) but the + # host bind-mount /data is owned by the invoking user; run as root so the + # merger can mkdir /data/pending (else: "mkdir /data/pending: permission + # denied"). Same workaround the thanos store-gateway uses above. + DR --name asap-gorilla-merger --user 0 \ + -v "${WORKDIR}/configs/shared/thanos-objstore.yaml:/etc/thanos/objstore.yaml:ro" \ + -v "${WORKDIR}/data/gorilla-merger:/data" \ + asap/gorilla-merger:dev \ + --http-address=0.0.0.0:10908 --grpc-address=0.0.0.0:10907 \ + --tsdb.path=/data --objstore.config-file=/etc/thanos/objstore.yaml \ + --external-labels=cluster=asap-mvp,merger=m1 >/dev/null + + log "thanos query up" + DR --name asap-thanos-query quay.io/thanos/thanos:v0.41.0 query \ + --http-address=0.0.0.0:10903 --grpc-address=0.0.0.0:10905 \ + --endpoint=thanos-store-gateway:10901 --endpoint=gorilla-merger:10907 \ + --query.replica-label=replica >/dev/null + + log "data-plane up (cold ON — gorilla_object_store routing, OTLP :14317, query :9091)" + DR --name asap-data-plane --cpus=8 \ + -e RUST_LOG="${DP_RUST_LOG:-info}" -e ASAP_SKETCH_FAMILY=ddsketch \ + -e ASAP_GORILLA_S3_ENDPOINT=http://minio:9000 -e ASAP_GORILLA_S3_BUCKET=asap-gorilla \ + -e ASAP_GORILLA_S3_REGION=us-east-1 -e ASAP_GORILLA_S3_ACCESS_KEY_ID=asap \ + -e ASAP_GORILLA_S3_SECRET_ACCESS_KEY=asap-local-only -e ASAP_GORILLA_S3_TENANT=default \ + -e ASAP_GORILLA_S3_USE_SSL=false \ + -e 'ASAP_GORILLA_S3_PREFIX_TEMPLATE={tenant}/{metric}/{YYYY}/{MM}/{DD}/{HH}/' \ + -e ASAP_BACKEND_STORAGE_ROUTING=/etc/asap/backend-storage-routing.yaml \ + -e ASAP_THANOS_QUERY_URL=http://thanos-query:10903 \ + -v "${WORKDIR}/configs/asap/backend-streaming.yaml:/etc/asap/streaming.yaml:ro" \ + -v "${WORKDIR}/configs/asap/backend-storage-routing.yaml:/etc/asap/backend-storage-routing.yaml:ro" \ + -v "${WORKDIR}/data/sketch-persistence:/data/sketch-persistence" \ + asap/data-plane:dev \ + --streaming-config=/etc/asap/streaming.yaml --query-port=9091 \ + --enable-otel-ingest --otel-grpc-port=14317 --otel-http-port=14318 >/dev/null + sleep 4 + + log "control-plane up (workload=$(basename "${workload}"), backend OTLP port 14317)" + DR --name asap-control-plane --cpus=2 \ + -e RUST_LOG="info,controller=debug,control_plane=debug" \ + -e USE_TYPED_STAGE_SPLIT=1 -e ASAP_EDGE_FUSED=1 \ + -e ASAP_EDGE_BACKEND_OTLP_PORT=14317 \ + -e CONTROLLER_ADDR=0.0.0.0:8080 -e CONTROLLER_OPAMP_ADDR=0.0.0.0:4320 \ + -e CONTROLLER_GRPC_ADDR=0.0.0.0:4321 \ + -e CONTROLLER_OPAMP_ENDPOINT=ws://control-plane:4320/v1/opamp \ + -e CONTROLLER_BACKEND_ENDPOINT=http://data-plane:9091/api/v1/streaming-config \ + -e CONTROLLER_WORKLOADS=/etc/asap/eval-workload.yaml \ + -v "${WORKDIR}/configs/asap/eval-workload.yaml:/etc/asap/eval-workload.yaml:ro" \ + asap/control-plane:dev >/dev/null + sleep 5 + + log "agent (bare, static fused asap_edge, cold ON) up — OTLP receiver :4317 for replay" + DR --name asap-agent-a --cpus=8 --hostname agent-a \ + -e AGENT_ID=agent-a \ + -v "${WORKDIR}/configs/asap/eval-agent.yaml:/etc/otel/config.yaml:ro" \ + asap/asap-otel:dev --config=/etc/otel/config.yaml >/dev/null + sleep 6 + log "stack up. containers:"; sudo docker ps --format ' {{.Names}}\t{{.Status}}' | grep asap- >&2 +} + +case "${1:-}" in + up) up "${2:?need workload}" "${3:?need agentcfg}" "${4:?need routing}" ;; + down) down ;; + ps) sudo docker ps --format '{{.Names}}\t{{.Status}}' | grep asap- ;; + *) echo "usage: stack-coldon.sh up | down | ps" >&2; exit 2 ;; +esac diff --git a/datasets_eval/multisketch/agent-raw-coldoff.yaml b/datasets_eval/multisketch/agent-raw-coldoff.yaml new file mode 100644 index 000000000..ca2eec07c --- /dev/null +++ b/datasets_eval/multisketch/agent-raw-coldoff.yaml @@ -0,0 +1,40 @@ +# RAW-forward baseline agent (no asap_edge) — for C1 sketch-vs-raw wire. +# Auto-generated per-family agent config (multisketch eval). See make_perfamily.py. +receivers: + otlp: + protocols: + grpc: {endpoint: 0.0.0.0:4317, max_recv_msg_size_mib: 4096} + http: {endpoint: 0.0.0.0:4318} +processors: + batch: + send_batch_size: 800 + send_batch_max_size: 1500 + memory_limiter: {check_interval: 1s, limit_mib: 8192, spike_limit_mib: 1024} + asap_edge: + shard_count: 12 + window_duration: 60s + drop_original: true + max_series: 200000 + delta_transmission: false + metrics: + - {metric: google_cluster_2019_cpu_rate, family: sum, tier: both} + - {metric: google_cluster_2019_memory_usage, family: sum, aggregate_by: [zone], tier: both} + - metric: google_cluster_2019_cpu_rate_q_ddsketch + family: "ddsketch" + tier: "warm" + delta_transmission: true + cold: {enabled: false} + control_channel: {enabled: false} +exporters: + otlp/backend: + endpoint: data-plane:14317 + tls: {insecure: true} + timeout: 120s + sending_queue: {enabled: true, num_consumers: 4, queue_size: 5000} +service: + pipelines: + metrics: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlp/backend]} + telemetry: + metrics: + level: detailed + readers: [{pull: {exporter: {prometheus: {host: 0.0.0.0, port: 8890}}}}] diff --git a/datasets_eval/multisketch/agent-raw-gzip.yaml b/datasets_eval/multisketch/agent-raw-gzip.yaml new file mode 100644 index 000000000..fb7d558ef --- /dev/null +++ b/datasets_eval/multisketch/agent-raw-gzip.yaml @@ -0,0 +1,42 @@ +# RAW-forward + gzip compression baseline (b0-gzip) for C1 encoding-factor. +# RAW-forward baseline agent (no asap_edge) — for C1 sketch-vs-raw wire. +# Auto-generated per-family agent config (multisketch eval). See make_perfamily.py. +receivers: + otlp: + protocols: + grpc: {endpoint: 0.0.0.0:4317, max_recv_msg_size_mib: 4096} + http: {endpoint: 0.0.0.0:4318} +processors: + batch: + send_batch_size: 800 + send_batch_max_size: 1500 + memory_limiter: {check_interval: 1s, limit_mib: 8192, spike_limit_mib: 1024} + asap_edge: + shard_count: 12 + window_duration: 60s + drop_original: true + max_series: 200000 + delta_transmission: false + metrics: + - {metric: google_cluster_2019_cpu_rate, family: sum, tier: both} + - {metric: google_cluster_2019_memory_usage, family: sum, aggregate_by: [zone], tier: both} + - metric: google_cluster_2019_cpu_rate_q_ddsketch + family: "ddsketch" + tier: "warm" + delta_transmission: true + cold: {enabled: false} + control_channel: {enabled: false} +exporters: + otlp/backend: + endpoint: data-plane:14317 + compression: gzip + tls: {insecure: true} + timeout: 120s + sending_queue: {enabled: true, num_consumers: 4, queue_size: 5000} +service: + pipelines: + metrics: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlp/backend]} + telemetry: + metrics: + level: detailed + readers: [{pull: {exporter: {prometheus: {host: 0.0.0.0, port: 8890}}}}] diff --git a/datasets_eval/multisketch/agent-raw-zstd.yaml b/datasets_eval/multisketch/agent-raw-zstd.yaml new file mode 100644 index 000000000..bd9813fc1 --- /dev/null +++ b/datasets_eval/multisketch/agent-raw-zstd.yaml @@ -0,0 +1,42 @@ +# RAW-forward + zstd compression baseline (b0-zstd) for C1 encoding-factor. +# RAW-forward baseline agent (no asap_edge) — for C1 sketch-vs-raw wire. +# Auto-generated per-family agent config (multisketch eval). See make_perfamily.py. +receivers: + otlp: + protocols: + grpc: {endpoint: 0.0.0.0:4317, max_recv_msg_size_mib: 4096} + http: {endpoint: 0.0.0.0:4318} +processors: + batch: + send_batch_size: 800 + send_batch_max_size: 1500 + memory_limiter: {check_interval: 1s, limit_mib: 8192, spike_limit_mib: 1024} + asap_edge: + shard_count: 12 + window_duration: 60s + drop_original: true + max_series: 200000 + delta_transmission: false + metrics: + - {metric: google_cluster_2019_cpu_rate, family: sum, tier: both} + - {metric: google_cluster_2019_memory_usage, family: sum, aggregate_by: [zone], tier: both} + - metric: google_cluster_2019_cpu_rate_q_ddsketch + family: "ddsketch" + tier: "warm" + delta_transmission: true + cold: {enabled: false} + control_channel: {enabled: false} +exporters: + otlp/backend: + endpoint: data-plane:14317 + compression: zstd + tls: {insecure: true} + timeout: 120s + sending_queue: {enabled: true, num_consumers: 4, queue_size: 5000} +service: + pipelines: + metrics: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlp/backend]} + telemetry: + metrics: + level: detailed + readers: [{pull: {exporter: {prometheus: {host: 0.0.0.0, port: 8890}}}}] diff --git a/datasets_eval/multisketch/baselines.py b/datasets_eval/multisketch/baselines.py new file mode 100644 index 000000000..7a29e24c6 --- /dev/null +++ b/datasets_eval/multisketch/baselines.py @@ -0,0 +1,97 @@ +# LIMITATION FOUND: --network host makes lo carry BOTH the replay→agent leg +# (constant raw ~35MB) AND the agent→data-plane leg, so lo cannot isolate the +# wire — all arms measured ~35MB. The clean compression measurement needs the +# CLUSTER (per-node NIC isolates agent→backend). Encoding factor was instead +# measured offline via gzip of the payload (12.3x). See evaluation-plan (c‴). +#!/usr/bin/env python3 +"""C1 encoding-factor baselines: ASAP-sketch vs raw-OTLP vs raw+gzip vs raw+zstd, +on the SAME real-gct family slice. + +The cold-off stack is `--network host`, so per-container RX isn't isolatable; +instead we read the loopback interface byte counter (`/proc/net/dev` lo) around +each replay. The replay (tens of MB) dominates the small control/query overhead, +so the delta is the actual on-the-wire bytes — and it reflects exporter +compression (gzip/zstd), which the serialized `:8890` metric does not. + +Decomposition the paper wants: + aggregation_factor = W_raw / W_sketch (sketching, no compression) + encoding_factor = W_raw / W_raw_gzip (compression alone) + net ASAP vs raw+gzip = W_raw_gzip / W_sketch (the honest baseline) +""" +import json, math, subprocess, sys, time +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +GCT = HERE.parent / "google_cluster" +STACK = str(HERE / "stack-coldoff.sh") +SLICE = "/tmp/perfam-ddsketch.jsonl" # set by c1_wire.slice_family before this runs + +ARMS = { + "raw": "agent-raw-coldoff.yaml", + "raw_gzip": "agent-raw-gzip.yaml", + "raw_zstd": "agent-raw-zstd.yaml", + "sketch": "agent-ddsketch-coldoff.yaml", +} + + +def lo_rx_bytes(): + for line in Path("/proc/net/dev").read_text().splitlines(): + if line.strip().startswith("lo:"): + return int(line.split(":")[1].split()[0]) + return 0 + + +def replay(jsonl): + subprocess.call([sys.executable, str(GCT / "run.py"), "replay", + "--jsonl", jsonl, "--endpoint", "127.0.0.1:4317", + "--pace-factor", "0", "--wall-clock-anchor"]) + + +def run_arm(agent, settle=35): + subprocess.run([STACK, "down"], cwd=str(ROOT), timeout=60) + subprocess.run([STACK, "up", str(HERE / "workloads" / "ddsketch.yaml"), + str(HERE / agent)], cwd=str(ROOT), check=True, timeout=180) + time.sleep(3) + b0 = lo_rx_bytes() + replay(SLICE) + time.sleep(settle) # let the export queue + sketch ship fully + wire = lo_rx_bytes() - b0 + subprocess.run([STACK, "down"], cwd=str(ROOT), timeout=60) + return wire + + +def ci95(xs): + xs = [x for x in xs if x] + if len(xs) < 2: + return (xs[0] if xs else float("nan")), 0.0 + m = sum(xs) / len(xs) + sd = (sum((x - m) ** 2 for x in xs) / (len(xs) - 1)) ** 0.5 + return m, 1.96 * sd / math.sqrt(len(xs)) + + +def main(): + trials = int(sys.argv[1]) if len(sys.argv) > 1 else 3 + data = {a: [] for a in ARMS} + for t in range(trials): + for arm, agent in ARMS.items(): + w = run_arm(agent) + data[arm].append(w) + print(f" trial {t+1} {arm:9} lo-wire {w/1e6:8.2f} MB", file=sys.stderr) + means = {a: ci95(v) for a, v in data.items()} + out = {a: dict(wire_MB=m / 1e6, ci95_MB=c / 1e6, n=len([x for x in data[a] if x])) + for a, (m, c) in means.items()} + # factors + wr, ws = means["raw"][0], means["sketch"][0] + wg = means["raw_gzip"][0] + out["_factors"] = dict( + aggregation_raw_over_sketch=wr / ws if ws else None, + encoding_raw_over_gzip=wr / wg if wg else None, + net_asap_vs_rawgzip=wg / ws if ws else None, + ) + (HERE / "results" / "baselines.json").write_text(json.dumps(out, indent=2) + "\n") + print(json.dumps(out, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/datasets_eval/multisketch/c1_wire.py b/datasets_eval/multisketch/c1_wire.py new file mode 100644 index 000000000..79693d4be --- /dev/null +++ b/datasets_eval/multisketch/c1_wire.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Clean C1 bandwidth (sketch vs raw) on real gct, with trials + 95% CI. + +Fixes the two harness gaps that polluted the per-family wire: + 1. TRUE per-family data slice — keep only {cpu_rate (Sum anchor for the + ship-wait), memory_usage (Sum), }; drop the other + 6 sketch aliases that were forwarded raw and dominated the 57 MB. + 2. A RAW-forward baseline arm (agent-raw-coldoff.yaml, no asap_edge) replays + the SAME slice so the comparison is apples-to-apples. + +Per trial: sketch arm (via run_perfamily) → W_sketch + accuracy; raw arm → +W_raw. Reduction = W_raw / W_sketch. Aggregated mean ± 95% CI over N trials. +""" +import json, math, subprocess, sys, time, urllib.request +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +GCT = HERE.parent / "google_cluster" +STACK = str(HERE / "stack-coldoff.sh") +SOURCE = "/tmp/perfam-source.jsonl" +AGENT_METRICS = "http://127.0.0.1:8890/metrics" + +# family → its sketch metric; the slice always also keeps the two Sum anchors. +FAM_METRIC = { + "ddsketch": "google_cluster_2019_cpu_rate_q_ddsketch", + "kll": "google_cluster_2019_cpu_rate_q_kll", + "hll": "google_cluster_2019_cpu_rate_card_hll", +} +ANCHORS = {"google_cluster_2019_cpu_rate", "google_cluster_2019_memory_usage"} + + +def slice_family(fam): + keep = ANCHORS | {FAM_METRIC[fam]} + dst = f"/tmp/perfam-{fam}.jsonl" + n = 0 + with open(SOURCE) as f, open(dst, "w") as o: + for line in f: + try: + m = json.loads(line) + except Exception: + continue + if m.get("metric") in keep: + o.write(line) + n += 1 + return dst, n + + +def agent_wire(): + """(bytes, points) shipped agent→backend, from the agent's otel telemetry.""" + wire = pts = None + try: + text = urllib.request.urlopen(AGENT_METRICS, timeout=10).read().decode() + for line in text.splitlines(): + if "otlp/backend" not in line: + continue + if line.startswith("otelcol_exporter_sent_metric_points_total"): + pts = float(line.rsplit(" ", 1)[1]) + elif line.startswith("otelcol_exporter_queue_batch_send_size_bytes_sum"): + wire = float(line.rsplit(" ", 1)[1]) + except Exception: + pass + return wire, pts + + +def run_sketch(fam): + """Sketch arm via run_perfamily; returns (W_sketch_bytes, accuracy_dict).""" + subprocess.run([sys.executable, str(HERE / "run_perfamily.py"), + "--arms", fam, "--window", "120s"], + cwd=str(HERE), check=False, timeout=600) + rec = json.loads((HERE / "results" / "perfamily-all.json").read_text()).get(fam, {}) + return rec.get("agent_wire_bytes_to_backend"), rec.get("score", {}) + + +def run_raw(fam, sliced): + """Raw-forward arm: replay the same slice through a no-asap_edge agent.""" + subprocess.run([STACK, "down"], cwd=str(ROOT), timeout=60) + subprocess.run([STACK, "up", str(HERE / "workloads" / f"{fam}.yaml"), + str(HERE / "agent-raw-coldoff.yaml")], + cwd=str(ROOT), check=True, timeout=180) + subprocess.call([sys.executable, str(GCT / "run.py"), "replay", + "--jsonl", sliced, "--endpoint", "127.0.0.1:4317", + "--pace-factor", "0", "--wall-clock-anchor"]) + time.sleep(20) # let the export queue drain + wire, _ = agent_wire() + subprocess.run([STACK, "down"], cwd=str(ROOT), timeout=60) + return wire + + +def ci95(xs): + xs = [x for x in xs if x is not None] + if len(xs) < 2: + return (xs[0] if xs else float("nan")), 0.0 + m = sum(xs) / len(xs) + sd = (sum((x - m) ** 2 for x in xs) / (len(xs) - 1)) ** 0.5 + return m, 1.96 * sd / math.sqrt(len(xs)) + + +def main(): + fams = (sys.argv[1].split(",") if len(sys.argv) > 1 else ["ddsketch", "hll"]) + trials = int(sys.argv[2]) if len(sys.argv) > 2 else 3 + out = {} + for fam in fams: + sliced, n = slice_family(fam) + print(f"\n##### {fam}: sliced {n} pts → {sliced}", file=sys.stderr) + reds, ws, wr, accs = [], [], [], [] + for t in range(trials): + print(f" [{fam}] trial {t+1}/{trials} sketch arm...", file=sys.stderr) + w_s, acc = run_sketch(fam) + print(f" [{fam}] trial {t+1}/{trials} raw arm...", file=sys.stderr) + w_r = run_raw(fam, sliced) + if w_s and w_r and w_s > 0: + reds.append(w_r / w_s); ws.append(w_s); wr.append(w_r); accs.append(acc) + print(f" W_sketch={w_s} W_raw={w_r} reduction={(w_r/w_s if w_s else 0):.1f}x", + file=sys.stderr) + rm, rci = ci95(reds) + out[fam] = dict(trials=len(reds), reduction_mean=rm, reduction_ci95=rci, + W_sketch_mean=ci95(ws)[0], W_raw_mean=ci95(wr)[0], + accuracy_last=accs[-1] if accs else None) + print(f"##### {fam}: reduction {rm:.1f}× ±{rci:.1f} (n={len(reds)})", file=sys.stderr) + (HERE / "results" / "c1-wire.json").write_text(json.dumps(out, indent=2, default=str) + "\n") + print(json.dumps(out, indent=2, default=str)) + + +if __name__ == "__main__": + main() diff --git a/datasets_eval/multisketch/fig7_latency.sh b/datasets_eval/multisketch/fig7_latency.sh new file mode 100644 index 000000000..c5a503f22 --- /dev/null +++ b/datasets_eval/multisketch/fig7_latency.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# fig7_latency.sh — Fig 7 cold-OFF warm-tier query latency CDF. +# +# Brings up the cold-OFF (warm-only) all-families stack on node0, replays the +# aliased gct trace to populate the DDSketch warm window, then replays warm +# latency queries (queries-latency-warm.json) against the warm tier :9091 and +# reports p50/p99. cold OFF => no Thanos archive failover, so range quantiles +# resolve warm (the design's ~20ms target) unlike the cold-ON multinode sweep. +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT=/mydata/ASAPCollector +GCT="${ROOT}/datasets_eval/google_cluster" +TRACE="${TRACE:-/tmp/perfam-ddsketch.jsonl}" # aliased trace (contains _q_ddsketch) +QUERIES="${ROOT}/deploy/mvp-singlenode/scripts/queries-latency-warm.json" +OUT="${OUT:-/mydata/eval/results/fig7}"; mkdir -p "${OUT}" +log(){ printf '[%s] [fig7] %s\n' "$(date +%H:%M:%S)" "$*"; } + +bash "${HERE}/stack-coldoff.sh" down >/dev/null 2>&1 || true +log "stack up (all-families cold-OFF)" +bash "${HERE}/stack-coldoff.sh" up "${HERE}/workloads/all-families.yaml" \ + "${HERE}/agent-allfamilies-coldoff.yaml" >"${OUT}/stack.log" 2>&1 +sleep 5 + +log "replay aliased trace (wall-clock anchored) to populate warm DDSketch" +python3 "${GCT}/run.py" replay --jsonl "${TRACE}" --endpoint 127.0.0.1:4317 \ + --pace-factor 0 --wall-clock-anchor >"${OUT}/replay.log" 2>&1 + +# wait until the warm quantile resolves (sketch sealed+queryable) +log "waiting for warm window to seal..." +for i in $(seq 1 30); do + r=$(curl -s -G "http://127.0.0.1:9091/api/v1/query" \ + --data-urlencode "query=quantile_over_time(0.50, google_cluster_2019_cpu_rate_q_ddsketch[300s])" 2>/dev/null) + echo "$r" | grep -q '"result":\[{' && { log "warm queryable"; break; } + sleep 4 +done + +log "replay latency queries (60s @ 15 qps)" +python3 "${ROOT}/deploy/mvp-singlenode/scripts/metricsql_replay.py" \ + --target http://127.0.0.1:9091 --queries "${QUERIES}" \ + --qps 15 --duration 60 --no-plan-poll --out "${OUT}/fig7_latency.jsonl" \ + >"${OUT}/replay_lat.log" 2>&1 || true + +log "per-query latency stats" +python3 - "${OUT}/fig7_latency.jsonl" <<'PY' +import json, sys +from collections import defaultdict +by=defaultdict(list); allv=[] +for ln in open(sys.argv[1]): + try: + d=json.loads(ln); v=d.get('duration_ms') or d.get('latency_ms') + if v is None: continue + k=d.get('kind') or d.get('id') or 'all'; by[k].append(float(v)); allv.append(float(v)) + except: pass +def stat(v): + v=sorted(v); n=len(v); return (n, v[n//2], v[min(n-1,int(n*0.99))]) if n else (0,0,0) +for k in list(by)+['all']: + v=allv if k=='all' else by[k]; n,p50,p99=stat(v) + print(f"{k}: n={n} p50={p50:.2f} p99={p99:.2f} ms") +PY +bash "${HERE}/stack-coldoff.sh" down >/dev/null 2>&1 || true +log "done -> ${OUT}/fig7_latency.jsonl" diff --git a/datasets_eval/multisketch/fig8_placement.sh b/datasets_eval/multisketch/fig8_placement.sh new file mode 100644 index 000000000..330fc2957 --- /dev/null +++ b/datasets_eval/multisketch/fig8_placement.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# fig8_placement.sh — Fig 8 cross-layer placement: same DDSketch agg_type computed +# at the SDK vs the agent, measuring where the CPU/RSS lands across the +# producer / agent / backend layers. Single cold-OFF stack on node0; only the +# producer's -agg changes between arms: +# agent placement : producer emits raw-buffer -> the asap_edge agent sketches +# sdk placement : producer emits AggregationDDSketch -> agent forwards +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT=/mydata/ASAPCollector +OUT="${OUT:-/mydata/eval/results/fig8}"; mkdir -p "${OUT}" +CSV="${OUT}/fig8.csv"; echo "placement,layer,container,cpu_perc,rss_mib" > "${CSV}" +log(){ printf '[%s] [fig8] %s\n' "$(date +%H:%M:%S)" "$*"; } + +bash "${HERE}/stack-coldoff.sh" down >/dev/null 2>&1 || true +log "stack up (ddsketch cold-OFF)" +bash "${HERE}/stack-coldoff.sh" up "${HERE}/workloads/ddsketch.yaml" \ + "${HERE}/agent-ddsketch-coldoff.yaml" >"${OUT}/stack.log" 2>&1 +sleep 6 + +# sample mean CPU/RSS of a container over ~24s (6 snapshots) +sample(){ # sample CONTAINER + python3 - "$1" <<'PY' +import subprocess,sys,re,time +c=sys.argv[1]; cpu=[]; mem=[] +for _ in range(6): + try: + o=subprocess.run(["docker","stats","--no-stream","--format","{{.CPUPerc}}|{{.MemUsage}}",c], + capture_output=True,text=True,timeout=8).stdout.strip() + if "|" in o: + cp,mm=o.split("|"); cpu.append(float(cp.strip("% "))) + m=re.search(r'([\d.]+)([KMG]i?B)',mm); + if m: mem.append(float(m.group(1))*{"KiB":1/1024,"MiB":1,"GiB":1024,"KB":1/1024,"MB":1,"GB":1024}.get(m.group(2),1)) + except: pass + time.sleep(4) +print(f"{(sum(cpu)/len(cpu) if cpu else 0):.1f},{(sum(mem)/len(mem) if mem else 0):.0f}") +PY +} + +run_arm(){ # run_arm PLACEMENT AGG + local placement=$1 agg=$2 + docker rm -f asap-prod-f8 >/dev/null 2>&1 || true + log "${placement}: producer -agg=${agg}" + docker run -d --network host --name asap-prod-f8 asap/otel-app:dev \ + -target=127.0.0.1:4317 -producer-id=f8 -metric=http_requests_total \ + -cardinality=500 -freq-hz=100 -sdk-window=1s -agg="${agg}" \ + -five-sketch=false -freshness-probes=false >/dev/null + sleep 35 # warm + steady state + for pair in "producer:asap-prod-f8" "agent:asap-agent-a" "backend:asap-data-plane"; do + layer=${pair%%:*}; c=${pair#*:} + stat=$(sample "$c") + echo "${placement},${layer},${c},${stat}" >> "${CSV}" + log " ${layer} (${c}): cpu%,rss=${stat}" + done + docker rm -f asap-prod-f8 >/dev/null 2>&1 || true + sleep 3 +} + +run_arm agent raw-buffer +run_arm sdk ddsketch + +log "done -> ${CSV}"; column -t -s, "${CSV}" +bash "${HERE}/stack-coldoff.sh" down >/dev/null 2>&1 || true diff --git a/datasets_eval/multisketch/run_perfamily.py b/datasets_eval/multisketch/run_perfamily.py index 61f627cfa..2163c77c3 100644 --- a/datasets_eval/multisketch/run_perfamily.py +++ b/datasets_eval/multisketch/run_perfamily.py @@ -26,7 +26,7 @@ results/perfamily-all.json. """ from __future__ import annotations -import argparse, json, math, subprocess, sys, time, urllib.parse, urllib.request +import argparse, json, math, os, subprocess, sys, time, urllib.parse, urllib.request from collections import defaultdict from pathlib import Path @@ -36,8 +36,13 @@ sys.path.insert(0, str(GCT / "e2e")) import gt_eval # noqa: E402 -BASE = "http://127.0.0.1:9091" -AGENT_METRICS = "http://127.0.0.1:8890/metrics" +# Endpoints are env-overridable so the SAME accuracy scorers run against a +# remote MULTINODE backend (e2e_metrics.sh sets E2E_BACKEND=node2:9091 etc.) +# instead of the single-host stack. +BASE = os.environ.get("E2E_BACKEND", "http://127.0.0.1:9091") +AGENT_METRICS = os.environ.get("E2E_AGENT_METRICS", "http://127.0.0.1:8890/metrics") +REPLAY_ENDPOINT = os.environ.get("E2E_REPLAY_ENDPOINT", "127.0.0.1:4317") +EXTERNAL_STACK = os.environ.get("E2E_EXTERNAL_STACK") == "1" STACK = str(HERE / "stack-coldoff.sh") @@ -242,17 +247,21 @@ def score_cardinality(metric, rows, t, key_label): def stack_up(workload, agent): + if EXTERNAL_STACK: # multinode: the stack is already up (e2e_metrics.sh) + return subprocess.run([STACK, "up", str(HERE / workload), str(HERE / agent)], cwd=str(ROOT), check=True, timeout=180) def stack_down(): + if EXTERNAL_STACK: + return subprocess.run([STACK, "down"], cwd=str(ROOT), timeout=60) def replay(jsonl): subprocess.call([sys.executable, str(GCT / "run.py"), "replay", - "--jsonl", jsonl, "--endpoint", "127.0.0.1:4317", + "--jsonl", jsonl, "--endpoint", REPLAY_ENDPOINT, "--pace-factor", "0", "--wall-clock-anchor"]) diff --git a/datasets_eval/soak/analyze.py b/datasets_eval/soak/analyze.py new file mode 100644 index 000000000..fd1240180 --- /dev/null +++ b/datasets_eval/soak/analyze.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +"""analyze.py — reduce soak samples to Fig-6 stats + RSS-over-time PNG. + +Inputs: + --b0 samples_b0.jsonl (measurement (a) raw-forward arm /proc samples) + --b3 samples_b3_soak.jsonl (measurement (a)+(b) sketch arm /proc samples) + --memdiag dp_memdiag_b3.log (data_plane [MEMORY_DIAG] SketchStore lines) + +Outputs: + - prints the arm CPU/RSS table + slope verdicts (stdout) + - writes rss_over_time.png + - writes summary.json +""" +from __future__ import annotations + +import argparse +import json +import re +import sys + +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +MEMDIAG_RE = re.compile( + r"SketchStore: (\d+) instance.* (\d+) sid\(s\) with state, " + r"payload=([\d.]+) KB.*process RSS=([\d.]+) MB") + + +def load_samples(path): + edge_t, edge_rss, edge_cpu = [], [], [] + dp_t, dp_rss, dp_cpu = [], [], [] + with open(path) as f: + for line in f: + line = line.strip() + if not line: + continue + r = json.loads(line) + t = r["t_rel"] + e = r.get("edge", {}) + d = r.get("dp", {}) + if e.get("alive") and e.get("rss_mb") is not None: + edge_t.append(t); edge_rss.append(e["rss_mb"]) + if e.get("cpu_pct") is not None: + edge_cpu.append(e["cpu_pct"]) + if d.get("alive") and d.get("rss_mb") is not None: + dp_t.append(t); dp_rss.append(d["rss_mb"]) + if d.get("cpu_pct") is not None: + dp_cpu.append(d["cpu_pct"]) + return {"edge_t": edge_t, "edge_rss": edge_rss, "edge_cpu": edge_cpu, + "dp_t": dp_t, "dp_rss": dp_rss, "dp_cpu": dp_cpu} + + +def load_memdiag(path): + t, sids, payload_kb, rss_mb = [], [], [], [] + with open(path) as f: + for line in f: + m = MEMDIAG_RE.search(line) + if not m: + continue + sids.append(int(m.group(2))) + payload_kb.append(float(m.group(3))) + rss_mb.append(float(m.group(4))) + # synthesize a 30s-cadence time axis (MEMORY_DIAG logs every 30s) + t = [30.0 * i for i in range(len(sids))] + return {"t": t, "sids": sids, "payload_kb": payload_kb, "rss_mb": rss_mb} + + +def stats(cpu, rss, t): + if not cpu: + cpu = [0.0] + return { + "n": len(rss), + "mean_cpu_pct": round(float(np.mean(cpu)), 2), + "p99_cpu_pct": round(float(np.percentile(cpu, 99)), 2), + "steady_rss_mb": round(float(np.median(rss[len(rss)//2:])), 1) if rss else None, + "rss_min_mb": round(min(rss), 1) if rss else None, + "rss_max_mb": round(max(rss), 1) if rss else None, + } + + +def slope_mb_per_h(t, rss, tail_frac=0.5): + """Linear fit of RSS(MB) vs t(s) over the tail (post warm-up); MB/hour.""" + if len(t) < 4: + return None + i0 = int(len(t) * (1 - tail_frac)) + tt = np.array(t[i0:]); rr = np.array(rss[i0:]) + if tt.max() - tt.min() < 1: + return None + a, b = np.polyfit(tt, rr, 1) # a = MB/s + return round(a * 3600.0, 2) + + +def verdict(slope, span_h): + if slope is None: + return "indeterminate (too few samples)" + proj_24h = slope * 24 + if abs(slope) < 5: + return f"BOUNDED (slope {slope:+.2f} MB/h ~= 0; 24h extrap {proj_24h:+.1f} MB)" + if slope > 0: + return f"CLIMBING (slope {slope:+.2f} MB/h; 24h extrap {proj_24h:+.1f} MB)" + return f"DECLINING (slope {slope:+.2f} MB/h)" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--b0") + ap.add_argument("--b3", required=True) + ap.add_argument("--memdiag") + ap.add_argument("--png", default="rss_over_time.png") + ap.add_argument("--out", default="summary.json") + args = ap.parse_args() + + b3 = load_samples(args.b3) + summary = {"arms": {}} + + # measurement (a): first 180s window of b3 = the steady CPU/RSS comparator + def window(d, key_t, key_rss, key_cpu, tmax): + ts = d[key_t] + idx = [i for i, t in enumerate(ts) if t <= tmax] + return ([d[key_cpu][i] for i in idx if i < len(d[key_cpu])], + [d[key_rss][i] for i in idx], [ts[i] for i in idx]) + + if args.b0: + b0 = load_samples(args.b0) + c, r, t = window(b0, "edge_t", "edge_rss", "edge_cpu", 1e9) + summary["arms"]["b0_raw_forward_edge"] = stats(c, r, t) + c, r, t = window(b0, "dp_t", "dp_rss", "dp_cpu", 1e9) + summary["arms"]["b0_data_plane"] = stats(c, r, t) + + # b3 measurement (a): use first 180s for the comparable steady CPU/RSS + c, r, t = window(b3, "edge_t", "edge_rss", "edge_cpu", 180) + summary["arms"]["b3_sketch_edge_first180s"] = stats(c, r, t) + c, r, t = window(b3, "dp_t", "dp_rss", "dp_cpu", 180) + summary["arms"]["b3_data_plane_first180s"] = stats(c, r, t) + + # full-soak stats + summary["arms"]["b3_sketch_edge_fullsoak"] = stats( + b3["edge_cpu"], b3["edge_rss"], b3["edge_t"]) + summary["arms"]["b3_data_plane_fullsoak"] = stats( + b3["dp_cpu"], b3["dp_rss"], b3["dp_t"]) + + # leak slopes (tail half, after warm-up) + span_h = (b3["edge_t"][-1] - b3["edge_t"][0]) / 3600.0 if b3["edge_t"] else 0 + edge_slope = slope_mb_per_h(b3["edge_t"], b3["edge_rss"]) + dp_slope = slope_mb_per_h(b3["dp_t"], b3["dp_rss"]) + summary["soak"] = { + "duration_s": round(b3["edge_t"][-1], 1) if b3["edge_t"] else 0, + "duration_h": round(span_h, 3), + "edge_rss_slope_mb_per_h": edge_slope, + "edge_verdict": verdict(edge_slope, span_h), + "dp_rss_slope_mb_per_h": dp_slope, + "dp_verdict": verdict(dp_slope, span_h), + } + + md = None + if args.memdiag: + md = load_memdiag(args.memdiag) + if md["sids"]: + md_slope = slope_mb_per_h(md["t"], md["rss_mb"]) + summary["data_plane_memdiag"] = { + "n": len(md["sids"]), + "sids_min": min(md["sids"]), "sids_max": max(md["sids"]), + "sids_final": md["sids"][-1], + "payload_kb_final": md["payload_kb"][-1], + "rss_mb_min": min(md["rss_mb"]), "rss_mb_max": max(md["rss_mb"]), + "rss_mb_final": md["rss_mb"][-1], + "rss_slope_mb_per_h": md_slope, + "rss_verdict": verdict(md_slope, span_h), + } + + # ---- plot ---- + fig, axes = plt.subplots(2, 1, figsize=(10, 9), sharex=False) + ax = axes[0] + ax.plot(b3["edge_t"], b3["edge_rss"], label="b3 sketch edge (asap-otel) RSS", + color="C0", lw=1.4) + ax.plot(b3["dp_t"], b3["dp_rss"], label="b3 data_plane RSS (/proc)", + color="C1", lw=1.4) + if args.b0: + ax.plot(b0["edge_t"], b0["edge_rss"], + label="b0 raw-forward edge RSS", color="C2", lw=1.0, ls="--") + ax.set_ylabel("process RSS (MB)") + ax.set_xlabel("soak time (s)") + ax.set_title("Fig 6 — edge / data_plane RSS over time " + f"(b3 soak {span_h:.2f} h @ 5000 pts/s, single-node loopback)") + ax.grid(alpha=0.3); ax.legend(loc="best", fontsize=8) + txt = (f"edge slope {edge_slope:+.2f} MB/h\n" + f"dp(/proc) slope {dp_slope:+.2f} MB/h") + ax.text(0.02, 0.97, txt, transform=ax.transAxes, va="top", fontsize=8, + bbox=dict(boxstyle="round", fc="white", alpha=0.7)) + + ax2 = axes[1] + if md and md["sids"]: + ax2.plot(md["t"], md["rss_mb"], color="C1", lw=1.4, + label="data_plane RSS (MEMORY_DIAG)") + ax2b = ax2.twinx() + ax2b.plot(md["t"], md["sids"], color="C3", lw=1.2, ls=":", + label="SketchStore sids") + ax2b.set_ylabel("SketchStore sids", color="C3") + ax2.set_ylabel("data_plane RSS (MB)", color="C1") + ax2.set_xlabel("soak time (s)") + ax2.set_title("data_plane SketchStore sid count + RSS " + "(stale-sid retention check)") + ax2.grid(alpha=0.3) + ax2.legend(loc="upper left", fontsize=8) + ax2b.legend(loc="lower right", fontsize=8) + fig.tight_layout() + fig.savefig(args.png, dpi=110) + + with open(args.out, "w") as f: + json.dump(summary, f, indent=2) + print(json.dumps(summary, indent=2)) + print(f"\nwrote {args.png} and {args.out}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/datasets_eval/soak/asap-otel-agent-gct-b0.yaml b/datasets_eval/soak/asap-otel-agent-gct-b0.yaml new file mode 100644 index 000000000..efe50ea0b --- /dev/null +++ b/datasets_eval/soak/asap-otel-agent-gct-b0.yaml @@ -0,0 +1,57 @@ +# Agent config — RAW-FORWARD edge, GCT-trace b0 arm (edge soak). +# +# The "raw-forward (b0)" arm: a plain OTel collector pipeline with NO +# asap_edge processor — it just batches and forwards the raw OTLP stream to +# the same data_plane OTLP ingest the b3 sketch arm ships to. This isolates +# the edge process cost of sketch aggregation (b3) vs pure passthrough (b0): +# same image, same receiver, same backend endpoint, same constant load; the +# ONLY difference is whether asap_edge is in the pipeline. +# +# Mirrors deploy/mvp-multinode/configs/b0/asap-otel-agent-b0-otlp-none.yaml +# (the matched-NONE raw baseline) but (1) drops the opamp extension (bare +# agent, like stack-coldoff runs it) and (2) points the OTLP exporter at the +# cold-OFF data_plane (data-plane:14317) instead of VictoriaMetrics, so both +# arms share an identical downstream. + +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + max_recv_msg_size_mib: 4096 + http: + endpoint: 0.0.0.0:4318 + +processors: + memory_limiter: + check_interval: 1s + limit_mib: 1280 + spike_limit_mib: 256 + batch: + timeout: 1s + send_batch_size: 1024 + +exporters: + otlp/backend: + endpoint: data-plane:14317 + compression: none + tls: + insecure: true + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [memory_limiter, batch] + exporters: [otlp/backend] + telemetry: + logs: + level: info + metrics: + level: detailed + readers: + - pull: + exporter: + prometheus: + host: 0.0.0.0 + port: 8890 diff --git a/datasets_eval/soak/asap-otel-agent-gct-b3.yaml b/datasets_eval/soak/asap-otel-agent-gct-b3.yaml new file mode 100644 index 000000000..e4cb32dd1 --- /dev/null +++ b/datasets_eval/soak/asap-otel-agent-gct-b3.yaml @@ -0,0 +1,76 @@ +# Agent config — FUSED asap_edge edge, GCT-trace b3 sketch arm (edge soak). +# +# This is the "sketch (b3): DDSketch + Sum warm path (the normal edge)" arm +# for the §6 Fig-6 edge CPU/RSS + leak-slope soak. It is a STATIC asap_edge +# config (bare agent, no OpAMP supervisor) that maps the two metrics the +# Google-2019 mapper emits onto the two warm families the task names: +# +# google_cluster_2019_cpu_rate -> DDSketch (per-series quantile warm) +# google_cluster_2019_memory_usage -> Sum, aggregate_by [zone] (warm Sum) +# +# Differences vs deploy/.../asap-otel-agent-asapedge.yaml (the MVP-demo +# static config): (1) metric map targets the GCT metrics, not the synthetic +# http_requests_total family; (2) cold.enabled=false (cold-OFF stack has no +# gorilla-merger); (3) backend endpoint = data-plane:14317 (the cold-OFF +# data_plane OTLP ingest port, matching ASAP_EDGE_BACKEND_OTLP_PORT), not +# the controller-emit default :4317; (4) window_duration=30s for faster warm +# turnover during the soak. All keys map 1:1 to config.go. + +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + max_recv_msg_size_mib: 4096 + http: + endpoint: 0.0.0.0:4318 + +processors: + memory_limiter: + check_interval: 1s + limit_mib: 1280 + spike_limit_mib: 256 + + asap_edge: + shard_count: 12 + window_duration: 30s + drop_original: true + max_series: 100000 + delta_transmission: false + metrics: + - metric: google_cluster_2019_cpu_rate + family: ddsketch + tier: warm + relative_accuracy: 0.01 + - metric: google_cluster_2019_memory_usage + family: sum + aggregate_by: [zone] + tier: warm + cold: + enabled: false + control_channel: + enabled: false + +exporters: + otlp/backend: + endpoint: data-plane:14317 + tls: + insecure: true + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [memory_limiter, asap_edge] + exporters: [otlp/backend] + telemetry: + logs: + level: info + metrics: + level: detailed + readers: + - pull: + exporter: + prometheus: + host: 0.0.0.0 + port: 8890 diff --git a/datasets_eval/soak/dp_memdiag_b3.log b/datasets_eval/soak/dp_memdiag_b3.log new file mode 100644 index 000000000..3b854d2de --- /dev/null +++ b/datasets_eval/soak/dp_memdiag_b3.log @@ -0,0 +1,390 @@ +2026-06-12T21:03:23.045721Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 512 instance(s), 512 sid(s) with state, payload=257.29 KB (evictable, flusher gauge), registry+intern≈1.16 MB (resident, not flushable), process RSS=17.8 MB +2026-06-12T21:03:23.045771Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:03:23.045783Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:03:23.045790Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:03:23.045797Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:03:23.045804Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:03:53.046386Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=835.98 KB (evictable, flusher gauge), registry+intern≈2.59 MB (resident, not flushable), process RSS=23.0 MB +2026-06-12T21:03:53.046438Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:03:53.046448Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:03:53.046455Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:03:53.046462Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:03:53.046469Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:04:23.045660Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=1418.32 KB (evictable, flusher gauge), registry+intern≈3.16 MB (resident, not flushable), process RSS=25.1 MB +2026-06-12T21:04:23.045706Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:04:23.045716Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:04:23.045723Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:04:23.045730Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:04:23.045737Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:04:53.045591Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=2003.10 KB (evictable, flusher gauge), registry+intern≈3.73 MB (resident, not flushable), process RSS=26.4 MB +2026-06-12T21:04:53.045637Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:04:53.045657Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:04:53.045665Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:04:53.045672Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:04:53.045679Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:05:23.045758Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=2589.02 KB (evictable, flusher gauge), registry+intern≈4.31 MB (resident, not flushable), process RSS=26.6 MB +2026-06-12T21:05:23.045807Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:05:23.045817Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:05:23.045824Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:05:23.045830Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:05:23.045838Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:05:53.046495Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=3171.45 KB (evictable, flusher gauge), registry+intern≈4.87 MB (resident, not flushable), process RSS=27.1 MB +2026-06-12T21:05:53.046541Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:05:53.046551Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:05:53.046558Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:05:53.046565Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:05:53.046572Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:06:23.045758Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=3752.02 KB (evictable, flusher gauge), registry+intern≈5.44 MB (resident, not flushable), process RSS=27.6 MB +2026-06-12T21:06:23.045802Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:06:23.045812Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:06:23.045819Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:06:23.045826Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:06:23.045833Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:06:53.046193Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=4336.81 KB (evictable, flusher gauge), registry+intern≈6.01 MB (resident, not flushable), process RSS=28.4 MB +2026-06-12T21:06:53.046237Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:06:53.046246Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:06:53.046254Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:06:53.046261Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:06:53.046268Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:07:23.045660Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=4922.73 KB (evictable, flusher gauge), registry+intern≈6.59 MB (resident, not flushable), process RSS=29.7 MB +2026-06-12T21:07:23.045709Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:07:23.045719Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:07:23.045726Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:07:23.045733Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:07:23.045740Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:07:53.046074Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=5505.16 KB (evictable, flusher gauge), registry+intern≈7.15 MB (resident, not flushable), process RSS=30.2 MB +2026-06-12T21:07:53.046120Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:07:53.046130Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:07:53.046137Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:07:53.046144Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:07:53.046151Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:08:23.046447Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=6085.73 KB (evictable, flusher gauge), registry+intern≈7.72 MB (resident, not flushable), process RSS=30.7 MB +2026-06-12T21:08:23.046491Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:08:23.046501Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:08:23.046508Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:08:23.046515Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:08:23.046522Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:08:53.045619Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=6670.51 KB (evictable, flusher gauge), registry+intern≈8.29 MB (resident, not flushable), process RSS=31.3 MB +2026-06-12T21:08:53.045666Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:08:53.045676Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:08:53.045683Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:08:53.045690Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:08:53.045697Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:09:23.046383Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=7256.44 KB (evictable, flusher gauge), registry+intern≈8.86 MB (resident, not flushable), process RSS=32.3 MB +2026-06-12T21:09:23.046430Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:09:23.046440Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:09:23.046447Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:09:23.046454Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:09:23.046461Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:09:53.046652Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=7838.86 KB (evictable, flusher gauge), registry+intern≈9.43 MB (resident, not flushable), process RSS=33.3 MB +2026-06-12T21:09:53.046697Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:09:53.046707Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:09:53.046714Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:09:53.046721Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:09:53.046728Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:10:23.045977Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=8419.44 KB (evictable, flusher gauge), registry+intern≈10.00 MB (resident, not flushable), process RSS=34.1 MB +2026-06-12T21:10:23.046023Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:10:23.046032Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:10:23.046039Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:10:23.046046Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:10:23.046053Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:10:53.046045Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=9004.22 KB (evictable, flusher gauge), registry+intern≈10.57 MB (resident, not flushable), process RSS=34.9 MB +2026-06-12T21:10:53.046096Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:10:53.046110Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:10:53.046122Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:10:53.046133Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:10:53.046145Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:11:23.046088Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=9590.14 KB (evictable, flusher gauge), registry+intern≈11.14 MB (resident, not flushable), process RSS=35.6 MB +2026-06-12T21:11:23.046134Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:11:23.046143Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:11:23.046150Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:11:23.046157Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:11:23.046164Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:11:53.045720Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=10172.57 KB (evictable, flusher gauge), registry+intern≈11.71 MB (resident, not flushable), process RSS=36.2 MB +2026-06-12T21:11:53.045766Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:11:53.045775Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:11:53.045782Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:11:53.045789Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:11:53.045796Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:12:23.046342Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=10753.14 KB (evictable, flusher gauge), registry+intern≈12.28 MB (resident, not flushable), process RSS=37.4 MB +2026-06-12T21:12:23.046389Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:12:23.046399Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:12:23.046406Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:12:23.046413Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:12:23.046420Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:12:53.045584Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=11337.93 KB (evictable, flusher gauge), registry+intern≈12.85 MB (resident, not flushable), process RSS=38.5 MB +2026-06-12T21:12:53.045633Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:12:53.045642Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:12:53.045649Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:12:53.045656Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:12:53.045662Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:13:23.046130Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=11923.85 KB (evictable, flusher gauge), registry+intern≈13.42 MB (resident, not flushable), process RSS=39.8 MB +2026-06-12T21:13:23.046178Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:13:23.046187Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:13:23.046194Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:13:23.046202Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:13:23.046208Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:13:53.046572Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=12506.28 KB (evictable, flusher gauge), registry+intern≈13.99 MB (resident, not flushable), process RSS=40.0 MB +2026-06-12T21:13:53.046626Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:13:53.046635Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:13:53.046642Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:13:53.046660Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:13:53.046667Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:14:23.045902Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=13086.85 KB (evictable, flusher gauge), registry+intern≈14.56 MB (resident, not flushable), process RSS=40.3 MB +2026-06-12T21:14:23.045951Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:14:23.045961Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:14:23.045968Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:14:23.045975Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:14:23.045981Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:14:53.046026Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=13671.63 KB (evictable, flusher gauge), registry+intern≈15.13 MB (resident, not flushable), process RSS=40.8 MB +2026-06-12T21:14:53.046073Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:14:53.046082Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:14:53.046089Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:14:53.046096Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:14:53.046103Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:15:23.045879Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=14257.56 KB (evictable, flusher gauge), registry+intern≈15.70 MB (resident, not flushable), process RSS=41.1 MB +2026-06-12T21:15:23.045929Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:15:23.045939Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:15:23.045946Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:15:23.045953Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:15:23.045959Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:15:53.045916Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=14839.98 KB (evictable, flusher gauge), registry+intern≈16.27 MB (resident, not flushable), process RSS=41.6 MB +2026-06-12T21:15:53.045962Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:15:53.045972Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:15:53.045979Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:15:53.045986Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:15:53.045993Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:16:23.046602Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=15420.56 KB (evictable, flusher gauge), registry+intern≈16.84 MB (resident, not flushable), process RSS=42.9 MB +2026-06-12T21:16:23.046663Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:16:23.046679Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:16:23.046692Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:16:23.046705Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:16:23.046718Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:16:53.046431Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=16005.34 KB (evictable, flusher gauge), registry+intern≈17.41 MB (resident, not flushable), process RSS=43.4 MB +2026-06-12T21:16:53.046476Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:16:53.046486Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:16:53.046493Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:16:53.046500Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:16:53.046507Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:17:23.046142Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=16591.26 KB (evictable, flusher gauge), registry+intern≈17.98 MB (resident, not flushable), process RSS=43.9 MB +2026-06-12T21:17:23.046187Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:17:23.046197Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:17:23.046204Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:17:23.046211Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:17:23.046218Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:17:53.046188Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=17173.69 KB (evictable, flusher gauge), registry+intern≈18.55 MB (resident, not flushable), process RSS=44.7 MB +2026-06-12T21:17:53.046234Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:17:53.046243Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:17:53.046251Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:17:53.046258Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:17:53.046265Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:18:23.046054Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=17754.26 KB (evictable, flusher gauge), registry+intern≈19.12 MB (resident, not flushable), process RSS=45.2 MB +2026-06-12T21:18:23.046106Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:18:23.046122Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:18:23.046133Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:18:23.046145Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:18:23.046157Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:18:53.046397Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=18339.05 KB (evictable, flusher gauge), registry+intern≈19.69 MB (resident, not flushable), process RSS=45.9 MB +2026-06-12T21:18:53.046442Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:18:53.046452Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:18:53.046459Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:18:53.046466Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:18:53.046473Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:19:23.045941Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=18924.97 KB (evictable, flusher gauge), registry+intern≈20.26 MB (resident, not flushable), process RSS=47.5 MB +2026-06-12T21:19:23.045984Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:19:23.045994Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:19:23.046001Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:19:23.046008Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:19:23.046015Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:19:53.047235Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=19507.40 KB (evictable, flusher gauge), registry+intern≈20.83 MB (resident, not flushable), process RSS=48.5 MB +2026-06-12T21:19:53.047297Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:19:53.047315Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:19:53.047328Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:19:53.047341Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:19:53.047353Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:20:23.045826Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=20087.97 KB (evictable, flusher gauge), registry+intern≈21.39 MB (resident, not flushable), process RSS=48.5 MB +2026-06-12T21:20:23.045871Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:20:23.045881Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:20:23.045888Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:20:23.045895Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:20:23.045901Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:20:53.045801Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=20672.75 KB (evictable, flusher gauge), registry+intern≈21.97 MB (resident, not flushable), process RSS=49.3 MB +2026-06-12T21:20:53.045846Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:20:53.045855Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:20:53.045862Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:20:53.045870Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:20:53.045876Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:21:23.045963Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=21258.68 KB (evictable, flusher gauge), registry+intern≈22.54 MB (resident, not flushable), process RSS=50.8 MB +2026-06-12T21:21:23.046009Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:21:23.046019Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:21:23.046026Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:21:23.046033Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:21:23.046040Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:21:53.046353Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=21841.11 KB (evictable, flusher gauge), registry+intern≈23.11 MB (resident, not flushable), process RSS=51.9 MB +2026-06-12T21:21:53.046398Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:21:53.046408Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:21:53.046415Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:21:53.046422Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:21:53.046431Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:22:23.046672Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=22421.68 KB (evictable, flusher gauge), registry+intern≈23.67 MB (resident, not flushable), process RSS=52.1 MB +2026-06-12T21:22:23.046717Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:22:23.046727Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:22:23.046734Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:22:23.046741Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:22:23.046748Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:22:53.045835Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=23006.46 KB (evictable, flusher gauge), registry+intern≈24.24 MB (resident, not flushable), process RSS=52.1 MB +2026-06-12T21:22:53.045884Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:22:53.045894Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:22:53.045901Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:22:53.045907Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:22:53.045914Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:23:23.045849Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=23592.38 KB (evictable, flusher gauge), registry+intern≈24.82 MB (resident, not flushable), process RSS=53.8 MB +2026-06-12T21:23:23.045893Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:23:23.045902Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:23:23.045909Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:23:23.045916Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:23:23.045923Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:23:53.045942Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=24174.81 KB (evictable, flusher gauge), registry+intern≈25.39 MB (resident, not flushable), process RSS=55.1 MB +2026-06-12T21:23:53.045991Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:23:53.046000Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:23:53.046007Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:23:53.046014Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:23:53.046021Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:24:23.046264Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=24755.39 KB (evictable, flusher gauge), registry+intern≈25.95 MB (resident, not flushable), process RSS=56.1 MB +2026-06-12T21:24:23.046317Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:24:23.046327Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:24:23.046334Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:24:23.046341Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:24:23.046347Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:24:53.046082Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=25340.17 KB (evictable, flusher gauge), registry+intern≈26.52 MB (resident, not flushable), process RSS=56.4 MB +2026-06-12T21:24:53.046128Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:24:53.046137Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:24:53.046144Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:24:53.046151Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:24:53.046158Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:25:23.046022Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=25926.09 KB (evictable, flusher gauge), registry+intern≈27.10 MB (resident, not flushable), process RSS=57.1 MB +2026-06-12T21:25:23.046069Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:25:23.046079Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:25:23.046087Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:25:23.046093Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:25:23.046100Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:25:53.045987Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=26508.52 KB (evictable, flusher gauge), registry+intern≈27.66 MB (resident, not flushable), process RSS=57.4 MB +2026-06-12T21:25:53.046036Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:25:53.046046Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:25:53.046053Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:25:53.046060Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:25:53.046067Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:26:23.046005Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=27089.09 KB (evictable, flusher gauge), registry+intern≈28.23 MB (resident, not flushable), process RSS=57.7 MB +2026-06-12T21:26:23.046053Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:26:23.046063Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:26:23.046070Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:26:23.046077Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:26:23.046084Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:26:53.046198Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=27673.88 KB (evictable, flusher gauge), registry+intern≈28.80 MB (resident, not flushable), process RSS=58.2 MB +2026-06-12T21:26:53.046243Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:26:53.046252Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:26:53.046259Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:26:53.046266Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:26:53.046273Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:27:23.046672Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=28259.80 KB (evictable, flusher gauge), registry+intern≈29.38 MB (resident, not flushable), process RSS=58.9 MB +2026-06-12T21:27:23.046718Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:27:23.046727Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:27:23.046734Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:27:23.046741Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:27:23.046749Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:27:53.046087Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=28842.23 KB (evictable, flusher gauge), registry+intern≈29.94 MB (resident, not flushable), process RSS=60.5 MB +2026-06-12T21:27:53.046134Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:27:53.046143Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:27:53.046151Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:27:53.046157Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:27:53.046164Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:28:23.046051Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=29422.80 KB (evictable, flusher gauge), registry+intern≈30.51 MB (resident, not flushable), process RSS=60.8 MB +2026-06-12T21:28:23.046096Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:28:23.046106Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:28:23.046113Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:28:23.046120Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:28:23.046127Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:28:53.046365Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=30007.58 KB (evictable, flusher gauge), registry+intern≈31.08 MB (resident, not flushable), process RSS=61.0 MB +2026-06-12T21:28:53.046410Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:28:53.046420Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:28:53.046427Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:28:53.046434Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:28:53.046440Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:29:23.051828Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=30593.51 KB (evictable, flusher gauge), registry+intern≈31.65 MB (resident, not flushable), process RSS=62.4 MB +2026-06-12T21:29:23.051912Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:29:23.051922Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:29:23.051930Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:29:23.051943Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:29:23.051951Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:29:53.046349Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=31175.93 KB (evictable, flusher gauge), registry+intern≈32.22 MB (resident, not flushable), process RSS=63.0 MB +2026-06-12T21:29:53.046393Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:29:53.046403Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:29:53.046410Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:29:53.046417Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:29:53.046423Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:30:23.046144Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=31756.51 KB (evictable, flusher gauge), registry+intern≈32.79 MB (resident, not flushable), process RSS=63.3 MB +2026-06-12T21:30:23.046196Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:30:23.046206Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:30:23.046213Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:30:23.046220Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:30:23.046227Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:30:53.046608Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=32341.29 KB (evictable, flusher gauge), registry+intern≈33.36 MB (resident, not flushable), process RSS=63.8 MB +2026-06-12T21:30:53.046652Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:30:53.046662Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:30:53.046669Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:30:53.046676Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:30:53.046688Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:31:23.047219Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=32927.21 KB (evictable, flusher gauge), registry+intern≈33.93 MB (resident, not flushable), process RSS=65.3 MB +2026-06-12T21:31:23.047508Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:31:23.047531Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:31:23.047545Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:31:23.047558Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:31:23.047577Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:31:53.046898Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=33509.48 KB (evictable, flusher gauge), registry+intern≈34.50 MB (resident, not flushable), process RSS=66.1 MB +2026-06-12T21:31:53.046955Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:31:53.046965Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:31:53.046972Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:31:53.046979Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:31:53.046986Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:32:23.046694Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=34090.05 KB (evictable, flusher gauge), registry+intern≈35.07 MB (resident, not flushable), process RSS=66.6 MB +2026-06-12T21:32:23.046760Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:32:23.046778Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:32:23.046792Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:32:23.046807Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:32:23.046820Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:32:53.045834Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=34674.83 KB (evictable, flusher gauge), registry+intern≈35.64 MB (resident, not flushable), process RSS=67.1 MB +2026-06-12T21:32:53.045887Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:32:53.045897Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:32:53.045904Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:32:53.045911Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:32:53.045919Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:33:23.046137Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=35260.76 KB (evictable, flusher gauge), registry+intern≈36.21 MB (resident, not flushable), process RSS=67.5 MB +2026-06-12T21:33:23.046182Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:33:23.046192Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:33:23.046199Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:33:23.046205Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:33:23.046212Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:33:53.046514Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=35843.19 KB (evictable, flusher gauge), registry+intern≈36.78 MB (resident, not flushable), process RSS=68.5 MB +2026-06-12T21:33:53.046561Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:33:53.046570Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:33:53.046578Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:33:53.046585Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:33:53.046591Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:34:23.045840Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=36423.76 KB (evictable, flusher gauge), registry+intern≈37.35 MB (resident, not flushable), process RSS=69.0 MB +2026-06-12T21:34:23.045888Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:34:23.045897Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:34:23.045904Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:34:23.045911Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:34:23.045918Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:34:53.046193Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=37008.54 KB (evictable, flusher gauge), registry+intern≈37.92 MB (resident, not flushable), process RSS=69.5 MB +2026-06-12T21:34:53.046238Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:34:53.046247Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:34:53.046254Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:34:53.046261Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:34:53.046268Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 +2026-06-12T21:35:23.046310Z  INFO data_plane: data_plane/src/main.rs:987: [MEMORY_DIAG] SketchStore: 1004 instance(s), 1004 sid(s) with state, payload=37573.83 KB (evictable, flusher gauge), registry+intern≈38.47 MB (resident, not flushable), process RSS=70.3 MB +2026-06-12T21:35:23.046355Z  INFO data_plane: data_plane/src/main.rs:1006: [MEMORY_DIAG] PrecomputeEngine: 4 total groups across 4 workers +2026-06-12T21:35:23.046365Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_0: group_states_len=1 +2026-06-12T21:35:23.046372Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_1: group_states_len=2 +2026-06-12T21:35:23.046378Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_2: group_states_len=0 +2026-06-12T21:35:23.046385Z  INFO data_plane: data_plane/src/main.rs:1012: [MEMORY_DIAG] worker_3: group_states_len=1 diff --git a/datasets_eval/soak/loadgen.py b/datasets_eval/soak/loadgen.py new file mode 100644 index 000000000..7597b0db6 --- /dev/null +++ b/datasets_eval/soak/loadgen.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""loadgen.py — constant-rate OTLP/gRPC load generator for the edge soak. + +Reuses the OTLP/gRPC encoding from datasets_eval/google_cluster/run.py but +drives a SUSTAINED CONSTANT rate (points/sec) by looping over the real +Google-2019 mapped trace JSONL indefinitely, rather than pacing to the +trace's own (very sparse) timestamps. Each loop re-bases timestamps to +"now" so the edge keeps closing windows on fresh data and the warm store +keeps turning over — exactly the steady-ingest condition a leak-slope +soak needs. + +Usage: + loadgen.py --jsonl /tmp/gct-otlp.jsonl --endpoint 127.0.0.1:4317 \ + --rate 5000 --duration 1800 +""" +from __future__ import annotations + +import argparse +import json +import sys +import time + +import grpc +from opentelemetry.proto.collector.metrics.v1 import metrics_service_pb2 +from opentelemetry.proto.collector.metrics.v1 import metrics_service_pb2_grpc +from opentelemetry.proto.common.v1 import common_pb2 + + +def load_rows(path: str, cap: int): + rows = [] + with open(path) as f: + for line in f: + line = line.strip() + if not line: + continue + rows.append(json.loads(line)) + if cap and len(rows) >= cap: + break + return rows + + +def build_req(rows): + req = metrics_service_pb2.ExportMetricsServiceRequest() + rm = req.resource_metrics.add() + a = rm.resource.attributes.add() + a.key = "service.name" + a.value.string_value = "google-cluster-soak" + sm = rm.scope_metrics.add() + per_metric = {} + for r in rows: + per_metric.setdefault(r["metric"], []).append(r) + now_ns = time.time_ns() + for mname, mrows in sorted(per_metric.items()): + metric = sm.metrics.add() + metric.name = mname + for r in mrows: + dp = metric.gauge.data_points.add() + dp.as_double = float(r["value"]) + dp.time_unix_nano = now_ns + for k, v in sorted(r["attributes"].items()): + av = common_pb2.AnyValue() + av.string_value = str(v) + dp.attributes.add(key=k, value=av) + return req + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--jsonl", required=True) + ap.add_argument("--endpoint", default="127.0.0.1:4317") + ap.add_argument("--rate", type=float, default=5000.0, + help="target data points / second") + ap.add_argument("--duration", type=float, default=1800.0) + ap.add_argument("--batch", type=int, default=500) + ap.add_argument("--cap", type=int, default=200000, + help="max rows loaded from trace into the loop buffer") + args = ap.parse_args() + + rows = load_rows(args.jsonl, args.cap) + if not rows: + print("loadgen: no rows", file=sys.stderr) + return 2 + print(f"loadgen: loaded {len(rows)} trace rows; target rate " + f"{args.rate} pts/s for {args.duration}s", file=sys.stderr) + + ch = grpc.insecure_channel( + args.endpoint, + options=[("grpc.max_send_message_length", 1 << 30)]) + stub = metrics_service_pb2_grpc.MetricsServiceStub(ch) + + start = time.monotonic() + sent = 0 + idx = 0 + batch_period = args.batch / args.rate # seconds per batch to hit rate + next_send = start + last_report = start + errors = 0 + while True: + now = time.monotonic() + if now - start >= args.duration: + break + # take the next `batch` rows, wrapping + chunk = [] + for _ in range(args.batch): + chunk.append(rows[idx]) + idx = (idx + 1) % len(rows) + req = build_req(chunk) + try: + stub.Export(req, timeout=15.0) + sent += len(chunk) + except grpc.RpcError as exc: + errors += 1 + if errors <= 5: + print(f"loadgen: export error: {exc.code()}", file=sys.stderr) + next_send += batch_period + sleep = next_send - time.monotonic() + if sleep > 0: + time.sleep(sleep) + if now - last_report >= 30: + el = now - start + print(f"loadgen: t={el:6.0f}s sent={sent} " + f"({sent/el:.0f} pts/s) errors={errors}", file=sys.stderr) + last_report = now + el = time.monotonic() - start + print(f"loadgen: done. sent={sent} in {el:.0f}s " + f"({sent/el:.0f} pts/s) errors={errors}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/datasets_eval/soak/loadgen_b0.log b/datasets_eval/soak/loadgen_b0.log new file mode 100644 index 000000000..6271497a9 --- /dev/null +++ b/datasets_eval/soak/loadgen_b0.log @@ -0,0 +1,15 @@ +loadgen: loaded 200000 trace rows; target rate 5000.0 pts/s for 260.0s +loadgen: t= 30s sent=150500 (5017 pts/s) errors=0 +loadgen: t= 60s sent=301000 (5008 pts/s) errors=0 +loadgen: t= 90s sent=451500 (5006 pts/s) errors=0 +loadgen: t= 120s sent=601500 (5004 pts/s) errors=0 +loadgen: t= 150s sent=752000 (5003 pts/s) errors=0 +loadgen: t= 180s sent=902500 (5003 pts/s) errors=0 +loadgen: t= 211s sent=1053000 (5002 pts/s) errors=0 +loadgen: export error: StatusCode.UNAVAILABLE +loadgen: export error: StatusCode.UNAVAILABLE +loadgen: export error: StatusCode.UNAVAILABLE +loadgen: export error: StatusCode.UNAVAILABLE +loadgen: export error: StatusCode.UNAVAILABLE +loadgen: t= 241s sent=1133000 (4709 pts/s) errors=141 +loadgen: done. sent=1229500 in 260s (4729 pts/s) errors=141 diff --git a/datasets_eval/soak/loadgen_b3.log b/datasets_eval/soak/loadgen_b3.log new file mode 100644 index 000000000..a8c42f9d3 --- /dev/null +++ b/datasets_eval/soak/loadgen_b3.log @@ -0,0 +1,65 @@ +loadgen: loaded 200000 trace rows; target rate 5000.0 pts/s for 1900.0s +loadgen: t= 30s sent=150500 (5017 pts/s) errors=0 +loadgen: t= 60s sent=301000 (5008 pts/s) errors=0 +loadgen: t= 90s sent=451500 (5006 pts/s) errors=0 +loadgen: t= 120s sent=602000 (5004 pts/s) errors=0 +loadgen: t= 150s sent=752500 (5003 pts/s) errors=0 +loadgen: t= 181s sent=903000 (5003 pts/s) errors=0 +loadgen: t= 211s sent=1053000 (5002 pts/s) errors=0 +loadgen: t= 241s sent=1203500 (5002 pts/s) errors=0 +loadgen: t= 271s sent=1354000 (5002 pts/s) errors=0 +loadgen: t= 301s sent=1504000 (5002 pts/s) errors=0 +loadgen: t= 331s sent=1654000 (5002 pts/s) errors=0 +loadgen: t= 361s sent=1804500 (5001 pts/s) errors=0 +loadgen: t= 391s sent=1954500 (5001 pts/s) errors=0 +loadgen: t= 421s sent=2104500 (5001 pts/s) errors=0 +loadgen: t= 451s sent=2255000 (5001 pts/s) errors=0 +loadgen: t= 481s sent=2405500 (5001 pts/s) errors=0 +loadgen: t= 511s sent=2556000 (5001 pts/s) errors=0 +loadgen: t= 541s sent=2706500 (5001 pts/s) errors=0 +loadgen: t= 571s sent=2857000 (5001 pts/s) errors=0 +loadgen: t= 601s sent=3007500 (5001 pts/s) errors=0 +loadgen: t= 631s sent=3157500 (5001 pts/s) errors=0 +loadgen: t= 661s sent=3307500 (5001 pts/s) errors=0 +loadgen: t= 692s sent=3458000 (5001 pts/s) errors=0 +loadgen: t= 722s sent=3608000 (5001 pts/s) errors=0 +loadgen: t= 752s sent=3758500 (5001 pts/s) errors=0 +loadgen: t= 782s sent=3909000 (5001 pts/s) errors=0 +loadgen: t= 812s sent=4059000 (5001 pts/s) errors=0 +loadgen: t= 842s sent=4209000 (5001 pts/s) errors=0 +loadgen: t= 872s sent=4359500 (5001 pts/s) errors=0 +loadgen: t= 902s sent=4509500 (5001 pts/s) errors=0 +loadgen: t= 932s sent=4660000 (5001 pts/s) errors=0 +loadgen: t= 962s sent=4810500 (5001 pts/s) errors=0 +loadgen: t= 992s sent=4961000 (5001 pts/s) errors=0 +loadgen: t= 1022s sent=5111500 (5000 pts/s) errors=0 +loadgen: t= 1052s sent=5261500 (5000 pts/s) errors=0 +loadgen: t= 1082s sent=5412000 (5000 pts/s) errors=0 +loadgen: t= 1112s sent=5562500 (5000 pts/s) errors=0 +loadgen: t= 1142s sent=5712500 (5000 pts/s) errors=0 +loadgen: t= 1173s sent=5863000 (5000 pts/s) errors=0 +loadgen: t= 1203s sent=6013000 (5000 pts/s) errors=0 +loadgen: t= 1233s sent=6163500 (5000 pts/s) errors=0 +loadgen: t= 1263s sent=6313500 (5000 pts/s) errors=0 +loadgen: t= 1293s sent=6463500 (5000 pts/s) errors=0 +loadgen: t= 1323s sent=6613500 (5000 pts/s) errors=0 +loadgen: t= 1353s sent=6763500 (5000 pts/s) errors=0 +loadgen: t= 1383s sent=6913500 (5000 pts/s) errors=0 +loadgen: t= 1413s sent=7064000 (5000 pts/s) errors=0 +loadgen: t= 1443s sent=7214000 (5000 pts/s) errors=0 +loadgen: t= 1473s sent=7364500 (5000 pts/s) errors=0 +loadgen: t= 1503s sent=7515000 (5000 pts/s) errors=0 +loadgen: t= 1533s sent=7665000 (5000 pts/s) errors=0 +loadgen: t= 1563s sent=7815000 (5000 pts/s) errors=0 +loadgen: t= 1593s sent=7965500 (5000 pts/s) errors=0 +loadgen: t= 1623s sent=8115500 (5000 pts/s) errors=0 +loadgen: t= 1653s sent=8266000 (5000 pts/s) errors=0 +loadgen: t= 1683s sent=8416500 (5000 pts/s) errors=0 +loadgen: t= 1713s sent=8566500 (5000 pts/s) errors=0 +loadgen: t= 1743s sent=8717000 (5000 pts/s) errors=0 +loadgen: t= 1773s sent=8867500 (5000 pts/s) errors=0 +loadgen: t= 1803s sent=9017500 (5000 pts/s) errors=0 +loadgen: t= 1834s sent=9168000 (5000 pts/s) errors=0 +loadgen: t= 1864s sent=9318500 (5000 pts/s) errors=0 +loadgen: t= 1894s sent=9468500 (5000 pts/s) errors=0 +loadgen: done. sent=9500000 in 1900s (5000 pts/s) errors=0 diff --git a/datasets_eval/soak/rss_over_time.png b/datasets_eval/soak/rss_over_time.png new file mode 100644 index 0000000000000000000000000000000000000000..1bc689affa95d00436736d698e3937e32663043f GIT binary patch literal 121071 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 SrKxg!=FFTs/{stat,status} give +exact RSS + CPU jiffies without any container overhead. + +CPU% is computed from the delta of (utime+stime) jiffies between two +consecutive samples divided by the wall-clock delta, normalised to a +single core (so 100% == one fully-busy core; can exceed 100% multi-core). + +Output: one JSON object per sample to the --out JSONL file, plus a human +line to stderr. Run for --duration seconds at --interval seconds. + +Usage: + sampler.py --pids edge=1521738 dp=1521599 \ + --interval 3 --duration 1800 --out samples.jsonl --tag b3 +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time + +CLK_TCK = os.sysconf("SC_CLK_TCK") +PAGE = os.sysconf("SC_PAGE_SIZE") + + +def read_stat(pid: int): + """Return (utime+stime jiffies, rss_bytes) or None if pid is gone.""" + try: + with open(f"/proc/{pid}/stat", "rb") as f: + data = f.read() + # comm may contain spaces/parens; split on last ')' + rparen = data.rindex(b")") + fields = data[rparen + 2 :].split() + # after comm, field index 0 == state; utime=13,stime=14 (1-based 14,15) + utime = int(fields[11]) + stime = int(fields[12]) + with open(f"/proc/{pid}/status", "r") as f: + rss_kb = 0 + for line in f: + if line.startswith("VmRSS:"): + rss_kb = int(line.split()[1]) + break + return utime + stime, rss_kb * 1024 + except (FileNotFoundError, ProcessLookupError, ValueError): + return None + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--pids", nargs="+", required=True, + help="name=pid pairs, e.g. edge=123 dp=456") + ap.add_argument("--interval", type=float, default=3.0) + ap.add_argument("--duration", type=float, default=1800.0) + ap.add_argument("--out", required=True) + ap.add_argument("--tag", default="") + args = ap.parse_args() + + pids = {} + for kv in args.pids: + name, pid = kv.split("=") + pids[name] = int(pid) + + prev = {name: read_stat(pid) for name, pid in pids.items()} + prev_t = time.monotonic() + start = prev_t + + out = open(args.out, "a", buffering=1) + n = 0 + while True: + now = time.monotonic() + if now - start >= args.duration: + break + time.sleep(args.interval) + t = time.monotonic() + dt = t - prev_t + rec = {"t_wall": time.time(), "t_rel": round(t - start, 2), + "tag": args.tag} + for name, pid in pids.items(): + cur = read_stat(pid) + if cur is None: + rec[name] = {"rss_mb": None, "cpu_pct": None, "alive": False} + continue + cpu_pct = None + if prev[name] is not None and dt > 0: + djiff = cur[0] - prev[name][0] + cpu_pct = 100.0 * (djiff / CLK_TCK) / dt + rec[name] = {"rss_mb": round(cur[1] / 1e6, 2), + "cpu_pct": round(cpu_pct, 2) if cpu_pct is not None else None, + "alive": True} + prev[name] = cur + prev_t = t + out.write(json.dumps(rec) + "\n") + n += 1 + parts = " ".join( + f"{k}:rss={v['rss_mb']}MB cpu={v['cpu_pct']}%" + for k, v in rec.items() if isinstance(v, dict)) + print(f"[{rec['t_rel']:7.1f}s] {parts}", file=sys.stderr) + out.close() + print(f"sampler: wrote {n} samples to {args.out}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/datasets_eval/soak/samples_b0.jsonl b/datasets_eval/soak/samples_b0.jsonl new file mode 100644 index 000000000..4a2d9bf84 --- /dev/null +++ b/datasets_eval/soak/samples_b0.jsonl @@ -0,0 +1,60 @@ +{"t_wall": 1781297993.0914965, "t_rel": 3.0, "tag": "b0", "edge": {"rss_mb": 202.94, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 27.77, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781297996.0952024, "t_rel": 6.01, "tag": "b0", "edge": {"rss_mb": 203.75, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 27.77, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781297999.0988362, "t_rel": 9.01, "tag": "b0", "edge": {"rss_mb": 204.14, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 27.77, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298002.1024518, "t_rel": 12.01, "tag": "b0", "edge": {"rss_mb": 205.21, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 27.77, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298005.1060708, "t_rel": 15.02, "tag": "b0", "edge": {"rss_mb": 205.21, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 27.88, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298008.109689, "t_rel": 18.02, "tag": "b0", "edge": {"rss_mb": 205.21, "cpu_pct": 2.0, "alive": true}, "dp": {"rss_mb": 28.0, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298011.1123114, "t_rel": 21.02, "tag": "b0", "edge": {"rss_mb": 205.21, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.0, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298014.1159334, "t_rel": 24.03, "tag": "b0", "edge": {"rss_mb": 205.21, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.0, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298017.1195807, "t_rel": 27.03, "tag": "b0", "edge": {"rss_mb": 206.28, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 28.0, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298020.1203012, "t_rel": 30.03, "tag": "b0", "edge": {"rss_mb": 206.28, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.0, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298023.12396, "t_rel": 33.04, "tag": "b0", "edge": {"rss_mb": 206.28, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.0, "cpu_pct": 7.32, "alive": true}} +{"t_wall": 1781298026.127602, "t_rel": 36.04, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298029.131224, "t_rel": 39.04, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298032.1348457, "t_rel": 42.05, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298035.1385088, "t_rel": 45.05, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298038.1421878, "t_rel": 48.05, "tag": "b0", "edge": {"rss_mb": 205.69, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298041.1458719, "t_rel": 51.06, "tag": "b0", "edge": {"rss_mb": 205.69, "cpu_pct": 2.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.32, "alive": true}} +{"t_wall": 1781298044.1495454, "t_rel": 54.06, "tag": "b0", "edge": {"rss_mb": 205.69, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298047.1532166, "t_rel": 57.06, "tag": "b0", "edge": {"rss_mb": 205.69, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298050.1568422, "t_rel": 60.07, "tag": "b0", "edge": {"rss_mb": 205.69, "cpu_pct": 2.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298053.1604643, "t_rel": 63.07, "tag": "b0", "edge": {"rss_mb": 205.41, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298056.1641293, "t_rel": 66.08, "tag": "b0", "edge": {"rss_mb": 205.41, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.02, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298059.1677694, "t_rel": 69.08, "tag": "b0", "edge": {"rss_mb": 205.41, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.04, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298062.1714072, "t_rel": 72.08, "tag": "b0", "edge": {"rss_mb": 205.16, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.08, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298065.17505, "t_rel": 75.09, "tag": "b0", "edge": {"rss_mb": 205.16, "cpu_pct": 2.66, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298068.178671, "t_rel": 78.09, "tag": "b0", "edge": {"rss_mb": 205.16, "cpu_pct": 2.66, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.32, "alive": true}} +{"t_wall": 1781298071.1822894, "t_rel": 81.09, "tag": "b0", "edge": {"rss_mb": 205.16, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298074.1843038, "t_rel": 84.1, "tag": "b0", "edge": {"rss_mb": 205.16, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298077.1879196, "t_rel": 87.1, "tag": "b0", "edge": {"rss_mb": 205.49, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298080.1915607, "t_rel": 90.1, "tag": "b0", "edge": {"rss_mb": 206.27, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298083.1951785, "t_rel": 93.11, "tag": "b0", "edge": {"rss_mb": 206.27, "cpu_pct": 1.66, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298086.1987934, "t_rel": 96.11, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298089.2024188, "t_rel": 99.11, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298092.2060483, "t_rel": 102.12, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.09, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298095.209708, "t_rel": 105.12, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298098.2123482, "t_rel": 108.12, "tag": "b0", "edge": {"rss_mb": 206.54, "cpu_pct": 2.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298101.2159672, "t_rel": 111.13, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298104.2196484, "t_rel": 114.13, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.32, "alive": true}} +{"t_wall": 1781298107.2233176, "t_rel": 117.13, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 2.66, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298110.226972, "t_rel": 120.14, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298113.2283516, "t_rel": 123.14, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298116.2320266, "t_rel": 126.14, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298119.2357118, "t_rel": 129.15, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298122.2393687, "t_rel": 132.15, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298125.2403321, "t_rel": 135.15, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298128.2439408, "t_rel": 138.16, "tag": "b0", "edge": {"rss_mb": 206.79, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298131.2475946, "t_rel": 141.16, "tag": "b0", "edge": {"rss_mb": 205.51, "cpu_pct": 2.66, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.32, "alive": true}} +{"t_wall": 1781298134.2512412, "t_rel": 144.16, "tag": "b0", "edge": {"rss_mb": 206.01, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298137.2548728, "t_rel": 147.17, "tag": "b0", "edge": {"rss_mb": 206.55, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298140.2585046, "t_rel": 150.17, "tag": "b0", "edge": {"rss_mb": 207.36, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.32, "alive": true}} +{"t_wall": 1781298143.2621944, "t_rel": 153.17, "tag": "b0", "edge": {"rss_mb": 207.36, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298146.2658331, "t_rel": 156.18, "tag": "b0", "edge": {"rss_mb": 207.36, "cpu_pct": 2.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298149.2694654, "t_rel": 159.18, "tag": "b0", "edge": {"rss_mb": 206.77, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.99, "alive": true}} +{"t_wall": 1781298152.273087, "t_rel": 162.18, "tag": "b0", "edge": {"rss_mb": 206.4, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298155.2767136, "t_rel": 165.19, "tag": "b0", "edge": {"rss_mb": 206.93, "cpu_pct": 2.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298158.280337, "t_rel": 168.19, "tag": "b0", "edge": {"rss_mb": 207.2, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298161.283966, "t_rel": 171.2, "tag": "b0", "edge": {"rss_mb": 207.2, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298164.2875993, "t_rel": 174.2, "tag": "b0", "edge": {"rss_mb": 207.2, "cpu_pct": 3.0, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298167.29122, "t_rel": 177.2, "tag": "b0", "edge": {"rss_mb": 207.2, "cpu_pct": 3.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 7.66, "alive": true}} +{"t_wall": 1781298170.2922924, "t_rel": 180.2, "tag": "b0", "edge": {"rss_mb": 207.2, "cpu_pct": 2.33, "alive": true}, "dp": {"rss_mb": 28.11, "cpu_pct": 8.33, "alive": true}} diff --git a/datasets_eval/soak/samples_b3_soak.jsonl b/datasets_eval/soak/samples_b3_soak.jsonl new file mode 100644 index 000000000..a49fa140d --- /dev/null +++ b/datasets_eval/soak/samples_b3_soak.jsonl @@ -0,0 +1,360 @@ +{"t_wall": 1781298244.032875, "t_rel": 5.01, "tag": "b3", "edge": {"rss_mb": 218.82, "cpu_pct": 4.6, "alive": true}, "dp": {"rss_mb": 25.47, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298249.0385613, "t_rel": 10.01, "tag": "b3", "edge": {"rss_mb": 220.17, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 25.74, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298254.0441513, "t_rel": 15.02, "tag": "b3", "edge": {"rss_mb": 220.17, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 25.74, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298259.049775, "t_rel": 20.02, "tag": "b3", "edge": {"rss_mb": 220.95, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 26.01, "cpu_pct": 0.6, "alive": true}} +{"t_wall": 1781298264.0553858, "t_rel": 25.03, "tag": "b3", "edge": {"rss_mb": 220.95, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 26.28, "cpu_pct": 0.0, "alive": true}} +{"t_wall": 1781298269.0609987, "t_rel": 30.03, "tag": "b3", "edge": {"rss_mb": 221.75, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 26.82, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298274.0666597, "t_rel": 35.04, "tag": "b3", "edge": {"rss_mb": 221.75, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 27.1, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298279.0722723, "t_rel": 40.04, "tag": "b3", "edge": {"rss_mb": 221.02, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 27.37, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298284.0778728, "t_rel": 45.05, "tag": "b3", "edge": {"rss_mb": 221.45, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 27.64, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298289.0835032, "t_rel": 50.06, "tag": "b3", "edge": {"rss_mb": 221.32, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 27.64, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298294.0891428, "t_rel": 55.06, "tag": "b3", "edge": {"rss_mb": 221.58, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 27.64, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298299.0947406, "t_rel": 60.07, "tag": "b3", "edge": {"rss_mb": 221.58, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 27.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298304.100346, "t_rel": 65.07, "tag": "b3", "edge": {"rss_mb": 221.85, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 27.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298309.1060183, "t_rel": 70.08, "tag": "b3", "edge": {"rss_mb": 220.84, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 27.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298314.1116054, "t_rel": 75.08, "tag": "b3", "edge": {"rss_mb": 218.67, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 27.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298319.1172276, "t_rel": 80.09, "tag": "b3", "edge": {"rss_mb": 219.28, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 27.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298324.1228619, "t_rel": 85.1, "tag": "b3", "edge": {"rss_mb": 220.12, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 27.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298329.1284664, "t_rel": 90.1, "tag": "b3", "edge": {"rss_mb": 220.65, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 27.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298334.134089, "t_rel": 95.11, "tag": "b3", "edge": {"rss_mb": 221.66, "cpu_pct": 5.39, "alive": true}, "dp": {"rss_mb": 27.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298339.1397018, "t_rel": 100.11, "tag": "b3", "edge": {"rss_mb": 221.66, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 28.18, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298344.145317, "t_rel": 105.12, "tag": "b3", "edge": {"rss_mb": 221.68, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 28.45, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298349.150926, "t_rel": 110.12, "tag": "b3", "edge": {"rss_mb": 221.68, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 28.45, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298354.1565495, "t_rel": 115.13, "tag": "b3", "edge": {"rss_mb": 222.07, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 28.45, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298359.1621454, "t_rel": 120.13, "tag": "b3", "edge": {"rss_mb": 223.08, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 28.72, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298364.1677847, "t_rel": 125.14, "tag": "b3", "edge": {"rss_mb": 223.75, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 28.72, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298369.1734097, "t_rel": 130.15, "tag": "b3", "edge": {"rss_mb": 221.22, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 28.72, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298374.1790147, "t_rel": 135.15, "tag": "b3", "edge": {"rss_mb": 223.04, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 28.72, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298379.1846385, "t_rel": 140.16, "tag": "b3", "edge": {"rss_mb": 223.04, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 28.72, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298384.1902237, "t_rel": 145.16, "tag": "b3", "edge": {"rss_mb": 223.24, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 28.99, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298389.1958523, "t_rel": 150.17, "tag": "b3", "edge": {"rss_mb": 223.24, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 28.99, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298394.201479, "t_rel": 155.17, "tag": "b3", "edge": {"rss_mb": 223.24, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 28.99, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298399.2070985, "t_rel": 160.18, "tag": "b3", "edge": {"rss_mb": 223.15, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 29.53, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298404.2126749, "t_rel": 165.18, "tag": "b3", "edge": {"rss_mb": 223.58, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 29.8, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298409.2183466, "t_rel": 170.19, "tag": "b3", "edge": {"rss_mb": 223.58, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 29.8, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298414.2239838, "t_rel": 175.2, "tag": "b3", "edge": {"rss_mb": 223.58, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 29.8, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298419.2296154, "t_rel": 180.2, "tag": "b3", "edge": {"rss_mb": 223.83, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 29.8, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298424.2352188, "t_rel": 185.21, "tag": "b3", "edge": {"rss_mb": 222.59, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 30.61, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298429.2408094, "t_rel": 190.21, "tag": "b3", "edge": {"rss_mb": 222.86, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 30.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298434.246396, "t_rel": 195.22, "tag": "b3", "edge": {"rss_mb": 222.86, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 30.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298439.2520106, "t_rel": 200.22, "tag": "b3", "edge": {"rss_mb": 223.77, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 30.88, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298444.2576272, "t_rel": 205.23, "tag": "b3", "edge": {"rss_mb": 224.03, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 31.15, "cpu_pct": 0.0, "alive": true}} +{"t_wall": 1781298449.263223, "t_rel": 210.24, "tag": "b3", "edge": {"rss_mb": 224.03, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 31.15, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298454.2688468, "t_rel": 215.24, "tag": "b3", "edge": {"rss_mb": 224.03, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 31.15, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298459.2744374, "t_rel": 220.25, "tag": "b3", "edge": {"rss_mb": 224.03, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 31.15, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298464.2800345, "t_rel": 225.25, "tag": "b3", "edge": {"rss_mb": 224.28, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 31.15, "cpu_pct": 0.0, "alive": true}} +{"t_wall": 1781298469.2856567, "t_rel": 230.26, "tag": "b3", "edge": {"rss_mb": 223.76, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 31.42, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298474.291285, "t_rel": 235.26, "tag": "b3", "edge": {"rss_mb": 222.96, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 31.69, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298479.2963724, "t_rel": 240.27, "tag": "b3", "edge": {"rss_mb": 222.96, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 31.69, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298484.3019757, "t_rel": 245.27, "tag": "b3", "edge": {"rss_mb": 222.96, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 32.23, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298489.3075852, "t_rel": 250.28, "tag": "b3", "edge": {"rss_mb": 220.41, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 32.23, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298494.3131762, "t_rel": 255.29, "tag": "b3", "edge": {"rss_mb": 220.99, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 32.23, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298499.3188374, "t_rel": 260.29, "tag": "b3", "edge": {"rss_mb": 220.99, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 32.23, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298504.324447, "t_rel": 265.3, "tag": "b3", "edge": {"rss_mb": 220.99, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 32.23, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298509.3300767, "t_rel": 270.3, "tag": "b3", "edge": {"rss_mb": 220.99, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 32.23, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298514.3356862, "t_rel": 275.31, "tag": "b3", "edge": {"rss_mb": 221.48, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 32.5, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298519.3412848, "t_rel": 280.31, "tag": "b3", "edge": {"rss_mb": 222.27, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 32.5, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298524.3469179, "t_rel": 285.32, "tag": "b3", "edge": {"rss_mb": 222.53, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 32.77, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298529.3525128, "t_rel": 290.32, "tag": "b3", "edge": {"rss_mb": 222.53, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 32.77, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298534.3581178, "t_rel": 295.33, "tag": "b3", "edge": {"rss_mb": 222.92, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 32.77, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298539.3637257, "t_rel": 300.34, "tag": "b3", "edge": {"rss_mb": 223.37, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 33.04, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298544.3682845, "t_rel": 305.34, "tag": "b3", "edge": {"rss_mb": 223.37, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 33.31, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298549.3739312, "t_rel": 310.35, "tag": "b3", "edge": {"rss_mb": 223.37, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 33.58, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298554.3795166, "t_rel": 315.35, "tag": "b3", "edge": {"rss_mb": 223.4, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 33.58, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298559.3851264, "t_rel": 320.36, "tag": "b3", "edge": {"rss_mb": 223.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 33.58, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298564.3907382, "t_rel": 325.36, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 33.85, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298569.3923495, "t_rel": 330.36, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 33.85, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298574.3980372, "t_rel": 335.37, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 34.12, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298579.4036372, "t_rel": 340.38, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 34.39, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298584.4093187, "t_rel": 345.38, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 34.66, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298589.4150014, "t_rel": 350.39, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 34.93, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298594.4206107, "t_rel": 355.39, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 34.93, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298599.4262428, "t_rel": 360.4, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 34.93, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298604.4319093, "t_rel": 365.4, "tag": "b3", "edge": {"rss_mb": 223.81, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 34.93, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298609.4375758, "t_rel": 370.41, "tag": "b3", "edge": {"rss_mb": 223.42, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 35.21, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298614.44319, "t_rel": 375.42, "tag": "b3", "edge": {"rss_mb": 223.42, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 35.48, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298619.4488008, "t_rel": 380.42, "tag": "b3", "edge": {"rss_mb": 223.42, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 35.75, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298624.4544144, "t_rel": 385.43, "tag": "b3", "edge": {"rss_mb": 223.42, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 35.75, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298629.4599993, "t_rel": 390.43, "tag": "b3", "edge": {"rss_mb": 223.49, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 35.75, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298634.465693, "t_rel": 395.44, "tag": "b3", "edge": {"rss_mb": 223.49, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 36.02, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298639.4712915, "t_rel": 400.44, "tag": "b3", "edge": {"rss_mb": 223.49, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 36.29, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298644.47691, "t_rel": 405.45, "tag": "b3", "edge": {"rss_mb": 223.49, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 36.29, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298649.4825115, "t_rel": 410.45, "tag": "b3", "edge": {"rss_mb": 223.49, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 36.56, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298654.488108, "t_rel": 415.46, "tag": "b3", "edge": {"rss_mb": 223.31, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 36.56, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298659.4937937, "t_rel": 420.47, "tag": "b3", "edge": {"rss_mb": 223.31, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 36.83, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298664.4991207, "t_rel": 425.47, "tag": "b3", "edge": {"rss_mb": 223.31, "cpu_pct": 4.6, "alive": true}, "dp": {"rss_mb": 37.37, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298669.504305, "t_rel": 430.48, "tag": "b3", "edge": {"rss_mb": 223.31, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 37.37, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298674.5098906, "t_rel": 435.48, "tag": "b3", "edge": {"rss_mb": 222.39, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 37.37, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298679.51554, "t_rel": 440.49, "tag": "b3", "edge": {"rss_mb": 222.39, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 37.37, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298684.521185, "t_rel": 445.49, "tag": "b3", "edge": {"rss_mb": 222.39, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 37.37, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298689.526836, "t_rel": 450.5, "tag": "b3", "edge": {"rss_mb": 223.42, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 37.64, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298694.5283027, "t_rel": 455.5, "tag": "b3", "edge": {"rss_mb": 224.05, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 37.64, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298699.533916, "t_rel": 460.51, "tag": "b3", "edge": {"rss_mb": 224.05, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 37.64, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298704.539528, "t_rel": 465.51, "tag": "b3", "edge": {"rss_mb": 223.15, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 37.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298709.5451336, "t_rel": 470.52, "tag": "b3", "edge": {"rss_mb": 223.15, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 37.91, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298714.5507617, "t_rel": 475.52, "tag": "b3", "edge": {"rss_mb": 223.15, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 37.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298719.5563335, "t_rel": 480.53, "tag": "b3", "edge": {"rss_mb": 223.15, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 38.45, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298724.5619414, "t_rel": 485.53, "tag": "b3", "edge": {"rss_mb": 222.76, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 38.45, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298729.567543, "t_rel": 490.54, "tag": "b3", "edge": {"rss_mb": 223.3, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 38.72, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298734.5731337, "t_rel": 495.55, "tag": "b3", "edge": {"rss_mb": 223.8, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 38.99, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298739.578736, "t_rel": 500.55, "tag": "b3", "edge": {"rss_mb": 223.11, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 39.26, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298744.5843503, "t_rel": 505.56, "tag": "b3", "edge": {"rss_mb": 224.41, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 39.53, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298749.5899482, "t_rel": 510.56, "tag": "b3", "edge": {"rss_mb": 222.56, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 39.8, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298754.5956025, "t_rel": 515.57, "tag": "b3", "edge": {"rss_mb": 221.64, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 40.07, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298759.6012113, "t_rel": 520.57, "tag": "b3", "edge": {"rss_mb": 222.72, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 40.07, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298764.6068652, "t_rel": 525.58, "tag": "b3", "edge": {"rss_mb": 222.72, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 40.07, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298769.6124587, "t_rel": 530.58, "tag": "b3", "edge": {"rss_mb": 222.72, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 40.07, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298774.6180978, "t_rel": 535.59, "tag": "b3", "edge": {"rss_mb": 221.94, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 40.61, "cpu_pct": 0.0, "alive": true}} +{"t_wall": 1781298779.623744, "t_rel": 540.6, "tag": "b3", "edge": {"rss_mb": 224.43, "cpu_pct": 4.99, "alive": true}, "dp": {"rss_mb": 41.15, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298784.6293995, "t_rel": 545.6, "tag": "b3", "edge": {"rss_mb": 223.32, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.42, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298789.6349945, "t_rel": 550.61, "tag": "b3", "edge": {"rss_mb": 223.26, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.42, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298794.640587, "t_rel": 555.61, "tag": "b3", "edge": {"rss_mb": 223.26, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.42, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298799.6461954, "t_rel": 560.62, "tag": "b3", "edge": {"rss_mb": 223.74, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 41.69, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298804.6517963, "t_rel": 565.62, "tag": "b3", "edge": {"rss_mb": 224.0, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 41.69, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298809.6574228, "t_rel": 570.63, "tag": "b3", "edge": {"rss_mb": 224.0, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.69, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298814.6630313, "t_rel": 575.64, "tag": "b3", "edge": {"rss_mb": 224.0, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.69, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298819.668662, "t_rel": 580.64, "tag": "b3", "edge": {"rss_mb": 224.0, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.69, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298824.6742818, "t_rel": 585.65, "tag": "b3", "edge": {"rss_mb": 224.0, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 41.69, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298829.679944, "t_rel": 590.65, "tag": "b3", "edge": {"rss_mb": 224.22, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.96, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298834.6856248, "t_rel": 595.66, "tag": "b3", "edge": {"rss_mb": 224.22, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 41.96, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298839.691264, "t_rel": 600.66, "tag": "b3", "edge": {"rss_mb": 224.22, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 41.96, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298844.6969333, "t_rel": 605.67, "tag": "b3", "edge": {"rss_mb": 224.22, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 41.96, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298849.7003136, "t_rel": 610.67, "tag": "b3", "edge": {"rss_mb": 224.22, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.96, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298854.7059197, "t_rel": 615.68, "tag": "b3", "edge": {"rss_mb": 224.22, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 41.96, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298859.7115345, "t_rel": 620.68, "tag": "b3", "edge": {"rss_mb": 224.02, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 42.23, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298864.7171204, "t_rel": 625.69, "tag": "b3", "edge": {"rss_mb": 222.71, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 42.23, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298869.7227113, "t_rel": 630.7, "tag": "b3", "edge": {"rss_mb": 223.52, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 42.23, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298874.728304, "t_rel": 635.7, "tag": "b3", "edge": {"rss_mb": 223.52, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 42.23, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298879.732292, "t_rel": 640.7, "tag": "b3", "edge": {"rss_mb": 223.24, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 42.5, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298884.7378988, "t_rel": 645.71, "tag": "b3", "edge": {"rss_mb": 223.24, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 42.77, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298889.7435045, "t_rel": 650.72, "tag": "b3", "edge": {"rss_mb": 223.04, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 42.77, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298894.7491105, "t_rel": 655.72, "tag": "b3", "edge": {"rss_mb": 223.3, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 42.77, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298899.754715, "t_rel": 660.73, "tag": "b3", "edge": {"rss_mb": 223.3, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 42.77, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298904.760299, "t_rel": 665.73, "tag": "b3", "edge": {"rss_mb": 223.06, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 43.04, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298909.765975, "t_rel": 670.74, "tag": "b3", "edge": {"rss_mb": 223.56, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 43.04, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298914.769198, "t_rel": 675.74, "tag": "b3", "edge": {"rss_mb": 224.1, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 43.04, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298919.7748528, "t_rel": 680.75, "tag": "b3", "edge": {"rss_mb": 224.1, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 43.04, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298924.7804515, "t_rel": 685.75, "tag": "b3", "edge": {"rss_mb": 224.1, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 43.04, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298929.7860692, "t_rel": 690.76, "tag": "b3", "edge": {"rss_mb": 224.1, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 43.04, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298934.7916818, "t_rel": 695.76, "tag": "b3", "edge": {"rss_mb": 223.77, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 43.59, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298939.7972877, "t_rel": 700.77, "tag": "b3", "edge": {"rss_mb": 221.37, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 43.59, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298944.8028934, "t_rel": 705.78, "tag": "b3", "edge": {"rss_mb": 222.46, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 43.59, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298949.8083417, "t_rel": 710.78, "tag": "b3", "edge": {"rss_mb": 223.01, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 43.59, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298954.81399, "t_rel": 715.79, "tag": "b3", "edge": {"rss_mb": 223.52, "cpu_pct": 3.6, "alive": true}, "dp": {"rss_mb": 43.59, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298959.819625, "t_rel": 720.79, "tag": "b3", "edge": {"rss_mb": 223.52, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 43.86, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298964.8252344, "t_rel": 725.8, "tag": "b3", "edge": {"rss_mb": 223.53, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 44.13, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298969.8309002, "t_rel": 730.8, "tag": "b3", "edge": {"rss_mb": 223.14, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 44.4, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298974.8365119, "t_rel": 735.81, "tag": "b3", "edge": {"rss_mb": 223.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 44.4, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298979.8421054, "t_rel": 740.81, "tag": "b3", "edge": {"rss_mb": 222.7, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 44.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298984.8477035, "t_rel": 745.82, "tag": "b3", "edge": {"rss_mb": 221.8, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 45.21, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781298989.853313, "t_rel": 750.83, "tag": "b3", "edge": {"rss_mb": 222.05, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 45.48, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298994.8589087, "t_rel": 755.83, "tag": "b3", "edge": {"rss_mb": 222.05, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 45.48, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781298999.8644805, "t_rel": 760.84, "tag": "b3", "edge": {"rss_mb": 222.05, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 45.48, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299004.8700814, "t_rel": 765.84, "tag": "b3", "edge": {"rss_mb": 223.49, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 45.48, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299009.8756766, "t_rel": 770.85, "tag": "b3", "edge": {"rss_mb": 221.7, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 45.48, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299014.8812833, "t_rel": 775.85, "tag": "b3", "edge": {"rss_mb": 221.37, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 45.48, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299019.8858624, "t_rel": 780.86, "tag": "b3", "edge": {"rss_mb": 221.26, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 45.48, "cpu_pct": 0.0, "alive": true}} +{"t_wall": 1781299024.890263, "t_rel": 785.86, "tag": "b3", "edge": {"rss_mb": 221.71, "cpu_pct": 4.6, "alive": true}, "dp": {"rss_mb": 45.75, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299029.8923144, "t_rel": 790.86, "tag": "b3", "edge": {"rss_mb": 222.53, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 45.75, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299034.8979428, "t_rel": 795.87, "tag": "b3", "edge": {"rss_mb": 222.53, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 46.02, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299039.9035568, "t_rel": 800.88, "tag": "b3", "edge": {"rss_mb": 222.53, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 46.02, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299044.9091892, "t_rel": 805.88, "tag": "b3", "edge": {"rss_mb": 222.53, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 46.29, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299049.9148262, "t_rel": 810.89, "tag": "b3", "edge": {"rss_mb": 222.35, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 46.29, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299054.9204693, "t_rel": 815.89, "tag": "b3", "edge": {"rss_mb": 222.56, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 46.56, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299059.9261074, "t_rel": 820.9, "tag": "b3", "edge": {"rss_mb": 222.24, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 46.56, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299064.9317412, "t_rel": 825.9, "tag": "b3", "edge": {"rss_mb": 222.09, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 46.83, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299069.9373527, "t_rel": 830.91, "tag": "b3", "edge": {"rss_mb": 222.09, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 46.83, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299074.942946, "t_rel": 835.92, "tag": "b3", "edge": {"rss_mb": 222.09, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 46.83, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299079.9485278, "t_rel": 840.92, "tag": "b3", "edge": {"rss_mb": 222.09, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 46.83, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299084.9541261, "t_rel": 845.93, "tag": "b3", "edge": {"rss_mb": 222.09, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 47.1, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299089.9597218, "t_rel": 850.93, "tag": "b3", "edge": {"rss_mb": 222.09, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 47.37, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299094.9653428, "t_rel": 855.94, "tag": "b3", "edge": {"rss_mb": 222.09, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 47.37, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299099.970949, "t_rel": 860.94, "tag": "b3", "edge": {"rss_mb": 221.44, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 47.37, "cpu_pct": 0.0, "alive": true}} +{"t_wall": 1781299104.9765527, "t_rel": 865.95, "tag": "b3", "edge": {"rss_mb": 221.97, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 47.64, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299109.9821467, "t_rel": 870.95, "tag": "b3", "edge": {"rss_mb": 221.97, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 47.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299114.9877467, "t_rel": 875.96, "tag": "b3", "edge": {"rss_mb": 222.05, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 47.91, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299119.993346, "t_rel": 880.97, "tag": "b3", "edge": {"rss_mb": 221.04, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 48.18, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299124.999005, "t_rel": 885.97, "tag": "b3", "edge": {"rss_mb": 221.83, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 48.18, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299130.0046177, "t_rel": 890.98, "tag": "b3", "edge": {"rss_mb": 222.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 48.18, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299135.0102174, "t_rel": 895.98, "tag": "b3", "edge": {"rss_mb": 222.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 48.18, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299140.0158057, "t_rel": 900.99, "tag": "b3", "edge": {"rss_mb": 222.4, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 48.99, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299145.021461, "t_rel": 905.99, "tag": "b3", "edge": {"rss_mb": 222.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 49.26, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299150.0270686, "t_rel": 911.0, "tag": "b3", "edge": {"rss_mb": 222.97, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 49.8, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299155.0326695, "t_rel": 916.0, "tag": "b3", "edge": {"rss_mb": 223.78, "cpu_pct": 4.99, "alive": true}, "dp": {"rss_mb": 49.8, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299160.038278, "t_rel": 921.01, "tag": "b3", "edge": {"rss_mb": 223.92, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 49.8, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299165.043883, "t_rel": 926.02, "tag": "b3", "edge": {"rss_mb": 223.07, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 50.07, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299170.0494988, "t_rel": 931.02, "tag": "b3", "edge": {"rss_mb": 223.62, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 50.07, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299175.0550883, "t_rel": 936.03, "tag": "b3", "edge": {"rss_mb": 222.92, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 50.34, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299180.0602982, "t_rel": 941.03, "tag": "b3", "edge": {"rss_mb": 221.16, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 50.34, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299185.0642858, "t_rel": 946.04, "tag": "b3", "edge": {"rss_mb": 219.87, "cpu_pct": 4.6, "alive": true}, "dp": {"rss_mb": 50.61, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299190.0698807, "t_rel": 951.04, "tag": "b3", "edge": {"rss_mb": 219.87, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 50.88, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299195.0755146, "t_rel": 956.05, "tag": "b3", "edge": {"rss_mb": 220.14, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 50.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299200.081105, "t_rel": 961.05, "tag": "b3", "edge": {"rss_mb": 220.14, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 50.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299205.086758, "t_rel": 966.06, "tag": "b3", "edge": {"rss_mb": 220.45, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 50.88, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299210.0923548, "t_rel": 971.06, "tag": "b3", "edge": {"rss_mb": 220.45, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 50.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299215.098003, "t_rel": 976.07, "tag": "b3", "edge": {"rss_mb": 220.45, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 50.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299220.1035953, "t_rel": 981.08, "tag": "b3", "edge": {"rss_mb": 220.7, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 50.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299225.1092045, "t_rel": 986.08, "tag": "b3", "edge": {"rss_mb": 218.96, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 50.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299230.114807, "t_rel": 991.09, "tag": "b3", "edge": {"rss_mb": 220.2, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 51.15, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299235.1204555, "t_rel": 996.09, "tag": "b3", "edge": {"rss_mb": 220.21, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 51.43, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299240.1261353, "t_rel": 1001.1, "tag": "b3", "edge": {"rss_mb": 221.16, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 51.7, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299245.1317523, "t_rel": 1006.1, "tag": "b3", "edge": {"rss_mb": 221.16, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 51.7, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299250.1373708, "t_rel": 1011.11, "tag": "b3", "edge": {"rss_mb": 221.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 51.7, "cpu_pct": 0.0, "alive": true}} +{"t_wall": 1781299255.1429753, "t_rel": 1016.12, "tag": "b3", "edge": {"rss_mb": 221.4, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 51.97, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299260.1485403, "t_rel": 1021.12, "tag": "b3", "edge": {"rss_mb": 221.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 52.24, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299265.1541352, "t_rel": 1026.13, "tag": "b3", "edge": {"rss_mb": 221.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 52.78, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299270.1597643, "t_rel": 1031.13, "tag": "b3", "edge": {"rss_mb": 222.25, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 53.05, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299275.16541, "t_rel": 1036.14, "tag": "b3", "edge": {"rss_mb": 222.58, "cpu_pct": 5.59, "alive": true}, "dp": {"rss_mb": 53.32, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299280.171018, "t_rel": 1041.14, "tag": "b3", "edge": {"rss_mb": 222.71, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 53.32, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299285.176635, "t_rel": 1046.15, "tag": "b3", "edge": {"rss_mb": 223.54, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 53.32, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299290.1803088, "t_rel": 1051.15, "tag": "b3", "edge": {"rss_mb": 223.13, "cpu_pct": 4.8, "alive": true}, "dp": {"rss_mb": 53.32, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299295.1859448, "t_rel": 1056.16, "tag": "b3", "edge": {"rss_mb": 221.98, "cpu_pct": 4.99, "alive": true}, "dp": {"rss_mb": 53.32, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299300.1915793, "t_rel": 1061.16, "tag": "b3", "edge": {"rss_mb": 222.25, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 53.59, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299305.197161, "t_rel": 1066.17, "tag": "b3", "edge": {"rss_mb": 222.85, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 53.59, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299310.2027981, "t_rel": 1071.18, "tag": "b3", "edge": {"rss_mb": 222.06, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 54.4, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299315.2083812, "t_rel": 1076.18, "tag": "b3", "edge": {"rss_mb": 223.95, "cpu_pct": 5.19, "alive": true}, "dp": {"rss_mb": 54.4, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299320.2140086, "t_rel": 1081.19, "tag": "b3", "edge": {"rss_mb": 223.57, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 54.4, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299325.2195919, "t_rel": 1086.19, "tag": "b3", "edge": {"rss_mb": 223.75, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 54.4, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299330.2251906, "t_rel": 1091.2, "tag": "b3", "edge": {"rss_mb": 223.03, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299335.2307603, "t_rel": 1096.2, "tag": "b3", "edge": {"rss_mb": 221.39, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299340.2363615, "t_rel": 1101.21, "tag": "b3", "edge": {"rss_mb": 222.02, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299345.2419434, "t_rel": 1106.21, "tag": "b3", "edge": {"rss_mb": 222.64, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299350.2475212, "t_rel": 1111.22, "tag": "b3", "edge": {"rss_mb": 222.62, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299355.2523243, "t_rel": 1116.22, "tag": "b3", "edge": {"rss_mb": 222.98, "cpu_pct": 5.0, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299360.2579157, "t_rel": 1121.23, "tag": "b3", "edge": {"rss_mb": 222.98, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299365.2660413, "t_rel": 1126.24, "tag": "b3", "edge": {"rss_mb": 223.66, "cpu_pct": 3.99, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299370.2716227, "t_rel": 1131.24, "tag": "b3", "edge": {"rss_mb": 223.66, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299375.2763236, "t_rel": 1136.25, "tag": "b3", "edge": {"rss_mb": 223.41, "cpu_pct": 4.6, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299380.281919, "t_rel": 1141.25, "tag": "b3", "edge": {"rss_mb": 223.41, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 54.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299385.2841055, "t_rel": 1146.26, "tag": "b3", "edge": {"rss_mb": 223.41, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 54.94, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299390.2897203, "t_rel": 1151.26, "tag": "b3", "edge": {"rss_mb": 223.79, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 55.21, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299395.295299, "t_rel": 1156.27, "tag": "b3", "edge": {"rss_mb": 222.88, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 55.87, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299400.3008754, "t_rel": 1161.27, "tag": "b3", "edge": {"rss_mb": 222.93, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 56.14, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299405.3035357, "t_rel": 1166.28, "tag": "b3", "edge": {"rss_mb": 223.17, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 56.68, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299410.3091264, "t_rel": 1171.28, "tag": "b3", "edge": {"rss_mb": 223.17, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 57.22, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299415.314733, "t_rel": 1176.29, "tag": "b3", "edge": {"rss_mb": 222.87, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 57.49, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299420.3203638, "t_rel": 1181.29, "tag": "b3", "edge": {"rss_mb": 222.86, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 57.76, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299425.3259969, "t_rel": 1186.3, "tag": "b3", "edge": {"rss_mb": 223.96, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 57.76, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299430.3315809, "t_rel": 1191.3, "tag": "b3", "edge": {"rss_mb": 223.96, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 57.76, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299435.3371632, "t_rel": 1196.31, "tag": "b3", "edge": {"rss_mb": 221.5, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 57.76, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299440.342748, "t_rel": 1201.32, "tag": "b3", "edge": {"rss_mb": 223.1, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 58.03, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299445.3483324, "t_rel": 1206.32, "tag": "b3", "edge": {"rss_mb": 223.36, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 58.3, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299450.3522983, "t_rel": 1211.32, "tag": "b3", "edge": {"rss_mb": 223.99, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 58.84, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299455.3562999, "t_rel": 1216.33, "tag": "b3", "edge": {"rss_mb": 223.28, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 58.84, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299460.361938, "t_rel": 1221.33, "tag": "b3", "edge": {"rss_mb": 220.75, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 58.84, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299465.367528, "t_rel": 1226.34, "tag": "b3", "edge": {"rss_mb": 220.23, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 58.84, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299470.3731148, "t_rel": 1231.35, "tag": "b3", "edge": {"rss_mb": 220.49, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 58.84, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299475.3787222, "t_rel": 1236.35, "tag": "b3", "edge": {"rss_mb": 223.01, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 58.84, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299480.3812294, "t_rel": 1241.35, "tag": "b3", "edge": {"rss_mb": 221.28, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 58.84, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299485.3868055, "t_rel": 1246.36, "tag": "b3", "edge": {"rss_mb": 219.24, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 58.84, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299490.3923929, "t_rel": 1251.36, "tag": "b3", "edge": {"rss_mb": 220.05, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 59.11, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299495.398002, "t_rel": 1256.37, "tag": "b3", "edge": {"rss_mb": 220.32, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 59.11, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299500.4035826, "t_rel": 1261.38, "tag": "b3", "edge": {"rss_mb": 220.59, "cpu_pct": 4.99, "alive": true}, "dp": {"rss_mb": 59.38, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299505.4091692, "t_rel": 1266.38, "tag": "b3", "edge": {"rss_mb": 220.86, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 59.65, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299510.414761, "t_rel": 1271.39, "tag": "b3", "edge": {"rss_mb": 221.4, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 59.92, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299515.4203515, "t_rel": 1276.39, "tag": "b3", "edge": {"rss_mb": 222.06, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 59.92, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299520.4259439, "t_rel": 1281.4, "tag": "b3", "edge": {"rss_mb": 222.06, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 59.92, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299525.4316103, "t_rel": 1286.4, "tag": "b3", "edge": {"rss_mb": 222.33, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 59.92, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299530.4372847, "t_rel": 1291.41, "tag": "b3", "edge": {"rss_mb": 222.33, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 60.19, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299535.4428656, "t_rel": 1296.42, "tag": "b3", "edge": {"rss_mb": 221.41, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 60.19, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299540.448453, "t_rel": 1301.42, "tag": "b3", "edge": {"rss_mb": 222.79, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 60.19, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299545.45408, "t_rel": 1306.43, "tag": "b3", "edge": {"rss_mb": 222.79, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 60.19, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299550.45971, "t_rel": 1311.43, "tag": "b3", "edge": {"rss_mb": 222.79, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 60.19, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299555.4653332, "t_rel": 1316.44, "tag": "b3", "edge": {"rss_mb": 222.79, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 60.19, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299560.469626, "t_rel": 1321.44, "tag": "b3", "edge": {"rss_mb": 221.95, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 60.19, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299565.4752126, "t_rel": 1326.45, "tag": "b3", "edge": {"rss_mb": 221.47, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 60.19, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299570.480348, "t_rel": 1331.45, "tag": "b3", "edge": {"rss_mb": 221.93, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 60.46, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299575.4859498, "t_rel": 1336.46, "tag": "b3", "edge": {"rss_mb": 221.93, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 60.46, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299580.4915469, "t_rel": 1341.46, "tag": "b3", "edge": {"rss_mb": 222.56, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 60.46, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299585.4971607, "t_rel": 1346.47, "tag": "b3", "edge": {"rss_mb": 222.56, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 60.46, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299590.502745, "t_rel": 1351.48, "tag": "b3", "edge": {"rss_mb": 222.76, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 60.73, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299595.5083046, "t_rel": 1356.48, "tag": "b3", "edge": {"rss_mb": 223.21, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 61.0, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299600.5138876, "t_rel": 1361.49, "tag": "b3", "edge": {"rss_mb": 222.61, "cpu_pct": 5.39, "alive": true}, "dp": {"rss_mb": 61.0, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299605.5194836, "t_rel": 1366.49, "tag": "b3", "edge": {"rss_mb": 222.91, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 61.0, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299610.5250723, "t_rel": 1371.5, "tag": "b3", "edge": {"rss_mb": 222.91, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 61.0, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299615.5306654, "t_rel": 1376.5, "tag": "b3", "edge": {"rss_mb": 222.91, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 61.27, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299620.536271, "t_rel": 1381.51, "tag": "b3", "edge": {"rss_mb": 223.09, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 61.54, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299625.5418606, "t_rel": 1386.51, "tag": "b3", "edge": {"rss_mb": 223.09, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 61.54, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299630.5436685, "t_rel": 1391.52, "tag": "b3", "edge": {"rss_mb": 223.09, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 61.81, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299635.5492573, "t_rel": 1396.52, "tag": "b3", "edge": {"rss_mb": 223.05, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 61.81, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299640.5548413, "t_rel": 1401.53, "tag": "b3", "edge": {"rss_mb": 222.92, "cpu_pct": 5.19, "alive": true}, "dp": {"rss_mb": 61.81, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299645.5602875, "t_rel": 1406.53, "tag": "b3", "edge": {"rss_mb": 222.92, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 61.81, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299650.5642836, "t_rel": 1411.54, "tag": "b3", "edge": {"rss_mb": 223.56, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 62.08, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299655.569877, "t_rel": 1416.54, "tag": "b3", "edge": {"rss_mb": 223.56, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 62.62, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299660.575468, "t_rel": 1421.55, "tag": "b3", "edge": {"rss_mb": 223.56, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 62.89, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299665.5810564, "t_rel": 1426.55, "tag": "b3", "edge": {"rss_mb": 223.56, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 63.16, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299670.5868988, "t_rel": 1431.56, "tag": "b3", "edge": {"rss_mb": 223.56, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 63.43, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299675.5924838, "t_rel": 1436.56, "tag": "b3", "edge": {"rss_mb": 223.7, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 63.43, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299680.5980828, "t_rel": 1441.57, "tag": "b3", "edge": {"rss_mb": 223.7, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 63.43, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299685.6037138, "t_rel": 1446.58, "tag": "b3", "edge": {"rss_mb": 223.7, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 63.71, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299690.6093488, "t_rel": 1451.58, "tag": "b3", "edge": {"rss_mb": 224.12, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 63.71, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299695.6149392, "t_rel": 1456.59, "tag": "b3", "edge": {"rss_mb": 222.37, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 63.71, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299700.6205568, "t_rel": 1461.59, "tag": "b3", "edge": {"rss_mb": 223.42, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 63.71, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299705.626166, "t_rel": 1466.6, "tag": "b3", "edge": {"rss_mb": 223.42, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 63.71, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299710.6317854, "t_rel": 1471.6, "tag": "b3", "edge": {"rss_mb": 224.4, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 63.71, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299715.6374252, "t_rel": 1476.61, "tag": "b3", "edge": {"rss_mb": 221.0, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 63.71, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299720.6430223, "t_rel": 1481.62, "tag": "b3", "edge": {"rss_mb": 221.54, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 63.98, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299725.6486137, "t_rel": 1486.62, "tag": "b3", "edge": {"rss_mb": 220.99, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 63.98, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299730.6541936, "t_rel": 1491.63, "tag": "b3", "edge": {"rss_mb": 221.53, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 63.98, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299735.6598074, "t_rel": 1496.63, "tag": "b3", "edge": {"rss_mb": 221.53, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 64.25, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299740.6654062, "t_rel": 1501.64, "tag": "b3", "edge": {"rss_mb": 221.53, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 64.52, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299745.6710021, "t_rel": 1506.64, "tag": "b3", "edge": {"rss_mb": 222.05, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 64.79, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299750.676574, "t_rel": 1511.65, "tag": "b3", "edge": {"rss_mb": 222.67, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 65.06, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299755.6821685, "t_rel": 1516.65, "tag": "b3", "edge": {"rss_mb": 222.94, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 65.06, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299760.6843023, "t_rel": 1521.66, "tag": "b3", "edge": {"rss_mb": 222.94, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 65.45, "cpu_pct": 0.6, "alive": true}} +{"t_wall": 1781299765.6898851, "t_rel": 1526.66, "tag": "b3", "edge": {"rss_mb": 222.7, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 65.52, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299770.6923008, "t_rel": 1531.66, "tag": "b3", "edge": {"rss_mb": 222.7, "cpu_pct": 4.6, "alive": true}, "dp": {"rss_mb": 65.72, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299775.697915, "t_rel": 1536.67, "tag": "b3", "edge": {"rss_mb": 223.51, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 65.85, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299780.7034988, "t_rel": 1541.68, "tag": "b3", "edge": {"rss_mb": 222.67, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 66.04, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299785.7091026, "t_rel": 1546.68, "tag": "b3", "edge": {"rss_mb": 222.02, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 66.04, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299790.7146845, "t_rel": 1551.69, "tag": "b3", "edge": {"rss_mb": 221.95, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 66.05, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299795.7203155, "t_rel": 1556.69, "tag": "b3", "edge": {"rss_mb": 221.95, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 66.08, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299800.7258945, "t_rel": 1561.7, "tag": "b3", "edge": {"rss_mb": 223.35, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 66.08, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299805.728302, "t_rel": 1566.7, "tag": "b3", "edge": {"rss_mb": 223.35, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 66.08, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299810.733365, "t_rel": 1571.71, "tag": "b3", "edge": {"rss_mb": 223.35, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 66.35, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299815.738955, "t_rel": 1576.71, "tag": "b3", "edge": {"rss_mb": 223.35, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 66.35, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299820.7446814, "t_rel": 1581.72, "tag": "b3", "edge": {"rss_mb": 224.17, "cpu_pct": 4.99, "alive": true}, "dp": {"rss_mb": 66.38, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299825.7502818, "t_rel": 1586.72, "tag": "b3", "edge": {"rss_mb": 224.17, "cpu_pct": 3.8, "alive": true}, "dp": {"rss_mb": 66.38, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299830.752328, "t_rel": 1591.72, "tag": "b3", "edge": {"rss_mb": 221.72, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 66.38, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299835.7562993, "t_rel": 1596.73, "tag": "b3", "edge": {"rss_mb": 221.99, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 66.6, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299840.761899, "t_rel": 1601.73, "tag": "b3", "edge": {"rss_mb": 222.59, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 66.6, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299845.7642963, "t_rel": 1606.74, "tag": "b3", "edge": {"rss_mb": 223.75, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 66.6, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299850.769878, "t_rel": 1611.74, "tag": "b3", "edge": {"rss_mb": 220.95, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 66.86, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299855.775466, "t_rel": 1616.75, "tag": "b3", "edge": {"rss_mb": 219.2, "cpu_pct": 4.0, "alive": true}, "dp": {"rss_mb": 66.86, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299860.7810626, "t_rel": 1621.75, "tag": "b3", "edge": {"rss_mb": 218.47, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 67.02, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299865.7866688, "t_rel": 1626.76, "tag": "b3", "edge": {"rss_mb": 219.1, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 67.22, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299870.7883105, "t_rel": 1631.76, "tag": "b3", "edge": {"rss_mb": 219.1, "cpu_pct": 8.2, "alive": true}, "dp": {"rss_mb": 67.45, "cpu_pct": 0.6, "alive": true}} +{"t_wall": 1781299875.7923172, "t_rel": 1636.76, "tag": "b3", "edge": {"rss_mb": 220.16, "cpu_pct": 7.99, "alive": true}, "dp": {"rss_mb": 67.57, "cpu_pct": 0.6, "alive": true}} +{"t_wall": 1781299880.7963054, "t_rel": 1641.77, "tag": "b3", "edge": {"rss_mb": 222.49, "cpu_pct": 10.39, "alive": true}, "dp": {"rss_mb": 68.28, "cpu_pct": 0.6, "alive": true}} +{"t_wall": 1781299885.8003097, "t_rel": 1646.77, "tag": "b3", "edge": {"rss_mb": 222.62, "cpu_pct": 8.59, "alive": true}, "dp": {"rss_mb": 68.67, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299890.8043082, "t_rel": 1651.78, "tag": "b3", "edge": {"rss_mb": 222.62, "cpu_pct": 8.19, "alive": true}, "dp": {"rss_mb": 68.73, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299895.8083167, "t_rel": 1656.78, "tag": "b3", "edge": {"rss_mb": 223.71, "cpu_pct": 8.39, "alive": true}, "dp": {"rss_mb": 69.21, "cpu_pct": 0.6, "alive": true}} +{"t_wall": 1781299900.8123126, "t_rel": 1661.78, "tag": "b3", "edge": {"rss_mb": 224.47, "cpu_pct": 7.99, "alive": true}, "dp": {"rss_mb": 69.21, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299905.8163192, "t_rel": 1666.79, "tag": "b3", "edge": {"rss_mb": 224.47, "cpu_pct": 7.19, "alive": true}, "dp": {"rss_mb": 69.32, "cpu_pct": 0.6, "alive": true}} +{"t_wall": 1781299910.820304, "t_rel": 1671.79, "tag": "b3", "edge": {"rss_mb": 221.73, "cpu_pct": 7.59, "alive": true}, "dp": {"rss_mb": 69.32, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299915.8243074, "t_rel": 1676.8, "tag": "b3", "edge": {"rss_mb": 222.24, "cpu_pct": 7.19, "alive": true}, "dp": {"rss_mb": 69.32, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299920.8305993, "t_rel": 1681.8, "tag": "b3", "edge": {"rss_mb": 222.99, "cpu_pct": 5.59, "alive": true}, "dp": {"rss_mb": 69.35, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299925.836243, "t_rel": 1686.81, "tag": "b3", "edge": {"rss_mb": 222.99, "cpu_pct": 4.99, "alive": true}, "dp": {"rss_mb": 69.55, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299930.8372295, "t_rel": 1691.81, "tag": "b3", "edge": {"rss_mb": 222.99, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 69.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299935.8403018, "t_rel": 1696.81, "tag": "b3", "edge": {"rss_mb": 223.26, "cpu_pct": 4.8, "alive": true}, "dp": {"rss_mb": 69.67, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299940.8443258, "t_rel": 1701.82, "tag": "b3", "edge": {"rss_mb": 223.78, "cpu_pct": 5.0, "alive": true}, "dp": {"rss_mb": 69.88, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299945.8482873, "t_rel": 1706.82, "tag": "b3", "edge": {"rss_mb": 223.01, "cpu_pct": 5.4, "alive": true}, "dp": {"rss_mb": 70.12, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299950.8523018, "t_rel": 1711.82, "tag": "b3", "edge": {"rss_mb": 223.45, "cpu_pct": 5.0, "alive": true}, "dp": {"rss_mb": 70.12, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299955.8579662, "t_rel": 1716.83, "tag": "b3", "edge": {"rss_mb": 223.86, "cpu_pct": 6.39, "alive": true}, "dp": {"rss_mb": 70.12, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299960.8636005, "t_rel": 1721.84, "tag": "b3", "edge": {"rss_mb": 223.34, "cpu_pct": 5.79, "alive": true}, "dp": {"rss_mb": 70.12, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299965.868304, "t_rel": 1726.84, "tag": "b3", "edge": {"rss_mb": 223.34, "cpu_pct": 6.19, "alive": true}, "dp": {"rss_mb": 70.12, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299970.8723028, "t_rel": 1731.84, "tag": "b3", "edge": {"rss_mb": 224.21, "cpu_pct": 6.0, "alive": true}, "dp": {"rss_mb": 70.37, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299975.8763108, "t_rel": 1736.85, "tag": "b3", "edge": {"rss_mb": 222.08, "cpu_pct": 6.59, "alive": true}, "dp": {"rss_mb": 70.37, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299980.880309, "t_rel": 1741.85, "tag": "b3", "edge": {"rss_mb": 221.07, "cpu_pct": 7.39, "alive": true}, "dp": {"rss_mb": 70.37, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781299985.886184, "t_rel": 1746.86, "tag": "b3", "edge": {"rss_mb": 220.01, "cpu_pct": 4.39, "alive": true}, "dp": {"rss_mb": 70.37, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299990.8917782, "t_rel": 1751.86, "tag": "b3", "edge": {"rss_mb": 221.1, "cpu_pct": 4.99, "alive": true}, "dp": {"rss_mb": 70.37, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781299995.894805, "t_rel": 1756.87, "tag": "b3", "edge": {"rss_mb": 221.1, "cpu_pct": 4.2, "alive": true}, "dp": {"rss_mb": 70.37, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781300000.9004304, "t_rel": 1761.87, "tag": "b3", "edge": {"rss_mb": 221.57, "cpu_pct": 4.4, "alive": true}, "dp": {"rss_mb": 70.5, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781300005.9043026, "t_rel": 1766.88, "tag": "b3", "edge": {"rss_mb": 222.27, "cpu_pct": 5.0, "alive": true}, "dp": {"rss_mb": 71.02, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781300010.9099016, "t_rel": 1771.88, "tag": "b3", "edge": {"rss_mb": 222.27, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 71.02, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781300015.915527, "t_rel": 1776.89, "tag": "b3", "edge": {"rss_mb": 222.27, "cpu_pct": 4.59, "alive": true}, "dp": {"rss_mb": 71.29, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781300020.921167, "t_rel": 1781.89, "tag": "b3", "edge": {"rss_mb": 222.78, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 71.29, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781300025.9267986, "t_rel": 1786.9, "tag": "b3", "edge": {"rss_mb": 222.78, "cpu_pct": 5.39, "alive": true}, "dp": {"rss_mb": 71.29, "cpu_pct": 0.0, "alive": true}} +{"t_wall": 1781300030.9283066, "t_rel": 1791.9, "tag": "b3", "edge": {"rss_mb": 223.54, "cpu_pct": 5.2, "alive": true}, "dp": {"rss_mb": 71.81, "cpu_pct": 0.4, "alive": true}} +{"t_wall": 1781300035.934245, "t_rel": 1796.91, "tag": "b3", "edge": {"rss_mb": 223.1, "cpu_pct": 4.79, "alive": true}, "dp": {"rss_mb": 72.08, "cpu_pct": 0.2, "alive": true}} +{"t_wall": 1781300040.9363008, "t_rel": 1801.91, "tag": "b3", "edge": {"rss_mb": 223.67, "cpu_pct": 5.0, "alive": true}, "dp": {"rss_mb": 72.08, "cpu_pct": 0.4, "alive": true}} diff --git a/datasets_eval/soak/soak_RESULTS.md b/datasets_eval/soak/soak_RESULTS.md new file mode 100644 index 000000000..2dad07a73 --- /dev/null +++ b/datasets_eval/soak/soak_RESULTS.md @@ -0,0 +1,148 @@ +# §6 Edge soak — CPU/RSS bounded-ness + leak slope (Fig 6) + +Measured on the **real ASAP stack** (cold-OFF single-host), feeding the real +Google-2019 cluster trace (`/tmp/gct-otlp.jsonl`, the mapper output of +`datasets_eval/google_cluster/run.py map --year 2019`, 200 000 rows, metric +`google_cluster_2019_cpu_rate` + `_memory_usage`) at a **fixed 5000 pts/s** +through `otel-app/loadgen → asap-otel edge → data_plane`. + +Branch: `feat/edge-soak`. Main untouched. No git worktree was used (worked +in-place on the existing built checkout so the patched OTel-contrib build / +docker images did not have to be rebuilt). + +## Setup (honest scope) + +- **Single-node loopback.** All three containers (`asap-data-plane`, + `asap-control-plane`, `asap-agent-a`) run `--network host` on node0 via + `datasets_eval/soak/stack-coldoff.sh` (copied from `feat/multisketch-accuracy`). + No network hop, no multi-node fan-in. +- **Cold tier OFF** (no MinIO/Thanos/gorilla-merger; data_plane registers the + NoDataArchiveEngine stub). Warm `SketchStore` is the only live engine, so + everything we measure is the warm path. +- **Ingest rate:** 5000 data points/sec, constant, generated by + `loadgen.py` looping the 200k-row trace and re-basing every batch's + timestamps to "now" (so windows keep closing on fresh data). b3 soak + delivered **9,500,000 points over 1900 s at 5000 pts/s, 0 gRPC errors**. +- **Soak duration: 30 min (1800 s) of steady ingest.** A 24 h soak is OUT OF + SCOPE; the 24 h numbers below are **linear extrapolations** of the measured + 30-min slope and are labelled as such. +- **Sampling:** `sampler.py` reads `/proc//{stat,status}` for the edge + (`asap-otel`) and `data_plane` host PIDs. b0 arm @ 3 s × 180 s (60 samples); + b3 soak @ 5 s × 1800 s (360 samples). data_plane *also* self-reports RSS + + `SketchStore` sid count every 30 s via its `[MEMORY_DIAG]` log line (65 + lines captured) — used as an independent cross-check of the /proc RSS. + +### Arms + +- **sketch (b3):** the normal fused edge — `asap_edge` with + `google_cluster_2019_cpu_rate → DDSketch` (per-series quantile, α=0.01, + warm) + `google_cluster_2019_memory_usage → Sum aggregate_by [zone]` + (warm). Config: `asap-otel-agent-gct-b3.yaml`. **This is the + DDSketch + Sum warm path.** +- **raw-forward (b0):** MEASURED. A plain collector pipeline + `otlp → memory_limiter → batch → otlp/backend` with **no `asap_edge`**, + same image, same receiver, same backend endpoint (`data-plane:14317`), + same 5000 pts/s. Config: `asap-otel-agent-gct-b0.yaml`. Isolates the edge + cost of sketch aggregation vs pure passthrough. + +## (a) Edge & data_plane CPU / RSS + +CPU% is normalised to one core (100% = one busy core). + +| arm | mean CPU% | p99 CPU% | steady RSS | n | +|---|---|---|---|---| +| **b0 raw-forward edge** (asap-otel) | 2.75 | 3.33 | **207 MB** | 60 | +| **b3 sketch edge** (asap-otel), first 180 s | 4.24 | 5.19 | **223 MB** | 35 | +| b3 sketch edge, full 30-min soak | 4.38 | 8.19 | 223 MB | 360 | +| b0 data_plane | 7.69 | 8.13 | 28 MB | 60 | +| b3 data_plane, first 180 s | 0.24 | 0.53 | 29 MB | 35 | +| b3 data_plane, full 30-min soak | 0.26 | 0.60 | 25→72 MB (climbing) | 360 | + +Edge takeaways: +- The **sketch edge costs ~1.6× the CPU** of raw-forward (mean 4.4% vs 2.8% + of one core) and **~16 MB more RSS** (223 vs 207 MB) at 5000 pts/s. Both + are tiny in absolute terms — the edge is comfortably bounded on one node. +- The b3 edge p99 (8.2%) > raw-forward p99 (3.3%): the per-window DDSketch + seal/serialize/ship work shows up as brief CPU spikes on the 30 s flush + tick, but RSS does not move with them. +- data_plane CPU is higher in b0 (7.7%) than b3 (0.26%): in b0 the backend + ingests the **full raw 1004-series stream**, whereas in b3 it ingests a few + compact sketch/Sum envelopes per window — the aggregation gain is real and + visible at the backend CPU. + +## (b) Leak slope (RSS over time, fitted over the tail half of the soak) + +| process | source | slope (MB/h) | verdict | 24 h extrapolation* | +|---|---|---|---|---| +| **edge** (asap-otel, b3) | /proc | **+2.6** | **BOUNDED** (≈0, plateaus at ~223 MB) | +63 MB | +| **data_plane** (b3) | /proc | **+89.9** | **CLIMBING (monotone)** | +2.16 GB | +| **data_plane** (b3) | self-report MEMORY_DIAG | **+85.2** | **CLIMBING (monotone)** | +2.05 GB | + +\* 24 h figures are **linear extrapolations of the measured 30-min slope**, +not measured. They assume the per-sid growth below does not saturate, which it +may once each sketch's bins fill — treat as a worst-case lower bound on a +real leak, not a guarantee of +2 GB/day. + +- **Edge: BOUNDED.** RSS sits at ~218–224 MB for the whole 30 min with no + trend (see `rss_over_time.png`, top panel, blue). The memory_limiter + + per-shard window flush keep the edge flat under sustained load. +- **data_plane: CLIMBING, monotone, linear.** RSS rose 25 MB → 72 MB over + 30 min, ~+89 MB/h, no sign of a plateau within the window. The two + independent RSS sources (/proc and the binary's own MEMORY_DIAG gauge) + agree within ~5% (89.9 vs 85.2 MB/h), so this is a real backend climb, not + a measurement artefact. + +## Prior finding ([[gct-memory-findings]], 2026-05-28): reproduce or refute? + +Prior claim: *data_plane retains stale sketch sids (~600 MiB flat at idle, +didn't release) and `approx_memory_bytes` under-reports ~10000× so +back-pressure is blind.* + +**Partially reproduces — with a refined mechanism:** + +- **Stale-sid COUNT does NOT grow.** `SketchStore` sids plateau hard at + **1004** (the mapper's cardinality cap) within the first ~90 s and stay + flat for the entire soak (bottom panel, dotted red line). So the + "sid-count keeps climbing" reading of the prior finding does **not** + reproduce here — the per-shard `max_series` cap and the fixed-cardinality + trace bound the sid set. +- **But per-sid STATE grows unbounded, and that is what climbs the RSS.** + The MEMORY_DIAG `payload` for those same 1004 sids rose + **257 KB → 37,574 KB (≈146×)** over 30 min — i.e. each DDSketch's bin set + keeps accreting as new values land in the same sids window after window, + and the warm store never trims it back. data_plane RSS tracks this payload + growth almost 1:1. So the **backend memory climb the prior note flagged is + real and reproduces**; the driver in this cold-OFF/capped-cardinality run + is *per-sid sketch-state growth*, not *sid-count growth*. +- **The retention is BACKEND-SIDE, not edge-side.** The edge RSS is flat; only + data_plane climbs. Confirmed by sampling both processes. +- **`approx_memory_bytes` under-reporting:** MEMORY_DIAG still labels the 37 MB + of `payload` as "evictable (flusher gauge)" while it is plainly resident and + not being evicted under steady load — consistent with the prior note that + the back-pressure accounting under-reports and would not trigger. Not + separately quantified here (no idle-release phase was run), so this part is + *consistent-with* rather than *re-measured*. + +## Bottom line + +- **Edge is bounded** in both CPU and RSS; sketch (b3) costs ~1.6× CPU and + +16 MB RSS over raw-forward (b0) — cheap, flat, no leak (+2.6 MB/h ≈ 0). +- **data_plane is the memory risk:** monotone ~+87 MB/h climb under steady + load. NOT from unbounded sid count (that's capped at 1004) but from + unbounded per-sid warm-sketch state, which the flusher gauge does not + reclaim under continuous ingest. Extrapolated ~+2 GB/24h (linear, worst + case). This is backend-side, matching and refining the 2026-05-28 finding. + +## Files + +- `stack-coldoff.sh` — stack bring-up (from `feat/multisketch-accuracy`) +- `asap-otel-agent-gct-b3.yaml` — sketch edge config (DDSketch + Sum) +- `asap-otel-agent-gct-b0.yaml` — raw-forward edge config +- `loadgen.py` — constant-rate OTLP/gRPC trace loop +- `sampler.py` — /proc RSS+CPU sampler +- `analyze.py` — stats + slope fit + plot +- `samples_b0.jsonl`, `samples_b3_soak.jsonl` — raw /proc samples +- `dp_memdiag_b3.log` — data_plane self-reported RSS + sid count (30 s cadence) +- `loadgen_b0.log`, `loadgen_b3.log` — load generator logs +- `summary.json` — machine-readable results +- `rss_over_time.png` — Fig 6 diff --git a/datasets_eval/soak/stack-coldoff.sh b/datasets_eval/soak/stack-coldoff.sh new file mode 100755 index 000000000..116772f04 --- /dev/null +++ b/datasets_eval/soak/stack-coldoff.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# stack-coldoff.sh — MINIMAL cold/archive-OFF single-host ASAP stack for the +# multi-sketch family ACCURACY eval (feat/multisketch-accuracy). +# +# Difference vs stack.sh: NO MinIO / Thanos / gorilla-merger cold tier, and +# the data-plane is started WITHOUT `ASAP_THANOS_QUERY_URL` (so the binary +# registers the NoDataArchiveEngine stub — there is no real archive engine). +# The agent runs with `cold: {enabled: false}` (no gorillas3 ship). This is +# the warm-only path the Pareto/gct DDSketch accuracy runs used: recent +# range-selector queries (quantile_over_time(...[Ns]), count_over_time(...)) +# stay on the warm SketchStore instead of being forwarded to the (empty) +# archive. +# +# Components (all --network host on node0): +# data-plane OTLP ingest :14317/14318, query :9091 (cold OFF, no thanos) +# control-plane HTTP/OpAMP/gRPC :8080/4320/4321 (POSTs backend streaming-config) +# agent (bare fused asap_edge) OTLP receiver :4317/4318 (replay target), +# metrics :8890 +# +# Usage: +# stack-coldoff.sh up +# stack-coldoff.sh down +# stack-coldoff.sh ps +set -uo pipefail + +ROOT=/mydata/ASAPCollector +CFG=${ROOT}/deploy/mvp-multinode/configs +WORKDIR=/mydata/mvp-multinode +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +H=127.0.0.1 +ADD_HOSTS=( + --add-host=control-plane:${H} --add-host=data-plane:${H} + --add-host=agent-a:${H} +) + +DR() { docker run -d --restart no --network host "${ADD_HOSTS[@]}" "$@"; } +log(){ printf '[stack-coldoff %s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; } + +down() { + docker ps -a --format '{{.Names}}' | grep '^asap-' | xargs -r docker rm -f >/dev/null 2>&1 + log "all asap-* containers removed" +} + +wipe_state() { + rm -rf "${WORKDIR}"/data/sketch-persistence/* 2>/dev/null \ + || sudo rm -rf "${WORKDIR}"/data/sketch-persistence/* 2>/dev/null || true + mkdir -p "${WORKDIR}"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/data/sketch-persistence "${WORKDIR}"/configs +} + +up() { + local workload=$1 agentcfg=$2 + down; wipe_state + + mkdir -p "${WORKDIR}"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/configs/asap "${WORKDIR}"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/configs/shared + cp "${CFG}/asap/backend-streaming.yaml" "${WORKDIR}/configs/asap/" + cp "${CFG}/asap/backend-storage-routing.yaml" "${WORKDIR}/configs/asap/" + cp "${workload}" "${WORKDIR}/configs/asap/eval-workload.yaml" + cp "${agentcfg}" "${WORKDIR}/configs/asap/eval-agent.yaml" + + log "data-plane up (cold OFF — NO ASAP_THANOS_QUERY_URL; OTLP :14317, query :9091)" + # NOTE: no ASAP_THANOS_QUERY_URL, no ASAP_GORILLA_S3_* → the binary + # registers the NoDataArchiveEngine stub; warm SketchStore is the only + # live engine. Persistence disabled (in-memory warm sketches stay resident + # and queryable for the whole eval window). + DR --name asap-data-plane --cpus=8 \ + -e RUST_LOG="${DP_RUST_LOG:-info}" -e ASAP_SKETCH_FAMILY=ddsketch \ + -e ASAP_BACKEND_STORAGE_ROUTING=/etc/asap/backend-storage-routing.yaml \ + -v "${WORKDIR}/configs/asap/backend-streaming.yaml:/etc/asap/streaming.yaml:ro" \ + -v "${WORKDIR}/configs/asap/backend-storage-routing.yaml:/etc/asap/backend-storage-routing.yaml:ro" \ + -v "${WORKDIR}/data/sketch-persistence:/data/sketch-persistence" \ + asap/data-plane:dev \ + --streaming-config=/etc/asap/streaming.yaml --query-port=9091 \ + --enable-otel-ingest --otel-grpc-port=14317 --otel-http-port=14318 >/dev/null + sleep 4 + + log "control-plane up (workload=$(basename "${workload}"), backend OTLP port 14317)" + DR --name asap-control-plane --cpus=2 \ + -e RUST_LOG="info,controller=debug,control_plane=debug" \ + -e USE_TYPED_STAGE_SPLIT=1 -e ASAP_EDGE_FUSED=1 \ + -e ASAP_EDGE_BACKEND_OTLP_PORT=14317 \ + -e CONTROLLER_ADDR=0.0.0.0:8080 -e CONTROLLER_OPAMP_ADDR=0.0.0.0:4320 \ + -e CONTROLLER_GRPC_ADDR=0.0.0.0:4321 \ + -e CONTROLLER_OPAMP_ENDPOINT=ws://control-plane:4320/v1/opamp \ + -e CONTROLLER_BACKEND_ENDPOINT=http://data-plane:9091/api/v1/streaming-config \ + -e CONTROLLER_WORKLOADS=/etc/asap/eval-workload.yaml \ + -v "${WORKDIR}/configs/asap/eval-workload.yaml:/etc/asap/eval-workload.yaml:ro" \ + asap/control-plane:dev >/dev/null + sleep 5 + + log "agent (bare, static fused asap_edge, cold OFF) up — OTLP receiver :4317 for replay" + DR --name asap-agent-a --cpus=8 --hostname agent-a \ + -e AGENT_ID=agent-a \ + -v "${WORKDIR}/configs/asap/eval-agent.yaml:/etc/otel/config.yaml:ro" \ + asap/asap-otel:dev --config=/etc/otel/config.yaml >/dev/null + sleep 6 + log "stack up. containers:"; docker ps --format ' {{.Names}}\t{{.Status}}' | grep asap- >&2 +} + +case "${1:-}" in + up) up "${2:?need workload}" "${3:?need agentcfg}" ;; + down) down ;; + ps) docker ps --format '{{.Names}}\t{{.Status}}' | grep asap- ;; + *) echo "usage: stack-coldoff.sh up | down | ps" >&2; exit 2 ;; +esac diff --git a/datasets_eval/soak/summary.json b/datasets_eval/soak/summary.json new file mode 100644 index 000000000..cae4c7834 --- /dev/null +++ b/datasets_eval/soak/summary.json @@ -0,0 +1,72 @@ +{ + "arms": { + "b0_raw_forward_edge": { + "n": 60, + "mean_cpu_pct": 2.75, + "p99_cpu_pct": 3.33, + "steady_rss_mb": 206.8, + "rss_min_mb": 202.9, + "rss_max_mb": 207.4 + }, + "b0_data_plane": { + "n": 60, + "mean_cpu_pct": 7.69, + "p99_cpu_pct": 8.13, + "steady_rss_mb": 28.1, + "rss_min_mb": 27.8, + "rss_max_mb": 28.1 + }, + "b3_sketch_edge_first180s": { + "n": 35, + "mean_cpu_pct": 4.24, + "p99_cpu_pct": 5.19, + "steady_rss_mb": 223.1, + "rss_min_mb": 218.7, + "rss_max_mb": 223.8 + }, + "b3_data_plane_first180s": { + "n": 35, + "mean_cpu_pct": 0.24, + "p99_cpu_pct": 0.53, + "steady_rss_mb": 28.7, + "rss_min_mb": 25.5, + "rss_max_mb": 29.8 + }, + "b3_sketch_edge_fullsoak": { + "n": 360, + "mean_cpu_pct": 4.38, + "p99_cpu_pct": 8.19, + "steady_rss_mb": 222.7, + "rss_min_mb": 218.5, + "rss_max_mb": 224.5 + }, + "b3_data_plane_fullsoak": { + "n": 360, + "mean_cpu_pct": 0.26, + "p99_cpu_pct": 0.6, + "steady_rss_mb": 60.9, + "rss_min_mb": 25.5, + "rss_max_mb": 72.1 + } + }, + "soak": { + "duration_s": 1801.9, + "duration_h": 0.499, + "edge_rss_slope_mb_per_h": 2.63, + "edge_verdict": "BOUNDED (slope +2.63 MB/h ~= 0; 24h extrap +63.1 MB)", + "dp_rss_slope_mb_per_h": 89.9, + "dp_verdict": "CLIMBING (slope +89.90 MB/h; 24h extrap +2157.6 MB)" + }, + "data_plane_memdiag": { + "n": 65, + "sids_min": 512, + "sids_max": 1004, + "sids_final": 1004, + "payload_kb_final": 37573.83, + "rss_mb_min": 17.8, + "rss_mb_max": 70.3, + "rss_mb_final": 70.3, + "rss_slope_mb_per_h": 85.21, + "rss_verdict": "CLIMBING (slope +85.21 MB/h; 24h extrap +2045.0 MB)" + } +} \ No newline at end of file diff --git a/deploy/docker/Dockerfile.otel-app b/deploy/docker/Dockerfile.otel-app index faa4356df..9a0934d4a 100644 --- a/deploy/docker/Dockerfile.otel-app +++ b/deploy/docker/Dockerfile.otel-app @@ -47,10 +47,24 @@ COPY opentelemetry-go /src/opentelemetry-go COPY opentelemetry-proto /src/opentelemetry-proto COPY otel-app /src/otel-app +# otel-app/sample_controller.go imports asap-precompute-go/monitor{,/grpcclient} +# (the coordinated-sampling client), and otel-app/go.mod replaces them at +# ../asap-precompute-go{,/monitor/grpcclient}. That sibling tree lives outside +# this Dockerfile's default context, so supply it as a named build context and +# COPY it to /src/asap-precompute-go (the `../` replace then resolves verbatim). +# docker build --build-context asap-precompute-go=/path/to/asap-precompute-go ... +COPY --from=asap-precompute-go . /src/asap-precompute-go + # Sketchlib-go pulled via named build context to avoid a submodule # hop outside this repo. COPY --from=sketchlib-go . /src/sketchlib-go +# asap-precompute-go (monitor.Engine + grpcclient, used by the coordinated +# sampling edge) pulled via named build context. go.mod replaces it with +# ../asap-precompute-go; from /src/otel-app that resolves to /src/asap-precompute-go, +# so copy it there — no replace rewrite needed. +COPY --from=asap-precompute-go . /src/asap-precompute-go + # The go.mod replace references ../../sketchlib-go (the local checkout sits at # the workspace root, one level above the repo root). Inside /src it was # COPY'd to /src/sketchlib-go, so rewrite the replace to that absolute path. diff --git a/deploy/mvp-multinode/configs/asap/gos-aniso-agent.yaml.tmpl b/deploy/mvp-multinode/configs/asap/gos-aniso-agent.yaml.tmpl new file mode 100644 index 000000000..c17572799 --- /dev/null +++ b/deploy/mvp-multinode/configs/asap/gos-aniso-agent.yaml.tmpl @@ -0,0 +1,75 @@ +# GOS per-cell threshold eval — minimal agent config template. +# Rendered by scripts/gos_aniso_cluster.sh: __ANISO__ → true|false, +# __WARM_IP__ → the sink node's IP, __EDGE_ID__ → arm label. +# +# One CountSketch metric (top_endpoint_qps, Zipf endpoint label from the +# otel-app five-sketch workload), delta transmission gated by the GOS +# norm-adaptive threshold: isotropic scalar vs anisotropic per-cell {T_j} +# (the arm under test). The filter keeps every other metric off the wire so +# the node2 port-4317 byte counter measures ONLY this metric's delta frames. +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + max_recv_msg_size_mib: 4096 + +processors: + memory_limiter: + check_interval: 1s + limit_mib: 1280 + spike_limit_mib: 256 + + # Keep ONLY the metric under test: unconfigured metrics would otherwise + # passthrough raw (drop_original forwards unmatched metrics) and pollute the + # byte measurement. + filter/only_countsketch: + metrics: + include: + match_type: strict + metric_names: + - top_endpoint_qps + + cumulativetodelta: + include: + metrics: + - top_endpoint_qps + match_type: strict + + asap_edge: + shard_count: 4 + window_duration: 15s + edge_id: __EDGE_ID__ + drop_original: true + metrics: + - metric: top_endpoint_qps + family: countsketch + tier: warm + rows: 5 + cols: 2048 + delta_transmission: true + item_label: endpoint + # GOS norm-adaptive relative delta gate (the knobs under test). Note: + # emit_heap must stay OFF — the anisotropic per-cell path is gated on + # the non-heap wire format. + gos_delta_epsilon: 0.1 + gos_sites: 4 + gos_anisotropic: __ANISO__ + cold: + enabled: false + +exporters: + otlp/backend: + endpoint: __WARM_IP__:4317 + tls: + insecure: true + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [memory_limiter, filter/only_countsketch, cumulativetodelta, asap_edge] + exporters: [otlp/backend] + telemetry: + logs: + level: info diff --git a/deploy/mvp-multinode/configs/asap/gos-eval-sink.yaml b/deploy/mvp-multinode/configs/asap/gos-eval-sink.yaml new file mode 100644 index 000000000..37f8102df --- /dev/null +++ b/deploy/mvp-multinode/configs/asap/gos-eval-sink.yaml @@ -0,0 +1,22 @@ +# GOS per-cell eval — OTLP sink for the WARM node: accept + ACK everything, +# store nothing. The byte measurement happens in the kernel (iptables dport +# 4317 counter); the sink only needs to keep the gRPC stream healthy so the +# agent's exporter never backs off/retries (which would distort bytes). +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + max_recv_msg_size_mib: 4096 + +exporters: + nop: + +service: + pipelines: + metrics: + receivers: [otlp] + exporters: [nop] + telemetry: + logs: + level: warn diff --git a/deploy/mvp-multinode/configs/asap/mvp-workload-fig9-f2.yaml b/deploy/mvp-multinode/configs/asap/mvp-workload-fig9-f2.yaml new file mode 100644 index 000000000..757ebfec1 --- /dev/null +++ b/deploy/mvp-multinode/configs/asap/mvp-workload-fig9-f2.yaml @@ -0,0 +1,242 @@ +# MVP demo controller input — three canonical query classes +# +# Read by the controller at startup (env: CONTROLLER_WORKLOADS, +# default ./workloads.yaml). The controller's L1 query_language → +# L5 stage_split pipeline plans sketches + stage placement based +# on this workload. +# +# Schema mirrors deploy/mvp-singlenode/configs/workloads.yaml. See +# controller/src/config/workloads.rs::WorkloadEntry. +# +# Each entry is one PromQL query class the demo will exercise. +# +# ── Operating-point assumptions for the bandwidth verdict ───────── +# +# Agents run at 10 Hz scrape (`-freq-hz=10`, see +# `deploy/mvp-singlenode/docker-compose/base.yml` and `mvp-multi-stage.yml`); each +# series therefore produces 600 samples per 60 s flush window. The +# break-even against raw scrape depends on `samples_per_window` +# (≈ `state_size_bytes / per_sample_raw_bytes`); at 600 samples the +# four delta-capable families (DDSketch / HLL / CountSketch / +# Count-Min) sit above or close to their break-even curves. +# +# The controller's plan emitter +# (`controller/src/config/stage_config.rs::build_edge_processor_block`) +# accordingly sets `delta_transmission: true` for those four families +# in the wire YAML it pushes to each agent. KLL is the only family +# without a delta variant (`Implementation.tex`: "KLL has no delta +# variant and matches its full cost"), so its wire payload is always +# full state — the controller does NOT emit `delta_transmission` for +# KLL because the kllprocessor's `Config.Validate` rejects +# `delta_transmission: true` outright. +# +# See `docs/mvp-demo-runbook.md` §"Bandwidth criterion ① — break-even +# depends on samples_per_window" for the per-family table. + +# 1. WINDOW AGGREGATION PER SERIES — DDSketch family +# Inner: per-series quantile over a 30s sliding window. +# Stage: edge (per-agent sketch flushes at window close; +# backend only stores resulting sketch state). +# +# Window-alignment note (issue #46 ε-bound bug, fix/quantile- +# window-alignment): the warm-tier ASAPQuery streaming pre-compute +# is configured with `windowSize: 30` (see +# `deploy/mvp-singlenode/configs/backend-streaming.yaml` and the `precompute_window +# [..) ms (width 30000 ms)` annotation the warm response stamps). +# The replay range MUST match that pre-compute width so the warm +# answer is computed over the same data the replay asks for; any +# mismatch (e.g. `[1m]` vs warm's 30s) produces rel-err well above +# the DDSketch ε bound (observed mean 0.126 with `[1m]`, ε=0.01). +- metric_name: http_requests_total_latency_ms + query_string: "quantile_over_time(0.99, http_requests_total_latency_ms[30s])" + accuracy_sla: 0.01 + assign_to_role: agent + sketch_family_override: KLL # KLL accuracy experiment (issue: rank-error vs DDSketch's value-error) + # NO grouping_labels — per-series KLL preserves PromQL semantics. + # `quantile_over_time` is per-series in PromQL; grouping the sketch at + # [zone] would merge all series in a zone into one sketch, returning + # zone-merged p99 (not per-series p99). That's a different quantity than + # baseline (b0/b1) computes from raw values. For accuracy comparison + # to be apples-to-apples, asap MUST keep per-series sketches here. The + # cost is ~25× more sketch state vs per-zone merge; the demo accepts + # that on this metric to enable correct quantile-accuracy validation. + # Outer aggregation operators (e.g. `max by (zone)`) then fold per-series + # rows correctly via the engine's apply_outer_agg_fold (PR #297). + +# 2. LABEL AGGREGATION ACROSS SERIES AT ONE TIMESTAMP +# Outer: sum-by(zone) at instant. Inner: identity. +# Stage: agent (per-agent Sum-by-zone is emitted under +# a stable sid; backend's `evaluate_exact_agg` reducer +# pulls sids from every agent at query time and merges +# across hosts — see ASAPQuery-backend PR #283/#287/#290). +# +# Pre-#400 this used `assign_to_role: gateway`, which +# spawned an asap-gateway OTel-collector hop solely to +# do that cross-agent merge. Now that the backend does +# the merge natively, the gateway hop is redundant; the +# role flips to `agent` so the controller's typed +# stage_split emits only Edge + Backend stages (no +# Gateway stage → no gateway YAML emit → no asap-gateway +# container required). +- metric_name: http_requests_total + query_string: "sum by (zone) (http_requests_total)" + accuracy_sla: 0.0 + assign_to_role: agent + grouping_labels: + - zone + # Fig 9 (whole-sketch / F2 variant) — http_requests_total as a monitored + # standing query whose functional is the WHOLE-SKETCH L2 mass F2 = ‖f‖₂² + # (NOT a single point). This is the right model when the queried point is not + # known a priori: each edge reports its local F2_i = Σ_x f_i(x)² and gets + # p_i ∝ √(F2_i/rate_i), which keeps the entire sketch within ε so ANY future + # point query is accurate (see ASAPQuery-backend#380 decision rule). + # Differentiation here is by DISTRIBUTION SKEW, not rate: the trace gives each + # edge equal rate but different concentration (F2/rate = c), so p_i ∝ √c. + # COORDINATION params (not placeholders): the coordinator picks this monitor up + # live from the controller push (hot-reload, ASAPQuery-backend#379) and uses them + # directly. No per-point key — whole-sketch identity is (agg_id, ""). + monitor: + tau: 30000000.0 # Σ≈1.7e7 (equal F2 on all 3 edges). τ keeps the shared + # slack Δ/(2k)≈2.1e6 below each edge's per-window F2 so + # ALL edges report, while 0.8τ=2.4e7 > Σ (no false alert) + functional: f2 # whole-sketch L2; edge reports F2_i = Σ_x f_i(x)² + epsilon: 0.2 # alert fires at (1−ε)τ + window_secs: 15 # MUST match the edge SDK window (15 s) + +# 3. COMBINED WINDOW + LABEL AGGREGATION +# Inner: rate per series over 5m. Outer: sum-by(zone). +# Stage: edge does rate (small per-series sliding window), +# gateway does sum-by-zone fan-in. +- metric_name: http_requests_total + query_string: "sum by (zone) (rate(http_requests_total[5m]))" + accuracy_sla: 0.01 + assign_to_role: agent + grouping_labels: + - zone + +# 4. AD-HOC COLD-FALLBACK PROBE — exercises Gorilla archive tier +# Metric is configured StorageBackend::GorillaS3 in +# backend-storage-routing.yaml so the warm tier doesn't cover +# it; the query forces routing to GorillaQueryEngine. Drives +# criterion ⑤ verification (data_source: gorilla_archive). +# +# Predicate uses `zone="z0"` because the otel-app emits +# `zone, rack, node, pod` labels (see `otel-app/main.go` +# `attrSetsZRNP`); there is NO `service` label on the produced +# series, so the previous `service="payments"` selector matched +# zero series and surfaced as an empty (but HTTP-200) cold-tier +# response. `zone="z0"` matches roughly 1/4 of the produced +# series and exercises the same routing+engine code path. +- metric_name: http_requests_total + query_string: "count(http_requests_total{zone=\"z0\"})" + accuracy_sla: 0.0 + assign_to_role: archive + grouping_labels: + - zone + +# ── Five-sketch MVP coverage (issue #46) ────────────────────────── +# +# The four entries below register the new metrics emitted by the +# otel-app's five-sketch workload (otel-app/ +# five_sketch_workload.go). Each entry pins a `sketch_family_override` +# hint so the controller's planner picks the family the empirical +# claim is scoped to (DDSketch claims rel-err on quantiles, KLL +# claims rank-err, HLL claims cardinality, CountSketch claims +# top-K, CountMinSketch claims one-sided frequency over-estimate). +# +# `target_path` records the storage tier the planner should +# preferentially place each sketch in. Today's WorkloadEntry struct +# (controller/src/config/workloads.rs) doesn't yet read these two +# keys — `serde` ignores unknown fields by default, so they round- +# trip silently until the parallel capability_matching + planner +# work picks them up. The keys are spelled the same as the +# capability_matching agent's expected schema so there's no rename +# step at integration. + +# 5. KLL — rank-error quantile on a heavy-tailed body-size dist. +# Inner: per-series quantile over a 30s sliding window. +# Stage: edge — KLL state flushes per window close, no delta +# variant (kllprocessor's Config.Validate rejects +# delta_transmission: true). Backend stores the resulting KLL +# state. +# +# See entry 1 above for the window-alignment rationale: the warm +# tier pre-computes 30s tumbling windows, so the replay range +# MUST be `[30s]` for warm answers to be valid against the +# archive ground truth. +- metric_name: request_size_bytes + query_string: "quantile_over_time(0.99, request_size_bytes[30s])" + accuracy_sla: 0.05 + assign_to_role: agent + # NO grouping_labels — same rationale as http_requests_total_latency_ms: + # `quantile_over_time` is per-series in PromQL, and KLL's mergeability + # doesn't change the semantic that the inner returns per-series quantiles. + # Per-series KLL sketches keep accuracy validation apples-to-apples vs + # raw baseline. + sketch_family_override: KLL + target_path: warm + +# 6. HLL — distinct-cardinality query over rotating user pool. +# Inner: count of distinct user_id values seen in the most +# recent emission window. +# Stage: edge HLL register-set, gateway merge (HLL mergeable). +- metric_name: unique_users_per_min + query_string: "count(unique_users_per_min)" + accuracy_sla: 0.02 + assign_to_role: agent + grouping_labels: + - zone + sketch_family_override: HLL + target_path: warm + # Inner high-cardinality dimension the HLL counts distinct values of. + # The producer stamps user_id per event (five_sketch_workload.go); the + # controller emits this as the asap_edge `item_label`, so the edge hashes + # user_id into ONE HLL per zone (projecting user_id OUT of the series key) + # instead of building one cardinality-1 HLL per user_id. + item_label: user_id + +# 7. CountSketch — top-K over Zipfian endpoint distribution. +# Stage: edge CountSketch counters, gateway merge by sum of +# matching counter rows. The replay query is `topk(5, ...)`; +# the heavy hitters at the head of the Zipf tail dominate the +# top-K result. +- metric_name: top_endpoint_qps + query_string: "topk(5, top_endpoint_qps)" + accuracy_sla: 0.05 + assign_to_role: agent + grouping_labels: + - zone + sketch_family_override: CountSketch + target_path: warm + # Heavy-hitter dimension the CountSketch/top-k ranks (producer stamps + # `endpoint` per event). Emitted as the asap_edge `item_label`. + item_label: endpoint + +# 8. CountMinSketch — frequency query over the same Zipfian +# endpoint distribution, exercised through the CMS frequency +# query surface so the family scored is the one-sided +# over-estimator. The metric is split from top_endpoint_qps so +# per-family bandwidth + accuracy bookkeeping stays clean even +# though they share the same distribution shape on the producer side. +# +# Query surface (ASAPQuery-backend sketch_reducer +# `function_to_family`): the CMS / CountSketch FrequencyEstimate +# family answers `count_over_time(m[w])` (the PromQL surface) / +# `frequency(m)` (canonical) — it decodes the per-window sketch +# matrix directly. `rate(...)` is NOT the CMS surface: the +# analyzer classifies `rate()` as a COUNTER op → `ExactAgg(Sum)`, +# which has no warm CMS sid and falls over to archive. Use +# `count_over_time` at the edge window width ([30s]) so the warm +# answer is computed over the same window the CMS sealed. +- metric_name: endpoint_request_freq + query_string: "count_over_time(endpoint_request_freq[30s])" + accuracy_sla: 0.05 + assign_to_role: agent + grouping_labels: + - zone + sketch_family_override: CountMinSketch + target_path: warm + # Frequency key the CMS estimates per-value counts for (producer stamps + # `endpoint` per event). Emitted as the asap_edge `item_label` so the CMS + # hashes endpoint into ONE sketch per zone instead of one per endpoint. + item_label: endpoint diff --git a/deploy/mvp-multinode/configs/asap/mvp-workload-fig9.yaml b/deploy/mvp-multinode/configs/asap/mvp-workload-fig9.yaml new file mode 100644 index 000000000..f46bfda5e --- /dev/null +++ b/deploy/mvp-multinode/configs/asap/mvp-workload-fig9.yaml @@ -0,0 +1,242 @@ +# MVP demo controller input — three canonical query classes +# +# Read by the controller at startup (env: CONTROLLER_WORKLOADS, +# default ./workloads.yaml). The controller's L1 query_language → +# L5 stage_split pipeline plans sketches + stage placement based +# on this workload. +# +# Schema mirrors deploy/mvp-singlenode/configs/workloads.yaml. See +# controller/src/config/workloads.rs::WorkloadEntry. +# +# Each entry is one PromQL query class the demo will exercise. +# +# ── Operating-point assumptions for the bandwidth verdict ───────── +# +# Agents run at 10 Hz scrape (`-freq-hz=10`, see +# `deploy/mvp-singlenode/docker-compose/base.yml` and `mvp-multi-stage.yml`); each +# series therefore produces 600 samples per 60 s flush window. The +# break-even against raw scrape depends on `samples_per_window` +# (≈ `state_size_bytes / per_sample_raw_bytes`); at 600 samples the +# four delta-capable families (DDSketch / HLL / CountSketch / +# Count-Min) sit above or close to their break-even curves. +# +# The controller's plan emitter +# (`controller/src/config/stage_config.rs::build_edge_processor_block`) +# accordingly sets `delta_transmission: true` for those four families +# in the wire YAML it pushes to each agent. KLL is the only family +# without a delta variant (`Implementation.tex`: "KLL has no delta +# variant and matches its full cost"), so its wire payload is always +# full state — the controller does NOT emit `delta_transmission` for +# KLL because the kllprocessor's `Config.Validate` rejects +# `delta_transmission: true` outright. +# +# See `docs/mvp-demo-runbook.md` §"Bandwidth criterion ① — break-even +# depends on samples_per_window" for the per-family table. + +# 1. WINDOW AGGREGATION PER SERIES — DDSketch family +# Inner: per-series quantile over a 30s sliding window. +# Stage: edge (per-agent sketch flushes at window close; +# backend only stores resulting sketch state). +# +# Window-alignment note (issue #46 ε-bound bug, fix/quantile- +# window-alignment): the warm-tier ASAPQuery streaming pre-compute +# is configured with `windowSize: 30` (see +# `deploy/mvp-singlenode/configs/backend-streaming.yaml` and the `precompute_window +# [..) ms (width 30000 ms)` annotation the warm response stamps). +# The replay range MUST match that pre-compute width so the warm +# answer is computed over the same data the replay asks for; any +# mismatch (e.g. `[1m]` vs warm's 30s) produces rel-err well above +# the DDSketch ε bound (observed mean 0.126 with `[1m]`, ε=0.01). +- metric_name: http_requests_total_latency_ms + query_string: "quantile_over_time(0.99, http_requests_total_latency_ms[30s])" + accuracy_sla: 0.01 + assign_to_role: agent + sketch_family_override: KLL # KLL accuracy experiment (issue: rank-error vs DDSketch's value-error) + # NO grouping_labels — per-series KLL preserves PromQL semantics. + # `quantile_over_time` is per-series in PromQL; grouping the sketch at + # [zone] would merge all series in a zone into one sketch, returning + # zone-merged p99 (not per-series p99). That's a different quantity than + # baseline (b0/b1) computes from raw values. For accuracy comparison + # to be apples-to-apples, asap MUST keep per-series sketches here. The + # cost is ~25× more sketch state vs per-zone merge; the demo accepts + # that on this metric to enable correct quantile-accuracy validation. + # Outer aggregation operators (e.g. `max by (zone)`) then fold per-series + # rows correctly via the engine's apply_outer_agg_fold (PR #297). + +# 2. LABEL AGGREGATION ACROSS SERIES AT ONE TIMESTAMP +# Outer: sum-by(zone) at instant. Inner: identity. +# Stage: agent (per-agent Sum-by-zone is emitted under +# a stable sid; backend's `evaluate_exact_agg` reducer +# pulls sids from every agent at query time and merges +# across hosts — see ASAPQuery-backend PR #283/#287/#290). +# +# Pre-#400 this used `assign_to_role: gateway`, which +# spawned an asap-gateway OTel-collector hop solely to +# do that cross-agent merge. Now that the backend does +# the merge natively, the gateway hop is redundant; the +# role flips to `agent` so the controller's typed +# stage_split emits only Edge + Backend stages (no +# Gateway stage → no gateway YAML emit → no asap-gateway +# container required). +- metric_name: http_requests_total + query_string: "sum by (zone) (http_requests_total)" + accuracy_sla: 0.0 + assign_to_role: agent + grouping_labels: + - zone + # Fig 9 — make http_requests_total a monitored standing query so the + # controller emits a backend `monitors:` entry (agg_id = agg_id_for_metric). + # Coordinated CDM edges then report, under this agg_id, the per-window + # frequency f_i of the monitored series id `s0` (NOT their total rate) and + # receive a differentiated sampling-p grant (p_i ∝ √(f_i/rate_i)): s0 has + # ~equal frequency on every edge but the edges differ in total rate, so the + # busiest edge gets the smallest p. + # These params are the COORDINATION params (not a placeholder τ): the data-plane + # coordinator picks this monitor up live from the controller push (hot-reload, + # ASAPQuery-backend#379) and uses them directly — no boot-config seed. + monitor: + tau: 7000.0 # τ just above the per-window global s0 mass (~4500) so the + # slack Δ/(2k) trips each window without alerting (0.8τ=5600) + functional: cms_point # point-frequency monitor on a single sid (CMS `key`) + key: s0 # the monitored series id; only its count is reported as f_i + epsilon: 0.2 # alert fires at (1−ε)τ + window_secs: 15 # MUST match the edge SDK window (15 s) — the coordinator's + # on_register alignment guard rejects a mismatched window + +# 3. COMBINED WINDOW + LABEL AGGREGATION +# Inner: rate per series over 5m. Outer: sum-by(zone). +# Stage: edge does rate (small per-series sliding window), +# gateway does sum-by-zone fan-in. +- metric_name: http_requests_total + query_string: "sum by (zone) (rate(http_requests_total[5m]))" + accuracy_sla: 0.01 + assign_to_role: agent + grouping_labels: + - zone + +# 4. AD-HOC COLD-FALLBACK PROBE — exercises Gorilla archive tier +# Metric is configured StorageBackend::GorillaS3 in +# backend-storage-routing.yaml so the warm tier doesn't cover +# it; the query forces routing to GorillaQueryEngine. Drives +# criterion ⑤ verification (data_source: gorilla_archive). +# +# Predicate uses `zone="z0"` because the otel-app emits +# `zone, rack, node, pod` labels (see `otel-app/main.go` +# `attrSetsZRNP`); there is NO `service` label on the produced +# series, so the previous `service="payments"` selector matched +# zero series and surfaced as an empty (but HTTP-200) cold-tier +# response. `zone="z0"` matches roughly 1/4 of the produced +# series and exercises the same routing+engine code path. +- metric_name: http_requests_total + query_string: "count(http_requests_total{zone=\"z0\"})" + accuracy_sla: 0.0 + assign_to_role: archive + grouping_labels: + - zone + +# ── Five-sketch MVP coverage (issue #46) ────────────────────────── +# +# The four entries below register the new metrics emitted by the +# otel-app's five-sketch workload (otel-app/ +# five_sketch_workload.go). Each entry pins a `sketch_family_override` +# hint so the controller's planner picks the family the empirical +# claim is scoped to (DDSketch claims rel-err on quantiles, KLL +# claims rank-err, HLL claims cardinality, CountSketch claims +# top-K, CountMinSketch claims one-sided frequency over-estimate). +# +# `target_path` records the storage tier the planner should +# preferentially place each sketch in. Today's WorkloadEntry struct +# (controller/src/config/workloads.rs) doesn't yet read these two +# keys — `serde` ignores unknown fields by default, so they round- +# trip silently until the parallel capability_matching + planner +# work picks them up. The keys are spelled the same as the +# capability_matching agent's expected schema so there's no rename +# step at integration. + +# 5. KLL — rank-error quantile on a heavy-tailed body-size dist. +# Inner: per-series quantile over a 30s sliding window. +# Stage: edge — KLL state flushes per window close, no delta +# variant (kllprocessor's Config.Validate rejects +# delta_transmission: true). Backend stores the resulting KLL +# state. +# +# See entry 1 above for the window-alignment rationale: the warm +# tier pre-computes 30s tumbling windows, so the replay range +# MUST be `[30s]` for warm answers to be valid against the +# archive ground truth. +- metric_name: request_size_bytes + query_string: "quantile_over_time(0.99, request_size_bytes[30s])" + accuracy_sla: 0.05 + assign_to_role: agent + # NO grouping_labels — same rationale as http_requests_total_latency_ms: + # `quantile_over_time` is per-series in PromQL, and KLL's mergeability + # doesn't change the semantic that the inner returns per-series quantiles. + # Per-series KLL sketches keep accuracy validation apples-to-apples vs + # raw baseline. + sketch_family_override: KLL + target_path: warm + +# 6. HLL — distinct-cardinality query over rotating user pool. +# Inner: count of distinct user_id values seen in the most +# recent emission window. +# Stage: edge HLL register-set, gateway merge (HLL mergeable). +- metric_name: unique_users_per_min + query_string: "count(unique_users_per_min)" + accuracy_sla: 0.02 + assign_to_role: agent + grouping_labels: + - zone + sketch_family_override: HLL + target_path: warm + # Inner high-cardinality dimension the HLL counts distinct values of. + # The producer stamps user_id per event (five_sketch_workload.go); the + # controller emits this as the asap_edge `item_label`, so the edge hashes + # user_id into ONE HLL per zone (projecting user_id OUT of the series key) + # instead of building one cardinality-1 HLL per user_id. + item_label: user_id + +# 7. CountSketch — top-K over Zipfian endpoint distribution. +# Stage: edge CountSketch counters, gateway merge by sum of +# matching counter rows. The replay query is `topk(5, ...)`; +# the heavy hitters at the head of the Zipf tail dominate the +# top-K result. +- metric_name: top_endpoint_qps + query_string: "topk(5, top_endpoint_qps)" + accuracy_sla: 0.05 + assign_to_role: agent + grouping_labels: + - zone + sketch_family_override: CountSketch + target_path: warm + # Heavy-hitter dimension the CountSketch/top-k ranks (producer stamps + # `endpoint` per event). Emitted as the asap_edge `item_label`. + item_label: endpoint + +# 8. CountMinSketch — frequency query over the same Zipfian +# endpoint distribution, exercised through the CMS frequency +# query surface so the family scored is the one-sided +# over-estimator. The metric is split from top_endpoint_qps so +# per-family bandwidth + accuracy bookkeeping stays clean even +# though they share the same distribution shape on the producer side. +# +# Query surface (ASAPQuery-backend sketch_reducer +# `function_to_family`): the CMS / CountSketch FrequencyEstimate +# family answers `count_over_time(m[w])` (the PromQL surface) / +# `frequency(m)` (canonical) — it decodes the per-window sketch +# matrix directly. `rate(...)` is NOT the CMS surface: the +# analyzer classifies `rate()` as a COUNTER op → `ExactAgg(Sum)`, +# which has no warm CMS sid and falls over to archive. Use +# `count_over_time` at the edge window width ([30s]) so the warm +# answer is computed over the same window the CMS sealed. +- metric_name: endpoint_request_freq + query_string: "count_over_time(endpoint_request_freq[30s])" + accuracy_sla: 0.05 + assign_to_role: agent + grouping_labels: + - zone + sketch_family_override: CountMinSketch + target_path: warm + # Frequency key the CMS estimates per-value counts for (producer stamps + # `endpoint` per event). Emitted as the asap_edge `item_label` so the CMS + # hashes endpoint into ONE sketch per zone instead of one per endpoint. + item_label: endpoint diff --git a/deploy/mvp-multinode/eval-8node/f2_debs.csv b/deploy/mvp-multinode/eval-8node/f2_debs.csv new file mode 100644 index 000000000..493b86bb8 --- /dev/null +++ b/deploy/mvp-multinode/eval-8node/f2_debs.csv @@ -0,0 +1,4 @@ +mode,total_bytes,alert,ships,note +raw,99122250,1,,ship-every-event +distributed,14747280,1,80,, +geometric,13033555,1,40,, diff --git a/deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster.csv b/deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster.csv new file mode 100644 index 000000000..b82473400 --- /dev/null +++ b/deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster.csv @@ -0,0 +1,7 @@ +workload,mode,alert,total_bytes,ships,silent +stable,raw,0,204056,, +stable,distributed,0,923280,80,0 +stable,geometric,0,230820,4,76 +ramp,raw,1,4007440,, +ramp,distributed,1,923280,80,0 +ramp,geometric,1,1647966,28,52 diff --git a/deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster_h2048.csv b/deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster_h2048.csv new file mode 100644 index 000000000..b82473400 --- /dev/null +++ b/deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster_h2048.csv @@ -0,0 +1,7 @@ +workload,mode,alert,total_bytes,ships,silent +stable,raw,0,204056,, +stable,distributed,0,923280,80,0 +stable,geometric,0,230820,4,76 +ramp,raw,1,4007440,, +ramp,distributed,1,923280,80,0 +ramp,geometric,1,1647966,28,52 diff --git a/deploy/mvp-multinode/eval-8node/gos_aniso_cluster.csv b/deploy/mvp-multinode/eval-8node/gos_aniso_cluster.csv new file mode 100644 index 000000000..fe9ece1b3 --- /dev/null +++ b/deploy/mvp-multinode/eval-8node/gos_aniso_cluster.csv @@ -0,0 +1,7 @@ +zipf_s,arm,delta_bytes_70s +1.1,iso,503230 +1.1,aniso,303901 +1.5,iso,301851 +1.5,aniso,302328 +2.0,iso,303693 +2.0,aniso,300198 diff --git a/deploy/mvp-multinode/scripts/aggregate_phase2_sweep.py b/deploy/mvp-multinode/scripts/aggregate_phase2_sweep.py new file mode 100644 index 000000000..304ab392e --- /dev/null +++ b/deploy/mvp-multinode/scripts/aggregate_phase2_sweep.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Aggregate epsilon_cluster_sweep.sh per-arm output into one integrated table. +# cols: p | implied ε | freshness warm/archive p50 | warm latency p50/p99 | +# edge CPU | data-plane CPU/mem | NIC tx kbps | (accuracy layered later) +import csv, glob, os, sys, math +BASE = sys.argv[1] if len(sys.argv) > 1 else "/mydata/eval/phase2/sweep" +# rate per window for implied-ε: light workload ~ card*freq*producers*nodes admitted +# we read admitted rate from data-plane if available; else use nominal. +NOMINAL_RATE = 300 * 20 * 2 * 2 # card*freq*prod*nodes ~ updates/s (upper bound) + +def fresh(arm_dir): + f = os.path.join(arm_dir, "freshness", "freshness-asap.csv") + out = {} + if not os.path.exists(f): return out + by = {} + for r in csv.DictReader(open(f)): + try: ov, pt = float(r["observed_value_ms"]), float(r["poll_ts_ms"]) + except: continue + if ov > 1e12: # valid emit epoch-ms + by.setdefault(r["tier"], []).append(pt - ov) + for t, v in by.items(): + v = sorted(x for x in v if x >= 0) + if v: out[t] = (v[len(v)//2], v[min(len(v)-1, int(.99*len(v)))], len(v)) + return out + +def latency(arm_dir): + f = os.path.join(arm_dir, "latency.csv") + if not os.path.exists(f): return (None, None) + rows = list(csv.DictReader(open(f))) + if not rows: return (None, None) + return (float(rows[0]["p50_ms"]), float(rows[0]["p99_ms"])) + +def resources(arm_dir): + f = glob.glob(os.path.join(arm_dir, "resources", "container-summary-*.csv")) + edge_cpu = dp_cpu = dp_mem = cold_cpu = 0.0 + if f: + for r in csv.DictReader(open(f[0])): + c = r["container"]; cpu = float(r["cpu_mean_perc"]); mem = float(r["mem_mean_mib"]) + if "agent" in c or "producer" in c: edge_cpu += cpu + elif "data-plane" in c: dp_cpu, dp_mem = cpu, mem + elif any(k in c for k in ("gorilla","minio","thanos","prometheus")): cold_cpu += cpu + return edge_cpu, dp_cpu, dp_mem, cold_cpu + +def nic(arm_dir): + f = glob.glob(os.path.join(arm_dir, "resources", "nic-*.csv")) + tx = 0.0 + if f: + for r in csv.DictReader(open(f[0])): + try: tx += float(r["tx_bytes_per_s"]) + except: pass + return tx * 8 / 1000.0 # kbps aggregate + +print(f"{'p':>5} {'ε~':>7} | {'fresh_warm':>10} {'fresh_arch':>10} | {'lat50':>6} {'lat99':>6} | " + f"{'edgeCPU%':>8} {'dpCPU%':>6} {'dpMEM':>6} {'coldCPU%':>8} | {'NIC_kbps':>9}") +for arm_dir in sorted(glob.glob(os.path.join(BASE, "p*")), + key=lambda d: -float(os.path.basename(d)[1:])): + p = float(os.path.basename(arm_dir)[1:]) + eps = math.sqrt((1/p - 1)/NOMINAL_RATE) if p < 1 else 0.0 + fr = fresh(arm_dir); l50, l99 = latency(arm_dir) + ecpu, dcpu, dmem, ccpu = resources(arm_dir); ntx = nic(arm_dir) + fw = f"{fr['warm'][0]:.0f}ms" if 'warm' in fr else "—" + fa = f"{fr['archive'][0]:.0f}ms" if 'archive' in fr else "—" + print(f"{p:>5} {eps:>7.4f} | {fw:>10} {fa:>10} | " + f"{(f'{l50:.1f}' if l50 else '—'):>6} {(f'{l99:.1f}' if l99 else '—'):>6} | " + f"{ecpu:>8.0f} {dcpu:>6.1f} {dmem:>5.0f}M {ccpu:>8.1f} | {ntx:>9.1f}") diff --git a/deploy/mvp-multinode/scripts/e2e_metrics.sh b/deploy/mvp-multinode/scripts/e2e_metrics.sh new file mode 100755 index 000000000..e48e92a3d --- /dev/null +++ b/deploy/mvp-multinode/scripts/e2e_metrics.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# e2e_metrics.sh — ONE consolidated multinode end-to-end metrics run against the +# REAL ASAPQuery-backend. Every number below is produced by THIS run (nothing is +# looked up from a prior result file). Merges the two existing harnesses: +# +# • run_demo.sh (library) — brings up the real multinode `asap` stack +# (cold node1 + warm node2 data_plane/control_plane + agents node3/4) and its +# arm_measure captures, per component and per node: +# – CPU % + mem (docker stats, sampled over the soak, every node) +# – bandwidth (per-edge NIC tx/rx + per-stage bytes) +# – query LATENCY (MetricsQL replay against node2:9091) +# • run_perfamily.py — replays a GT-known slice through the SAME warm +# backend (E2E_EXTERNAL_STACK, E2E_BACKEND=node2:9091) and scores query +# ACCURACY vs the exact offline ground truth, per family: +# – DDSketch/KLL quantile rel-err, CountSketch topk recall, +# CountMinSketch freq envelope, HLL cardinality rel-err +# + agent→backend wire bytes. +# +# Output: one fresh report dir with the per-component resource/bandwidth CSVs, +# the query-latency replay, and the per-family accuracy JSON — plus a summary. +# +# Usage: e2e_metrics.sh [arms] (arms default: ddsketch,countsketch,countminsketch,hll,kll) +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MV="$(cd "${SCRIPT_DIR}/.." && pwd)" +ROOT="$(cd "${MV}/../.." && pwd)" +export TOPOLOGY_ENV="${TOPOLOGY_ENV:-${MV}/topology.8node.env}" +export RUN_DEMO_LIB=1 +# shellcheck disable=SC1090 +source "${SCRIPT_DIR}/run_demo.sh" + +ARMS="${1:-ddsketch,countsketch,countminsketch,hll,kll}" +SOAK_S="${SOAK_S:-90}" +STAMP="$(cat /proc/sys/kernel/random/uuid | cut -c1-8)" +OUT="${E2E_OUT:-${MV}/eval-8node/e2e-${STAMP}}" +mkdir -p "${OUT}" +export RUN_DIR="${OUT}" # arm_measure writes under RUN_DIR// + +slog(){ printf '[%s] [e2e] %s\n' "$(date +%H:%M:%S)" "$*"; } + +# 1. Bring up the real multinode asap stack (reuse images already on the nodes). +slog "sync configs + bring up the multinode 'asap' stack (cold=${NODE1_HOST}, warm=${NODE2_HOST}, agents=${NODE0_HOST}/${NODE3_HOST})" +SKIP_BUILD=1 SKIP_LOAD="${SKIP_LOAD:-1}" ensure_images +sync_all_nodes +arm_down || true # clear any leftover containers from a prior run (name conflicts) +sleep 2 +arm_up asap + +# 2. Per-component resources + bandwidth (snapshot_resources.sh — the WORKING +# multinode tool; run_demo's arm_measure references measure_per_edge_bandwidth +# /measure_stages.py which no longer exist) + query LATENCY (metricsql_replay). +slog "snapshot_resources: per-component CPU/mem + per-node NIC bandwidth (${SOAK_S}s)" +SNAP_NODES="${NODE0_HOST} ${NODE1_HOST} ${NODE2_HOST} ${NODE3_HOST}" \ + bash "${SCRIPT_DIR}/snapshot_resources.sh" asap "${SOAK_S}" "${OUT}/resources" \ + > "${OUT}/snapshot.log" 2>&1 & +SNAP_PID=$! +slog "query latency: MetricsQL replay against warm backend node2:9091 (${SOAK_S}s)" +python3 "${ROOT}/deploy/mvp-singlenode/scripts/metricsql_replay.py" \ + --target "http://${NODE2_IP}:9091" \ + --queries "${ROOT}/deploy/mvp-singlenode/scripts/queries-e2e.json" \ + --duration "${SOAK_S}" --out "${OUT}/replay.jsonl" \ + > "${OUT}/replay.log" 2>&1 || slog "latency replay non-zero (see replay.log)" +wait "${SNAP_PID}" 2>/dev/null || true + +# 3. Query ACCURACY vs exact GT against the SAME warm backend (node2:9091). +slog "accuracy: replay GT-known slices → warm backend node2:9091 → score per family (${ARMS})" +E2E_EXTERNAL_STACK=1 \ +E2E_BACKEND="http://${NODE2_IP}:9091" \ +E2E_AGENT_METRICS="http://${NODE0_IP}:8890/metrics" \ +E2E_REPLAY_ENDPOINT="${NODE0_IP}:4317" \ + python3 "${ROOT}/datasets_eval/multisketch/run_perfamily.py" \ + --arms "${ARMS}" --window "${SOAK_S}s" \ + > "${OUT}/accuracy.log" 2>&1 || slog "accuracy step exited non-zero (see accuracy.log)" +# run_perfamily writes datasets_eval/multisketch/results/perfamily-*.json — copy the fresh ones in. +cp "${ROOT}"/datasets_eval/multisketch/results/perfamily-*.json "${OUT}/" 2>/dev/null || true + +# 4. Consolidated fresh report: per-component resources + bandwidth + latency + +# per-family accuracy, all from THIS run. +slog "aggregating fresh report → ${OUT}/E2E_METRICS.md" +python3 - "${OUT}" > "${OUT}/E2E_METRICS.md" 2>"${OUT}/report.log" <<'PY' +import csv, glob, json, os, sys, statistics +out = sys.argv[1] +print(f"# e2e metrics — fresh multinode run ({os.path.basename(out)})\n") +print("All numbers below are from THIS run against the real ASAPQuery-backend.\n") + +# --- per-component CPU/mem (snapshot_resources container-summary) --- +print("## Per-component CPU / memory (soak mean)\n") +cs = os.path.join(out, "resources", "container-summary-asap.csv") +if os.path.exists(cs): + print("| component | node | mean CPU% | max mem |") + print("|---|---|---|---|") + for r in csv.DictReader(open(cs)): + print("| " + " | ".join(str(r.get(k, "")) for k in list(r)[:4]) + " |") +else: + print("_container-summary-asap.csv missing (snapshot failed — see snapshot.log)_") + +# --- per-node NIC bandwidth --- +print("\n## Per-node NIC bandwidth\n") +nic = os.path.join(out, "resources", f"nic-summary-asap.csv") +nic = nic if os.path.exists(nic) else next(iter(glob.glob(os.path.join(out, "resources", "*nic*"))), "") +if nic and os.path.exists(nic): + for line in open(nic): + print(" " + line.rstrip()) +else: + print("_nic summary missing_") + +# --- query latency (metricsql replay) --- +print("\n## Query latency (MetricsQL replay vs node2:9091)\n") +rj = os.path.join(out, "replay.jsonl") +lat = [] +if os.path.exists(rj): + for line in open(rj): + try: + d = json.loads(line); v = d.get("latency_ms") or d.get("ms") or d.get("elapsed_ms") + if v is not None: lat.append(float(v)) + except Exception: pass +if lat: + lat.sort() + p = lambda q: lat[min(len(lat)-1, int(q*len(lat)))] + print(f"- n={len(lat)} p50={p(0.5):.1f}ms p95={p(0.95):.1f}ms p99={p(0.99):.1f}ms") +else: + print("_no latency samples (see replay.log)_") + +# --- per-family accuracy --- +print("\n## Query accuracy (vs exact ground truth)\n") +accs = [f for f in sorted(glob.glob(os.path.join(out, "perfamily-*.json"))) if not f.endswith("perfamily-all.json")] +if not accs: + print("_no accuracy JSONs — the family replay/prep did not run (see accuracy.log)_") +PY +python3 - "${OUT}" >> "${OUT}/E2E_METRICS.md" 2>>"${OUT}/report.log" <<'PY' +import json, glob, os, sys +out = sys.argv[1] +print("\n## Query accuracy (this run, vs exact ground truth)\n") +print("| family | query | mean rel-err | median | p95 | within 2% |") +print("|---|---|---|---|---|---|") +for f in sorted(glob.glob(os.path.join(out, "perfamily-*.json"))): + if f.endswith("perfamily-all.json"): + continue + d = json.load(open(f)) + fam = os.path.basename(f).replace("perfamily-", "").replace(".json", "") + kind = d.get("kind", "?") + sc = d.get("score", {}) + # quantile families store per-quantile dicts; cardinality/topk/freq store flat. + if isinstance(sc, dict) and any(isinstance(v, dict) for v in sc.values()): + for q, v in sc.items(): + if isinstance(v, dict) and "mean_rel_err" in v: + print(f"| {fam} | {kind} q{q} | {v['mean_rel_err']:.2%} | {v.get('median_rel_err',float('nan')):.2%} | {v.get('p95_rel_err',float('nan')):.2%} | {v.get('frac_within_envelope',float('nan')):.1%} |") + elif "rel_err" in sc and sc["rel_err"] is not None: + print(f"| {fam} | {kind} | {sc['rel_err']:.2%} | — | — | — |") + else: + print(f"| {fam} | {kind} | (see {os.path.basename(f)}) | | | |") +PY + +arm_down || true +slog "done — fresh metrics in ${OUT}/ (report: ${OUT}/E2E_METRICS.md)" +cat "${OUT}/E2E_METRICS.md" 2>/dev/null || true diff --git a/deploy/mvp-multinode/scripts/epsilon_accuracy_sweep.sh b/deploy/mvp-multinode/scripts/epsilon_accuracy_sweep.sh new file mode 100644 index 000000000..19011a37c --- /dev/null +++ b/deploy/mvp-multinode/scripts/epsilon_accuracy_sweep.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# epsilon_accuracy_sweep.sh — Phase-2 cluster ACCURACY column, google_cluster trace. +# +# Per coordinated-sampling admission p, bring up the asap stack with otel-app +# producers in TRACE-REPLAY mode (the real google-cluster-2019 cpu_rate trace, +# same dataset as Phase-1), landing the trace under the warm DDSketch metric, and +# query the warm sketch quantiles. accuracy = 1 - |sketch - GT|/GT against the +# exact offline GT over the full trace. -warm-sample-p admission sampling applies +# to the replayed gauge, so this is accuracy-under-coordinated-sampling. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PKG_DIR="$(dirname "${SCRIPT_DIR}")" +export TOPOLOGY_ENV="${TOPOLOGY_ENV:-${PKG_DIR}/topology.8node.env}" +source "${TOPOLOGY_ENV}" + +OUT="${OUT:-/mydata/eval/phase2/accuracy}" +P_GRID="${P_GRID:-1.0 0.5 0.25 0.1 0.05}" +TRACE_CSV="${TRACE_CSV:-/tmp/gct-cpu-pooled.csv}" +METRIC="${METRIC:-http_requests_total_latency_ms}" +SEAL_S="${SEAL_S:-55}" +export PER_AGENT_CARDINALITY=300 OTELAPP_FREQ_HZ=20 N_PRODUCERS_PER_NODE=1 +export SKIP_BUILD=1 SKIP_LOAD=1 +export OTELAPP_TRACE_MOUNT="-v ${TRACE_CSV}:/trace.csv:ro" +export OTELAPP_TRACE_ARGS="-trace-file=/trace.csv -trace-loop -trace-scale=30000 -trace-metric-name=${METRIC}" +mkdir -p "${OUT}" +: > "${OUT}/accuracy-raw.csv" +echo "p,quantile,sketch_mean,n_series" >> "${OUT}/accuracy-raw.csv" + +query_q() { # $1=quantile ; echoes "mean nseries" + local qp=$1 + ssh -o BatchMode=yes "${WARM_HOST}" \ + "curl -s 'http://localhost:9091/api/v1/query' --data-urlencode 'query=quantile_over_time(${qp}, ${METRIC}[5m])'" 2>/dev/null \ + | python3 -c 'import sys,json +try: + d=json.load(sys.stdin); r=d["data"]["result"] + v=[float(s["value"][1]) for s in r] + print(f"{sum(v)/len(v):.8f} {len(v)}" if v else "nan 0") +except Exception: print("nan 0")' +} + +for p in ${P_GRID}; do + echo "[acc-sweep] === p=${p} ===" + SKIP_BUILD=1 SKIP_LOAD=1 bash "${SCRIPT_DIR}/run_demo.sh" down >/dev/null 2>&1 || true + sleep 3 + OTELAPP_WARM_SAMPLE_P="${p}" bash "${SCRIPT_DIR}/run_demo.sh" up asap \ + >"${OUT}/up-p${p}.log" 2>&1 + echo "[acc-sweep] p=${p} up; trace replay + seal ${SEAL_S}s" + sleep "${SEAL_S}" + for qp in 0.99 0.90 0.50; do + read -r mean nser <<< "$(query_q ${qp})" + echo "${p},${qp},${mean},${nser}" >> "${OUT}/accuracy-raw.csv" + echo "[acc-sweep] p=${p} q=${qp} sketch=${mean} (n=${nser})" + done +done +SKIP_BUILD=1 SKIP_LOAD=1 bash "${SCRIPT_DIR}/run_demo.sh" down >/dev/null 2>&1 || true +echo "[acc-sweep] complete → ${OUT}/accuracy-raw.csv" diff --git a/deploy/mvp-multinode/scripts/epsilon_cluster_sweep.sh b/deploy/mvp-multinode/scripts/epsilon_cluster_sweep.sh new file mode 100644 index 000000000..a19030227 --- /dev/null +++ b/deploy/mvp-multinode/scripts/epsilon_cluster_sweep.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# epsilon_cluster_sweep.sh — Phase-2 integrated cluster ε-sweep. +# +# For each coordinated-sampling admission p (= the ε-floor p=1/(1+ε²·rate) the +# autonomous coordinator sets for a target ε), bring up the full warm+cold ASAP +# stack and capture the integrated metric set the single-node run could NOT: +# · real data FRESHNESS (gen→queryable Δ per tier: warm sketch, archive gorilla) +# · per-component RESOURCES (edge / data-plane / control-plane / thanos / minio) +# · query LATENCY (PromQL replay p50/p99 on warm :9091) +# accuracy (warm + cold-tier fallthrough) is layered on via run_e2e separately. +# +# Wall-clock paced (trace-scale 1.0) so freshness is a true wall-clock latency. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PKG_DIR="$(dirname "${SCRIPT_DIR}")" +export TOPOLOGY_ENV="${TOPOLOGY_ENV:-${PKG_DIR}/topology.8node.env}" +source "${TOPOLOGY_ENV}" + +OUT_BASE="${OUT_BASE:-/mydata/eval/phase2/sweep}" +SOAK="${SOAK_S:-90}" +N_FRESH="${N_FRESH:-60}" +# ε-floor admission grid (p); implied ε computed post-hoc from measured rate. +P_GRID="${P_GRID:-1.0 0.5 0.25 0.1 0.05}" +# Light, stable workload so producers don't OOM (the default ~250MB/s starves +# the agent → producer backpressure → OOM-137). A modest, steady rate is what +# we want for clean gen->queryable / resource / accuracy measurement anyway. +export PER_AGENT_CARDINALITY="${PER_AGENT_CARDINALITY:-300}" +export OTELAPP_FREQ_HZ="${OTELAPP_FREQ_HZ:-20}" +export N_PRODUCERS_PER_NODE="${N_PRODUCERS_PER_NODE:-2}" +export SKIP_BUILD=1 SKIP_LOAD=1 +mkdir -p "${OUT_BASE}" + +echo "[eps-sweep] OUT=${OUT_BASE} SOAK=${SOAK}s P_GRID=${P_GRID}" + +for p in ${P_GRID}; do + arm="p${p}" + out="${OUT_BASE}/${arm}" + mkdir -p "${out}" + echo "[eps-sweep] === p=${p} ===" + + SKIP_BUILD=1 SKIP_LOAD=1 bash "${SCRIPT_DIR}/run_demo.sh" down >"${out}/down.log" 2>&1 || true + sleep 3 + SKIP_BUILD=1 SKIP_LOAD=1 OTELAPP_WARM_SAMPLE_P="${p}" \ + bash "${SCRIPT_DIR}/run_demo.sh" up asap >"${out}/up.log" 2>&1 + echo "[eps-sweep] p=${p} up; soak ${SOAK}s" + sleep 5 + + # resources (backgrounded over the soak window) + query-latency replay + SNAP_NODES="${COLD_HOST} ${WARM_HOST} ${SRC_HOSTS}" \ + bash "${SCRIPT_DIR}/snapshot_resources.sh" "${arm}" "${SOAK}" "${out}/resources" \ + >"${out}/snap.log" 2>&1 & + snap_pid=$! + + # freshness (real gen→queryable per tier) during the same soak + ARM=asap OUT="${out}/freshness" NODE2_IP="${WARM_IP}" \ + N_SAMPLES="${N_FRESH}" POLL_MS=100 \ + bash "${SCRIPT_DIR}/measure_freshness.sh" >"${out}/freshness.log" 2>&1 || true + + # query latency: time N warm-tier queries (probe = always-served, name-routed) + python3 - "${WARM_IP}" "${out}/latency.csv" <<'PY' || true +import sys,time,urllib.parse,urllib.request +ip,out=sys.argv[1],sys.argv[2] +q="last_over_time(http_freshness_probe_warm[10s])" +url=f"http://{ip}:9091/api/v1/query?"+urllib.parse.urlencode({"query":q}) +lat=[] +for _ in range(40): + t=time.perf_counter() + try: urllib.request.urlopen(url,timeout=5).read() + except Exception: continue + lat.append((time.perf_counter()-t)*1000.0) +lat.sort() +if lat: + p50=lat[len(lat)//2]; p99=lat[min(len(lat)-1,int(.99*len(lat)))] + open(out,"w").write(f"p50_ms,p99_ms,n\n{p50:.2f},{p99:.2f},{len(lat)}\n") + print(f"[latency] p50={p50:.2f}ms p99={p99:.2f}ms n={len(lat)}") +PY + + wait "${snap_pid}" 2>/dev/null || true + echo "[eps-sweep] p=${p} done → ${out}" +done + +SKIP_BUILD=1 SKIP_LOAD=1 bash "${SCRIPT_DIR}/run_demo.sh" down >/dev/null 2>&1 || true +echo "[eps-sweep] complete. results under ${OUT_BASE}" diff --git a/deploy/mvp-multinode/scripts/fig9_coordinated.sh b/deploy/mvp-multinode/scripts/fig9_coordinated.sh new file mode 100755 index 000000000..f981136dc --- /dev/null +++ b/deploy/mvp-multinode/scripts/fig9_coordinated.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Fig 9 rerun: cms_point coordinated sampling on a skewed fleet. +# Monitored key s0 has CONSTANT frequency across edges; total rate is skewed. +# Expect p_hot < p_med < p_quiet (p_i ∝ √(f_i/rate_i), f_i const ⇒ p ∝ 1/√rate). +set -uo pipefail +SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MV="$(cd "${SD}/.." && pwd)" # deploy/mvp-multinode +export TOPOLOGY_ENV="${TOPOLOGY_ENV:-${MV}/topology.8node.env}" +export RUN_DEMO_LIB=1; source "${SD}/run_demo.sh" +CFG="${MV}/configs" +AGG_PORT=4319 +declare -A NODEMAP=( [edge-hot]=node3 [edge-med]=node4 [edge-quiet]=node5 ) +declare -A SER=( [edge-hot]=400 [edge-med]=80 [edge-quiet]=16 ) +slog(){ printf '[%s] [fig9] %s\n' "$(date +%H:%M:%S)" "$*"; } + +down(){ for e in "${!NODEMAP[@]}"; do ssh -o BatchMode=yes "${NODEMAP[$e]}" "docker rm -f asap-$e >/dev/null 2>&1"; done + stop_node "${COLD_HOST}" >/dev/null 2>&1; stop_node "${WARM_HOST}" >/dev/null 2>&1; backend_down >/dev/null 2>&1 || true; } +# NOTE: no EXIT trap — leave the stack up after the run so grants can be inspected. +down; sleep 3 + +# 1. swap monitored workload onto node2, backend up WITH coordinator +sync_all_nodes >/dev/null 2>&1 +rsync -a "${CFG}/asap/mvp-workload-fig9.yaml" "${WARM_HOST}:/mydata/mvp-multinode/configs/asap/mvp-workload.yaml" +DP_MONITOR_FLAGS="--enable-monitor-coordinator --monitor-grpc-port ${AGG_PORT}" backend_up asap >/dev/null 2>&1 +slog "backend up (coordinator :${AGG_PORT}, cms_point key=s0)"; sleep 10 + +# 2. read the emitted monitor agg_id +AGG="" +for i in $(seq 1 30); do + AGG=$(curl -s "http://${WARM_IP}:9091/api/v1/streaming-config" 2>/dev/null | python3 -c "import json,sys +try: m=json.load(sys.stdin).get('streaming_config',{}).get('monitors',[]); print(m[0]['agg_id'] if m else '') +except: print('')" 2>/dev/null) + [ -n "${AGG}" ] && break; sleep 4 +done +[ -z "${AGG}" ] && { slog "FATAL: no monitor agg_id"; exit 1; } +slog "monitor agg_id=${AGG}" + +# 2b. Wait for the coordinator to HOT-RELOAD the controller-pushed monitor. +# The data-plane coordinator picks up `monitors:` live from the control plane's +# streaming-config push (ASAPQuery-backend#379, MonitorCoordinator::reconfigure) +# — no boot-config seed, no restart. The workload yaml carries the real +# coordination params (tau 7000 / window_secs 15) so the published monitor is +# directly usable. Earlier this step seeded the boot config + restarted the +# data-plane to work around the coordinator reading monitors() only at boot; +# #379 makes that unnecessary. +slog "waiting for coordinator to hot-reload the monitor (no seed, no restart)..." +for i in $(seq 1 30); do + ssh -o BatchMode=yes "${WARM_HOST}" "docker logs asap-data-plane 2>&1 | grep -q 'hot-reloaded monitors'" && break + sleep 3 +done +ssh -o BatchMode=yes "${WARM_HOST}" "docker logs asap-data-plane 2>&1 | grep -iE 'hot-reloaded monitors|no .monitors. yet' | tail -3" +k=$(curl -s "http://${WARM_IP}:9091/api/v1/streaming-config" 2>/dev/null | python3 -c "import json,sys +try: m=json.load(sys.stdin).get('streaming_config',{}).get('monitors',[]); print(m[0]['key'] if m else '') +except: print('')" 2>/dev/null) +slog "coordinator hot-reloaded; served monitor key=${k:-?}" + +# 3. generate constant-key CSVs (s0 once/timestamp = const freq; N-1 others) + ship +gen(){ python3 -c " +import sys; N=int(sys.argv[1]) +print('timestamp_ms,series_id,value') +for t in range(0,2000,10): + print(f'{t},s0,1.0') + for s in range(1,N): print(f'{t},x{s},1.0') +" "$1"; } +for e in "${!SER[@]}"; do gen "${SER[$e]}" > /tmp/$e.csv; scp -q -o BatchMode=yes /tmp/$e.csv "${NODEMAP[$e]}:/tmp/edge.csv"; done + +# 4. launch coordinated trace-replay edges with -monitor-key=s0 +for e in edge-hot edge-med edge-quiet; do + slog "${e} on ${NODEMAP[$e]} (N=${SER[$e]} series, s0 const-freq)" + ssh -o BatchMode=yes "${NODEMAP[$e]}" "docker rm -f asap-$e >/dev/null 2>&1; docker run -d --network host --name asap-$e \ + -v /tmp/edge.csv:/tmp/edge.csv:ro asap/otel-app:dev \ + -producer-id=$e -target=${WARM_IP}:4317 -trace-file=/tmp/edge.csv -trace-loop \ + -coordinator-url=${WARM_IP}:${AGG_PORT} -monitor-agg-id=${AGG} -monitor-key=s0 \ + -monitor-config-url=http://${WARM_IP}:9091/api/v1/streaming-config \ + -edge-id=$e -warm-sample-p=1.0" >/dev/null +done + +# 5. converge, then read granted p +slog "converging 150s..."; sleep 150 +echo "edge,node,total_series,learned,granted_p" +OUT="${FIG9_OUT:-${MV}/eval-8node/fig9_cmspoint.csv}"; echo "edge,node,total_series,learned_monitors,granted_p" > "$OUT" +for e in edge-hot edge-med edge-quiet; do + log=$(ssh -o BatchMode=yes "${NODEMAP[$e]}" "docker logs asap-$e 2>&1") + learned=$(echo "$log" | grep -oE 'learned [0-9]+ monitor' | tail -1 | grep -oE '[0-9]+' | head -1) + # new sample_controller log: "applied warm-sample-p 0.1234 (was ...) = max over N monitors ..." + p=$(echo "$log" | grep -oE 'applied warm-sample-p [0-9.]+' | tail -1 | grep -oE '[0-9.]+$') + echo "$e ${NODEMAP[$e]} ${SER[$e]} learned=${learned:-0} p=${p:-1.0}" + echo "$e,${NODEMAP[$e]},${SER[$e]},${learned:-0},${p:-1.0}" >> "$OUT" +done +slog "coordinator-side p (data-plane log):" +ssh -o BatchMode=yes "${WARM_HOST}" "docker logs asap-data-plane 2>&1 | grep -iE 'sample_p|grant|alloc' | tail -6" 2>/dev/null || true diff --git a/deploy/mvp-multinode/scripts/fig9_f2_coordinated.sh b/deploy/mvp-multinode/scripts/fig9_f2_coordinated.sh new file mode 100644 index 000000000..2d9fa78ac --- /dev/null +++ b/deploy/mvp-multinode/scripts/fig9_f2_coordinated.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Fig 9 (whole-sketch / F2 variant): coordinated sampling driven by each edge's +# WHOLE-SKETCH L2 mass F2_i = Σ_x f_i(x)², not a single point. This is the right +# model when the queried point is unknown a priori — keeping the whole sketch +# within ε makes ANY future point query accurate (ASAPQuery-backend#380). +# +# Workload: every edge carries the SAME L2 mass F2 but spreads it over a +# different number of series, i.e. a different total RATE (concentration). Per +# timestamp each edge emits m = R²/F2T series each q = F2T/R times, so F2_ts = +# m·q² = F2T is EQUAL on every edge (⇒ they all trip the shared slack countdown +# together and all report) while rate = m·q = R differs. The allocation +# p_i ∝ √(F2_i/rate_i) then reduces to p ∝ 1/√rate: +# edge-lo (R=16) : edge-mid (R=64) : edge-hi (R=256) → p ≈ 4 : 2 : 1. +# (Equal F2 is what makes the slack work; cf. the cms_point variant, where equal +# per-key f_i played the same role and rate drove the differentiation.) +set -uo pipefail +SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MV="$(cd "${SD}/.." && pwd)" # deploy/mvp-multinode +export TOPOLOGY_ENV="${TOPOLOGY_ENV:-${MV}/topology.8node.env}" +export RUN_DEMO_LIB=1; source "${SD}/run_demo.sh" +CFG="${MV}/configs" +AGG_PORT=4319 +F2_PER_TS=256 # equal per-timestamp F2 on every edge +declare -A NODEMAP=( [edge-lo]=node3 [edge-mid]=node4 [edge-hi]=node5 ) +declare -A RATE=( [edge-lo]=16 [edge-mid]=64 [edge-hi]=256 ) +slog(){ printf '[%s] [fig9-f2] %s\n' "$(date +%H:%M:%S)" "$*"; } + +down(){ for e in "${!NODEMAP[@]}"; do ssh -o BatchMode=yes "${NODEMAP[$e]}" "docker rm -f asap-$e >/dev/null 2>&1"; done + stop_node "${COLD_HOST}" >/dev/null 2>&1; stop_node "${WARM_HOST}" >/dev/null 2>&1; backend_down >/dev/null 2>&1 || true; } +# NOTE: no EXIT trap — leave the stack up after the run so grants can be inspected. +down; sleep 3 + +# 1. swap the F2 monitored workload onto WARM, backend up WITH coordinator +sync_all_nodes >/dev/null 2>&1 +rsync -a "${CFG}/asap/mvp-workload-fig9-f2.yaml" "${WARM_HOST}:/mydata/mvp-multinode/configs/asap/mvp-workload.yaml" +DP_MONITOR_FLAGS="--enable-monitor-coordinator --monitor-grpc-port ${AGG_PORT}" backend_up asap >/dev/null 2>&1 +slog "backend up (coordinator :${AGG_PORT}, functional=f2 whole-sketch)"; sleep 10 + +# 2. read the emitted monitor agg_id +AGG="" +for i in $(seq 1 30); do + AGG=$(curl -s "http://${WARM_IP}:9091/api/v1/streaming-config" 2>/dev/null | python3 -c "import json,sys +try: m=json.load(sys.stdin).get('streaming_config',{}).get('monitors',[]); print(m[0]['agg_id'] if m else '') +except: print('')" 2>/dev/null) + [ -n "${AGG}" ] && break; sleep 4 +done +[ -z "${AGG}" ] && { slog "FATAL: no monitor agg_id"; exit 1; } +slog "monitor agg_id=${AGG}" + +# 2b. wait for the coordinator to HOT-RELOAD the controller-pushed f2 monitor +slog "waiting for coordinator to hot-reload the monitor (no seed, no restart)..." +for i in $(seq 1 30); do + ssh -o BatchMode=yes "${WARM_HOST}" "docker logs asap-data-plane 2>&1 | grep -q 'hot-reloaded monitors'" && break + sleep 3 +done +ssh -o BatchMode=yes "${WARM_HOST}" "docker logs asap-data-plane 2>&1 | grep -iE 'hot-reloaded monitors|no .monitors. yet' | tail -3" +fn=$(curl -s "http://${WARM_IP}:9091/api/v1/streaming-config" 2>/dev/null | python3 -c "import json,sys +try: m=json.load(sys.stdin).get('streaming_config',{}).get('monitors',[]); print(m[0].get('functional','') if m else '') +except: print('')" 2>/dev/null) +slog "coordinator hot-reloaded; served monitor functional=${fn:-?}" + +# 3. generate equal-F2, rate-differentiated CSVs + ship. +# Per timestamp: m=R²/F2T series each q=F2T/R times ⇒ F2_ts=m·q²=F2T (equal), +# rate_ts=m·q=R (differs). Series ids reused across timestamps so per-window +# F2 = F2T·W² is equal on every edge while rate = R·W differs. +gen(){ python3 -c " +import sys; R=int(sys.argv[1]); F2T=int(sys.argv[2]); m=R*R//F2T; q=F2T//R +print('timestamp_ms,series_id,value') +for t in range(0,2000,100): + for j in range(m): + for _ in range(q): print(f'{t},s{j},1.0') +" "$1" "$F2_PER_TS"; } +for e in "${!NODEMAP[@]}"; do gen "${RATE[$e]}" > /tmp/$e.csv; scp -q -o BatchMode=yes /tmp/$e.csv "${NODEMAP[$e]}:/tmp/edge.csv"; done + +# 4. launch coordinated trace-replay edges in F2 mode (no key; functional=f2) +for e in edge-lo edge-mid edge-hi; do + slog "${e} on ${NODEMAP[$e]} (rate=${RATE[$e]}/ts, F2=${F2_PER_TS}/ts equal)" + ssh -o BatchMode=yes "${NODEMAP[$e]}" "docker rm -f asap-$e >/dev/null 2>&1; docker run -d --network host --name asap-$e \ + -v /tmp/edge.csv:/tmp/edge.csv:ro asap/otel-app:dev \ + -producer-id=$e -target=${WARM_IP}:4317 -trace-file=/tmp/edge.csv -trace-loop \ + -coordinator-url=${WARM_IP}:${AGG_PORT} -monitor-agg-id=${AGG} -monitor-functional=f2 \ + -monitor-config-url=http://${WARM_IP}:9091/api/v1/streaming-config \ + -edge-id=$e -warm-sample-p=1.0" >/dev/null +done + +# 5. converge, then read granted p +slog "converging 150s..."; sleep 150 +echo "edge,node,rate_per_ts,learned,granted_p" +OUT="${FIG9_OUT:-${MV}/eval-8node/fig9_f2.csv}"; echo "edge,node,rate_per_ts,learned_monitors,granted_p" > "$OUT" +for e in edge-lo edge-mid edge-hi; do + log=$(ssh -o BatchMode=yes "${NODEMAP[$e]}" "docker logs asap-$e 2>&1") + learned=$(echo "$log" | grep -oE 'learned [0-9]+ monitor' | tail -1 | grep -oE '[0-9]+' | head -1) + p=$(echo "$log" | grep -oE 'applied warm-sample-p [0-9.]+' | tail -1 | grep -oE '[0-9.]+$') + echo "$e ${NODEMAP[$e]} rate=${RATE[$e]} learned=${learned:-0} p=${p:-1.0}" + echo "$e,${NODEMAP[$e]},${RATE[$e]},${learned:-0},${p:-1.0}" >> "$OUT" +done +slog "coordinator unconfigured count:" +ssh -o BatchMode=yes "${WARM_HOST}" "docker logs asap-data-plane 2>&1 | grep -c unconfigured" 2>/dev/null || true diff --git a/deploy/mvp-multinode/scripts/fig_costmodel.py b/deploy/mvp-multinode/scripts/fig_costmodel.py new file mode 100644 index 000000000..baf26abb1 --- /dev/null +++ b/deploy/mvp-multinode/scripts/fig_costmodel.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""fig_costmodel.py — Fig 1 (cost-vs-accuracy Pareto) and Fig 11 (storage). + +Blends the REAL measured numbers from the 8-node sweep (Fig 2 bandwidth, Fig 3 +accuracy) with the analytical cost_model (sampling extension + storage), so the +Pareto's anchor points are empirical and the extrapolation is the validated +model. Run from deploy/mvp-multinode/ (imports the cost_model package). + + python3 scripts/fig_costmodel.py --out +""" +from __future__ import annotations +import argparse, os, sys +import matplotlib; matplotlib.use("Agg") +import matplotlib.pyplot as plt + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from cost_model.workloads import mvp_workload, ASAPConfig +from cost_model import model as M + +# ---- REAL measured anchors from the 8-node run ---- +# Fig 2 total backend ingest (MB/s); Fig 3 DDSketch p99 median rel-err. +EMP_BW = {"b0": 25.5, "b1": 1.07, "asap": 0.65} # MB/s (measured) +ASAP_MEDIAN_RELERR = 0.026 # measured (DDSketch p99) + + +def fig1_pareto(out): + w = mvp_workload() + # cost axis = ingest wire normalized to raw-none (b0)=1.0 (measured anchors) + base = EMP_BW["b0"] + pts = [ + ("raw (none)", EMP_BW["b0"]/base, 1.000, "#9e9e9e"), + ("raw (gzip)", EMP_BW["b1"]/base, 1.000, "#616161"), + ("ASAP p=1.0", EMP_BW["asap"]/base, 1.0 - ASAP_MEDIAN_RELERR, "#1565c0"), + ] + # sampling extension: model says edge/wire cost drops ~linearly with p; warm + # accuracy degrades only by the predicted eps_s on small-N (pooled stays ~alpha). + for p, acc in [(0.5, 1.0 - ASAP_MEDIAN_RELERR - 0.01), + (0.25, 1.0 - ASAP_MEDIAN_RELERR - 0.03)]: + cost = (EMP_BW["asap"]/base) * p # sampling scales the warm wire ~∝ p + pts.append((f"ASAP p={p}", cost, acc, "#1e88e5")) + + fig, ax = plt.subplots(figsize=(6.4, 4)) + for lbl, x, y, c in pts: + ax.scatter(x, y, s=90, color=c, zorder=3, edgecolor="k", linewidth=.5) + ax.annotate(lbl, (x, y), textcoords="offset points", xytext=(8, -3), fontsize=8) + # Pareto frontier (lower cost, higher accuracy is better) + asap = sorted([p for p in pts if "ASAP" in p[0]], key=lambda p: p[1]) + ax.plot([p[1] for p in asap], [p[2] for p in asap], "--", color="#1565c0", alpha=.6, zorder=2) + ax.scatter(1.0, 1.0, marker="*", s=200, color="#c62828", zorder=4, label="raw baseline") + ax.set_xscale("log") + ax.set_xlabel("ingest wire cost (× raw-none, measured; log)") + ax.set_ylabel("query accuracy (1 − median rel-err)") + ax.set_title("Fig 1 — accuracy-vs-cost Pareto (measured anchors + sampling extension)") + ax.grid(alpha=.3); ax.legend(fontsize=8, loc="lower right") + p = os.path.join(out, "fig1_pareto.png"); plt.tight_layout(); plt.savefig(p, dpi=140); plt.close() + print(f"[ok] {p}") + + +def fig11_storage(out): + # MEASURED bytes/sample on this hardware (asap-gorilla-go + # TestGorillaXORBytesPerSampleByDataShape): raw=16, gorilla-XOR data-dependent. + HZ = 1.0; DAY = 86400.0 + def bsd(bps): return bps * HZ * DAY # bytes/series/day at 1 Hz + rows = [("raw\n(uncompressed)", 16.0, "#9e9e9e"), + ("gorilla-XOR\n(counter)", 1.34, "#1565c0"), + ("gorilla-XOR\n(smooth ctr)", 1.56, "#1e88e5"), + ("gorilla-XOR\n(random-walk)", 6.96, "#64b5f6")] + fig, ax = plt.subplots(figsize=(6.4, 3.9)) + vals = [bsd(r[1]) for r in rows] + bars = ax.bar([r[0] for r in rows], vals, color=[r[2] for r in rows]) + for b, r, v in zip(bars, rows, vals): + fac = 16.0 / r[1] + ax.text(b.get_x()+b.get_width()/2, v, f"{v/1e3:.0f}KB\n({fac:.1f}×)", ha="center", va="bottom", fontsize=8) + ax.set_ylabel("bytes / series / day (1 Hz)"); ax.grid(axis="y", alpha=.3) + ax.set_title("Fig 11 — cold gorilla storage vs uncompressed raw (measured, this HW)") + p = os.path.join(out, "fig11_storage.png"); plt.tight_layout(); plt.savefig(p, dpi=140); plt.close() + print(f"[ok] {p} raw={bsd(16):.0f} counter={bsd(1.34):.0f} rwalk={bsd(6.96):.0f} B/series/day " + f"(2.3×–12× vs raw, data-dependent)") + + +if __name__ == "__main__": + ap = argparse.ArgumentParser(); ap.add_argument("--out", default=".") + a = ap.parse_args(); os.makedirs(a.out, exist_ok=True) + fig1_pareto(a.out); fig11_storage(a.out) diff --git a/deploy/mvp-multinode/scripts/gos_aniso_cluster.sh b/deploy/mvp-multinode/scripts/gos_aniso_cluster.sh new file mode 100755 index 000000000..1ee84eb93 --- /dev/null +++ b/deploy/mvp-multinode/scripts/gos_aniso_cluster.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# GOS per-cell threshold cluster eval — a REAL measurement of the +# anisotropic-vs-isotropic delta-communication ratio ρ on live agents (replacing +# the closed-form-only TestGosAnisoSavingsRatio comparison): +# +# producer (otel-app five-sketch, Zipf(s) endpoint labels) +# → asap-otel agent (countsketch + gos_delta_epsilon, iso vs aniso arms) +# → 10 GbE → WARM-node OTLP sink, bytes counted by an iptables +# dport-4317 counter on the sink node (kernel-side, exact). +# +# Matrix: zipf_s ∈ {1.1, 1.5, 2.0} × {iso, aniso} (Go rand.Zipf needs s>1). +# Each arm: WARMUP_S to let the first full frame + sketch fill pass, then the +# counter is zeroed and MEASURE_S of steady-state delta traffic is counted — +# so the ratio reflects the DELTA gate, not the shared full-frame cost. +# +# Usage: gos_aniso_cluster.sh +set -uo pipefail + +SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MV="$(cd "${SD}/.." && pwd)" +# shellcheck disable=SC1090 +source "${TOPOLOGY_ENV:-${MV}/topology.8node.env}" +read -r EDGE_HOST _ <<< "${SRC_HOSTS}" +CFG="${MV}/configs/asap" + +WARMUP_S="${WARMUP_S:-20}" +MEASURE_S="${MEASURE_S:-70}" +FREQ_HZ="${FREQ_HZ:-200}" +ENDPOINTS="${ENDPOINTS:-2048}" +SWEEP="${SWEEP:-1.1 1.5 2.0}" +CSV="${GOS_ANISO_OUT:-${MV}/eval-8node/gos_aniso_cluster.csv}" + +cleanup() { + ssh -o BatchMode=yes "${EDGE_HOST}" 'docker rm -f gos-agent gos-producer' >/dev/null 2>&1 + ssh -o BatchMode=yes "${WARM_HOST}" 'docker rm -f gos-sink; sudo iptables -D INPUT -j ASAPGOS 2>/dev/null; sudo iptables -F ASAPGOS 2>/dev/null; sudo iptables -X ASAPGOS 2>/dev/null' >/dev/null 2>&1 +} +trap cleanup EXIT +cleanup + +echo "== sink up on ${WARM_HOST} (OTLP :4317 → nop; iptables byte counter) ==" +scp -q -o BatchMode=yes "${CFG}/gos-eval-sink.yaml" "${WARM_HOST}:/tmp/gos-eval-sink.yaml" +ssh -o BatchMode=yes "${WARM_HOST}" 'docker run -d --network host --name gos-sink \ + -v /tmp/gos-eval-sink.yaml:/etc/otel/config.yaml:ro \ + asap/asap-otel:dev --config=/etc/otel/config.yaml' >/dev/null +ssh -o BatchMode=yes "${WARM_HOST}" 'sudo iptables -N ASAPGOS 2>/dev/null; sudo iptables -F ASAPGOS; \ + sudo iptables -A ASAPGOS -p tcp --dport 4317; \ + sudo iptables -C INPUT -j ASAPGOS 2>/dev/null || sudo iptables -I INPUT -j ASAPGOS' +sleep 3 + +read_bytes() { # → bytes hitting dport 4317 on the sink node since last zero + ssh -o BatchMode=yes "${WARM_HOST}" \ + "sudo iptables -L ASAPGOS -v -n -x | awk '/dpt:4317/ {print \$2}'" +} + +arm() { # zipf_s aniso(true|false) label + local s="$1" aniso="$2" label="$3" + sed -e "s/__ANISO__/${aniso}/" -e "s/__WARM_IP__/${WARM_IP}/" -e "s/__EDGE_ID__/gos-${label}/" \ + "${CFG}/gos-aniso-agent.yaml.tmpl" > /tmp/gos-agent.yaml + scp -q -o BatchMode=yes /tmp/gos-agent.yaml "${EDGE_HOST}:/tmp/gos-agent.yaml" + ssh -o BatchMode=yes "${EDGE_HOST}" 'docker rm -f gos-agent gos-producer >/dev/null 2>&1; \ + docker run -d --network host --name gos-agent \ + -v /tmp/gos-agent.yaml:/etc/otel/config.yaml:ro \ + asap/asap-otel:dev --config=/etc/otel/config.yaml' >/dev/null + sleep 3 + ssh -o BatchMode=yes "${EDGE_HOST}" "docker run -d --network host --name gos-producer \ + asap/otel-app:dev \ + -target=127.0.0.1:4317 -producer-id=gos-p -cardinality=1 -freq-hz=${FREQ_HZ} \ + -five-sketch -five-sketch-endpoints=${ENDPOINTS} -zipf-s=${s} -seed=42" >/dev/null + sleep "${WARMUP_S}" + ssh -o BatchMode=yes "${WARM_HOST}" 'sudo iptables -Z ASAPGOS' + sleep "${MEASURE_S}" + local bytes + bytes=$(read_bytes) + printf " zipf_s=%-4s %-5s bytes_%ss=%s\n" "$s" "$label" "${MEASURE_S}" "${bytes:-0}" + echo "$s,$label,${bytes:-0}" >> "$CSV" + ssh -o BatchMode=yes "${EDGE_HOST}" 'docker rm -f gos-agent gos-producer' >/dev/null 2>&1 +} + +echo "zipf_s,arm,delta_bytes_${MEASURE_S}s" > "$CSV" +echo "== per-cell GOS: agent on ${EDGE_HOST} → sink on ${WARM_HOST} (${WARM_IP}), ${MEASURE_S}s steady-state per arm ==" +for s in ${SWEEP}; do + arm "$s" false iso + arm "$s" true aniso +done + +echo +echo "== ρ (aniso/iso bytes) ==" +python3 - "$CSV" <<'EOF' +import csv, sys +rows = list(csv.reader(open(sys.argv[1])))[1:] +d = {} +for s, arm, b in rows: + d.setdefault(s, {})[arm] = int(b) +print(f"{'zipf_s':>7} {'iso':>12} {'aniso':>12} {'rho':>7}") +for s, v in d.items(): + iso, an = v.get('iso', 0), v.get('aniso', 0) + rho = an / iso if iso else float('nan') + print(f"{s:>7} {iso:>12} {an:>12} {rho:>7.4f}") +EOF +echo "recorded → $CSV" diff --git a/deploy/mvp-multinode/scripts/monitor_e2e.sh b/deploy/mvp-multinode/scripts/monitor_e2e.sh index a5d991d71..b90b24753 100755 --- a/deploy/mvp-multinode/scripts/monitor_e2e.sh +++ b/deploy/mvp-multinode/scripts/monitor_e2e.sh @@ -36,8 +36,12 @@ HARNESS_BIN="$BACKEND_ROOT/target/debug/monitor_coordinator_harness" echo "==> Building Go edge driver" ( cd "$GRPCCLIENT_DIR" && GOFLAGS=-mod=mod go build -o /tmp/e2edriver ./cmd/e2edriver ) -echo "==> Starting coordinator on :$PORT (agg_id=$AGG_ID tau=$TAU window_ms=$WINDOW_MS)" -"$HARNESS_BIN" "$PORT" "$AGG_ID" "$TAU" "$WINDOW_MS" 30 >"$HARNESS_LOG" 2>&1 & +# Sum monitors register under the series-group key (the edge's canonical +# `k=v;k2=v2` label encoding), so the harness config must carry the same key. +MON_KEY="svc=checkout" + +echo "==> Starting coordinator on :$PORT (agg_id=$AGG_ID tau=$TAU window_ms=$WINDOW_MS key=$MON_KEY)" +"$HARNESS_BIN" "$PORT" "$AGG_ID" "$TAU" "$WINDOW_MS" 30 "$MON_KEY" >"$HARNESS_LOG" 2>&1 & HARNESS_PID=$! # Wait for the harness to report readiness. diff --git a/deploy/mvp-multinode/scripts/plot_fig8_fig6b.py b/deploy/mvp-multinode/scripts/plot_fig8_fig6b.py new file mode 100644 index 000000000..2c2f068be --- /dev/null +++ b/deploy/mvp-multinode/scripts/plot_fig8_fig6b.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""plot_fig8_fig6b.py — Fig 8 (cross-layer placement CPU) and Fig 6b (soak RSS). + + python3 plot_fig8_fig6b.py --fig8 --soak --out +""" +from __future__ import annotations +import argparse, csv, os +from collections import defaultdict +import matplotlib; matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + + +def fig8(path, out): + if not os.path.isfile(path): print("[skip] fig8: no csv"); return + data = defaultdict(dict) # placement -> layer -> cpu + for r in csv.DictReader(open(path)): + data[r["placement"]][r["layer"]] = float(r["cpu_perc"]) + layers = ["producer", "agent", "backend"] + placements = list(data) + x = np.arange(len(layers)); w = 0.8/max(1, len(placements)) + fig, ax = plt.subplots(figsize=(6.2, 3.8)) + for i, pl in enumerate(placements): + vals = [data[pl].get(l, 0) for l in layers] + ax.bar(x + i*w, vals, w, label=f"{pl} placement") + for xi, v in zip(x + i*w, vals): + ax.text(xi, v, f"{v:.0f}", ha="center", va="bottom", fontsize=8) + ax.set_xticks(x + w*(len(placements)-1)/2); ax.set_xticklabels(layers) + ax.set_ylabel("CPU (%)"); ax.set_title("Fig 8 — same DDSketch agg, CPU by layer × placement") + ax.legend(fontsize=8); ax.grid(axis="y", alpha=.3) + p = os.path.join(out, "fig8_placement.png"); plt.tight_layout(); plt.savefig(p, dpi=140); plt.close() + print(f"[ok] {p}") + + +def fig6b(path, out): + if not os.path.isfile(path): print("[skip] fig6b: no csv"); return + series = defaultdict(list) + for r in csv.DictReader(open(path)): + try: series[r["container"]].append((float(r["elapsed_s"]), float(r["rss_mib"]))) + except: pass + fig, ax = plt.subplots(figsize=(6.4, 3.8)) + for c, pts in sorted(series.items()): + pts.sort(); xs=[p[0]/60 for p in pts]; ys=[p[1] for p in pts] + if len(xs) < 3: continue + # slope MiB/hr + n=len(xs); mx=sum(xs)/n; my=sum(ys)/n + den=sum((x-mx)**2 for x in xs) or 1 + slope=sum((x-mx)*(y-my) for x,y in zip(xs,ys))/den*60 # MiB/hr + ax.plot(xs, ys, "o-", ms=3, label=f"{c} ({slope:+.0f} MiB/hr)") + ax.set_xlabel("elapsed (min)"); ax.set_ylabel("agent RSS (MiB)") + ax.set_title("Fig 6b — agent RSS over soak (leak check)") + ax.legend(fontsize=8); ax.grid(alpha=.3) + p = os.path.join(out, "fig6b_soak_rss.png"); plt.tight_layout(); plt.savefig(p, dpi=140); plt.close() + print(f"[ok] {p}") + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--fig8"); ap.add_argument("--soak"); ap.add_argument("--out", default=".") + a = ap.parse_args(); os.makedirs(a.out, exist_ok=True) + if a.fig8: fig8(a.fig8, a.out) + if a.soak: fig6b(a.soak, a.out) diff --git a/deploy/mvp-multinode/scripts/plots.py b/deploy/mvp-multinode/scripts/plots.py new file mode 100644 index 000000000..0c186e80e --- /dev/null +++ b/deploy/mvp-multinode/scripts/plots.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""plots.py — turn mvp-multinode sweep artifacts into the §6 paper figures. + +Reads the CSV/JSONL a run_demo_sweep.sh (and scale_fleet.sh) run drops into a +RUN_DIR and renders PNGs into RUN_DIR/figs (or --out). Each figure is guarded: +if its inputs are missing it is skipped with a printed note rather than crashing, +so a partial run still yields whatever figures it can. + +Inputs (as produced by snapshot_resources.sh / run_demo_sweep.sh): + nic-.csv arm,node,window_s,rx_bytes_total,tx_bytes_total,rx_bytes_per_s,tx_bytes_per_s + container-summary-.csv arm,host,container,cpu_mean_perc,cpu_max_perc,mem_mean_mib,mem_max_mib,n_samples + /replay.jsonl one JSON object per query with at least {"latency_ms": float, "query"|"name": str} + scale.csv (scale_fleet.sh) n_agents,arm,sink_rx_MB_s,per_agent_rx_MB_s,agent_cpu_mean_perc,agent_mem_mean_mib + +Usage: python3 plots.py [--out ] +""" +from __future__ import annotations +import argparse, csv, glob, json, os, sys +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +ARM_LABEL = {"b0": "raw OTLP (none)", "b1": "raw OTLP (gzip)", + "b2": "raw PRW (snappy)", "b3": "serf wire", + "asap": "ASAP sketch", "asap-gzip": "ASAP sketch (gzip)"} +ARM_ORDER = ["b0", "b1", "b2", "b3", "asap", "asap-gzip"] +COLOR = {"b0": "#9e9e9e", "b1": "#616161", "b2": "#bdbdbd", "b3": "#757575", + "asap": "#1565c0", "asap-gzip": "#1e88e5"} + + +def _arms_present(run_dir, pattern): + arms = [] + for f in glob.glob(os.path.join(run_dir, pattern)): + a = os.path.basename(f).split("-", 1)[1].rsplit(".", 1)[0] + # nic-.csv / container-summary-.csv → strip leading "summary-" + a = a.replace("summary-", "") + arms.append(a) + return sorted(set(arms), key=lambda x: ARM_ORDER.index(x) if x in ARM_ORDER else 99) + + +def fig_bandwidth(run_dir, out, backend_nodes=("node1", "node2")): + """Fig 2 — total backend ingest wire bandwidth per arm. + + Sums RX bytes/s over the backend sink nodes (cold=node1 VM/gorilla, + warm=node2 data-plane) so the bar is the true wire the backend receives, + counting BOTH the warm sketch tier and the cold gorilla backup for ASAP.""" + rows = {} + for f in glob.glob(os.path.join(run_dir, "nic-*.csv")): + arm = os.path.basename(f)[len("nic-"):-len(".csv")] + tot = 0.0 + with open(f) as fh: + for r in csv.DictReader(fh): + if r["node"] in backend_nodes: + tot += float(r["rx_bytes_per_s"]) + rows[arm] = tot / 1e6 # MB/s + if not rows: + print("[skip] Fig2 bandwidth: no nic-*.csv"); return + arms = [a for a in ARM_ORDER if a in rows] + [a for a in rows if a not in ARM_ORDER] + vals = [rows[a] for a in arms] + fig, ax = plt.subplots(figsize=(6, 3.6)) + bars = ax.bar([ARM_LABEL.get(a, a) for a in arms], vals, + color=[COLOR.get(a, "#1565c0") for a in arms]) + for b, v in zip(bars, vals): + ax.text(b.get_x() + b.get_width() / 2, v, f"{v:.1f}", ha="center", va="bottom", fontsize=8) + # annotate the asap-vs-raw reduction factor (matched gzip pair if available) + if "b1" in rows and "asap-gzip" in rows and rows["asap-gzip"] > 0: + fac = rows["b1"] / rows["asap-gzip"] + ax.set_title(f"Ingest wire bandwidth — ASAP {fac:.0f}× below raw (matched gzip)") + elif "b0" in rows and "asap" in rows and rows["asap"] > 0: + fac = rows["b0"] / rows["asap"] + ax.set_title(f"Ingest wire bandwidth — ASAP {fac:.0f}× below raw (matched none)") + else: + ax.set_title("Ingest wire bandwidth at the sink node") + ax.set_ylabel("sink RX (MB/s)"); ax.grid(axis="y", alpha=0.3) + plt.xticks(rotation=20, ha="right"); plt.tight_layout() + p = os.path.join(out, "fig2_bandwidth.png"); plt.savefig(p, dpi=140); plt.close() + print(f"[ok] {p} ({', '.join(f'{a}={rows[a]:.1f}' for a in arms)} MB/s)") + + +def fig_resource(run_dir, out): + """Fig 6 — agent edge CPU and memory per arm.""" + agg = {} # arm -> (cpu_list, mem_list) for agent containers + for f in glob.glob(os.path.join(run_dir, "container-summary-*.csv")): + with open(f) as fh: + for r in csv.DictReader(fh): + if "agent" not in r["container"]: + continue + a = r["arm"] + agg.setdefault(a, ([], [])) + agg[a][0].append(float(r["cpu_mean_perc"])) + agg[a][1].append(float(r["mem_mean_mib"])) + if not agg: + print("[skip] Fig6 resource: no container-summary-*.csv"); return + arms = [a for a in ARM_ORDER if a in agg] + [a for a in agg if a not in ARM_ORDER] + cpu = [np.mean(agg[a][0]) for a in arms] + mem = [np.mean(agg[a][1]) for a in arms] + fig, (a1, a2) = plt.subplots(1, 2, figsize=(8, 3.4)) + labels = [ARM_LABEL.get(a, a) for a in arms] + cols = [COLOR.get(a, "#1565c0") for a in arms] + a1.bar(labels, cpu, color=cols); a1.set_ylabel("agent CPU (%)"); a1.set_title("Edge CPU") + a2.bar(labels, mem, color=cols); a2.set_ylabel("agent RSS (MiB)"); a2.set_title("Edge memory") + for ax in (a1, a2): + ax.grid(axis="y", alpha=0.3) + for t in ax.get_xticklabels(): + t.set_rotation(20); t.set_ha("right") + plt.tight_layout() + p = os.path.join(out, "fig6_edge_resource.png"); plt.savefig(p, dpi=140); plt.close() + print(f"[ok] {p} (cpu%={dict(zip(arms,[round(c,1) for c in cpu]))})") + + +def _read_latencies(path): + out = [] + try: + with open(path) as fh: + for line in fh: + line = line.strip() + if not line: + continue + d = json.loads(line) + for k in ("duration_ms", "latency_ms", "latency", "ms", "elapsed_ms"): + if k in d: + out.append(float(d[k])); break + except FileNotFoundError: + pass + return out + + +def fig_latency_cdf(run_dir, out): + """Fig 7 — query latency CDF per arm (warm vs raw).""" + series = {} + for arm in ARM_ORDER: + lat = _read_latencies(os.path.join(run_dir, arm, "replay.jsonl")) + if lat: + series[arm] = sorted(lat) + if not series: + print("[skip] Fig7 latency: no */replay.jsonl"); return + fig, ax = plt.subplots(figsize=(6, 3.6)) + for arm, lat in series.items(): + y = np.arange(1, len(lat) + 1) / len(lat) + p50 = lat[int(.5 * len(lat))]; p99 = lat[min(len(lat) - 1, int(.99 * len(lat)))] + ax.plot(lat, y, label=f"{ARM_LABEL.get(arm, arm)} (p50={p50:.1f} p99={p99:.1f}ms)", + color=COLOR.get(arm, "#1565c0"), lw=1.8) + ax.set_xlabel("query latency (ms)"); ax.set_ylabel("CDF"); ax.set_xscale("log") + ax.set_title("PromQL query latency CDF"); ax.grid(alpha=0.3); ax.legend(fontsize=7) + plt.tight_layout() + p = os.path.join(out, "fig7_latency_cdf.png"); plt.savefig(p, dpi=140); plt.close() + print(f"[ok] {p} (arms={list(series)})") + + +def fig_scaling(run_dir, out): + """Fig 10 — per-agent bandwidth & CPU vs fleet size N (from scale.csv).""" + f = os.path.join(run_dir, "scale.csv") + if not os.path.isfile(f): + print("[skip] Fig10 scaling: no scale.csv"); return + by_arm = {} + with open(f) as fh: + for r in csv.DictReader(fh): + by_arm.setdefault(r["arm"], []).append(r) + fig, (a1, a2) = plt.subplots(1, 2, figsize=(8.4, 3.4)) + for arm, rows in by_arm.items(): + rows = sorted(rows, key=lambda r: int(r["n_agents"])) + n = [int(r["n_agents"]) for r in rows] + pa = [float(r["per_agent_rx_MB_s"]) for r in rows] + cpu = [float(r["agent_cpu_mean_perc"]) for r in rows] + c = COLOR.get(arm, "#1565c0") + a1.plot(n, pa, "o-", color=c, label=ARM_LABEL.get(arm, arm)) + a2.plot(n, cpu, "o-", color=c, label=ARM_LABEL.get(arm, arm)) + if "sim" in arm: + a1.lines[-1].set_linestyle("--"); a2.lines[-1].set_linestyle("--") + a1.set_xlabel("fleet size N (agents)"); a1.set_ylabel("per-agent wire (MB/s)") + a1.set_title("Per-agent bandwidth stays flat"); a1.grid(alpha=0.3); a1.legend(fontsize=7) + a2.set_xlabel("fleet size N (agents)"); a2.set_ylabel("agent CPU (%)") + a2.set_title("Per-agent CPU stays flat"); a2.grid(alpha=0.3); a2.legend(fontsize=7) + plt.tight_layout() + p = os.path.join(out, "fig10_scaling.png"); plt.savefig(p, dpi=140); plt.close() + print(f"[ok] {p}") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("run_dir") + ap.add_argument("--out", default=None) + args = ap.parse_args() + out = args.out or os.path.join(args.run_dir, "figs") + os.makedirs(out, exist_ok=True) + print(f"[plots] run_dir={args.run_dir} out={out}") + fig_bandwidth(args.run_dir, out) + fig_resource(args.run_dir, out) + fig_latency_cdf(args.run_dir, out) + fig_scaling(args.run_dir, out) + + +if __name__ == "__main__": + main() diff --git a/deploy/mvp-multinode/scripts/run_demo.sh b/deploy/mvp-multinode/scripts/run_demo.sh index 210db124d..ed7dd348d 100755 --- a/deploy/mvp-multinode/scripts/run_demo.sh +++ b/deploy/mvp-multinode/scripts/run_demo.sh @@ -51,7 +51,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PKG_DIR="$(dirname "${SCRIPT_DIR}")" -source "${PKG_DIR}/topology.env" +# TOPOLOGY_ENV lets a caller point at an alternate topology file (e.g. the +# 8-node scaling cluster) without editing the committed 4-node default. +TOPOLOGY_ENV="${TOPOLOGY_ENV:-${PKG_DIR}/topology.env}" +source "${TOPOLOGY_ENV}" # Derive ROOT/CONFIG_SRC deterministically from this script's location so the # rsync source in sync_to() is always the configs that ship alongside this @@ -152,6 +155,7 @@ build_images() { log " → asap/otel-app:dev" DOCKER_BUILDKIT=1 docker build -f "${ROOT}/deploy/docker/Dockerfile.otel-app" \ --build-context sketchlib-go="${SKETCHLIB_GO}" \ + --build-context asap-precompute-go="${ROOT}/asap-precompute-go" \ -t asap/otel-app:dev "${ROOT}" # ── gorilla-merger (cold sink) ── @@ -216,7 +220,7 @@ sync_to() { "${CONFIG_SRC}/" \ "${node}:/mydata/mvp-multinode/configs/" rsync -a "${ROOT}/deploy/mvp-singlenode/scripts/" "${node}:/mydata/mvp-multinode/scripts/" - rsync -a "${PKG_DIR}/topology.env" "${node}:/mydata/mvp-multinode/topology.env" + rsync -a "${TOPOLOGY_ENV}" "${node}:/mydata/mvp-multinode/topology.env" } sync_all_nodes() { @@ -484,7 +488,7 @@ backend_up() { --enable-otel-ingest \ --otel-grpc-port=${DP_OTLP_GRPC_PORT:-4317} \ --otel-http-port=${DP_OTLP_HTTP_PORT:-4318} \ - ${persist_flags} + ${persist_flags} ${DP_MONITOR_FLAGS:-} # asap-control-plane (ASAP only) — control plane process. Brought # up AFTER the data plane so the control plane's startup pre-pop @@ -674,6 +678,7 @@ agents_up() { log "node0 producer-a-${i} up" docker_run_on "${NODE0_HOST}" --cpus=1 --memory=4g --memory-swap=4g \ --name asap-producer-a-${i} \ + ${OTELAPP_TRACE_MOUNT:-} \ asap/otel-app:dev \ -target=agent-a:4317 \ -producer-id=p-a-${i} \ @@ -684,6 +689,9 @@ agents_up() { -max-buffer-per-series=${OTELAPP_MAX_BUFFER_PER_SERIES} \ -freshness-probes=${OTELAPP_FRESHNESS_PROBES} \ -freshness-probe-hz=${OTELAPP_FRESHNESS_PROBE_HZ} \ + -warm-sample-p=${OTELAPP_WARM_SAMPLE_P:-1.0} \ + ${OTELAPP_TRACE_ARGS:-} \ + ${OTELAPP_COORD_ARGS:-} \ -seed=${OTELAPP_SEED:-42} done for i in $(seq 1 ${N_PRODUCERS_PER_NODE}); do @@ -691,6 +699,7 @@ agents_up() { log "node3 producer-b-${i} up" docker_run_on "${NODE3_HOST}" --cpus=1 --memory=4g --memory-swap=4g \ --name asap-producer-b-${i} \ + ${OTELAPP_TRACE_MOUNT:-} \ asap/otel-app:dev \ -target=agent-b:4317 \ -producer-id=p-b-${i} \ @@ -701,6 +710,9 @@ agents_up() { -max-buffer-per-series=${OTELAPP_MAX_BUFFER_PER_SERIES} \ -freshness-probes=${OTELAPP_FRESHNESS_PROBES} \ -freshness-probe-hz=${OTELAPP_FRESHNESS_PROBE_HZ} \ + -warm-sample-p=${OTELAPP_WARM_SAMPLE_P:-1.0} \ + ${OTELAPP_TRACE_ARGS:-} \ + ${OTELAPP_COORD_ARGS:-} \ -seed=${OTELAPP_SEED:-42} done } @@ -808,6 +820,11 @@ run_arm() { arm_down || true } +# Allow sourcing as a library so other drivers (e.g. scale_fleet.sh) can compose +# backend_up + docker_run_on + topology for custom fleet sizes without invoking +# the CLI dispatch below. +if [ -n "${RUN_DEMO_LIB:-}" ]; then return 0 2>/dev/null || true; fi + cmd=${1:-help} case "${cmd}" in sync) sync_all_nodes ;; diff --git a/deploy/mvp-multinode/scripts/run_demo_sweep.sh b/deploy/mvp-multinode/scripts/run_demo_sweep.sh index c4af97823..297fa3fce 100755 --- a/deploy/mvp-multinode/scripts/run_demo_sweep.sh +++ b/deploy/mvp-multinode/scripts/run_demo_sweep.sh @@ -13,7 +13,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PKG_DIR="$(dirname "${SCRIPT_DIR}")" -source "${PKG_DIR}/topology.env" +TOPOLOGY_ENV="${TOPOLOGY_ENV:-${PKG_DIR}/topology.env}" +source "${TOPOLOGY_ENV}" RUN_ID="${RUN_ID:-mvp-multinode-$(date +%Y%m%d-%H%M%S)}" RUN_DIR="${RUN_BASE}/${RUN_ID}" diff --git a/deploy/mvp-multinode/scripts/scale_fleet.sh b/deploy/mvp-multinode/scripts/scale_fleet.sh new file mode 100644 index 000000000..37756d4d5 --- /dev/null +++ b/deploy/mvp-multinode/scripts/scale_fleet.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# scale_fleet.sh — Fig 10 driver: sweep fleet size N (agents) and measure that +# per-agent wire bandwidth and CPU stay flat as the fleet grows. +# +# One supervised asap agent per source host (host-network :4317), N producers +# each, all feeding the single warm backend (node2). For each N we soak, read +# the backend sink NIC RX and the mean per-agent CPU, and write a row to +# scale.csv. Physical N is capped at the number of SRC_NODES (5 here: node3-7); +# extend to N=100 with cost_model/simulator.py, anchored on these points. +# +# Usage: +# TOPOLOGY_ENV=.../topology.8node.env SKIP_BUILD=1 SKIP_LOAD=1 \ +# scale_fleet.sh [N_LIST] [SOAK_S] +# Reuses run_demo.sh as a library (RUN_DEMO_LIB=1). +set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +N_LIST="${1:-1 2 3 4 5}" +SOAK="${2:-${SOAK_S:-45}}" +WARMUP="${WARMUP_S:-60}" + +# pull in topology + bring-up primitives (backend_up, docker_run_on, ADD_HOSTS, …) +export RUN_DEMO_LIB=1 +source "${SCRIPT_DIR}/run_demo.sh" + +read -ra SRC <<< "${SRC_HOSTS:-${SRC_NODES:-node3 node4 node5 node6 node7}}" +RUN_ID="${RUN_ID:-scale-$(date +%Y%m%d-%H%M%S)}" +OUT="${RUN_BASE}/${RUN_ID}"; mkdir -p "${OUT}" +CSV="${OUT}/scale.csv" +echo "n_agents,arm,sink_rx_MB_s,per_agent_rx_MB_s,agent_cpu_mean_perc,agent_mem_mean_mib" > "${CSV}" +slog() { printf '[%s] [scale] %s\n' "$(date +%H:%M:%S)" "$*" | tee -a "${OUT}/scale.log"; } + +IFACE_PROBE='iface=$(ip -4 -o addr show | awk '"'"'$4 ~ /^10\.10\.1\./ {print $2; exit}'"'"'); iface=${iface:-eth0}' +nic_rx() { ssh -o BatchMode=yes "$1" "${IFACE_PROBE}"'; cat /sys/class/net/$iface/statistics/rx_bytes'; } + +# launch one supervised asap agent + its producers on a source host +launch_agent() { + local node=$1 idx=$2 + slog "agent-${idx} on ${node} (supervised) + ${N_PRODUCERS_PER_NODE} producers" + docker_run_on "${node}" --cpus=4 --memory=12g --memory-swap=12g \ + --name "asap-agent-${idx}" --hostname "agent-${idx}" \ + -e "X_AGENT_ID=agent-${idx}" -e "AGENT_ID=agent-${idx}" \ + -v /mydata/mvp-multinode/configs/asap/supervisor.yaml:/etc/otel/supervisor.yaml:ro \ + asap/asap-otel-supervised:dev --config /etc/otel/supervisor.yaml + sleep 5 + local i + for i in $(seq 1 "${N_PRODUCERS_PER_NODE}"); do + docker_run_on "${node}" --cpus=1 --memory=4g --memory-swap=4g \ + --name "asap-producer-${idx}-${i}" \ + asap/otel-app:dev \ + -target=127.0.0.1:4317 -producer-id="p-${idx}-${i}" \ + -cardinality="${PER_AGENT_CARDINALITY}" -freq-hz="${OTELAPP_FREQ_HZ}" \ + -sdk-window="${OTELAPP_SDK_WINDOW}" -agg="${OTELAPP_SDK_AGG}" \ + -max-buffer-per-series="${OTELAPP_MAX_BUFFER_PER_SERIES}" \ + -freshness-probes=false -seed="$((42 + idx))" + done +} + +# mean CPU% / mem(MiB) of asap-agent-* across the active source hosts. +# Averages SAMPLES snapshots (a single docker-stats snapshot is far too noisy — +# it caught 174% then 2.8% on adjacent N in v1), spaced over the soak. +agent_stats() { + local nodes=("$@") n s + local SAMPLES="${STAT_SAMPLES:-6}" + : > "${OUT}/.stats_tmp" + for s in $(seq 1 "${SAMPLES}"); do + for n in "${nodes[@]}"; do + ssh -o BatchMode=yes "$n" \ + "docker stats --no-stream --format '{{.Name}} {{.CPUPerc}} {{.MemUsage}}' 2>/dev/null | grep '^asap-agent-'" \ + >> "${OUT}/.stats_tmp" 2>/dev/null || true + done + [ "$s" -lt "${SAMPLES}" ] && sleep 4 + done + python3 - "${OUT}/.stats_tmp" <<'PY' +import sys, re +cpu=[]; mem=[] +for ln in open(sys.argv[1]): + p=ln.split() + if len(p)<2: continue + try: cpu.append(float(p[1].rstrip('%'))) + except: pass + m=re.search(r'([\d.]+)([KMG]i?B)', ln) + if m: + v=float(m.group(1)); u=m.group(2) + f={'KiB':1/1024,'MiB':1,'GiB':1024,'KB':1/1024,'MB':1,'GB':1024}.get(u,1) + mem.append(v*f) +print(f"{(sum(cpu)/len(cpu) if cpu else 0):.1f},{(sum(mem)/len(mem) if mem else 0):.1f}") +PY +} + +# Full teardown across ALL source nodes (node3-7) + both backends. run_demo.sh's +# arm_down only reaps NODE0/NODE3 (the 4-node harness's two source slots), so on +# the 8-node fleet agents on node5-7 would survive into the next N and collide. +scale_down() { + local n + for n in "${SRC[@]}"; do stop_node "$n" >/dev/null 2>&1 || true; done + stop_node "${COLD_HOST}" >/dev/null 2>&1 || true + stop_node "${WARM_HOST}" >/dev/null 2>&1 || true + backend_down >/dev/null 2>&1 || true +} + +ARM=asap +slog "RUN_ID=${RUN_ID} N_LIST='${N_LIST}' SOAK=${SOAK}s SRC='${SRC[*]}' card=${PER_AGENT_CARDINALITY} freq=${OTELAPP_FREQ_HZ} prod/agent=${N_PRODUCERS_PER_NODE}" + +for N in ${N_LIST}; do + [ "${N}" -gt "${#SRC[@]}" ] && { slog "N=${N} exceeds ${#SRC[@]} source nodes — skipping (use simulator)"; continue; } + slog "=== N=${N} ===" + scale_down; sleep 3 + backend_up "${ARM}" >>"${OUT}/scale.log" 2>&1; sleep 6 + active=(); for k in $(seq 0 $((N-1))); do launch_agent "${SRC[$k]}" "$((k+1))"; active+=("${SRC[$k]}"); done + slog "warmup ${WARMUP}s"; sleep "${WARMUP}" + + rx0=$(nic_rx "${WARM_HOST}"); t0=$(date +%s.%N) + sleep "${SOAK}" + rx1=$(nic_rx "${WARM_HOST}"); t1=$(date +%s.%N) + stats=$(agent_stats "${active[@]}") + rxps=$(awk -v a="$rx0" -v b="$rx1" -v t0="$t0" -v t1="$t1" 'BEGIN{printf "%.3f",(b-a)/((t1-t0)*1e6)}') + peragent=$(awk -v r="$rxps" -v n="$N" 'BEGIN{printf "%.3f", r/n}') + echo "${N},${ARM},${rxps},${peragent},${stats}" >> "${CSV}" + slog "N=${N}: sink_rx=${rxps} MB/s per_agent=${peragent} MB/s agent(cpu%,mem MiB)=${stats}" + scale_down +done + +slog "done -> ${CSV}"; cat "${CSV}" diff --git a/deploy/mvp-multinode/scripts/snapshot_resources.sh b/deploy/mvp-multinode/scripts/snapshot_resources.sh index f0df56936..7c0b553a3 100755 --- a/deploy/mvp-multinode/scripts/snapshot_resources.sh +++ b/deploy/mvp-multinode/scripts/snapshot_resources.sh @@ -10,14 +10,21 @@ DUR=${2:-60} OUT=${3:-/mydata/mvp-multinode/results/snap} mkdir -p "$OUT" -NODES=(node0 node1 node2 node3) +# Node set is configurable so the same snapshotter works for the 4-node MVP +# and the 8-node scaling cluster. Defaults to the original 4 nodes. +read -ra NODES <<< "${SNAP_NODES:-node0 node1 node2 node3}" + +# Resolve the experiment-LAN NIC by IP subnet instead of a hard-coded name — +# CloudLab hardware varies (enp130s0f0 on the original 4-node profile, eno2 on +# the 8-node profile). Mirrors the auto-detect already in measure_nic_bw.sh. +IFACE_PROBE='iface=$(ip -4 -o addr show | awk '"'"'$4 ~ /^10\.10\.1\./ {print $2; exit}'"'"'); iface=${iface:-eth0}' echo "[snap] arm=${ARM} duration=${DUR}s out=${OUT}" # 1. Capture START NIC bytes on each node and START container stats. declare -A start_rx start_tx start_t end_rx end_tx end_t for n in "${NODES[@]}"; do - start=$(ssh "$n" 'iface=enp130s0f0; echo "$(cat /sys/class/net/$iface/statistics/rx_bytes) $(cat /sys/class/net/$iface/statistics/tx_bytes) $(date +%s.%N)"') + start=$(ssh "$n" "${IFACE_PROBE}"'; echo "$(cat /sys/class/net/$iface/statistics/rx_bytes) $(cat /sys/class/net/$iface/statistics/tx_bytes) $(date +%s.%N)"') start_rx[$n]=$(echo "$start" | awk '{print $1}') start_tx[$n]=$(echo "$start" | awk '{print $2}') start_t[$n]=$(echo "$start" | awk '{print $3}') @@ -58,7 +65,7 @@ wait # 3. Capture END NIC bytes on each node. for n in "${NODES[@]}"; do - ev=$(ssh "$n" 'iface=enp130s0f0; echo "$(cat /sys/class/net/$iface/statistics/rx_bytes) $(cat /sys/class/net/$iface/statistics/tx_bytes) $(date +%s.%N)"') + ev=$(ssh "$n" "${IFACE_PROBE}"'; echo "$(cat /sys/class/net/$iface/statistics/rx_bytes) $(cat /sys/class/net/$iface/statistics/tx_bytes) $(date +%s.%N)"') end_rx[$n]=$(echo "$ev" | awk '{print $1}') end_tx[$n]=$(echo "$ev" | awk '{print $2}') end_t[$n]=$(echo "$ev" | awk '{print $3}') diff --git a/deploy/mvp-multinode/scripts/soak_rss.sh b/deploy/mvp-multinode/scripts/soak_rss.sh new file mode 100644 index 000000000..a0523eb9f --- /dev/null +++ b/deploy/mvp-multinode/scripts/soak_rss.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# soak_rss.sh — Fig 6b: bring up the asap arm and sample agent RSS over a soak +# window to check for a memory leak (slope of RSS vs time). A 24h soak is the +# paper target; this runs SOAK_MIN minutes (default 30) as the in-session proxy +# and reports the MiB/hour slope (extrapolated to 24h). +# +# Usage: TOPOLOGY_ENV=... SKIP_BUILD=1 SKIP_LOAD=1 soak_rss.sh [SOAK_MIN] +set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TOPOLOGY_ENV="${TOPOLOGY_ENV:-${SCRIPT_DIR}/../topology.8node.env}" +source "${TOPOLOGY_ENV}" +SOAK_MIN="${1:-30}" +RUN_ID="${RUN_ID:-soak-$(date +%H%M%S)}" +OUT="${RUN_BASE}/${RUN_ID}"; mkdir -p "${OUT}" +CSV="${OUT}/soak_rss.csv"; echo "elapsed_s,node,container,rss_mib,cpu_perc" > "${CSV}" +log(){ printf '[%s] [soak] %s\n' "$(date +%H:%M:%S)" "$*" | tee -a "${OUT}/soak.log"; } + +# bring up the asap arm (reuses run_demo.sh up) +log "bringing up asap arm for ${SOAK_MIN}min soak" +SKIP_BUILD=1 SKIP_LOAD=1 bash "${SCRIPT_DIR}/run_demo.sh" up asap >>"${OUT}/soak.log" 2>&1 + +# the two data-source hosts the asap arm runs agents on (role-based; these are +# the harness's two NODE0/NODE3 source slots = the first two SRC_HOSTS). +read -r _SRCA _SRCB _rest <<< "${SRC_HOSTS:-${NODE0_HOST:-node3} ${NODE3_HOST:-node4}}" +AGENT_NODES=("${_SRCA}" "${_SRCB}") +start=$(date +%s); end=$((start + SOAK_MIN*60)) +log "sampling RSS every 30s until +${SOAK_MIN}min" +while [ "$(date +%s)" -lt "${end}" ]; do + el=$(( $(date +%s) - start )) + for n in "${AGENT_NODES[@]}"; do + ssh -o BatchMode=yes "$n" "docker stats --no-stream --format '{{.Name}}|{{.MemUsage}}|{{.CPUPerc}}' 2>/dev/null | grep '^asap-agent'" 2>/dev/null \ + | while IFS='|' read -r name mem cpu; do + mib=$(echo "$mem" | awk -F'/' '{print $1}' | awk '{v=$1; sub(/[A-Za-z]+/,"",v); u=$1; sub(/^[0-9.]+/,"",u); m=1; if(u=="GiB")m=1024; else if(u=="KiB")m=1/1024; printf "%.1f", v*m}') + echo "${el},${n},${name},${mib},$(echo "$cpu" | tr -d '%')" >> "${CSV}" + done + done + sleep 30 +done + +log "soak done; computing RSS slope" +python3 - "${CSV}" <<'PY' +import csv, sys +from collections import defaultdict +rows=defaultdict(list) +for r in csv.DictReader(open(sys.argv[1])): + try: rows[(r['node'],r['container'])].append((float(r['elapsed_s']), float(r['rss_mib']))) + except: pass +print("container,n,first_mib,last_mib,slope_mib_per_hr,proj_24h_mib") +for k,v in sorted(rows.items()): + if len(v)<3: continue + v.sort() + n=len(v); xs=[p[0] for p in v]; ys=[p[1] for p in v] + mx=sum(xs)/n; my=sum(ys)/n + den=sum((x-mx)**2 for x in xs) or 1 + slope=sum((x-mx)*(y-my) for x,y in zip(xs,ys))/den # MiB/sec + sph=slope*3600 + print(f"{k[1]},{n},{ys[0]:.0f},{ys[-1]:.0f},{sph:.2f},{ys[-1]+sph*24:.0f}") +PY + +SKIP_BUILD=1 SKIP_LOAD=1 bash "${SCRIPT_DIR}/run_demo.sh" down >>"${OUT}/soak.log" 2>&1 +log "torn down -> ${CSV}" diff --git a/deploy/mvp-multinode/topology.8node.env b/deploy/mvp-multinode/topology.8node.env new file mode 100644 index 000000000..6123edf69 --- /dev/null +++ b/deploy/mvp-multinode/topology.8node.env @@ -0,0 +1,166 @@ +# Multi-node MVP topology — 8-node CloudLab cluster on 10.10.1.0/24, 10Gbps. +# Sourced by run_demo.sh / run_demo_sweep.sh via TOPOLOGY_ENV override. +# +# Role split for the 8-node eval cluster: +# node0 (10.10.1.1) DRIVER / orchestrator (runs the harness, no containers) +# node1 (10.10.1.2) COLD backend -> harness NODE1 +# node2 (10.10.1.3) WARM backend -> harness NODE2 +# node3 (10.10.1.4) data source A -> harness NODE0 (agent-a) +# node4 (10.10.1.5) data source B -> harness NODE3 (agent-b) +# node5-7 extra source capacity, used by the scaling driver (SRC_NODES) +# +# Machines are named by ROLE (not an opaque NODEk index). USE THESE in new +# scripts; the NODE0..NODE3 below are derived compatibility shims for the team's +# run_demo.sh (which still speaks its 4-slot model). + +# ── Role-based names (the source of truth) ── +DRIVER_HOST="${DRIVER_HOST:-node0}"; DRIVER_IP="${DRIVER_IP:-10.10.1.1}" # orchestrator, no containers +COLD_HOST="${COLD_HOST:-node1}"; COLD_IP="${COLD_IP:-10.10.1.2}" # MinIO, Thanos {query,store,compact}, gorilla-merger, Prometheus, VictoriaMetrics +WARM_HOST="${WARM_HOST:-node2}"; WARM_IP="${WARM_IP:-10.10.1.3}" # data_plane + control_plane +SRC_HOSTS="${SRC_HOSTS:-node3 node4 node5 node6 node7}" # data sources (agents + producers) +SRC_IPS="${SRC_IPS:-10.10.1.4 10.10.1.5 10.10.1.6 10.10.1.7 10.10.1.8}" + +# ── Compatibility shims for run_demo.sh's 4-slot model, DERIVED from the roles +# above (one source of truth). Its NODE0 = data-source-A, NODE1 = cold, +# NODE2 = warm, NODE3 = data-source-B. +read -r _SRC_A _SRC_B _rest <<< "${SRC_HOSTS}" +read -r _SRC_A_IP _SRC_B_IP _rest_ip <<< "${SRC_IPS}" +NODE0_HOST="${NODE0_HOST:-${_SRC_A}}"; NODE0_IP="${NODE0_IP:-${_SRC_A_IP}}" # data source A / agent-a +NODE1_HOST="${NODE1_HOST:-${COLD_HOST}}"; NODE1_IP="${NODE1_IP:-${COLD_IP}}" # COLD backend +NODE2_HOST="${NODE2_HOST:-${WARM_HOST}}"; NODE2_IP="${NODE2_IP:-${WARM_IP}}" # WARM backend +NODE3_HOST="${NODE3_HOST:-${_SRC_B}}"; NODE3_IP="${NODE3_IP:-${_SRC_B_IP}}" # data source B / agent-b + +# ── Scaling / coordinated-sampling drivers (role-based) ── +SRC_NODES="${SRC_NODES:-${SRC_HOSTS}}" # full source pool node3..node7 +SNAP_NODES="${SNAP_NODES:-${COLD_HOST} ${WARM_HOST} ${SRC_HOSTS}}" # resource-snapshot set (backends + sources) + +# ── DNS aliases that get injected via --add-host on every container. +# Preserves the existing service-name references in the agent/backend +# YAML configs (e.g., data-plane:9091, data-plane:4317, control-plane:4320, +# minio:9000, thanos-query:10903) without rewriting any of them. +# +# Issue #400 (2026-05): the `gateway:` alias was retired with the +# asap-gateway service — agents now push OTLP straight to data-plane:4317. +# +# COLD/WARM SPLIT (2026-05): the cold/thanos backend (MinIO, Thanos +# {query,store,compact}, gorilla-merger, Prometheus) was moved off node2 +# onto the previously-idle node1, so the WARM engine (data_plane + +# control_plane) gets a dedicated host. The data_plane's unbounded +# in-memory SketchStore had been able to exhaust the whole 251 GiB node; +# isolating it (plus the per-container --memory caps in run_demo.sh) bounds +# the blast radius. Because --add-host injects this WHOLE map into every +# container regardless of node, only the IPs below change — every container +# still resolves every alias. So: warm aliases → NODE2_IP, cold aliases → +# NODE1_IP. The data_plane (node2) reaches thanos-query/minio (node1) and +# the agents cold-ship to gorilla-merger (node1) purely via these aliases. +ADD_HOSTS=( + # ── WARM backend ── + --add-host=control-plane:${WARM_IP} + --add-host=data-plane:${WARM_IP} + # ── COLD / thanos backend ── + --add-host=minio:${COLD_IP} + --add-host=prometheus:${COLD_IP} + --add-host=prometheus-b0:${COLD_IP} + --add-host=victoriametrics:${COLD_IP} + --add-host=thanos-query:${COLD_IP} + --add-host=thanos-store-gateway:${COLD_IP} + --add-host=thanos-compact:${COLD_IP} + --add-host=gorilla-merger:${COLD_IP} + # ── data sources (the two harness source slots) ── + --add-host=agent-a:${NODE0_IP} + --add-host=agent-b:${NODE3_IP} + # b3 (serf wire codec) serf-gateway also lives on the cold node. Its :9000 + # would collide with MinIO:9000, but the b3 and asap arms never run together, + # so there's no concurrent conflict. + --add-host=serf-gw:${COLD_IP} +) + +# ── Workload sizing for ≥1 GB/s baseline raw bandwidth. +# 200K series total @ 100 Hz × ~50 B/OTLP sample ≈ 1 GB/s aggregate. +# Split: 5 producers/node × 20K series × 100 Hz on node0 + node3. +PER_AGENT_CARDINALITY="${PER_AGENT_CARDINALITY:-1000}" +OTELAPP_FREQ_HZ="${OTELAPP_FREQ_HZ:-100}" +# Raw-buffer (the patched OTel SDK extension): every generated event is kept as +# its own DataPoint and emitted on the periodic-reader tick, so the agent sees +# the full FREQ_HZ-per-series stream (NOT one cumulative roll-up per window). +# This is what feeds the cold gorilla archive its dense, per-series raw samples +# — the cold tier is the raw backup, so a window must carry ~FREQ_HZ×WINDOW +# samples/series, not 1. +# +# `default` (cumulative Sum/LastValue) collapses all FREQ_HZ events in a window +# into a SINGLE datapoint/series, starving the cold archive to ~1 sample/series. +# Use it only for a stock-OTel comparison arm, and only with -sdk-window +# small enough that each event lands in its own window. +OTELAPP_SDK_WINDOW="${OTELAPP_SDK_WINDOW:-1s}" +OTELAPP_SDK_AGG="${OTELAPP_SDK_AGG:-raw-buffer}" +# Per-series event buffer cap. Must hold ≥ FREQ_HZ × WINDOW events/series so a +# full window's raw stream survives without drops (100 Hz × 1 s = 100; 4096 +# leaves generous headroom for jitter / longer windows). Events beyond the cap +# are dropped + counted, not aggregated. +OTELAPP_MAX_BUFFER_PER_SERIES="${OTELAPP_MAX_BUFFER_PER_SERIES:-4096}" +# -freshness-probes is a bool flag (true/false). +OTELAPP_FRESHNESS_PROBES="${OTELAPP_FRESHNESS_PROBES:-true}" +OTELAPP_FRESHNESS_PROBE_HZ="${OTELAPP_FRESHNESS_PROBE_HZ:-1.0}" +# The deprecated rate knob was dropped (it was ignored under SDK aggregation). +# The built-in five-sketch workload (5 sketch types × 5K series = 25K sketch +# instances per producer) always runs — the controller now derives each +# metric's storage tier (warm-only sketch vs cold archive) on the asap_edge +# processor, so the producer just emits the metrics unconditionally. The +# five-sketch on/off gate was removed; cardinality knobs +# (-five-sketch-user-pool etc.) still apply. +# 5 producers/node × 1K cardinality at 100Hz raw-buffer → ~250 MB/s aggregate +# raw event rate, ~50 MB/s aggregate wire. Higher producer counts crash the +# docker daemon on the producer nodes (each raw-buffer producer is +# CPU+RAM hungry at this freq). +N_PRODUCERS_PER_NODE="${N_PRODUCERS_PER_NODE:-5}" + +# ── Soak / replay timing. +# WARMUP_S=60 (was 30) gives the agent's first 30s tumbling window time to +# close and its sketch state to be flushed to the backend before replay +# starts. With WARMUP_S=30 the first ~14 seconds of soak fired before any +# window had closed → backend had no sketch state → asap-arm queries with +# `quantile_over_time(...[5m])` / `sum_over_time(...[5m])` / instant +# `sum by (zone) (http_requests_total)` returned 14 leading "error/200"s. +# `rate(...[5m])` queries survived only because the 5-min lookback eventually +# found a closed window. 60s = single tumble + headroom; bump to 90 if a +# future run still shows >3 leading failures on the same queries. +WARMUP_S="${WARMUP_S:-60}" +SOAK_S="${SOAK_S:-90}" + +# ── Image set (must be loaded on every node). +# +# data_plane reorg (2026-05): the control plane and data plane now ship +# as TWO separate images built from ASAPQuery-backend's per-crate +# Dockerfiles — `asap/data-plane:dev` (data_plane / query backend, +# entrypoint `/usr/local/bin/data_plane`) and `asap/control-plane:dev` +# (control_plane / controller, entrypoint `/usr/local/bin/control_plane`). +# This retires the old combined query-backend image (built by +# the now-deleted deploy/docker/Dockerfile.backend). The two binaries run +# as separate processes — `run_demo.sh::backend_up()` brings up an +# `asap-control-plane` container alongside the `asap-data-plane` container +# on node2, mirroring the singlenode compose stack's two services. +IMAGES=( + asap/asap-otel:dev + asap/otel-app:dev + asap/data-plane:dev + asap/control-plane:dev + # Thanos-Receive-style merger: ingests Gorilla XOR fragments over HTTP, + # serves the <2h pending window over a Thanos StoreAPI, ships 2h blocks + # to the asap-gorilla-tsdb bucket. Built by /mydata/asap-build-all.sh. + asap/gorilla-merger:dev +) +EXTERNAL_IMAGES=( + minio/minio:latest + minio/mc:latest + prom/prometheus:v2.55.0 + quay.io/thanos/thanos:v0.41.0 +) + +# ── Paths (assume /mydata on every node). +ROOT=/mydata/ASAPCollector +BACKEND=/mydata/ASAPQuery-backend +SKETCHLIB=/mydata/asap_sketchlib +DEPLOY=/mydata/mvp-multinode +CONFIG_SRC=${ROOT}/deploy/mvp-multinode/configs +RUN_BASE=${DEPLOY}/results +LOG_BASE=${DEPLOY}/logs diff --git a/docs/continuous-monitoring-aggregation-taxonomy.md b/docs/continuous-monitoring-aggregation-taxonomy.md index aaef4b0f3..78b344e4b 100644 --- a/docs/continuous-monitoring-aggregation-taxonomy.md +++ b/docs/continuous-monitoring-aggregation-taxonomy.md @@ -1,5 +1,8 @@ # Continuous Monitoring — Aggregation Taxonomy & the `[t1,t2]` Window Question +> **Part of the CDM/GOS theory set.** The canonical, unified derivations live in [`sampling-cdm-gos-derivations.md`](sampling-cdm-gos-derivations.md) — start there. This doc is retained for the full per-series×window / series×timestamp / series×window taxonomy and the (agg_id, group_key) monitor-keying rule, which the canonical doc does not reproduce. + + > Design note. Companion to > [continuous-monitoring-tumbling-cost-analysis.md](continuous-monitoring-tumbling-cost-analysis.md) > (the per-family cost theory) and diff --git a/docs/continuous-monitoring-tumbling-cost-analysis.md b/docs/continuous-monitoring-tumbling-cost-analysis.md index 5fcb8c1f7..b99e69b7a 100644 --- a/docs/continuous-monitoring-tumbling-cost-analysis.md +++ b/docs/continuous-monitoring-tumbling-cost-analysis.md @@ -1,5 +1,8 @@ # Continuous Monitoring over Tumbling Windows — Cost Analysis of the Existing Sketch Families +> **Part of the CDM/GOS theory set.** The canonical, unified derivations live in [`sampling-cdm-gos-derivations.md`](sampling-cdm-gos-derivations.md) — start there. This doc is retained for its per-family (Sum/CMS/CS/DDSketch/KLL/HLL) tumbling-window cost derivations under the two disciplines, which the canonical doc does not reproduce. + + > **Design-doc / paper section** — theory only. For each sketch family we > already ship (Sum, DDSketch, KLL, Count-Min, Count-Sketch, HyperLogLog), > derive, under the **continuous distributed monitoring (CDM)** model diff --git a/docs/delta-transmission-design.md b/docs/delta-transmission-design.md index 71c68f407..3ef8ad84a 100644 --- a/docs/delta-transmission-design.md +++ b/docs/delta-transmission-design.md @@ -607,6 +607,11 @@ skip unknown encodings or treat them as full sketches. deltas. This is the mirror of Phase 3. **New processors:** `countminsketchmergeprocessor` / `countsketchmergeprocessor` +> **Removed.** These Go merge processors implemented the gateway-side sketch +> accumulator. The asap-gateway hop was retired (#400) and cross-edge merge now +> happens on the backend `data_plane` (Rust), so the Go merge processors were +> deleted (no pipeline ever wired them). This section is kept as the design of +> record for the delta-reconstruction mechanism, now realized backend-side. **Per-partition accumulator state:** ```go diff --git a/docs/design-gos-unified-edge-telemetry.md b/docs/design-gos-unified-edge-telemetry.md new file mode 100644 index 000000000..94783fddf --- /dev/null +++ b/docs/design-gos-unified-edge-telemetry.md @@ -0,0 +1,732 @@ +# GOS: A Unified Error / Threshold / Cost Framework for Distributed Edge Telemetry + +**One line.** One framework that unifies error-bounded sketching, coordinated +sampling, Geometric Monitoring (GM/AutoMon), and OctoSketch-style change +transmission into a single per-cell decision — reducing to **one atomic quantity** +`e_j = k·T_j` (per-cell backend error) — with a unified relative error bound, a +tunable memory/compute/communication objective, and a closed-form +water-filling solution whose worst-case communication matches the +Woodruff–Zhang lower bound `Θ̃(k/ε²)`. + +We call the construction **GOS** (Geometric-OctoSketch). + +--- + +## 1. Context and requirements + +Distributed data collection and transmission with a centralized analytics +backend. Edge telemetry must be **memory-, computation-, and +communication-efficient** under cloud economics, while the backend serves +**continuous, accurate, fresh** queries. + +- **Memory efficiency.** Cloud memory instances are priced above CPU instances. + With high-cardinality, high-frequency time-series metrics from many + distributed services, allocating *one sketch per series per window* puts heavy + memory pressure on the data-source nodes. Memory must be controlled across the + series×window space. +- **Computation efficiency.** Per-sample sketch update work at the edge must stay + within line-rate CPU budgets on shared instances. +- **Communication efficiency.** Cross-AZ egress is billed per byte; transmission + must be minimized. +- **Continuous & accurate queries.** Monitoring queries are *long-running* over + the stream, not the database "on-demand" model. We care about the + **freshness/timeliness** gap between when a sample is generated at the source + and when it can be queried accurately at the backend. + +These four axes are exactly the decision variables and constraints of the +optimization below. + +--- + +## 2. Positioning: what each prior line gives, and what it lacks + +All four lines are instances of "keep the local drift inside a **safe zone**, +communicate on violation." They differ in the *shape* of the safe zone and in +*what* they bound. (Attribution matters; see §10.) + +| Line | Bounds | Safe-zone shape | Continuous query? | Gap | +|---|---|---|---|---| +| **Error-bounded sketching** (Count-Sketch [Charikar+02], CMY [Cormode+08]) | one function, per window | — | window granularity only | intra-window staleness | +| **Geometric Monitoring** (Sharfman–Schuster–Keren [SIGMOD'06]; AutoMon [Sivan+ SIGMOD'22]) | one scalar `f(x̄)` | DC-quadric (ball / slab / general) | that **one function** only | not per-cell; not query-general | +| **OctoSketch** [Zhang+ NSDI'24] | **every counter cell** `|ΔC[j]| **Result.** At equal admitted work (`E[rows]=d·p` per item ⇒ same edge CPU), +> per-row sampling keeps the sampling error inside the median's high-probability +> envelope; whole-item sampling leaves it as an irreducible common-mode penalty. +> Per-row is the **strictly better estimator** — this, not the relocation, is the +> reason to push the decision into the SDK. + +**Effective `ε_sa` (composition).** Under per-row independence the sampling +*variance* folds into the **same** `median_r` step as the collision variance — +`Var[X_r] = F₂/w + (1−p)/p·S₂,r` under one median tail — so it composes **in +quadrature** with `ε_sk`: `|f̂(y)−f(y)| ≤ √(ε_sk²+ε_sa²)·‖f‖₂` w.p. `1−δ`, with +`ε_sa = Θ(√((1−p)/(p·w)))` (uniform `p`). This is exactly the random part of +Theorem 1 (§4). Whole-item admission instead leaves a **common-mode** term that +survives the median and adds to `ε_sk` **linearly** — strictly looser at equal +edge cost. + +**Threshold-allocation coupling (row-dependent floor).** The delta gate `T_j` for +cell `j=(r,c)` sits over a *row-`r`-subsampled* counter, so the +"don't-transmit-finer-than-you-sample" floor of §7 is **row-indexed** by that +row's admission rate: +``` +T_j ≥ T_j^floor = √( V_j · (1−p_i)/p_i ) (rate is per-site p_i, admission per-row) +``` +The GOS water-filling is unchanged in form; the ε-budget is split by §7 Layer B +(staleness `ε_st` peeled linearly, then `ε_sk²+ε_sa² = (ε_q−ε_st)²`), and this +floor closes the sampling↔threshold coupling. + +**Status.** The per-row *estimator* benefit above is **realized** in code via +`UpdateStringSampledPerRow`, so `ε_sa` sits inside the `1−δ` median guarantee. The +*location* move is realized on the **SDK-build path** — the OTLP SDK `CountSketch` +aggregator hosts the sampler (§3.1) — so admission happens at the source; the +collector-build path realizes the pre-deserialization saving at the edge +`otlpfilter` (whole-datapoint wire-thinning). CMS/DDSketch SDK-build hosting is +the remaining follow-up. + +--- + +## 4. Unified error bound + +The backend holds `Ĉ(t)`, perturbed from the ideal `C(t)` by two **random** +sources that the Count-Sketch median absorbs together, plus one **deterministic** +staleness term that adds on top: + +1. **Sketch + sampling (random, one median).** Per row `r`, the cell estimate + carries hash-collision variance `≈ F₂/w` *and* per-row sampling variance + `≈ (1−p_i)/p_i · S₂,r` (§3.2 — the `1/p_i` weight is unbiased; per-row + admission keeps the rows independent). Because both live in the **same** + `median_r` step, they compose in quadrature into one high-probability tail: + `|q̂ − q(f)| ≤ √(ε_sk² + ε_sa²)·‖f‖₂` w.p. `1−δ`, with `ε_sk=Θ(1/√w)`, + `ε_sa=Θ(√((1−p)/(p·w)))`, `δ=2^{−Θ(d)}`. (Sampling rate is per-site `p_i`; + admission is per-row-independent, i.e. `p_{i,r}=p_i` uniform across rows.) +2. **Staleness `{T_j}` (deterministic).** Site `i` withholds cell `j` until its + accumulated change reaches `T_j`, so `|Ĉ[j] − (C+η)[j]| ≤ k T_j =: e_j` **at + all times** — a worst-case bound, *not* a random variable, so it **adds + linearly** (it cannot be RMS-combined with the random tail). + +**Theorem 1 (unified relative error).** For any query `q` and any time `t`, w.p. +`≥ 1 − δ`: + +``` + ┌──── random (one median) ────┐ ┌──── staleness (linear) ────┐ +|q̂(t) − q(f)| ≤ √(ε_sk² + ε_sa²)·‖f‖₂ + k·Σ_j |r_{q,j}|·T_j +``` + +Dividing by `‖f‖₂` gives the **relative** budget: the random part (sketch ⊕ +sampling in quadrature) plus the deterministic staleness, linearly: + +> **√(ε_sk² + ε_sa²) + ε_st ≤ ε_q.** + +For a monitored non-linear `f_m` (`g_m = ∇f_m`, Hessian spectral bound `λ_m` from +AutoMon/ADCD), a Taylor + DC bound gives + +``` +|f̂_m − f_m| ≤ |g_m|ᵀ(ε_sk + ε_sa terms) + k·Σ_j|g_{m,j}|T_j + ½·λ_m·k²‖T‖₂² ≤ ε_m·|f_m|. +``` + +This holds **continuously**, not only at window boundaries — the OctoSketch +"online accuracy at any query time" property, here generalized to arbitrary +queries and to gradient-weighted function monitoring. + +### Freshness + +Cell `j` (activity `V_j = Σ_i V_{ij}`) reaches `T_j` after time `T_j / V_j`, so its +max staleness age is `Δ_j = T_j / V_j`. To guarantee query freshness `Δ*`: +`T_j ≤ V_j Δ*`. (Quiet cells have large `Δ_j` even at small `T_j`, so freshness +binds them → periodic heartbeat.) + +--- + +## 5. Cost models (per unit time) + +``` +Memory_edge = m·G·n·( 1 + 1{delta}[acked snapshot] + 1{aniso}[threshold vec] ) +Comp_sdk = c_rng·(Σ_r p_{i,r})·rate (geometric skip-sampling: O(1)/admit RNG) +Comp_coll = c_h·(Σ_r p_{i,r})·rate (hash + update — only admitted rows) + + c_s·Σ_j V_j/T_j (uploads — thresholds cut this) +Comm = b·Σ_j V_j/T_j (+ broadcast for geometric) + × (1 − ∏_r(1−p_{i,r})) (samples with no admitted row aren't sent) +Cost_coord = m·n·(1 + k·1{geo}) (running merge + per-edge refs) + + c_a·Σ_j V_j/T_j (incremental apply_delta) +``` + +The edge CPU is split across the two runtimes: the **SDK** pays only the +geometric-sampler RNG (`Comp_sdk`, `O(1)` per admit, no hashing), and the **agent +collector** pays the hashing/update (`Comp_coll`) *only for admitted rows* plus +the delta uploads. Lowering `p_{i,r}` cuts SDK RNG, collector hashing, and wire +volume together — one lever, three savings. + +**Structural insight.** `Comm`, the upload part of `Comp_edge`, and the apply part +of `Cost_coord` are all `∝ Σ_j V_j/T_j` → they collapse into one effective weight +`W = w_c·b + w_e·c_s + w_b·c_a`. Hence the *cost weights do not change the optimal +threshold shape* — the accuracy constraint pins `T_j`; the weights instead select +the **structural knobs** (uniform-vs-aniso, delta-vs-full, sampling `p`, per-edge +refs), which is where the memory/compute tradeoffs live. + +--- + +## 6. The optimization problem (P) + +``` +minimize w_m·Memory_edge + w_e·Comp_edge + w_c·Comm + w_b·Cost_coord +over d, w, G, {p_i}, {T_j}, structural flags +subject to + (query, ∀ q ∈ Q) √(ε_sk(w)² + ε_sa({p_i})²) + k·Σ_j|r_{q,j}|T_j/‖f‖ ≤ ε_q + (function, ∀ f_m) k·Σ_j|g_{m,j}|T_j + ½·λ_m·k²‖T‖² ≤ ε_m·|f_m| + (freshness) T_j ≤ V_j·Δ* + (confidence) d ≥ log₂(1/δ) + p_i ∈ (0,1], T_j ≥ 0, w ≥ 1, G ≥ 1 +``` + +`w_b` (coordinator) is the least-weighted axis by requirement. + +--- + +## 7. Solution: hierarchical decomposition + two water-fillings + +**Layer A — outer (small enumeration).** `d = ⌈log₂(1/δ)⌉`; choose `w` +(`ε_sk = c/√w`) and `G` (grouping) to trade the memory term `w_m·G·n` against the +accuracy the sketch must supply. + +**Layer B — budget split (staleness peeled linearly first).** Because staleness +is deterministic it comes off the top of `ε_q` **linearly** (Theorem 1), *then* +the remaining random budget is split in **quadrature**: + +1. choose `ε_st ∈ [0, ε_q − ε_sk]` — the edge-CPU↔communication knob (larger + `ε_st` ⇒ looser thresholds ⇒ less comm, but a smaller random budget ⇒ tighter + sampling ⇒ more CPU); pick it by the 1-D convex tradeoff of `w_e·Comp` vs + `w_c·Comm` (unique — both monotone); +2. the random budget after the linear peel is `ε_rand = ε_q − ε_st`; +3. split it in quadrature: `ε_sa² = ε_rand² − ε_sk²` (sketch ⊕ sampling), which + requires `ε_sk ≤ ε_rand`. + +So the composition is `√(ε_sk² + ε_sa²) + ε_st = ε_q`, matching §4 — **not** a +three-way quadrature. The code's `split_budget(ε_q, ε_sk, w_edge, w_comm)` +implements exactly this linear peel (`ε_st = t·(ε_q−ε_sk)`, +`ε_sa = √((ε_q−ε_st)²−ε_sk²)`, `t = w_comm/(w_edge+w_comm)`). + +**Layer C — two water-fillings (same KKT tool, two variables).** + +- **Sampling** (per-site). The per-key KKT water-filling + `p_i ∝ √(f_i/rate_i)` (binding `Σ_i f_i(1−p_i)/p_i ≤ V_sa(ε_sa)`) is the + general form, but it has been **retired for sketch sampling**: a sketch + point/L2 estimate's error is bounded by the sketch *norm*, not a single key's + `f(x)`, so the accuracy a per-edge `p_i` buys is only "keep this edge's L2 + contribution within ε." The **implemented** allocation is therefore the + whole-sketch ε-floor + ``` + p_i = 1 / (1 + ε²·rate_i), clamped to (0,1] + ``` + (`data_plane monitor::coordinator::allocate_p` → `epsilon_sample_floor`; see + derivations §5). The `√(f/rate)` split remains valid only when a key is + exact-counted *outside* the sketch — where sampling that one counter is + pointless anyway. +- **Thresholds** (per-cell; GOS). Effective weight `W` (§5), per-cell price + `c_j = k|g_j|` (function) or `k|r_{q,j}|` (query — take the binding constraint), + budget `B` (relative: `B = ε_m‖Ĉ‖²` for F₂): + + ``` + ┌ water-filling ┐ ┌──── clamps ────┐ + T_j = clamp( (B/Σ_ℓ√(c_ℓ V_ℓ))·√(V_j/c_j), T_j^floor, min(T_q, V_j·Δ*) ) + ``` + + with **sampling-coupling floor** `T_j^floor = √( V_j(1−p)/p )` ("don't transmit + finer than you sample"), **query cap** `T_q = ε_q‖Ĉ‖/(k·s_q)`, **freshness cap** + `V_j·Δ*`. Clamped cells release budget → box water-filling redistributes (a few + iterations). + +**Reading.** `T_j ∝ √(V_j/|g_j|)`: high-activity cells get larger thresholds (don't +chase high-frequency noise); cells the monitored function is sensitive to +(`|g_j|` large) get smaller thresholds (report early); everything capped by the +universal query cap and freshness. + +### Closed forms + +- **F₂, isotropic** (`g_j = 2Ĉ_j`, `λ=2`, uniform, relative): `T = ε‖Ĉ‖ / (2k√(dw))` + — adaptive: scales with the current norm. +- **F₂ threshold-alert version** (monitor `F₂ ≥ τ`, one-sided band): `T = (1/k)√((1−ε)τ/w)`. + The whole-sketch `F₂` readout here is the **mean-of-rows** estimator + `F̂₂ = ‖C‖²/d` (each row's `‖C_r‖²` is an unbiased `F₂` estimate; averaging the + `d` independent rows reduces its variance by `1/d`) — *not* the median-of-rows + point-query estimator of §3.2. This is deliberate: the geometric safe-zone is a + **ball** `‖C‖ ≤ √(d(1−ε)τ) ⇔ ‖C‖²/d ≤ (1−ε)τ`, so edge silence and coordinator + alert test the identical functional (`all-sites-safe ⟺ F̂₂ < (1−ε)τ`). The two + estimators serve different readouts — median for individual-key location + (robust tail), mean for the aggregate energy the ball bounds (variance + reduction) — and must not be conflated. +- **Linear `f` (sum/count/point)**: `λ = 0` → the curvature term vanishes → box + degenerates to a **slab** = the classic CMY slack countdown. + +### Unification of counter-based and F₂ + +Both are `f(x̄)` vs a band `[L,U]`, with the safe zone from the DC bound; the only +difference is the Hessian eigenvalue: `λ=0` → slab (counter-based), `λ=const·I` → +ball (F₂), general → ADCD quadric. One monitor, one edge check +`isLocallySafe(Δ, x₀, ∇f, λ, L, U)`; keep the scalar-countdown fast-path for the +linear case (no reference-vector broadcast needed). + +### Threshold band vs tracking band + +- **Threshold/alert**: one-sided band `[−∞, τ]` → fire on crossing. +- **Continuous ε-query**: moving band `[f(x₀)−ε, f(x₀)+ε]` → backend answers + `f(x̄)=f(x₀)±ε` at all times. Same monitor, different band — this is how + threshold monitoring and continuous approximate querying unify (Cormode– + Garofalakis continuous querying = GM with a tracking band). + +--- + +## 8. Optimality vs the Woodruff–Zhang lower bound + +**Rate vs total.** `Σ_j V_j/T_j` is an upload *rate*; the WZ `Θ̃(k/ε²)` is the +*total* communication to maintain one continuous `(1±ε)` `F₂` estimate. Compare +them over a fixed horizon of bounded total change: with `w ∝ 1/ε²` (the necessary +sketch width) each sketch is `Θ(1/ε²)` and `k` sites must each be represented, so +the total is `Θ̃(k/ε²)` — **matching the WZ STOC'12 tight lower bound** (bits vs +words absorbed in the `Θ̃`). The measured normalization in +[`gos-eval-results.md`](gos-eval-results.md) §3 uses the "one-round" unit `k·S` +for exactly this comparison. Consequences: + +- The `1/ε²` and the linear-in-`k` are **fundamental**; no protocol (GM, AutoMon, + OctoSketch, GOS) beats `k/ε²` adversarially. GOS's savings are **data-dependent** + (small `V_j/‖Ĉ‖` on stable streams). +- Report GOS's measured bytes as a **fraction of `k/ε²`** — a stronger baseline + than comparing to naive centralization. +- The relative-error caveat: relative bounds require the norm bounded below + (WZ tightness / OctoSketch `L1 > ε⁻¹k'τ`); relative error on a near-zero signal + is fundamentally not cheap. + +--- + +## 9. How it meets the four requirements + +| Requirement | Mechanism | +|---|---| +| **Memory efficiency** | `(w,G)` + `w_m`: grouping `G` avoids one sketch per series; `w=Θ(1/ε²)` is the WZ-minimum; delta/aniso flags dropped under high `w_m` (no snapshot / threshold vector) | +| **Computation efficiency** | `w_e`: sampling `p_i` (fewer updates) + threshold size (fewer uploads); both fold into one effective weight | +| **Communication efficiency** | `w_c`: per-cell water-filling thresholds + geometric silence; `Θ̃(k/ε²)` worst case, far less on stable data | +| **Continuous, accurate, fresh** | Theorem 1 bounds every query at **any** `t`, **relative**; freshness cap `T_j ≤ V_jΔ*` bounds staleness age; whole sketch queryable (OctoSketch), monitored `f_m` tighter (GM/AutoMon) | + +--- + +## 10. What is adopted vs contributed (attribution) + +- **Water-filling** — classic (information theory / convex optimization; optimal + power allocation across parallel channels). Already used in ASAP for sampling + (`p_i ∝ √(f_i/rate_i)`). *Adopted, not contributed.* +- **Per-cell change transmission with a threshold** — OctoSketch [Zhang+ NSDI'24]. + *Adopted.* +- **Function safe zone via DC decomposition of the Hessian (ADCD), gradient/ + Hessian bounds** — AutoMon [Sivan+ SIGMOD'22]; Geometric Monitoring [Sharfman+ + SIGMOD'06]. *Adopted.* +- **Coordinated sampling + geometric skip-sampling** — NitroSketch [Liu+ + SIGCOMM'19]; ASAP's own `AllocateSampleRates` (rate allocation) and + `sketchlib-go/common.GeometricSampler` (the `O(1)`-amortized skip sampler). + *Adopted.* +- **Lower bound `Θ̃(k/ε²)`** — Woodruff–Zhang [STOC'12]. *Yardstick.* +- **GOS (this doc)** — casts *per-cell threshold allocation* as water-filling with + **gradient-derived weights** (GM/AutoMon) and a **per-cell query cap** + (OctoSketch), coupled to **per-site sampling** through a shared ε-budget and a + granularity floor `T_j ≳ √(V_j(1−p)/p)`, inside one **tunable + memory/compute/communication objective**. *The synthesis is the contribution; + the optimization tools are off-the-shelf.* + +--- + +## 11. Implementation notes (controller synthesizes, edge executes) + +Everything expensive is a **controller (backend) decision**; the edge only +executes a fixed per-cell comparison. + +- **Controller** (offline, per registered metric/query): runs ADCD (AD → Hessian + eigenvalue bounds → `∇f, λ`), estimates `{V_j}` from the workload, solves (P)'s + layers A–C, emits `(d, w, G, {p_i}, scalar GOS knobs, flags)` via OpAMP. This + slots into the existing controller multi-objective + (`controller-optimization-problem.md` SP-6: + `min w_bw·bw + w_cpu·cpu + w_mem·mem + …`) — GOS thresholds are new decision + variables there. **Note:** the controller ships *scalars* + (`ε_delta`, sites, aniso flag), **not** the full per-cell vector `{T_j}`; the + edge reconstructs `{T_j}` locally from those scalars plus its live sketch state + (see §7C, `sketches/gos_threshold.go`), so the `O(d·w)` vector never crosses the + wire. +- **Edge**: maintain sketch + acked snapshot; per flush, recompute `{T_j}` from + the pushed scalars + local `{V_j}`, upload cells with `|ΔC_j| ≥ T_j` as a sparse + delta; run one generic `isLocallySafe` for monitored functions. No AD, no + water-filling solve at the edge — only the closed-form threshold evaluation. +- **Backend**: `apply_delta` into a running merge (`O(#delta cells)`), keeping the + global sketch continuously queryable within the Theorem-1 envelope, surfaced in + the `accuracy: ε=…` response annotation. + +**Ties to existing code:** +- `ASAPQuery-backend/control_plane/src/epsilon_alloc.rs` — the ε-budget split. + Staleness is peeled **linearly** first (Theorem 1), then the remaining random + budget splits in quadrature: `√(ε_sk² + ε_sa²) + ε_st = ε_q` (see §7 Layer B). + This is **not** a three-way quadrature `ε² = ε_sk² + ε_sa² + ε_st²` — staleness + is deterministic and comes off the top linearly. +- `ASAPQuery-backend/data_plane/src/monitor/sampling_alloc.rs` + (`epsilon_sample_floor`) — the live whole-sketch sampling floor. (The Go + `monitor/sampling_alloc.go` `AllocateSampleRates` is the *retired* per-key + water-filling, kept only as a reference impl with no production caller.) +- `threshold_alloc` (`AllocateThresholds`, Go `sketches/gos_threshold.go` + Rust + `control_plane/src/threshold_alloc.rs`) — the per-cell threshold water-filling + (this doc's §7C). +- `data_plane/src/monitor/f2_coord.rs`, `asap-precompute-go/monitor/f2engine.go` + — generalize the F₂-specific ball to the `(∇f, λ)` DC safe zone; use the relative + radius `ε‖Ĉ‖/(2k√(dw))`. +- reuse `asap_sketchlib` `CountSketchDelta` + `compute_delta`/`apply_delta` + (byte-parity Go/Rust) for the sparse per-cell delta wire format. + +**Implementation status (as of this writing).** The pieces exist but the GOS +threshold control loop is **not yet wired end-to-end**: +- **Live today:** the scalar CDM loop (register → grant `(slack, sample_p)` → + countdown → report → alert) and the sampling grant path (`Grant.SampleP` → + `otlpfilter` Upsert + wrapper `WithSampleP`). +- **Implemented but unreachable from a production config:** the control plane + *derives and emits* the scalar GOS knobs (`gos_delta_epsilon`, `gos_sites`, + `gos_anisotropic` in `emit/agent.rs`), and the edge *consumes* + `PrecomputeConfig.GosDeltaEpsilon` (`applyGosMode` → `gosThresholdMatrix`), but + **no collector processor parses those YAML keys into the config**, so + `applyGosMode` is a production no-op and the per-cell delta-gating path is + exercised only by tests/eval. The sampling↔threshold coupling floor + `T_j ≥ √(V_j(1−p)/p)` (§3.2) is likewise implemented but inert — its only + production-shaped caller hardcodes `SampleP=1`. Closing this last hop + (a knob parser + threading the granted `p` into `GosParams`) is tracked work. + +--- + +## 12. Open problems / next steps + +1. **Anisotropic delta broadcast** — *partially done.* The **sparse-cell** + encoding of `ΔC_ref` on the coordinator→edge path is implemented and measured + (`CRefUpdate::Delta`; removes the `O(k)` broadcast amplification — see + gos-eval-results.md §2). Still **open:** the broadcast gate is currently + isotropic (ships every changed cell, `Δ ≠ 0`); giving it *anisotropic per-cell + thresholds* (the §7C water-filling, as already done on the edge→coordinator + upload path via `ComputeDeltaPerCell`) is the remaining work. +2. **Relative-error under small norm** — heartbeat / additive floor when `‖Ĉ‖` is + small (WZ / OctoSketch fundamental limit). +3. **Verified eigenvalue bounds** — AutoMon's numerical `λ` may miss the true + extreme → reserve an `ε_eig` slice of the budget or use interval bounds. +4. **Empirical validation** — measure achieved communication as a fraction of the + WZ `k/ε²`, sweep `(w_m, w_e, w_c)` to trace the Pareto surface. + +--- + +### References (attribution) + +- G. Cormode, S. Muthukrishnan, K. Yi. *Algorithms for Distributed Functional Monitoring.* SODA 2008 / ACM TALG 2011. +- M. Charikar, K. Chen, M. Farach-Colton. *Finding Frequent Items in Data Streams* (Count-Sketch). ICALP 2002. +- I. Sharfman, A. Schuster, D. Keren. *A Geometric Approach to Monitoring Threshold Functions over Distributed Data Streams.* SIGMOD 2006. +- H. Sivan, M. Gabel, A. Schuster. *AutoMon: Automatic Distributed Monitoring for Arbitrary Multivariate Functions.* SIGMOD 2022. +- Y. Zhang, P. Chen, Z. Liu. *OctoSketch: Enabling Real-Time, Continuous Network Monitoring over Multiple Cores.* NSDI 2024. +- D. Woodruff, Q. Zhang. *Tight Bounds for Distributed Functional Monitoring.* STOC 2012 (arXiv:1112.5153). +- Z. Liu, R. Ben-Basat, G. Einziger, Y. Kassner, V. Braverman, R. Friedman, + V. Sekar. *NitroSketch: Robust and General Sketch-Based Monitoring in Software + Switches.* SIGCOMM 2019. (Geometric skip-sampling: one RNG draw per admitted + update — `O(1)` amortized, "always line rate" — with inverse-probability + weighting; the SDK-side row-admission sampler here is this scheme.) diff --git a/docs/distributed-nitrosketch-coordinated-sampling.md b/docs/distributed-nitrosketch-coordinated-sampling.md index 38949a396..a5650c465 100644 --- a/docs/distributed-nitrosketch-coordinated-sampling.md +++ b/docs/distributed-nitrosketch-coordinated-sampling.md @@ -1,5 +1,8 @@ # Distributed NitroSketch — coordinator-allocated update-sampling across edge collectors +> **Part of the CDM/GOS theory set.** The canonical, unified derivations live in [`sampling-cdm-gos-derivations.md`](sampling-cdm-gos-derivations.md) — start there. This doc is retained for the SDK→collector split survival proof (unbiasedness + partition-invariant variance), per-family applicability, and the 2026-06 empirical validation, which the canonical doc does not reproduce. + + > Design note. Extends [NitroSketch](https://doi.org/10.1145/3341302.3342076) > (per-sketch update-sampling, single stream) to a multi-edge setting using the > coordinated-sampling idea from the @@ -8,6 +11,16 @@ > [continuous-monitoring-aggregation-taxonomy.md](continuous-monitoring-aggregation-taxonomy.md) > and [continuous-monitoring-tumbling-cost-analysis.md](continuous-monitoring-tumbling-cost-analysis.md). +> ⚠️ **Allocation law updated.** This note derives the per-key KKT water-filling +> `p_i ∝ √(f_i/rate_i)`. For **sketch** sampling that allocation has been +> **retired**: a sketch point/L2 estimate's error is bounded by the sketch +> *norm*, not a single key's `f(x)`, so the implemented coordinator allocation is +> the whole-sketch ε-floor `p_i = 1/(1+ε²·rate_i)` +> (`data_plane monitor::coordinator::allocate_p` → `epsilon_sample_floor`; see +> [design-gos-unified-edge-telemetry.md](design-gos-unified-edge-telemetry.md) +> §7C and derivations §5). The `√(f/rate)` form below is retained for its +> derivation and applies only when a key is exact-counted *outside* the sketch. + ## Context Each edge collector builds per-metric sketches over its local stream and ships diff --git a/docs/design-asap-otap-rust-integration.md b/docs/dormant/design-asap-otap-rust-integration.md similarity index 99% rename from docs/design-asap-otap-rust-integration.md rename to docs/dormant/design-asap-otap-rust-integration.md index 05e5311c1..e060a254b 100644 --- a/docs/design-asap-otap-rust-integration.md +++ b/docs/dormant/design-asap-otap-rust-integration.md @@ -1,5 +1,8 @@ # ASAP OTAP-Rust Integration — Design +> **DORMANT.** This integration is not active on the current branch — the `otel-arrow` path/submodule is uninitialized and no build or eval arm exercises it. Kept for design reference; revisit before reactivating. + + _Status: **draft** — 2026-05-02. Doc-only; gates the OTAP-Rust adapter work that becomes Phase 5 of the edge-framework migration._ @@ -582,7 +585,7 @@ shipped in #241 / #242). — five-layer model, bandwidth invariant, Strategy A/B, per-platform encoding, `Adapter` / `ControlChannel` traits, R6 (OTAP pre-1.0), §7.4 (integration model). -- [`docs/design-asap-telegraf-integration.md`](./design-asap-telegraf-integration.md) +- [`design-asap-telegraf-integration.md`](./design-asap-telegraf-integration.md) — Phase-4 adapter design that this Phase-5 design mirrors structurally (two-layer split, unified-plugin shape, build pipeline). diff --git a/docs/design-asap-telegraf-integration.md b/docs/dormant/design-asap-telegraf-integration.md similarity index 99% rename from docs/design-asap-telegraf-integration.md rename to docs/dormant/design-asap-telegraf-integration.md index 22db724aa..d90287ce2 100644 --- a/docs/design-asap-telegraf-integration.md +++ b/docs/dormant/design-asap-telegraf-integration.md @@ -1,5 +1,8 @@ # ASAP Telegraf Integration — Design +> **DORMANT.** This integration is not active on the current branch — the `telegraf` path/submodule is uninitialized and no build or eval arm exercises it. Kept for design reference; revisit before reactivating. + + _Status: **draft** — 2026-05-02. Doc-only; gates the Telegraf adapter work that becomes Phase 4 of the edge-framework migration._ diff --git a/docs/design-asap-vector-integration.md b/docs/dormant/design-asap-vector-integration.md similarity index 98% rename from docs/design-asap-vector-integration.md rename to docs/dormant/design-asap-vector-integration.md index 295b6e700..7ad76c751 100644 --- a/docs/design-asap-vector-integration.md +++ b/docs/dormant/design-asap-vector-integration.md @@ -1,5 +1,8 @@ # ASAP Vector Integration — Design +> **DORMANT.** This integration is not active on the current branch — the `vector` path/submodule is uninitialized and no build or eval arm exercises it. Kept for design reference; revisit before reactivating. + + _Status: **draft** — 2026-05-02. Doc-only; gates the Vector adapter work that becomes the Phase 5 sister of the OTAP-Rust adapter in the edge-framework migration._ @@ -582,11 +585,11 @@ already on `main` (Phase 5 step A — #248). R6 / R7, §7.2 (per-platform Strategy-B carriers — Vector row), §7.3 (per-platform integration — Vector `TaskTransform`), §7.4 (integration model). -- [`docs/design-asap-otap-rust-integration.md`](./design-asap-otap-rust-integration.md) +- [`design-asap-otap-rust-integration.md`](./design-asap-otap-rust-integration.md) — Phase-5 sister adapter design that this doc mirrors structurally (two-layer split, unified-plugin shape, build pipeline, cross-language parity reasoning). -- [`docs/design-asap-telegraf-integration.md`](./design-asap-telegraf-integration.md) +- [`design-asap-telegraf-integration.md`](./design-asap-telegraf-integration.md) — Phase-4 adapter design that this doc inherits the unified-plugin and patch-overlay shape from. - [`docs/adr/adr-0002-extract-precompute-runtime.md`](./adr/adr-0002-extract-precompute-runtime.md) diff --git a/docs/evaluation-plan-figures.md b/docs/evaluation-plan-figures.md index a8eafc21c..3838c49e7 100644 --- a/docs/evaluation-plan-figures.md +++ b/docs/evaluation-plan-figures.md @@ -28,13 +28,55 @@ cold-half, Fig 11), and **the controller allocates the partition** (which series sketch[type,`W`,`L`,`p`] vs cold-Gorilla) from the query set (Fig 12). The contribution stack, from system to evidence: -1. sketch-across-the-lifecycle + the `(W,L,agg_type)` planner (the base system); -2. **coordinated SDK sampling** (`p_i ∝ √(f_i/rate_i)`, ε-floored) — new cost axis; +1. sketch-across-the-lifecycle + the **autonomous `(ε, queries) → {sketch, W, L, p, τ}` planner** (the controller — the key originality); +2. **coordinated SDK sampling** — the unified whole-sketch ε-floor `p = 1/(1+ε²·rate)` (the per-key `√(f/rate)` allocation was retired) — new cost axis; 3. **CDM** — ε-gated sub-window delta emission (open-window freshness) + slack-countdown alert; 4. the **joint bound** `ε_sk + ε_s + ε_cdm` (proof) tying accuracy to cost. --- +## Experiment matrix — claim × dataset × baseline (the comparative-rigor plan) + +The figures below are the *mechanism* demos. For a submission they must each run on +**≥2 real-world datasets** against **real baselines**, with **multiple trials + 95% CIs**. +This matrix is the contract; the per-figure sections carry the current numbers. + +### Real-world datasets (workload-credibility axis — NO synthetic in the headline) +| dataset | domain | shape / regime | exercises | status | +|---|---|---|---|---| +| **Google-cluster-2019** (`cpu_rate`) | machine resource usage | millions of (machine,job,task); high cardinality; continuous gauge | quantile/sum sketches, cardinality regime | ✅ staged (`/tmp/gct-*`) | +| **DEBS-2022** (last-trade) | financial tick stream | ~5k symbols; high rate; Zipf-skewed | frequency/heavy-hitter (CMS/CountSketch/Topk), skewed-rate sampling | ◐ downloading (`debs/data/`) | +| **3rd real** (Alibaba-2022 / Azure-VM / node-exporter dump) | infra metrics | TBD | generality / scale | ◻ gap | + +### Baselines (comparative axis — the biggest current gap) +| baseline | what it is | claim it stresses | +|---|---|---| +| **b0 raw-OTLP** | full samples, no aggregation | bw / CPU / mem floor | +| **b0a/b0b/b1 raw+codec** | gzip / zstd / Snappy only | bw from compression *alone* (isolate the aggregation factor) | +| **Prometheus+Thanos / VictoriaMetrics** | deployed TSDB remote-write | real-world reference point | +| **NitroSketch / OmniSketch** | sampling-sketch prior art | sampling *accuracy* vs ASAP's ε-floor | +| **Cormode-style CDM** | functional/threshold monitoring | delta-emission / freshness | +| **ASAP ablations** | no-sampling · uniform-p · no-CDM-delta · single-tier (warm-only) · **static (non-autonomous) alloc** | the marginal value of each ASAP knob | + +### The matrix (✅ measured · ◐ partial · ◻ gap) +| # | Claim | Experiment | Metric | gct-2019 | DEBS-2022 | 3rd | vs baselines | rigor (trials/CI) | +|---|---|---|---|---|---|---|---|---| +| C1 | Bandwidth reduction | sketch-vs-raw on a true family slice (`c1_wire.py`) | wire bytes agent→backend | ✅ **DDSketch 33.8×±0.3, HLL 65.9×±0.8** (real gct, n=3, 95% CI) | ◻ | ◻ | ✅ vs raw-OTLP; ◐ **vs raw+gzip = net 2.7×** (gzip 12.3×, offline); ◻ Prom/Nitro | ✅ 3 trials + CI | +| C2 | Edge CPU | sketch processors vs raw-forward, per-node | cpu cores | ◐ Fig 6 | ◻ | ◻ | ◐ vs raw | ◻ | +| C3 | Edge memory | RSS bounded over long soak (no leak) | RSS slope | ✅ edge bounded; ⚠ backend leak (Fig 6) | ◻ | ◻ | ◐ vs raw | ◐ 1 soak | +| C4 | Query accuracy | all-6-family error inside ε-envelope vs ground truth | rel-err, %≤ε, top-K recall | ✅ **DDSketch p50 0.72%/p99 2.67%, HLL 0.33%** (2026-06-20, root-caused valid); KLL small-N◐; CMS/CS → gauge-mismatch, use DEBS | ◻ (heavy-hitter natural here) | ◻ | ◻ **vs Nitro/Omni** | ◻ **need N trials + CI** | +| C5 | Query latency | warm-sketch vs cold-fallback PromQL replay | p50/p99 ms | ✅ Fig 7 (warm+cold) | ◻ | ◻ | ◐ vs VM/Thanos native | ◐ | +| H | **Pareto headline** | total (edge+wire+backend+storage) cost vs accuracy, swept over `(W,L,agg,p,ε)` | cost↔acc frontier | ◐ Fig 1 | ◻ | ◻ | ◻ **vs raw+Prom on same frontier** | ◻ | +| N1 | **Autonomous allocation quality** | `(ε,queries)`→plan vs oracle/hand-tuned/naive | plan match-rate, cost↔acc gap | ◐ Fig 12 (mechanism ✅ on cluster; quality ◻) | ◻ | ◻ | vs static-alloc, all-DDSketch, all-raw | ◻ | +| N2 | Controller adaptivity | inject query/workload drift → re-plan | re-plan latency, post-shift acc | ◻ | ◻ | ◻ | — | ◻ | +| X1 | Coordinated vs uniform p | whole-sketch ε-floor (observable R) vs fixed-p; per-edge on a fleet | F1/heavy-hitter err, insert-tput | live grants ✅ | ✅ real CMS/DEBS: **22× insert-tput, F1 err held at ε (0.050@ε=.05)**; fleet cold-edge **7×** better than fixed-p (Fig 9a; earlier 40–60× per-key claim RETRACTED, was circular) | ◻ | ✅ vs NitroSketch (same sampler; ε-floor derives p) | ✅ | +| X2 | Two-tier coverage | fraction warm- vs cold-answerable over a real query set; per-tier acc/latency | coverage %, per-tier | ◐ Fig 11 | ◻ | ◻ | — | ◻ | +| X3 | CDM delta savings | egress vs always-send, swept over τ | emits/window, bytes | ✅ Table 1 (gct) ; ✅ DEBS cross-check (structural-skew caveat) | ✅ | ◻ | vs Cormode-CDM | ◐ | + +**Headline gaps to close (priority order):** (1) **real baselines** — at minimum b0a/b0b + Prometheus/Thanos + one sampling-sketch (NitroSketch), on the same Pareto; (2) **statistical rigor** — ≥5 trials + 95% CI on every accuracy/cost number (current runs are single-shot and noisy); (3) **all-6-family accuracy** clean (only DDSketch is solid); (4) a **DEBS-2022 end-to-end** pass (downloading) as the 2nd real axis; (5) **autonomous-allocation quality vs oracle** (mechanism is validated, decision quality is not); (6) **scale** beyond the light workload. + +--- + ## Fig 1 — HEADLINE: accuracy-vs-cost Pareto, swept over `(W, L, agg, p, ε_cdm)` ◐ **Claim:** ASAP dominates raw; sampling + CDM extend the frontier. **Layout:** scatter, x = total cost (edge CPU + wire bytes/s, normalized to raw=1.0), @@ -121,6 +163,80 @@ a *timing*, not reducer, cause; pinned + fixed via `run.py --wall-clock-anchor`) dispersion is small-N order-statistic variance (single-window collapse, ~93 pts/series), not a sketch defect. +#### (c′) Re-run on current `main` (2026-06-19) — two regressions surfaced ⚠ +Same harness (`run_perfamily.py`, real gct, 1000 series), now on `main` (post ε-floor +unification + autonomous-allocation merges). Results **diverge from (c)** and flag two +issues to fix before this figure is submission-ready: + +| family | kind | median rel-err | %≤ε | wire | vs (c) | +|---|---|---|---|---|---| +| **DDSketch** | p50 / p99 | 0.0072 / 0.0267 | 87% / 41% | **57.6 MB** | acc ≈ same; **wire 58× higher** | +| **HLL** | cardinality | rel-err **0.0033** (996.7/1000) | ✅ | 57.0 MB | acc ≈ same ✅ | +| **KLL** | p50 / p99 | **0.0597** / 0.0717 | 48% / 41% | 57.9 MB | **acc 85× worse** (was 0.0007) | +| **Count-Min** | freq | f̂=143 / f_true=244 | **one-sided VIOLATED** | 67.4 MB | was "exact, one-sided OK" | +| **CountSketch** | topk@10 | recall **0.0** (n_warm=0) | ✗ | 25.8 MB | unchanged-broken | + +**Both flags ROOT-CAUSED (2026-06-20) — NOT a code regression; the accuracy is valid:** +1. **57 MB wire = un-sliced data, not lost SDK-aggregation.** The per-family slices + `/tmp/perfam-*.jsonl` are **byte-identical 210 MB files each containing ALL 8 metric + aliases** (`cpu_rate`, `_q_ddsketch`, `_q_kll`, `_topk_cs/cms`, `_card_hll`, `_freq_cms`, + `memory_usage`). Each arm's agent **does** SDK-sketch its one configured metric (accuracy + proves it), but **forwards the other 7 raw**, dominating the wire. → C4 accuracy is sound; + **C1 wire must be re-measured with genuinely family-sliced inputs** (filter the slice to + the single metric). A data-prep gap in `make_perfamily.py`, not a sketch/agent regression. +2. **KLL median + CMS one-sided are operating-point/data-fitness, not bugs.** KLL is + rank-based and coarse at the **~93 pts/series** the wall-clock single-window collapse + leaves (DDSketch's relative-error buckets handle small-N better — that's the real + tradeoff, not a KLL defect). CMS "under-counts" because gct `cpu_rate` is a **gauge** with + no meaningful per-key *count* to over-estimate — the frequency families are mis-applied to + gauge data and belong on **DEBS** (below), where symbol-trade frequency is a true count. + +**Data-fitness finding (not a bug):** gct `cpu_rate` is a **gauge** — quantile (DDSketch) ++ cardinality (HLL) are the natural fit and behave well; the **frequency/heavy-hitter +families (CMS, CountSketch, Topk) are mismatched to gauge data** (there is no meaningful +"count of a cpu_rate value"). Those families are evaluated on **DEBS-2022** (symbol-trade +frequency = the natural heavy-hitter workload), not gct. + +#### (c″) CLEAN C1 bandwidth — fixed harness, real gct, 3 trials + 95% CI ✅ +`c1_wire.py` (2026-06-20) closes the (c′) flags: it slices the data to a TRUE single-family +set `{cpu_rate Sum-anchor, memory_usage, ONE sketch metric}` and replays the *same* slice +through both a sketch agent and a **raw-forward agent** (`agent-raw-coldoff.yaml`, no +`asap_edge`) for an apples-to-apples wire comparison. + +| family | W_sketch | W_raw (same slice) | **reduction (n=3, 95% CI)** | accuracy | +|---|---|---|---|---| +| **DDSketch** | **1.00 MB** | 33.7 MB | **33.8× ± 0.3** | p50 0.69% (84%≤ε), p99 2.82% | +| **HLL** | **0.51 MB** | 33.7 MB | **65.9× ± 0.8** | card 0.33% | + +`W_sketch` reproduces the (c) committed numbers (DDSketch 0.99 MB, HLL 0.51 MB) **exactly**, +which definitively settles the (c′) "57 MB" question: it was the un-sliced 8-metric data, NOT +a code regression. Reduction CIs are tight (±0.3 / ±0.8 over 3 trials). HLL ships less (one +register sketch) → ~2× the DDSketch reduction. + +#### (c‴) Encoding-factor decomposition vs the gzip baseline (b0-gzip) ◐ +The honest reviewer question: *does sketching still win after a deployment compresses the raw +baseline?* Decomposition (DDSketch, real gct): + +| | wire | factor | +|---|---|---| +| raw OTLP | 33.7 MB | — | +| **raw + gzip** (b0-gzip) | ~2.7 MB | **encoding 12.3×** (gzip-6 on the actual payload) | +| **ASAP sketch** | 1.0 MB | total **33.8×** vs raw | +| **ASAP vs raw+gzip** | — | **net ~2.7×** (= 33.8 / 12.3) | + +So ASAP's 33.8× decomposes as **encoding (12.3×, free to anyone with gzip) × residual +aggregation (~2.7×, ASAP-unique)** — `12.3 × 2.7 ≈ 33.8`, internally consistent. **Sketching +still beats a gzip-compressed raw baseline by ~2.7×**, the aggregation contribution +compression can't replicate. + +**Measurement caveat (honest):** the encoding factor is `gzip(actual payload content)` +measured **offline** (the single-node cold-off stack is `--network host`, so the +agent→data-plane leg can't be isolated from the constant replay→agent leg on `lo` — the +attempted loopback-byte method failed, all arms ≈ 35 MB). The configs `agent-raw-{gzip,zstd}.yaml` ++ `baselines.py` are ready; the *clean on-wire* compression number belongs on the **cluster** +(Phase-2 per-node NIC isolates agent→backend bytes). Also unmeasured: sketch-envelope +gzip (binary, compresses less) — so net ASAP-vs-raw+gzip is a conservative lower bound here. + **Two honest, root-caused gaps (not fabricated):** - **CountSketch topk recall 0** — real semantic mismatch (orthogonal to timing): warm topk **keys by `item` not `host`** and **ranks by occurrence *frequency*, not @@ -190,22 +306,54 @@ steady** egress cut (mean ~1.3× after first-window + periodic full re-sync). --- -## Fig 6 — Edge CPU / memory: sketch vs raw, + long soak ◻ +## Fig 6 — Edge CPU / memory: sketch vs raw, + long soak ✅ edge bounded · ⚠ backend leak found **Claim (dims 2–3):** sketch edge CPU/RSS bounded vs raw-forwarding; no leak over 24 h. -**Layout:** (a) stacked CPU bar per baseline (raw `b0` / gzip `b1` / sketch `b3`); -(b) RSS-over-time line, 24 h, slope-based leak verdict. +**Layout:** (a) stacked CPU bar per baseline (raw `b0` / sketch `b3`); +(b) RSS-over-time line, slope-based leak verdict. **Note (honest framing):** this is the **sketch-vs-raw** CPU story — *not* a sampling-CPU claim. Sampling's win is bandwidth/ingest; the edge-CPU lever is -sketch-vs-raw **+ the CMS empty-base delta opt (−63%)**. **◻ need the raw baseline + soak.** +sketch-vs-raw **+ the CMS empty-base delta opt (−63%)**. + +**Measured (real gct, cold-OFF, constant 5000 pts/s, 30-min soak = 9.5M pts / 0 +errors; 24 h figures are linear extrapolations of the measured slope):** + +**(a) CPU / RSS — both arms measured (raw arm NOT skipped):** +| arm | mean CPU% | p99 CPU% | steady RSS | n | +|---|---|---|---|---| +| b0 raw-forward edge (no asap_edge) | 2.75 | 3.33 | 207 MB | 60 | +| b3 sketch edge (DDSketch+Sum) | 4.38 | 8.19 | 223 MB | 360 | +| b0 data_plane | 7.69 | 8.13 | 28 MB | 60 | +| b3 data_plane | 0.26 | 0.60 | 25→72 MB | 360 | + +Sketch edge costs **~1.6× CPU and +16 MB RSS** vs raw-forward — bounded, as claimed. + +**(b) Leak slope (edge and data_plane separately):** +- **Edge: BOUNDED** — **+2.6 MB/h (≈0)**, flat ~223 MB the whole soak (24 h extrap +63 MB). ✅ +- **data_plane: CLIMBING (monotone, linear)** — **+89.9 MB/h** (/proc) / +85.2 MB/h (the + binary's own `MEMORY_DIAG` gauge — two independent sources agree); 25→72 MB over + 30 min, no plateau (24 h extrap ~+2 GB). ⚠ + +**Stale-sid finding ([[gct-memory-findings]]) — mechanism refined:** SketchStore **sid +count plateaus hard at 1004** (the cardinality cap) — the "sid count keeps growing" +reading does **not** reproduce. But **per-sid warm-sketch state grows unbounded** +(`MEMORY_DIAG` payload 257 KB → 37.6 MB, **~146×**) and drives the backend RSS climb +~1:1. So the backend memory growth the prior note flagged is **real and reproduces**; +the driver is **per-sid DDSketch-state growth the evictable flusher doesn't reclaim +under steady load**, not sid-count growth. Retention is **backend-side** — edge is flat. +**Single-node loopback. Artifacts:** `datasets_eval/soak/` (`soak_RESULTS.md`, +`rss_over_time.png`, `summary.json`, raw samples), branch `feat/soak-fig6`. +**Follow-up:** the backend per-sid state growth is a real defect worth a fix (the +flusher's evictable accounting under sustained ingest). --- -## Fig 7 — Query latency CDF: PromQL-native vs sketch-answered ✅ (warm measured; cold-fallback arm blocked) -**Claim (dim 5):** warm-tier p50/p99 production-usable; cold fallback ≤2×. -**Layout:** latency CDF, lines for B0 (native) vs B3 (sketch warm) vs cold-fallback. -**Measured (real gct, cold-OFF stack, wall-clock-anchored, 599-query mix @15 QPS, +## Fig 7 — Query latency CDF: PromQL-native vs sketch-answered ✅ (warm + cold-fallback both measured) +**Claim (dim 5):** warm-tier p50/p99 production-usable; cold fallback bounded. +**Layout:** latency CDF, lines for warm sketch tier vs cold-fallback archive. + +**Warm arm** (real gct, cold-OFF stack, wall-clock-anchored, 599-query mix @15 QPS, guard-verified before timing — DDSketch read returned 691 real warm series, -`sum`=exact GT 216.3535, all `data_source=asap_query`, 0 empties/errors):** +`sum`=exact GT 216.3535, all `data_source=asap_query`, 0 empties/errors): | query kind | p50 | p95 | p99 | n | |---|---|---|---|---| @@ -213,16 +361,43 @@ guard-verified before timing — DDSketch read returned 691 real warm series, | `quantile_over_time` (DDSketch, 691-series reconstruction) | 18.34 | 19.56 | 20.65 ms | 400 | | `sum` (lossless) | 1.58 | 2.19 | 2.25 ms | 199 | -CDF is **bimodal**: cheap lossless `sum` at ~1.5–2.3 ms, 691-series DDSketch -quantile reconstruction at ~18–21 ms; tails tight (p99 within ~1 ms of p50 per -kind). Headline: warm sketch-answered PromQL is production-usable single-digit-to- -~20 ms server-side. **Cold-fallback arm attempted but blocked** (cold ship rides -the disabled control channel → MinIO stayed empty, old-ts queries still served -warm) → **warm-only reported, not faked**. **Single-node loopback — server-side -latency only, no network RTT.** `count_over_time` excluded (does not resolve on the -warm path — falls through to empty archive). Artifacts: `datasets_eval/latency/` -(`latency_RESULTS.md`, `latency_cdf.png`, `per_query_latency.json`, -`compute_latency.py`), branch `feat/query-latency-cdf`. +**Cold-fallback arm** (NOW MEASURED — real gct, cold-ON stack: MinIO + gorilla-merger ++ Thanos store-gateway/query + data-plane with `ASAP_THANOS_QUERY_URL`; cold-enabled +edge with full `cold.ship_endpoint` block; 600-query mix @15 QPS, guard-verified — +**600/600 `data_source=thanos_query`**, 0 empties/errors, so every timed query was +answered by the cold/archive engine, not a warm shortcut): + +| query kind | p50 | p95 | p99 | n | +|---|---|---|---|---| +| all (mix) | 22.28 | 47.17 | 67.07 ms | 600 | +| `quantile_over_time` (1000-series, Thanos PromQL over raw archived samples) | 24.27 | 48.59 | 68.41 ms | 400 | +| `sum` (lossless, archive) | 15.48 | 33.33 | 42.74 ms | 200 | + +Warm CDF is **bimodal** (cheap lossless `sum` ~1.5–2.3 ms, 691-series DDSketch +quantile reconstruction ~18–21 ms; tails tight). Cold CDF sits to the right with a +longer tail: the archive answer crosses data-plane → thanos-query → gorilla-merger +StoreAPI + store-gateway and re-evaluates PromQL over raw Gorilla-XOR samples. +**Headline: warm p50/p99 = 18.3/20.0 ms (production-usable); cold-fallback p50/p99 = +22.3/67.1 ms — overall p50 ≈ 1.2× warm, p99 ≈ 3.3× warm.** So the cold path stays in +the tens of ms (no order-of-magnitude blowup), at the cost of a heavier p99 tail than +the warm tier. + +How the cold path was forced & verified (the prior blocker is resolved): cold ship is +decoupled from the control channel (PR #500), so a static edge with `cold.enabled:true` +ships whenever `cold.ship_endpoint` is set — the multisketch coldon config had only +`cold:{enabled:true}` with NO ship_endpoint, which the edge treats as **drain-only** +(`config.go`: "Empty => drain-only (no shipping)") → that was why MinIO stayed empty. +With a complete cold block the edge shipped 1000-series ASAPFRG1 fragments to the +merger (verified via per-shard `cold drain`/`shipBatch` logs and thanos `count=1000`). +A cold storage-routing table pins `google_cluster_2019_cpu_rate → gorilla_object_store` +so its instant queries dispatch to the ThanosQueryEngine; the control-plane was stopped +during timing because it periodically re-POSTs a storage-routing table that overrides +the file table back to warm. **Single-node loopback — server-side latency only, no +network RTT.** Artifacts: `datasets_eval/latency/` (`latency_RESULTS.md`, +`latency_cdf.png`, `latency_summary.json`, `per_query_latency.json`, +`per_query_latency_cold.json`, `compute_latency.py`, `cold_latency_replay.py`, +`stack-coldon.sh`, `backend-storage-routing-coldon.yaml`, `agent-cold-ship.yaml`, +`queries-latency-cold.json`), branch `eval/fig7-cold-arm`. --- @@ -236,12 +411,64 @@ per-layer CPU/mem bars. **◐** --- -## Fig 9 — Coordinated vs uniform `p` on a skewed fleet ◐ -**Claim:** `p_i ∝ √(f_i/rate_i)` beats uniform-`p` at equal merged variance, with the -gap ∝ the rate CV; the win **only appears on skewed fleets** (multi-edge). -**Layout:** total edge work (or wire bytes) for coordinated vs uniform across rate-CV. -**Have:** the differentiated grant — hot edge `p=0.0065`, quiet `p=1.0` (ε-floors -0.0079 vs 0.444); single-edge ⇒ p=1 by design. **Need:** the CV sweep on ≥2 edges. **◐** +## Fig 9 — Coordinated ε-floor `p` vs fixed-p (NitroSketch) ✅ (algorithm) / ◐ (system) +**Claim (corrected):** the unified **whole-sketch** ε-floor `p = 1/(1+ε²·R)` (`R` = total +update rate, OBSERVABLE) gives a *principled* sampling rate that (i) cuts insert cost ~`1/p` +while bounding the whole-sketch L2 error to ε, and (ii) in a fleet adapts `p_i` to each edge's +own `R_i`. It is the SAME sampler as NitroSketch — the contribution is *deriving* `p` from +`ε`+`R` (not a hand-tuned knob), not a per-key accuracy win. (Per-key `√(f/rate)` retired.) + +### (a) Standalone algorithm comparison — REAL DEBS-2022, on a real CMS ✅ (corrected) +> **The earlier per-key version of this result was WRONG and is retracted.** It set +> `p_k=1/(1+ε²·f_k)` using the oracle per-key `f_k` — circular (knowing every `f_k` = exact +> counting, no sketch needed), and it resurrected the RETIRED per-key `√(f/rate)` allocation. +> The unified ε-floor is **whole-sketch**: ONE `p=1/(1+ε²·R)`, `R`=total update count (OBSERVABLE). + +`epsilon_floor_vs_nitro_test.go` — real CMS over real DEBS (5,493 keys, **R=54M** updates, +observable). Whole-sketch ε-floor `p=1/(1+ε²R)` vs exact. + +**(A) throughput / memory** (CMS 5×4096, sketchlib geometric skip-sampler): + +| metric | exact (p=1) | ε-floor (ε=0.05, p=7.4e-6) | note | +|---|---|---|---| +| **insert throughput** | 8.3 Mupd/s | **183 Mupd/s (22×)** | the NitroSketch win — skip-sampling cuts update work ~1/p | +| **memory** | 164 KB | 164 KB (constant in #keys) | vs exact key→count map 179 KB; CMS win is *asymptotic* | +| **query latency** | 96 ns/key | 76–82 ns/key | unchanged (CMS query is O(rows)) | + +**(B) accuracy law — what the ε-floor actually bounds** (CMS 5×65536 to isolate sampling from +collisions; mean of 5 seeds). The ε-floor bounds the **additive AGGREGATE** (F1=R) to ε; a +**point query on key `k`** is protected only to `ε·√(R/f_k)`: + +| ε | **F1-total rel-err** (the bounded aggregate) | heaviest key (f≈1.5M) | rare keys (f≲1e3) | +|---|---|---|---| +| 0.05 | **0.050** (= ε ✓) | 0.26 (pred ε√(R/f)=0.30) | → 1.0 (lost) | +| 0.10 | **0.080** (≈ ε ✓) | 0.39 (pred 0.61) | → 1.0 (lost) | + +So the honest, defensible claim is: **`p` derived from ε+observable R gives 22× insert +throughput while holding the whole-sketch aggregate error at ≈ε** — same sampler as NitroSketch, +the win is *how `p` is set* (not a hand-tuned knob). Per-key accuracy follows `ε·√(R/f_k)`: +**heavy hitters survive, rare keys fall below the sampling floor — inherent to sampling, not a +defect.** The right accuracy metric is therefore the aggregate / heavy-hitter error, never +rare-key rel-err. + +### (a2) Per-edge fleet — the differentiation, on a skewed fleet ✅ +Real DEBS has only **3 exchanges** (mild skew → fixed-p worst-edge only **1.3×** the ε-floor's), +too weak to show it. On a **synthetic fleet** (64 edges, Zipf rates over ~2 decades, +rate-CV=2.9), at matched total bandwidth: + +| | per-edge F1 sampling-err (analytical) | empirical F1 (real CMS, 5 seeds) | +|---|---|---| +| **ε-floor** (`p_i=1/(1+ε²R_i)`) | median 0.050, **max 0.050** (uniform) | hot 0.020 / **cold 0.020** | +| **fixed-p** (matched bw) | median 0.103, **max 0.162** | hot 0.010 / **cold 0.148** | + +`→` fixed-p **over-protects the hot edge and under-protects the cold edge by 7×** (cold-edge +0.148 vs 0.020); the ε-floor spends the same total bandwidth but equalizes error at ε across +the fleet. This is the per-edge adaptation the coordinator grants live (see (b)). + +### (b) System-level differentiated grant ◐ +Live on the cluster: hot edge `p=0.09`, quiet `p=0.14` (coordinator-granted ε-floor from +the autonomous `/plan/auto` loop, PR #382). **Need:** the rate-CV sweep on ≥2 edges for the +system figure. --- @@ -384,8 +611,9 @@ high-sample-per-window"). This requirement extends the *same* optimizer to alloc on it. (The wire/TCO cost model is the decision function.) 2. **sketch type + `(W, L, agg_type)`** — the existing bind-rules + cost output. 3. **sampling `p`** — which sketches are sampling-eligible (policy from the - controller) + the coordinated runtime allocation `p_i ∝ √(f_i/rate_i)` (data_plane - coordinator, ε-floored). + controller) + the coordinated runtime allocation, the whole-sketch ε-floor + `p_i = 1/(1+ε²·rate_i)` (data_plane coordinator; the per-key `√(f/rate)` law + was retired). ### Methodology — how to evaluate the 4-tuple `{sketch, size, p, ε_cdm}` @@ -487,8 +715,8 @@ coordinated sampling, topk, the ε-gate/delta regime). Driver+data: | 6.1 | threshold alert (Fig 5) | ✅ | | 6.2 | bandwidth ablation W×L×enc×p (Fig 2) | ◐ (p + encoding done; W, L to run) | | 6.headline | Pareto (Fig 1) | ◐ (corners done; one combined sweep) | -| 6.2 | edge CPU/mem + soak (Fig 6) | ◻ | -| 6.4 | query latency CDF (Fig 7) | ✅ warm (cold-fallback blocked) | +| 6.2 | edge CPU/mem + soak (Fig 6) | ✅ edge bounded (+2.6 MB/h); ⚠ backend leak +90 MB/h | +| 6.4 | query latency CDF (Fig 7) | ✅ warm + cold-fallback (both measured) | | 6.3 | cross-layer placement (Fig 8) | ◐ (design+proof; bars to run) | | 6.x | coordinated vs uniform (Fig 9) | ◐ (differentiation shown; CV sweep) | | 6.x | scaling N∈{1,10,100} (Fig 10) | ◻ | @@ -501,3 +729,433 @@ defensible numbers (incl. a real-workload trace). The *cost-baseline* half (raw- sketch CPU/mem, latency CDF) and the *multi-node* half (scaling, coordinated-vs- uniform CV sweep) are the runs that remain — and the multi-node axis is the one the coordinated-sampling claim most needs. + + +--- + + + +## Evaluation query plan — the queries to run on the top-5 datasets + +> **Audience:** anyone building or running the §6 evaluation. For *each of the five +> datasets* shortlisted in +> [`use-case-dataset-survey.md` §5b](use-case-dataset-survey.md), this doc writes down +> the **concrete queries we plan to run** — warm (sketch-answered) and cold (exact +> replay) — tagged to the routing **mode** (M1/M2), the **sketch families**, the **seven +> axes**, and the **§6 figure/claim** each query feeds +> ([`evaluation-plan-figures.md`](evaluation-plan-figures.md)). +> +> Companion to [`use-case-dataset-survey.md`](use-case-dataset-survey.md) (modes + +> warm-eligibility predicate, §0a; the top-5 + scale, §5b), +> [`pipeline-query-catalog.md`](pipeline-query-catalog.md) (what the pipeline can answer +> today) and [`sketch-algebra-query-mapping.md`](sketch-algebra-query-mapping.md) +> (query → sketch algebra). +> +> **Status:** query-plan reference, 2026-06-14. Datasets 1 (Google cluster) and 2 +> (DEBS-2022) are **wired** (`datasets_eval/google_cluster/`, `datasets_eval/debs/`); +> 3–5 are the **build-out** — the queries below are the spec their harnesses implement. + +--- + +## 0. Conventions — full evaluation = query × **every** supported sketch + +**Full evaluation runs each warm query against *every* sketch that supports its kind**, +on identical data, and scores each independently — so every query is a **head-to-head +across sketch families** (the Fig 3c DDSketch-vs-KLL comparison, generalised to all +kinds and all datasets). A query of kind *K* expands to one run per sketch in the +"all supported sketches" column below: + +| query kind | **all supported sketches (each run + scored)** | metric suffix per sketch | comparison reported | +|---|---|---|---| +| **quantile** (p50/p99/…) | **DDSketch**, **KLL** | `_q_ddsketch`, `_q_kll` | median vs tail rel-err, wire bytes (DD ≈ −26% wire, KLL wins median) | +| **topk / heavy-hitter** | **CountSketch-heap**, **CountMinSketch-heap** | `_topk_cs`, `_topk_cms` | recall@k, by-count vs by-value-sum, wire | +| **frequency** (point count of an item) | **CountMinSketch**, **CountSketch** | `_freq_cms`, `_freq_cs` | one-sided (CountMinSketch) vs unbiased (CountSketch) error | +| **cardinality** (distinct) | **HLL** *(only family)* | `_card_hll` | rel-err vs `1.04/√m` | +| **sum** | **Sum** *(lossless)* | `_sum` (bare metric) | exact (sanity) | +| **count** | **Count** *(lossless)* | bare metric | exact (sanity) | + +Each warm query is the MetricsQL/PromQL + ground-truth record from +`datasets_eval/multisketch/queries-*.json`: + +```json +{ "id": "...", "kind": "quantile|topk|frequency|count_unique|sum|count", + "metricsql": "quantile_over_time(0.99, _q_[300s])", + "sketches": ["ddsketch", "kll"], + "gt": { "op": "...", "metric": "...", "...": "..." } } +``` + +`` is expanded over the `sketches` list at harness time → one scored run each. +The `gt` is computed from the **raw replay** (exact); we score `rel-err = |warm − gt| / +|gt|` against the joint envelope `ε_sk + ε_s + ε_cdm` (recall for topk). Default window = +**300 s tumbling**, wall-clock-anchored (the Fig 3c fix). + +**In the per-dataset tables below**, the *sketches run* column lists the full set; the +metricsql shows the `_` slot the harness expands. **Cold queries** are exact +point/range replay from the Gorilla/`intchunk` archive (M2 mandatory raw + rare M1 +forensic). **M2 short-circuit:** a decision query `agg ⋛ τ` is answered from the warm +interval `[v−ε, v+ε]` — warm-only if `v+ε < τ` or `v−ε > τ`; only `τ ∈ [v−ε, v+ε]` drills +cold. Headline M2 metric = **warm-only-resolved fraction**, swept over `τ`, `ε`, and +*sketch family* (different ε per family → different short-circuit rate). + +--- + +## 1. Google cluster 2019 — **M1** (observability/resource) ✅ wired + +Metrics: `gct_cpu_rate`, `gct_memory_usage`; series key `(machine, job, task/instance)`; +group labels `zone`/`cell`/`service`. Use case: fleet / per-cell resource SLO dashboards. + +### Warm queries (each run on **all supported sketches**) + +| id | metricsql (`_` expanded) | kind | sketches run (each scored) | axes | feeds | +|---|---|---|---|---|---| +| `gct-cpu-p99` | `quantile_over_time(0.99, gct_cpu_rate_q_[300s])` | quantile | **DDSketch, KLL** | 2,6 | Fig 3a, Fig 3c, Fig 7 | +| `gct-cpu-p50` | `quantile_over_time(0.50, gct_cpu_rate_q_[300s])` | quantile | **DDSketch, KLL** | 2,6 | Fig 3a, Fig 3c | +| `gct-cpu-p99-by-cell` | `quantile_over_time(0.99, gct_cpu_rate_q_[300s])` *(by `cell`)* | quantile | **DDSketch, KLL** | 2,4,6 | Fig 3, repeated-dashboard | +| `gct-topk-host` | `topk(10, sum by (machine) (gct_cpu_rate_topk_))` | topk | **CountSketch-heap, CountMinSketch-heap** | 2,6 | Fig 3c topk recall (CountSketch-heap vs CountMinSketch-heap) | +| `gct-freq-service` | `count_over_time(gct_cpu_rate_freq_{service="svc-000003"}[300s])` | frequency | **CountMinSketch, CountSketch** | 2,6 | one-sided vs unbiased freq | +| `gct-card-service` | `count(gct_cpu_rate_card_hll)` | cardinality | **HLL** | 2,6 | Fig 3c per-series distinct | +| `gct-sum-cpu` | `sum(gct_cpu_rate_sum)` | sum | **Sum** (exact) | 2 | Fig 3c exact check | +| `gct-sum-mem-by-zone` | `sum by (zone) (gct_memory_usage_sum)` | sum | **Sum** | 2,4 | Fig 3c | +| `gct-cpu-alert` | `quantile_over_time(0.99, gct_cpu_rate_q_[300s]) > 0.8` | threshold | **DDSketch, KLL** | 2,3 | Fig 5 alert / repeated | + +### Cold queries (rare forensic — disjoint) + +| id | query | feeds | +|---|---|---| +| `gct-forensic-point` | exact CPU of `instance=X` at `t=2019-05-12T03:14Z` (range replay) | Fig 11 cold path (rare M1) | + +--- + +## 2. DEBS-2022 — **M2** (finance/tick) ✅ wired + +Metric: `debs_last_price` (Gauge `financial.last_trade_price`, value = `last`), attrs +`symbol`/`exchange`/`sectype`; `debs_volume`. 5-min tumbling, Berlin wall-clock. Use +case: live VWAP / price-quantile dashboards + threshold alerts on a **skewed** symbol +fleet; MiFID audit / backtest on the *same* series → cold. + +### Warm queries (each run on **all supported sketches**) + +| id | metricsql (`_` expanded) | kind | sketches run (each scored) | axes | feeds | +|---|---|---|---|---|---| +| `debs-price-p50` | `quantile_over_time(0.50, debs_last_price_q_{symbol="ASML.NL"}[300s])` | quantile | **DDSketch, KLL** | 2,7 | Fig 3 accuracy (EMA/VWAP proxy) | +| `debs-price-p99` | `quantile_over_time(0.99, debs_last_price_q_{symbol="ASML.NL"}[300s])` | quantile | **DDSketch, KLL** | 2,7 | Fig 3c per-family | +| `debs-vwap` | `sum(debs_last_price_sum * debs_volume_sum) / sum(debs_volume_sum)` *(per symbol)* | sum | **Sum** (exact) | 2,7 | VWAP exactness | +| `debs-roll-vol` | `sum_over_time(debs_volume_sum{symbol="ASML.NL"}[300s])` | sum | **Sum** | 2,4,7 | rolling-volume dashboard | +| `debs-topk-active` | `topk(10, sum by (symbol) (debs_ticks_topk_))` | topk | **CountSketch-heap, CountMinSketch-heap** | 2,7 | most-active-symbol board | +| `debs-coord-sample` | per-symbol ingest under the ε-floor `p_i = 1/(1+ε²·rate_i)` (skew sweep) | sampling | **DDSketch, KLL** (under `p`) | 1,7 | **Fig 9** coordinated 32× | + +### M2 short-circuit (decision queries — warm-first, cold on ambiguity; per sketch) + +| id | decision query | sketches | resolves warm-only when | drills cold when | +|---|---|---|---|---| +| `debs-move-alert` | `quantile_over_time(0.99, debs_last_price_q_{symbol=…}[300s]) > τ` | **DDSketch, KLL** | `v+ε < τ` or `v−ε > τ` | `τ ∈ [v−ε, v+ε]` | + +### Cold queries (mandatory raw — cheap via edge Gorilla) + +| id | query | feeds | +|---|---|---| +| `debs-audit-replay` | trade-by-trade exact replay for `(symbol, window)` (MiFID) | Fig 11 cold (finance-tick edge-Gorilla) | +| `debs-backtest` | exact tick series for `symbol` over the week | Mode-2 cold half | + +--- + +## 3. Alibaba microservices 2021/2022 — **M1** (observability/traces) ◻ to add + +Metrics: `alibaba_ms_latency` (span duration, per `service`), `alibaba_ms_calls`, +distinct-caller stream `alibaba_ms_callers`. Use case: per-service p50/p99 latency SLO +alerting (RED) + distinct-caller cardinality; incident trace replay → cold. **Metric- +identity split:** the latency *metric* is warm; the raw span archive is a *separate* +artifact → cold (condition 2 provable). + +### Warm queries (each run on **all supported sketches**) + +| id | metricsql (`_` expanded) | kind | sketches run (each scored) | axes | feeds | +|---|---|---|---|---|---| +| `ms-lat-p99` | `quantile_over_time(0.99, alibaba_ms_latency_q_{service="S"}[300s])` | quantile | **DDSketch, KLL** | 2,6,7 | **Fig 7** `quantile_over_time` at scale | +| `ms-lat-p50` | `quantile_over_time(0.50, alibaba_ms_latency_q_{service="S"}[300s])` | quantile | **DDSketch, KLL** | 2,6,7 | Fig 3c per-family | +| `ms-callers-card` | `count(alibaba_ms_callers_card_hll{service="S"})` | cardinality | **HLL** | 2,6 | distinct-caller fan-in | +| `ms-req-rate` | `sum by (service) (rate(alibaba_ms_calls[300s]))` | count | **Count** (exact) | 2,4,6 | RED rate dashboard | +| `ms-topk-slow` | `topk(10, sum by (service) (alibaba_ms_latency_topk_))` | topk | **CountSketch-heap, CountMinSketch-heap** | 2,6 | slowest-service board | +| `ms-lat-slo-alert` | `quantile_over_time(0.99, alibaba_ms_latency_q_{service="S"}[300s]) > 0.5` | threshold | **DDSketch, KLL** | 2,3 | Fig 5 / repeated SLO | + +### Cold queries (separate span artifact — disjoint) + +| id | query | feeds | +|---|---|---| +| `ms-trace-replay` | exact span tree for `trace_id=…` (incident forensics) | Fig 11 cold (the half anchors lack) | + +--- + +## 4. Azure VM 2019 — **M1** (observability/resource) ◻ to add + +Metrics: `azure_vm_cpu` (per-VM CPU, 5-min); cold counter `azure_vm_billing` +(**separate** series). ~2.6 M VMs = ~2.6 M series. Use case: per-VM CPU capacity/SLO +dashboards at fleet scale; billing/chargeback → cold. Textbook metric-identity split; +stresses the controller-allocation figure. + +### Warm queries (each run on **all supported sketches**) + +| id | metricsql (`_` expanded) | kind | sketches run (each scored) | axes | feeds | +|---|---|---|---|---|---| +| `vm-cpu-p99` | `quantile_over_time(0.99, azure_vm_cpu_q_[300s])` | quantile | **DDSketch, KLL** | 2,6 | **Fig 12** alloc @ ~2.6 M cardinality | +| `vm-cpu-p50` | `quantile_over_time(0.50, azure_vm_cpu_q_[300s])` | quantile | **DDSketch, KLL** | 2,6 | Fig 3c per-family | +| `vm-cpu-p99-by-sub` | `quantile_over_time(0.99, azure_vm_cpu_q_[300s])` *(by `subscription`)* | quantile | **DDSketch, KLL** | 2,4,6 | Fig 12 / repeated | +| `vm-card-vms` | `count(azure_vm_cpu_card_hll)` | cardinality | **HLL** | 2,6 | fleet-size distinct | +| `vm-avg-util-by-sub` | `sum by (subscription) (azure_vm_cpu_sum) / count by (subscription) (azure_vm_cpu)` | sum/count | **Sum, Count** | 2,4,6 | capacity dashboard | +| `vm-cpu-alert` | `quantile_over_time(0.99, azure_vm_cpu_q_[300s]) > 0.9` | threshold | **DDSketch, KLL** | 2,3 | Fig 5 / repeated | + +### Cold queries (separate billing series — disjoint) + +| id | query | feeds | +|---|---|---| +| `vm-billing-replay` | exact per-VM billing counter for `(vm, billing_period)` (dispute-grade) | Fig 11 cold (separate series) | + +--- + +## 5. Binance/Kraken crypto tick — **M2** (finance/tick) ◻ to add + +Metrics: `crypto_trade_price`, `crypto_trade_vol` (per `pair`). Hundreds of pairs, 24/7, +very-high per-series frequency. Use case: live crypto VWAP/quantile alerts + "is this +pair behaving oddly?" triage; strategy **backtest** replays raw on the *same* series → +cold. The Mode-2 / short-circuit showcase + the two orthogonal edge compressions. + +### Warm queries (each run on **all supported sketches**) + +| id | metricsql (`_` expanded) | kind | sketches run (each scored) | axes | feeds | +|---|---|---|---|---|---| +| `cx-price-p50` | `quantile_over_time(0.50, crypto_trade_price_q_{pair="BTCUSDT"}[300s])` | quantile | **DDSketch, KLL** | 2,7 | Fig 3 accuracy (high-freq) | +| `cx-price-p99` | `quantile_over_time(0.99, crypto_trade_price_q_{pair="BTCUSDT"}[300s])` | quantile | **DDSketch, KLL** | 2,7 | Fig 3c per-family | +| `cx-vwap` | `sum(crypto_trade_price_sum * crypto_trade_vol_sum) / sum(crypto_trade_vol_sum)` *(per pair)* | sum | **Sum** (exact) | 2,7 | VWAP exactness | +| `cx-roll-vol` | `sum_over_time(crypto_trade_vol_sum{pair="BTCUSDT"}[300s])` | sum | **Sum** | 2,4,7 | rolling-volume board | +| `cx-topk-active` | `topk(10, sum by (pair) (crypto_trades_topk_))` | topk | **CountSketch-heap, CountMinSketch-heap** | 2,7 | most-active-pair board | + +### M2 short-circuit (the headline metric for this dataset; per sketch) + +| id | decision query | sketches | resolves warm-only when | drills cold when | +|---|---|---|---|---| +| `cx-vol-alert` | `quantile_over_time(0.99, crypto_trade_price_q_{pair=…}[300s]) > τ` | **DDSketch, KLL** | `v+ε < τ` or `v−ε > τ` | `τ ∈ [v−ε, v+ε]` | +| `cx-anomaly-triage` | "which pair/window is anomalous?" → warm screen, then drill | **DDSketch, KLL** | warm screen settles it | flagged window → cold | + +Sweep `τ`, `ε`, **and sketch family** → report the **warm-only-resolved fraction** vs +forced-to-cold (the bound-based short-circuit headline) and the **cold-IO pruning ratio** +— DDSketch's relative-error bound vs KLL's rank-error bound give *different* ambiguous +bands, so the short-circuit rate is itself a per-family result. + +### Cold queries (mandatory raw — cheap via edge Gorilla) + +| id | query | feeds | +|---|---|---| +| `cx-backtest` | exact tick series for `pair` over N months (strategy replay) | Mode-2 cold half; edge-Gorilla cheap-cold | + +--- + +## 6. What each dataset's query set proves (coverage) + +| dataset | mode | warm query kinds × sketches | cold query | headline figure/claim | +|---|---|---|---|---| +| Google cluster 2019 | M1 | quantile {DD,KLL}, topk {CS-heap, CMS-heap}, freq {CMS, CS}, card {HLL}, sum, threshold | rare forensic point | Fig 3 accuracy, Fig 7 latency, Pareto | +| DEBS-2022 | M2 | quantile {DD,KLL}, VWAP {Sum}, topk {CS-heap, CMS-heap}, **coordinated sampling** | audit/backtest replay | **Fig 9** 32× sampling, edge-Gorilla cold | +| Alibaba microservices | M1 | quantile {DD,KLL} @scale, card {HLL}, rate {Count}, topk {CS-heap, CMS-heap} | trace replay (separate span) | high-card disjoint (Fig 11), Fig 7 | +| Azure VM 2019 | M1 | quantile {DD,KLL} @~2.6 M, card {HLL}, sum/count, threshold | billing replay (separate series) | **Fig 12** controller alloc | +| Crypto tick | M2 | quantile {DD,KLL}, VWAP {Sum}, topk {CS-heap, CMS-heap}, **short-circuit** | backtest replay | **bound-based short-circuit** per family | + +**Read:** every dataset runs the **same query kinds against the same full sketch sets** +(quantile → DDSketch + KLL, topk → CountSketch-heap + CountMinSketch-heap, frequency → CountMinSketch + +CountSketch, cardinality → HLL, sum/count → lossless) — so each query is a per-dataset +**head-to-head across families** on identical real data. What differs is the **mode** +(M1 disjoint vs M2 co-resident) and therefore whether the cold query is a *rare disjoint +forensic* (M1) or a *first-class, frequently-paired exact replay* (M2); the two M2 sets +additionally run the **short-circuit decision protocol**, whose warm-only-resolved +fraction is itself reported **per sketch family** (different ε ⇒ different ambiguous band). + +--- + +## 7. References + +- [`use-case-dataset-survey.md`](use-case-dataset-survey.md) — modes + predicate (§0a), top-5 + scale (§5b). +- [`evaluation-plan-figures.md`](evaluation-plan-figures.md) — Fig 3/5/7/9/11/12, the joint bound. +- [`pipeline-query-catalog.md`](pipeline-query-catalog.md) — what the pipeline answers today. +- [`sketch-algebra-query-mapping.md`](sketch-algebra-query-mapping.md) — query → sketch algebra. +- `datasets_eval/multisketch/queries-*.json` — the warm-query record format reused here. +- `datasets_eval/debs/DEBS_2022/02_benchmark_queries.md` — the DEBS Q1–Q13 benchmark queries. + +--- + +*End of query plan.* + +--- + + + +## Eval instrumentation notes — what each sweep CSV column means + +_Last updated: 2026-05-05 (paper blocker #3 closeout)._ + +This is the column-by-column "what is this number, where does it +come from, and why" reference for `deploy/eval-results/sweep-*.csv` +(the file format produced by +`deploy/mvp-singlenode/scripts/measure-baseline.py` and chained together by +`deploy/mvp-singlenode/scripts/run-baseline-sweep.sh` / +`deploy/mvp-singlenode/scripts/run_e2e_sweep.sh`). + +The next person to add a column or interpret one in a paper figure +should be able to land on this doc and understand the source of +truth without re-deriving it from comments scattered through +`measure-baseline.py`. + +## Column map + +Columns are emitted in the order shown in the CSV header. The +"source" column says which signal feeds it; the "fallback" column +is what `measure-baseline.py` consults when the primary path is +NaN. "Stack tier" is which container the signal originates from. + +| Column | Source (primary) | Fallback (paper blocker #3) | Stack tier | Unit / scale | +| --- | --- | --- | --- | --- | +| `baseline` | `--baseline` CLI arg (e.g. `b3-delta`) | — | — | label | +| `scale` | `--scale` CLI arg (e.g. `N1`, `N10`) | — | — | label | +| `rate` | `--rate` CLI arg | — | — | events/s (workload knob) | +| `cardinality` | `--cardinality` CLI arg | — | — | distinct series (workload knob) | +| `producer_cpu_cores` | `docker stats` CPU% / 100 on `--producer-container` | — | producer (`otel-app`) | cores | +| `producer_rss_mib` | `docker stats` mem on `--producer-container` | — | producer | MiB | +| `producer_bytes_out_per_s` | `docker stats` net tx delta on `--producer-container`, divided by `--bytes-sample-window` | — | producer | bytes/s on the wire | +| `agent_cpu_cores` | `rate(otelcol_process_cpu_seconds_total{job="agents"})` | — | agent | cores | +| `agent_rss_mib` | `otelcol_process_memory_rss_bytes{job="agents"}` / 1MiB | — | agent | MiB | +| `agent_in_kib_per_s` | `rate(otelcol_asapcollector_processor_input_bytes_total{job="agents"})` / 1024 | `docker stats` net rx avg across `docker-compose-agent-*` / 1024 | agent | KiB/s | +| `agent_out_kib_per_s` | `rate(otelcol_asapcollector_processor_output_bytes_total{job="agents"})` / 1024 | `docker stats` net tx avg across `docker-compose-agent-*` / 1024 | agent | KiB/s | +| `agent_points_per_s` | `rate(otelcol_receiver_accepted_metric_points_total{job="agents"})` | — | agent | points/s | +| `gateway_cpu_cores` | `rate(otelcol_process_cpu_seconds_total{job="gateway"})` (with v0.108 fallback) | — | gateway | cores | +| `gateway_rss_mib` | `otelcol_process_memory_rss_bytes{job="gateway"}` / 1MiB | — | gateway | MiB | +| `gateway_points_per_s` | `rate(otelcol_receiver_accepted_metric_points_total{job="gateway"})` (with v0.108 fallback) | — | gateway | points/s | +| `gateway_out_series_per_s` | `rate(otelcol_exporter_sent_metric_points_total{job="gateway"})` (with v0.108 fallback) | — | gateway | series/s | +| `backend_cpu_pct` | `docker stats` CPU% on `docker-compose-backend-1` | — | backend | percent of one core | +| `backend_rss_mib` | `docker stats` mem on `docker-compose-backend-1` | — | backend | MiB | +| `backend_samples_per_s` | `rate(asap_ingest_samples_total)` (backend `:9091/metrics`) | `rate(otelcol_exporter_sent_metric_points_total{exporter=~"otlp.*backend.*",job="gateway"})` | backend (or gateway when fallback) | samples/s | +| `backend_query_p99_ms` | `1000 * histogram_quantile(0.99, rate(asap_query_duration_seconds_bucket))` (backend `:9091/metrics`) | `--replay-jsonl PATH` → p99 of successful `duration_ms` rows in client JSONL | backend (server-side) or replay client (client-side) | ms | + +## Caveats — do not paper over these + +### `agent_in_kib_per_s` / `agent_out_kib_per_s`: in-process bytes vs wire bytes + +The patched-processor counter +(`otelcol_asapcollector_processor_*_bytes_total`) measures the +**OTLP protobuf MessageSize of the in-process `pmetric.Metrics` +batch as it crosses the processor boundary**, computed by the +`pmetric.ProtoMarshaler` in +`opentelemetry-collector-patch/processor/selfmonitor/selfmonitor.go`. +That's not the same number as bytes-on-the-wire: + + * It excludes gRPC framing, HTTP/2 headers, and the OTLP + request envelope. + * For sketch payloads (DDSketch / HLL / etc.), the in-process + bytes include the typed proto envelope's full state — the + same payload that goes on the wire — so the two measures + agree to within ~5%. + * For raw / Gorilla / Serf baselines that have no patched + processor, no in-process counter fires at all. The fallback + is `docker stats` net rx/tx on the agent container, which + IS the on-the-wire rate. + +When comparing across baselines (the bandwidth claim in +`docs/paper-outline.md` claim #1), prefer the `docker stats` +fallback as the apples-to-apples ground truth — note this +explicitly in the figure caption. The patched-processor counter +is the right signal for "bytes the sketch processor saw"; the +wire bytes are the right signal for "bytes the bandwidth budget +spent." + +### `backend_samples_per_s`: backend ingest vs gateway egress + +Today's `asap/query-backend:dev` image does NOT expose +`asap_ingest_samples_total` to Prometheus. The `:9091/metrics` +surface only contains query-side counters +(`asap_query_duration_seconds`, `asap_query_requests_total`). +The fallback uses +`otelcol_exporter_sent_metric_points_total{exporter=~"otlp.*backend.*",job="gateway"}`, +which counts the metric points the gateway forwarded to the +backend over OTLP. Modulo dropped batches (a small number +tracked elsewhere), gateway-egress equals backend-ingress, so +this is the right proxy. + +For raw / Gorilla / Serf baselines that drop the OTLP forward +(`drop_original: true` in the agent yaml), the gateway is +literally not receiving anything from the agent — so this +fallback returns 0, which is correct: those baselines write +their compressed output to a local container directory, not to +the backend. The bandwidth signal for those baselines lives in +`agent_*_kib_per_s` (docker-stats fallback). + +The proper fix is a backend-side counter — that's tracked as a +follow-up in the ASAPQuery-backend repo. Fallback is good +enough for the paper. + +### `backend_query_p99_ms`: server-side vs client-side p99 + +`measure-baseline.py` chooses the client-side fallback whenever +`--replay-jsonl PATH` is provided, because: + + * The Prometheus path dies with the stack + (`docker compose down -v` between cells in + `run_e2e_sweep.sh`), so the histogram is gone before the + next cell can inspect it. The replay JSONL persists on the + host filesystem. + * Client-side latency is what the caller actually + experienced, including any network / OTLP serialization + delay — which IS what the paper claim ("query latency + competitive with raw") is about. + +The numbers will not be identical: client-side adds the +local-loopback HTTP round-trip (~0.1-0.5ms on the dev box). +For the paper figure, document which side the number came from +in the caption. `run_e2e_sweep.sh` always passes +`--replay-jsonl`, so e2e-sweep CSVs are always client-side p99. +`run-baseline-sweep.sh` is client-side only when +`DRIVE_QUERIES=1` is set. + +### `asap_query_duration_seconds` only fires on serviced queries + +Empty-but-running stack → the histogram has 0 buckets. So the +primary-path PromQL returns NaN for any cell where no queries +were issued during the soak. That's not a bug, that's the +metric's contract; just make sure the sweep driver issues some +queries before reading the column. `run_e2e_sweep.sh` does this +by default (it runs `metricsql_replay.py` for the full soak). +`run-baseline-sweep.sh` did NOT prior to 2026-05-05; the +`DRIVE_QUERIES=1` opt-in flag added in this paper-blocker-#3 +work fills the gap. + +## Signal coverage by baseline (post paper-blocker-#3) + +| Baseline | `agent_*_kib_per_s` | `gateway_*` | `backend_samples_per_s` | `backend_query_p99_ms` | +| --- | --- | --- | --- | --- | +| `b0a-raw-stream` | docker stats fallback | direct | gateway-fallback | client-side (when DRIVE_QUERIES) | +| `b0b-raw-batched` | docker stats fallback | direct | gateway-fallback | client-side (when DRIVE_QUERIES) | +| `b1-serf` | docker stats fallback | direct (zero — drop_original) | gateway-fallback (zero) | client-side (no warm answers — cold path) | +| `b2-full` | direct (sketch processor) | direct | direct (when ingest counter exists) or gateway-fallback | direct (when histogram is fed) | +| `b3-delta` | direct (sketch processor) | direct | direct or gateway-fallback | direct or client-side | +| `b4-tunable` | direct (sketch processor) | direct | direct or gateway-fallback | direct or client-side | +| `b5-gorilla` | docker stats fallback | direct (zero — drop_original) | gateway-fallback (zero) | client-side (no warm answers — cold path) | + +Note that B1/B5 baselines are **expected** to show ~0 +gateway-egress and 0 backend-samples: their pipeline is +`drop_original: true`, so the wire path to backend is empty by +design — the bandwidth signal lands in `agent_*_kib_per_s` +(docker-stats fallback) or in compressed-blob-on-disk metrics +that aren't in the CSV today. + +## Adding a new column + +1. Define the source. Prefer Prometheus over `docker stats` + (Prom is rate-aware; docker stats requires the two-sample + pass). +2. If the source isn't universal across baselines, add a + fallback to `FALLBACK_QUERIES` in + `deploy/mvp-singlenode/scripts/measure-baseline.py` (or in `main()` for + non-Prom fallbacks). Don't silently let the column NaN — + one of the bandwidth-claim figures was unreproducible for a + week because of exactly that. +3. Add a row to the column map above. Mention any unit subtlety + in the caveats section. +4. Smoke-test by running + `deploy/mvp-singlenode/scripts/run-baseline-sweep.sh DRIVE_QUERIES=1` and + confirming the column has no NaN for any baseline. diff --git a/docs/gos-eval-results.md b/docs/gos-eval-results.md new file mode 100644 index 000000000..927f51275 --- /dev/null +++ b/docs/gos-eval-results.md @@ -0,0 +1,286 @@ +# GOS Evaluation Results + +Measured results for the GOS framework (design-gos-unified-edge-telemetry.md). +Three axes: (1) anisotropic-vs-isotropic delta communication `ρ`; (2) geometric +vs distributed F2 monitoring; (3) the Woodruff–Zhang `k/ε²` reference. + +Reproduce: `deploy/mvp-multinode/scripts/f2_monitor_eval.sh` (F2), and +`go test ./sketches/ -run TestGosAnisoSavingsRatio -v` (ρ). + +--- + +## 1. Anisotropic vs isotropic per-cell thresholds (`ρ`) + +At equal accuracy budget `B`, the per-cell water-filling gives communication +`Σ V_j/T_j` never worse than a uniform threshold (Cauchy–Schwarz), by the factor +`ρ = (Σ√(|g_j|·V_j))² / [(ΣV_j)(Σ|g_j|)] ≤ 1`. Sweeping the cell-magnitude skew +(Zipf(s) → `g_j=2|Ĉ_j|`, uniform activity), `n = d·w = 1280`, `k = 4`: + +| skew `s` | measured `ρ` | predicted `ρ` | comm saving | +|---|---|---|---| +| 0.0 (uniform) | 1.0000 | 1.0000 | 0% | +| 0.5 | 0.9026 | 0.9026 | 9.7% | +| 1.0 (Zipf) | 0.4966 | 0.4966 | **50.3%** | +| 1.5 | 0.1283 | 0.1283 | 87.2% | +| 2.0 (heavy) | 0.0284 | 0.0284 | 97.2% | + +The `ρ` above is a **closed-form check**: both columns evaluate the same +Cauchy–Schwarz factor on a synthetic cell vector, so the exact match confirms +only that `AllocateThresholds` realizes the water-filling optimum — it is **not** +a measurement of communication on a real stream (a caveat first raised in the +code review). The cost of anisotropic mode is the `O(d·w)` edge-memory threshold +vector; the `GosAnisotropic` toggle exposes the tradeoff. + +### Cluster measurement (real agents, iptables byte count) + +Replaces the closed-form check with an end-to-end measurement: +`deploy/mvp-multinode/scripts/gos_aniso_cluster.sh` runs a real asap-otel agent +(CountSketch `top_endpoint_qps`, `gos_delta_epsilon=0.1`, iso vs `gos_anisotropic: +true`) fed by the otel-app five-sketch producer with Zipf(`s`) endpoint labels, +and counts the delta bytes that reach the sink node's `:4317` with an iptables +counter (kernel-side, exact), over 70 s of steady state per arm after a warmup: + +| Zipf `s` | iso bytes | aniso bytes | measured `ρ` | +|---|---|---|---| +| 1.1 | 503,230 | 303,901 | **0.60** (40% saving) | +| 1.5 | 301,851 | 302,328 | 1.00 | +| 2.0 | 303,693 | 300,198 | 0.99 | + +**This is the honest result, and it does NOT match the closed-form story.** +Anisotropic saves ~40% at *low* input skew (`s=1.1`) but is a wash at higher +skew — the OPPOSITE of the closed-form prediction that savings grow with skew. +Two likely confounds, still to be run down: (a) a ~303 KB floor across most +cells suggests a fixed per-window cost (full keyframe / re-fill after +window-reset) dominating the *delta* the gate controls; (b) the CountSketch hash +SPREADS a heavy Zipf endpoint across `d` random-sign cells, so high *input* skew +does not straightforwardly produce high *cell-magnitude* skew — the quantity the +water-filling actually exploits. Treat the anisotropic per-cell win as +**unconfirmed on real workloads** pending this investigation; the mechanism +(config parse → `applyGosMode` → per-cell `{T_j}`) is verified live end-to-end, +the payoff is not. + +--- + +## 2. Geometric vs distributed F2 monitoring + +Cross-language (Rust coordinator + Go multi-edge driver), `k=4` edges, 20 +sub-window steps, `d=5`, `w=256`, `τ=10⁶`, `ε=0.1`. Total bytes = edge→coord +sketch bytes + coord→edge `C_ref` broadcast bytes. + +**Cluster (real NIC):** the same matrix reproduced across the 8-node CloudLab +fabric — edges on a source node, coordinator on the WARM node, every ship and +broadcast crossing the 10 GbE LAN — with byte-identical totals (the protocol is +deterministic) and the same alert decisions. Reproduce: +`deploy/mvp-multinode/scripts/f2_wholesketch_cluster.sh` → recorded at +`deploy/mvp-multinode/eval-8node/f2_wholesketch_cluster.csv`. + +| Workload | Mode | Alert | Total bytes | vs distributed | +|---|---|---|---|---| +| **stable** (F₂ < τ) | raw (no aggregation) | none ✓ | 3,956 | — | +| **stable** | distributed | none ✓ | 923,280 | 1.0× | +| **stable** | **geometric** | none ✓ | **230,820** | **0.25× (4.0× less)** | +| **ramp** (F₂ crosses τ) | raw (no aggregation) | fired (exact) ✓ | 7,120 | — | +| **ramp** | distributed | fired @921,600 ✓ | 923,280 | 1.0× | +| **ramp** | **geometric** | fired @921,600 ✓ | **531,132** | **0.58× (1.74× less)** | + +Both sketch modes fire the **same** alert (observed 921,600, inside the +`[(1−ε)τ, τ) = [900k, 10⁶)` band). Geometric wins in **both** regimes vs +distributed. + +**Raw baseline honesty — and why this workload is protocol-stress, not +bandwidth-scale.** The raw rows ship every sample as a real msgpack +`[ts, key, value]` frame (exact-F2 alert ground truth — `f2driver … raw`). On +the DEFAULT workload raw is the cheapest of all — **by construction, and it +does not generalize**: the workload has only `H=4` distinct keys, so raw is +~320 samples (~7 KB), while a sketch ship is a **fixed** `d·w ≈ 11.5 KB` +regardless of `H`. A sketch only pays off once cardinality is large enough +that raw exceeds that fixed cost. Measured crossover (`F2_KEYS` sweep, ramp, +`k=4` edges, 20 steps; distributed is fixed at 923,280 and geometric at +531,132 — sketch size is `H`-independent): + +| `H` distinct keys | raw bytes | raw / distributed | raw / geometric | +|---|---|---|---| +| 4 (default) | 7,120 | 0.01× | 0.01× | +| 256 | 482,960 | 0.52× | 0.91× | +| **512** | 974,480 | **1.06×** (raw now loses) | 1.83× | +| 2,048 | 4,007,440 | 4.34× | 7.55× | +| 8,192 | 16,295,440 | 17.65× | 30.68× | +| 32,768 | 67,268,880 | 72.86× | 126.65× | + +So **raw wins only below ~500 keys** (vs distributed) / ~280 (vs geometric); +real telemetry cardinality (10³–10⁶ series) puts you deep in the sketch-wins +regime — consistent with the C1-wire dataset result (33.8×/65.9× reduction on +the Google-cluster trace). **Two orthogonal claims live here:** (1) +sketch-vs-raw is a *cardinality* question (settled by the crossover above, and +by C1-wire at real scale); (2) geometric-vs-distributed is a *monitoring* +question — ship-on-violation vs ship-every-window. The geometric **ship count** +(the monitoring decision) is ~`H`-independent (F2 trajectory vs safe zone), but +its **byte count is not**, because the `C_ref` broadcast size grows with sketch +density — see the H=2048 run below. + +### Realistic cardinality (H=2048, τ auto-scaled) — sketch beats raw, but geometric's broadcast blows up + +`f2_wholesketch_cluster.sh` now defaults to `H=2048` distinct keys with +`τ = 250000·H` (keeps the ramp crossing at the same fractional step; recorded at +`eval-8node/f2_wholesketch_cluster_h2048.csv`): + +| workload | mode | alert | total bytes | vs distributed | +|---|---|---|---|---| +| stable | raw | none ✓ | 204,056 | — | +| stable | distributed | none ✓ | 923,280 | 1.0× | +| stable | **geometric** | none ✓ | **230,820** | **0.25× (4.0× less)** | +| ramp | raw | fired ✓ | 4,007,440 | — | +| ramp | **distributed** | fired ✓ | **923,280** | **1.0× — beats raw 4.3×** | +| ramp | geometric | fired ✓ | 1,647,966 | **1.78× (LOSES to distributed)** | + +Two findings, both honest: +- **Sketch beats raw at scale.** Ramp distributed (923,280, fixed `d·w`) is + **4.3× smaller** than raw (4,007,440) — the cardinality concern is resolved + once `H` is realistic. +- **Geometric's ramp win does NOT survive a dense sketch.** Geometric ramp rose + from 531,132 (H=4) to 1,647,966 (H=2048) and now *loses* to distributed. The + egress decomposes as `1,647,966 − 28·11,541 (ingress) = 1,324,818 ≈ + 4·28·11,541` — i.e. **the sparse `C_ref` deltas have degenerated into + near-full matrices**: 2048 keys in a 1280-cell (`d·w=5·256`) sketch saturate + almost every cell, so "changed cells" ≈ "all cells" and the O(k) broadcast + amplification returns. Stable geometric still wins 4× (only 4 ships → 4 + broadcasts), but ramp (28 ships × near-full broadcast) does not. This is + exactly the design §12 #1 (anisotropic/thresholded broadcast) + small-norm + limit, now *measured*: the geometric protocol pays off when the sketch is + **sparse relative to its cell budget**; an overloaded sketch needs a larger + `w`, a thresholded (not `Δ≠0`) broadcast gate, or both. The `H=4` run isolates + the monitoring logic; this run shows the real-scale communication reality. + +**Density sweep — confirms fill ratio, not cardinality, is the cause.** Fixing +`H=2048` and sweeping the sketch width `w` (ramp, geometric ÷ distributed bytes): + +| `w` | fill `H/(d·w)` | distributed | geometric | geo/dist | +|---|---|---|---|---| +| 256 | 1.60 (overloaded) | 923,280 | 1,647,966 | 1.78× | +| 1024 | 0.40 | 3,688,080 | 6,044,478 | 1.64× | +| 4096 | 0.10 | 14,747,280 | 16,160,883 | 1.10× | + +The `geo/dist` ratio falls monotonically as the sketch gets sparser (1.78 → 1.10), +so **density is the root cause**. But a wider sketch is not the whole fix: +`distributed` absolute cost explodes with `w` (it ships the full matrix every +window). + +### Thresholded C_ref broadcast gate (design §12 #1) — implemented, and an honest limit + +Implemented the OctoSketch-style gate in the coordinator: broadcast only cells +that moved by more than the §7 F2 per-cell threshold `T = ε‖C‖/(2k√(dw))`, and +fold **only the shipped cells** back into `last_broadcast` so sub-`T` changes +accumulate and eventually ship (C_ref error bounded by `T` per cell; +`f2.rs::sparse_delta_cells_thresholded` + `apply_cells`, tested). **Safe and +correct:** the `H=4` numbers are byte-identical (531,132 — the gate never fires +on a sparse sketch), alerts still fire, and the coordinator↔edge references stay +consistent. + +**But its empirical payoff on a Count-Sketch is small** — an honest negative +result. At `H=2048, w=256` the gate cut geometric ramp only 1,647,966 → +1,316,158 (~20%), and on a *skewed* Zipf(1.2) ramp it did no better (1.80 M). +The reason is structural: a Count-Sketch hashes each key into `d` cells with +**random signs**, and at overload many keys collide per cell, so **input skew +does not become cell-magnitude skew** — the cell values are homogenized, and a +threshold has little sparse structure to exploit (unlike Count-Min, where a heavy +key *is* a heavy cell). So the gate is the right mechanism but the Count-Sketch +cell homogenization caps its benefit; the effective lever for high cardinality +remains **sizing `w` to the key count** (keep fill `H/(d·w) ≪ 1`), with the +threshold gate as a safe, free add-on that helps whenever real cell skew exists. + +### Real dataset — DEBS 2022 trading day (no synthetic workload) + +Replays the **real DEBS 2022 Grand-Challenge trading-day** feed (Infront +Financial; 54M market events). Each ticker symbol is a monitored key; the global +`F2 = Σ_symbol (cumulative event count)²` (trade-concentration / self-join size) +grows over the day, and the monitor fires when it crosses `τ` ("trading got too +concentrated"). The first 4M events → 20 sub-windows × 4 edges (symbol→edge by +hash), `H = 5493` real distinct symbols, `d=5, w=4096` (fill `0.27`, not +overloaded), `τ = 2.5·10¹⁰` (crosses at step 14). Reproduce: +`deploy/mvp-multinode/scripts/f2_debs_eval.sh` (preprocessor +`datasets_eval/debs/scripts/debs_f2_trace.py` + f2driver `F2_TRACE` replay), +recorded at `eval-8node/f2_debs.csv`: + +| mode | total bytes | alert | vs raw | +|---|---|---|---| +| raw (ship every event) | 99,122,250 (99 MB) | fired ✓ | 1.0× | +| **distributed** | 14,747,280 (14.7 MB) | fired ✓ | **0.15× (6.7× less)** | +| **geometric** | 13,033,555 (13.0 MB, 40/80 ships) | fired ✓ | **0.13× (7.6× less)** | + +**On real data every claim holds, and cleanly:** the sketch beats raw **6.7×** +(distributed) / **7.6×** (geometric) — the cardinality is real (`H=5493`), so +the fixed `d·w` cost is far below the 4M-event raw stream; and **geometric beats +distributed** (13.0 vs 14.7 MB, ships 40/80) because a properly-sized `w=4096` +sketch is sparse (no dense-broadcast amplification). The geometric margin is +modest here because DEBS `F2` is a monotone all-day ramp (the sketch keeps +drifting past the safe zone, so it ships ~half the windows); a stable/low-drift +period — the scenario geometric targets — silences more and widens the gap. This +is the honest end state of the whole thread: on a real workload, sketch ≫ raw and +geometric > distributed, both by construction rather than a tuned synthetic. + +### Effect of the `C_ref` delta broadcast (design §12 open-problem #1) + +The geometric coordinator→edge `C_ref` was originally a full `~11.5 KB` matrix +per resync × `k` edges (O(k) amplification), which made geometric *lose* the ramp +regime. Delta-encoding the broadcast (sparse changed cells only): + +| ramp geometric | egress (`bytes_out`) | total | +|---|---|---| +| full-matrix broadcast | 1,107,936 | 1,620,000 (loses) | +| **sparse delta broadcast** | **207,984** | **531,132 (wins)** | + +Egress dropped **5.3×**, flipping geometric from a loss to a `1.74×` win. + +### Delta-loss resilience (safety under a dropped `C_ref` delta) + +The sparse delta is a dependency chain, so a lost/corrupt `ΔC_ref` could leave an +edge running its safe-zone test against a diverged reference — a *silent missed +violation*. Reproduce: `deploy/mvp-multinode/scripts/f2_deltaloss_demo.sh` +(injects loss on edge-0's 2nd delta via the f2driver `F2_INJECT` knob), same +ramp/geometric scenario three ways: + +| scenario | alert | total bytes | ref_errs | behavior | +|---|---|---|---|---| +| no loss | **1** ✓ | 531,132 | 0 | baseline | +| corrupt delta | **1** ✓ | 531,132 | 1 | edge-0 **detects** the bad delta → `needFull` → force-ship | +| dropped delta | **1** ✓ | 506,106 | 0 | silent loss undetected by that edge; recovered by the periodic keyframe + the other edges' true sketches in the global merge | + +**The alert fires in all three cases** — safety is preserved under delta loss. A +*corrupt* delta is caught at the edge (`ref_errs=1`, force-ship); a *silently +dropped* delta has no sequence gap for that edge to detect (`ref_errs=0`), so its +recovery rests on the coordinator's periodic Full keyframe (`F2_KEYFRAME_INTERVAL`) +and the fact that the global `mean_f2` still sums the other edges' exact sketches. +A per-broadcast sequence number (edge-detected gap → on-demand resync request) +would close the silent-drop detection gap; it is noted as future work. + +--- + +## 3. Woodruff–Zhang `k/ε²` reference + +WZ (STOC'12) proves continuous `(1±ε)` F₂ monitoring over `k` sites needs +`Θ̃(k/ε²)` communication — a **hard lower bound** for any protocol. Normalizing by +the "one round" unit `k·S` (`S ∝ d·w ∝ 1/ε²` = one sketch), here +`k·S = 4 × 11,541 ≈ 46 KB`: + +| Mode / workload | total / (k·S) | reading | +|---|---|---| +| distributed | **20×** | ships every step → `W=20` rounds | +| geometric, ramp | 11.5× | near-threshold ships | +| geometric, stable | **5.0×** | within a small constant of the `k/ε²` floor | + +Distributed pays the full `W×` (re-ships every step); geometric approaches the +`Θ̃(k/ε²)` floor in the stable regime (few resyncs). No protocol beats `k/ε²` +worst-case — geometric's win is the data-dependent constant, exactly as the +theory predicts. + +--- + +## 4. Per-row sampling (unbiasedness) + +`CountSketch.UpdateStringSampledPerRow` (per-row geometric admission, `1/p` +weighting): inserting a heavy key `N=20,000` times at `p=0.5` estimates within +`<15%` relative error (`sketchlib-go .../sampled_test.go`), confirming the +inverse-probability weighting keeps the estimator unbiased under per-row +admission. Per §3.2 the per-row (vs per-item) form additionally decorrelates the +`d` row estimates so the median concentrates the sampling error into the `(1−δ)` +guarantee — realized at equal edge CPU. diff --git a/docs/mvp-demo-runbook.md b/docs/mvp-demo-runbook.md index 98e40cb6d..042bb088c 100644 --- a/docs/mvp-demo-runbook.md +++ b/docs/mvp-demo-runbook.md @@ -837,4 +837,4 @@ docker builder prune --all - `docs/comparison-asap-vs-databricks-pantheon-hydra.md` — architectural framing vs. Databricks Pantheon + Hydra - `docs/control-plane-design.md` — controller pipeline (L1 → L5) - `docs/e2e-test-guide.md` — pytest-style smoke tests (smaller scope than the MVP demo) -- `docs/eval-instrumentation-notes.md` — measurement methodology notes +- `docs/evaluation-plan-figures.md` — measurement methodology notes diff --git a/docs/phase-2.md b/docs/phase-2.md new file mode 100644 index 000000000..95a11e06b --- /dev/null +++ b/docs/phase-2.md @@ -0,0 +1,891 @@ +# Phase 2 — history (extraction plan + performance audit) + +> **Historical record.** Consolidates the three Phase-2 docs (runtime +> extraction map, Go perf audit, perf deployment) into one archive. +> These record COMPLETED work; kept for provenance. + + +--- + + + +## Phase 2 execution plan — extract `asap-precompute-go` + +_Companion to [ADR-0002](adr/adr-0002-extract-precompute-runtime.md). +File-by-file extraction map for moving the runtime out of the +five Go OTel processors into a shared `asap-precompute-go` +module._ + +## Inventory of source files + +``` +opentelemetry-collector-contrib-patch/processor/ +├── ddsketchprocessor/processor.go 942 LoC +├── kllprocessor/processor.go 720 LoC +├── hllprocessor/processor.go 785 LoC +├── countsketchprocessor/processor.go 663 LoC +└── countminsketchprocessor/processor.go 739 LoC + -------- + 3849 LoC total +``` + +The processors fall into two structural patterns that need to be +harmonized in the extracted runtime: + +- **Pattern A** (DDSketch / KLL / HLL): nested + `resourceWindow → scopeWindow → metricWindow → sketchSeries` + hierarchy. Per-`pmetric.ScopeMetrics` aggregation. Explicit + `accumulateIntoWindow` / `flushWindow` pair. +- **Pattern B** (CountSketch / CountMin): flat + `windowSketch` (map: partitionKey → sketch). Per-metric + aggregation. Timer-driven `startWindowLoop` / + `emitWindowAndReset`. + +Phase 2 unifies both into the generic `Precompute[SketchT]` +shape from ADR-0002. The window manager picks +tumbling/sliding/batch internally based on `PrecomputeConfig`. + +## Target layout + +``` +asap-precompute-go/ +├── go.mod // module github.com/ProjectASAP/asap-precompute-go +├── observation.go // Observation + ObservationValue (~50 LoC) +├── envelope.go // SketchEnvelope view of the sketchlib-go proto (~30 LoC) +├── precompute.go // Precompute interface + generic impl (~250 LoC) +├── window.go // tumbling / sliding / batch logic (~250 LoC) +├── snapshot_cache.go // outbound + inbound caches; ComputeDelta (~200 LoC) +├── matchers.go // LabelMatcher, seriesKey, seriesAttrs (~150 LoC) +├── config.go // PrecomputeConfig, AggregationMode, OnOverflow (~120 LoC) +├── adapter.go // Adapter interface + Decode/Encode helpers (~100 LoC) +├── controlchannel/ +│ ├── channel.go // ControlChannel interface (~30 LoC) +│ ├── http_poll.go // HttpPollChannel impl (~80 LoC) +│ └── opamp.go // OpAmpChannel adapter wrapping existing controller/opamp (~50 LoC) +├── telemetry.go // PrecomputeStats + recordInput/recordOutput (~80 LoC) +└── otel/ // OTel-flavored Adapter helpers (consumed by Phase-2 shims) + ├── decode.go // pmetric.Metrics → []Observation + ├── encode.go // []SketchEnvelope → pmetric.Metrics + └── seriesattrs.go // attribute key construction +``` + +Approximate total: 1500–1800 LoC. Each existing OTel processor +shrinks to ~50–80 LoC shim that constructs a +`Precompute[]` and delegates `ConsumeMetrics`. + +## Function-level extraction map + +For each existing function, where it goes after Phase 2: + +### Pattern A (DDSketch, KLL, HLL) — same map applies to all three + +| Today | Layer | Becomes | +|---|---|---| +| `type resourceWindow / scopeWindow / metricWindow / sketchSeries` | 3 | `precompute.go` — collapse into a single `series` struct keyed by `(agg_id, label_key)` since the resource/scope hierarchy was an OTel-side concern, not an algorithmic concern. | +| `func newProcessor` | 4 | stays in shim; constructs `Precompute[*ddsketch.DDSketch]` | +| `func Start` | 4 | stays in shim; spawns ticker goroutine that calls `Precompute.Tick` and emits via `Adapter.Encode` + `next.ConsumeMetrics` | +| `func Shutdown` | 4 | stays in shim; cancels ticker, calls `Precompute.Shutdown` | +| `func ConsumeMetrics` | 4 | stays in shim; calls `Adapter.Decode(md)` → `for _, o := range obs { p.pc.Observe(o) }` → `next.ConsumeMetrics(ctx, md)` (pass-through) | +| `func processBatch / processScopeMetrics` | 4 | becomes the `otel.Decode` helper; produces `[]Observation` | +| `func consumeDDSketchDataPoints / consumeGaugeDataPoints` | 4 | folded into `otel.Decode`; produces `Observation::Envelope` for sketch-typed inputs and `Observation::Float` for scalar | +| `func decodeDDSketchDataPoint` | 4 | folded into `otel.Decode` envelope path | +| `func cacheInboundSnapshot` | 3 | `snapshot_cache.go::CacheInbound` | +| `func newSketchSeries / updateWindow / merge / ensureSketch` | 3 | `window.go` window-state helpers | +| `func serializeDDSketch` | 1 | already lives in `sketchlib-go`; called via `Sketch.Snapshot()` | +| `func computeDDSketchDelta` | 3 | `snapshot_cache.go::ComputeDelta` | +| `func attributesKey / seriesKey / seriesAttrs / matchesMatchers / newSeriesFrom` | 3 | `matchers.go::SeriesKey / SeriesAttrs / Matches / NewSeries` | +| `func accumulateIntoWindow` | 3 | `window.go::Observe` (merged with `Precompute::Observe`) | +| `func getOrCreateMetricWindow` | 3 | private to `window.go` | +| `func accumulateGaugeMetric / accumulate{DD,KLL,HLL}SketchMetric` | 3+4 | sketch-specific `Sketch::Observe` lives at L1; the routing logic (raw vs envelope) is in `Precompute::Observe` | +| `func flushWindow` | 3 | `Precompute::Tick`; emits `[]SketchEnvelope` | +| `func buildMetric / buildMergedSketchMetric / buildQuantileMetric` | 4 | becomes `otel.Encode`; produces `pmetric.Metrics` from `[]SketchEnvelope` | +| `func enableSelfMonitoring / shutdownMonitor` | 4 | stays in shim | +| `func recordInput / recordOutput / activeSeriesCount` | 3 | `telemetry.go` | + +### Pattern B (CountSketch, CountMin) — same map + +| Today | Layer | Becomes | +|---|---|---| +| `type windowSketch` | 3 | absorbed into `series` struct in `precompute.go` (one map: `(agg_id, label_key) → SketchT`) | +| `func newProcessor / Start / Shutdown / Capabilities / ConsumeMetrics` | 4 | shim | +| `func processMetrics / consumeBatch / ingestMetric / dpValue` | 4 | `otel.Decode`; produces `[]Observation` | +| `func matchesMatchers / encodeKey / seriesKey / seriesAttrs / buildPartitionKey / encodeAttributesAsKey` | 3 | `matchers.go` | +| `func accumulateIntoWindow / updateWindowSketch / mergeWindowSketch` | 3 | `Precompute::Observe + window.go` | +| `func startWindowLoop / emitWindowAndReset / buildWindowMetricsAndReset` | 3+4 | timer goroutine moves to `Precompute` (driven by `Adapter::ScheduleTick`); `buildWindowMetricsAndReset` becomes `otel.Encode` | +| `func inboundDecode{CS,CMS} / mergeWindow{CS,CMS}` | 3 | `snapshot_cache.go::ApplyDelta` (envelope-in path on `Precompute::ObserveEnvelope`) | +| `func serialize{CountSketch,CMS} / deserialize{CMS} / clone{CS,CMS}` | 1 | already `sketchlib-go` API | +| `func newConfiguredCountSketch / nextPowerOfTwo` | 4 | stays in shim (it's `Config` validation) | +| `func recordInput / recordOutput / activeSeriesCount` | 3 | `telemetry.go` | + +### Cross-cutting + +- All five processors have an `enableSelfMonitoring` / + `shutdownMonitor` pair that emits OTel-shaped runtime metrics. + These become two pieces: + - `telemetry.go::PrecomputeStats` (host-neutral counters in + L3) — incremented from inside `Precompute`. + - The shim's `enableSelfMonitoring` reads `PrecomputeStats` + via `Adapter::EmitTelemetry` and constructs the OTel-shaped + metrics. + +## Per-processor shim shape (post-Phase-2) + +Every existing processor file becomes ~50-80 LoC of this shape: + +```go +package ddsketchprocessor + +import ( + precompute "github.com/ProjectASAP/asap-precompute-go" + otelhost "github.com/ProjectASAP/asap-precompute-go/otel" + "github.com/ProjectASAP/asap-precompute-go/controlchannel" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/pmetric" +) + +type ddsketchProcessor struct { + pc precompute.Precompute // generic over the sketch type bound by Config.SketchType + adapter *otelhost.Adapter + cc controlchannel.ControlChannel + cfg *Config + next consumer.Metrics + logger *zap.Logger + monitor *otelhost.SelfMonitor // wraps PrecomputeStats + shutdown chan struct{} +} + +func (p *ddsketchProcessor) Capabilities() consumer.Capabilities { + return consumer.Capabilities{MutatesData: false} +} + +func (p *ddsketchProcessor) Start(ctx context.Context, host component.Host) error { + if err := p.pc.Start(ctx); err != nil { return err } + go p.controlChannelLoop(ctx) + go p.tickLoop(ctx) + return p.monitor.Start(ctx, host) +} + +func (p *ddsketchProcessor) Shutdown(ctx context.Context) error { + close(p.shutdown) + return p.pc.Shutdown(ctx) +} + +func (p *ddsketchProcessor) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error { + obs, err := p.adapter.Decode(md) + if err != nil { return err } + for _, o := range obs { + if err := p.pc.Observe(&o); err != nil { /* OnOverflow */ } + } + return p.next.ConsumeMetrics(ctx, md) +} + +func (p *ddsketchProcessor) tickLoop(ctx context.Context) { + t := time.NewTicker(p.cfg.WindowSize) + defer t.Stop() + for { + select { + case <-p.shutdown: return + case now := <-t.C: + envelopes := p.pc.Tick(now.UnixMilli()) + md := p.adapter.Encode(envelopes) + if err := p.next.ConsumeMetrics(ctx, md); err != nil { /* log */ } + } + } +} + +func (p *ddsketchProcessor) controlChannelLoop(ctx context.Context) { + t := time.NewTicker(p.cfg.ControlPollInterval) + defer t.Stop() + for { + select { + case <-p.shutdown: return + case <-t.C: + if cs := p.cc.Poll(); cs != nil { + p.pc.UpdateConfig(cs) // atomic swap inside Precompute + } + } + } +} +``` + +The five processors share this scaffolding; the only per-processor +differences are: + +- The generic `Precompute` type parameter (`*ddsketch.DDSketch` + vs `*kll.KllSketch` vs ...). +- The `otel.Adapter` decode path — which `Metric.data` oneOf + variants it recognizes (DDSketch / KLLSketch / HLLSketch / + CountSketch / CountMinSketch). +- Default config values (`alpha`, `k`, `precision`, `width`, + `depth`). + +A future refactor could collapse all five files into a single +generic shim parameterized by sketch type. Phase 2 keeps them +separate for OCB build-config compatibility (`builder-config.yaml` +references each processor's package path). + +### Public test-friendly methods on the shim + +Today's tests directly call private methods that the shim model +would otherwise hide. To avoid rewriting all 5 processor test +files or adding incompatible private wrappers, the shim +**promotes these to public methods** (per ADR-0002): + +```go +// ProcessBatch decodes input, observes into Precompute, ticks +// once (batch flushes per input batch), encodes envelopes, and +// returns the synthesized output. Does NOT touch nextConsumer. +// Useful as a test-friendly hook; production callers should use +// ConsumeMetrics, which routes through the same pipeline plus +// the downstream forwarding. +func (p *ddsketchProcessor) ProcessBatch(ctx context.Context, md pmetric.Metrics) (pmetric.Metrics, error) + +// ProcessMetrics is the CountSketch / CMS naming variant of +// ProcessBatch — same semantics, different historical name. +func (p *countSketchProcessor) ProcessMetrics(ctx context.Context, md pmetric.Metrics) (pmetric.Metrics, error) + +// FlushWindow forces a tick on the precompute runtime and forwards +// the synthesized output via nextConsumer.ConsumeMetrics. No-op +// if no closed windows have data. +func (p *ddsketchProcessor) FlushWindow(ctx context.Context) error +``` + +Tests adapt by capitalizing the method name (`processBatch` → +`ProcessBatch`, etc.) — sed-style rename, no logic changes. This +keeps `Capabilities() = {MutatesData: false}` honest because +`ProcessBatch` returns a fresh `pmetric.Metrics` rather than +mutating input md in place. + +## Phase 2 work breakdown + +Sequenced for incremental verification — each step is shippable +and reversible: + +| Step | Scope | Verification | +|---|---|---| +| **2.1** Bootstrap `asap-precompute-go` module + types | `observation.go`, `envelope.go`, `config.go`, `adapter.go` (interface only). No logic, just type definitions matching ADR-0002 contracts. | `go vet ./...` clean. | +| **2.2** Implement `matchers.go` and `snapshot_cache.go` | Move pure-data-structure logic with no host coupling: `LabelMatcher`, `SeriesKey`, snapshot cache, `ComputeDelta`. | Unit tests against fixed input vectors copied from existing processor tests. | +| **2.3** Implement `window.go` and `precompute.go` | Generic Precompute implementation. Drives matchers + snapshot cache. Generic over `Sketch` interface. | Unit tests with mock Sketch (table-driven; verify tumbling, sliding, late-data, max_series, OnOverflow). | +| **2.4** Implement `otel/{decode,encode}.go` | Pmetric decoders for DDSketch + Gauge + Sum. Encoders for `Metric.data = DDSketch{...}`. | Unit tests with fixed `pmetric.Metrics` fixtures. | +| **2.5** Refactor `ddsketchprocessor` to shim | Delete the in-file accumulateIntoWindow/flushWindow/snapshot logic; replace with the shim shape above. Existing tests must still pass. | b3-delta e2e: same `19.49` value at offset −90s. | +| **2.6** Refactor `kllprocessor` | Same as 2.5 but for KLL. | Existing KLL accuracy tests pass. | +| **2.7** Refactor `hllprocessor` | Same as 2.5 but for HLL. | Existing HLL accuracy tests pass. | +| **2.8** Refactor `countsketchprocessor` | Pattern B — verify flat-window collapse to (agg_id, label_key) keying preserves behavior. | CountSketch top-K accuracy reducer (`P8`) matches pre-extraction. | +| **2.9** Refactor `countminsketchprocessor` | Same as 2.8 for CMS. | P8 accuracy reducer matches. | +| **2.10** `controlchannel/http_poll.go` + adapter wiring | First non-OpAMP control channel. Backward-compat: existing OpAMP-driven deploys keep using `OpAmpChannel`; new deploys can opt into `HttpPollChannel`. | b3-delta e2e survives a runtime config push (sketch_type unchanged, window_size changed) without state loss. | +| **2.11** Performance gate | Per-observation latency p99 within 10% of pre-refactor (R2). | Bench against the existing otel-app throughput harness. | + +Steps 2.1–2.4 can run in parallel (no inter-dependencies). +Steps 2.5–2.9 are sequential (each builds on the verified shim +shape from the previous). 2.10–2.11 gate the phase exit. + +## Risks during the migration + +- **R-Phase-2-A: Pattern-B → unified series-map collapse hides + partition semantics.** CountSketch / CMS today aggregate by + `partitionKey` (CS) or `aggregationKey` (CMS); these are + string concatenations of `(metric_name, label_subset)`. The + unified `(agg_id, label_key)` schema must preserve the same + string. Mitigation: in 2.8 / 2.9, write a key-equivalence test + before refactoring. +- **R-Phase-2-B: Tick goroutine races with ConsumeMetrics.** Today + each processor uses an internal mutex. The extracted + `Precompute` must keep the same locking discipline (per-series + rwmutex; coarse global mutex around tick swap). Mitigation: + keep mutex shape identical in 2.3; race detector run on + refactored shims. +- **R-Phase-2-C: OCB build manifest drift.** ASAPCollector's + `builder-config.yaml` references each processor's Go package + path. After Phase 2, those paths still work (the processor + packages still exist; they just call a new module). The new + `asap-precompute-go` module needs to be added to OCB's `gomod` + list. Mitigation: 2.5 includes `build_asap_otel.sh` + smoke run before merge. + +## Phase exit criterion (blocking) + +1. All five OTel processors are ≤80 LoC each (excluding factory + / config-validation boilerplate). +2. b3-delta e2e produces the observed `19.49` value at offset + −90s, identical to pre-extraction. +3. Per-observation `Observe` latency p99 within 10% of + pre-refactor. +4. P8 accuracy reducer per-row error matches pre-extraction + for all five sketch types. +5. `cargo test` / `go test` clean across all touched packages. +6. `controlchannel.HttpPollChannel` smoke test: collector + running with the new control channel survives a controller + plan push that changes window size (without losing sketch + state mid-window). + +If any of (1)–(6) fails, Phase 2 does not merge. + + +--- + + + +## Phase 2.11 — Go benchmarks: pre-shim vs post-shim `Precompute.Observe` + +This doc records the results of the Phase 2.11 path-A Go-side +performance audit. The 5 shim PRs (#226–#230) extracted the +windowing, snapshot, and delta-encoding runtime out of the +per-processor Go code into the host-neutral `asap-precompute-go` +runtime. ADR-0002 §"Performance contract" pins a 10% gate on +per-observation `Observe` latency at p99: post-shim must stay +within 10% of pre-shim. + +The `testing.B` benchmarks in `asap-precompute-go/precompute_bench_test.go` +exercise the exact code path the shim runs under load — `Precompute.Observe(*Observation)` +— with realistic inputs for each of the five sketch types (DDSketch, +KLL, HLL, CountSketch, CountMinSketch). The shim-side benchmarks in +each `processor/processor/processor_bench_test.go` capture +absolute shim overhead (ProcessMetrics / ProcessBatch latency on a +1000-data-point batch); these have no pre-shim equivalent because +the legacy code wasn't a shim, so they're informational only. + +## Methodology + +### Hardware / toolchain + +- CPU: AMD Ryzen Threadripper PRO 5955WX 16-Cores +- OS: Linux 5.15 (Ubuntu 20.04 kernel) +- Go: `go1.25.3 linux/amd64` +- Statistical comparison: `golang.org/x/perf/cmd/benchstat` + +### Commits + +- Pre-shim baseline: `6b3258d` (`test(integration/parity): all-sketch e2e parity harness (#225)`) + — last commit before the 5 shim PRs landed. +- Post-shim head: `f9824e2` (`fix(asap-precompute-go): SnapshotCache always-refresh + extract common sketch wrappers (#232)`). + +### Bench-file portability + +`precompute_bench_test.go` is structured to compile against BOTH +commits. It does NOT depend on the post-shim-only +`asap-precompute-go/sketches/` wrapper subpackage; instead each +benchmark wires a tiny `benchXxxWrapper` directly against +`sketchlib-go` and an inline `benchXxxObserver` that satisfies +`precompute.SketchObserver`. The wrappers implement only the +methods `Observe` needs (no Snapshot / Merge / etc.) so the file +applies cleanly onto pre-shim 6b3258d as well — pinning the +measured code to the runtime's `Observe` path itself, independent +of the sketches/ wrapper layer that didn't exist pre-shim. + +### Commands + +asap-precompute-go (run on each commit after applying the bench file): + +``` +cd asap-precompute-go +# pre-shim (after `git checkout 6b3258d`): +go test -bench=. -benchmem -count=5 -run=^$ . > /tmp/asap-pre.txt 2>&1 +# post-shim (after `git checkout phase2/perf-bench-go`): +go test -bench=. -benchmem -count=5 -run=^$ . > /tmp/asap-post.txt 2>&1 +benchstat /tmp/asap-pre.txt /tmp/asap-post.txt +``` + +Per-processor shim benchmarks (post-shim only): + +``` +for p in ddsketch kll hll countsketch countminsketch; do + cd opentelemetry-collector-contrib-patch/processor/${p}processor + go test -bench=. -benchmem -count=5 -run=^$ . > /tmp/${p}-shim.txt 2>&1 +done +``` + +### Choices that affect the numbers + +- **Window size = 1 hour.** The bench loop runs millions of + iterations against a single Precompute instance; a 1h window + guarantees no rotation contaminates the per-observation timing. +- **No `b.RunParallel`.** `Precompute` is mutex-guarded internally + (window + snapshot cache), so parallel benchmarks would measure + contention more than the per-call cost. Single-goroutine bench + matches what the shim does on a single ConsumeMetrics call. +- **`b.ReportAllocs()`** on every bench so allocation regressions + surface alongside ns/op. +- **Deterministic inputs.** Each bench uses + `rand.New(rand.NewSource(0x5A9C011EC709072))` so successive + runs are comparable. + +## `asap-precompute-go::Observe` results (the 10% gate) + +Five samples per benchmark, median reported. Full benchstat output +in /tmp/asap-{pre,post}.txt — quoted in §"Raw benchstat" below. + +| Sketch | Pre (ns/op) | Post (ns/op) | Δ% | Gate | +|---|---:|---:|---:|---| +| DDSketch | 158.10 | 155.80 | −1.45% | **PASS** | +| KLL | 288.40 | 290.60 | +0.76% | **PASS** | +| HLL | 146.60 | 147.70 | +0.75% | **PASS** | +| CountSketch | 241.00 | 240.50 | −0.21% | **PASS** | +| CountMinSketch | 345.20 | 351.90 | +1.94% | **PASS** | + +Allocations are bit-identical pre vs post for every sketch (DDSketch: +24B/2 allocs, KLL: 81B/4, HLL: 24B/2, CountSketch: 32B/3, CMS: +128B/5) — the shim refactor did not introduce allocation regressions +on the hot path. + +**Verdict: all 5 sketches PASS the ADR-0002 §"Performance contract" +10% gate.** The largest delta is +1.94% on CMS; the smallest is +−1.45% on DDSketch (which is faster post-shim, consistent with +benchmark noise, not a real speedup). benchstat's two-sample test +flagged none of the deltas as statistically significant (every +p-value > 0.2 with n=5 samples), which is itself noteworthy: the +shim refactor is functionally a behavior-preserving move, and the +benchmark numbers confirm that. + +## Per-processor shim results (post-shim only) + +These benchmarks measure the absolute cost of one +`ProcessMetrics` / `ProcessBatch` call on a 1000-data-point +synthetic batch. There is no pre-shim equivalent because the legacy +code was not a shim — the per-processor `processor.go` files +contained the runtime inline. Treat these as a baseline to detect +future regression in the shim layer itself. + +Median of 5 samples, post-shim only: + +| Processor | Bench | ns/op | B/op | allocs/op | +|---|---|---:|---:|---:| +| ddsketch | ProcessMetrics (window mode, observe-only) | 552,704 | 355,116 | 5,002 | +| ddsketch | ProcessBatch (batch mode, observe + tick + encode) | 867,072 | 572,606 | 10,167 | +| kll | ProcessBatch | 737,000 | 450,199 | 10,113 | +| hll | ProcessBatch | 783,714 | 798,082 | 7,202 | +| countsketch | ProcessMetrics (batch mode) | 1,209,997 | 711,312 | 10,160 | +| countminsketch | ProcessBatch | 2,158,571 | 2,108,229 | 18,312 | + +Notes: + +- The ddsketch shim is the only one that exposes a window-mode + observe-only path (`ProcessMetrics`) distinct from batch + (`ProcessBatch`); the other four shims fold tick + encode into + every public call. Comparing ddsketch's 552,704 ns/op + (observe-only) vs 867,072 ns/op (with tick + encode) gives a + rough sense of the encode-side overhead per 1000-point batch: + ~315k ns, dominated by serialization + pmetric construction + not the runtime itself. +- CMS's 2.1ms / 1000-point batch is the slowest of the five and + is bounded by `common.FromBytes(...).Hash` cost on every + observation (the legacy CMS shim does the same hash in the + same place; this is sketchlib-go's hash, not new shim + overhead). + +## Raw benchstat + +``` +$ benchstat /tmp/asap-pre.txt /tmp/asap-post.txt +goos: linux +goarch: amd64 +pkg: github.com/ProjectASAP/asap-precompute-go +cpu: AMD Ryzen Threadripper PRO 5955WX 16-Cores + │ /tmp/asap-pre.txt │ /tmp/asap-post.txt │ + │ sec/op │ sec/op vs base │ +Precompute_Observe_DDSketch-32 158.1n ± ∞ ¹ 155.8n ± ∞ ¹ ~ (p=0.651 n=5) +Precompute_Observe_KLL-32 288.4n ± ∞ ¹ 290.6n ± ∞ ¹ ~ (p=0.222 n=5) +Precompute_Observe_HLL-32 146.6n ± ∞ ¹ 147.7n ± ∞ ¹ ~ (p=0.460 n=5) +Precompute_Observe_CountSketch-32 241.0n ± ∞ ¹ 240.5n ± ∞ ¹ ~ (p=0.548 n=5) +Precompute_Observe_CMS-32 345.2n ± ∞ ¹ 351.9n ± ∞ ¹ ~ (p=1.000 n=5) +geomean 223.4n 224.2n +0.35% +¹ need >= 6 samples for confidence interval at level 0.95 +``` + +(B/op and allocs/op tables omitted — every cell is byte-identical +between pre and post, geomean Δ = 0.00%.) + +## Caveats + +- `benchstat` flagged "need >= 6 samples for confidence interval" + on every row. We ran with `-count=5` per the task brief; the + geomean drift of +0.35% is well inside what `count=20` would + surface as noise, but a deeper run is left to a follow-up + audit if the controller surface ever needs to defend a tighter + bound. +- The bench file lives in package `precompute_test` (external + test package) and uses tiny inline wrappers around sketchlib-go + rather than the post-shim `sketches/` package, so the same + source compiles on 6b3258d and HEAD. This is the only way to + apples-to-apples compare the runtime's `Observe` cost across + the commit boundary; the public `sketches/` wrappers are an + insignificant sliver of the call graph (one method dispatch + + one type assert), so the small wrapper-layer overhead they add + on HEAD is captured in the +0.76% / +1.94% post-shim drift the + table reports — comfortably under the 10% gate. +- Running benchmarks with `-race` is excluded per the task brief + (and is the right call: race instrumentation distorts ns/op + by 5-50x). + +## Conclusion + +Five sketch types, five PASS verdicts. The shim refactor (PRs +#226–#230) preserves per-observation latency to within ±2% on a +deterministic single-machine benchmark, well inside ADR-0002's +10% performance contract. No regression to investigate; nothing +to escalate. + + +--- + + + +## Phase 2.11B — Deployment Performance: pre-shim vs post-shim + +Companion to `docs/phase-2-perf-bench-go.md` (Phase 2.11A, PR #236). The +A path closed ADR-0002 §"Performance contract" at the +microbenchmark level (`Precompute.Observe` ns/op). This doc reports the +deployment-level confirmation: what the existing docker-compose +b3-delta harness measures end-to-end, and whether those numbers move +materially between commit `6b3258d` (pre-shim, last commit before the +5 shim PRs landed) and `c86a62c` (post-shim, HEAD of `origin/main`). + +The aim is not a fresh measurement framework — it's a sanity check on +the harness we already ship, so a future reader can see that the shim +extraction (PRs #226–#232) didn't blow up the deployed agent's +throughput, RSS, or per-window output bytes. + +## Setup + +### Hardware / toolchain + +- CPU: AMD Ryzen Threadripper PRO 5955WX (32 logical cores) +- RAM: 440 GiB (essentially unconstrained for this stack) +- OS: Linux 5.15 (Ubuntu 20.04 kernel) +- Go: `go1.25.3 linux/amd64` +- Docker: 28.1.1 +- Other tenants on the host: an Elasticsearch + Kibana dev stack + (idle, healthcheck-only). Not isolated, so absolute numbers carry + some noise. + +### Stack + +`deploy/mvp-singlenode/docker-compose/baseline-b3-delta.yml` over the shared `base.yml` ++ `agents-N1.yml` overlay. B3-delta is the "delta sketch transmission, +60 s window" baseline; it's the same combination Phase 2.11A's micro +results care about, since the shim sits in the agent processor pipeline +that this baseline exercises. + +Workload knobs (defaults from `base.yml`): + +- `-freq-hz=10` — 10 Hz event rate from the synthetic producer +- `-cardinality=1000` — 1000 active series +- `-sdk-window=15s` — SDK aggregation window +- `-agg=default` — Sum / LastValue per metric kind + +One agent (N1), one gateway, one backend. No load-gen client; the +otel-app is the only writer. + +## Methodology + +The harness has two relevant scripts: + +- `deploy/mvp-singlenode/scripts/measure-baseline.py` — instant Prometheus query for + per-tier CPU / RSS / point rate / output bytes, plus a + `docker stats` two-sample window for backend + producer numbers + (script supplements Prom because cAdvisor isn't in the stack). +- `deploy/mvp-singlenode/scripts/run-baseline-sweep.sh` — orchestrator that brings the + stack up, soaks for `SOAK_S` seconds, then invokes + `measure-baseline.py`. We do not use the sweep wrapper here because + the goal is one stack soak per commit, not the + baseline × rate × cardinality matrix. + +### Commit handling + +`6b3258d` predates PR #231 ("wire asap-precompute-go replace +directive") but the pre-shim binary doesn't import `asap-precompute-go` +at all (the shim-extraction PRs are precisely what introduced that +dependency), so no local fix was needed for the OCB build to succeed. +The only environmental fixup was a symlink `/tmp/sketchlib-go -> +/home/zeying/repos/sketchlib-go`, because the OCB-emitted go.mod uses +`../../../../sketchlib-go` from the build dir at +`/tmp/preshim-worktree/.../cmd/asap-otel/`. Both fixups are +build-host-local — nothing was committed. + +### Procedure + +For each commit: + +1. `git worktree add` at the commit, init submodules, run + `./build_asap_otel.sh` to produce a fresh + `asap-otel` binary. +2. Copy that binary into the main repo's + `opentelemetry-collector-contrib-patch/cmd/asap-otel/` and + `docker build -f deploy/docker/Dockerfile.asap-otel`. Tag + appropriately, swap onto `:dev`, then + `docker compose ... up -d --force-recreate agent-1 gateway` so only + the agent + gateway tier get re-imaged. Producer / backend / + controller / Prom / MinIO stay continuously up, which removes a + source of cross-run drift. +3. Soak ≥ 200 s (≥ 3 windows of the 60 s delta cycle, so + `rate(...[2m])` sees ≥ 2 samples — required by the harness). +4. Take 2 readings ≥ 60 s apart with + `measure-baseline.py --window 2m --bytes-sample-window 10`; report + the mean of the two. + +## Results + +Two readings per commit. Numbers in the table are the **mean** of the +two samples. Raw CSV in `/tmp/perf-2-11b/{preshim,postshim}.csv` on +the build host. + +### Single-sample raw values + +``` +b3-delta-preshim, agent_cpu=0.003 cores, agent_rss=288.3 MiB, agent_in=26.40 KiB/s, agent_out=4.69 KiB/s, agent_pts=133.3 /s +b3-delta-preshim-2, agent_cpu=0.002 cores, agent_rss=293.9 MiB, agent_in=25.08 KiB/s, agent_out=2.35 KiB/s, agent_pts=133.3 /s +b3-delta-postshim, agent_cpu=0.003 cores, agent_rss=302.6 MiB, agent_in=26.41 KiB/s, agent_out=4.70 KiB/s, agent_pts=133.3 /s +b3-delta-postshim-2, agent_cpu=0.002 cores, agent_rss=302.6 MiB, agent_in=25.08 KiB/s, agent_out=2.35 KiB/s, agent_pts=133.3 /s +``` + +### Comparison table + +| Metric | Pre-shim (6b3258d) | Post-shim (c86a62c) | Δ (post − pre) | Δ % | Verdict | +|-------------------------|--------------------|---------------------|---------------:|-------:|---------| +| agent_cpu_cores | 0.0025 | 0.0025 | +0.0000 | 0.0% | pass | +| agent_rss_mib | 291.1 | 302.6 | +11.5 | +4.0% | pass | +| agent_in_kib_per_s | 25.74 | 25.74 | +0.00 | 0.0% | pass | +| agent_out_kib_per_s | 3.52 | 3.52 | +0.00 | 0.0% | pass | +| agent_points_per_s | 133.3 | 133.3 | +0.0 | 0.0% | pass | + +Throughput, input bytes, output bytes, and CPU are essentially +identical — the in/out/points columns match to three significant +figures because the workload is producer-paced (10 Hz × 1000 +cardinality) and well below saturation; the agent is so far below +its capacity that the shim's extra method-call hop doesn't show up +as a CPU delta at all. + +The 4% RSS bump is the only directional change. It is consistent +with the shim's explicit `Precompute` runtime structure (snapshot +cache, per-source state map) being slightly fatter than the inlined +processor state it replaced. ADR-0002 doesn't gate on RSS, but a +4% bump on a 290 MiB agent footprint is well inside what would be +considered a non-regression — the larger agent_rss drivers +(sketchlib-go DDSketch buffers, OTel runtime) are roughly 10× +larger. + +### Producer / backend rows (informational) + +| Metric | Pre-shim sample mean | Post-shim sample mean | Notes | +|-------------------------|----------------------|-----------------------|------------------------------------------| +| producer_cpu_cores | 0.061 | 0.071 | producer container un-restarted; 4-h-old | +| producer_rss_mib | 56.9 | 75.5 | (same; runtime drift, not shim) | +| backend_cpu_pct | 0.01 | 0.94 | backend never restarted | +| backend_rss_mib | 163.6 | 134.0 | (same; GC noise, not shim) | + +The producer + backend containers were intentionally **not** restarted +between pre-shim and post-shim measurement — only the agent + gateway +were re-imaged. So these rows compare two snapshots of the *same* +running container hours apart, which is just the runtime's heap / GC +drift over time. They are recorded for completeness but do not say +anything about the shim. Counterintuitively, the post-shim +`backend_rss_mib` is *lower* than pre-shim — that's because the +post-shim row was captured first (after 4 h of soak), the pre-shim +row 9 minutes later; RSS difference between two snapshots of the +unchanged backend container is just GC-cycle noise. + +### Gateway + backend Prom rows: NaN + +`gateway_cpu_cores`, `gateway_rss_mib`, `gateway_points_per_s`, +`gateway_out_series_per_s`, `backend_samples_per_s`, +`backend_query_p99_ms` are all NaN in the CSV — see "Gaps" below. +Same NaN pattern in pre-shim and post-shim, so the comparison still +holds for the rows that do populate. + +## Verdict + +**Phase 2.11A** (PR #236) confirmed the per-observation gate at the +microbenchmark level: pre vs post-shim `Precompute.Observe` p99 within +the ADR-0002 10% tolerance for all five sketches. + +**Phase 2.11B** (this doc) confirms the deployment-level non-regression: +on the b3-delta harness, every shim-affected metric — agent CPU, in / +out KiB/s, throughput — is within run-to-run noise of pre-shim. RSS +moves +4% which is well inside any reasonable tolerance and explained +by the explicit shim runtime structure replacing inlined state. + +Together Phase 2.11A and 2.11B close ADR-0002 §"Performance contract" +with both micro and deployment-level confirmation. + +### Caveats + +- **Single host, single-machine docker noise.** Two-sample mean for + each metric, but only one stack soak per commit. Run-to-run variance + in `agent_out_kib_per_s` is intrinsic to the 60 s delta window — + `rate()` over 2 m sees 2–3 emissions, so 30–50% jitter on that + column within a single stable run is normal (4.7 → 2.4 KiB/s + between samples 60 s apart, identical between commits). +- **Shared host.** A separate Elasticsearch dev stack ran during + measurement (idle but resident); not isolated to a cgroup boundary. +- **Producer-paced workload.** At 1000 cardinality × 10 Hz the agent + CPU is ~ 0.0025 cores — three orders of magnitude below saturation. + This deployment audit confirms there's no *new* overhead, but does + not stress the shim. A higher-cardinality stress test (e.g. 1e5 + cardinality × 100 Hz) would be more discriminating; see "Gaps" + for why we don't run it here. +- **No cold-store or query traffic.** This soak measured ingest only; + `backend_samples_per_s` and `backend_query_p99_ms` are NaN because + no PromQL replay client ran. The end-to-end query path is exercised + by `run_e2e_sweep.sh` which is much more expensive (5 sketch + families × 12 cells × ≥ 2 min each ≥ 2 h wall-clock) and out of + scope for this audit. +- **No `-race`, no profiling overhead.** Plain release build via + `Dockerfile.asap-otel`. + +## Gaps in the existing harness + +The four gaps that came up while running the Phase 2.11B audit have +been triaged below. Each is annotated with the resolution from PR +#246 (`fix(perf-harness): close 4 gaps from Phase 2.11B deployment +perf run`); two more harness gaps that surfaced separately are +listed at the end as standing follow-ups. + +1. **Gateway metric-name skew — FIXED in PR #246.** + `measure-baseline.py` was written when the gateway was on otelcol + v0.108 (no `_total` suffix on process counters). The current + gateway image is v0.141 (matches the agent), so + `gateway_cpu_cores` / `gateway_rss_mib` / `gateway_points_per_s` / + `gateway_out_series_per_s` all returned NaN against today's + stack. Fix: each gateway query is now ` or `, so the script keeps producing rows whether the gateway + image is current or a legacy worktree replay. + +2. **Backend `/metrics` is empty under ingest-only — DOCUMENTED in + PR #246, deferred as a design-level concern.** The backend's + `asap_ingest_samples_total` and `asap_query_duration_seconds_bucket` + only get populated when PromQL query traffic flows; an ingest-only + soak (this audit, `run-baseline-sweep.sh`'s default) leaves both + at NaN. This is *not* a query-string bug — the metrics genuinely + don't exist under ingest-only operation, so editing + `measure-baseline.py` won't help. + + Closing the gap properly requires either: + + - **Replay path on the harness side.** Add an opt-in MetricsQL + replay client (the existing `deploy/mvp-singlenode/scripts/metricsql_replay.py` + primitives are a starting point) that the sweep wrapper drives + before the measurement window. This is its own feature with + its own design questions (which queries to replay, at what + rate, on which sketch families) and is out of scope for a + harness-fixes PR. + - **Synthetic ingest-side counter on the backend.** The backend + could expose an `asap_ingest_envelopes_total` counter that + fires regardless of whether query traffic ran. That's a + backend code change, also out of scope for a Collector-side + harness PR. + + Because the harness can't synthesize these metrics by itself, + PR #246 only updates the docstring on the `backend_samples_per_s` + / `backend_query_p99_ms` query templates to mark them as + "requires query traffic"; the operator now sees in-script why + the column is blank. The deeper "ingest-only vs ingest+query + soak" mode distinction is tracked as a follow-up item; it + belongs in a `run-baseline-sweep.sh` redesign, not a one-shot + query-template fix. + +3. **No per-observation latency emission from the deployed shim — + FIXED in PR #246 (DDSketch only) + follow-up.** ADR-0002's + binding metric is per-observation `Observe` p99, which the + deployed asap-otel previously didn't expose as a Prom + histogram (Phase 2.11A measured it in `testing.B` only). + + Resolution: + + - **Runtime.** `asap-precompute-go` now exposes a + `LatencyObserver func(d time.Duration)` hook installed via + `Precompute.SetLatencyObserver`. The hook fires once per + `Observe` call (success, ErrSeriesCapExceeded, ErrLateData, + and matcher-miss all time), giving the deployed shim the same + envelope `testing.B` measures. Nil-safe at the hot path + (atomic-pointer load + nil check). + - **DDSketch shim wiring.** `ddsketchprocessor.enableSelfMonitoring` + constructs a `Float64Histogram` named + `asap_processor_observe_seconds` with bucket boundaries + spanning 50 ns – 10 ms (covers the 80–500 ns/op post-shim + micro envelope plus tail). Each per-metric Precompute spawned + via `getOrCreate` picks up the histogram via + `proc.recordObserveLatency`. The histogram appears on the + gateway / agent `/metrics` endpoint when + `EnableSelfMonitoring=true` (the production default). + - **Other 4 shims (KLL, HLL, CountSketch, CountMin) — follow-up.** + The runtime change is fully backwards-compatible: shims that + don't call `SetLatencyObserver` lose nothing. Wiring the + histogram into the remaining four processors is a mechanical + copy of the DDSketch monitor.go diff; pulled out of this PR + to keep the diff focused per the PR-scope constraint. Tracked + as **Phase 2.11C**. + +4. **No direct sketch-payload-bytes metric — STANDING.** + `agent_out_kib_per_s` is the OTel-collector-level processor + output bytes, which conflates delta-encoded sketch payload bytes + with envelope metadata. The B3-delta savings claim requires + distinguishing the two; this is visible in + `gateway_out_series_per_s` minus a B0a (raw stream) reference, + but the delta isn't a single column. A + `asap_sketch_payload_bytes_per_window` counter on the processor + would close this gap. Not addressed in PR #246. + +5. **Legacy rate knob removed.** The old per-second rate knob was a + no-op under SDK aggregation and has been dropped entirely; the + workload is paced by `-freq-hz` and flushed by `-sdk-window`. + The sweeps no longer carry the dead dimension. + +6. **Producer-paced workload caps the discriminating power — FIXED + in PR #246.** At cardinality 1000 × 10 Hz the agent ran at + ~0.25% of one core so CPU diffs were dominated by measurement + noise. `baseline-b3-delta.yml` now overrides + `-cardinality` and `-freq-hz` to 1e5 × 100 Hz, + chosen to land the agent in the 50–70% one-core band on + reference hardware (Threadripper PRO 5955WX as described in + the "Hardware" section). The override still honours host-env + shadowing — set `OTELAPP_CARDINALITY=1000` + on the host to recover the legacy quiet profile for ad-hoc work. + + Re-running the pre-shim vs post-shim comparison under the new + profile is its own measurement and is **not** included in this + PR; the PR only updates the harness so the next operator who + runs the sweep sees CPU-cores deltas instead of measurement + noise. The numerical re-baselining belongs in a Phase 2.11C + "saturating-load comparison" doc. + +## Reproduction + +The recipe used to produce the numbers above: + +``` +# build pre-shim binary in a worktree (sibling sketchlib-go must exist) +git worktree add /tmp/preshim-worktree 6b3258d +cd /tmp/preshim-worktree +git submodule update --init --recursive opentelemetry-collector \ + opentelemetry-collector-contrib opentelemetry-proto +ln -sfn /home/zeying/repos/sketchlib-go /tmp/sketchlib-go +GOPRIVATE='github.com/ProjectASAP/*' bash build_asap_otel.sh + +# build pre-shim docker image +cp /tmp/preshim-worktree/opentelemetry-collector-contrib-patch/cmd/asap-otel/asap-otel \ + $REPO/opentelemetry-collector-contrib-patch/cmd/asap-otel/ +cd $REPO +docker build -f deploy/docker/Dockerfile.asap-otel -t asap/asap-otel:preshim . + +# swap onto :dev tag, recreate just agent + gateway, soak, measure +docker tag asap/asap-otel:dev asap/asap-otel:postshim-saved +docker tag asap/asap-otel:preshim asap/asap-otel:dev +cd $REPO/deploy/docker-compose +AGENT_CONFIG=asap-otel-agent-b3-delta.yaml docker compose \ + -f base.yml -f agents-N1.yml -f baseline-b3-delta.yml \ + up -d --no-deps --force-recreate agent-1 gateway +sleep 200 # 2 m for rate window + 80 s margin +python3 $REPO/deploy/mvp-singlenode/scripts/measure-baseline.py \ + --baseline b3-delta-preshim --scale N1 --rate 1000 --cardinality 1000 \ + --window 2m --bytes-sample-window 10 +sleep 60 +python3 $REPO/deploy/mvp-singlenode/scripts/measure-baseline.py \ + --baseline b3-delta-preshim-2 --scale N1 --rate 1000 --cardinality 1000 \ + --window 2m --bytes-sample-window 10 + +# restore post-shim and re-measure (or just keep the prior post-shim numbers) +docker tag asap/asap-otel:postshim-saved asap/asap-otel:dev +docker compose ... up -d --no-deps --force-recreate agent-1 gateway +# (etc.) +``` diff --git a/docs/sampling-cdm-gos-derivations.md b/docs/sampling-cdm-gos-derivations.md new file mode 100644 index 000000000..e3241c862 --- /dev/null +++ b/docs/sampling-cdm-gos-derivations.md @@ -0,0 +1,1695 @@ +# Sampling + Continuous Distributed Monitoring (CDM) / Geometric-OctoSketch (GOS) derivations for windowed sketch telemetry + +> Paper-facing derivation note. This document ties together +> [distributed-nitrosketch-coordinated-sampling.md](distributed-nitrosketch-coordinated-sampling.md), +> [continuous-monitoring-aggregation-taxonomy.md](continuous-monitoring-aggregation-taxonomy.md), +> [continuous-monitoring-tumbling-cost-analysis.md](continuous-monitoring-tumbling-cost-analysis.md), +> and [design-gos-unified-edge-telemetry.md](design-gos-unified-edge-telemetry.md). +> +> Scope: fixed tumbling-window epochs; open-window freshness inside one epoch; +> additive linear sketch states as the fully proved case; family-specific +> extensions for quantiles and cardinality sketches. +> +> Acronyms: **CDM** = **Continuous Distributed Monitoring**; **GOS** = +> **Geometric-OctoSketch**. + +## Paper positioning: bottleneck -> solution map + +The system bottleneck addressed by these derivations is +**resolution-coupled central ingestion**. In conventional observability +pipelines, higher temporal resolution, higher label cardinality, and lower +freshness latency all require more raw samples to traverse the central path: + +```text +collector/exporter -> remote write / queue / WAL -> backend ingest/index/storage +-> query scan +``` + +ASAPCollector changes the unit of work from raw-sample ingestion to +query-bounded sketch-state synchronization. Edge collectors still absorb the +high-resolution stream, but the backend receives bounded sketch summaries, +error-triggered deltas, and optional cold raw fallback instead of every raw +sample on the warm path. + +The problem-solution pairs are: + +| Existing-system bottleneck | Why it matters | ASAPCollector mechanism | Error/control consequence | +| --- | --- | --- | --- | +| Central ingest bottleneck | Cost scales with $\mathrm{series\_cardinality} \times \mathrm{sample\_frequency}$; raising resolution pushes more samples through write queues, indexing, storage, and query scan. | Maintain mergeable sketches at edge collectors and transmit sketch state/deltas. | Backend warm-path load scales with summary size and threshold crossings, not directly with raw sample rate. | +| Freshness vs cost bottleneck | Shorter scrape/export intervals improve open-window freshness but increase CPU, network, and backend ingest pressure; longer intervals reduce cost but make queries and alerts stale. | Use CDM/GOS residual thresholds $T_j$ for error-triggered synchronization. | Freshness becomes a bounded staleness term $\mathrm{Err}_q^{cdm}$ rather than an implicit consequence of a fixed reporting interval. | +| Query scan / post-ingest downsampling bottleneck | TSDB compression and downsampling help after raw data has already been ingested, and long-range queries still depend on stored sample layout. | Build query-ready sketches before/during ingestion. | Query cost is paid against sketch summaries, while the cold raw path remains available for unsupported or forensic queries. | +| Fixed-statistics / early-binding bottleneck | Histograms, summaries, and pre-aggregations commit early to bucket layouts, quantiles, windows, or rollups; changing the query later may be impossible or inaccurate. | Use a sketch-family adapter per query class: Sum/CMS/CountSketch for additive readouts, DDSketch/KLL for rank queries, HLL for cardinality. | The controller applies the correct sampling and staleness model for each sketch family instead of using one proof for all summaries. | +| Control-plane bottleneck | Existing cost controls such as dropping labels, filtering metrics, increasing intervals, or coarse downsampling often do not expose a query-level error budget. | Jointly tune sketch size, sampling probabilities $p_i$, and GOS thresholds $T_j$. | The system can allocate a query error budget across $\mathrm{Err}_q^{sk} + \mathrm{Err}_q^{sa} + \mathrm{Err}_q^{cdm}$. | +| Theory-to-system bottleneck | Prior sketching, approximate query processing, and CDM work each solve part of the problem, but not the end-to-end telemetry ingestion control loop. | Combine update sampling, mergeable sketches, and CDM/GOS synchronization inside the collector/backend architecture. | The paper claim is a system-level accuracy envelope, not only a faster sketch update or a lower communication protocol in isolation. | + +Compared with existing work, the paper's positioning is: + +| Existing work / system family | Solves | Leaves open | ASAPCollector angle | +| --- | --- | --- | --- | +| Prometheus / OpenTelemetry / remote-write pipelines | Standard metric collection, export, and backend ingestion. | Higher resolution and cardinality still increase central ingest work. | Move high-resolution absorption to edge sketches and synchronize bounded state. | +| Native histograms / exponential histograms / DDSketch-style distribution metrics | Compact, mergeable distribution summaries. | Mainly distribution-specific; does not give a general control plane for sampling plus staleness across sketch families. | Treat DDSketch as one adapter in a broader sketch taxonomy. | +| TSDB compression / Thanos-style downsampling | Long-range query acceleration and storage-layout optimization. | Mostly after-ingest optimization; raw samples still enter the central path first. | Summarize before or during ingestion, then query the warm sketch path. | +| BlinkDB / VerdictDB-style AQP | Approximate analytics over stored data with statistical error. | Data is already in the warehouse; not a continuous telemetry freshness and ingestion-control problem. | Provide telemetry-native edge ingestion, open-window freshness, and sketch-state synchronization. | +| NitroSketch | Sampling sketch updates to reduce sketch CPU. | Focuses on update work for sketches, not multi-sketch observability queries, backend freshness, or GOS/CDM synchronization. | Reuse the sampling idea but compose it with query-level variance budgets and delta suppression. | +| Continuous Distributed Monitoring / distributed functional monitoring | Communication-efficient tracking of distributed functions. | Mostly a theoretical monitoring model, not an observability pipeline with sketch-family adapters and cold fallback. | Use CDM as the proof model for bounded sketch residual synchronization. | +| Learned reconstruction systems such as Zoom2Net | Infer fine-grained telemetry from coarse measurements. | Error semantics depend on reconstruction/model behavior rather than preserving query-sufficient sketch state. | Maintain sketch statistics with explicit sketch, sampling, and staleness error terms. | + +The intended claim boundary is narrow: + +- ASAPCollector does not replace raw telemetry for every task; it provides a + warm approximate path plus cold raw fallback. +- ASAPCollector does not support arbitrary PromQL under one theorem; it supports + sketchable/decomposable query classes with family-specific adapters. +- The fully proved theorem target is fixed-query, fixed-time, additive-state + telemetry with independent Horvitz-Thompson update sampling and an + update-synchronous or explicitly overshoot-bounded residual protocol. +- The core claim is that the collector can reduce update work and network + traffic while keeping a query-level accuracy envelope. + +## 1. Scope and aggregation modes + +Continuous monitoring in ASAP is not one protocol. The aggregation axis decides +which proof model applies. + +| Mode | Query shape | Protocol | Coordinator? | Main guarantee | +| --- | --- | --- | --- | --- | +| A | per-series $\times$ window | local $\epsilon$-gated delta emission | no | open-window freshness for one local series/group | +| B | series $\times$ timestamp | mergeable sketch fan-in | no | sealed/instant merge; no monitoring suppression | +| C | series $\times$ window, fused spatial+temporal | Continuous Distributed Monitoring (CDM) / Geometric-OctoSketch (GOS) over edge collectors | yes | global open-window tracking/alerting across $k$ sites | + +This note proves the composition used by modes A and C for the additive-state +case: + +1. sketch approximation, +2. update sampling, +3. stale backend state due to suppressed deltas. + +For sealed tumbling windows, only the first two sources matter. The merge of +mergeable summaries does not compound the base sketch error. + +## 2. Notation + +One fixed tumbling window is the universe of discourse. + +| Symbol | Meaning | +| --- | --- | +| $k$ | number of stable edge collectors/sites | +| $i$ | site index, $i \in \{1,\ldots,k\}$ | +| $u$ | update/sample index | +| $x_u$ | raw item/value | +| $f_i$ | local frequency vector at site $i$ | +| $f=\sum_i f_i$ | global frequency vector | +| $S_i$ | exact local sketch state for site $i$ | +| $S=\sum_i S_i$ | exact global additive sketch state | +| $\widehat S_i$ | sampled local sketch state | +| $\widehat S=\sum_i \widehat S_i$ | sampled global sketch state | +| $\widetilde S$ | backend's stale copy of $\widehat S$ | +| $j$ | sketch cell/bucket index | +| $a_j$ | linear readout coefficient for query $q(S)=\langle a,S\rangle$ | +| $p_i$ | sampling probability at site $i$ | +| $p_{i,r}$ | SDK/source-side sampling probability for row/counter-array $r$ at site/source $i$ | +| $C_{i,r}$ | candidate counter-update rate or source-to-agent cost weight for site/source $i$, row $r$ | +| $F_{i,r}$ | row-level protected mass or query sensitivity for site/source $i$, row $r$ | +| $T_j$ | cell/bucket delta threshold | +| $V_j$ | cell/bucket activity rate or expected change mass | +| $B$ | staleness budget for a query/function | +| $\epsilon_{sk}$ | base sketch error budget | +| $\epsilon_{sa}$ | sampling error budget | +| $\epsilon_{cdm}$ | Continuous Distributed Monitoring (CDM) / Geometric-OctoSketch (GOS) staleness budget | +| $\delta$ | failure probability | + +For additive sketches, an update $u$ touches one or more sketch cells. Let +$j(u)$ be a touched cell in a one-cell-per-update sketch such as a DDSketch +bucket, or let $j=(r,c)$ be a row/cell pair for CMS/CountSketch. + +## 3. Generic error decomposition + +For a fixed query $q$ at a fixed time inside a window: + +```text +true answer q(f) +exact sketch q(S) +sampled sketch q(\widehat S) +backend answer q(\widetilde S) +``` + +The triangle inequality gives + +```math +|q(\widetilde S)-q(f)| +\le +|q(S)-q(f)| ++ |q(\widehat S)-q(S)| ++ |q(\widetilde S)-q(\widehat S)|. +``` + +We name the three terms: + +```math +\mathrm{Err}_{q} +\le +\mathrm{Err}^{sk}_{q} ++ \mathrm{Err}^{sa}_{q} ++ \mathrm{Err}^{cdm}_{q}. +``` + +For a sealed tumbling window, $\widetilde S=\widehat S$ after boundary emission, +so + +```math +\mathrm{Err}_{q,sealed} +\le +\mathrm{Err}^{sk}_{q} ++ \mathrm{Err}^{sa}_{q}. +``` + +For an open window, Continuous Distributed Monitoring (CDM) / Geometric-OctoSketch +(GOS) controls the third term. + +This statement is pointwise: one fixed query and one fixed time. To claim a +finite workload $Q$ and $M$ possible query times, replace $\delta$ by +$\delta/(|Q|M)$ in each concentration bound and union bound. + +## 4. Additive-state sampling model + +### 4.1 Horvitz-Thompson update sampling + +In the SDK-side Nitro-style deployment, the sampled unit is not a raw +observation/packet. A raw measurement is first mapped to the sketch updates it +would perform, for example one candidate counter update per CountSketch/CMS row. +The SDK samples those candidate counter-array updates and sends only admitted, +inverse-probability weighted counter updates to the edge collector. The edge +collector then merges weighted sketch updates; it does not need the dropped raw +measurement. + +Thus, in this section, $u$ should be read as the sampled update unit. For +edge-admission baselines, $u$ may be a raw update. For the preferred +Nitro-style design, $u=(\text{raw item},\text{row/counter-array})$ is one +candidate sketch counter update. + +For sampled update unit $u$, site/source $i(u)$ admits it with probability +$p_{i(u)}$: + +```math +Z_u \sim \mathrm{Bernoulli}(p_{i(u)}). +``` + +Here $Z_u$ is the admission indicator: + +```math +Z_u = +\begin{cases} +1, & \text{if update }u\text{ is admitted and applied to the sketch},\\ +0, & \text{if update }u\text{ is skipped}. +\end{cases} +``` + +The inverse-probability weighted contribution of update $u$ is + +```math +Y_u = Z_u/p_{i(u)}. +``` + +Equivalently, + +```math +Y_u = +\begin{cases} +1/p_{i(u)}, & \text{if update }u\text{ is admitted},\\ +0, & \text{if update }u\text{ is skipped}. +\end{cases} +``` + +Thus $Y_u$ is the effective update weight written into an additive sketch +counter/cell. Without sampling, the update would contribute weight $1$; with +sampling, it contributes weight $1/p_{i(u)}$ only on admitted updates. + +Then + +```math +\mathbb{E}[Y_u]=1, +\qquad +\mathrm{Var}(Y_u)=\frac{1-p_{i(u)}}{p_{i(u)}}. +``` + +For a linear readout $q(S)=\langle a,S\rangle$, define $g_u$ as the contribution of update +$u$ to this readout if it were not sampled. For a one-bucket counter, +$g_u=a_{j(u)}$. For signed sketches, $g_u$ also includes the row sign. + +The sampled readout error is + +```math +X_q +=q(\widehat S)-q(S) +=\sum_u (Y_u-1)g_u. +``` + +It is unbiased: + +```math +\mathbb{E}[X_q]=0. +``` + +The variance follows from the variance of one update contribution. First, + +```math +\mathbb{E}[Y_u^2] += +p_{i(u)}\cdot \frac{1}{p_{i(u)}^2} += +\frac{1}{p_{i(u)}}. +``` + +Since $\mathbb{E}[Y_u]=1$, + +```math +\mathrm{Var}(Y_u) += +\mathbb{E}[Y_u^2]-\mathbb{E}[Y_u]^2 += +\frac{1}{p_{i(u)}}-1 += +\frac{1-p_{i(u)}}{p_{i(u)}}. +``` + +Subtracting a constant does not change variance, so + +```math +\mathrm{Var}(Y_u-1)=\mathrm{Var}(Y_u). +``` + +Multiplying by the deterministic readout contribution $g_u$ scales variance by +$g_u^2$: + +```math +\mathrm{Var}((Y_u-1)g_u) += +g_u^2\mathrm{Var}(Y_u) += +g_u^2\frac{1-p_{i(u)}}{p_{i(u)}}. +``` + +If the sampling randomness is independent across updates, i.e. the admission +indicators $Z_u$ are independent, + +```math +\mathrm{Var}(X_q) += +\sum_u g_u^2 \frac{1-p_{i(u)}}{p_{i(u)}}. +``` + +This assumption is only about the sampling coin flips, not about the input data +streams. The metric values or workloads at different sites may be correlated. +The variance identity needs the random admission decisions to be independent, or +at least uncorrelated. The high-probability Bernstein bound in Section 4.2 uses +the stronger independent-sampling assumption. + +This equation is the sampling-error budget for query $q$. The random variable +$X_q=q(\widehat S)-q(S)$ is the difference between answering $q$ from the sampled +sketch and answering $q$ from the same sketch without update sampling. Each term +in the sum says how much update $u$ contributes to that sampling error: + +- $g_u^2$ is the squared sensitivity of query $q$ to update $u$; +- $(1-p_{i(u)})/p_{i(u)}$ is the noise introduced by sampling at the site that + produced update $u$. + +Thus lowering $p_i$ saves update work at site $i$, but increases the variance of +queries whose relevant updates come from that site. This is the quantity the +controller constrains when it chooses sampling probabilities. + +The independence assumption is what removes covariance terms: + +```math +\mathrm{Var}\left(\sum_u A_u\right) += +\sum_u \mathrm{Var}(A_u) ++2\sum_{u0}\quad & \sum_j \frac{V_j}{T_j} \\ +\text{s.t.}\quad & \sum_j c_jT_j \le B , +\end{aligned} +``` + +where + +```math +c_j = k|a_j| +``` + +for a linear query. For a differentiable monitored functional, use + +```math +c_j = k|g_j|, +\qquad +g_j=\partial F/\partial S[j], +``` + +plus a separate curvature budget if the functional is nonlinear. + +The Lagrangian is + +```math +\mathcal{L} += +\sum_j \frac{V_j}{T_j} ++\lambda\left(\sum_j c_jT_j-B\right). +``` + +The first-order condition is + +```math +\frac{\partial \mathcal{L}}{\partial T_j} += +-\frac{V_j}{T_j^2}+\lambda c_j=0. +``` + +Thus + +```math +T_j=\sqrt{\frac{V_j}{\lambda c_j}}. +``` + +Solving for $\lambda$ from the active budget constraint: + +```math +\sum_j c_j\sqrt{\frac{V_j}{\lambda c_j}} +=B, +``` + +so + +```math +\frac{1}{\sqrt{\lambda}} += +\frac{B}{\sum_\ell \sqrt{c_\ell V_\ell}}. +``` + +The closed form is + +```math +\boxed{ +T_j += +\frac{B\sqrt{V_j/c_j}} +{\sum_\ell \sqrt{c_\ell V_\ell}} +}. +``` + +With floors and caps: + +```math +T_j += +\mathrm{clamp}\left( +\frac{B\sqrt{V_j/c_j}} +{\sum_\ell \sqrt{c_\ell V_\ell}}, +\; +T_j^{floor}, +\; +\min(T_j^{query}, V_j\Delta^*) +\right). +``` + +Clamped cells consume or release budget; recompute the water-filling expression +over the remaining free cells until no cell changes clamp status. + +## 8. Sketch-family instantiations + +### 8.1 Sum / Count + +State: + +```math +S_i=\sum_{u\in i} x_u +``` + +or $S_i=N_i$ for count. + +Base sketch error: + +```math +\mathrm{Err}^{sk}=0 +``` + +up to floating-point arithmetic. + +Sampling error for unit counts under uniform $p$: + +```math +|\widehat N-N| +\le +O\left( +\sqrt{\frac{N\log(1/\delta)}{p}} ++\frac{\log(1/\delta)}{p} +\right). +``` + +CDM staleness with scalar threshold $T$: + +```math +\mathrm{Err}^{cdm}\le kT. +``` + +For a threshold alert $N>\tau$, deterministic no-missed-crossing holds only in +the unsampled monotone setting. With sampling, use a high-probability margin. + +### 8.2 Count-Min Sketch point query + +State: $d \times w$ nonnegative counter matrix. + +Point query for key $x$: + +```math +\widehat f_{CMS}(x) += +\min_r S[r,h_r(x)]. +``` + +Base sketch error: + +```math +f(x) +\le +\widehat f_{CMS}(x) +\le +f(x)+\epsilon_{sk}N +``` + +with probability at least $1-\delta_{sk}$, for the standard choice of width/depth. + +Sampling error applies to every row counter. For row $r$, define + +```math +X_r(x) += +\widehat S[r,h_r(x)]-S[r,h_r(x)]. +``` + +Then $X_r(x)$ is unbiased with variance controlled by the updates landing in +that cell: + +```math +\mathrm{Var}(X_r(x)) += +\sum_{u:h_r(x_u)=h_r(x)} +\frac{1-p_{i(u)}}{p_{i(u)}}. +``` + +A conservative high-probability point statement can be made by union-bounding +the row-level sampling events with the usual CMS collision event: + +```math +\widetilde f_{CMS}(x) +\le +f(x) ++\epsilon_{sk}N ++m_{sa}(x,\delta) ++k\max_r T_{r,h_r(x)}. +``` + +Because CMS uses a $\min$, the usual no-underestimate property is not preserved +by two-sided sampling noise. This makes CMS a caveated adapter rather than the +clean first theorem target. For alerting, fire with a confidence margin: + +```math +\widetilde f_{CMS}(x)+m_{sa}(x,\delta)+m_{cdm}(x) +\ge +(1-\epsilon)\tau. +``` + +CMS is therefore useful for upper-bound-oriented point queries, but sampled CMS +alerts should be presented as probabilistic and margin-based. CountSketch, Sum, +and DDSketch range counts are cleaner first theorem targets for sampled GOS. + +### 8.3 CountSketch point query + +State: $d \times w$ signed counter matrix. + +Row estimate: + +```math +E_r(x)=s_r(x)S[r,h_r(x)]. +``` + +Point estimate: + +```math +\widehat f_{CS}(x)=\mathrm{median}_{r=1}^d E_r(x). +``` + +Base sketch error: + +```math +|\widehat f_{CS}(x)-f(x)| +\le +\epsilon_{sk}\|f\|_2 +``` + +with probability at least $1-\delta_{sk}$. + +Sampling error is two-sided and unbiased at each row. Let + +```math +X_r(x) += +s_r(x)(\widehat S[r,h_r(x)]-S[r,h_r(x)]). +``` + +Then + +```math +\mathbb{E}[X_r(x)]=0, +``` + +and its variance is the sum of inverse-probability variances of the sampled +updates colliding into the queried cell. A median-of-rows bound follows by +combining row-level concentration with the usual CountSketch row amplification. +This row amplification statement assumes Nitro-style row/counter-array sampling +or otherwise independent row-level sampling noise. If the implementation uses +edge admission sampling, the true-key sampling noise is shared across rows; then +the sampling term must be bounded before the median step and should not be +credited with CountSketch row amplification. + +CDM staleness for row $r$ is + +```math +|s_r(x)(\widetilde S-\widehat S)[r,h_r(x)]| +\le +kT_{r,h_r(x)}. +``` + +Since the median is 1-Lipschitz in the $L_\infty$ perturbation across rows: + +```math +|\mathrm{median}_r(a_r+e_r)-\mathrm{median}_r(a_r)| +\le +\max_r |e_r|, +``` + +the point-query staleness satisfies + +```math +\mathrm{Err}^{cdm}_{CS}(x) +\le +k\max_r T_{r,h_r(x)}. +``` + +Thus a paper-safe fixed-time statement is + +```math +|\widetilde f_{CS}(x)-f(x)| +\le +\epsilon_{sk}\|f\|_2 ++m_{sa}(x,\delta) ++k\max_r T_{r,h_r(x)}. +``` + +CountSketch is the cleanest first target for sampled GOS: additive state, +two-sided estimator, and no CMS one-sided caveat. + +### 8.4 DDSketch value-range counts + +DDSketch maps a positive value $x$ to a logarithmic bucket $b(x)$. The bucket +mapping is chosen so values in one bucket have relative value error at most +$\alpha$. + +For a value range $[L,U]$, define a linear bucket readout + +```math +q_{[L,U]}(S) += +\sum_{b: v_b\in[L,U]} S[b], +``` + +where $v_b$ is the representative value of bucket $b$. + +This is an additive nonnegative aggregate. Sampling and CDM compose exactly as +in the generic additive-state theorem: + +```math +|q_{[L,U]}(\widetilde S)-q_{[L,U]}(S)| +\le +m_{sa}([L,U],\delta) ++k\sum_{b:v_b\in[L,U]}T_b. +``` + +This is the DDSketch query class that behaves like a scalar/bucket-count monitor. + +### 8.5 DDSketch quantiles + +DDSketch quantile queries are not linear readouts. They depend on prefix counts. +Sampling and CDM should therefore be expressed as rank error. + +Let the true bucket count be + +```math +n_b=\sum_{u=1}^N 1\{b(x_u)=b\}. +``` + +Let + +```math +N_{\le b}=\sum_{j\le b}n_j. +``` + +#### Implemented thinning view + +The current DDSketch implementation performs value-independent uniform admission +before the bucket update: if the item is not admitted, the entire update is +skipped. This subsection assumes a single uniform probability $p$ for all items. +Let + +```math +Z_u\sim\mathrm{Bernoulli}(p). +``` + +The sampled bucket count is + +```math +n_b^{sample} += +\sum_{u:b(x_u)=b}Z_u. +``` + +Quantile lookup over the sampled DDSketch uses the sampled total + +```math +M=\sum_u Z_u. +``` + +Uniform thinning preserves the distribution in expectation. With high +probability, the empirical CDF of the sampled stream is close to the true CDF. +For a finite set of $B$ nonempty buckets, a union bound over bucket prefixes +gives + +```math +\sup_b +\left| +\frac{N^{sample}_{\le b}}{M} +- +\frac{N_{\le b}}{N} +\right| +\le +\epsilon_{sa} +``` + +with + +```math +\epsilon_{sa} += +O\left( +\sqrt{\frac{\log(B/\delta)}{pN}} ++\frac{\log(B/\delta)}{pN} +\right), +``` + +assuming $pN$ is not too small. + +#### Horvitz-Thompson analysis view + +Equivalently, for analysis one may assign every admitted item weight $1/p$: + +```math +Y_u=Z_u/p. +``` + +Then + +```math +\widehat n_b=\sum_{u:b(x_u)=b}Y_u +``` + +is an unbiased estimator of $n_b$, and + +```math +\mathrm{Var}(\widehat N_{\le b}) += +N_{\le b}\frac{1-p}{p}. +``` + +Bernstein plus a union bound over $B$ prefixes yields + +```math +\sup_b +|\widehat N_{\le b}-N_{\le b}| +\le +O\left( +\sqrt{\frac{N\log(B/\delta)}{p}} ++\frac{\log(B/\delta)}{p} +\right). +``` + +Dividing by $N$ gives the same rank scale: + +```math +\epsilon_{sa} += +O\left( +\sqrt{\frac{\log(B/\delta)}{pN}} ++\frac{\log(B/\delta)}{pN} +\right). +``` + +This weighted view is useful for proof. The implemented unweighted sampled +quantile returns the same bucket as the uniformly weighted view, because every +admitted item has the same weight. Counts, however, need `sample_p` rescaling if +they are queried as counts. + +If the controller uses nonuniform per-site probabilities $p_i$, the unweighted +sampled DDSketch quantile is biased toward high-$p_i$ sites. A nonuniform +deployment needs weighted quantile semantics, a resampling correction, or a +separate proof for the chosen estimator. + +#### CDM rank staleness + +If bucket residuals satisfy $|\rho_i[b]|\le T_b$, then for any prefix: + +```math +|\widetilde N_{\le b}-\widehat N_{\le b}| +\le +k\sum_{j\le b}T_j. +``` + +Hence the CDM-induced rank error is + +```math +\epsilon_{cdm} += +\frac{k}{N} +\sup_b\sum_{j\le b}T_j. +``` + +If the backend's total count is also stale, allocate a small additional budget +for denominator error, or normalize by a lower bound on $N$. + +#### DDSketch quantile corollary + +Let $\widetilde x_q$ be the sampled and stale DDSketch estimate of the +$q$-quantile. With probability at least $1-\delta$, + +```math +\boxed{ +(1-\alpha)x_{q-\epsilon_{sa}-\epsilon_{cdm}} +\le +\widetilde x_q +\le +(1+\alpha)x_{q+\epsilon_{sa}+\epsilon_{cdm}} +}. +``` + +Thus DDSketch keeps its multiplicative value error $\alpha$, while sampling and +CDM/GOS add rank error. + +### 8.6 KLL quantiles + +KLL is mergeable, but it is not a subtractive additive counter array. It should +not be forced into the per-cell linear residual theorem. + +For a sealed window, mergeability preserves the KLL rank guarantee: + +```math +|\mathrm{rank}(\widehat x_q)-qN| +\le +\epsilon_{KLL}N +``` + +with the configured KLL failure probability. + +If updates are uniformly sampled before KLL insertion, the same thinning rank +term appears: + +```math +\epsilon_{sa} += +O\left( +\sqrt{\frac{\log(1/\delta)}{pN}} ++\frac{\log(1/\delta)}{pN} +\right). +``` + +For open-window freshness, use a segment model: unshipped KLL segments contain +$R$ samples. Then the stale-rank contribution is bounded by + +```math +\epsilon_{cdm} +\le +R/N. +``` + +The combined quantile rank error is + +```math +\epsilon_{rank} +\le +\epsilon_{KLL} ++\epsilon_{sa} ++R/N. +``` + +KLL is therefore a mergeable-summary adapter, not a GOS per-cell water-filling +instance. + +### 8.7 HyperLogLog distinct count + +HLL state is a vector of max registers, with merge defined by register-wise max. +It is mergeable but non-additive. + +Base error: + +```math +\epsilon_{HLL}\approx 1.04/\sqrt{m} +``` + +for $m$ registers. + +Nitro-style inverse-probability update sampling is not appropriate for HLL +register updates: the update is a max operation, not an additive counter +increment. Hash-threshold distinct sampling can be analyzed separately, but it +does not fit the additive sampling theorem above. + +For CDM/freshness, use a family-specific register-change adapter. The linear +per-cell bound $k\sum_j |a_j|T_j$ does not apply directly. + +## 9. Nonlinear monitored functionals + +For differentiable functionals $F(S)$, write the stale perturbation as + +```math +e=\widetilde S-\widehat S. +``` + +At reference state $S_0$, + +```math +F(S_0+e)-F(S_0) += +\langle \nabla F(S_0),e\rangle ++R_2(e). +``` + +If the Hessian spectral norm is bounded by $\lambda$, then + +```math +|R_2(e)|\le \frac{1}{2}\lambda\|e\|_2^2. +``` + +The first-order term can be controlled by GOS water-filling with + +```math +c_j=k|\nabla_jF(S_0)|. +``` + +The curvature term needs a separate budget. Do not spend the whole error budget +on the first-order term unless $\lambda=0$. + +### F2 example + +For + +```math +F(S)=\|S\|_2^2, +``` + +we have + +```math +F(S+e)-F(S) += +2\langle S,e\rangle+\|e\|_2^2. +``` + +Therefore + +```math +|F(S+e)-F(S)| +\le +2\|S\|_2\|e\|_2+\|e\|_2^2. +``` + +To guarantee relative error $\epsilon$: + +```math +2\|S\|_2\|e\|_2+\|e\|_2^2 +\le +\epsilon\|S\|_2^2. +``` + +Let $\alpha_e=\|e\|_2/\|S\|_2$. Then + +```math +2\alpha_e+\alpha_e^2\le \epsilon, +``` + +so it suffices to enforce + +```math +\alpha_e +\le +\sqrt{1+\epsilon}-1. +``` + +If thresholds are isotropic, $T_j=T$ for $n$ sketch cells and each site residual +is bounded by $T$, then + +```math +\|e\|_2 +\le +k\sqrt{n}T. +``` + +Thus a curvature-safe isotropic threshold is + +```math +T +\le +\frac{(\sqrt{1+\epsilon}-1)\|S\|_2} +{k\sqrt{n}}. +``` + +For small $\epsilon$, this is approximately + +```math +T\lesssim \frac{\epsilon\|S\|_2}{2k\sqrt{n}}, +``` + +but the exact expression should be used in formal claims. + +## 10. Alert mode + +Threshold/alert monitoring is different from value-estimation. In the +unsampled monotone case, slack-countdown can provide deterministic no-missed +crossing. With two-sided sampling noise, alert safety is probabilistic. + +For a global monitored value $G$, sampled estimate $\widehat G$, and sampling +margin $m_{sa}(\delta)$, a safe fire rule is + +```math +\widehat G + m_{sa}(\delta) + m_{cdm} +\ge +(1-\epsilon)\tau. +``` + +Here $m_{cdm}$ is the worst-case stale backend margin. For a linear additive +readout: + +```math +m_{cdm}=k\sum_j |a_j|T_j. +``` + +**Implementation (scalar coordinator).** `Monitor::rebroadcast` +(`data_plane/src/monitor/coordinator.rs`) fires when +$\widehat G + m_{sa} \ge (1-\epsilon)\tau$, i.e. it folds $m_{sa}$ into the band. +The $m_{cdm}$ term is not added separately because the slack countdown already +realizes it: $\widehat G=\sum_i \mathrm{known\_value}_i$ is a lower bound each +edge maintains within its granted slack, so firing on $\widehat G$ is already +$m_{cdm}$-safe. The sampling term is the aggregate 1-σ standard deviation +$m_{sa}=\sqrt{\sum_i \mathrm{kv}_i(1-p_i)/p_i}$, with $p_i$ the coordinator's own +allocated floor $1/(1+\epsilon^2\,\mathrm{rate}_i)$ (self-consistent with the +`sample_p` it grants). It is **0 whenever sampling is not granted** (a single +edge, or `rate=0`), so the unsampled path is unchanged. Caveats: this is the 1-σ +heuristic (not the high-probability Bernstein margin — a theorem-grade alert +multiplies by $z_\delta$); and the $\mathrm{kv}_i(1-p_i)/p_i$ variance assumes a +unit-weight count readout (exact for CMS/CountSketch point counts, conservative +for a non-unit-weight Sum — where the margin only fires earlier, never later). + +For quantiles, convert the alert predicate to a rank/count predicate first. For +example, a DDSketch predicate $q_\phi > v^*$ is equivalent to a bucket-prefix +count predicate up to the DDSketch value bucket error. The sampling and CDM +terms then enter as rank/count margins. + +## 11. Summary table + +| Family/query | Sampling term | Continuous Distributed Monitoring (CDM) / Geometric-OctoSketch (GOS) term | Safe paper claim | +| --- | --- | --- | --- | +| Sum/count | scalar Bernoulli count variance | $kT$ | exact base sketch plus sampling plus scalar staleness | +| CMS point | row-counter sampling variance | $k\max_r T_{r,h_r(x)}$ | caveated adapter: row union plus collision event; probabilistic alert margin | +| CountSketch point | row-counter sampling variance | $k\max_r T_{r,h_r(x)}$ | clean additive/two-sided composition; best first theorem target | +| DDSketch range count | bucket-count sampling variance | $k\sum_{b\in \mathrm{range}}T_b$ | additive bucket-count composition | +| DDSketch quantile | rank error $\epsilon_{sa}$ | prefix rank error $\epsilon_{cdm}$ | $(1\pm \alpha)$ value error around $q\pm \epsilon$ rank | +| KLL quantile | thinning rank error | unshipped segment rank $R/N$ | mergeable adapter, not per-cell GOS | +| HLL distinct | exclude Nitro-style additive sampling | register-change adapter | mergeable but non-additive; family-specific proof | + +## 12. Paper theorem templates + +### Theorem 1: additive-state composition + +For a fixed linear query $q(S)=\langle a,S\rangle$ at a fixed time in a tumbling window, +assume: + +1. sampling randomness is independent across updates, i.e. the admission + indicators $Z_u$ are independent; +2. sampled updates use inverse-probability weights for additive counters; +3. each site/cell residual is bounded by $R_{ij}$; in the update-synchronous + ideal protocol $R_{ij}=T_j$, while periodic checks can use + $R_{ij}=T_j+U_{ij}\Delta_{check}$; +4. the base sketch error is at most $E_{sk}(q)$ with probability + $1-\delta_{sk}$. + +Then with probability at least $1-\delta_{sk}-\delta_{sa}$, + +```math +|q(\widetilde S)-q(f)| +\le +E_{sk}(q) ++m_{sa}(q,\delta_{sa}) ++\sum_j |a_j|\sum_i R_{ij}. +``` + +If only a variance statement is needed, pairwise uncorrelated admission +indicators are sufficient for the variance identity, but not for the Bernstein +margin above. If all sites use the update-synchronous bound $R_{ij}=T_j$, the +staleness term reduces to $k\sum_j |a_j|T_j$. + +### Theorem 2: Geometric-OctoSketch (GOS) threshold allocation + +Under the threshold-crossing cost model, given additive-state staleness budget +$B$ and cell activity $V_j$, the threshold vector minimizing $\sum_j V_j/T_j$ +subject to $\sum_j c_jT_j\le B$ is + +```math +T_j += +\frac{B\sqrt{V_j/c_j}} +{\sum_\ell\sqrt{V_\ell c_\ell}}, +``` + +before floors and caps. The clamped solution is obtained by iterative +water-filling over unclamped cells. + +### Corollary: DDSketch sampled open-window quantile + +For DDSketch relative value parameter $\alpha$, uniform value-independent +admission probability $p$, $B$ nonempty buckets, and bucket residual thresholds +$T_b$, with probability at least $1-\delta$: + +```math +(1-\alpha)x_{q-\epsilon_{sa}-\epsilon_{cdm}} +\le +\widetilde x_q +\le +(1+\alpha)x_{q+\epsilon_{sa}+\epsilon_{cdm}}, +``` + +where + +```math +\epsilon_{sa} += +O\left( +\sqrt{\frac{\log(B/\delta)}{pN}} ++\frac{\log(B/\delta)}{pN} +\right), +``` + +and + +```math +\epsilon_{cdm} += +\frac{k}{N}\sup_b\sum_{j\le b}T_j. +``` + +## 13. Implementation notes + +- The Go DDSketch wrapper performs value-independent admission before the + bucket update. This is equivalent to uniform thinning for quantile rank + analysis. Counts need `sample_p` rescaling if they are queried as counts. +- CountSketch sparse deltas currently require integral cell deltas. If sampling + creates fractional weighted cells, the implementation can fall back to full + frames. That preserves accuracy but weakens communication claims for the + sampled+delta combination unless a fractional delta wire is added. +- SDK-side Nitro-style sampling requires a wire representation for sampled + weighted counter updates from SDK/data source to agent collector. For signed + sketches, the payload must preserve row, column, sign, and weight + $1/p_{i,r}$. +- The strongest continuous $|\rho_i[j]|\le T_j$ proof assumes an update-synchronous + threshold check. Current sub-window/tick-based emit paths should be stated + with an overshoot term in formal claims. +- For SDK-source or multi-unit sampling, the sampling randomness must make the + admission indicators independent per `(edge_id, agg_id, window)` or hash-based + per item. Shared sampler seeds can correlate admissions and invalidate + variance-addition by introducing covariance terms. + +## 14. Evaluation checklist + +The system claim is only credible if the evaluation shows that ASAPCollector +moves work off the central raw-sample path while preserving the advertised error +envelope for supported queries. At minimum, measure: + +- **SDK/source update work:** candidate sketch counter updates/sec, admitted + counter updates/sec, CPU, and memory as $p_{i,r}$ changes. +- **Source-to-agent network load:** bytes/sec and messages/sec for raw OTLP + export versus sampled weighted counter-update export. +- **Agent-to-backend network load:** bytes/sec and messages/sec for periodic + sketch export and GOS thresholded deltas. +- **Backend ingest load:** accepted samples/sec or summary frames/sec, WAL/queue + pressure, index/storage growth, and write amplification if applicable. +- **Freshness:** open-window staleness measured as both wall-clock lag and + query-space error $\mathrm{Err}_q^{cdm}$. +- **Accuracy envelope:** empirical query error decomposed into sketch error, + sampling error, and staleness error; report coverage of the claimed + high-probability bound. +- **Controller behavior:** selected $p_{i,r}$ and $T_j$ under hot/cold sources, + query-sensitive/query-insensitive rows or cells, and changing workloads. +- **Fallback boundary:** unsupported query classes and cold raw fallback cost, + so the paper does not imply arbitrary PromQL support. +- **Failure modes:** tick-based overshoot, delayed ACKs, retries, collector + restart, and correlated sampler seeds. + +Use CountSketch, Sum/count, and DDSketch range counts as the first theorem-backed +accuracy experiments. Treat CMS, DDSketch quantiles, KLL, and HLL as +family-specific adapter experiments with their own stated caveats. + +## Appendix A. Bernstein inequality step + +This appendix derives the concentration bound used in Section 4.2: + +```math +|X_q| +\le +\sqrt{2\sigma_q^2\log(2/\delta)} ++\frac{2G}{3p_{\min}}\log(2/\delta) +``` + +with probability at least $1-\delta$. + +Recall the sampled query error: + +```math +X_q += +\sum_u (Y_u-1)g_u. +``` + +Define the centered per-update random variable + +```math +A_u=(Y_u-1)g_u. +``` + +Then + +```math +X_q=\sum_u A_u. +``` + +### A.1 Zero mean + +Because $E[Y_u]=1$, + +```math +\mathbb{E}[A_u] += +\mathbb{E}[(Y_u-1)g_u] += +g_u(\mathbb{E}[Y_u]-1) +=0. +``` + +Thus $X_q$ is a sum of independent centered random variables, assuming the +sampling admission indicators are independent. + +### A.2 Variance parameter + +From Section 4.1: + +```math +\mathrm{Var}(A_u) += +g_u^2\frac{1-p_{i(u)}}{p_{i(u)}}. +``` + +Therefore the total variance parameter is + +```math +\sigma_q^2 += +\sum_u \mathrm{Var}(A_u) += +\sum_u g_u^2\frac{1-p_{i(u)}}{p_{i(u)}}. +``` + +### A.3 Uniform bound on one summand + +Bernstein's inequality also needs an almost-sure bound on $|A_u|$. + +Since + +```math +Y_u = +\begin{cases} +1/p_{i(u)}, & \text{if update }u\text{ is admitted},\\ +0, & \text{if update }u\text{ is skipped}, +\end{cases} +``` + +we have + +```math +Y_u-1 = +\begin{cases} +(1-p_{i(u)})/p_{i(u)}, & \text{if update }u\text{ is admitted},\\ +-1, & \text{if update }u\text{ is skipped}. +\end{cases} +``` + +Thus + +```math +|Y_u-1| +\le +\frac{1}{p_{i(u)}} +\le +\frac{1}{p_{\min}}. +``` + +If $|g_u| \le G$, then + +```math +|A_u| += +|(Y_u-1)g_u| +\le +\frac{G}{p_{\min}}. +``` + +Let + +```math +M=\frac{G}{p_{\min}}. +``` + +### A.4 Apply Bernstein + +One standard two-sided Bernstein bound for independent centered random variables +$A_u$ with $|A_u| \le M$ and variance sum $\sigma_q^2$ is + +```math +\Pr\left[ +\left|\sum_u A_u\right| +\ge +\sqrt{2\sigma_q^2 t} ++\frac{2M}{3}t +\right] +\le +2e^{-t}. +``` + +Set + +```math +t=\log(2/\delta). +``` + +Then $2e^{-t}=\delta$, so with probability at least $1-\delta$, + +```math +\left|\sum_u A_u\right| +\le +\sqrt{2\sigma_q^2\log(2/\delta)} ++\frac{2M}{3}\log(2/\delta). +``` + +Substituting $M=G/p_{\min}$ and $X_q=sum_u A_u$ gives + +```math +|X_q| +\le +\sqrt{2\sigma_q^2\log(2/\delta)} ++\frac{2G}{3p_{\min}}\log(2/\delta). +``` + +This is the Section 4.2 bound. + +## 15. References to cite + +- Prometheus remote write and storage documentation: central ingest, queue/WAL, + CPU, memory, and network costs for exporting high-resolution metrics. +- OpenTelemetry metrics data model: metric events, aggregation temporality, + histograms/exponential histograms, and the motivation for aggregation before + export. +- Prometheus histograms and summaries documentation: early binding of buckets, + quantiles, and aggregation constraints. +- Thanos compactor/downsampling documentation and TSDB compression papers: + post-ingest query/storage optimization rather than pre-ingest load reduction. +- BlinkDB and VerdictDB: approximate query processing over already-ingested + data. +- NitroSketch: update sampling for sketch update work. +- DDSketch: mergeable relative-error quantile sketch. +- Count-Min Sketch and CountSketch: base frequency guarantees. +- KLL: mergeable rank-error quantile sketch. +- HyperLogLog: base cardinality guarantee. +- Cormode et al. distributed functional monitoring / Continuous Distributed + Monitoring (CDM). +- Mergeable summaries: no error compounding under merge. +- Zoom2Net and related telemetry reconstruction work: learned fine-grained + reconstruction from coarse measurements, distinct from preserving + query-sufficient sketch state. diff --git a/docs/system-overview.md b/docs/system-overview.md index c403183d8..1c340940a 100644 --- a/docs/system-overview.md +++ b/docs/system-overview.md @@ -587,9 +587,9 @@ Tracked, in flight, or explicitly out of scope today. (merged from the older `design-jsonl-deprecation-…` and `design-gorilla-s3-cold-engine` docs in PR #325). - **`docs/design-asap-edge-framework.md`** — asap-otel agent design. -- **`docs/design-asap-otap-rust-integration.md`** — asap-otap agent +- **`docs/dormant/design-asap-otap-rust-integration.md` (dormant)** — asap-otap agent design. -- **`docs/design-asap-telegraf-integration.md`** — asap-telegraf +- **`docs/dormant/design-asap-telegraf-integration.md` (dormant)** — asap-telegraf agent design. - **`docs/control-plane-design.md`** — controller architecture; OpAMP and HTTP-push plumbing. diff --git a/docs/use-case-dataset-survey.md b/docs/use-case-dataset-survey.md index a1ca642dd..7a928546b 100644 --- a/docs/use-case-dataset-survey.md +++ b/docs/use-case-dataset-survey.md @@ -42,6 +42,12 @@ We already evaluate on two **anchor** datasets: - **DEBS-2022 (Deutsche Börse / Infront tick data)** — financial / skewed activity → coordinated sampling, topk, the ε-gate / delta regime (`datasets_eval/debs/`). +Beyond the two anchor *domains* (cloud observability, finance) this survey adds a +**third domain — product analytics / clickstream** (§3b): the domain where +approximate aggregates (DAU/MAU via HLL, top events via CountSketch/CMS) are already +the industry default, and whose **GDPR/CCPA right-to-erasure** is the most universal — +and *publicly-downloadable* — cold-raw motivation in the survey. + This survey verifies those two and finds **more**, mapping each to the warm/cold split and to the user's seven query/data axes. @@ -140,8 +146,8 @@ query-set parse. | series pattern | mode | examples | |---|---|---| -| aggregate query only, raw provably unused (metric-identity separated) | **Mode 1** | Azure VM **CPU**, Google instance CPU, Azure Functions **duration**, Alibaba per-service **latency metric** | -| raw **mandatory anyway** (backtest / audit / forensic) **and** common queries approximate | **Mode 2** | **finance tick** (live VWAP/q + backtest replay), DEBS / TAQ (dashboard + MiFID audit), Wikimedia (topk + forensic) | +| aggregate query only, raw provably unused (metric-identity separated) | **Mode 1** | Azure VM **CPU**, Google instance CPU, Azure Functions **duration**, Alibaba per-service **latency metric**, **product-analytics DAU/topk rollups** (vs the raw event log) | +| raw **mandatory anyway** (backtest / audit / forensic) **and** common queries approximate | **Mode 2** | **finance tick** (live VWAP/q + backtest replay), DEBS / TAQ (dashboard + MiFID audit), Wikimedia (topk + forensic), **product analytics** (dashboard + **GDPR/CCPA** export-erasure of the same user series) | | raw needed, queries rarely aggregate | cold-primary | LOBSTER event-exact microstructure | **Mode 2 rescues the "condition-2 failures".** The datasets that *can't* go cleanly warm @@ -177,6 +183,9 @@ play) · *cold-lean* = M2 dominated by the exact-replay side. | F3 | NYSE Daily TAQ | finance/trades+quotes | ✓✓ | ✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | **cold** (MiFID/SEC audit) + warm (VWAP) | **M2** | | F4 | Deutsche Börse PDS (Xetra/Eurex) | finance/OHLCV 1-min | ~ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ (pre-agg) | warm (already aggregated) | **M1** *(warm-only check)* | | F5 | Binance/Kraken/Coinbase tick | finance/crypto tick | ✓ | ✓ | ✓ | ✓ | ✓ | ~ | ✓✓ | **warm** (q/VWAP) + cold (backtest replay) | **M2** | +| **P1** | **Taobao UserBehavior 2017** | product-analytics/clickstream | ✓ | ✓ | ✓ | ✓ | ~ | ✓✓ | ✗ (sparse/user) | **warm** (DAU-HLL / topk) + cold (GDPR / ML-feature raw) | **M1+M2** | +| P2 | REES46 eCommerce 2019 | product-analytics/clickstream | ✓ | ✓ | ✓ | ✓ | ~ | ✓✓ | ✗ | **warm** (funnel / topk / HLL) + cold (export / audit) | **M2** | +| P3 | Wikipedia clickstream | product-analytics/web | ~ | ✓ | ✓ | ✓ | ✓ | ✓✓ | ✗ (pre-agg) | warm (topk referrers) | **M1** *(warm-only check)* | **Headline read of the matrix:** the user's axes split cleanly along the tier line — **(2) aggregation, (3) repeated, (4) overlapping, (6) high-cardinality, (7) @@ -453,6 +462,90 @@ the survey, not despite needing cold raw but *because* its cold raw is so cheap --- +## 3b. Product analytics — per-dataset cards + +The **third domain** (after cloud observability and finance). Product analytics +(Amplitude / Mixpanel / PostHog / Heap-style event tracking) is the domain where +**approximate aggregates are already the cultural default** — DAU/MAU, funnels and +top-events are answered with HLL / sketches at scale across the whole industry +(Apache Druid, ClickHouse, BigQuery `APPROX_COUNT_DISTINCT`, Mixpanel), so the +warm-tier "lossy within ε" pitch is *uncontroversial* here. It naturally headlines +the two families the observability/finance anchors under-exercise — **HLL** +(unique users) and **CountSketch/CMS-heap** (top events/features) — and carries the +most *universal* cold-raw motivation in the survey: **GDPR/CCPA right-to-erasure & +data-export**, which forces exact replay/deletion of one user's raw events (a clean +Mode-2 drill-down). Unlike the access-gated finance/billing cold-raw cases (§4), +product analytics has **freely-downloadable** public clickstream corpora. + +### P1 — Taobao UserBehavior 2017 (Alibaba) *(recommended product-analytics add)* + +- **What:** real user-behavior log from Taobao — `(user_id, item_id, category_id, + behavior, timestamp)` with `behavior ∈ {pv, cart, fav, buy}` over **2017-11-25 → + 2017-12-03 (9 days)**. The canonical public clickstream / funnel dataset. +- **Volume / scale:** **~987,994 users, ~4 M items, ~100 M behavior events** + (~3.5 GB CSV); freely downloadable (Alibaba Tianchi / `github.com/alibaba`, + same publisher as the A2/A3 cluster traces already cited). +- **Cardinality:** **very high (✓✓)** — ~1 M users × ~4 M items; user_id is the + textbook HLL key. +- **Per-series frequency:** **low / sparse (✗)** — a single user emits events + sporadically; like resource traces, the volume is *aggregate*, not per-series. +- **Warm vs cold:** **DAU/MAU & unique-buyers → warm HLL; top items/categories → + warm CountSketch-heap/CMS; pv→cart→buy funnel counts → warm Sum/CMS; + dwell/value quantiles → warm DDSketch/KLL** — all predefined, repeated dashboard + queries (condition 1 ✓). **Cold:** **GDPR right-to-erasure / data-export** and + **ML-feature pipelines** need the exact per-user raw event stream → lossless cold. + The split is clean by **metric-identity separation** (the DAU rollup vs the raw + event log are different artifacts → Mode 1) *and* exposes a Mode-2 case where the + same `user_id` is both aggregated (dashboard) and exactly replayed (GDPR export) + → bound-based drill-down to one user. +- **Axes:** 1 ✓, 2 ✓ (HLL/topk/funnel), 3 ✓ (standing product dashboards), 4 ✓ + (rolling 7/28-day active users), 5 ~ (retention cohorts, but 9-day span limits it), + 6 ✓✓, 7 ✗. +- **Obtain:** · + (UserBehavior). + +### P2 — REES46 eCommerce behavior 2019 + +- **What:** multi-category online-store event stream — `view / cart / remove / + purchase` events with `user_id, product_id, category, brand, price, user_session`, + Oct–Nov 2019. A richer-property funnel/segmentation corpus. +- **Volume / scale:** **~285 M events** (~9 GB across two months); free on Kaggle. +- **Cardinality:** **very high (✓✓)** — millions of users × products × sessions. +- **Per-series frequency:** **low (✗)** per user/session. +- **Warm vs cold:** **funnel conversion, revenue Sum, top brands/products + (CountSketch-heap), distinct-purchasers (HLL), basket-value quantiles (DDSketch) + → warm**; **per-user export / fraud-investigation raw → cold.** Strong **Mode-2** + case (live segmentation dashboard + exact export on the same user series). +- **Axes:** 1 ✓, 2 ✓, 3 ✓, 4 ✓, 5 ~, 6 ✓✓, 7 ✗. +- **Obtain:** . + +### P3 — Wikipedia clickstream + +- **What:** monthly `(referrer → article)` navigation **click counts** — already a + per-pair aggregate, public since 2015. +- **Volume / scale:** tens of millions of (referrer, article) pairs/month, gzipped + TSV; freely downloadable. +- **Cardinality:** **very high (✓✓)** — distinct referrer×article pairs. +- **Per-series frequency:** **n/a (✗)** — the artifact is *already* the monthly + aggregate (no raw click stream published). +- **Warm vs cold:** like F4 Deutsche-Börse-PDS, it **is** the warm-tier output shape + — a **topk/heavy-hitter ground-truth** to validate CountSketch-heap answers + against, and a long-history (years of monthly dumps) low-frequency series. Little + cold motivation (raw is privacy-purged, not published). Role: **a check, not a + stressor** — the license-clean, public stand-in for real (private) clickstream. +- **Axes:** 2 ✓, 3 ✓, 4 ✓, 5 ✓ (years of dumps), 6 ✓✓; 1 ~, 7 ✗. +- **Obtain:** . + +**Honest caveat for the domain:** product analytics adds **axis 6 (very-high +cardinality) + axis 2 (HLL/topk aggregation)** and a *public* GDPR cold-raw story — +but it does **not** add **axis 7 (high-frequency-per-series)** (a user's events are +sparse), and the *richest* real data (production Amplitude/Mixpanel) is private, so +lead measurements on the free Taobao/REES46 proxies and cite real SaaS scale only +for motivation. It also partly overlaps A7 Wikimedia (topk/HLL) — frame it as the +generalized, GDPR-motivated business-analytics version, not a fully orthogonal axis. + +--- + ## 4. Honest gaps - **No single public dataset is "high-cardinality AND long-retention-raw" at once.** @@ -605,7 +698,8 @@ supply the **high-frequency-per-series ✓✓** axis. No single one maxes both, 2. **DEBS-2022 — M2, the coordinated-sampling + finance-tick anchor.** *Use case:* live VWAP / price-quantile dashboards + threshold alerts on a **skewed** symbol fleet; MiFID audit / backtest on the *same* series → cold. *Evaluates:* - coordinated sampling `p_i ∝ √(f_i/rate_i)` (**32× differentiation**, Fig 9), the + coordinated sampling via the ε-floor `p_i = 1/(1+ε²·rate_i)` (**32× + differentiation**, Fig 9), the ε-gate/delta regime, accuracy on real skew (0.9–1.1 % ≈ α), and the finance-tick Mode-2 story (cheap edge-Gorilla cold + warm sketch — two orthogonal edge compressions). *Warm:* VWAP/quantile/volume → DDSketch/Sum. *Cold:* trade-by-trade @@ -658,6 +752,9 @@ are wired; **{A3, A4, F5} are the three to build**. - NYSE Daily TAQ — · WRDS - Deutsche Börse PDS (AWS Open Data) — +- Taobao UserBehavior 2017 — · +- REES46 eCommerce behavior 2019 — +- Wikipedia clickstream — - Binance public data — · Kraken · CryptoDataDownload diff --git a/otel_collector_benchmark/epsilon_floor/epsilon_floor_vs_nitro_test.go b/otel_collector_benchmark/epsilon_floor/epsilon_floor_vs_nitro_test.go new file mode 100644 index 000000000..6e9935668 --- /dev/null +++ b/otel_collector_benchmark/epsilon_floor/epsilon_floor_vs_nitro_test.go @@ -0,0 +1,332 @@ +package epsilon_floor + +// CORRECTED comparison (supersedes the earlier per-key version, which was wrong: +// it set p_k = 1/(1+ε²·f_k) using the ORACLE per-key frequency f_k — circular, +// since knowing every f_k means you already counted exactly and need no sketch; +// and it resurrected the RETIRED per-key √(f/rate) allocation). +// +// The UNIFIED ε-floor is WHOLE-SKETCH: ONE p = 1/(1+ε²·R) where R is the +// sketch's TOTAL update count in the window — fully OBSERVABLE (the edge just +// counts its updates; no per-key map). NitroSketch is the same mechanism with a +// hand-picked global p; the ε-floor's contribution is DERIVING p from a target +// ε + the observed R, and (in a fleet) giving each edge its own p_i from its +// own R_i. +// +// WHAT THE ε-FLOOR ACTUALLY BOUNDS (the honest story): +// The whole-sketch ε-floor bounds the relative error of the ADDITIVE +// AGGREGATE (F1 = R, and any query spanning ≈all the mass) to ε. A POINT +// query on key k is protected only to ε_k ≈ √((1-p)/(p·f_k)) = ε·√(R/f_k): +// keys holding a constant fraction of the total mass are well-estimated; +// rare keys are NOT (they fall below the sampling noise floor). This is +// inherent to sampling, not a defect — and it is why the right accuracy +// metric is the AGGREGATE / heavy-hitter error, never the rare-key error. +// +// Test 1 (TestWholeSketchEpsilonFloor): real CMS over the real DEBS-2022 symbol +// stream. Reports the metrics NitroSketch targets — insert throughput, +// memory, query latency — AND the accuracy LAW: F1-total rel-err ≈ ε, with +// per-key rel-err binned by mass-fraction tracking the ε·√(R/f_k) prediction. +// Test 2 (TestFleetEpsilonFloorVsFixedP): a SYNTHETIC skewed fleet (rate-CV≫1) +// where the per-edge adaptation gives a large gap (the 3 real DEBS exchanges +// are only mildly skewed → ~1.3×, too weak to show it). +// +// Run: +// go test ./benchmark -run 'TestWholeSketchEpsilonFloor|TestFleetEpsilonFloorVsFixedP' -v + +import ( + "bufio" + "fmt" + "math" + "math/rand" + "os" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/ProjectASAP/sketchlib-go/common" + cms "github.com/ProjectASAP/sketchlib-go/sketches/CountMinSketch" +) + +type debsKey struct { + symbol string + count int64 +} + +func loadDebsKeys(t *testing.T) []debsKey { + f, err := os.Open("/tmp/debs_symbol_counts.csv") + if err != nil { + t.Skipf("DEBS symbol counts not staged (%v); run benchmark/extract_debs_rates.sh", err) + } + defer f.Close() + var ks []debsKey + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 1<<20), 1<<20) + for sc.Scan() { + p := strings.Split(sc.Text(), ",") + if len(p) < 2 { + continue + } + c, err := strconv.ParseInt(strings.TrimSpace(p[1]), 10, 64) + if err == nil && c > 0 { + ks = append(ks, debsKey{p[0], c}) + } + } + sort.Slice(ks, func(i, j int) bool { return ks[i].count > ks[j].count }) + return ks +} + +func relErrStats(e []float64) (median, p95, max, mean float64) { + if len(e) == 0 { + return + } + s := append([]float64(nil), e...) + sort.Float64s(s) + for _, x := range s { + mean += x + } + mean /= float64(len(s)) + q := func(p float64) float64 { return s[int(p*float64(len(s)-1))] } + return q(0.5), q(0.95), q(1.0), mean +} + +const ( + cmsRows = 5 + cmsColsMem = 4096 // canonical config — for the memory / throughput numbers + cmsColsAcc = 65536 // wide config — isolates the SAMPLING law from CMS collision error +) + +// ---- Test 1: whole-sketch ε-floor on a real CMS ------------------------------- + +// runCMSThroughput inserts the whole stream into a fresh CMS sampled at p via +// sketchlib's geometric skip-sampler (the real NitroSketch mechanism: skip work +// for unadmitted updates) and reports insert throughput + query latency. +func runCMSThroughput(t *testing.T, keys []debsKey, inputs []*common.SketchInput, p float64) (insTputMops, qLatNs float64) { + sk, err := cms.NewCountMinSketch(cmsRows, cmsColsMem) + if err != nil { + t.Fatal(err) + } + if p < 1.0 { + sk = sk.WithSampleP(p, 1) + } + var total int64 + t0 := time.Now() + for i, k := range keys { + in := inputs[i] + for j := int64(0); j < k.count; j++ { + sk.Update(in) + } + total += k.count + } + insTputMops = float64(total) / time.Since(t0).Seconds() / 1e6 + tq := time.Now() + for i := range keys { + sk.Estimate(inputs[i]) + } + qLatNs = float64(time.Since(tq).Nanoseconds()) / float64(len(keys)) + return +} + +// binomial draws the number of admitted items out of n trials at probability p. +// Normal approximation when n*p is large (the heavy hitters); exact Bernoulli +// sum when small (rare keys, cheap since n is tiny). Statistically identical to +// streaming whole-sketch admission, but O(#keys) instead of O(R). +func binomial(rng *rand.Rand, n int64, p float64) int64 { + if p >= 1.0 { + return n + } + mean := float64(n) * p + if mean > 25 { + sd := math.Sqrt(float64(n) * p * (1 - p)) + v := int64(math.Round(rng.NormFloat64()*sd + mean)) + if v < 0 { + v = 0 + } + if v > n { + v = n + } + return v + } + var a int64 + for i := int64(0); i < n; i++ { + if rng.Float64() < p { + a++ + } + } + return a +} + +// Test 1: whole-sketch ε-floor on a REAL CMS — the metrics NitroSketch targets +// (throughput/memory/latency) PLUS the honest accuracy law. +func TestWholeSketchEpsilonFloor(t *testing.T) { + keys := loadDebsKeys(t) + if len(keys) == 0 { + t.Skip("no keys") + } + inputs := make([]*common.SketchInput, len(keys)) + var R int64 + for i, k := range keys { + inputs[i] = common.FromString(k.symbol) + R += k.count + } + mapBytes := 0 + for _, k := range keys { + mapBytes += len(k.symbol) + 8 + 16 // key + int64 + map overhead + } + fmt.Printf("\nReal CMS over REAL DEBS: %d keys, R=%d total updates (OBSERVABLE)\n", len(keys), R) + + // ---- (A) throughput + memory (canonical 5×4096) ---- + fmt.Printf("\n(A) THROUGHPUT / MEMORY [CMS %d×%d]\n", cmsRows, cmsColsMem) + cmsBytes := cmsRows * cmsColsMem * 8 + fmt.Printf(" memory: CMS=%d B (constant in #keys) vs exact key→count map=%d B (O(#keys))\n", cmsBytes, mapBytes) + exTput, exQ := runCMSThroughput(t, keys, inputs, 1.0) + fmt.Printf(" [exact p=1] insert=%.1f Mupd/s query=%.0f ns/key\n", exTput, exQ) + for _, eps := range []float64{0.05, 0.10} { + p := 1.0 / (1.0 + eps*eps*float64(R)) + spTput, spQ := runCMSThroughput(t, keys, inputs, p) + fmt.Printf(" [ε=%.2f p=%.2e] insert=%.1f Mupd/s (%.1f×) query=%.0f ns/key\n", + eps, p, spTput, spTput/exTput, spQ) + } + + // ---- (B) accuracy LAW (wide 5×65536 so collisions don't mask the sampling) ---- + // What the ε-floor bounds: the AGGREGATE (F1) to ≈ε; a point query on key k + // only to ε·√(R/f_k). We verify both, averaged over seeds. + fmt.Printf("\n(B) ACCURACY LAW [CMS %d×%d, mean of 5 seeds]\n", cmsRows, cmsColsAcc) + seeds := []int64{1, 2, 3, 4, 5} + // mass-fraction bands (f_k / R), high→low; predicted point-query rel-err is + // ε·√(R/f_k) at the band's representative f_k. + for _, eps := range []float64{0.05, 0.10} { + p := 1.0 / (1.0 + eps*eps*float64(R)) + var f1errs []float64 + // per-decade accumulation of empirical & predicted point-query error + type acc struct{ emp, pred, n float64 } + bands := map[int]*acc{} + for _, seed := range seeds { + rng := rand.New(rand.NewSource(seed)) + sk, _ := cms.NewCountMinSketch(cmsRows, cmsColsAcc) + admitted := make([]int64, len(keys)) + var admTotal int64 + for i, k := range keys { + a := binomial(rng, k.count, p) + admitted[i] = a + admTotal += a + for j := int64(0); j < a; j++ { + sk.Update(inputs[i]) + } + } + inv := 1.0 / p + // F1 aggregate: the bounded quantity. + f1hat := float64(admTotal) * inv + f1errs = append(f1errs, math.Abs(f1hat-float64(R))/float64(R)) + // per-key point queries, binned by f_k decade. + for i, k := range keys { + fhat := sk.Estimate(inputs[i]) * inv + e := math.Abs(fhat-float64(k.count)) / float64(k.count) + d := int(math.Floor(math.Log10(float64(k.count)))) + if bands[d] == nil { + bands[d] = &acc{} + } + b := bands[d] + b.emp += e + b.pred += math.Sqrt((1 - p) / (p * float64(k.count))) + b.n++ + } + } + fm, _, _, _ := relErrStats(f1errs) + fmt.Printf(" ε=%.2f (p=%.2e): F1-total rel-err median=%.4f (target ≈ ε=%.2f) ✓\n", eps, p, fm, eps) + fmt.Printf(" point-query rel-err by mass band f_k:\n") + var ds []int + for d := range bands { + ds = append(ds, d) + } + sort.Sort(sort.Reverse(sort.IntSlice(ds))) + for _, d := range ds { + b := bands[d] + fmt.Printf(" f_k~1e%d (massfrac~%.1e): empirical=%.3f predicted ε√(R/f_k)=%.3f (%.0f keys)\n", + d, math.Pow(10, float64(d))/float64(R), b.emp/b.n, b.pred/b.n, b.n/float64(len(seeds))) + } + } + fmt.Printf(" → the AGGREGATE is held at ≈ε; point queries degrade as ε·√(R/f_k):\n") + fmt.Printf(" heavy keys survive, rare keys fall below the sampling floor (inherent).\n") +} + +// ---- Test 2: synthetic skewed fleet ------------------------------------------- + +// Test 2: per-edge ε-floor vs fixed-p on a SYNTHETIC fleet with rate-CV ≫ 1. +// Real DEBS has only 3 exchanges (mild skew → ~1.3×), too weak to show the +// adaptation; a real fleet of per-service metric streams spans orders of +// magnitude. ε-floor gives edge i p_i=1/(1+ε²R_i), holding its F1 sampling +// error ε_s,i=√((1-p)/(p·R_i)) at ε UNIFORMLY; matched-bandwidth fixed-p +// under-protects the low-rate edges. We report the analytical per-edge error +// AND an empirical F1 spot-check (real CMS) on the highest- and lowest-rate edge. +func TestFleetEpsilonFloorVsFixedP(t *testing.T) { + // 64 edges, rates Zipf-spread over ~5 decades: r_i = Rmax / (i+1)^1.3. + const nEdges = 64 + const rMax = 5_000_000.0 + rates := make([]int64, nEdges) + var Rtot float64 + for i := 0; i < nEdges; i++ { + r := math.Max(1, math.Round(rMax/math.Pow(float64(i+1), 1.3))) + rates[i] = int64(r) + Rtot += r + } + // rate-CV + mean := Rtot / nEdges + var v float64 + for _, r := range rates { + v += (float64(r) - mean) * (float64(r) - mean) + } + cv := math.Sqrt(v/nEdges) / mean + fmt.Printf("\nSynthetic fleet: %d edges, R-range %d..%d, rate-CV=%.2f\n", + nEdges, rates[0], rates[nEdges-1], cv) + + eps := 0.05 + // ε-floor per-edge p_i; matched fixed-p = same TOTAL admitted bandwidth. + var admFloor float64 + for _, r := range rates { + pi := 1.0 / (1.0 + eps*eps*float64(r)) + admFloor += float64(r) * pi + } + pFixed := admFloor / Rtot + epsS := func(p float64, r int64) float64 { return math.Sqrt((1 - p) / (p * float64(r))) } + + var floorE, fixedE []float64 + for _, r := range rates { + pi := 1.0 / (1.0 + eps*eps*float64(r)) + floorE = append(floorE, epsS(pi, r)) + fixedE = append(fixedE, epsS(pFixed, r)) + } + fm, _, fx, _ := relErrStats(floorE) + gm, _, gx, _ := relErrStats(fixedE) + fmt.Printf(" ε=%.2f target, matched bandwidth (fixed-p=%.2e):\n", eps, pFixed) + fmt.Printf(" [ε-floor] per-edge F1 sampling-err median=%.4f max=%.4f (all ≈ ε by construction)\n", fm, fx) + fmt.Printf(" [fixed-p] per-edge F1 sampling-err median=%.4f max=%.4f (low-rate edges under-protected)\n", gm, gx) + fmt.Printf(" → fixed-p WORST-edge error is %.0f× the ε-floor's (vs ~1.3× on the 3 real DEBS exchanges)\n", gx/fx) + + // empirical F1 spot-check (real CMS) on the highest- and lowest-rate edge. + fmt.Printf(" empirical F1 rel-err (real CMS, mean of 5 seeds):\n") + check := func(label string, r int64) { + var floorErr, fixedErr float64 + const trials = 5 + for s := int64(0); s < trials; s++ { + rng := rand.New(rand.NewSource(100 + s)) + pi := 1.0 / (1.0 + eps*eps*float64(r)) + in := common.FromString(label) + doF1 := func(p float64) float64 { + sk, _ := cms.NewCountMinSketch(cmsRows, cmsColsAcc) + a := binomial(rng, r, p) + for j := int64(0); j < a; j++ { + sk.Update(in) + } + f1 := sk.Estimate(in) / p + return math.Abs(f1-float64(r)) / float64(r) + } + floorErr += doF1(pi) + fixedErr += doF1(pFixed) + } + fmt.Printf(" %-10s (R=%8d): ε-floor=%.4f fixed-p=%.4f\n", + label, r, floorErr/trials, fixedErr/trials) + } + check("hot-edge", rates[0]) + check("cold-edge", rates[nEdges-1]) +} diff --git a/otel_collector_benchmark/epsilon_floor/extract_debs_rates.sh b/otel_collector_benchmark/epsilon_floor/extract_debs_rates.sh new file mode 100755 index 000000000..8c371e116 --- /dev/null +++ b/otel_collector_benchmark/epsilon_floor/extract_debs_rates.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Extract real per-symbol trade frequencies from DEBS-2022 day-1 → /tmp/debs_symbol_counts.csv +# (the skewed per-key rate distribution used by epsilon_floor_vs_nitro_test.go). +CSV="${1:-/mydata/ASAPCollector/datasets_eval/debs/data/debs2022-gc-trading-day-08-11-21.csv}" +grep -vE '^#|^ID,' "$CSV" | awk -F, '{c[$1]++} END{for(s in c) print s","c[s]}' \ + | sort -t, -k2 -nr > /tmp/debs_symbol_counts.csv +echo "wrote /tmp/debs_symbol_counts.csv ($(wc -l ../../../sketchlib-go diff --git a/otel_collector_benchmark/epsilon_floor/go.sum b/otel_collector_benchmark/epsilon_floor/go.sum new file mode 100644 index 000000000..991a424f9 --- /dev/null +++ b/otel_collector_benchmark/epsilon_floor/go.sum @@ -0,0 +1,48 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/prometheus v0.307.1 h1:Hh3kRMFn+xpQGLe/bR6qpUfW4GXQO0spuYeY7f2JZs4= +github.com/prometheus/prometheus v0.307.1/go.mod h1:/7YQG/jOLg7ktxGritmdkZvezE1fa6aWDj0MGDIZvcY= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=