Skip to content

runtime: allocation profile is stale when collected multiple times before GC happens #81967

Open

Description

@nsrip-dd

Go version

go version go1.27.1 darwin/arm64

Output of go env in your module/workspace:

N/A

What 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.

Activity

  1. n1snt commented on Oct 2, 2026

    @n1snt

    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.

  2. dmitshur commented on Oct 5, 2026

    @dmitshur
    Member

    CC @golang/runtime.

  3. added
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Oct 5, 2026
  4. added this to the Backlog milestone on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.compiler/runtimeIssues related to the Go compiler and/or runtime.

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions