From 3565dd010ee999ba5d1aa41a422ce8bce46683ae Mon Sep 17 00:00:00 2001 From: Andrew Hu Date: Fri, 6 Nov 2026 17:20:23 -0500 Subject: [PATCH] =?UTF-8?q?fix(test):=20=F0=9F=90=9B=20pause=20the=20colle?= =?UTF-8?q?ctor=20while=20an=20allocation=20gate=20measures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Volume commit gate failed on macOS at 252,064 bytes per run against a ceiling of 252,032. One commit allocates about 252 KB, so the 100 commits of a measurement trigger several garbage collections. Each collection empties the sync.Pool caches. The commits then allocate again the objects that those caches held, and the amount depends on the timing of each collection. - The gates now pause the collector during each measurement. A collection still runs before each measurement. - With the collector paused, the commit gate measures 135 allocations in all 150 runs, so this change lowers its allocation ceiling from 136 to 135. Its byte ceiling stays at 252,032. - In a few runs, the read gates measure two more allocations than usual, and pausing the collector does not remove that difference. Their ceilings already cover both counts and remain unchanged. - docs/BENCHMARKS.md explains the pause and lists the new ceiling. Co-Authored-By: Claude Opus 5.5 --- docs/BENCHMARKS.md | 7 +++++-- internal/testutil/allocgate.go | 9 ++++++++- lode/volume/alloc_test.go | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index df3e0581..f963ad1c 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -78,7 +78,10 @@ given toolchain, so the gates run in every `go test` without the race detector, which adds allocations of its own. The gates count the allocations of the whole test binary, so each measurement starts after a garbage collection; a collection that earlier tests leave pending would -otherwise add to a figure. The byte ceiling catches a +otherwise add to a figure. No collection runs during a measurement either. +A collection empties the caches of `sync.Pool`, and the measured operation +then allocates what it would have reused, by an amount that depends on when +the collection falls. The byte ceiling catches a single oversized allocation, such as a fixed buffer, which the allocation count does not; each byte ceiling is the measured figure rounded up to the next 64 bytes, because map growth in the in-memory store varies it by a few @@ -93,7 +96,7 @@ figures in the other sections were measured with Go 1.25. | Record StreamWrite of three records | 63 | 7,296 | | JSONL Decode of three records | 33 | 5,504 | | Volume ReadAt within one of ten blocks | 41 | 10,816 | -| Volume Commit of one block onto a small volume | 136 | 252,032 | +| Volume Commit of one block onto a small volume | 135 | 252,032 | A change that exceeds a ceiling fails the test and must justify the increase in its pull request before the ceiling is raised. A change that reduces a diff --git a/internal/testutil/allocgate.go b/internal/testutil/allocgate.go index 0a1b49bc..927d2bb6 100644 --- a/internal/testutil/allocgate.go +++ b/internal/testutil/allocgate.go @@ -2,6 +2,7 @@ package testutil import ( "runtime" + "runtime/debug" "testing" ) @@ -34,7 +35,10 @@ const AllocGateCalls = 1 + (gateRuns + 1) + gateRuns // toolchain, but the race detector adds allocations of its own, so callers // exclude race builds. Each measurement starts after a garbage collection: // the figures count the allocations of the whole test binary, and a -// collection that earlier tests leave pending otherwise adds to them. Each +// collection that earlier tests leave pending otherwise adds to them. No +// collection runs during a measurement either: a collection empties the +// caches of sync.Pool, and the operation then allocates what it would have +// reused, by an amount that depends on when the collection falls. Each // gate logs the ceilings its measured figures call for, so `task gates` // reports them without an edit to the gate table. func RunAllocGates(t *testing.T, gates []AllocGate) { @@ -43,6 +47,9 @@ func RunAllocGates(t *testing.T, gates []AllocGate) { t.Run(g.Name, func(t *testing.T) { op := g.Setup(t) op() + // A collection still runs on runtime.GC, but none starts on its + // own until the gate restores the collector's target. + defer debug.SetGCPercent(debug.SetGCPercent(-1)) runtime.GC() allocs := testing.AllocsPerRun(gateRuns, op) runtime.GC() diff --git a/lode/volume/alloc_test.go b/lode/volume/alloc_test.go index f8536b42..84abf7a4 100644 --- a/lode/volume/alloc_test.go +++ b/lode/volume/alloc_test.go @@ -24,7 +24,7 @@ const ( // in-memory store varies it by a few bytes. var allocGates = []testutil.AllocGate{ {Name: "ReadAt within one of ten blocks", Allocs: 41, Bytes: 10816, Setup: gateReadAt}, - {Name: "Commit of one block onto a small volume", Allocs: 136, Bytes: 252032, Setup: gateCommit}, + {Name: "Commit of one block onto a small volume", Allocs: 135, Bytes: 252032, Setup: gateCommit}, } // TestAllocationGates holds each Volume hot path to its allocation ceiling.