Files
discord9 a7590f8174 perf(promql): avoid repeated scans in sliding range evaluation (#8646)
* perf(promql): use two pointers for sliding range boundaries

Replace the stale cursor heuristic in RangeManipulateStream::calculate_range
with monotonic left/right cursors. The old path rescanned each evaluation
window (O(E x samples-per-window)) and could lose valid samples after sparse
gaps or trailing empty windows. The two pointers keep strict monotonic
progress, reducing boundary generation to O(N + E) while preserving
(curr-range, curr] semantics, start/end shortening, and empty-window output.

Controlled release benchmarks (fixed CPU, ABBA):
- Public RangeManipulate wall time: ~28% faster at 1m/15s, ~66% at 5m/15s,
  ~96% at 1h/15s.
- Warmed distributed TQL ANALYZE 1h queries: ~17-21% faster end to end;
  shorter windows stayed within run-order noise.

Signed-off-by: discord9 <discord9@163.com>

* perf(promql): specialize changes/resets with adaptive edge counting

The generic range_fn macro slices, downcasts, and rescans every overlapping
window for changes() and resets(). Replace the macro path for these two
functions with hand-written UDF wrappers backed by a shared private
edge-count kernel: direct raw-offset scans when requested edges are few,
otherwise one global u64 edge prefix so each window is answered by a prefix
difference.

Behavior is preserved bit-for-bit, including raw null-buffer values, NaN
semantics, signed zero, infinities, empty/singleton windows, independent
timestamp/value offsets, arbitrary window layouts, and exact DataFusion
error messages. The shared proc macro, planner, serializer, and other range
functions are untouched.

Controlled release benchmarks (fixed CPU, ABBA):
- Dense sliding windows (k=4/20/240): 91.7-95.6% less public UDF wall time.
- Low-coverage fallback (N=4096, 8 windows): 73.9-74.4% faster.
- Warmed distributed TQL ANALYZE 5m/1h changes/resets: 12.1-19.7% client
  and 12.0-20.9% server latency improvement; controls stayed within drift.

Signed-off-by: discord9 <discord9@163.com>

* ci(query-regression): include PromQL range boundary case in defaults

An audit of historical query-regression runs found zero range-query
coverage: all 208 PromQL ANALYZE samples were bare selectors, so range
evaluation could regress without CI noticing. Wire the
promql_range_boundary case (introduced in #8646) into DEFAULT_CASES so
label-triggered runs measure the range path. The case is cheap: a ~0.3s
synthetic fixture and about a minute of query execution per base/candidate
pass.

Signed-off-by: discord9 <discord9@163.com>

* chore(promql): address sliding range review nits

Move test-only imports into their test modules and remove the unused
pre-specialization changes and resets helpers.

Signed-off-by: discord9 <discord9@163.com>

* style(promql): apply pinned rustfmt

Signed-off-by: discord9 <discord9@163.com>

* test(promql): cover sparse range results

Share the changes and resets test scaffolding while keeping their behavior
oracles independent. Add an end-to-end sqlness regression for sparse samples,
empty intermediate windows, and a valid trailing sample.

Signed-off-by: discord9 <discord9@163.com>

---------

Signed-off-by: discord9 <discord9@163.com>
2026-08-03 06:53:21 +00:00
..

Query regression self-hosted runners

The Query Regression workflow uses the dedicated ARC runner scale set perf-regression-8-cores. ARC runner Pods run in the target Kubernetes cluster and connect outbound to GitHub. The live scale set is currently paused: minRunners=0, maxRunners=0, and no runner Pods. Do not resume it without explicit approval.

Prerequisites and trust admission

Install the ARC scale set controller if it is not already installed:

helm upgrade --install arc \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \
  --namespace arc-systems \
  --create-namespace \
  --version 0.14.2

Create the GitHub App secret in the runner namespace. Prefer an App limited to GreptimeTeam/greptimedb:

kubectl -n arc-runners create secret generic greptimedb-arc-github-app \
  --from-literal=github_app_id=<app-id> \
  --from-literal=github_app_installation_id=<installation-id> \
  --from-file=github_app_private_key=<private-key.pem>

The values files here reference that secret by name.

A maintainer applying the query-regression or heavy-regression label is trust admission for that exact PR revision. query-regression runs the six routine default cases; heavy-regression runs only the high-cardinality prom_remote_write_7913 remote-write case. The admitted job may use this scale set's dedicated, writable persistent cache. pull_request: labeled is the only PR trigger: the label event snapshots its merge, head, and base SHAs. A queued job fetches that immutable event merge SHA directly, verifies it is a two-parent merge whose parents include the snapshotted head exactly once, and uses its other parent as the actual base build revision. The snapshotted event base is retained for audit only, so a difference from the merge's non-head parent is not a failure. The job never follows a newer mutable PR merge ref. An unavailable event merge, or one that does not contain exactly one snapshotted head parent, fails closed. A later PR head change does not retarget an already queued run: it may execute only its previously trusted event revision if that revision remains fetchable. To run the new revision, the maintainer must review it, remove the label, and re-add the desired regression label; cancel the old run if it is no longer wanted. An existing label does not automatically rerun the benchmark.

Admission does not relax runner hardening or GitHub permissions. Keep service-account token mounting disabled; do not mount host paths, the Docker socket, kubeconfig, or long-lived credentials. The runner and cache initializer use UID/GID 1001, disallow privilege escalation, drop all capabilities, and use the RuntimeDefault seccomp profile. Keep GitHub tokens least-privilege and review workflow changes before admission. Where the CNI supports it, restrict egress to required GitHub Actions, artifact/cache, Rust/crate/toolchain, DNS, and image-registry endpoints; block unrelated cluster services, private ranges, and metadata endpoints unless a case requires them.

Network routing prerequisite

Required split routing is an external environment-specific prerequisite. The responsible network operator must route GitHub Actions, GitHub content, artifact/cache, crates.io, Rust toolchain, and image-registry traffic through the approved path rather than the VPN where applicable. Neither this repository nor Kubernetes configures that route. Verify it with the responsible network operator before any canary.

Runner image and workflow tools

Build and push the derived runner image; it preserves the official /home/runner/run.sh entrypoint and supplies CI tools needed at runtime. The image builds otelgen from WenyXu/otelgen commit 863a3f395d062c7322cc1de08a38774b7fdaa6c8 so trace cases do not download or compile tools during a benchmark run:

docker build \
  --platform linux/amd64 \
  -f .github/runner-scale-sets/query-regression/Dockerfile \
  -t greptime-registry.cn-hangzhou.cr.aliyuncs.com/greptime/greptimedb-query-regression-runner:latest \
  .github/runner-scale-sets/query-regression

docker push greptime-registry.cn-hangzhou.cr.aliyuncs.com/greptime/greptimedb-query-regression-runner:latest

Deploy by digest, not mutable tag, by updating both image references in values-8-cores.yaml after a rebuild. Update RUNNER_IMAGE_DIGEST and bump RUNNER_IMAGE_EPOCH in query-regression.yml at the same time. If the registry is private, use a dedicated read-only pull secret only as imagePullSecrets; never expose registry credentials to runner containers. Both digest-pinned init and runner containers use IfNotPresent: the immutable digest makes a cached image safe and avoids adding a registry dependency to every runner startup.

The runner optionally imports only the non-sensitive HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables from the query-regression-runner-local-env ConfigMap. Manage that ConfigMap locally in the target namespace; private endpoint configuration must not be committed, and credentials or secrets must never be placed in a ConfigMap.

Before builds, the workflow asserts UID/GID 1001 and exact image tool versions: libprotoc 3.21.12, uv 0.11.26, mold 2.30.0, Python 3.12.3, sccache 0.16.0, otelgen commit 863a3f395d062c7322cc1de08a38774b7fdaa6c8, root-owned rustup 1.29.0, and the image-baked nightly-2026-03-21 Rust toolchain. Rustup, Cargo, and Rustc must resolve from /opt/cargo/bin; the runner cannot write /opt/rustup or /opt/cargo/bin. Protobuf well-known includes, including google/protobuf/any.proto and google/protobuf/empty.proto, are an image contract and must compile with protoc. actions-rust-lang/setup-rust-toolchain@v1 is intentionally removed. The workflow sets its warning-denying mold RUSTFLAGS directly, disables automatic Rustup installation, and performs no runtime toolchain downloads.

The workflow no longer uses GitHub rust-cache, setup-protoc, setup-uv, or runtime Rust setup: the image establishes immutable executable state and the PVC supplies only reusable Cargo data. Do not reintroduce those actions unless the corresponding cache or image contract changes.

Capacity and persistent cache

values-8-cores.yaml is normal operation: minRunners=0, maxRunners=1. values-paused.yaml is the mandatory pause overlay: minRunners=0, maxRunners=0. The job uses group query-regression-persistent-cache-v1, queue: max, and cancel-in-progress: false; admitted jobs queue rather than replacing older pending jobs. During maintenance, cancel admitted queued runs as well as pausing ARC. Runner Pods have activeDeadlineSeconds=12600. The runner requests 6 CPU and limits at 8 CPU to preserve minipc-3 allocatable-capacity scheduling headroom; do not reset the request to 8 CPU without revalidating scheduling capacity.

The cache claim query-regression-build-cache is a nominal 600Gi local-path PVC in arc-runners. It is ReadWriteOnce; local-path uses WaitForFirstConsumer binding and Delete reclaim behavior, produces a node-affine local PV, is non-expandable, and the 600Gi request is not a hard storage quota. The runner's minipc-3 selector is its only consumer candidate.

The initializer mounts the PVC root at /cache, creates and write-tests these versioned subpaths as non-root UID/GID 1001, and the runner mounts them as:

Persistent state PVC subpath Runner mount
Ephemeral Cargo home emptyDir /home/runner/.cargo
Cargo registry data cargo-registry-v1 /home/runner/.cargo/registry
Cargo Git data cargo-git-v1 /home/runner/.cargo/git
Cargo target query-regression-target-v1 /home/runner/query-regression-target
Cache metadata meta-v1 /home/runner/query-regression-cache-meta
sccache local disk cache sccache-v1 /home/runner/.cache/sccache
Immutable Rust toolchain image-owned /opt/rustup, /opt/cargo/bin

The Pod security context uses UID/GID and fsGroup 1001 with fsGroupChangePolicy: OnRootMismatch; no privileged chown or raw hostPath is used. CARGO_HOME is a per-Pod emptyDir; only its nested registry and git mounts are persistent. RUSTUP_HOME=/opt/rustup and /opt/cargo/bin are image-owned immutable paths, while CARGO_TARGET_DIR, cache metadata, and SCCACHE_DIR are persistent absolute paths. The runner sets RUSTC_WRAPPER=/usr/local/bin/sccache, SCCACHE_DIR=/home/runner/.cache/sccache, SCCACHE_CACHE_SIZE=40G, and CARGO_INCREMENTAL=0. sccache uses its local PVC disk backend and self-evicts at 40G; do not add runtime downloads, object storage, or a shared backend.

The repository's .cargo/config.toml remains a trusted per-revision build input. In contrast, $CARGO_HOME/config*, credentials, installed bins, and Cargo metadata outside the persistent registry and git data mounts are ephemeral and cannot survive to another Pod.

The local disk backend has a one-server constraint. maxRunners=1 and the unchanged query-regression-persistent-cache-v1 workflow concurrency group serialize runs; do not increase runner capacity or relax that serialization while this backend is in use. Base and candidate builds share the target; Cargo fingerprints invalidate source and dependency changes. The workflow records the sccache version and relevant environment in the target ABI marker, starts and zeros sccache after cache and toolchain checks, shows initial/base/candidate statistics, and resets statistics between base and candidate builds.

Disk preflight and cleanup contract

Before applying or unpausing, verify the backing filesystem on minipc-3 has at least 900GiB free. The current local-path provisioner source is /opt/local-path-provisioner; measure the filesystem containing it:

df -PB1G /opt/local-path-provisioner

The workflow reports du, df -P, human-readable free space, and inode availability before builds and in an always-run report. Its cleanup is narrow and non-destructive:

  • warn at target size 400GiB; at 450GiB clear only the complete target root;
  • warn at Cargo registry-plus-Git data size 60GiB; at 80GiB remove only registry/src and git/checkouts, then abort if that persistent data remains at least 80GiB;
  • below 300GiB backing free space, clear the complete target root first, remeasure, then remove only those Cargo extracted trees and checkouts; abort if free space is still below 300GiB;
  • never automatically remove Cargo registry cache/index, Git database, the image-owned Cargo bin or Rustup toolchain, cache metadata, the self-evicting sccache directory, or the PVC.

The target clear uses fixed absolute roots and removes all entries, including dotfiles. After migration validation, remove obsolete cargo-home-v1 and rustup-home-v1 only in explicit maintenance while ARC is 0/0 and no runner Pod exists; they are not mounted by the current configuration.

Deploy and pause safely

First verify the configured external network route and the disk preflight. Apply the PVC; while the scale set is paused, it is expected to remain Pending because WaitForFirstConsumer has no scheduled runner:

kubectl apply --dry-run=server \
  -f .github/runner-scale-sets/query-regression/cache-pvc.yaml
kubectl apply -f .github/runner-scale-sets/query-regression/cache-pvc.yaml

Render normal and paused configurations. Normal values are always first; the pause overlay is always last:

helm template perf-regression-8-cores \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --namespace arc-runners --version 0.14.2 \
  --set controllerServiceAccount.name=arc-gha-rs-controller \
  --set controllerServiceAccount.namespace=arc-systems \
  -f .github/runner-scale-sets/query-regression/values-8-cores.yaml

helm template perf-regression-8-cores \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --namespace arc-runners --version 0.14.2 \
  --set controllerServiceAccount.name=arc-gha-rs-controller \
  --set controllerServiceAccount.namespace=arc-systems \
  -f .github/runner-scale-sets/query-regression/values-8-cores.yaml \
  -f .github/runner-scale-sets/query-regression/values-paused.yaml

The first post-merge Helm deployment must reconcile the release in paused mode. Keep the pause overlay last:

# First post-merge deployment and every return to paused mode: 0/0.
helm upgrade --install perf-regression-8-cores \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --namespace arc-runners --create-namespace --version 0.14.2 \
  --reset-values --wait \
  -f .github/runner-scale-sets/query-regression/values-8-cores.yaml \
  -f .github/runner-scale-sets/query-regression/values-paused.yaml

# Expect 0/0 and no runner resources before considering normal mode.
kubectl -n arc-runners get autoscalingrunnerset perf-regression-8-cores \
  -o jsonpath='{.spec.minRunners}{"/"}{.spec.maxRunners}{"\n"}'
kubectl -n arc-runners get ephemeralrunners,pods \
  -l actions.github.com/scale-set-name=perf-regression-8-cores

Only after that verification and separate explicit approval, apply normal 0/1 operation without the pause overlay:

helm upgrade --install perf-regression-8-cores \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --namespace arc-runners --create-namespace --version 0.14.2 \
  --reset-values --wait \
  -f .github/runner-scale-sets/query-regression/values-8-cores.yaml

Do not use bare helm rollback, --atomic, or --reuse-values: a stored revision can restore nonzero runner capacity. Inspect rendered manifests for capacity, the minipc-3 selector, cache claim and mounts, initializer, security context, and resources. After approved normal mode receives its first canary, the PVC binds to minipc-3.

For local-PV node loss, cache recovery is intentionally disposable: return to 0/0, recreate the PVC on a healthy node, cold-fill it, and run a new canary.

Canary and rollback

With explicit approval, run two identical workflow_dispatch canaries on perf-regression-8-cores, using immutable full base and candidate commit SHAs and cargo_profile=nightly. The first is the cold fill; the second verifies warm reuse. Record the workflow's base/candidate build elapsed logs and cache ABI-marker output, initial/base/candidate sccache statistics, and cache report. Confirm the image tool contract (root-owned Rustup/Cargo paths, baked nightly, and non-writable /opt roots) and that the ephemeral Cargo home contains only the mounted registry/Git data before Cargo creates per-Pod state. Obtain dependency and tool network byte counters from the configured environment counter source, filtered to minipc-3 and the dependency/tool destinations.

Accept the canary only when all of the following hold:

  • exactly one runner Pod runs on minipc-3, and both jobs use the same bound PV;
  • UID/GID 1001 cache mounts are writable; the warm run does not invalidate the target or bulk-redownload crates or toolchains; sccache reports separate base and candidate build statistics without server or cache-path errors; immutable Rustup/Cargo roots remain non-writable and only registry/Git data persists;
  • warm base build time is at most 50% of cold base build time;
  • warm dependency/tool network bytes are at most 10% of cold fill bytes;
  • cache sizes remain below soft watermarks, node free space remains at least 300GiB, and the benchmark is correct without TLS EOFs or timeouts;
  • the configured environment counter source confirms this traffic is outside VPN accounting.

Immediately return to 0/0 after either canary unless ongoing normal operation has been explicitly approved; return immediately on any traffic, cache, disk, TLS, or correctness failure. To roll back, use the paused Helm upgrade above, or another explicit helm upgrade --install with known-good values followed by values-paused.yaml, --reset-values, and --wait. Do not delete the PVC automatically; preserve it for diagnosis unless intentionally discarding cache.

Future optional phases

The current phase uses a digest-pinned image with sccache 0.16.0 and no shared cache service. Optional follow-ups are an image additionally seeded with the exact Rust toolchain and cargo fetch --locked; or an internal read/write sccache backend or Cargo/Git mirror. Evaluate them only if persistent PVC reuse is insufficient.