Skip to content

Commit 9c88db5

Browse files
prattmicgopherbot
authored andcommitted
runtime: always show runfinq in traceback
Today, runtime.runfinq is hidden whenever runtime frames are hidden. However this frame serves as a hint that this goroutine is running finalizers, which is otherwise unclear, but can be useful when debugging issues with finalizers. Fixes #73011. Change-Id: I6a6a636cb63951fbe1fefc3554fe9cea5d0a0fb6 Reviewed-on: https://go-review.googlesource.com/c/go/+/660295 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Michael Pratt <[email protected]> Reviewed-by: Michael Knyszek <[email protected]>
1 parent aaf9b46 commit 9c88db5

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

src/runtime/crash_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"errors"
1111
"flag"
1212
"fmt"
13+
"internal/profile"
1314
"internal/testenv"
1415
traceparse "internal/trace"
1516
"io"
@@ -1100,3 +1101,77 @@ func TestNetpollWaiters(t *testing.T) {
11001101
t.Fatalf("output is not %q\n%s", want, output)
11011102
}
11021103
}
1104+
1105+
// The runtime.runfinq frame should appear in panics, even if runtime frames
1106+
// are normally hidden (GOTRACEBACK=all).
1107+
func TestFinalizerDeadlockPanic(t *testing.T) {
1108+
t.Parallel()
1109+
output := runTestProg(t, "testprog", "FinalizerDeadlock", "GOTRACEBACK=all", "GO_TEST_FINALIZER_DEADLOCK=panic")
1110+
1111+
want := "runtime.runfinq()"
1112+
if !strings.Contains(output, want) {
1113+
t.Errorf("output does not contain %q:\n%s", want, output)
1114+
}
1115+
}
1116+
1117+
// The runtime.runfinq frame should appear in runtime.Stack, even though
1118+
// runtime frames are normally hidden.
1119+
func TestFinalizerDeadlockStack(t *testing.T) {
1120+
t.Parallel()
1121+
output := runTestProg(t, "testprog", "FinalizerDeadlock", "GO_TEST_FINALIZER_DEADLOCK=stack")
1122+
1123+
want := "runtime.runfinq()"
1124+
if !strings.Contains(output, want) {
1125+
t.Errorf("output does not contain %q:\n%s", want, output)
1126+
}
1127+
}
1128+
1129+
// The runtime.runfinq frame should appear in goroutine profiles.
1130+
func TestFinalizerDeadlockPprofProto(t *testing.T) {
1131+
t.Parallel()
1132+
output := runTestProg(t, "testprog", "FinalizerDeadlock", "GO_TEST_FINALIZER_DEADLOCK=pprof_proto")
1133+
1134+
p, err := profile.Parse(strings.NewReader(output))
1135+
if err != nil {
1136+
// Logging the binary proto data is not very nice, but it might
1137+
// be a text error message instead.
1138+
t.Logf("Output: %s", output)
1139+
t.Fatalf("Error parsing proto output: %v", err)
1140+
}
1141+
1142+
want := "runtime.runfinq"
1143+
for _, s := range p.Sample {
1144+
for _, loc := range s.Location {
1145+
for _, line := range loc.Line {
1146+
if line.Function.Name == want {
1147+
// Done!
1148+
return
1149+
}
1150+
}
1151+
}
1152+
}
1153+
1154+
t.Errorf("Profile does not contain %q:\n%s", want, p)
1155+
}
1156+
1157+
// The runtime.runfinq frame should appear in goroutine profiles (debug=1).
1158+
func TestFinalizerDeadlockPprofDebug1(t *testing.T) {
1159+
t.Parallel()
1160+
output := runTestProg(t, "testprog", "FinalizerDeadlock", "GO_TEST_FINALIZER_DEADLOCK=pprof_debug1")
1161+
1162+
want := "runtime.runfinq+"
1163+
if !strings.Contains(output, want) {
1164+
t.Errorf("output does not contain %q:\n%s", want, output)
1165+
}
1166+
}
1167+
1168+
// The runtime.runfinq frame should appear in goroutine profiles (debug=2).
1169+
func TestFinalizerDeadlockPprofDebug2(t *testing.T) {
1170+
t.Parallel()
1171+
output := runTestProg(t, "testprog", "FinalizerDeadlock", "GO_TEST_FINALIZER_DEADLOCK=pprof_debug2")
1172+
1173+
want := "runtime.runfinq()"
1174+
if !strings.Contains(output, want) {
1175+
t.Errorf("output does not contain %q:\n%s", want, output)
1176+
}
1177+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"flag"
9+
"fmt"
10+
"os"
11+
"runtime"
12+
"runtime/pprof"
13+
)
14+
15+
var finalizerDeadlockMode = flag.String("finalizer-deadlock-mode", "panic", "Trigger mode of FinalizerDeadlock")
16+
17+
func init() {
18+
register("FinalizerDeadlock", FinalizerDeadlock)
19+
}
20+
21+
func FinalizerDeadlock() {
22+
flag.Parse()
23+
24+
started := make(chan struct{})
25+
b := new([16]byte)
26+
runtime.SetFinalizer(b, func(*[16]byte) {
27+
started <- struct{}{}
28+
select {}
29+
})
30+
b = nil
31+
32+
runtime.GC()
33+
34+
<-started
35+
// We know the finalizer has started running. The goroutine might still
36+
// be running or it may now be blocked. Either is fine, the goroutine
37+
// should appear in stacks either way.
38+
39+
mode := os.Getenv("GO_TEST_FINALIZER_DEADLOCK")
40+
switch mode {
41+
case "panic":
42+
panic("panic")
43+
case "stack":
44+
buf := make([]byte, 4096)
45+
for {
46+
n := runtime.Stack(buf, true)
47+
if n >= len(buf) {
48+
buf = make([]byte, 2*len(buf))
49+
continue
50+
}
51+
buf = buf[:n]
52+
break
53+
}
54+
fmt.Printf("%s\n", string(buf))
55+
case "pprof_proto":
56+
if err := pprof.Lookup("goroutine").WriteTo(os.Stdout, 0); err != nil {
57+
fmt.Fprintf(os.Stderr, "Error writing profile: %v\n", err)
58+
os.Exit(1)
59+
}
60+
case "pprof_debug1":
61+
if err := pprof.Lookup("goroutine").WriteTo(os.Stdout, 1); err != nil {
62+
fmt.Fprintf(os.Stderr, "Error writing profile: %v\n", err)
63+
os.Exit(1)
64+
}
65+
case "pprof_debug2":
66+
if err := pprof.Lookup("goroutine").WriteTo(os.Stdout, 2); err != nil {
67+
fmt.Fprintf(os.Stderr, "Error writing profile: %v\n", err)
68+
os.Exit(1)
69+
}
70+
default:
71+
fmt.Fprintf(os.Stderr, "Unknown mode %q. GO_TEST_FINALIZER_DEADLOCK must be one of panic, stack, pprof_proto, pprof_debug1, pprof_debug2\n", mode)
72+
os.Exit(1)
73+
}
74+
}

src/runtime/traceback.go

+15
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,21 @@ func showfuncinfo(sf srcFunc, firstFrame bool, calleeID abi.FuncID) bool {
11311131
return false
11321132
}
11331133

1134+
// Always show runtime.runfinq as context that this goroutine is
1135+
// running finalizers, otherwise there is no obvious indicator.
1136+
//
1137+
// TODO(prattmic): A more general approach would be to always show the
1138+
// outermost frame (besides runtime.goexit), even if it is a runtime.
1139+
// Hiding the outermost frame allows the apparent outermost frame to
1140+
// change across different traces, which seems impossible.
1141+
//
1142+
// Unfortunately, implementing this requires looking ahead at the next
1143+
// frame, which goes against traceback's incremental approach (see big
1144+
// coment in traceback1).
1145+
if sf.funcID == abi.FuncID_runfinq {
1146+
return true
1147+
}
1148+
11341149
name := sf.name()
11351150

11361151
// Special case: always show runtime.gopanic frame

0 commit comments

Comments
 (0)