proxy: add mirror-selection benchmarks #123

Merged
benvin merged 1 commits from benvin/proxy-selection-benchmarks into master 2026-08-13 17:40:22 +10:00
Member

Why

We added a mirror load-balancing strategy (round_robin / least_conn) in #122 and need hard numbers on whether mirror selection adds meaningful request latency. This PR adds microbenchmarks that isolate the selection cost (no network/DB/redis) and commits the results as a permanent regression guard.

How

internal/proxy/selection_bench_test.go builds a zero-value Engine (same setup as leastconn_test.go / multibaseurl_test.go) and calls the selection functions directly:

  • BenchmarkBaseURLAttemptOrder_SingleURL — the len<=1 early-return no-op path
  • BenchmarkBaseURLAttemptOrder_RoundRobin — pools of 3 and 8
  • BenchmarkBaseURLAttemptOrder_LeastConn — pools of 3 and 8, with in-flight skew preloaded on the gauges
  • BenchmarkBeginEndAttempt — the gauge inc/dec pair
  • _Parallel (RunParallel) variants of RR / least_conn / begin-end to surface sync.Map + atomic contention

Run: go test -run=^$ -bench='BaseURLAttemptOrder|BeginEndAttempt' -benchmem -benchtime=1s -count=6 -cpu=8 ./internal/proxy/

internal/proxy/BENCHMARKS.md has the median-of-6 table (Ryzen 7 4700U, go1.26.5) plus the raw runs.

Headline numbers (median of 6, ns/op — sequential | parallel@8)

selection seq ns/op parallel ns/op allocs/op
single-URL ~133 1
round_robin (3) ~462 ~67 4
round_robin (8) ~682 ~130 4
least_conn (3) ~2690 ~292 22
least_conn (8) ~15200 ~1673 132
beginAttempt+endAttempt ~514 ~72 4

Key caveat: baseURLAttemptOrder is only called from headUpstream / fetchFromUpstream / checkUpstream — the cache-miss/upstream path. A cache hit returns Source: "cache" before any selection runs, so the cache-hit hot path pays zero selection cost regardless of strategy.

Verdict: even the worst case (least_conn across 8 mirrors, ~15 µs) is <1% of the multi-millisecond upstream round-trip it accompanies; round_robin (~0.5 µs) is negligible. The strategy adds no meaningful latency. least_conn's cost scales super-linearly because the sort.SliceStable comparator re-derives each mirror's gauge via inflightCounter (string-concat key + speculative new(atomic.Int64)) O(n·log n) times — a possible future micro-opt (snapshot loads before sorting), out of scope here.

## Why We added a mirror load-balancing strategy (round_robin / least_conn) in #122 and need hard numbers on whether mirror *selection* adds meaningful request latency. This PR adds microbenchmarks that isolate the selection cost (no network/DB/redis) and commits the results as a permanent regression guard. ## How `internal/proxy/selection_bench_test.go` builds a zero-value `Engine` (same setup as `leastconn_test.go` / `multibaseurl_test.go`) and calls the selection functions directly: - `BenchmarkBaseURLAttemptOrder_SingleURL` — the `len<=1` early-return no-op path - `BenchmarkBaseURLAttemptOrder_RoundRobin` — pools of 3 and 8 - `BenchmarkBaseURLAttemptOrder_LeastConn` — pools of 3 and 8, with in-flight skew preloaded on the gauges - `BenchmarkBeginEndAttempt` — the gauge inc/dec pair - `_Parallel` (`RunParallel`) variants of RR / least_conn / begin-end to surface sync.Map + atomic contention Run: `go test -run=^$ -bench='BaseURLAttemptOrder|BeginEndAttempt' -benchmem -benchtime=1s -count=6 -cpu=8 ./internal/proxy/` `internal/proxy/BENCHMARKS.md` has the median-of-6 table (Ryzen 7 4700U, go1.26.5) plus the raw runs. ## Headline numbers (median of 6, ns/op — sequential | parallel@8) | selection | seq ns/op | parallel ns/op | allocs/op | |---|---:|---:|---:| | single-URL | ~133 | — | 1 | | round_robin (3) | ~462 | ~67 | 4 | | round_robin (8) | ~682 | ~130 | 4 | | least_conn (3) | ~2690 | ~292 | 22 | | least_conn (8) | ~15200 | ~1673 | 132 | | beginAttempt+endAttempt | ~514 | ~72 | 4 | **Key caveat:** `baseURLAttemptOrder` is only called from `headUpstream` / `fetchFromUpstream` / `checkUpstream` — the cache-miss/upstream path. A cache hit returns `Source: "cache"` before any selection runs, so the **cache-hit hot path pays zero** selection cost regardless of strategy. **Verdict:** even the worst case (least_conn across 8 mirrors, ~15 µs) is <1% of the multi-millisecond upstream round-trip it accompanies; round_robin (~0.5 µs) is negligible. The strategy adds no meaningful latency. least_conn's cost scales super-linearly because the `sort.SliceStable` comparator re-derives each mirror's gauge via `inflightCounter` (string-concat key + speculative `new(atomic.Int64)`) O(n·log n) times — a possible future micro-opt (snapshot loads before sorting), out of scope here.
unkin-agent added 1 commit 2026-08-13 17:32:19 +10:00
proxy: add mirror-selection benchmarks
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
f555f41ada
Quantify the latency the mirror load-balancing strategy adds versus a
single-URL remote, and lock it in as a regression guard.

- selection_bench_test.go: isolates baseURLAttemptOrder / beginAttempt /
  endAttempt against a zero-value Engine (no network/DB/redis), covering
  single-URL, round_robin (3/8), least_conn (3/8) and the gauge inc/dec
  pair, plus RunParallel variants to surface sync.Map/atomic contention.
- BENCHMARKS.md: median-of-6 results on a Ryzen 7 4700U with the raw runs
  and the cache-miss-only caveat.

Headline (median of 6, ns/op): single-URL ~133; round_robin ~462 (3) /
~682 (8); least_conn ~2690 (3) / ~15200 (8). Selection runs only on
cache-miss, so cache hits pay zero.
benvin merged commit bc8a72e5cc into master 2026-08-13 17:40:22 +10:00
benvin deleted branch benvin/proxy-selection-benchmarks 2026-08-13 17:40:22 +10:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unkin/artifactapi#123