Skip to content

fix: max, min, count and avg over a range function are one series per group on query_range - #734

Merged
szibis merged 8 commits into
mainfrom
fix/non-sum-vector-aggregation-unreduced
Oct 23, 2026
Merged

szibis merged 8 commits into
mainfrom
fix/non-sum-vector-aggregation-unreduced

Conversation

@szibis

@szibis szibis commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Gap semantics/non-sum-vector-aggregation-unreduced. On query_range, max, min, count and avg over a range function returned one series per stream instead of one per group when the window is not the step. max(count_over_time({service_name="api-gateway"}[5m])) returned 2,160 series over an hour at step 60 s, where Loki 3.7.7 returns 1. max by (baz), a label no stream has, returned 10,656 against 1. Nested chains were also wrong: sum(max(rate(...))) counted lines instead of computing a rate.

Cause.

  • The two-stage translation (fix: count, min, max and avg read one value per Loki series #582) only takes effect where VictoriaLogs runs the whole query: instant queries and tumbling windows. The inner stats stage keeps series identity and the outer stage applies the operator.
  • A sliding window goes to the window evaluator instead. That evaluator built its spec from the first stats pipe only (parseStatsCompatSpec), so it returned per-stream series and dropped the outer operator.
  • Separately, parseOriginalRangeMetricSpec stripped only the outermost aggregation.

Fix.

  • VictoriaLogs still computes each series' window values: one stats query, no raw rows. A sliding window needs each series' window before any fold, so VictoriaLogs can't do the fold itself.
  • The proxy then folds those series per step and per group, as Loki's VectorAggEvaluator does (pkg/logql/evaluator.go:400-529). This is foldVectorAggregations in the new internal/proxy/vector_agg_fold.go, built on applyWithoutGrouping/withoutMergeForOp. It:
    • works innermost first, across chains of sum, count, min, max and avg that contain no without;
    • checks max_query_series on the folded answer, as Loki does.
  • Grouping on a parsed label: count by (route) over | logfmt route folds on that label. The window evaluator drops parser-extracted labels unless asked for them, and the fold now asks for them.
  • parseOriginalRangeMetricSpec now walks nested aggregations down to the range function. It stops at a nested without that isn't the outermost one, so those shapes answer as before (semantics/without-nested-under-aggregation).
  • Folding series min/max no longer lets a NaN win, matching Loki's evaluator (evaluator.go:525-535). This also fixes the existing without path.
  • Instant queries and tumbling windows are unchanged.

Rebase onto main (2f52014)

  • CHANGELOG: the entry is filed under [Unreleased]. The released sections are identical to main.
  • range_metric_compat.go: main added withQuantileShape and moved withoutUnwrapGate into proxyManualRangeMetricInstant. Both are kept; the PR's change is still the swap from foldUngroupedSum to foldVectorAggregations at the range and instant call sites.
  • extraction_list_outside_without_test.go: main's goldens are kept, Sent text included. This PR changes only the answers of three sliding goldens to the folded series:
    • count by (ns) over count_over_time with | logfmt route;
    • count by (ns) over rate with | logfmt route;
    • count by (route) over count_over_time with | logfmt route.
  • bench/visual/spec.json: main's new pages are kept, and this PR's explore-max-by-count-over-time-sliding fix-proof page is added after them.
  • Regenerated: wiring.json, errors.json (line numbers), gaps.md, parity-gaps.md and roadmap.md.

Registry

Changed row conformance/registry/cases/semantics/non-sum-vector-aggregation-unreduced.yaml:

Also updated:

  • semantics/without-over-inner-aggregation-collapses: the query_range rate case is now fixed.
  • semantics/unwrap-extraction-list-series-identity: the sliding figure is recorded.

parity-gaps.md moves the row from rank 2 (open) to the fixed list.

Tests

  • TestVectorAggregationOverRangeFunctionDifferential runs the real handler against a VictoriaLogs emulator and compares it with a Loki-semantics reference. It covers:

    • count/min/max/avg over count_over_time and rate;
    • ungrouped, by, by (), a missing label, and without;
    • logfmt on and off;
    • range and instant;
    • emulated VictoriaLogs v1.49.0, v1.52.0 and an unknown version.

    Before the fix, 290 range subtests failed.

  • Also new:

    • TestVectorAggregationGroupedByParsedLabelSlidingWindow (max|min|count|avg by (route) over | logfmt route on a sliding window)
    • TestVectorAggregationGroupedLabelsSlidingWindow
    • TestVectorAggregationUnderBinaryScalar
    • TestVectorAggregationOverRateCounterFolds
    • TestParseOriginalRangeMetricSpecNestedAggregations
    • TestFoldByGroupingEdgeCases, including NaN
    • TestApplyWithoutGroupingNaNMinMax
  • Changed: TestInstantUnwrapOtherAggregationsKeepPerStreamValues asserted the old, non-Loki answer. It is now TestInstantUnwrapOtherAggregationsFoldPerStreamValues and asserts Loki's.

  • e2e vs Loki: TestCompat_NonSumVectorAggregationRange (test/e2e-compat, group semantics).

Proofs on the head (6042bc3)

  • go build ./... and go vet ./internal/proxy/ ./internal/translator/: ok.
  • Touched tests with -race (VectorAgg|FoldByGrouping|ApplyWithoutGrouping|ParseOriginalRangeMetricSpec|InstantUnwrap|ExtractionList|WithoutOpenShape): 1,020 passed. After the leak fix below, VectorAgg|FoldByGrouping: 866 passed.
  • Capped full run (GOMEMLIMIT=3GiB GOMAXPROCS=4 go test -short -count=1 -p 1 ./cmd/... ./internal/...): 13/13 packages ok.
  • Test memory. The first full run peaked at 2.58 GiB against 1.68 GiB on main.
    • Cause: TestVectorAggregationOverRangeFunctionDifferential built a proxy for each of its 6,336 scenarios and closed only the stub backend. 38,016 cache goroutines stayed alive.
    • Fix (6042bc3, test-only): each scenario now shuts its proxy down and closes the caches.
    • After the fix: the test alone peaks at 91 MB instead of 704 MB. The internal/proxy test binary peaked at 1.93 and 1.90 GB on this PR, against 1.89 and 1.91 GB on main (two runs each, same machine).
    • Proxy side: Shutdown leaves the two caches the proxy creates running. That is fixed separately in fix: shutdown closes the caches the proxy creates, with lifecycle and fuzz leak guards #857.
  • python3 scripts/ci/check_conformance.py: conformance gate ok; ratchet coverage 54%, unchanged.
  • python3 scripts/ci/check_changelog_pr.py --base origin/main --head HEAD: ok.

e2e against Loki on the supported VictoriaLogs range

TestCompat_NonSumVectorAggregationRange (test/e2e-compat) ran on an isolated bench/ab stack. The stack used Loki 3.7.7 with the e2e configuration, and VictoriaLogs at each version (verified via vm_app_version). The test ran against main's binary and this PR's, both with the flags of the e2e loki-vl-proxy service. It refuses to compare when Loki answers no sample.

VictoriaLogs main this PR Loki / VL restarts
v1.49.0 188 pass, 209 fail 397 pass, 0 fail 0→0 / 0→0
v1.53.0 188 pass, 209 fail 397 pass, 0 fail 0→0 / 0→0

CI's vl-pinned job covers v1.52.0.

Data-to-data against Loki (differential run, bench/parity)

Setup.

  • Stack: isolated, project parity. Loki 3.7.7 and VictoriaLogs v1.52.0 were seeded with the same 1.5 h of generator data: 286,227 lines, equal in every 10-minute slice before and after each run.
  • Proxies: main (9c5e6df) and this PR's binary ran against the same Loki and VictoriaLogs, with the same flags.
  • Health: Loki's restart count was 8 at the start and the end of every sub-run, and VictoriaLogs' was 0. The proxy logs show no backend 5xx and no fallback after a 5xx.

Subset 1: every corpus request whose outer operator is count, min, max or avg. That is 556 queries, each sent with and without categorize-labels, so 1,112 requests.

match Loki differ both empty (no proof)
main 54 / 418 (12.9%) 364 694
this PR 110 / 418 (26.3%) 308 694
  • Fixed: 56 requests match Loki that didn't on main.
  • Regressions: none. No request that matched on main differs on this PR.
  • Series now right, values still differing: 128 requests had the wrong series on main (one per stream). They now have Loki's series and differ only in values. That residue comes from series identity, not from the fold:
    • Loki's inner series also split on structured metadata and on parsed labels. For example, {namespace="prod"} is 2,507 series on Loki and 514 on the proxy.
    • So count is lower, and max/min/avg over merged series differ. That is the recorded deviation metric-identity/structured-metadata-splits-series, plus the parsed-label identity rows.
  • Unchanged from main, and not claimed here: without label sets, parse-error texts, and a nested outer by on the instant path.

Subset 2: nested chains (sum(max(...)) and deeper), 14 requests.

  • Fixed: sum(max(rate({namespace="prod"}[5m]))) and sum(max(bytes_over_time({namespace="prod"}[3m]))). Each now matches Loki on the endpoint that differed on main.
  • Regressions: none.
  • Still differing, as on main:
    • 1 s and 2 s windows, where Loki answers no series;
    • a label_format parsed-label case;
    • the nested outer by on the instant path.

Pending before ready

Each item below gets its result added here:

  • A/B at 1h/6h/24h/7d plus the control set, with benchstat;
  • the visual fix proof (explore-max-by-count-over-time-sliding);
  • the range matrix with the backend-safety load test.

Not changed here (separate rows)

@github-actions github-actions Bot added size/XL Extra large change scope/proxy Proxy core scope/docs Documentation scope/tests Tests bugfix Bug fix labels Oct 12, 2026
szibis added a commit that referenced this pull request Oct 12, 2026
…ion-unreduced, with its visual fix proof page
@github-actions github-actions Bot added size/XL Extra large change and removed size/XL Extra large change labels Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

⚪ Performance A/B: no change beyond noise

131 shape×range — 131 ⚪ same · results vs Loki: 101 same, 0 new difference(s), 30 pre-existing

Every shape is within noise of the base build (±30% and 100 ms).

131 unchanged shape×range (within noise)
shape range base p50 PR p50 change Loki p50 PR ÷ Loki result vs Loki
⚪ bytes_over_time by app 1h 78 ms 72 ms ⚪ -8% 118 ms 0.61× ✅ same
⚪ drilldown service landing 1h 38 ms 32 ms ⚪ -16% 49 ms 0.65× ✅ same
⚪ instant json filtered instant 11 ms 11 ms ⚪ +0% 87 ms 0.13× ✅ same
⚪ instant json volume instant 34 ms 34 ms ⚪ +0% 117 ms 0.29× ✅ same
⚪ instant ungrouped rate instant 6 ms 6 ms ⚪ +0% 99 ms 0.06× ✅ same
⚪ json rate by level (no filter, no drop) 1h 400 400 ⚪ +0% 400 — ✅ same
⚪ json volume no filters 1h 221 ms 206 ms ⚪ -7% 65 ms 3.17× ✅ same
⚪ logfmt count by level, filter, drop 1h 454 ms 455 ms ⚪ +0% 80 ms 5.69× ✅ same
⚪ logfmt volume 1h 223 ms 221 ms ⚪ -1% 85 ms 2.60× ✅ same
⚪ logs plain 1h 74 ms 71 ms ⚪ -4% 54 ms 1.31× ✅ same
⚪ logs | json 1h 117 ms 111 ms ⚪ -5% 82 ms 1.35× ✅ same
⚪ logs | logfmt 1h 157 ms 188 ms ⚪ +20% 64 ms 2.94× ✅ same
⚪ stream-label count by app 1h 27 ms 26 ms ⚪ -4% 58 ms 0.45× ✅ same
⚪ topk rate by app 1h 50 ms 44 ms ⚪ -12% 114 ms 0.39× ➖ differs (as base)
⚪ ungrouped rate 1h 12 ms 15 ms ⚪ +25% 103 ms 0.15× ✅ same
⚪ DB3 json field breakdown user_id 1h 52 ms 48 ms ⚪ -8% 46 ms 1.04× ✅ same
⚪ DB4 json field breakdown batch_id 1h 103 ms 109 ms ⚪ +6% 92 ms 1.18× ✅ same
⚪ X1 json filter level_extracted, by level 1h 407 ms 405 ms ⚪ -0% 115 ms 3.52× ✅ same
⚪ X2 json group by level_extracted 1h 316 ms 320 ms ⚪ +1% 73 ms 4.38× ✅ same
⚪ X3 json rate, level_extracted equality 1h 285 ms 291 ms ⚪ +2% 54 ms 5.39× ✅ same
⚪ X4 json bytes, level_extracted regexp 1h 413 ms 413 ms ⚪ +0% 112 ms 3.69× ✅ same
⚪ X5 logfmt filter level_extracted, by level 1h 684 ms 688 ms ⚪ +1% 74 ms 9.30× ✅ same
⚪ E1 logs, json parser over stream labels 1h 126 ms 120 ms ⚪ -5% 72 ms 1.67× ✅ same
⚪ E2 logs, logfmt parser over stream labels 1h 86 ms 109 ms ⚪ +27% 65 ms 1.68× ✅ same
⚪ E3 logs, json, existence filter on level_extracted 1h 171 ms 153 ms ⚪ -11% 88 ms 1.74× ✅ same
⚪ E4 logs, logfmt, existence filter on level_extracted 1h 111 ms 100 ms ⚪ -10% 82 ms 1.22× ✅ same
⚪ E5 logs, json, drop level_extracted 1h 121 ms 123 ms ⚪ +2% 74 ms 1.66× ✅ same
⚪ E6 logfmt count by level_extracted 1h 48 ms 48 ms ⚪ +0% 9 ms 5.33× ✅ same
⚪ E7 json count by level_extracted 1h 150 ms 147 ms ⚪ -2% 19 ms 7.74× ✅ same
⚪ E8 instant logfmt by level_extracted instant 8 ms 7 ms ⚪ -12% 11 ms 0.64× ✅ same
⚪ J1 json no-error count by service_name, one service 1h 107 ms 111 ms ⚪ +4% 38 ms 2.92× ✅ same
⚪ J2 json no-error count by app, namespace 1h 321 ms 299 ms ⚪ -7% 105 ms 2.85× ✅ same
⚪ J3 json no-error count by parsed method 1h 164 ms 181 ms ⚪ +10% 29 ms 6.24× ✅ same
⚪ J4 json no-error count by service_name, all services 1h 333 ms 341 ms ⚪ +2% 145 ms 2.35× ✅ same
⚪ J5 json no-error bytes_rate by app 1h 284 ms 284 ms ⚪ +0% 88 ms 3.23× ✅ same
⚪ A explore volume, 2 filters, drop 1h 233 ms 181 ms ⚪ -22% 314 ms 0.58× ✅ same
⚪ B grouped sum, filter, no drop 1h 105 ms 108 ms ⚪ +3% 58 ms 1.86× ✅ same
⚪ C ungrouped sum, filter, no drop 1h 106 ms 105 ms ⚪ -1% 57 ms 1.84× ✅ same
⚪ D volume, no filters 1h 236 ms 232 ms ⚪ -2% 74 ms 3.14× ✅ same
⚪ E field breakdown pipeline (explore) 1h 100 ms 101 ms ⚪ +1% 56 ms 1.80× ✅ same
⚪ F field breakdown service_version (explore) 1h 147 ms 134 ms ⚪ -9% 68 ms 1.97× ✅ same
⚪ G field breakdown pipeline (drilldown) 1h 45 ms 48 ms ⚪ +7% 46 ms 1.04× ✅ same
⚪ H field breakdown service_version (drilldown) 1h 69 ms 66 ms ⚪ -4% 47 ms 1.40× ✅ same
⚪ I grafana volume of B 1h 71 ms 67 ms ⚪ -6% 55 ms 1.22× ✅ same
⚪ O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 70 ms 71 ms ⚪ +1% 62 ms 1.15× ✅ same
⚪ DL1 drilldown field breakdown json-expr, level filter 1h 524 ms 533 ms ⚪ +2% 47 ms 11× ✅ same
⚪ DL2 drilldown field breakdown, level + stream-label filter before json 1h 45 ms 45 ms ⚪ +0% 11 ms 4.09× ✅ same
⚪ DL3 drilldown logfmt field breakdown, level filter 1h 669 ms 676 ms ⚪ +1% 139 ms 4.86× ➖ differs (as base)
⚪ DL4 instant count, level filter, json+logfmt, 1h instant 511 ms 516 ms ⚪ +1% 1.35 s 0.38× ✅ sum ±0.35%
⚪ DL6 explore volume, level filter before json 1h 145 ms 146 ms ⚪ +1% 61 ms 2.39× ✅ same
⚪ OR1 logs, or alternatives 1h 88 ms 80 ms ⚪ -9% 117 ms 0.68× ✅ same
⚪ OR2 logs, negated or alternatives 1h 90 ms 81 ms ⚪ -10% 86 ms 0.94× ✅ same
⚪ OR3 logs, regexp or alternatives 1h 63 ms 78 ms ⚪ +24% 97 ms 0.80× ✅ same
⚪ OR4 count by level, or alternatives with parser 1h 82 ms 83 ms ⚪ +1% 55 ms 1.51× ✅ same
⚪ OR5 rate, negated regexp or alternatives 1h 56 ms 59 ms ⚪ +5% 162 ms 0.36× ✅ same
⚪ OR6 instant, or alternatives instant 8 ms 8 ms ⚪ +0% 82 ms 0.10× ✅ same
⚪ P1 api-gateway empty pattern 1h 24 ms 24 ms ⚪ +0% 20 ms 1.20× ✅ same
⚪ P2 api-gateway not empty pattern, line filter 1h 81 ms 70 ms ⚪ -14% 35 ms 2.00× ✅ same
⚪ P3 nginx count_over_time, pattern with spaces 1h 13 ms 13 ms ⚪ +0% 10 ms 1.30× ✅ same
⚪ P4 nginx count_over_time, GET request pattern 1h 14 ms 15 ms ⚪ +7% 7 ms 2.14× ✅ same
⚪ A1 nginx include GET, log query 1h 20 ms 21 ms ⚪ +5% 9 ms 2.33× ✅ same
⚪ A2 nginx exclude GET, log query 1h 34 ms 33 ms ⚪ -3% 10 ms 3.30× ✅ same
⚪ A3 nginx include GET, count_over_time 1h 18 ms 19 ms ⚪ +6% 8 ms 2.38× ✅ same
⚪ A4 nginx exclude GET, count_over_time 1h 19 ms 19 ms ⚪ +0% 6 ms 3.17× ✅ same
⚪ A5 drilldown exclude, volume by detected_level 1h 26 ms 26 ms ⚪ +0% 6 ms 4.33× ✅ same
⚪ A6 nginx include ends on a literal, log query 1h 15 ms 15 ms ⚪ +0% 5 ms 3.00× ✅ same
⚪ A7 nginx exclude ends on a literal, log query 1h 23 ms 21 ms ⚪ -9% 10 ms 2.10× ✅ same
⚪ A8 nginx include two literals, count_over_time 1h 24 ms 23 ms ⚪ -4% 7 ms 3.29× ✅ same
⚪ A9 nginx include two literals, ends on a literal, count_over_time 1h 21 ms 21 ms ⚪ +0% 7 ms 3.00× ✅ same
⚪ P1 logs, json, level equals 1h 132 ms 129 ms ⚪ -2% 90 ms 1.43× ✅ same
⚪ P2 logs, logfmt, level not equal 1h 42 ms 41 ms ⚪ -2% 9 ms 4.56× ✅ same
⚪ P3 logs, json, level existence 1h 124 ms 124 ms ⚪ +0% 85 ms 1.46× ✅ same
⚪ P4 logfmt count by level 1h 32 ms 32 ms ⚪ +0% 9 ms 3.56× ✅ same
⚪ P5 json count by level 1h 84 ms 83 ms ⚪ -1% 18 ms 4.61× ✅ same
⚪ P6 json count, level filter 1h 15 ms 17 ms ⚪ +13% 17 ms 1.00× ✅ same
⚪ P7 json rate by level, level regexp 1h 17 ms 18 ms ⚪ +6% 18 ms 1.00× ✅ same
⚪ P8 json count by a name that is no stream label 1h 88 ms 88 ms ⚪ +0% 18 ms 4.89× ✅ same
⚪ P9 instant logfmt by level instant 6 ms 6 ms ⚪ +0% 11 ms 0.55× ✅ same
⚪ E1 json duration_ms sum by service 1h 94 ms 88 ms ⚪ -6% 27 ms 3.26× ➖ differs (as base)
⚪ E2 json duration_ms p95 by service 1h 300 ms 272 ms ⚪ -9% 19 ms 14× ✅ same
⚪ E3 logfmt latency_ms rate, production 1h 525 ms 522 ms ⚪ -1% 154 ms 3.39× ➖ differs (as base)
⚪ E4 logfmt size_bytes max by service 1h 51 ms 53 ms ⚪ +4% 17 ms 3.12× ➖ differs (as base)
⚪ E5 json duration_ms sum, instant instant 39 ms 35 ms ⚪ -10% 30 ms 1.17× ✅ same
⚪ E6 logfmt ttl unit string (Loki 400) 1h 400 400 ⚪ +2% 400 — ✅ same
⚪ E7 json hexadecimal trace_id (Loki 400) 1h 400 400 ⚪ -3% 400 — ✅ same
⚪ E8 json duration_ms sum by service, namespace prod 1h 198 ms 198 ms ⚪ +0% 130 ms 1.52× ➖ differs (as base)
⚪ E9 logfmt latency_ms sum by service, namespace prod 1h 314 ms 329 ms ⚪ +5% 114 ms 2.89× ➖ differs (as base)
⚪ G1 logfmt amount sum_over_time, no filter 1h 41 ms 39 ms ⚪ -5% 18 ms 2.17× ➖ differs (as base)
⚪ G10 logfmt latency_ms sum_over_time, production 1h 504 ms 472 ms ⚪ -6% 165 ms 2.86× ➖ differs (as base)
⚪ G11 logfmt amount sum_over_time, instant instant 7 ms 7 ms ⚪ +0% 11 ms 0.64× ✅ same
⚪ G12 json duration_ms sum_over_time, instant instant 36 ms 35 ms ⚪ -3% 27 ms 1.30× ✅ same
⚪ G2 json duration_ms sum_over_time, no filter 1h 86 ms 89 ms ⚪ +3% 39 ms 2.28× ➖ differs (as base)
⚪ G3 logfmt amount sum_over_time, error filter 1h 42 ms 40 ms ⚪ -5% 13 ms 3.08× ➖ differs (as base)
⚪ G4 json duration_ms sum_over_time, error filter 1h 87 ms 87 ms ⚪ +0% 30 ms 2.90× ➖ differs (as base)
⚪ G5 logfmt amount sum by rate 1h 43 ms 43 ms ⚪ +0% 13 ms 3.31× ✅ same
⚪ G6 json duration_ms sum by rate 1h 98 ms 97 ms ⚪ -1% 30 ms 3.23× ✅ same
⚪ G7 duration() of a unit string 1h 65 ms 65 ms ⚪ +0% 13 ms 5.00× ➖ differs (as base)
⚪ G8 bytes() of a number 1h 65 ms 63 ms ⚪ -3% 10 ms 6.30× ➖ differs (as base)
⚪ G9 json duration_ms max_over_time 1h 102 ms 99 ms ⚪ -3% 138 ms 0.72× ➖ differs (as base)
⚪ W1 sum without pod, max_over_time 1h 47 ms 52 ms ⚪ +11% 24 ms 2.17× ➖ differs (as base)
⚪ W2 sum without pod, sum_over_time 1h 47 ms 46 ms ⚪ -2% 17 ms 2.71× ➖ differs (as base)
⚪ W3 sum without pod, rate 1h 143 ms 146 ms ⚪ +2% 17 ms 8.59× ✅ same
⚪ W4 max without pod, max_over_time 1h 48 ms 48 ms ⚪ +0% 19 ms 2.53× ➖ differs (as base)
⚪ W5 sum without pod, max_over_time, instant instant 8 ms 9 ms ⚪ +12% 15 ms 0.60× ✅ sum ±0.00%
⚪ W6 sum without pod, rate, instant instant 8 ms 8 ms ⚪ +0% 15 ms 0.53× ✅ same
⚪ W7 topk 5 of sum without pod 1h 49 ms 47 ms ⚪ -4% 18 ms 2.61× ➖ differs (as base)
⚪ W8 sum without pod, count_over_time (production) 1h 968 ms 906 ms ⚪ -6% 0 — ➖ differs (as base)
⚪ W9 label_replace control, instant instant 5 ms 4 ms ⚪ -20% 76 ms 0.05× ✅ same
⚪ U1 logfmt amount rate 1h 39 ms 41 ms ⚪ +5% 19 ms 2.16× ✅ same
⚪ U2 json duration_ms rate 1h 93 ms 91 ms ⚪ -2% 36 ms 2.53× ✅ same
⚪ U3 json duration_ms rate, error filter 1h 93 ms 89 ms ⚪ -4% 32 ms 2.78× ✅ same
⚪ U4 logfmt latency_ms rate, production 1h 370 ms 366 ms ⚪ -1% 163 ms 2.25× ➖ differs (as base)
⚪ U5 logfmt amount rate, instant instant 7 ms 8 ms ⚪ +14% 13 ms 0.62× ✅ same
⚪ U6 json duration_ms rate, instant instant 12 ms 14 ms ⚪ +17% 26 ms 0.54× ✅ same
⚪ U7 json duration_ms sum_over_time, instant instant 14 ms 17 ms ⚪ +21% 25 ms 0.68× ✅ same
⚪ U8 logfmt amount rate by service (grouped) 1h 53 ms 49 ms ⚪ -8% 15 ms 3.27× ✅ same
⚪ U9 logfmt amount rate_counter (folded per series) 1h 306 ms 309 ms ⚪ +1% 45 ms 6.87× ➖ differs (as base)
⚪ U1 json duration_ms sum by service 1h 90 ms 87 ms ⚪ -3% 28 ms 3.11× ➖ differs (as base)
⚪ U10 instant sum_over_time unwrap instant 34 ms 39 ms ⚪ +15% 31 ms 1.26× ✅ same
⚪ U11 instant max_over_time missing label instant 6 ms 6 ms ⚪ +0% 18 ms 0.33× ✅ same
⚪ U12 json duration_ms over every service 1h 250 ms 248 ms ⚪ -1% 115 ms 2.16× ➖ differs (as base)
⚪ U13 logfmt latency_ms over every service 1h 529 ms 540 ms ⚪ +2% 91 ms 5.93× ➖ differs (as base)
⚪ U14 duration() of a unit string 1h 60 ms 60 ms ⚪ +0% 8 ms 7.50× ➖ differs (as base)
⚪ U2 json duration_ms max by service 1h 99 ms 103 ms ⚪ +4% 52 ms 1.98× ➖ differs (as base)
⚪ U3 json duration_ms sliding sum 1h 86 ms 89 ms ⚪ +3% 42 ms 2.12× ➖ differs (as base)
⚪ U4 logfmt size_bytes sum by service 1h 49 ms 48 ms ⚪ -2% 11 ms 4.36× ➖ differs (as base)
⚪ U5 logfmt ttl unit string (no sample) 1h 49 ms 45 ms ⚪ -8% 7 ms 6.43× ✅ same
⚪ U6 missing label, no parser 1h 21 ms 21 ms ⚪ +0% 13 ms 1.62× ✅ same
⚪ U7 missing label, ungrouped sum 1h 19 ms 20 ms ⚪ +5% 13 ms 1.54× ✅ same
⚪ U8 rate of an unwrapped label 1h 91 ms 92 ms ⚪ +1% 18 ms 5.11× ✅ same
⚪ U9 bytes() conversion 1h 61 ms 59 ms ⚪ -3% 14 ms 4.21× ➖ differs (as base)
Cold (first-run) timings — what a user opening a panel sees
shape range base cold PR cold Loki cold PR ÷ Loki cold
bytes_over_time by app 1h 72 ms 75 ms 624 ms 0.12×
drilldown service landing 1h 36 ms 50 ms 540 ms 0.09×
instant json filtered instant 11 ms 17 ms 89 ms 0.19×
instant json volume instant 55 ms 40 ms 123 ms 0.33×
instant ungrouped rate instant 7 ms 7 ms 93 ms 0.08×
json rate by level (no filter, no drop) 1h 400 400 400 —
json volume no filters 1h 301 ms 275 ms 1.07 s ↺ 0.26×
logfmt count by level, filter, drop 1h 480 ms 453 ms 856 ms 0.53×
logfmt volume 1h 231 ms 230 ms 950 ms 0.24×
logs plain 1h 98 ms 76 ms 82 ms 0.93×
logs | json 1h 116 ms 112 ms 77 ms 1.45×
logs | logfmt 1h 154 ms 166 ms 64 ms 2.59×
stream-label count by app 1h 27 ms 53 ms 1.05 s 0.05×
topk rate by app 1h 81 ms 44 ms 615 ms 0.07×
ungrouped rate 1h 12 ms 12 ms 599 ms 0.02×
DB3 json field breakdown user_id 1h 60 ms 67 ms 776 ms 0.09×
DB4 json field breakdown batch_id 1h 139 ms 122 ms 973 ms 0.13×
X1 json filter level_extracted, by level 1h 4.08 s 3.58 s 1.01 s 3.54×
X2 json group by level_extracted 1h 314 ms 312 ms 1.01 s 0.31×
X3 json rate, level_extracted equality 1h 288 ms 283 ms 678 ms 0.42×
X4 json bytes, level_extracted regexp 1h 431 ms 417 ms 753 ms 0.55×
X5 logfmt filter level_extracted, by level 1h 682 ms 695 ms 1.03 s 0.67×
E1 logs, json parser over stream labels 1h 170 ms 124 ms 90 ms 1.38×
E2 logs, logfmt parser over stream labels 1h 65 ms 89 ms 61 ms 1.46×
E3 logs, json, existence filter on level_extracted 1h 151 ms 174 ms 110 ms 1.58×
E4 logs, logfmt, existence filter on level_extracted 1h 87 ms 126 ms 75 ms 1.68×
E5 logs, json, drop level_extracted 1h 126 ms 124 ms 70 ms 1.77×
E6 logfmt count by level_extracted 1h 48 ms 46 ms 49 ms 0.94×
E7 json count by level_extracted 1h 1.43 s 1.42 s 193 ms 7.37×
E8 instant logfmt by level_extracted instant 7 ms 8 ms 10 ms 0.80×
J1 json no-error count by service_name, one service 1h 223 ms 210 ms 198 ms 1.06×
J2 json no-error count by app, namespace 1h 439 ms 592 ms 733 ms 0.81×
J3 json no-error count by parsed method 1h 156 ms 158 ms 194 ms 0.81×
J4 json no-error count by service_name, all services 1h 522 ms 515 ms 837 ms 0.62×
J5 json no-error bytes_rate by app 1h 287 ms 280 ms 798 ms 0.35×
A explore volume, 2 filters, drop 1h 2.32 s 2.17 s 1.66 s 1.31×
B grouped sum, filter, no drop 1h 193 ms 187 ms 639 ms 0.29×
C ungrouped sum, filter, no drop 1h 124 ms 124 ms 647 ms 0.19×
D volume, no filters 1h 290 ms 284 ms 1.07 s 0.27×
E field breakdown pipeline (explore) 1h 117 ms 112 ms 656 ms 0.17×
F field breakdown service_version (explore) 1h 1.37 s 1.38 s 917 ms 1.50×
G field breakdown pipeline (drilldown) 1h 44 ms 48 ms 640 ms 0.07×
H field breakdown service_version (drilldown) 1h 110 ms 105 ms 725 ms 0.14×
I grafana volume of B 1h 115 ms 109 ms 622 ms 0.18×
O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 161 ms 152 ms 750 ms 0.20×
DL1 drilldown field breakdown json-expr, level filter 1h 506 ms 494 ms 546 ms 0.90×
DL2 drilldown field breakdown, level + stream-label filter before json 1h 75 ms 54 ms 32 ms 1.69×
DL3 drilldown logfmt field breakdown, level filter 1h 697 ms 728 ms 1.17 s 0.62×
DL4 instant count, level filter, json+logfmt, 1h instant 510 ms 518 ms 1.41 s 0.37×
DL6 explore volume, level filter before json 1h 200 ms 218 ms 1.01 s 0.22×
OR1 logs, or alternatives 1h 108 ms 77 ms 108 ms 0.71×
OR2 logs, negated or alternatives 1h 77 ms 85 ms 87 ms 0.98×
OR3 logs, regexp or alternatives 1h 62 ms 73 ms 100 ms 0.73×
OR4 count by level, or alternatives with parser 1h 128 ms 125 ms 691 ms 0.18×
OR5 rate, negated regexp or alternatives 1h 56 ms 56 ms 691 ms 0.08×
OR6 instant, or alternatives instant 8 ms 8 ms 82 ms 0.10×
P1 api-gateway empty pattern 1h 33 ms 24 ms 98 ms 0.24×
P2 api-gateway not empty pattern, line filter 1h 80 ms 82 ms 37 ms 2.22×
P3 nginx count_over_time, pattern with spaces 1h 13 ms 13 ms 29 ms 0.45×
P4 nginx count_over_time, GET request pattern 1h 14 ms 17 ms 30 ms 0.57×
A1 nginx include GET, log query 1h 33 ms 21 ms 11 ms 1.91×
A2 nginx exclude GET, log query 1h 30 ms 33 ms 16 ms 2.06×
A3 nginx include GET, count_over_time 1h 18 ms 15 ms 30 ms ↺ 0.50×
A4 nginx exclude GET, count_over_time 1h 19 ms 22 ms 35 ms 0.63×
A5 drilldown exclude, volume by detected_level 1h 41 ms 40 ms 33 ms 1.21×
A6 nginx include ends on a literal, log query 1h 18 ms 15 ms 25 ms 0.60×
A7 nginx exclude ends on a literal, log query 1h 20 ms 22 ms 11 ms 2.00×
A8 nginx include two literals, count_over_time 1h 23 ms 23 ms 32 ms 0.72×
A9 nginx include two literals, ends on a literal, count_over_time 1h 21 ms 22 ms 29 ms 0.76×
P1 logs, json, level equals 1h 143 ms 140 ms 107 ms 1.31×
P2 logs, logfmt, level not equal 1h 32 ms 32 ms 40 ms 0.80×
P3 logs, json, level existence 1h 132 ms 134 ms 87 ms 1.54×
P4 logfmt count by level 1h 32 ms 32 ms 45 ms 0.71×
P5 json count by level 1h 95 ms 89 ms 185 ms 0.48×
P6 json count, level filter 1h 18 ms 15 ms 167 ms 0.09×
P7 json rate by level, level regexp 1h 17 ms 19 ms 190 ms 0.10×
P8 json count by a name that is no stream label 1h 97 ms 97 ms 158 ms 0.61×
P9 instant logfmt by level instant 9 ms 6 ms 10 ms 0.60×
E1 json duration_ms sum by service 1h 101 ms 87 ms 176 ms 0.49×
E2 json duration_ms p95 by service 1h 268 ms 263 ms 328 ms 0.80×
E3 logfmt latency_ms rate, production 1h 661 ms 517 ms 861 ms 0.60×
E4 logfmt size_bytes max by service 1h 49 ms 50 ms 186 ms 0.27×
E5 json duration_ms sum, instant instant 40 ms 55 ms 31 ms 1.77×
E6 logfmt ttl unit string (Loki 400) 1h 400 400 400 —
E7 json hexadecimal trace_id (Loki 400) 1h 400 400 400 —
E8 json duration_ms sum by service, namespace prod 1h 218 ms 197 ms 618 ms 0.32×
E9 logfmt latency_ms sum by service, namespace prod 1h 307 ms 312 ms 618 ms 0.50×
G1 logfmt amount sum_over_time, no filter 1h 47 ms 45 ms 55 ms 0.82×
G10 logfmt latency_ms sum_over_time, production 1h 475 ms 502 ms 902 ms 0.56×
G11 logfmt amount sum_over_time, instant instant 7 ms 7 ms 12 ms 0.58×
G12 json duration_ms sum_over_time, instant instant 38 ms 36 ms 28 ms 1.29×
G2 json duration_ms sum_over_time, no filter 1h 89 ms 86 ms 168 ms 0.51×
G3 logfmt amount sum_over_time, error filter 1h 39 ms 39 ms 54 ms 0.72×
G4 json duration_ms sum_over_time, error filter 1h 88 ms 89 ms 155 ms 0.57×
G5 logfmt amount sum by rate 1h 47 ms 43 ms 65 ms ↺ 0.66×
G6 json duration_ms sum by rate 1h 94 ms 96 ms 172 ms 0.56×
G7 duration() of a unit string 1h 65 ms 64 ms 39 ms 1.64×
G8 bytes() of a number 1h 64 ms 63 ms 38 ms 1.66×
G9 json duration_ms max_over_time 1h 133 ms 129 ms 1.46 s 0.09×
W1 sum without pod, max_over_time 1h 79 ms 58 ms 131 ms 0.44×
W2 sum without pod, sum_over_time 1h 45 ms 47 ms 110 ms 0.43×
W3 sum without pod, rate 1h 143 ms 141 ms 109 ms 1.29×
W4 max without pod, max_over_time 1h 46 ms 51 ms 129 ms 0.40×
W5 sum without pod, max_over_time, instant instant 10 ms 8 ms 16 ms 0.50×
W6 sum without pod, rate, instant instant 8 ms 8 ms 15 ms 0.53×
W7 topk 5 of sum without pod 1h 49 ms 50 ms 117 ms 0.43×
W8 sum without pod, count_over_time (production) 1h 936 ms 890 ms 0 —
W9 label_replace control, instant instant 6 ms 6 ms 78 ms 0.08×
U1 logfmt amount rate 1h 54 ms 42 ms 59 ms 0.71×
U2 json duration_ms rate 1h 89 ms 89 ms 159 ms 0.56×
U3 json duration_ms rate, error filter 1h 118 ms 116 ms 201 ms 0.58×
U4 logfmt latency_ms rate, production 1h 364 ms 386 ms 938 ms 0.41×
U5 logfmt amount rate, instant instant 7 ms 11 ms 13 ms 0.85×
U6 json duration_ms rate, instant instant 14 ms 12 ms 26 ms 0.46×
U7 json duration_ms sum_over_time, instant instant 16 ms 14 ms 26 ms 0.54×
U8 logfmt amount rate by service (grouped) 1h 44 ms 47 ms 65 ms 0.72×
U9 logfmt amount rate_counter (folded per series) 1h 317 ms 306 ms 338 ms 0.91×
U1 json duration_ms sum by service 1h 96 ms 91 ms 257 ms 0.35×
U10 instant sum_over_time unwrap instant 33 ms 38 ms 30 ms 1.27×
U11 instant max_over_time missing label instant 6 ms 5 ms 20 ms 0.25×
U12 json duration_ms over every service 1h 250 ms 247 ms 955 ms 0.26×
U13 logfmt latency_ms over every service 1h 531 ms 538 ms 1.69 s 0.32×
U14 duration() of a unit string 1h 60 ms 61 ms 58 ms 1.05×
U2 json duration_ms max by service 1h 98 ms 96 ms 1.14 s 0.08×
U3 json duration_ms sliding sum 1h 90 ms 86 ms 258 ms 0.33×
U4 logfmt size_bytes sum by service 1h 50 ms 47 ms 56 ms 0.84×
U5 logfmt ttl unit string (no sample) 1h 59 ms 86 ms 61 ms 1.41×
U6 missing label, no parser 1h 21 ms 21 ms 99 ms 0.21×
U7 missing label, ungrouped sum 1h 19 ms 19 ms 104 ms 0.18×
U8 rate of an unwrapped label 1h 88 ms 89 ms 240 ms 0.37×
U9 bytes() conversion 1h 59 ms 60 ms 60 ms 1.00×

↺ Loki cold taken from the first set that ran the same query; Loki's cache answered this one.

Why these shapes
  • internal/translator/translator.go → 129: control/bytes_over_time by app, control/instant json filtered, control/instant json volume, control/instant ungrouped rate, control/json rate by level (no filter, no drop), control/json volume no filters, control/logfmt count by level, filter, drop, control/logfmt volume …
  • internal/proxy/vector_matching.go → 31: control/stream-label count by app, unwrap-gate/G1 logfmt amount sum_over_time, no filter, unwrap-gate/G10 logfmt latency_ms sum_over_time, production, unwrap-gate/G11 logfmt amount sum_over_time, instant, unwrap-gate/G12 json duration_ms sum_over_time, instant, unwrap-gate/G2 json duration_ms sum_over_time, no filter, unwrap-gate/G3 logfmt amount sum_over_time, error filter, unwrap-gate/G4 json duration_ms sum_over_time, error filter …
  • internal/proxy/range_metric_compat.go → 21: control/bytes_over_time by app, control/instant json volume, control/instant ungrouped rate, control/json volume no filters, control/stream-label count by app, control/topk rate by app, control/ungrouped rate, json-filter-pushdown/A explore volume, 2 filters, drop …
  • control smoke → 8: control/drilldown service landing, control/instant ungrouped rate, control/json rate by level (no filter, no drop), control/json volume no filters, control/logs plain, control/logs | json, control/stream-label count by app, control/topk rate by app

base 2f520141a7 → PR 5e4d72bba0 · 4 runs (first is cold), warm p50 · ranges 1h · noise ±30% and 100 ms · VictoriaLogs restarts 0→0 · 19.9 min. Same runner, one fresh stack, targets interleaved per request. How to read and reproduce: bench/ab/README.md.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

✅ Visual smoke: passed with warnings

Base 2f520141a7 vs PR 6042bc3047, Grafana through the proxy builds and Loki on one fresh stack; run.
Loaded again, once and more patiently, after a first-pass difference or an unsettled page: explore-max-by-count-over-time-sliding 15m, explore-max-by-count-over-time-sliding 1h, explore-max-by-count-over-time-sliding 6h.
Core set: 5 capture(s) at 1h. Detailed set: 29 entries (explore-max-by-count-over-time-sliding, explore-metric-plain, explore-A, explore-B, explore-C, explore-E, explore-F, explore-logs-json, ...). 37 capture(s), 15m/1h/6h, 24h and 7d stay out of PR runs.
Trimmed to the 1h range for the capture budget: explore-A, explore-B, explore-C, explore-E, explore-F, explore-logs-json, explore-logfmt-metric, explore-logs-extracted, explore-extracted-metric, explore-extracted-filter-metric, explore-extracted-group-metric, explore-logs-plain-level, explore-plain-level-metric, explore-logs-line-filter-or, explore-metric-line-filter-or, explore-unwrap-sum, explore-unwrap-unit-string, explore-unwrap-missing-label, explore-unwrap-conversion-error, explore-unwrap-rate-sum, explore-empty-pattern-filter, explore-pattern-filter-anchored, explore-unwrap-quantile-over-time, explore-json-error-filter-metric, explore-unwrap-max-over-time-end-sample, explore-unwrap-first-last-under-sum, dd-field-service_version, dd-field-pipeline, explore-sum-without-rate, explore-topk-sum-without, explore-sum-without-count.
Not run (capture budget): explore-sum-without-count, explore-topk-sum-without, explore-sum-without-rate, dd-field-pipeline, dd-field-service_version.

Fix proof

Captures of the exact cases this pull request adds or fixes (registry case, base | PR | Loki). The verdict compares the PR with Loki where the base did not match. The gate fails a case unless one capture is fixed or improved, or, beyond the Loki data window, the base failed where Loki and the PR both answered with data (the label does not excuse it; only visual: none on the case does).

semantics/non-sum-vector-aggregation-unreduced: 🟡 improved

page range base = PR (data) vs Loki (PR) verdict
explore-max-by-count-over-time-sliding 15m differs (1) 1 difference(s), 1 on base too 🟡 improved
explore-max-by-count-over-time-sliding 1h differs (1) 1 difference(s), 1 on base too 🟡 improved
explore-max-by-count-over-time-sliding 6h differs (1) n/a (beyond the Loki window) ⚪ undecided
explore-max-by-count-over-time-sliding-15m: improved

explore-max-by-count-over-time-sliding-15m

explore-max-by-count-over-time-sliding-1h: improved

explore-max-by-count-over-time-sliding-1h

explore-max-by-count-over-time-sliding-6h: undecided

explore-max-by-count-over-time-sliding-6h

Regression check

page range set base = PR (data) vs Loki (PR) pixel diff result
dd-fields 1h core ✅ identical identical (4 explained, 1 history-dependent) 0.11% ✅
dd-landing 1h core ✅ identical identical (3 explained) 2.16% ✅
dd-service-logs 1h core ✅ identical identical (3 explained, 1 history-dependent) 0.11% ✅
explore-logs-plain 1h core ✅ identical identical (1 explained) 0.07% ✅
explore-metric-plain 1h core ✅ identical identical (1 explained) 0.04% ✅
explore-A 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-B 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-C 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-E 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-F 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-empty-pattern-filter 1h detailed ✅ identical 1 difference(s), 1 on base too 0.07% ✅
explore-extracted-filter-metric 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-extracted-group-metric 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-extracted-metric 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-json-error-filter-metric 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-logfmt-metric 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-logs-extracted 1h detailed ✅ identical 1 difference(s), 1 on base too 0.07% ✅
explore-logs-json 1h detailed ✅ identical 1 difference(s), 1 on base too 0.07% ✅
explore-logs-line-filter-or 1h detailed ✅ identical 1 difference(s), 1 on base too 0.07% ✅
explore-logs-plain-level 1h detailed ✅ identical 1 difference(s), 1 on base too; 1 explained 0.07% ✅
explore-max-by-count-over-time-sliding 15m detailed differs (1): query ('max by (service_name) (count_over_time({service_name 1 difference(s), 1 on base too 1.64% ✅ improved (closer to Loki)
explore-max-by-count-over-time-sliding 1h detailed differs (1): query ('max by (service_name) (count_over_time({service_name 1 difference(s), 1 on base too 1.78% ✅ improved (closer to Loki)
explore-max-by-count-over-time-sliding 6h detailed differs (1): query ('max by (service_name) (count_over_time({service_name n/a (beyond the Loki window) 1.97% ✅ improved (closer to Loki)
explore-metric-line-filter-or 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-metric-plain 15m detailed ✅ identical identical (1 explained) 0.04% ✅
explore-metric-plain 6h detailed ✅ identical n/a (beyond the Loki window) 0.04% ✅
explore-pattern-filter-anchored 1h detailed ✅ identical 1 difference(s), 1 on base too 0.07% ✅
explore-plain-level-metric 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-unwrap-conversion-error 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-unwrap-first-last-under-sum 1h detailed ✅ identical 2 difference(s), 2 on base too 0.04% ✅
explore-unwrap-max-over-time-end-sample 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-unwrap-missing-label 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-unwrap-quantile-over-time 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-unwrap-rate-sum 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-unwrap-sum 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-unwrap-unit-string 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
tail-json-filter live detailed ✅ identical entries in the window: PR 1978, Loki 653, in both 653 0.15% ✅

Gate: a base-vs-PR data difference passes when the PR is closer to Loki, and fails when the PR diverges from Loki or Loki cannot decide it (at a range Loki does not hold, a difference passes when the same page proved it closer to Loki at a shorter range; otherwise label the pull request visual-change-expected to accept an intended change). Also failing: a panel empty on the PR but not on the base, an error banner, panel error, error answer or new "No data" panel on the PR, a PR side that never settled, a difference that did not reproduce on the recapture. Pixel differences only warn.

explore-max-by-count-over-time-sliding-6h: no Loki data at this range; the same differences match Loki at a shorter range (1): query ('max by (service_name) (count
  • no Loki data at this range; the same differences match Loki at a shorter range (1): query ('max by (service_name) (count_over_time({service_name="api-gateway"}[5m]))', 'range', '17: series sets differ: 12038 only left ['(\'A\', \'\', \'{"app":

explore-max-by-count-over-time-sliding-6h

Core set montages (5): base | PR | Loki

dd-fields-1h

dd-fields-1h

dd-landing-1h

dd-landing-1h

dd-service-logs-1h

dd-service-logs-1h

explore-logs-plain-1h

explore-logs-plain-1h

explore-metric-plain-1h

explore-metric-plain-1h

All montages of this run: pr-visuals/pr-734.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

PR Quality Report

Compared against base branch main.

Coverage and tests

Signal Base PR Delta
Test count 12835 13704 869
Coverage 88.5% 88.5% 0.0% (stable)

Compatibility

Track Base PR Delta
Loki API 100.0% 11/11 (100.0%) 0.0% (stable)
Logs Drilldown 100.0% 17/17 (100.0%) 0.0% (stable)
VictoriaLogs 100.0% 11/11 (100.0%) 0.0% (stable)

Performance smoke

Lower CPU cost (ns/op) is better. Lower benchmark memory cost (B/op, allocs/op) is better. Higher throughput is better. Lower load-test memory growth is better. Benchmark rows are medians from repeated samples.

Signal Base PR Delta
QueryRange cache-hit CPU cost 2101.0 ns/op 2070.0 ns/op -1.5% (stable)
QueryRange cache-hit memory 1240.0 B/op 1240.0 B/op 0.0% (stable)
QueryRange cache-hit allocations 27.0 allocs/op 27.0 allocs/op 0.0% (stable)
QueryRange cache-bypass CPU cost 2408.0 ns/op 2359.0 ns/op -2.0% (stable)
QueryRange cache-bypass memory 1281.0 B/op 1282.0 B/op +0.1% (stable)
QueryRange cache-bypass allocations 27.0 allocs/op 27.0 allocs/op 0.0% (stable)
Labels cache-hit CPU cost 597.0 ns/op 577.7 ns/op -3.2% (stable)
Labels cache-hit memory 48.0 B/op 48.0 B/op 0.0% (stable)
Labels cache-hit allocations 3.0 allocs/op 3.0 allocs/op 0.0% (stable)
Labels cache-bypass CPU cost 952.3 ns/op 936.3 ns/op -1.7% (stable)
Labels cache-bypass memory 396.0 B/op 396.0 B/op 0.0% (stable)
Labels cache-bypass allocations 14.0 allocs/op 14.0 allocs/op 0.0% (stable)

State

  • Coverage, compatibility, and sampled performance are reported here from the same PR workflow.
  • This is a delta report, not a release gate by itself. Required checks still decide merge safety.
  • Performance is a smoke comparison, not a full benchmark lab run.
  • Delta states use the same noise guards as the quality gate (percent + absolute + low-baseline checks), so report labels match merge-gate behavior.

… group on query_range

A window that is not the step is evaluated by the proxy's range evaluator from
the first stats pipe of the translation alone, which holds the series identity
and not the outer operator, so max(count_over_time(...[5m])) returned one series
per stream where Loki returns one. The query's vector aggregations are now
applied to the series the evaluator returns, per step, the way Loki's
VectorAggEvaluator folds them (by keeps the listed labels, none keeps none;
count is the number of series, avg their mean, min and max the extreme), and
max_query_series is checked on that answer.

A nested aggregation (sum(max(rate(...)))) also evaluated a count of lines
where the query asked for a rate, because only the outermost operator was
stripped to find the range function.
…ion-unreduced, with its visual fix proof page
…a nested without is answered as before

count by (route) over | logfmt route on a sliding window folded the series
into one without a label, because the window evaluator drops the labels a parser
extracts unless the proxy folds a without. The fold of the vector aggregations
now asks for them the same way.

The nested aggregations are read down to the range function except under a
without that is not the one the proxy folds (the outermost): that shape is
answered as it was, so the without fold keeps its scope.

The goldens and open-shape pins that recorded the per-stream series are
updated to the folded answers.
…, record the sliding figures

The changelog entry and the registry rows say what the fold covers: chains of
sum, count, min, max and avg without a without, and a grouping on a parsed
label. A without nested under another aggregation stays with
semantics/without-nested-under-aggregation; the query_range rate case of
semantics/without-over-inner-aggregation-collapses is fixed, and the sliding
figure of semantics/unwrap-extraction-list-series-identity is recorded.
@szibis
szibis force-pushed the fix/non-sum-vector-aggregation-unreduced branch from a47440d to 63e6190 Compare October 23, 2026 09:20
@github-actions github-actions Bot added size/XL Extra large change scope/translator LogQL translator and removed size/XL Extra large change labels Oct 23, 2026
@szibis
szibis marked this pull request as ready for review October 23, 2026 09:21
…fferential leaves no goroutines behind

The differential built one proxy per scenario (6,336) and closed only the stub
backend. Every proxy kept two caches running and the scenario's own cache, two
goroutines each: 38,016 goroutines stayed alive and the internal/proxy test
binary peaked at 2.8 GB against 1.8 GB on main. With the proxy shut down and the
three caches closed, the test alone peaks at 91 MB and the package at main's
level.
@szibis
szibis merged commit 6170ae9 into main Oct 23, 2026
76 checks passed
@szibis
szibis deleted the fix/non-sum-vector-aggregation-unreduced branch October 23, 2026 11:41
szibis added a commit that referenced this pull request Oct 26, 2026
The README led with measurements from 2026-05/06 (a Drilldown bench with Loki
at 12 GiB, proxy v1.50.0 throughput tables) that read as the current position,
and docs/compatibility-coverage.md still showed 15/35 implemented endpoints
(current registry: 23/35).

- README: new "Quality and performance vs Loki (current)" section with the
  registry scores (23/35 routed, 19/35 compared against Loki, 12/35 proven),
  the ranked-difference counts (88 rows: 69 open, 12 documented, 5 owner-kept,
  2 upstream), the cold first-request count (54 pairs slower than Loki), the
  repeated and refreshed window numbers of #862, the load test, and the shapes
  that are still slower or fail where Loki answers. The 2026-06-05 one-glance
  comparison and the v1.50.0 throughput section are labelled historical with
  their conditions.
- docs/performance.md: "Current position versus Loki" with the sources and
  method for each number; the 2026-06-05 metadata table is dated.
- docs/benchmarks.md, docs/honest-tldr.md: historical banners with the
  conditions; methodology and reproduction steps are kept.
- docs/KNOWN_ISSUES.md: numeric tenant routing (#846), kept stats buckets
  (#862), a "where the proxy is slower" paragraph, and the max/min/count/avg
  fold (#734) and delete 404 (#855) under the closed gaps.
- docs/real-window-compatibility-gaps.md: marked as a historical record and
  linked to the current summary.
- docs/compatibility-coverage.md: regenerated by conformance/scripts/coverage_map.py.
szibis added a commit that referenced this pull request Oct 27, 2026
… the timeout

perf-smoke on #746 was cancelled at its 30-minute limit. Of the 1658 s the
shapes ran, W8 (sum without (pod) (count_over_time({env="production"}[1m])))
took 1019 s: Loki timed out at 90 s on all 4 runs of the first pass and on all
7 runs of the confirm pass the PR's speedup on W8 (0.58 s against 0.80 s)
started. Loki timed out on W8 in the perf-smoke runs of #739 and #734 as well
(they had no confirm pass on W8). Under a `without`, Loki keeps the structured metadata of the
OTel-instrumented streams (trace_id, span_id) in the series, one series per
line; in the daily run its 1h answer filled 80.9 MB of results cache against
the 41 series the proxy answers.

W8 now uses the selector the nested-aggregation set uses ({env="production",
instrumentation!="otel", app!="otel-collector"}), as W1-W4 moved to
`| logfmt amount` for the same reason. It still measures the stream-label
`sum without` fold. The other 172 selected shapes ran at about 5 s each, so
the selection of 20 sets should finish in about 18 to 20 minutes with the
stack and seed.
szibis added a commit that referenced this pull request Oct 27, 2026
…ant queries and tumbling windows, like Loki (#746)

* fix: a vector aggregation over another one keeps every level's own by

Loki folds the innermost level first and each level keeps exactly its own
grouping labels (pkg/logql/evaluator.go VectorAggEvaluator.Next). The native
routes (an instant query, a tumbling window) dropped the outer by of a nested
aggregation and answered one series without labels. Each level is now a stats
pipe with its own by; a level above the first keeps only labels the previous
one kept, and an innermost without is folded over the series rows level by
level in the proxy, under the without marker main's fold uses (the outermost
level's operator, series rows, the limit on the folded answer).

On a sliding window the proxy evaluates the first stats pipe, the series of the
innermost level, and folds every level over them: the fold of the vector
aggregations also takes a nest of sums and a static without level, the
evaluator names the series by the innermost level's labels (they took the
outermost level's by position), and a nest spelled sum(...) by (l) reads its
range function from the text as written.

The shapes the open without rows pinned (max(sum without ...), sum by (ns)
(sum without ...), sum without (sum by ...)) now answer Loki's series and
values; their pinned answers are updated, and the two rows are fixed.

* test: nested vector aggregations against Loki on an instant query and a tumbling window

* docs(registry): state the VictoriaLogs versions the nested aggregation chain was checked on

* chore(conformance): visual fix proof page for nested-aggregation-outer-by-native-routes

* chore(conformance): record #746 as the fix of nested-aggregation-outer-by-native-routes

* test(bench): A/B set for nested vector aggregations

Adds the nested-aggregation set to bench/ab/shapes.json: 15 shapes of a
vector aggregation over another one, on the generator's data. N1-N8 are range
queries over tumbling windows (range == step via $__auto): two-level nests of
sum, max, min, avg and count, a three-level chain, a `| json` inner level with
an explicit extraction, a `| logfmt` inner level, and a `without` over a level
that names its labels. N9-N13 are the same forms as instant queries, N13 with
an innermost `without`. N14-N15 are sliding windows (range 5m over the step),
one of them a nest of sums.

An innermost `without` over a tumbling window has no range shape: its series
rows are folded in the proxy exactly as the single-level `avg without (pod)`
already is on main (about 1 s at 6h and a 10 s step on both builds), so a shape
there would only repeat that cost against a base that answered the wrong thing.

The selector leaves out the OTel-instrumented streams and the otel-collector:
Loki carries their structured metadata (trace_id, span_id) in the series, so an
inner level grouped by pod counts per line there, a series identity difference
that predates the nest fix.

Checked on a 90 minute seed against Loki first (all 200, non-empty): origin/main
differs from Loki on N1-N13 and N15 (one series, or the wrong series count);
this branch is the same as Loki on all 15.

The registry item's `area` now names the source files with their paths
(internal/translator/nested_vector_chain.go, internal/proxy/nested_vector_fold.go,
internal/proxy/vector_agg_fold.go), so selection.py ties a change to them to the
set; generated performance evidence regenerated.

* perf: VictoriaLogs folds a tumbling window's `without`, and the proxy's fold decodes once

A nest of vector aggregations with an innermost `without` kept a row per stream
and bucket and folded every level in the proxy: `avg without (pod)
(count_over_time({env="production"}[10s]))` over 6h at a 10 s step took 0.8 to
1.2 s on main, and the same `without` under `min by (app)` 1.44 s on this branch.

- A `<op> without (...)` over a tumbling window (range equal to the step), alone
  or as the innermost level of a nest, is cut in VictoriaLogs when the excluded
  labels are plain names (letters and digits; not level, detected_level,
  service_name or a label the service name is derived from; no custom field
  mapping renames them): the labels of a series are the pairs of its `_stream`
  text, so the pairs are cut out of it between the stats pipe that counts a
  series and the one that combines the series now sharing a stream, and the
  levels above read their labels as columns of the pipes
  (translator.PushDownWithout). A parser, a sliding window, an instant query and
  every other name keep the proxy's fold. Measured on a seeded stack, 6h, 10 s
  step: the single-level form 0.8 to 1.2 s on main, 25 to 70 ms warm and 175 ms
  cold here; the nest 14 ms against Loki's 45 ms (178 ms cold, Loki 1.26 s).
- The proxy's fold of series rows (a sliding window, an instant query, a parser,
  the names above) decodes the answer once, folds every level over the series
  and encodes it once (typed_fold.go), where it parsed and wrote the JSON again
  at every level: 47 ms and 89k allocations against 164 ms and 3.1M on 200
  series of 2160 points. The sliding `sum by (app) (max by (app, pod)
  (count_over_time(...[5m])))` is 28% (6h) and 66% (1h) faster.
- A query with one aggregation no longer parses for a nest: the benchmarks of
  the control queries show the allocations of main (translate: 38 -> 43 per
  query before, 38 now; handler: +17 to +34 before, identical now).

Differential: TestWithoutPushdownMatchesFoldAndLoki (320 random label sets, a
label on some streams only, excluded names the cut takes and does not take,
values with quotes, commas, backslashes and pair-like text) holds both routes to a
reference of Loki's evaluator and to each other; TestWithoutPushdownDirectedStreams
covers streams that differ only by the excluded label, first, in the middle and
last of the pairs, for every operator and both functions;
TestTypedFoldMatchesGenericFold holds the typed fold to the generic one.
TestCompat_NestedVectorAggregation gains single-level `without` forms; it and the
controls pass on VictoriaLogs v1.49.0, v1.50.0, v1.51.1, v1.52.0 and v1.53.0
(979 subtests on the floor and the newest, 391 on the others).

TestWithoutFoldRowBudgetIsNotASeriesLimit no longer expects the row-limit error
for a tumbling `sum without` of count_over_time: VictoriaLogs folds it and the
two series it answers fit, as Loki answers it.

* perf: the typed fold reads the answer with a scanner, not a document tree

Under 32 concurrent clients the proxy's peak RSS with the typed fold was 3.53,
3.78, 3.93 and 3.73 GB in four runs against 3.17, 2.88, 3.16 and 3.20 GB for
main (same stack, same queries, ABBA). The heap profile taken at each of those
peaks held 1.5 GB in fastjson.(*cache).getValue under decodeTypedBody: a
document tree of a sliding nest's answer (millions of samples, one allocated
value per number, string and array), alive while the fold ran, on top of the
evaluator's own matrices that main also holds.

decodeTypedBody now scans the bytes into the series directly (16 bytes a
sample) and falls back to the generic fold for anything it does not read. On
200 series of 2160 points: 36 ms, 28 MB and 7.3k allocations against 47 ms,
67 MB and 89k for the tree and 143 ms, 99 MB and 3.1M for the generic fold.
Re-run, same method, eight slots ABBA: peak RSS 2.87, 3.06, 2.93 and 2.81 GB for
the branch against 2.93, 2.88, 3.32 and 2.91 GB for main, and 2672 requests
against 2501 in 150 s per slot.

TestTypedScannerReadsWhatTheDocumentTreeReads holds the scanner to
encoding/json's tree over 800 generated bodies (indented or compact, extra keys
and nested stats, escapes in labels, string and number values, null and short
samples, result before resultType).

* fix: the typed fold reads sample timestamps without package unsafe

gosec v2.29.0 (Security / static) flagged G103 at typed_fold.go:220, where
the scanner handed strconv.ParseFloat an unsafe.String over the body bytes
to avoid a string conversion per number. main has no use of unsafe and
gosec is clean on it.

The scanner now reads a whole-second timestamp (up to 15 digits, which a
float64 holds exactly) straight from the bytes, and passes anything else to
strconv.ParseFloat through a plain string conversion that does not escape.
BenchmarkFoldWithoutMatrix/typed, GOMAXPROCS=1, 20 iterations x3: 35.4-36.9
ms with unsafe.String, 42.6-43.6 ms with only the string conversion, 29.6-33.5
ms with the digit fast path; 28.0 MB and 7310 allocations in all three.

TestScannerNumberMatchesParseFloat holds the scanner to strconv.ParseFloat,
bit for bit, on integers at and past the 15-digit edge, fractions, signs,
exponents and malformed numbers.

* test(bench): the nest fix proof pages measure the nests, not OTel series identity

explore-nested-aggregation-outer-by. On visual-smoke for 0f0ece6 the PR
answered the 15 service_name series Loki answers (1365 and 5415 non-zero
points on both at 15m and 1h; main answered one series {}), and the first
differing series was checkout-svc: 2 against Loki's 1 at 15m, 5 against 1 at
1h. checkout-svc is one of the generator's OTel-instrumented services, whose
lines carry trace_id and span_id as structured metadata. Loki puts structured
metadata into a sample's labels (pkg/logql/log/metrics_extraction.go:104,109),
so count_over_time there is one series per line, each counting 1, and the avg
by (service_name, level) over them is 1; VictoriaLogs counts the stream. That
is a series identity difference the single-level query has on main too, and
the bench/ab nested-aggregation set leaves those streams out for the same
reason. The page now uses the set's selector: {env="production",
instrumentation!="otel", app!="otel-collector"}. main still collapses the nest
on it (its translation ends in an ungrouped `stats min(...)` with either
selector) and the PR keeps `stats by (service_name)`.

explore-without-nested-under-aggregation. `sum without (service_name)` merges
no series on the generator's data (every stream's app equals its
service_name), so main's answer, the max over the unmerged streams, was
already Loki's: base and PR were identical. The page now asks
max(sum without (pod) (count_over_time({app="payment-service"}[5m]))): pods
change every generator batch (10 s apart), so a 5m window sums several pods per
level, and main reads the max over single pods. Through the handlers on
emulated VictoriaLogs, main answers 5 where Loki's evaluation of the same rows
is 20 (instant) and the PR 20; on query_range main answers two series
{ns=x},{ns=y} where Loki answers one {} = 8,16,20. A tumbling $__auto window
would not show it at 15m and 1h: their steps hold one batch each. The
payment-service streams carry no structured metadata, which also leaves out
the per-line OTel series that kept Loki from settling the old query at 1h.

* test(bench): W8 leaves out the OTel streams so Loki answers it within the timeout

perf-smoke on #746 was cancelled at its 30-minute limit. Of the 1658 s the
shapes ran, W8 (sum without (pod) (count_over_time({env="production"}[1m])))
took 1019 s: Loki timed out at 90 s on all 4 runs of the first pass and on all
7 runs of the confirm pass the PR's speedup on W8 (0.58 s against 0.80 s)
started. Loki timed out on W8 in the perf-smoke runs of #739 and #734 as well
(they had no confirm pass on W8). Under a `without`, Loki keeps the structured metadata of the
OTel-instrumented streams (trace_id, span_id) in the series, one series per
line; in the daily run its 1h answer filled 80.9 MB of results cache against
the 41 series the proxy answers.

W8 now uses the selector the nested-aggregation set uses ({env="production",
instrumentation!="otel", app!="otel-collector"}), as W1-W4 moved to
`| logfmt amount` for the same reason. It still measures the stream-label
`sum without` fold. The other 172 selected shapes ran at about 5 s each, so
the selection of 20 sets should finish in about 18 to 20 minutes with the
stack and seed.

* test: a max over a sum without on a sliding count matches Loki

The visual fix proof page of semantics/without-nested-under-aggregation now
asks max(sum without (pod) (count_over_time({app="payment-service"}[5m]))).
TestWithoutNestedAggregationMatchesLoki held max over a without only for the
unwrap rate; it now also holds this shape, range and instant, to Loki's
evaluation of the emulator's rows (one series {} = 8,16,20; instant 20). On
origin/main the same request answers {ns=x}=2,3,5 {ns=y}=2,4,5 and, at an
instant, 5: the max over single pods.

---------

Co-authored-by: szibis <szibis@users.noreply.github.com>
szibis added a commit that referenced this pull request Oct 27, 2026
The README led with measurements from 2026-05/06 (a Drilldown bench with Loki
at 12 GiB, proxy v1.50.0 throughput tables) that read as the current position,
and docs/compatibility-coverage.md still showed 15/35 implemented endpoints
(current registry: 23/35).

- README: new "Quality and performance vs Loki (current)" section with the
  registry scores (23/35 routed, 19/35 compared against Loki, 12/35 proven),
  the ranked-difference counts (88 rows: 69 open, 12 documented, 5 owner-kept,
  2 upstream), the cold first-request count (54 pairs slower than Loki), the
  repeated and refreshed window numbers of #862, the load test, and the shapes
  that are still slower or fail where Loki answers. The 2026-06-05 one-glance
  comparison and the v1.50.0 throughput section are labelled historical with
  their conditions.
- docs/performance.md: "Current position versus Loki" with the sources and
  method for each number; the 2026-06-05 metadata table is dated.
- docs/benchmarks.md, docs/honest-tldr.md: historical banners with the
  conditions; methodology and reproduction steps are kept.
- docs/KNOWN_ISSUES.md: numeric tenant routing (#846), kept stats buckets
  (#862), a "where the proxy is slower" paragraph, and the max/min/count/avg
  fold (#734) and delete 404 (#855) under the closed gaps.
- docs/real-window-compatibility-gaps.md: marked as a historical record and
  linked to the current summary.
- docs/compatibility-coverage.md: regenerated by conformance/scripts/coverage_map.py.
szibis added a commit that referenced this pull request Oct 27, 2026
The README led with measurements from 2026-05/06 (a Drilldown bench with Loki
at 12 GiB, proxy v1.50.0 throughput tables) that read as the current position,
and docs/compatibility-coverage.md still showed 15/35 implemented endpoints
(current registry: 23/35).

- README: new "Quality and performance vs Loki (current)" section with the
  registry scores (23/35 routed, 19/35 compared against Loki, 12/35 proven),
  the ranked-difference counts (88 rows: 69 open, 12 documented, 5 owner-kept,
  2 upstream), the cold first-request count (54 pairs slower than Loki), the
  repeated and refreshed window numbers of #862, the load test, and the shapes
  that are still slower or fail where Loki answers. The 2026-06-05 one-glance
  comparison and the v1.50.0 throughput section are labelled historical with
  their conditions.
- docs/performance.md: "Current position versus Loki" with the sources and
  method for each number; the 2026-06-05 metadata table is dated.
- docs/benchmarks.md, docs/honest-tldr.md: historical banners with the
  conditions; methodology and reproduction steps are kept.
- docs/KNOWN_ISSUES.md: numeric tenant routing (#846), kept stats buckets
  (#862), a "where the proxy is slower" paragraph, and the max/min/count/avg
  fold (#734) and delete 404 (#855) under the closed gaps.
- docs/real-window-compatibility-gaps.md: marked as a historical record and
  linked to the current summary.
- docs/compatibility-coverage.md: regenerated by conformance/scripts/coverage_map.py.
szibis added a commit that referenced this pull request Oct 27, 2026
…#872)

* docs: state the current quality and performance position against Loki

The README led with measurements from 2026-05/06 (a Drilldown bench with Loki
at 12 GiB, proxy v1.50.0 throughput tables) that read as the current position,
and docs/compatibility-coverage.md still showed 15/35 implemented endpoints
(current registry: 23/35).

- README: new "Quality and performance vs Loki (current)" section with the
  registry scores (23/35 routed, 19/35 compared against Loki, 12/35 proven),
  the ranked-difference counts (88 rows: 69 open, 12 documented, 5 owner-kept,
  2 upstream), the cold first-request count (54 pairs slower than Loki), the
  repeated and refreshed window numbers of #862, the load test, and the shapes
  that are still slower or fail where Loki answers. The 2026-06-05 one-glance
  comparison and the v1.50.0 throughput section are labelled historical with
  their conditions.
- docs/performance.md: "Current position versus Loki" with the sources and
  method for each number; the 2026-06-05 metadata table is dated.
- docs/benchmarks.md, docs/honest-tldr.md: historical banners with the
  conditions; methodology and reproduction steps are kept.
- docs/KNOWN_ISSUES.md: numeric tenant routing (#846), kept stats buckets
  (#862), a "where the proxy is slower" paragraph, and the max/min/count/avg
  fold (#734) and delete 404 (#855) under the closed gaps.
- docs/real-window-compatibility-gaps.md: marked as a historical record and
  linked to the current summary.
- docs/compatibility-coverage.md: regenerated by conformance/scripts/coverage_map.py.

* docs: mark unverified Loki cold times and drop unsourced claims

Review of the previous commit: the 7d unwrap rows come from the 2026-10-08 run,
which recorded no Loki cache counters, so their Loki cold time is unverified;
say so beside the number. Remove registry ids that point at fixed correctness
cases from the slow-shape bullets, a microbenchmark clause with no source, and
a sentence about Loki's failures that the page's own conclusion contradicts.
Correct the #846 logging description and the true-cold counts (0 of 221 slower
than the previous build), and date the cold run by its review, not its merge.

* docs: the current-position sections name release 2.35.0, which ships #862

* feat: summarize the performance evidence versus Loki by workload

perf_evidence.py now writes a Summary versus Loki section into the performance
report, computed with the script's own rules over the latest saved run of each
shape and range: cold first request faster, on par and slower counts over the
comparable pairs (both sides 200, Loki's cache did not answer the cold run);
warm and refresh counts for runs that gave each set its own windows (none of
the saved runs did, and the report says so); results versus Loki; a table of
medians by workload; and the metadata calls with the pairs Loki's cache
answered counted apart. The rest of the report is unchanged.

* docs: README states the proxy against Loki in numbers, without history

The current section is now organised as better, on par and worse in numbers
(counts and medians generated by the performance report's summary), then the
read API quality counts. Dates, pull request numbers and before and after
history stay in the performance guide. The latency table is replaced by the
generated per-workload medians, the label browser table by the generated
metadata calls, and the translation overhead is re-measured; the loki-bench
throughput table is labelled historical.

* docs: drop dates and the release name from the README position sections

* docs: regenerate the published coverage map without the write endpoints

* docs: refresh the position against Loki with the 2026-10-27 daily run

Re-take every figure from the regenerated reports: first-request counts over
455 comparable pairs (213 faster, 164 on par, 78 slower), the workload and
metadata tables, 87 ranked differences, and the 165 rows above 2x Loki (55
first request, 110 repeat). Add the bounded-staleness label lists and the
nested aggregation fix, and state that rows above 2x Loki are defects.

* docs: README scope line uses the owner's wording for write endpoints

* docs: the README scope line says no other write endpoint is planned

---------

Co-authored-by: szibis <szibis@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fix scope/docs Documentation scope/proxy Proxy core scope/tests Tests scope/translator LogQL translator size/XL Extra large change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant