Skip to content

🧪 test(collect): make the load test assert something it can fail (PERF-FIX-04) - #297

Merged
konih merged 2 commits into
mainfrom
fix/perf-fix-04-honest-load-test
Aug 16, 2026
Merged

🧪 test(collect): make the load test assert something it can fail (PERF-FIX-04)#297
konih merged 2 commits into
mainfrom
fix/perf-fix-04-honest-load-test

Conversation

@konih

@konih konih commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

PERF-FIX-04 — make the load test load-test something

test/load/collect_test.go called extractor.Extract on one identical object in a tight single-threaded loop, with no assertion beyond "did not return an error" — so it could not fail for being slow. It duplicated BenchmarkExtract while carrying the name load-test:10k and a nightly ubuntu-8-core CI job, which reads as cluster-scale evidence.

The danger was never that it was fast. It was that a green nightly implied scale confidence nobody measured.

What changed

  • test/load/ deleted entirely, along with its //go:build load tag. .golangci.yaml has no build-tags key, so that code was never linted — removing it is a net lint-coverage gain.
  • New TestExtractHotPathBudget lives in package collect beside BenchmarkExtract, both driving one shared extractWorkload.
  • Objects are varied: a deterministic 128-object pool built outside the timed region, ~1 in 7 omitting status.readyReplicas so the CEL has() takes both branches.
  • Ceilings that can actually fail, plus a vacuous-pass guard (N>0 and non-zero alloc metrics) so a failed benchmark reporting zeroes cannot silently satisfy every ceiling and recreate the original defect.
  • CI job load-test-10k (permanently gated behind run_scale_jobs) replaced by extract-budget, which actually runs every nightly. scale-envtest-10k — the real scale job — is untouched.

ON THE RECORD — AC1 is amended

Acceptance said ">25% throughput regression or memory ceiling exceeded ⇒ fails". Independent review proved the throughput half did not hold: an injected CPU-only regression made Extract 3.5× slower (39144 → 11078 ops/s) at unchanged B/op/allocs/op and passed both ceilings.

AC1 is met for the allocation dimensions only. The wall-clock ceiling is a coarse catastrophic-regression net, not a >25% gate. This was decided, not silently dropped: B/op and allocs/op are hardware-independent (demonstrated across two Go patch releases, two architectures, -race and -cover), while ns/op is not — a reviewer's unloaded machine measured 1.47× the baseline with no regression present, so a 1.25× ceiling would false-red on the fastest hardware available. A flaky gate is worse than no gate.

Nine surfaces that claimed a ">25% wall-clock gate" are corrected, and the counterexample is written into the test header and ADR-0706 so nobody re-derives the false claim.

Verification

task test:run, task lint, task verify, task extract-budget, task bench, task lint:shell, task lint:markdown — all exit 0. AC1 re-proven red-then-green per dimension by an independent reviewer on this exact head.

Deferred deliberately: nightly -race margin and native-x86 calibration → follow-up story (needs CI measurements that do not exist yet).

konih added 2 commits August 16, 2026 08:42
…F-FIX-04)

`test/load` was never a load test. It called `extractor.Extract` on ONE
identical object in a single-threaded loop with no API server, cluster, sinks,
controller or concurrency, and asserted nothing beyond "did not return an
error" — so it could not fail for being slow, allocating more, or regressing.
It duplicated `BenchmarkExtract` while carrying the name `load-test:10k` and a
nightly `ubuntu-latest-8-cores` job, which read as cluster-scale evidence
nobody had measured. `test/load/load_test.go` was worse: it asserted env-var
parsing and logged.

Option (b) from the story — make it honest — plus assertions that can fail:

- Delete `test/load/` and the `load` build tag (untagged code was invisible to
  golangci-lint, so this also widens lint coverage).
- Add `TestExtractHotPathBudget` in `internal/collect`, driving the SINGLE
  shared `extractWorkload` that `BenchmarkExtract` also drives — one workload
  definition, no duplication. Test-package functions are not importable across
  packages, so co-location is the only way to genuinely share it.
- Vary the object: a deterministic pool of 128 distinct Deployments, built
  outside the timed region, ~1 in 7 omitting `status.readyReplicas` so the CEL
  `has()` expression takes both branches.
- Assert three ceilings against a recorded baseline +25%, with a vacuous-pass
  guard (N > 0, non-zero alloc counters) so a broken benchmark cannot report
  zeroes and sail through every check.

Calibration, because a test that false-reds is worse than one that cannot fail.
B/op and allocs/op are hardware-independent for a fixed toolchain, so they sit
at baseline +25% and carry the regression signal. ns/op is not, and it is
measured under two different conditions:

- In the required PR gate this runs inside `go test $(go list ./...)
  -coverprofile`, i.e. instrumented (+9% measured) while ~40 sibling packages,
  several spinning envtest apiserver/etcd, contend for the runner. Two
  concurrent commands alone cost 2.8x on an 18-core laptop. The in-code default
  is therefore a deliberately absurd 50x net for order-of-magnitude regressions.
- `task extract-budget` runs the test alone and uninstrumented, so it pins
  KOLECT_EXTRACT_MAX_NS_PER_OP=204408 (8x baseline) — ~6x tighter, and that is
  where the real latency gate lives. Calibrated hardware should set it to its
  own measured ns/op x 1.25 for the literal >25% floor.

Baseline (S-LOCAL, darwin/arm64 Apple M5 Max, go1.26.5, worst of -count=5):
25551 ns/op · 32218 B/op · 452 allocs/op. Varying the pool did not move the
allocation profile from the single-object measurement it replaces.

Naming and CI:

- `task load-test` / `task load-test:10k` -> `task extract-budget`, whose
  description states what is and is not exercised so `task --list` cannot
  mislead.
- Workflow job `load-test-10k` (8-core, disabled behind `run_scale_jobs`) ->
  `extract-budget` on plain `ubuntu-latest`, no envtest cache, running every
  nightly instead of never. `scale-envtest-10k` remains the only 10k scale job.
- `KOLECT_LOAD_TEST` / `KOLECT_LOAD_TEST_MAX` are gone; `hack/perf-report.sh`
  no longer derives a "load" scale tier from a dead env var.
- LAB-DOC-03 now fails closed if a `load-test-10k` job reappears.
- ADR-0603/0706/0707 (incl. the L5 tier row, mermaid node and ownership note),
  REQUIREMENTS NFR-PERF-1, performance.md, the load-test runbook tiers,
  testing.md, guidelines.md and setup.md restated: this is a micro-benchmark
  budget, not a scale tier.
… not enforce

Independent review, F1 (P1): seven surfaces said `task extract-budget` fails a
>25% ns/op regression. It does not. The reviewer injected a CPU-only slowdown
into `Extract` and measured throughput fall 39144 -> 11078 ops/s (-72%, 3.5x)
at UNCHANGED B/op and allocs/op. It passed both the task-pinned ceiling
(204408) and the in-code default (1596900). The claim was false on every
surface that made it.

Coordinator decision: fix the overclaiming, not the ceiling. Do NOT tighten
ns/op. The asymmetry argument stands and the reviewer independently confirmed
it — B/op is hardware-independent (arm64 32217 vs amd64 32212, unchanged under
-cover, 32980/459 under -race, and unchanged across go1.26.5 -> go1.26.6) while
ns/op had only a 4.65x margin under -race on the fastest hardware available, so
tightening would red the nightly. A flaky wall-clock gate is worse than none.

ON THE RECORD, so a later reader does not think it was dropped silently:
PERF-FIX-04's acceptance criterion "throughput regresses >25% => test FAILS" is
AMENDED to be met for the ALLOCATION dimensions only. B/op and allocs/op are a
real >25% gate that fails on any runner; the ns/op ceiling is a
catastrophic-regression net and is now described as exactly that everywhere.
This amendment is recorded in the test's own header comment, not just here.

Corrected surfaces: Taskfile.yml, hack/task/Taskfile.test.yml, ADR-0706,
docs/development/{guidelines,testing,setup}.md, docs/operator-manual/
performance.md, docs/operator-manual/load-test-runbook.md, and the
CALIBRATION / headroom / assertion comments in extractor_budget_test.go.
performance.md and setup.md had additionally contradicted themselves inside a
single section. The ns/op failure message now says what breaching it means.

Also from review:

- F5 (P3): dropped `CGO_ENABLED: "1"` from test:extract-budget — carried over
  from the deleted load-test task; nothing here needs cgo. Verified green.
- F6 (P3): dropped the "+9% coverage instrumentation" claim. The reviewer
  measured -cover at 24994 ns/op, marginally FASTER than uninstrumented. The
  50x default absorbs runner contention, not -cover; the comment now says so.
- F7 (P3): `task bench` and hack/perf-report.sh ran the whole package, so a red
  TestExtractHotPathBudget could fail a benchmark task. Both now pass
  `-run='^$'`, making them pure benchmark passes as their names imply. Proved:
  KOLECT_EXTRACT_MAX_BYTES_PER_OP=1 task bench is still green.
- MD046: the new performance.md admonition tripped the same indented-code-block
  trap as 4ccba73; promoted to a subsection, per that fix.

F2 (nightly -race margin) and F3 (never measured on native ubuntu x86) are
deliberately NOT addressed here — the coordinator is filing them as a follow-up
to re-pin against the first real nightly `extract-budget` ns/op from CI.

Rebased onto origin/main (PERF-FIX-06 Go 1.26.6 + CHANGELOG sync), 0 conflicts.
Baseline re-measured under go1.26.6: 32218 B/op / 452 allocs/op unchanged,
ns/op 25077-26311 across -count=5, so the recorded 25551 still holds.
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@sonarqubecloud

Copy link
Copy Markdown

@konih
konih merged commit 736d556 into main Aug 16, 2026
36 checks passed
@konih
konih deleted the fix/perf-fix-04-honest-load-test branch August 16, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant