I diagnosed the problem, built a verified standalone port, and cross-checked it against the actual Go 1.26 runtime pacer via a go test -overlay oracle. Everything passes. Full solution is at ~/gcpacer/SOLUTION.md; below is the complete write-up.
I diagnosed the problem, built a verified standalone port, and cross-checked it against the actual Go 1.26 runtime pacer via a go test -overlay oracle. Everything passes. Full solution is at ~/gcpacer/SOLUTION.md; below is the complete write-up.
The pacer is a floating-point feedback controller. The bug class is not a logic error that a "mathematically equivalent" rewrite avoids — it is that any rewrite of the operand grouping, integer truncation, or branch order changes the low bits of heapGoal, trigger, and the assist ratio, which then feeds back through consMark/runway and diverges every subsequent cycle. The fix is a mechanical, expression-for-expression transcription of src/runtime/mgcpacer.go (plus the assist-credit arithmetic in src/runtime/mgcmark.go/malloc.go) into a pure, deterministic package, verified against the actual runtime via an injected go test -overlay probe.
Working module: ~/gcpacer (oracle under ~/refprobe).
The task ("exact trigger heap sizes, assist-credit balances, heap goal after N cycles") fails against a naive reimplementation for these reasons, in rough order of how much damage they do:
gcPercentHeapGoal = heapMarked + (heapMarked+lastStackScan+globalsScan)*pct/100 multiplies then divides. heapMarked + (heapMarked+...)*(pct/100) is a different number.memoryLimitHeapGoal computes headroom := goal/100*percent — divide first, then multiply. goal*percent/100 differs whenever goal%100 >= 34 (at a 3% headroom). This single line moves the goal by up to a few bytes.
The memory-limit branch changes what other adjustments are allowed. When memoryLimitHeapGoal() < gcPercentHeapGoal, heapGoalInternal returns early with minTrigger == 0. Only in the else branch are the sweep-distance trigger and the 64 KiB minimum-runway correction applied, and only there is minTrigger propagated to trigger(). Reversing this ordering (e.g. always applying min-runway) makes memory-limited cycles overshoot.
Conditional corrections.
c.triggered != ^uint64(0) && goal < c.triggered+64KiB.maxTrigger is capped at goal - defaultHeapMinimum only if goal > defaultHeapMinimum (strict).memoryLimitHeapGoal clamps to heapMarked last, and uses the odd goal < headroom || goal-headroom < headroom condition to avoid underflow.
The mid-cycle overshoot/undershoot corrections are coupled.
work > scanWorkExpected, heapGoal is extrapolated to maxScanWork and then capped by hardGoal = (1+GOGC/100)*heapGoal.Overshoot: if live > heapGoal, heapGoal = int64(float64(heapGoal)*1.1) and scanWorkExpected = maxScanWork. Both feed scanWorkRemaining (floored at 1000) and heapRemaining (floored at 1) before the assist ratio is formed.
State carried between cycles.
endCycle sets consMark = max(current, last 4) then shifts the history.resetLive sets heapScan = heapScanWork (not lastHeapScan), updates lastHeapScan/lastStackScan, and sets triggered = ^uint64(0).commit computes runway = uint64(consMark*(1-0.25)/0.25 * (lastHeapScan+lastStackScan+globalsScan)). A last-bit difference here shifts the trigger of the next cycle while leaving the goal unchanged — exactly the "trigger vs goal separate" symptom.
Assist credit has its own rounding contract. gp.gcAssistBytes += 1 + int64(assistBytesPerWork*float64(x)) is a deliberate round-up. scanWork < gcOverAssistWork forces scanWork = 64<<10 and recomputes debtBytes. Omitting the 1+ or the slack branch makes credit balances drift.
Build-configuration constant. defaultHeapMinimum is HeapMinimum512KiBInt*512KiB + (1-HeapMinimum512KiBInt)*4MiB. In the default Go 1.26 toolchain the heapminimum512kib experiment is off, so the value is 4<<20. Using 512 KiB changes every trigger for small heaps.
The "root cause" is therefore: the pacer is defined operationally, not algebraically. The only correct fix is a transcription that preserves every operation's type, order, and truncation, and an oracle test that proves it.
The module is at ~/gcpacer:
| File | Purpose |
|---|---|
go.mod |
module gcpacer, go 1.26 |
pacer.go |
expression-for-expression transcription (412 lines) |
cmd/trace/main.go |
5-cycle scripted allocation trace printer |
crosscheck_test.go |
asserts the port equals runtime goldens bit-for-bit |
unit_test.go |
truncation-order, overshoot, extrapolation, assist-credit tests |
overlay.json + ~/refprobe/zz_pacer_probe_test.go |
runtime oracle injected into go test runtime |
verify.sh |
one-shot reproduction |
The transcription (pacer.go) reproduces all pacer functions — SetGCPercent, SetMemoryLimit, StartCycle, Revise, EndCycle, ResetLive, HeapGoal, HeapGoalInternal, MemoryLimitHeapGoal, Trigger, Commit, plus DeductAssistCredit/assistAlloc. The full source is in SOLUTION.md and on disk. Critical excerpts:
// gcPercentHeapGoal: multiply before integer divide.
gcPercentHeapGoal = c.HeapMarked + (c.HeapMarked+c.LastStackScan+c.GlobalsScan)*uint64(gcPercent)/100
// memoryLimitHeapGoal: divide first, min headroom, odd underflow guard, clamp last.
headroom := goal / 100 * memoryLimitHeapGoalHeadroomPercent
if headroom < memoryLimitMinHeapGoalHeadroom { headroom = memoryLimitMinHeapGoalHeadroom }
if goal < headroom || goal-headroom < headroom { goal = headroom } else { goal = goal - headroom }
if goal < c.HeapMarked { goal = c.HeapMarked }
// revise: extrapolate, hard cap, overshoot, then floors.
hardGoal := int64((1.0 + float64(gcPercent)/100.0) * float64(heapGoal))
if int64(live) > heapGoal { heapGoal = int64(float64(heapGoal) * 1.1); scanWorkExpected = maxScanWork }
if scanWorkRemaining < 1000 { scanWorkRemaining = 1000 }
if heapRemaining <= 0 { heapRemaining = 1 }
// trigger bounds: integer divide by 64 first; cap only if goal > defaultHeapMinimum.
triggerLowerBound := ((goal-c.HeapMarked)/triggerRatioDen)*minTriggerRatioNum + c.HeapMarked
maxTrigger := ((goal-c.HeapMarked)/triggerRatioDen)*maxTriggerRatioNum + c.HeapMarked
if goal > defaultHeapMinimum && goal-defaultHeapMinimum > maxTrigger { maxTrigger = goal - defaultHeapMinimum }
// commit runway.
c.Runway = uint64((c.ConsMark * (1 - gcGoalUtilization) / (gcGoalUtilization)) * float64(c.LastHeapScan+c.LastStackScan+c.GlobalsScan))
// assist round-up.
g.GCAssistBytes += 1 + int64(assistBytesPerWork*float64(workDone))
Also encoded: int64(c.triggered) is -1 when no cycle is active (^uint64(0)), and heapScan = heapScanWork on resetLive.
Verification has two independent legs.
go test -overlay)Instead of trusting the port, I drive the actual Go 1.26 gcControllerState
through its test-only export hooks (runtime.NewGCController,
GCController.StartCycle/Revise/EndCycle, HeapGoal, Triggered,
AssistWorkPerByte) by injecting a probe into the standard library with
go test -overlay — no GOROOT write required:
cd ~/gcpacer
go test -overlay=overlay.json -run TestZZPacerProbe -v runtime
Output (awpb = math.Float64bits(AssistWorkPerByte)):
PROBE steady
CYC 0 trigger=3997696 goal=22020096 heapLive=10485760 heapMarked=10485760 awpb=3c9f400000001e85
CYC 1 trigger=18595840 goal=42991616 heapLive=20971520 heapMarked=20971520 awpb=4013b9b462d454ba
CYC 2 trigger=37272111 goal=63963136 heapLive=31457280 heapMarked=31457280 awpb=4014de9c3eb9e502
PROBE limited
CYC 0 trigger=3997696 goal=20971520 heapLive=10485760 heapMarked=10485760 awpb=3c9f400000001e85
CYC 1 trigger=17858560 goal=24117248 heapLive=20971520 heapMarked=20971520 awpb=401d084210842108
CYC 2 trigger=23183360 goal=31457280 heapLive=31457280 heapMarked=31457280 awpb=4158700000000000
--- PASS: TestZZPacerProbe (0.00s)
TestCrossCheckSteady / TestCrossCheckMemoryLimited replay the identical
operation sequence through the port and compare trigger, goal, heapLive,
heapMarked and the 64-bit float pattern of assistWorkPerByte with zero
tolerance. The limited trace exercises the memory-limit clamp: at cycle 2
gcPercentHeapGoal is 60 MiB but memoryLimitHeapGoal clamps the goal to
heapMarked = 30 MiB.
go test -count=1 -v ./...
--- PASS: TestCrossCheckSteady
--- PASS: TestCrossCheckMemoryLimited
--- PASS: TestGCPercentHeapGoalIntegerOrder (multiply-before-divide)
--- PASS: TestMemoryLimitHeadroomIntegerOrder (goal/100*3, not goal*3/100)
--- PASS: TestOvershootCorrection (live>goal => *1.1, remaining=1000)
--- PASS: TestExtrapolatedGoalAndHardCap (ext goal + (1+GOGC/100) cap)
--- PASS: TestAssistCreditRounding (1+int64(abpw*x), stealing)
--- PASS: TestTriggerMaxRatio ((goal/64)*61 bound)
--- PASS: TestConsMarkMaxOfLast4 (max over current+last 4)
PASS
ok gcpacer 0.002s
TestMemoryLimitHeadroomIntegerOrder is the sharpest single check: with
memoryLimit = 36700250, the correct goal/100*3 headroom is 1101006 → goal
35599244; the plausible-but-wrong goal*3/100 gives 35599243.
cd ~/gcpacer && ./verify.sh
# ==> OK: port matches the real runtime pacer bit-for-bit
defaultHeapMinimum = 4<<20 for stock Go 1.26 (only 512 KiB under GOEXPERIMENT=heapminimum512kib).gcPercentHeapGoal: multiply, then /100.memoryLimitHeapGoal: goal/100*percent, min headroom 1 MiB, goal<headroom || goal-headroom<headroom, then clamp to heapMarked.minTrigger = 0.triggered != ^uint64(0).trigger bounds: (goal-heapMarked)/64 first then *num; maxTrigger cap only when goal > defaultHeapMinimum.revise: extrapolate on work > scanWorkExpected, hard-cap at (1+GOGC/100)*goal; overshoot *1.1 + force maxScanWork; floors 1000 and 1.endCycle: max(current, last4) then shift; resetLive sets heapScan = heapScanWork.commit runway: consMark*0.75/0.25 * (lastHeapScan+lastStackScan+globalsScan).scanWork = 64<<10 when small, recompute debtBytes, and always add 1 + int64(abpw*x).# Evidence - Problem class: go-gc-pacer-heap-goal-scan-work-assist-credit-exactness - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-10-02T04:05:20.164Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement the Go runtime's concurrent GC pacer: from the previous cycle's live heap, the trigger ratio and the observed concurrent-mark duration, compute the exact mark trigger point, per-mutator assist credit, and the final heap goal. The module must reproduce the pacer's floating-point heap-goal and scan-work accounting bit-for-bit across a scripted trace of cycles, including overshoot/undershoot correction when the goal is exceeded mid-cycle and the memory-limit clamping rule. Tests assert exact trigger heap sizes, assist-credit balances, and the resulting heap goal after N cycles for a fixed allocation trace over go1.26.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-gc-pacer-heap-goal-scan-work-assist-credit-exactness", "provider": "openrouter", "solved_at": "2026-10-02T04:05:20.168Z", "version": "1.26"}