Skip to content

runtime: goroutine leak detection by using the garbage collector #74622

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
19b2ec4
Proposal #74609: goroutine leak detection by using the garbage collec…
VladSaiocUber Jul 15, 2025
824aa0c
Corrected sema dequeue implementation.
VladSaiocUber Jul 16, 2025
a1215dc
Ordered wait reasons for easier checks.
VladSaiocUber Jul 16, 2025
32c8b19
Renamed deadlockgc to golfgc and deadlocks to goroutine leaks to avoi…
VladSaiocUber Jul 17, 2025
970fdb6
Fixed status text for leaked goroutines.
VladSaiocUber Jul 18, 2025
e298bc0
Fixed bad goroutine status.
VladSaiocUber Jul 18, 2025
107cf86
Addressing some of the comments.
VladSaiocUber Jul 21, 2025
d0b7e08
Removed bitmask and switched to tristate pointers (thank you @mknyszek).
VladSaiocUber Jul 24, 2025
f8ed73d
Switched markrootNext and markrootJobs to atomic.Uint32. Added GCDEBU…
VladSaiocUber Jul 24, 2025
84ae3ed
Tests for goroutine leak finder GC
VladSaiocUber Jul 25, 2025
a0594bc
Renamed maybeLivePtr to maybeTraceablePtr.
VladSaiocUber Jul 25, 2025
3db58fb
Cleaned up experimental flags some more.
VladSaiocUber Jul 25, 2025
882d77d
Renamed nMaybeLiveStackRoots to nMaybeRunnableStackRoots. Refactoring.
VladSaiocUber Jul 30, 2025
f7bbd07
Test for goroutine leak GC.
VladSaiocUber Aug 1, 2025
932befe
Removed FindGoLeaks API, addressed nits, and cleaned comments.
VladSaiocUber Aug 4, 2025
b25627d
Added missing expected leak in GC test.
VladSaiocUber Aug 4, 2025
0f9715b
Addressed flakiness on some tests.
VladSaiocUber Aug 5, 2025
de7bf04
Addressed more flakiness issues.
VladSaiocUber Aug 5, 2025
0081519
Fixed TODO comment.
VladSaiocUber Aug 7, 2025
0ac1b47
Collect goroutine leaks via profiling.
VladSaiocUber Aug 7, 2025
cf1109f
Goroutine leak tests updated to collect goroutine leak profiles.
VladSaiocUber Aug 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/internal/goexperiment/exp_goleakfindergc_off.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/internal/goexperiment/exp_goleakfindergc_on.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/internal/goexperiment/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@ type Flags struct {
// RandomizedHeapBase enables heap base address randomization on 64-bit
// platforms.
RandomizedHeapBase64 bool

// GoroutineLeakFinderGC enables the Deadlock GC implementation.
GoroutineLeakFinderGC bool
}
34 changes: 18 additions & 16 deletions src/net/http/pprof/pprof.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,25 +351,27 @@ func collectProfile(p *pprof.Profile) (*profile.Profile, error) {
}

var profileSupportsDelta = map[handler]bool{
"allocs": true,
"block": true,
"goroutine": true,
"heap": true,
"mutex": true,
"threadcreate": true,
"allocs": true,
"block": true,
"goroutine": true,
"goroutineleak": true,
"heap": true,
"mutex": true,
"threadcreate": true,
}

var profileDescriptions = map[string]string{
"allocs": "A sampling of all past memory allocations",
"block": "Stack traces that led to blocking on synchronization primitives",
"cmdline": "The command line invocation of the current program",
"goroutine": "Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.",
"heap": "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.",
"mutex": "Stack traces of holders of contended mutexes",
"profile": "CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.",
"symbol": "Maps given program counters to function names. Counters can be specified in a GET raw query or POST body, multiple counters are separated by '+'.",
"threadcreate": "Stack traces that led to the creation of new OS threads",
"trace": "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.",
"allocs": "A sampling of all past memory allocations",
"block": "Stack traces that led to blocking on synchronization primitives",
"cmdline": "The command line invocation of the current program",
"goroutine": "Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.",
"goroutineleak": "Stack traces of all leaked goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.",
"heap": "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.",
"mutex": "Stack traces of holders of contended mutexes",
"profile": "CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.",
"symbol": "Maps given program counters to function names. Counters can be specified in a GET raw query or POST body, multiple counters are separated by '+'.",
"threadcreate": "Stack traces that led to the creation of new OS threads",
"trace": "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.",
}

type profileEntry struct {
Expand Down
32 changes: 16 additions & 16 deletions src/runtime/chan.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,11 @@ func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.elem.set(ep)
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
mysg.c.set(c)
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
Expand Down Expand Up @@ -298,7 +298,7 @@ func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
mysg.c.set(nil)
releaseSudog(mysg)
if closed {
if c.closed == 0 {
Expand Down Expand Up @@ -336,9 +336,9 @@ func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
}
if sg.elem != nil {
if sg.elem.get() != nil {
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
sg.elem.set(nil)
}
gp := sg.g
unlockf()
Expand Down Expand Up @@ -395,7 +395,7 @@ func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
// Once we read sg.elem out of sg, it will no longer
// be updated if the destination's stack gets copied (shrunk).
// So make sure that no preemption points can happen between read & use.
dst := sg.elem
dst := sg.elem.get()
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
// No need for cgo write barrier checks because dst is always
// Go memory.
Expand All @@ -406,7 +406,7 @@ func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
// dst is on our stack or the heap, src is on another stack.
// The channel is locked, so src will not move during this
// operation.
src := sg.elem
src := sg.elem.get()
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
memmove(dst, src, t.Size_)
}
Expand Down Expand Up @@ -441,9 +441,9 @@ func closechan(c *hchan) {
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
if sg.elem.get() != nil {
typedmemclr(c.elemtype, sg.elem.get())
sg.elem.set(nil)
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
Expand All @@ -463,7 +463,7 @@ func closechan(c *hchan) {
if sg == nil {
break
}
sg.elem = nil
sg.elem.set(nil)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
Expand Down Expand Up @@ -642,13 +642,13 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.elem.set(ep)
mysg.waitlink = nil
gp.waiting = mysg

mysg.g = gp
mysg.isSelect = false
mysg.c = c
mysg.c.set(c)
gp.param = nil
c.recvq.enqueue(mysg)
if c.timer != nil {
Expand Down Expand Up @@ -680,7 +680,7 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
}
success := mysg.success
gp.param = nil
mysg.c = nil
mysg.c.set(nil)
releaseSudog(mysg)
return true, success
}
Expand Down Expand Up @@ -727,14 +727,14 @@ func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
typedmemmove(c.elemtype, ep, qp)
}
// copy data from sender to queue
typedmemmove(c.elemtype, qp, sg.elem)
typedmemmove(c.elemtype, qp, sg.elem.get())
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
sg.elem = nil
sg.elem.set(nil)
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
Expand Down
17 changes: 17 additions & 0 deletions src/runtime/crash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ func buildTestProg(t *testing.T, binary string, flags ...string) (string, error)
t.Logf("running %v", cmd)
cmd.Dir = "testdata/" + binary
cmd = testenv.CleanCmdEnv(cmd)

// Add the goroutineleakfindergc GOEXPERIMENT unconditionally since some tests depend on it.
// TODO(61405): Remove this once it's enabled by default.
//
// FIXME: Remove this once profiling is enabled and goroutineleakfindergc experiment is phased out.
edited := false
for i := range cmd.Env {
e := cmd.Env[i]
if _, vars, ok := strings.Cut(e, "GOEXPERIMENT="); ok {
cmd.Env[i] = "GOEXPERIMENT=" + vars + ",goroutineleakfindergc"
edited = true
}
}
if !edited {
cmd.Env = append(cmd.Env, "GOEXPERIMENT=goroutineleakfindergc")
}

out, err := cmd.CombinedOutput()
if err != nil {
target.err = fmt.Errorf("building %s %v: %v\n%s", binary, flags, err, out)
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,7 @@ func (t *SemTable) Enqueue(addr *uint32) {
s.releasetime = 0
s.acquiretime = 0
s.ticket = 0
t.semTable.rootFor(addr).queue(addr, s, false)
t.semTable.rootFor(addr).queue(addr, s, false, false)
}

// Dequeue simulates dequeuing a waiter for a semaphore (or lock) at addr.
Expand Down
Loading