Repository navigation
runtime: allocation profile is stale when collected multiple times before GC happens #81967
Description
Go version
go version go1.27.1 darwin/arm64
Output of go env in your module/workspace:
N/AWhat did you do?
Ran this program:
package main
import (
"fmt"
"runtime"
"runtime/debug"
)
func sampledAllocs() int64 {
n, _ := runtime.MemProfile(nil, true)
var p []runtime.MemProfileRecord
for {
p = make([]runtime.MemProfileRecord, n+50)
var ok bool
n, ok = runtime.MemProfile(p, true)
if ok {
break
}
}
var allocs int64
for _, r := range p[:n] {
allocs += r.AllocBytes
}
return allocs
}
func main() {
debug.SetGCPercent(-1)
runtime.MemProfileRate = 1
var b [][]byte
for range 4 {
b = append(b, make([]byte, 1<<20))
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
fmt.Println(sampledAllocs(), stats.HeapAlloc)
}
runtime.KeepAlive(b)
}What did you see happen?
The number of bytes allocated, estimated by the profiler, is the same every time:
% go run .
1050720 1232328
1050720 2298976
1050720 3366096
1050720 4433120
What did you expect to see?
I expected the value to update as the program did more allocation. Something closer to:
% go run .
1067104 1252832
2135800 2319480
3202920 3386600
4269944 4453624
From the runtime/pprof docs: "If there has been no garbage collection at all, the heap profile reports all known allocations."
In memProfileInternal we'll accumulate memory profile data into the "active" records early if we don't see any non-zero values, under the assumption that GC hasn't happened yet: https://github.com/golang/go/blob/go1.27.1/src/runtime/mprof.go#L964-L982. But after that, we'll see non-zero values in the active records. That means we won't accumulate again, even if GC still hasn't happened.
Fixing this seems a little tricky. The memory profiler has a multi-cycle accumulation strategy, intended to give a consistent heap snapshot and avoid biasing towards allocs, where an allocation won't be counted in the "active" record until two mark terminations after when it was sampled. Maybe the right approach is to do early accumulation until we've seen two mark terminations (e.g. until mProf_NextCycle is called twice) following the first sample? After that point I think we'd want to stop doing early accumulation and follow the normal multi-cycle accumulation scheme.
Of course, if someone isn't trying to completely avoid doing GC, they can just call runtime.GC() and get an up-to-date profile.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
gabyhelp commentedon Oct 2, 2026
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
n1snt commentedon Oct 2, 2026
I’d like to work on this. I’ll look into tracking GC cycles after the first sample, as you suggested, and add a test for repeated profile reads with GC disabled.
dmitshur commentedon Oct 5, 2026
CC @golang/runtime.