Skip to content

Commit 3a81338

Browse files
committed
runtime: make stack traces of endless recursion print only top and bottom 50
This CL makes it so that instead of printing massive stack traces during endless recursion, which spams users and aren't useful, it now prints out the top and bottom 50 frames. If the number of frames <= 100 (_TracebackMaxFrames), we'll just print all the frames out. Modified gentraceback to return counts of: * ntotalframes * nregularframes which allows us to get accurate counts of the various kinds of frames. While here, also fixed a bug that resulted from CL 37222, in which we no longer accounted for decrementing requested frame skips, and assumed that when printing, that skip would always be 0. The fix is instead to add precondition that we'll only print if skip <= 0, but also decrement skip as we iterate. Fixes #7181. Fixes #24628. Change-Id: Ie31ec6413fdfbe43827b254fef7d99ea26a5277f Reviewed-on: https://go-review.googlesource.com/c/go/+/37222 Run-TryBot: Emmanuel Odeke <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Keith Randall <[email protected]> Trust: Emmanuel Odeke <[email protected]>
1 parent 5736eb0 commit 3a81338

File tree

3 files changed

+265
-47
lines changed

3 files changed

+265
-47
lines changed

src/runtime/crash_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,132 @@ func TestStackOverflow(t *testing.T) {
240240
}
241241
}
242242

243+
func TestStackOverflowTopAndBottomTraces(t *testing.T) {
244+
output := runTestProg(t, "testprog", "StackOverflowTopAndBottomTraces")
245+
246+
// 1. First things first, we expect to traverse from
247+
// runtime: goroutine stack exceeds 10000-byte limit
248+
// and down to the very end until we see:
249+
// runtime.goexit()
250+
mustHaves := []string{
251+
// Top half expectations
252+
"\\s*runtime: goroutine stack exceeds 10000-byte limit\n",
253+
"\\s*fatal error: stack overflow\n",
254+
"\\s*runtime stack:\n",
255+
"\\s*runtime.throw[^\n]+\n\t.+:\\d+ [^\n]+",
256+
"\\s+runtime\\.newstack[^\n]+\n\t.+:\\d+ [^\n]+",
257+
"\\s+runtime.morestack[^\n]+\n\t.+:\\d+ [^\n]+",
258+
"\\s+goroutine 1 \\[running\\]:",
259+
260+
// Bottom half expectations
261+
"\\s*main.main\\(\\)\n",
262+
"\\s*runtime.main\\(\\)\n",
263+
"\\s*runtime.goexit\\(\\)\n",
264+
}
265+
266+
for _, pat := range mustHaves {
267+
reg := regexp.MustCompile(pat)
268+
match := reg.FindAllString(output, -1)
269+
if len(match) == 0 {
270+
t.Errorf("Failed to find pattern %q", pat)
271+
}
272+
}
273+
274+
// 2. Split top and bottom halves by the "... ({n} stack frames omitted)" message
275+
regHalving := regexp.MustCompile("\\.{3} \\(\\d+ stack frames omitted\\)")
276+
halverMatches := regHalving.FindAllString(output, -1)
277+
if len(halverMatches) != 1 {
278+
t.Fatal("Failed to find the `stack frames omitted` pattern")
279+
}
280+
str := string(output)
281+
halver := halverMatches[0]
282+
midIndex := strings.Index(str, halver)
283+
topHalf, bottomHalf := str[:midIndex], str[midIndex+len(halver):]
284+
// 2.1. Sanity check, len(topHalf) >= halver || len(bottomHalf) >= halver
285+
if len(topHalf) < len(halver) || len(bottomHalf) < len(halver) {
286+
t.Fatalf("Sanity check len(topHalf) = %d len(bottomHalf) = %d; both must be >= len(halver) %d",
287+
len(topHalf), len(bottomHalf), len(halver))
288+
}
289+
290+
// 3. In each of the halves, we should have an equal number
291+
// of stacktraces before and after the "omitted frames" message.
292+
regStackTraces := regexp.MustCompile("\n[^\n]+\n\t.+:\\d+ .+ fp=0x.+ sp=0x.+ pc=0x.+")
293+
topHalfStackTraces := regStackTraces.FindAllString(topHalf, -1)
294+
bottomHalfStackTraces := regStackTraces.FindAllString(bottomHalf, -1)
295+
nTopHalf, nBottomHalf := len(topHalfStackTraces), len(bottomHalfStackTraces)
296+
if nTopHalf == 0 || nBottomHalf == 0 {
297+
t.Fatal("Both lengths of stack-halves should be non-zero")
298+
}
299+
// The bottom half will always have the 50 non-runtime frames along with these 3 frames:
300+
// * main.main()
301+
// * "runtime.main"
302+
// * "runtime.goexit"
303+
// hence we need to decrement 3 counted lines.
304+
if nTopHalf != nBottomHalf-3 {
305+
t.Errorf("len(topHalfStackTraces)=%d len(bottomHalfStackTraces)-3=%d yet must be equal\n", nTopHalf, nBottomHalf-3)
306+
}
307+
308+
// 4. Next, prune out the:
309+
// func...
310+
// line...
311+
// pairs in both of the halves.
312+
prunes := []struct {
313+
src *string
314+
matches []string
315+
}{
316+
{src: &topHalf, matches: topHalfStackTraces},
317+
{src: &bottomHalf, matches: bottomHalfStackTraces},
318+
}
319+
320+
for _, prune := range prunes {
321+
str := *prune.src
322+
for _, match := range prune.matches {
323+
index := strings.Index(str, match)
324+
str = str[:index] + str[index+len(match):]
325+
}
326+
*prune.src = str
327+
}
328+
329+
// 5. Now match and prune out the remaining patterns in the top and bottom halves.
330+
// We aren't touching the bottom stack since its patterns are already matched
331+
// by the:
332+
// func...
333+
// line...
334+
// pairs
335+
topPartPrunables := []string{
336+
"^\\s*runtime: goroutine stack exceeds 10000-byte limit\n",
337+
"\\s*fatal error: stack overflow\n",
338+
"\\s*runtime stack:\n",
339+
"\\s*runtime.throw[^\n]+\n\t.+:\\d+ [^\n]+",
340+
"\\s+runtime\\.newstack[^\n]+\n\t.+:\\d+ [^\n]+",
341+
"\\s+runtime.morestack[^\n]+\n\t.+:\\d+ [^\n]+",
342+
"\\s+goroutine 1 \\[running\\]:",
343+
}
344+
345+
for _, pat := range topPartPrunables {
346+
reg := regexp.MustCompile(pat)
347+
matches := reg.FindAllString(topHalf, -1)
348+
if len(matches) == 0 {
349+
t.Errorf("top stack traces do not contain pattern: %q", reg)
350+
} else if len(matches) != 1 {
351+
t.Errorf("inconsistent state got %d matches want only 1", len(matches))
352+
} else {
353+
match := matches[0]
354+
idx := strings.Index(topHalf, match)
355+
topHalf = topHalf[:idx] + topHalf[idx+len(match):]
356+
}
357+
}
358+
359+
// 6. At the end we should only be left with
360+
// newlines in both the top and bottom halves.
361+
topHalf = strings.TrimSpace(topHalf)
362+
bottomHalf = strings.TrimSpace(bottomHalf)
363+
if topHalf != "" && bottomHalf != "" {
364+
t.Fatalf("len(topHalf)=%d len(bottomHalf)=%d\ntopHalf=\n%s\n\nbottomHalf=\n%s",
365+
len(topHalf), len(bottomHalf), topHalf, bottomHalf)
366+
}
367+
}
368+
243369
func TestThreadExhaustion(t *testing.T) {
244370
output := runTestProg(t, "testprog", "ThreadExhaustion")
245371
want := "runtime: program exceeds 10-thread limit\nfatal error: thread exhaustion"

src/runtime/testdata/testprog/deadlock.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func init() {
2020
register("LockedDeadlock2", LockedDeadlock2)
2121
register("GoexitDeadlock", GoexitDeadlock)
2222
register("StackOverflow", StackOverflow)
23+
register("StackOverflowTopAndBottomTraces", StackOverflowTopAndBottomTraces)
2324
register("ThreadExhaustion", ThreadExhaustion)
2425
register("RecursivePanic", RecursivePanic)
2526
register("RecursivePanic2", RecursivePanic2)
@@ -85,6 +86,18 @@ func StackOverflow() {
8586
f()
8687
}
8788

89+
func StackOverflowTopAndBottomTraces() {
90+
var fi, gi func()
91+
fi = func() {
92+
gi()
93+
}
94+
gi = func() {
95+
fi()
96+
}
97+
debug.SetMaxStack(10000)
98+
fi()
99+
}
100+
88101
func ThreadExhaustion() {
89102
debug.SetMaxThreads(10)
90103
c := make(chan int)

0 commit comments

Comments
 (0)