Skip to content

Commit 9e5ae30

Browse files
committed
Tests for goroutine leak finder GC
1 parent db4fd32 commit 9e5ae30

File tree

4 files changed

+336
-6
lines changed

4 files changed

+336
-6
lines changed

src/runtime/crash_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,18 +187,18 @@ func buildTestProg(t *testing.T, binary string, flags ...string) (string, error)
187187
cmd.Dir = "testdata/" + binary
188188
cmd = testenv.CleanCmdEnv(cmd)
189189

190-
// Add the rangefunc GOEXPERIMENT unconditionally since some tests depend on it.
190+
// Add the rangefunc and goroutineleakfindergc GOEXPERIMENT unconditionally since some tests depend on it.
191191
// TODO(61405): Remove this once it's enabled by default.
192192
edited := false
193193
for i := range cmd.Env {
194194
e := cmd.Env[i]
195195
if _, vars, ok := strings.Cut(e, "GOEXPERIMENT="); ok {
196-
cmd.Env[i] = "GOEXPERIMENT=" + vars + ",rangefunc"
196+
cmd.Env[i] = "GOEXPERIMENT=" + vars + ",rangefunc,goroutineleakfindergc"
197197
edited = true
198198
}
199199
}
200200
if !edited {
201-
cmd.Env = append(cmd.Env, "GOEXPERIMENT=rangefunc")
201+
cmd.Env = append(cmd.Env, "GOEXPERIMENT=rangefunc,goroutineleakfindergc")
202202
}
203203

204204
out, err := cmd.CombinedOutput()

src/runtime/gc_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"math/rand"
1515
"os"
1616
"reflect"
17+
"regexp"
1718
"runtime"
1819
"runtime/debug"
1920
"slices"
@@ -1095,3 +1096,140 @@ func TestDetectFinalizerAndCleanupLeaks(t *testing.T) {
10951096
t.Fatalf("expected %d symbolized locations, got:\n%s", wantSymbolizedLocations, got)
10961097
}
10971098
}
1099+
1100+
func TestGoroutineLeakGC(t *testing.T) {
1101+
type testCase struct {
1102+
tname string
1103+
funcName string
1104+
expectedLeaks map[*regexp.Regexp]int
1105+
}
1106+
1107+
testCases := []testCase{{
1108+
tname: "ChanReceiveNil",
1109+
funcName: "GoroutineLeakNilRecv",
1110+
expectedLeaks: map[*regexp.Regexp]int{
1111+
regexp.MustCompile(`\[chan receive \(nil chan\)\]`): 0,
1112+
},
1113+
}, {
1114+
tname: "ChanSendNil",
1115+
funcName: "GoroutineLeakNilSend",
1116+
expectedLeaks: map[*regexp.Regexp]int{
1117+
regexp.MustCompile(`\[chan send \(nil chan\)\]`): 0,
1118+
},
1119+
}, {
1120+
tname: "SelectNoCases",
1121+
funcName: "GoroutineLeakSelectNoCases",
1122+
expectedLeaks: map[*regexp.Regexp]int{
1123+
regexp.MustCompile(`\[select \(no cases\)\]`): 0,
1124+
},
1125+
}, {
1126+
tname: "ChanRecv",
1127+
funcName: "GoroutineLeakChanRecv",
1128+
expectedLeaks: map[*regexp.Regexp]int{
1129+
regexp.MustCompile(`\[chan receive\]`): 0,
1130+
},
1131+
}, {
1132+
tname: "ChanSend",
1133+
funcName: "GoroutineLeakChanSend",
1134+
expectedLeaks: map[*regexp.Regexp]int{
1135+
regexp.MustCompile(`\[chan send\]`): 0,
1136+
},
1137+
}, {
1138+
tname: "Select",
1139+
funcName: "GoroutineLeakSelect",
1140+
expectedLeaks: map[*regexp.Regexp]int{
1141+
regexp.MustCompile(`\[select\]`): 0,
1142+
},
1143+
}, {
1144+
tname: "WaitGroup",
1145+
funcName: "GoroutineLeakWaitGroup",
1146+
expectedLeaks: map[*regexp.Regexp]int{
1147+
regexp.MustCompile(`\[sync\.WaitGroup\.Wait\]`): 0,
1148+
},
1149+
}, {
1150+
tname: "MutexStack",
1151+
funcName: "GoroutineLeakMutexStack",
1152+
expectedLeaks: map[*regexp.Regexp]int{
1153+
regexp.MustCompile(`\[sync\.Mutex\.Lock\]`): 0,
1154+
},
1155+
}, {
1156+
tname: "MutexHeap",
1157+
funcName: "GoroutineLeakMutexHeap",
1158+
expectedLeaks: map[*regexp.Regexp]int{
1159+
regexp.MustCompile(`\[sync\.Mutex\.Lock\]`): 0,
1160+
},
1161+
}, {
1162+
tname: "Cond",
1163+
funcName: "GoroutineLeakCond",
1164+
expectedLeaks: map[*regexp.Regexp]int{
1165+
regexp.MustCompile(`\[sync\.Cond\.Wait\]`): 0,
1166+
},
1167+
}, {
1168+
tname: "RWMutexRLock",
1169+
funcName: "GoroutineLeakRWMutexRLock",
1170+
expectedLeaks: map[*regexp.Regexp]int{
1171+
regexp.MustCompile(`\[sync\.RWMutex\.RLock\]`): 0,
1172+
},
1173+
}, {
1174+
tname: "RWMutexLock",
1175+
funcName: "GoroutineLeakRWMutexLock",
1176+
expectedLeaks: map[*regexp.Regexp]int{
1177+
// Invoking Lock on a RWMutex may either put a goroutine a waiting state
1178+
// of either sync.RWMutex.Lock or sync.Mutex.Lock.
1179+
regexp.MustCompile(`\[sync\.(RW)?Mutex\.Lock\]`): 0,
1180+
},
1181+
}, {
1182+
tname: "Mixed",
1183+
funcName: "GoroutineLeakMixed",
1184+
expectedLeaks: map[*regexp.Regexp]int{
1185+
regexp.MustCompile(`\[sync\.WaitGroup\.Wait\]`): 0,
1186+
regexp.MustCompile(`\[chan send\]`): 0,
1187+
},
1188+
}, {
1189+
tname: "NoLeakGlobal",
1190+
funcName: "NoGoroutineLeakGlobal",
1191+
}}
1192+
1193+
failStates := regexp.MustCompile(`fatal|panic`)
1194+
1195+
for _, tcase := range testCases {
1196+
t.Run(tcase.tname, func(t *testing.T) {
1197+
exe, err := buildTestProg(t, "testprog")
1198+
if err != nil {
1199+
t.Fatal(fmt.Sprintf("building testprog failed: %v", err))
1200+
}
1201+
output := runBuiltTestProg(t, exe, tcase.funcName, "GODEBUG=gctrace=1,gcgoroutineleaks=1")
1202+
1203+
if len(tcase.expectedLeaks) == 0 && strings.Contains(output, "goroutine leak!") {
1204+
t.Fatalf("output:\n%s\n\nunexpected goroutines leaks detected", output)
1205+
return
1206+
}
1207+
1208+
if failStates.MatchString(output) {
1209+
t.Fatalf("output:\n%s\n\nunexpected fatal exception or panic", output)
1210+
return
1211+
}
1212+
1213+
for _, line := range strings.Split(output, "\n") {
1214+
if strings.Contains(line, "goroutine leak!") {
1215+
for expectedLeak, count := range tcase.expectedLeaks {
1216+
if expectedLeak.MatchString(line) {
1217+
tcase.expectedLeaks[expectedLeak] = count + 1
1218+
}
1219+
}
1220+
}
1221+
}
1222+
1223+
missingLeakStrs := make([]string, 0, len(tcase.expectedLeaks))
1224+
for expectedLeak, count := range tcase.expectedLeaks {
1225+
if count == 0 {
1226+
missingLeakStrs = append(missingLeakStrs, expectedLeak.String())
1227+
}
1228+
}
1229+
1230+
if len(missingLeakStrs) > 0 {
1231+
t.Fatalf("output:\n%s\n\nnot enough goroutines leaks detected. Missing:\n%s", output, strings.Join(missingLeakStrs, ", "))
1232+
}
1233+
})
1234+
}
1235+
}

src/runtime/mgc.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,9 +1253,11 @@ func findGoleaks() bool {
12531253
casgstatus(gp, _Gwaiting, _Gleaked)
12541254
fn := findfunc(gp.startpc)
12551255
if fn.valid() {
1256-
print("goroutine leak! goroutine ", gp.goid, ": ", funcname(fn), " Stack size: ", gp.stack.hi-gp.stack.lo, " bytes\n")
1256+
print("goroutine leak! goroutine ", gp.goid, ": ", funcname(fn), " Stack size: ", gp.stack.hi-gp.stack.lo, " bytes ",
1257+
"[", waitReasonStrings[gp.waitreason], "]\n")
12571258
} else {
1258-
print("goroutine leak! goroutine ", gp.goid, ": !unnamed goroutine!", " Stack size: ", gp.stack.hi-gp.stack.lo, " bytes\n")
1259+
print("goroutine leak! goroutine ", gp.goid, ": !unnamed goroutine!", " Stack size: ", gp.stack.hi-gp.stack.lo, " bytes ",
1260+
"[", waitReasonStrings[gp.waitreason], "]\n")
12591261
}
12601262
traceback(gp.sched.pc, gp.sched.sp, gp.sched.lr, gp)
12611263
println()
@@ -1500,7 +1502,11 @@ func gcMarkTermination(stw worldStop) {
15001502
printlock()
15011503
print("gc ", memstats.numgc,
15021504
" @", string(itoaDiv(sbuf[:], uint64(work.tSweepTerm-runtimeInitTime)/1e6, 3)), "s ",
1503-
util, "%: ")
1505+
util, "%")
1506+
if work.goroutineLeakFinder.done {
1507+
print(" (goroutine leak finder GC)")
1508+
}
1509+
print(": ")
15041510
prev := work.tSweepTerm
15051511
for i, ns := range []int64{work.tMark, work.tMarkTerm, work.tEnd} {
15061512
if i != 0 {
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package main
2+
3+
import (
4+
"runtime"
5+
"sync"
6+
"time"
7+
)
8+
9+
func init() {
10+
register("GoroutineLeakNilRecv", GoroutineLeakNilRecv)
11+
register("GoroutineLeakNilSend", GoroutineLeakNilSend)
12+
register("GoroutineLeakSelectNoCases", GoroutineLeakSelectNoCases)
13+
register("GoroutineLeakChanRecv", GoroutineLeakChanRecv)
14+
register("GoroutineLeakChanSend", GoroutineLeakChanSend)
15+
register("GoroutineLeakSelect", GoroutineLeakSelect)
16+
register("GoroutineLeakWaitGroup", GoroutineLeakWaitGroup)
17+
register("GoroutineLeakMutexStack", GoroutineLeakMutexStack)
18+
register("GoroutineLeakMutexHeap", GoroutineLeakMutexHeap)
19+
register("GoroutineLeakRWMutexRLock", GoroutineLeakRWMutexRLock)
20+
register("GoroutineLeakRWMutexLock", GoroutineLeakRWMutexLock)
21+
register("GoroutineLeakCond", GoroutineLeakCond)
22+
register("GoroutineLeakMixed", GoroutineLeakMixed)
23+
register("NoGoroutineLeakGlobal", NoGoroutineLeakGlobal)
24+
}
25+
26+
func GoroutineLeakNilRecv() {
27+
go func() {
28+
var c chan int
29+
<-c
30+
panic("should not be reached")
31+
}()
32+
time.Sleep(10 * time.Millisecond)
33+
runtime.GC()
34+
}
35+
36+
func GoroutineLeakNilSend() {
37+
go func() {
38+
var c chan int
39+
c <- 0
40+
panic("should not be reached")
41+
}()
42+
time.Sleep(10 * time.Millisecond)
43+
runtime.GC()
44+
}
45+
46+
func GoroutineLeakChanRecv() {
47+
go func() {
48+
<-make(chan int)
49+
panic("should not be reached")
50+
}()
51+
time.Sleep(10 * time.Millisecond)
52+
runtime.GC()
53+
}
54+
55+
func GoroutineLeakSelectNoCases() {
56+
go func() {
57+
select {}
58+
panic("should not be reached")
59+
}()
60+
time.Sleep(10 * time.Millisecond)
61+
runtime.GC()
62+
}
63+
64+
func GoroutineLeakChanSend() {
65+
go func() {
66+
make(chan int) <- 0
67+
panic("should not be reached")
68+
}()
69+
time.Sleep(10 * time.Millisecond)
70+
runtime.GC()
71+
}
72+
73+
func GoroutineLeakSelect() {
74+
go func() {
75+
select {
76+
case make(chan int) <- 0:
77+
case <-make(chan int):
78+
}
79+
panic("should not be reached")
80+
}()
81+
time.Sleep(10 * time.Millisecond)
82+
runtime.GC()
83+
}
84+
85+
func GoroutineLeakWaitGroup() {
86+
go func() {
87+
var wg sync.WaitGroup
88+
wg.Add(1)
89+
wg.Wait()
90+
panic("should not be reached")
91+
}()
92+
time.Sleep(10 * time.Millisecond)
93+
runtime.GC()
94+
}
95+
96+
func GoroutineLeakMutexStack() {
97+
for i := 0; i < 1000; i++ {
98+
go func() {
99+
var mu sync.Mutex
100+
mu.Lock()
101+
mu.Lock()
102+
panic("should not be reached")
103+
}()
104+
}
105+
time.Sleep(10 * time.Millisecond)
106+
runtime.GC()
107+
time.Sleep(10 * time.Millisecond)
108+
}
109+
110+
func GoroutineLeakMutexHeap() {
111+
for i := 0; i < 1000; i++ {
112+
go func() {
113+
mu := &sync.Mutex{}
114+
go func() {
115+
mu.Lock()
116+
mu.Lock()
117+
panic("should not be reached")
118+
}()
119+
}()
120+
}
121+
time.Sleep(10 * time.Millisecond)
122+
runtime.GC()
123+
time.Sleep(10 * time.Millisecond)
124+
}
125+
126+
func GoroutineLeakRWMutexRLock() {
127+
go func() {
128+
mu := &sync.RWMutex{}
129+
mu.Lock()
130+
mu.RLock()
131+
panic("should not be reached")
132+
}()
133+
time.Sleep(10 * time.Millisecond)
134+
runtime.GC()
135+
}
136+
137+
func GoroutineLeakRWMutexLock() {
138+
go func() {
139+
mu := &sync.RWMutex{}
140+
mu.Lock()
141+
mu.Lock()
142+
panic("should not be reached")
143+
}()
144+
time.Sleep(10 * time.Millisecond)
145+
runtime.GC()
146+
}
147+
148+
func GoroutineLeakCond() {
149+
go func() {
150+
cond := sync.NewCond(&sync.Mutex{})
151+
cond.L.Lock()
152+
cond.Wait()
153+
panic("should not be reached")
154+
}()
155+
time.Sleep(10 * time.Millisecond)
156+
runtime.GC()
157+
}
158+
159+
func GoroutineLeakMixed() {
160+
go func() {
161+
ch := make(chan int)
162+
wg := sync.WaitGroup{}
163+
wg.Add(1)
164+
go func() {
165+
ch <- 0
166+
wg.Done()
167+
panic("should not be reached")
168+
}()
169+
wg.Wait()
170+
<-ch
171+
panic("should not be reached")
172+
}()
173+
time.Sleep(10 * time.Millisecond)
174+
runtime.GC()
175+
}
176+
177+
var ch = make(chan int)
178+
179+
// No leak should be reported by this test
180+
func NoGoroutineLeakGlobal() {
181+
go func() {
182+
<-ch
183+
}()
184+
time.Sleep(10 * time.Millisecond)
185+
runtime.GC()
186+
}

0 commit comments

Comments
 (0)