Skip to content

perf(ring): prevent resize oscillation - #179

Merged
appleboy merged 1 commit into
masterfrom
perf/ring-shrink-hysteresis
Oct 31, 2026
Merged

appleboy merged 1 commit into
masterfrom
perf/ring-shrink-hysteresis

Conversation

@appleboy

@appleboy appleboy commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Summary and scope

A Ring backlog oscillating between 1,024 and 1,025 entries used to grow from 1,024 to 2,048 slots and immediately shrink back on every enqueue/dequeue pair. That allocated two backing arrays and copied roughly 2,048 backlog entries per pair while holding the Ring mutex.

Shrink only when utilization is at most 25%, while retaining growth at full capacity. After growth to 2,048, draining to 1,024 no longer shrinks; draining to 512 halves the buffer. This separates growth/shrink thresholds and removes repeated allocations/copies. Capacity limits, FIFO, clearing dequeued references, and minimum capacity 2 remain unchanged.

This PR changes only ring.go, ring_resize_test.go, and ring_performance_test.go. No Queue scheduler, timeout, retry, or callback changes. It targets master directly and can merge independently of the Queue worker-reuse PR #180. Split from #178; no issue-closing keyword is intended.

Flow

flowchart TD
    Queue[Existing Ring.Queue: grow when full] --> Request[Ring.Request: dequeue and clear reference]
    Request --> Threshold{Remaining count at most one quarter?}
    Threshold -->|Yes, capacity above minimum| Shrink[Existing resize: halve and preserve FIFO]
    Threshold -->|No| Keep[Keep current backing array]
    style Request fill:#dff0d8,stroke:#3c763d
    style Threshold fill:#dff0d8,stroke:#3c763d
Loading

Standalone performance results

Fresh measurements on 2026-10-31 compare baseline 9721be66093a0e2889129e4751154d8388698b98 with only this PR at 59d05041a4d2c8195d96883220b5cf5a1e2ac28b. These replace the combined-change numbers from #178. Every summary value is the median of six samples.

Scenario Before ns/pair After ns/pair Time reduction Derived throughput gain
Backlog 64 464.35 9.471 98.0% 4802.9%
Backlog 1,024 4,997.5 9.173 99.8% 54380.5%

Time reduction = 1 - after/before; throughput gain = before/after - 1.

Scenario Before → after B/op Before → after allocs/op
Backlog 64 3,456 → 0 2 → 0
Backlog 1,024 51,200 → 0 2 → 0
Scenario Before time range, ns/op After time range, ns/op
Backlog 64 455.5–642 9.283–9.72
Backlog 1,024 4,937–5,164 9.105–9.453

Measurement method and limitations

  • Apple M4 Pro, darwin/arm64, Go 1.26.10, GOMAXPROCS=12; 500ms per case, three rounds with two samples per round.
  • Interleaved sequence: baseline → Ring-only → Queue-only; Queue-only → baseline → Ring-only; Ring-only → Queue-only → baseline. Runs were sequential, without simultaneous test/lint runs or race instrumentation.
  • Baseline included both benchmark families. Each changed version ran only its relevant family; the matching benchmark source file is byte-identical between that version and baseline.
  • These are microbenchmarks on one machine. Scheduler, GC, and background load cause variability. No statistical-significance claim is made; the ranges above should be considered alongside medians.
  • One operation is a successful enqueue/dequeue pair at the indicated stable backlog. This specifically triggers the old resize oscillation; it is not an overall Queue throughput multiplier. Initial growth still allocates; allocation metrics round to zero after amortization.

Raw samples and source hashes

All relevant samples are embedded here; no report, plan, or raw-data files are added to the repository. Baseline output below is filtered to this PR’s benchmark family; other case lines are omitted, but environment, command, round markers, and hashes are preserved.

baseline — six samples per case
# baseline
# source commit: 9721be66093a0e2889129e4751154d8388698b98
# 2026-10-31T10:59:28.726129+08:00
# go test -run ^$ -bench ^(BenchmarkRingBoundary|BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd)$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# ring.go SHA256: 9d2cf12d3f5a02c7cebcc75c2d032122243384063d3b1f837fbe0d0eb4b9600f
# queue.go SHA256: fd7b78f7269d316ffa25bfe4ebd0a81664431d6d08534df8e29fc357e6545fdf
# ring_performance_test.go SHA256: 133d56843339be04d5a3253f903571f4effe06103173cf3a37b1f0c5b79ed11b
# queue_performance_test.go SHA256: e6ef2c4acd222d077b82bbbbd9cf04e2411ea0eed3513a9d7a17d484e061859f

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12          	 1000000	       642.0 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=64-12          	 1177713	       492.2 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12        	  109708	      4990 ns/op	   51200 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12        	  121315	      5164 ns/op	   51200 B/op	       2 allocs/op
PASS
ok  	github.com/golang-queue/queue	9.165s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12          	 1319043	       468.4 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=64-12          	 1281014	       460.3 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12        	  113829	      5005 ns/op	   51200 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12        	  119961	      4937 ns/op	   51200 B/op	       2 allocs/op
PASS
ok  	github.com/golang-queue/queue	9.081s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12          	 1330266	       459.7 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=64-12          	 1332910	       455.5 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12        	  124999	      4984 ns/op	   51200 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12        	  110356	      5030 ns/op	   51200 B/op	       2 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.472s
ring — six samples per case
# ring
# source commit: 59d05041a4d2c8195d96883220b5cf5a1e2ac28b
# 2026-10-31T10:59:28.726468+08:00
# go test -run ^$ -bench ^BenchmarkRingBoundary$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# ring.go SHA256: d38f2e662c201c46278d084a751b26188a7c119c4ddd016630a6a0288f51d5d7
# queue.go SHA256: fd7b78f7269d316ffa25bfe4ebd0a81664431d6d08534df8e29fc357e6545fdf
# ring_performance_test.go SHA256: 133d56843339be04d5a3253f903571f4effe06103173cf3a37b1f0c5b79ed11b

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12         	64632837	         9.554 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12         	63701596	         9.720 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12       	64113126	         9.368 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12       	64855224	         9.453 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/golang-queue/queue	3.310s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12         	64732195	         9.684 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12         	63883876	         9.283 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12       	66038384	         9.105 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12       	66727216	         9.176 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/golang-queue/queue	2.645s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12         	64426225	         9.324 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12         	64548520	         9.388 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12       	66505022	         9.170 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12       	66017799	         9.125 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/golang-queue/queue	3.124s

Verification

Setup: check out perf/ring-shrink-hysteresis at 59d05041a4d2c8195d96883220b5cf5a1e2ac28b; run from the repository root with Go 1.26.10 and a working race-detector toolchain. Run go mod download for dependencies; lint tooling is pinned in go.tools.mod. No external service, account, or persistent test data is needed.

Check / command Expected behavior Status and observed result
go tool -modfile=go.tools.mod golangci-lint fmt No further formatting changes Passed
go test -race ./... Complete suite passes without races or goleak failures Passed on this standalone branch
go tool -modfile=go.tools.mod golangci-lint run No diagnostics Passed: 0 issues
git diff --check and staged check before commit No whitespace errors Passed

Behavioral regression: run go test -race -run '^TestRingShrinkHysteresisAndFIFO$' -timeout=30s .. The test fills 16 entries, drains to 8 (capacity must remain 16), drains to 4 (capacity becomes 8), then refills and drains across wrap/grow/shrink. Expected: original order preserved and final capacity 2. Passed in the full race suite and in the combined-branches compatibility check. Existing capacity/shutdown tests also passed.

Benchmark reproduction: export baseline 9721be66093a0e2889129e4751154d8388698b98 to a disposable directory and copy this PR's ring_performance_test.go into it. Run the same command in baseline and this branch, alternating order across three rounds:

go test -run '^$' -bench '^BenchmarkRingBoundary$' -benchmem -benchtime=500ms -count=2 -cpu=12 .

Passed: six samples per case/version recorded above. Expected: oscillation disappears and steady-state allocation metrics round to zero; exact timings depend on the machine. Tests/benchmarks keep data in memory; delete the disposable baseline directory afterward. Not run: production/multi-producer soak or tail-latency measurements; outside this targeted comparison.

Classification and AI authorship

  • Core change: shared in-memory backend; recommend module owner plus another reviewer.
  • AI tool: OpenAI Codex.
  • AI-authored files: ring.go, ring_resize_test.go, ring_performance_test.go.
  • Human line-by-line reviewed: None — not yet reviewed by a human.
  • Goal/scope is documented in this description; plan/report files intentionally remain local.
  • Security: no credentials included and no security-sensitive public interface changed.

Risk, rollback, and reviewer focus

Shrinking later retains larger backing arrays at some occupancies, trading memory retention for fewer copies and allocations. Review the quarter-full condition and FIFO checks. Roll back by reverting this PR's single commit; no migration or Queue scheduler change is involved.

Shrink only at quarter utilization and add FIFO regression coverage and capacity-boundary benchmarks.
@appleboy
appleboy merged commit eaf4947 into master Oct 31, 2026
6 checks passed
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