Skip to content

fix: sum, avg, max and min over time with unwrap answer Loki's window on query_range for any range - #740

Closed
szibis wants to merge 8 commits into
mainfrom
fix/unwrap-over-time-range-windows
Closed

szibis wants to merge 8 commits into
mainfrom
fix/unwrap-over-time-range-windows

Conversation

@szibis

@szibis szibis commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #727. This branch contains #727's commits. Merge #727 first; this PR is then rebased and its diff shrinks to its own two commits (48b6aa6, c4e30c5).

Summary

Gap semantics/unwrap-over-time-range-windows.

  • On query_range, when the range is not the step, sum/avg/max/min_over_time with unwrap computed each point from a single VictoriaLogs step bucket.
  • Loki uses the window (t-range, t] (pkg/logql/range_vector.go:148-189, functions at :371-425).
  • On sparse data even sample presence differed: with one line every 4.5 minutes at step 60s, Loki answers 61 samples for [5m] and 7 for [30s].

Fix: VictoriaLogs first, then the proxy.

  • What VictoriaLogs computes: for each series and anchored bucket, sum|max|min(__lvp_v) plus a sample count. A bucket is gcd(step, range) wide, or one step with the phase filter when range < step. The unwrap gate and the conversion-error counter ride along.
  • What the proxy computes: mergeUnwrapBuckets merges the buckets of each Loki window.
    • sum: the sum of the bucket sums.
    • avg: that sum divided by the window's sample count.
    • max/min: the extreme value, with NaN handled as Loki does.
    • A point exists only where the window holds a sample.
  • Ungrouped sum: an ungrouped sum of avg/max/min folds the per-stream windows. The per-stream buckets are not cut by -max-stats-query-series; Loki counts the folded answer.
  • Raw fallback: VictoriaLogs before v1.45 with an off-grid anchor, and queries needing more than 11,000 buckets, use the existing raw evaluator. On this route it now reads (t-range, t].
  • Unchanged: instant queries, offset, and range == step on a supported grid (fix: sum, avg, max and min over time with unwrap answer the sample at end on query_range, like Loki #727).

Tests

  • TestUnwrapOverTimeWindowsDifferential: 150 subtests failed before the fix. It covers:
    • VictoriaLogs ""/v1.52/v1.40;
    • no parser, logfmt and json;
    • the 4 functions;
    • sum by, per stream, sum, topk, sum without and a binary;
    • random dense and sparse rows, edge lines ±1ns, ranges longer than, shorter than and not a multiple of the step, and offsets.
  • TestUnwrapOverTimeSparseSamplePresence: the registry's sparse case, 12 failed before the fix.
  • TestUnwrapOverTimeRangeEmitsEndSample (from fix: sum, avg, max and min over time with unwrap answer the sample at end on query_range, like Loki #727) now also checks values.
  • Regression guards for the series-limit fold and for the residual shapes.
  • e2e vs Loki: TestCompat_UnwrapOverTimeRangeWindows.

Residuals, registered as open rows:

Pending before ready

  • e2e against Loki on VictoriaLogs v1.52.0 and v1.47.0.
  • A/B plus the control set, including sum(avg_over_time({many streams} | unwrap x [5m])) at 24h/7d. Per-stream buckets are folded there.
  • Visual capture, benchstat and the load test.

… end on query_range, like Loki

Loki evaluates a range query at start, start+step, ... up to and including end, each over (t-range, t]. These functions were read from VictoriaLogs' step buckets as labelled, so the point at end was the bucket after the last line and was missing while the data stops at end. They are now fetched from start-step on anchored step buckets and relabelled onto the evaluation time each closes, as rate and count_over_time over a range equal to the step already are. For a range equal to the step the values are Loki's window; a longer or shorter range keeps the step-sized bucket value.
…r the stddev/stdvar and v1.40 epoch-grid gaps, keep literal-name detection cases meaningful
szibis added a commit that referenced this pull request Oct 12, 2026
@github-actions github-actions Bot added size/XL Extra large change scope/proxy Proxy core scope/docs Documentation scope/tests Tests bugfix Bug fix 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

16 shape×range — 16 ⚪ same · results vs Loki: 15 same, 0 new difference(s), 1 pre-existing

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

16 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 60 ms 64 ms ⚪ +7% 87 ms 0.74× ✅ same
⚪ drilldown service landing 1h 27 ms 26 ms ⚪ -4% 23 ms 1.13× ✅ same
⚪ instant json volume instant 401 ms 399 ms ⚪ -0% 98 ms 4.07× ✅ same
⚪ instant ungrouped rate instant 6 ms 7 ms ⚪ +17% 71 ms 0.10× ✅ same
⚪ json rate by level (no filter, no drop) 1h 400 400 ⚪ +0% 400 — ✅ same
⚪ json volume no filters 1h 1 ms 1 ms ⚪ +0% 2 ms 0.50× ✅ same
⚪ logs plain 1h 58 ms 58 ms ⚪ +0% 37 ms 1.57× ✅ same
⚪ logs | json 1h 85 ms 78 ms ⚪ -8% 58 ms 1.34× ✅ same
⚪ stream-label count by app 1h 22 ms 26 ms ⚪ +18% 37 ms 0.70× ✅ same
⚪ topk rate by app 1h 36 ms 38 ms ⚪ +6% 82 ms 0.46× ➖ differs (as base)
⚪ ungrouped rate 1h 10 ms 11 ms ⚪ +10% 84 ms 0.13× ✅ same
⚪ A explore volume, 2 filters, drop 1h 129 ms 131 ms ⚪ +2% 108 ms 1.21× ✅ same
⚪ D volume, no filters 1h 176 ms 177 ms ⚪ +1% 120 ms 1.47× ✅ same
⚪ I grafana volume of B 1h 90 ms 86 ms ⚪ -4% 35 ms 2.46× ✅ same
⚪ O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 96 ms 89 ms ⚪ -7% 33 ms 2.70× ✅ same
⚪ DL6 explore volume, level filter before json 1h 437 ms 443 ms ⚪ +1% 39 ms 11× ✅ sum ±0.23%
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 64 ms 65 ms 716 ms 0.09×
drilldown service landing 1h 28 ms 28 ms 622 ms 0.05×
instant json volume instant 391 ms 392 ms 96 ms 4.08×
instant ungrouped rate instant 8 ms 6 ms 73 ms 0.08×
json rate by level (no filter, no drop) 1h 400 400 400 —
json volume no filters 1h 161 ms 165 ms 2 ms 82.50×
logs plain 1h 82 ms 62 ms 37 ms 1.68×
logs | json 1h 78 ms 86 ms 57 ms 1.51×
stream-label count by app 1h 29 ms 21 ms 586 ms 0.04×
topk rate by app 1h 38 ms 39 ms 703 ms 0.06×
ungrouped rate 1h 10 ms 9 ms 714 ms 0.01×
A explore volume, 2 filters, drop 1h 1.68 s 1.72 s 1.33 s 1.29×
D volume, no filters 1h 172 ms 168 ms 1.62 s 0.10×
I grafana volume of B 1h 101 ms 100 ms 1.02 s 0.10×
O2 explore log volume (Grafana form), underscore structured-metadata filter 1h 145 ms 129 ms 540 ms 0.24×
DL6 explore volume, level filter before json 1h 877 ms 853 ms 699 ms 1.22×
Why these shapes
  • internal/proxy/range_metric_compat.go → 12: 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 b20a550f6f → PR 0402a437fe · 4 runs (first is cold), warm p50 · ranges 1h · noise ±30% and 100 ms · VictoriaLogs restarts 0→0 · 2.9 min. Same runner, one fresh stack, targets interleaved per request. How to read and reproduce: bench/ab/README.md.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Visual smoke: failed

Base b20a550f6f vs PR 92a58a8f1a, 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-unwrap-avg-over-time-sliding-window 15m, explore-unwrap-avg-over-time-sliding-window 1h, explore-unwrap-avg-over-time-sliding-window 6h, explore-unwrap-max-over-time-end-sample 15m, explore-unwrap-max-over-time-end-sample 1h, explore-unwrap-max-over-time-end-sample 6h.
Core set: 5 capture(s) at 1h. Detailed set: 4 entries (explore-unwrap-max-over-time-end-sample, explore-unwrap-avg-over-time-sliding-window, explore-metric-plain, explore-A). 16 capture(s), 15m/1h/6h, 24h and 7d stay out of PR runs.

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/unwrap-over-time-range-windows: ❌ still differs

page range base = PR (data) vs Loki (PR) verdict
explore-unwrap-avg-over-time-sliding-window 15m differs (1) 2 difference(s), 1 on base too, 1 new ❌ still differs
explore-unwrap-avg-over-time-sliding-window 1h differs (1) 2 difference(s), 1 on base too, 1 new ❌ still differs
explore-unwrap-avg-over-time-sliding-window 6h differs (1) n/a (beyond the Loki window) ⚪ undecided
explore-unwrap-avg-over-time-sliding-window-15m: still differs

explore-unwrap-avg-over-time-sliding-window-15m

explore-unwrap-avg-over-time-sliding-window-1h: still differs

explore-unwrap-avg-over-time-sliding-window-1h

explore-unwrap-avg-over-time-sliding-window-6h: undecided

explore-unwrap-avg-over-time-sliding-window-6h

semantics/unwrap-range-last-point: ❌ still differs

page range base = PR (data) vs Loki (PR) verdict
explore-unwrap-max-over-time-end-sample 15m differs (1) 2 difference(s), 1 on base too, 1 new ❌ still differs
explore-unwrap-max-over-time-end-sample 1h differs (1) 2 difference(s), 1 on base too, 1 new ❌ still differs
explore-unwrap-max-over-time-end-sample 6h differs (1) n/a (beyond the Loki window) ⚪ undecided
explore-unwrap-max-over-time-end-sample-15m: still differs

explore-unwrap-max-over-time-end-sample-15m

explore-unwrap-max-over-time-end-sample-1h: still differs

explore-unwrap-max-over-time-end-sample-1h

explore-unwrap-max-over-time-end-sample-6h: undecided

explore-unwrap-max-over-time-end-sample-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) 0.13% ✅
dd-service-logs 1h core ✅ identical identical (3 explained, 1 history-dependent) 0.11% ✅
explore-logs-plain 1h core ✅ identical 1 difference(s), 1 on base too 0.07% ✅
explore-metric-plain 1h core ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-A 15m detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-A 1h detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-A 6h detailed ✅ identical n/a (beyond the Loki window) 0.04% ✅
explore-metric-plain 15m detailed ✅ identical 1 difference(s), 1 on base too 0.04% ✅
explore-metric-plain 6h detailed ✅ identical n/a (beyond the Loki window) 0.04% ✅
explore-unwrap-avg-over-time-sliding-window 15m detailed differs (1): query ('sum by (service_name) (avg_over_time({service_name=" 2 difference(s), 1 on base too, 1 new 0.40% ❌ data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if int
explore-unwrap-avg-over-time-sliding-window 1h detailed differs (1): query ('sum by (service_name) (avg_over_time({service_name=" 2 difference(s), 1 on base too, 1 new 1.06% ❌ data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if int
explore-unwrap-avg-over-time-sliding-window 6h detailed differs (1): query ('sum by (service_name) (avg_over_time({service_name=" n/a (beyond the Loki window) 3.91% ❌ data differs base vs PR and no Loki data for this range; label the pull request `visual-change-expected` if in
explore-unwrap-max-over-time-end-sample 15m detailed differs (1): query ('sum by (level) (max_over_time({service_name="api-gat 2 difference(s), 1 on base too, 1 new 0.28% ❌ data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if int
explore-unwrap-max-over-time-end-sample 1h detailed differs (1): query ('sum by (level) (max_over_time({service_name="api-gat 2 difference(s), 1 on base too, 1 new 0.11% ❌ data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if int
explore-unwrap-max-over-time-end-sample 6h detailed differs (1): query ('sum by (level) (max_over_time({service_name="api-gat n/a (beyond the Loki window) 0.08% ❌ data differs base vs PR and no Loki data for this range; label the pull request `visual-change-expected` if in

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-unwrap-avg-over-time-sliding-window-15m: data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if intended (1):
  • data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if intended (1): query ('sum by (service_name) (avg_over_time({service_name="api-gateway"} | json | unwrap durati: {"service_name": "api-gateway"}: timestamps differ (90 vs 901
  • vs Loki: query ('sum by (service_name) (avg_over_time({service_name="api-gateway"} | json | unwrap durati: {"service_name": "api-gateway"}: value at 1791802680000: 234.2812199807877 vs 975547
  • vs Loki: resource /api/datasources/uid/*/resources/index/stats?query=%7Bservice_name%3D%22api-gateway%22%7D&: body: content differs (rows 0 vs 0)

explore-unwrap-avg-over-time-sliding-window-15m

explore-unwrap-avg-over-time-sliding-window-1h: data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if intended (1):
  • data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if intended (1): query ('sum by (service_name) (avg_over_time({service_name="api-gateway"} | json | unwrap durati: {"service_name": "api-gateway"}: timestamps differ (360 vs 721
  • vs Loki: query ('sum by (service_name) (avg_over_time({service_name="api-gateway"} | json | unwrap durati: {"service_name": "api-gateway"}: value at 1791799980000: 231.79774223894637 vs 985604
  • vs Loki: resource /api/datasources/uid/*/resources/index/stats?query=%7Bservice_name%3D%22api-gateway%22%7D&: body: content differs (rows 0 vs 0)

explore-unwrap-avg-over-time-sliding-window-1h

explore-unwrap-avg-over-time-sliding-window-6h: data differs base vs PR and no Loki data for this range; label the pull request `visual-change-expected` if intended (1)
  • data differs base vs PR and no Loki data for this range; label the pull request `visual-change-expected` if intended (1): query ('sum by (service_name) (avg_over_time({service_name="api-gateway"} | json | unwrap durati: {"service_name": "api-gateway"}: timestamps differ (1080 vs 10
  • pixel diff 3.9%

explore-unwrap-avg-over-time-sliding-window-6h

explore-unwrap-max-over-time-end-sample-15m: data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if intended (1):
  • data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if intended (1): query ('sum by (level) (max_over_time({service_name="api-gateway"} | json | unwrap status | __er: {"level": "error"}: timestamps differ (90 vs 90 points)
  • vs Loki: query ('sum by (level) (max_over_time({service_name="api-gateway"} | json | unwrap status | __er: {"level": "error"}: value at 1791802681000: 503 vs 1506
  • vs Loki: resource /api/datasources/uid/*/resources/index/stats?query=%7Bservice_name%3D%22api-gateway%22%7D&: body: content differs (rows 0 vs 0)

explore-unwrap-max-over-time-end-sample-15m

explore-unwrap-max-over-time-end-sample-1h: data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if intended (1):
  • data differs base vs PR and neither build matches Loki; label the pull request `visual-change-expected` if intended (1): query ('sum by (level) (max_over_time({service_name="api-gateway"} | json | unwrap status | __er: {"level": "error"}: timestamps differ (358 vs 358 points)
  • vs Loki: query ('sum by (level) (max_over_time({service_name="api-gateway"} | json | unwrap status | __er: {"level": "error"}: value at 1791799995000: 503 vs 3013
  • vs Loki: resource /api/datasources/uid/*/resources/index/stats?query=%7Bservice_name%3D%22api-gateway%22%7D&: body: content differs (rows 0 vs 0)

explore-unwrap-max-over-time-end-sample-1h

explore-unwrap-max-over-time-end-sample-6h: data differs base vs PR and no Loki data for this range; label the pull request `visual-change-expected` if intended (1)
  • data differs base vs PR and no Loki data for this range; label the pull request `visual-change-expected` if intended (1): query ('sum by (level) (max_over_time({service_name="api-gateway"} | json | unwrap status | __er: {"level": "error"}: timestamps differ (1080 vs 1081 points)

explore-unwrap-max-over-time-end-sample-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-740.

@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 8648 8999 351
Coverage 87.6% 87.7% +0.1% (improved)

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 2967.0 ns/op 2997.0 ns/op +1.0% (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 3488.0 ns/op 3506.0 ns/op +0.5% (stable)
QueryRange cache-bypass memory 1292.0 B/op 1292.0 B/op 0.0% (stable)
QueryRange cache-bypass allocations 27.0 allocs/op 27.0 allocs/op 0.0% (stable)
Labels cache-hit CPU cost 894.9 ns/op 864.4 ns/op -3.4% (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 1443.0 ns/op 1438.0 ns/op -0.3% (stable)
Labels cache-bypass memory 399.0 B/op 398.0 B/op -0.3% (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.

…hich a parser's series identity cannot change
… on query_range for any range

Loki evaluates these functions over (t-range, t] at every step. For a range longer or shorter than the step the proxy answered each point from one VictoriaLogs step bucket, so values were off (about a fifth of Loki's for a 5m range at step 60s) and on sparse data whether a step had a sample followed the bucket rather than the window. VictoriaLogs now returns, per series and per bucket anchored at the request, the sum, max or min of the unwrapped values with their count, and the proxy merges the buckets of each window like the window evaluator already does for rate with unwrap: the sum, the sum over the window's count for avg, the max or the min, with a sample only where the window holds a line. An ungrouped sum of avg, max or min folds the per-stream windows and counts as one series against the series limit. VictoriaLogs before v1.45 with a start off the bucket's epoch grid and more than 11,000 buckets use the raw-row fallback, which reads (t-range, t] for these functions; on those versions that includes a range equal to the step with an off-grid start, which read epoch-grid buckets one step off before. Instant queries, offset handling and a range equal to the step on a supported grid are unchanged.
…fix proof page for unwrap-over-time-range-windows and regenerated reports

Registers the forms the window merge does not answer yet as open rows (an ungrouped sum or a without of avg, max or min over a parser query; an outer aggregation other than sum dropped by the window evaluator), updates the range == step row and the v1.40 epoch-grid row to what the window merge now answers, and adds an Explore page with a 5m range at Explore's step.
…e, which a parser's series identity cannot change
@szibis
szibis force-pushed the fix/unwrap-over-time-range-windows branch from 92a58a8 to c882ee3 Compare October 12, 2026 12:35
szibis added a commit that referenced this pull request Oct 28, 2026
…han the step answer Loki's window

Loki reads each step's sample from (t-range, t]
(pkg/logql/range_vector.go:102-189 batchRangeVectorIterator.Next and load;
the functions at range_vector.go:371-420), and the windows overlap when the
range is longer than the step. statsRateRangeEqualsStepShift read every range
of these functions as the step and shouldUseManualRangeMetricCompat kept them
off the window evaluator, so each point was one VictoriaLogs step bucket
(t-step, t]: sum(sum_over_time({app="payment-service"} | logfmt | unwrap
amount [5m])) at step 60 was 2.00257e+06 where Loki has 9.84307e+06 on the
stack, sum_over_time({app="uw"} | unwrap amount [150s]) at step 2m 725.875
where Loki has 942.125 on the emulated backend, and a line every 4.5 minutes
gave 13 samples in an hour where Loki has 61.

A range longer than the step now goes to the window evaluator that already
answers rate with unwrap (unwrapWindowBuckets). VictoriaLogs returns, per
series, buckets of gcd(range, step) anchored at the request with the partial
aggregate of the converted value (sum, max or min) and the line count
(unwrapBucketAgg; avg keeps the counts to divide by), and the proxy merges the
buckets of each window (mergeUnwrapBuckets) with a sample only where the
window holds a line. VictoriaLogs has no sliding-window stats and one step
bucket cannot hold an overlapping window, so the fold is done in the proxy,
over aggregated buckets only; a grid over 11,000 buckets goes to the raw-row
evaluator bounded by -manual-range-metric-row-limit, which reads the same
window since the previous commit. The route takes only the aggregations the
evaluator applies after the window (unwrapWindowForm): sums by labels or none,
topk and bottomk, label_replace, binary operands, count/min/max/avg by labels
and an outermost labelled without; an outer fold reads under the raw-row
budget (recordFoldBudget, shared with withWithoutFold) and Loki's series
limit applies to its answer. JSON parser queries grouped per stream and line
under an outer aggregation, nested aggregations and an empty without () keep
the step buckets (registered open:
semantics/unwrap-over-time-range-windows-residual-forms). Per-stream series of the route carry detected_level="unknown" for a
stream without a level, as Loki's and rate's do. Ranges up to the step and
instant queries are unchanged; the 7-day E1/E4/E8/E9 shapes ([1m] and [5m] at
step 10m) keep their single phase-filtered step bucket query.

The window phase filter now names the window start modulo the step instead
of the request's start: the same lines, but the query text, and so the stats
extent cache key, stays the same when a window moves by whole steps (one miss
and five partial answers where main had six misses).

Tests: TestUnwrapOverTimeWindowsDifferential (generated rows, edges at +-1ns,
offsets, instant queries, ten query forms, VictoriaLogs v1.49.0, v1.53.0 and
unknown) against a reference evaluator of Loki's windows,
TestUnwrapOverTimeRangeWindowEdgeCases (answers spelled out),
TestUnwrapOverTimeRawWindowBounds, TestUnwrapOverTimeSparseSamplePresence,
TestUnwrapOverTimeUngroupedSumIgnoresSeriesLimit,
TestUnwrapOverTimeResidualShapesKeepStatsBuckets,
TestUnwrapOverTimeShortRangeStaysOnStepBuckets,
TestWindowPhaseFilterKeepsExtentCacheAcrossSteps and the e2e
TestCompat_UnwrapOverTimeRangeWindows (shared fixtures plus lines on every
window edge, range and instant). The differential, edge and presence tests
fail on main for range > step. Pinned answers that read the step bucket move
to the window: five sliding extraction-list goldens and two max without
answers. Brought over from #740 (its tests, adapted to the supported
VictoriaLogs lines), which this supersedes.
szibis added a commit that referenced this pull request Oct 28, 2026
…han the step answer Loki's window

Loki reads each step's sample from (t-range, t]
(pkg/logql/range_vector.go:102-189 batchRangeVectorIterator.Next and load;
the functions at range_vector.go:371-420), and the windows overlap when the
range is longer than the step. statsRateRangeEqualsStepShift read every range
of these functions as the step and shouldUseManualRangeMetricCompat kept them
off the window evaluator, so each point was one VictoriaLogs step bucket
(t-step, t]: sum(sum_over_time({app="payment-service"} | logfmt | unwrap
amount [5m])) at step 60 was 2.00257e+06 where Loki has 9.84307e+06 on the
stack, sum_over_time({app="uw"} | unwrap amount [150s]) at step 2m 725.875
where Loki has 942.125 on the emulated backend, and a line every 4.5 minutes
gave 13 samples in an hour where Loki has 61.

A range longer than the step now goes to the window evaluator that already
answers rate with unwrap (unwrapWindowBuckets). VictoriaLogs returns, per
series, buckets of gcd(range, step) anchored at the request with the partial
aggregate of the converted value (sum, max or min) and the line count
(unwrapBucketAgg; avg keeps the counts to divide by), and the proxy merges the
buckets of each window (mergeUnwrapBuckets) with a sample only where the
window holds a line. VictoriaLogs has no sliding-window stats and one step
bucket cannot hold an overlapping window, so the fold is done in the proxy,
over aggregated buckets only; a grid over 11,000 buckets goes to the raw-row
evaluator bounded by -manual-range-metric-row-limit, which reads the same
window since the previous commit. The route takes only the aggregations the
evaluator applies after the window (unwrapWindowForm): sums by labels or none,
topk and bottomk, label_replace, binary operands, count/min/max/avg by labels
and an outermost labelled without; an outer fold reads under the raw-row
budget (recordFoldBudget, shared with withWithoutFold) and Loki's series
limit applies to its answer. JSON parser queries grouped per stream and line
under an outer aggregation, nested aggregations and an empty without () keep
the step buckets (registered open:
semantics/unwrap-over-time-range-windows-residual-forms). Per-stream series of the route carry detected_level="unknown" for a
stream without a level, as Loki's and rate's do. Ranges up to the step and
instant queries are unchanged; the 7-day E1/E4/E8/E9 shapes ([1m] and [5m] at
step 10m) keep their single phase-filtered step bucket query.

The window phase filter now names the window start modulo the step instead
of the request's start: the same lines, but the query text, and so the stats
extent cache key, stays the same when a window moves by whole steps (one miss
and five partial answers where main had six misses).

Tests: TestUnwrapOverTimeWindowsDifferential (generated rows, edges at +-1ns,
offsets, instant queries, ten query forms, VictoriaLogs v1.49.0, v1.53.0 and
unknown) against a reference evaluator of Loki's windows,
TestUnwrapOverTimeRangeWindowEdgeCases (answers spelled out),
TestUnwrapOverTimeRawWindowBounds, TestUnwrapOverTimeSparseSamplePresence,
TestUnwrapOverTimeUngroupedSumIgnoresSeriesLimit,
TestUnwrapOverTimeResidualShapesKeepStatsBuckets,
TestUnwrapOverTimeShortRangeStaysOnStepBuckets,
TestWindowPhaseFilterKeepsExtentCacheAcrossSteps and the e2e
TestCompat_UnwrapOverTimeRangeWindows (shared fixtures plus lines on every
window edge, range and instant). The differential, edge and presence tests
fail on main for range > step. Pinned answers that read the step bucket move
to the window: five sliding extraction-list goldens and two max without
answers. Brought over from #740 (its tests, adapted to the supported
VictoriaLogs lines), which this supersedes.
szibis added a commit that referenced this pull request Oct 28, 2026
…han the step answer Loki's window

Loki reads each step's sample from (t-range, t]
(pkg/logql/range_vector.go:102-189 batchRangeVectorIterator.Next and load;
the functions at range_vector.go:371-420), and the windows overlap when the
range is longer than the step. statsRateRangeEqualsStepShift read every range
of these functions as the step and shouldUseManualRangeMetricCompat kept them
off the window evaluator, so each point was one VictoriaLogs step bucket
(t-step, t]: sum(sum_over_time({app="payment-service"} | logfmt | unwrap
amount [5m])) at step 60 was 2.00257e+06 where Loki has 9.84307e+06 on the
stack, sum_over_time({app="uw"} | unwrap amount [150s]) at step 2m 725.875
where Loki has 942.125 on the emulated backend, and a line every 4.5 minutes
gave 13 samples in an hour where Loki has 61.

A range longer than the step now goes to the window evaluator that already
answers rate with unwrap (unwrapWindowBuckets). VictoriaLogs returns, per
series, buckets of gcd(range, step) anchored at the request with the partial
aggregate of the converted value (sum, max or min) and the line count
(unwrapBucketAgg; avg keeps the counts to divide by), and the proxy merges the
buckets of each window (mergeUnwrapBuckets) with a sample only where the
window holds a line. VictoriaLogs has no sliding-window stats and one step
bucket cannot hold an overlapping window, so the fold is done in the proxy,
over aggregated buckets only; a grid over 11,000 buckets goes to the raw-row
evaluator bounded by -manual-range-metric-row-limit, which reads the same
window since the previous commit. The route takes only the aggregations the
evaluator applies after the window (unwrapWindowForm): sums by labels or none,
topk and bottomk, label_replace, binary operands, count/min/max/avg by labels
and an outermost labelled without; an outer fold reads under the raw-row
budget (recordFoldBudget, shared with withWithoutFold) and Loki's series
limit applies to its answer. JSON parser queries grouped per stream and line
under an outer aggregation, nested aggregations and an empty without () keep
the step buckets (registered open:
semantics/unwrap-over-time-range-windows-residual-forms). Per-stream series of the route carry detected_level="unknown" for a
stream without a level, as Loki's and rate's do. Ranges up to the step and
instant queries are unchanged; the 7-day E1/E4/E8/E9 shapes ([1m] and [5m] at
step 10m) keep their single phase-filtered step bucket query.

The window phase filter now names the window start modulo the step instead
of the request's start: the same lines, but the query text, and so the stats
extent cache key, stays the same when a window moves by whole steps (one miss
and five partial answers where main had six misses).

Tests: TestUnwrapOverTimeWindowsDifferential (generated rows, edges at +-1ns,
offsets, instant queries, ten query forms, VictoriaLogs v1.49.0, v1.53.0 and
unknown) against a reference evaluator of Loki's windows,
TestUnwrapOverTimeRangeWindowEdgeCases (answers spelled out),
TestUnwrapOverTimeRawWindowBounds, TestUnwrapOverTimeSparseSamplePresence,
TestUnwrapOverTimeUngroupedSumIgnoresSeriesLimit,
TestUnwrapOverTimeResidualShapesKeepStatsBuckets,
TestUnwrapOverTimeShortRangeStaysOnStepBuckets,
TestWindowPhaseFilterKeepsExtentCacheAcrossSteps and the e2e
TestCompat_UnwrapOverTimeRangeWindows (shared fixtures plus lines on every
window edge, range and instant). The differential, edge and presence tests
fail on main for range > step. Pinned answers that read the step bucket move
to the window: five sliding extraction-list goldens and two max without
answers. Brought over from #740 (its tests, adapted to the supported
VictoriaLogs lines), which this supersedes.
szibis added a commit that referenced this pull request Oct 28, 2026
…every range and step (#885)

* perf: a range shorter than the step parses only the lines inside a window

Loki reads a step's sample from the window (t-range, t] only
(pkg/logql/range_vector.go:102-189, batchRangeVectorIterator.Next and load):
a line between two windows belongs to no sample. The window evaluator already
kept only those lines with a filter on the stored _time, but it appended the
filter after the whole log pipeline, so VictoriaLogs derived service_name,
unpacked and ran the unwrap gate on every line of the range first.

The filter now follows the query's filters, ahead of every pipe. For
sum by (service_name) (rate({env="production"} | logfmt | unwrap latency_ms [5m]))
over 7 days (step 10m, the daily E3 shape at 13 s cold and warm) half the
lines are parsed; a [1m] range skips nine lines in ten. The pipes the filter
moves ahead of are row-local and never write _time, so the stats see the
same rows and the answer is unchanged. A query holding the derived
detected_level chain keeps the filter after the chain, whose base keys its
remembered presence probe. The unwrap conversion-error lookup carries the
filter once, ahead of the parser, instead of appending a second one.

The conversion-error counter itself is not the cost: the 2026-10-08 A/B ran
E3 at 7d in 13.37 s on main without it and 13.44 s with it, and the metric
sends one stats_query_range and no lookup on clean data.

* fix: sum, avg, max and min over time with unwrap and a range shorter than the step answer Loki's window

Loki reads each step's sample from (t-range, t] only
(pkg/logql/range_vector.go:102-189, batchRangeVectorIterator.Next and load).
With a range shorter than the step the windows are disjoint: most lines belong
to no sample and a step whose window is empty has none. The proxy answered
these functions from VictoriaLogs' anchored step buckets (t-step, t]
(statsRateRangeEqualsStepShift), so
sum by (service_name) (sum_over_time({service_name="api-gateway"} | json | unwrap duration_ms [1m]))
over 7 days at step 10m (the daily E1 shape; E4, E8 and E9 at 7d likewise)
summed ten minutes of lines per point where Loki sums one, and a step with
lines only outside its window had a sample Loki does not.

The stats query now carries the window filter the other range metrics use,
right after the query's filters, so each bucket holds exactly Loki's window and
VictoriaLogs parses only those lines. The unwrap conversion-error check rides
on the same query and counts only the lines inside a window, the only ones Loki
fails on.

TestUnwrapOverTimeShortRangeMatchesLoki compares the four functions with a
reference Loki evaluator over generated rows (edges, empty windows) on three
VictoriaLogs lines, with the error filter, with the conversion check, with a
scalar operand and between two vector operands: all 96 subtests fail before
this change and pass after. No visual fix proof: the visual stack holds Loki
for 1.5h, where Explore's step is seconds and every panel range is longer than
the step. A range longer than the step stays open
(semantics/unwrap-over-time-range-windows).

* fix: the range-vector window excludes its start and includes its end for every function

Loki's batchRangeVectorIterator.load skips `sample.Timestamp <= start` (the
comment in that loop reads "the lower bound of the range is not inclusive")
and keeps samples up to and including the evaluation time, and unwrapped
ranges share that iterator (pkg/logql/range_vector.go:148-189, v3.7.7).
aggregateManualWindow excluded the lower bound only for rate,
count_over_time, bytes_over_time and bytes_rate, so every unwrapped range
function read the sample on windowStart that Loki drops, and answered one
sample high.

Correcting only the lower bound would leave the window open at both ends:
the 1ns extension of the VictoriaLogs fetch, which is exclusive at its end
where Loki is inclusive, was applied to quantile and the log-range functions
alone, so a sample exactly on the evaluation timestamp was never fetched. The
extension is now unconditional, on query_range and on instant queries.

TestLowerBoundIsExclusiveForEveryRangeFunction fails without the change and
passes with it (its quantile subcase uses q=0, which tells {1,2,3} from
{2,3}); TestCompatHelpers_AggregateManualWindow encoded the old bound and is
updated to Loki's.

* fix: sum, avg, max and min over time with unwrap and a range longer than the step answer Loki's window

Loki reads each step's sample from (t-range, t]
(pkg/logql/range_vector.go:102-189 batchRangeVectorIterator.Next and load;
the functions at range_vector.go:371-420), and the windows overlap when the
range is longer than the step. statsRateRangeEqualsStepShift read every range
of these functions as the step and shouldUseManualRangeMetricCompat kept them
off the window evaluator, so each point was one VictoriaLogs step bucket
(t-step, t]: sum(sum_over_time({app="payment-service"} | logfmt | unwrap
amount [5m])) at step 60 was 2.00257e+06 where Loki has 9.84307e+06 on the
stack, sum_over_time({app="uw"} | unwrap amount [150s]) at step 2m 725.875
where Loki has 942.125 on the emulated backend, and a line every 4.5 minutes
gave 13 samples in an hour where Loki has 61.

A range longer than the step now goes to the window evaluator that already
answers rate with unwrap (unwrapWindowBuckets). VictoriaLogs returns, per
series, buckets of gcd(range, step) anchored at the request with the partial
aggregate of the converted value (sum, max or min) and the line count
(unwrapBucketAgg; avg keeps the counts to divide by), and the proxy merges the
buckets of each window (mergeUnwrapBuckets) with a sample only where the
window holds a line. VictoriaLogs has no sliding-window stats and one step
bucket cannot hold an overlapping window, so the fold is done in the proxy,
over aggregated buckets only; a grid over 11,000 buckets goes to the raw-row
evaluator bounded by -manual-range-metric-row-limit, which reads the same
window since the previous commit. The route takes only the aggregations the
evaluator applies after the window (unwrapWindowForm): sums by labels or none,
topk and bottomk, label_replace, binary operands, count/min/max/avg by labels
and an outermost labelled without; an outer fold reads under the raw-row
budget (recordFoldBudget, shared with withWithoutFold) and Loki's series
limit applies to its answer. JSON parser queries grouped per stream and line
under an outer aggregation, nested aggregations and an empty without () keep
the step buckets (registered open:
semantics/unwrap-over-time-range-windows-residual-forms). Per-stream series of the route carry detected_level="unknown" for a
stream without a level, as Loki's and rate's do. Ranges up to the step and
instant queries are unchanged; the 7-day E1/E4/E8/E9 shapes ([1m] and [5m] at
step 10m) keep their single phase-filtered step bucket query.

The window phase filter now names the window start modulo the step instead
of the request's start: the same lines, but the query text, and so the stats
extent cache key, stays the same when a window moves by whole steps (one miss
and five partial answers where main had six misses).

Tests: TestUnwrapOverTimeWindowsDifferential (generated rows, edges at +-1ns,
offsets, instant queries, ten query forms, VictoriaLogs v1.49.0, v1.53.0 and
unknown) against a reference evaluator of Loki's windows,
TestUnwrapOverTimeRangeWindowEdgeCases (answers spelled out),
TestUnwrapOverTimeRawWindowBounds, TestUnwrapOverTimeSparseSamplePresence,
TestUnwrapOverTimeUngroupedSumIgnoresSeriesLimit,
TestUnwrapOverTimeResidualShapesKeepStatsBuckets,
TestUnwrapOverTimeShortRangeStaysOnStepBuckets,
TestWindowPhaseFilterKeepsExtentCacheAcrossSteps and the e2e
TestCompat_UnwrapOverTimeRangeWindows (shared fixtures plus lines on every
window edge, range and instant). The differential, edge and presence tests
fail on main for range > step. Pinned answers that read the step bucket move
to the window: five sliding extraction-list goldens and two max without
answers. Brought over from #740 (its tests, adapted to the supported
VictoriaLogs lines), which this supersedes.

* perf: the window evaluator reads its stats buckets with a pooled JSON parser

collectRangeMetricHitsAux parsed every stats_query_range answer with
fj.ParseBytes, a new parser per call whose value cache is allocated again for
every [timestamp, value] pair. A range longer than the step now sends sum,
avg, max and min over time with unwrap through this read with a value and a
count series per group, so the allocation grew with the answer: a canned
10-series answer of a 24h query at step 60 with a [5m] range allocated
50 MiB per request (main's step-bucket answer 10 MiB). It now takes a parser
from statsQRFJPool, the pool the step-bucket trims use: 18 MiB per request,
1h at step 5s 28 MiB to 9 MiB. Nothing parsed outlives the call (labels are
string copies, values floats).

* fix: the unwrap window route records the fold budget only for a query that folds

The window route of sum, avg, max and min over time with unwrap recorded the
raw-row budget in the request's series limit scope for every query it took,
also a grouped sum that folds nothing; a folded read later in the same
request (the other operand of a binary expression) would then read under that
budget and its error text. It is now recorded only for the outer folds the
route applies afterwards: a series-level aggregation chain
(foldVectorAggregations) and an ungrouped sum of a function VictoriaLogs does
not sum (foldUngroupedSum). A labelled without records it in withWithoutFold.

Two handler tests cover what the differential's emulator cannot:
TestUnwrapOverTimeRangeWindowsNaN sends the counting gate's nan group through
the merged buckets (sum and avg are NaN in the five windows that hold the nan
line, max and min keep the clean value; on main one step is NaN), and
TestRateCounterReadsLokiWindow checks rate_counter's raw window on query_range
and at an instant (0.666667 over (t-1m, t]; main answers 0.333333 with the
old bounds).

* perf: the unwrap window merge groups max by and min by upstream and reads its buckets in place

The corrected range-longer-than-step route cost the proxy 2 to 5 times the
memory of main's (wrong) step-bucket answer. Measured with a canned
10-series VictoriaLogs answer served from memory (the proxy's own cost):
E8 24h 6.8 MiB on main against 11.4 MiB, E4 6h 7.4 against 44.9 MiB.

- max by (L) (max_over_time(...)) and min by (L) (min_over_time(...)), L
  possibly empty: Loki takes the extreme per series over the window and then
  per group (pkg/logql/range_vector.go:405-420; evaluator.go
  VectorAggEvaluator.Next, a NaN giving way in both), which is the extreme of
  the group's lines in the window. unwrapGroupPushdown has VictoriaLogs group
  the buckets by L instead of by stream (or stream and line for a JSON query),
  and the evaluator answers without the JSON fold. Loki's series limit counts
  the groups (pkg/logql/engine.go:530-590 JoinSampleVector), as the read does.
  The translated grouping must be L plus the series identity (_stream, _msg,
  level); anything else keeps the fold. JSON max by and min by of the same
  function leave the residual forms.
- sum, max and min over time read no sample count: VictoriaLogs answers a
  bucket only when it holds a row, so the aggregate's point is the presence
  (the counting gate's nan group still poisons a sum's point); avg keeps it.
- collectRangeMetricHitsAux reads the [ts, value] pairs in place with
  jsonparser and parses only each series' labels (pooled fastjson parser);
  buildHitsRangeMetricMatrix writes the points into a raw JSON array instead
  of an interface pair per point (byte for byte what encoding/json writes);
  the unwrap counter's kept flag leaves the answer in one in-place pass
  instead of three copies (dropKeptFlag, also on the step-bucket route).

Same canned answer, after: E8 24h 4.5 MiB in 1.6k allocations (main 6.8 MiB,
4.2k), E8 1h 2.3 (3.0), E4 6h 4.5 (7.4), avg 24h 7.2 (6.3), the 7-day [1m]
and [5m] shapes 2.9 (3.5, 4.7).

TestUnwrapGroupPushdownMatchesPerStreamFold compares the pushed-down answer
with the per-stream fold's (unwrapGroupPushdownOff) over generated rows,
three VictoriaLogs versions, grouped, ungrouped and under topk, series limits
0 to 3: the same series byte for byte and the same 400s. The differential
gains max by, min by, max and topk of max by over JSON;
TestUnwrapOverTimeRangeWindowsNaN the max by and min by forms;
TestDropKeptFlagMatchesReplacements holds the one-pass removal to the three
replacements. Five sliding extraction-list goldens send the new bucket query;
max(max_over_time(... | logfmt amount, route ...)) now answers Loki's window.

* perf: the window merge moves its bounds with the step and scans bucket pairs directly

buildHitsRangeMetricMatrix searched the window bounds of every step (two
binary searches over the samples, one over the presence, two over the counts
of avg). The windows only move forward with t, so the bounds are now cursors
that only move forward; the prefix sums are built only for the additive
functions that read them. eachBucketPoint reads VictoriaLogs' pairs
`[number,"number"]` with a direct scan instead of two nested jsonparser
walks per pair.

Interleaved benchstat (n=10, canned 10-series answer served from memory,
GOMAXPROCS=2, load average 7 to 9), main / before this branch's perf commits /
now: E8 24h [5m] step 60 3.09 / 5.90 / 2.83 ms, 6.9 / 11.7 / 4.2 MiB; E4 6h
max by 3.14 / 18.66 / 2.85 ms, 7.0 / 42.3 / 4.2 MiB; E8 1h step 5 1.59 /
3.43 / 2.15 ms; avg 24h 3.02 / 7.00 / 5.34 ms, 6.3 / 12.6 / 6.7 MiB; the
7-day range-shorter-than-step shapes 2.25 / 2.29 / 2.01 ms.

* fix: the group push-down names every grouped value by its own label and keeps the fold for detected_level

unwrapGroupPushdown checked that the translation's grouping held the outer
labels as a set and then paired them by position with the user's by-list,
while the collector names each grouped field after the label at its index
(collectRangeMetricHitsAux vlToLoki): a by-list the translation writes in
another order would put a value under the wrong name. Each grouped field is
now named after the label whose name or VictoriaLogs name it is.

A detected_level (or level) grouping keeps the per-stream fold: a stream
without a level is labelled detected_level="unknown" by the per-stream read
(buildMetricSeriesEntry), which a grouping by the stored field does not give.
service_name is pushed down: the translation derives it ahead of the stats
(the coalesce over the stored names), as for sum by (service_name).

TestUnwrapGroupPushdownMatchesPerStreamFold gains max by (app, pod), min by
(pod, app) and max by (detected_level, pod); TestUnwrapGroupPushdownDerivesServiceName
checks the derivation ahead of the grouping. The e2e
TestCompat_UnwrapOverTimeRangeWindows asks max by (app), min by (route) and
max over max_over_time over json and logfmt, on the shared and the edge
fixtures, range and instant.

* perf: the window matrix sizes each series' points from its buckets, not from the range

buildHitsRangeMetricMatrix reserved 32 bytes for every step of the range for
every series that held a point. A query that builds a series per stream before
an outer fold reserved that for thousands of sparse series: the 6h
`sum without (pod) (count_over_time({env="production", ...}[1m]))` shape (W8 of
the unwrap-sum-without set, step 10s, 2161 steps) held 1.9 GB live and
allocated 22 GB per six queries on the stack, a quarter more than main, whose
point slices reserved 24 bytes a step. The process ran at its memory limit and
spent the difference in the collector.

A point needs a bucket in its window, and a bucket lies in the window of at
most window/step+2 evaluation times, so a series cannot hold more points than
its buckets (its presence buckets for count and rate) times that, and never
more than the steps of the range. The reservation is now that bound; a series
that fills every step reserves what it did.

Measured on the A/B stack (6h, step 10s, the same windows, six queries each):
main 2.6 s and 3.3 GB peak RSS, before this commit 5.3 s and 4.1 GB, with it
1.4 s and 0.38 GB. The answers are identical (the same bytes per window).

* docs(changelog): move the unwrap window entries back to Unreleased

---------

Co-authored-by: szibis <szibis@users.noreply.github.com>
Co-authored-by: Igor Kudrin <igor@maestra.io>
@szibis

szibis commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #885, which landed these windows for every range and step (shorter, equal and longer than the step, not a multiple, sliding, json and logfmt, grouped and ungrouped). Its tests were brought over and adapted there, and its registry cases and visual pages are on main, so nothing from this branch is left to land.

@szibis szibis closed this Oct 28, 2026
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 size/XL Extra large change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant