Skip to content

Commit

Permalink
runtime: make stack traces of endless recursion print only top and bo…
Browse files Browse the repository at this point in the history
…ttom 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]>
  • Loading branch information
odeke-em committed Nov 6, 2020
1 parent 5736eb0 commit 3a81338
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 47 deletions.
126 changes: 126 additions & 0 deletions src/runtime/crash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,132 @@ func TestStackOverflow(t *testing.T) {
}
}

func TestStackOverflowTopAndBottomTraces(t *testing.T) {
output := runTestProg(t, "testprog", "StackOverflowTopAndBottomTraces")

// 1. First things first, we expect to traverse from
// runtime: goroutine stack exceeds 10000-byte limit
// and down to the very end until we see:
// runtime.goexit()
mustHaves := []string{
// Top half expectations
"\\s*runtime: goroutine stack exceeds 10000-byte limit\n",
"\\s*fatal error: stack overflow\n",
"\\s*runtime stack:\n",
"\\s*runtime.throw[^\n]+\n\t.+:\\d+ [^\n]+",
"\\s+runtime\\.newstack[^\n]+\n\t.+:\\d+ [^\n]+",
"\\s+runtime.morestack[^\n]+\n\t.+:\\d+ [^\n]+",
"\\s+goroutine 1 \\[running\\]:",

// Bottom half expectations
"\\s*main.main\\(\\)\n",
"\\s*runtime.main\\(\\)\n",
"\\s*runtime.goexit\\(\\)\n",
}

for _, pat := range mustHaves {
reg := regexp.MustCompile(pat)
match := reg.FindAllString(output, -1)
if len(match) == 0 {
t.Errorf("Failed to find pattern %q", pat)
}
}

// 2. Split top and bottom halves by the "... ({n} stack frames omitted)" message
regHalving := regexp.MustCompile("\\.{3} \\(\\d+ stack frames omitted\\)")
halverMatches := regHalving.FindAllString(output, -1)
if len(halverMatches) != 1 {
t.Fatal("Failed to find the `stack frames omitted` pattern")
}
str := string(output)
halver := halverMatches[0]
midIndex := strings.Index(str, halver)
topHalf, bottomHalf := str[:midIndex], str[midIndex+len(halver):]
// 2.1. Sanity check, len(topHalf) >= halver || len(bottomHalf) >= halver
if len(topHalf) < len(halver) || len(bottomHalf) < len(halver) {
t.Fatalf("Sanity check len(topHalf) = %d len(bottomHalf) = %d; both must be >= len(halver) %d",
len(topHalf), len(bottomHalf), len(halver))
}

// 3. In each of the halves, we should have an equal number
// of stacktraces before and after the "omitted frames" message.
regStackTraces := regexp.MustCompile("\n[^\n]+\n\t.+:\\d+ .+ fp=0x.+ sp=0x.+ pc=0x.+")
topHalfStackTraces := regStackTraces.FindAllString(topHalf, -1)
bottomHalfStackTraces := regStackTraces.FindAllString(bottomHalf, -1)
nTopHalf, nBottomHalf := len(topHalfStackTraces), len(bottomHalfStackTraces)
if nTopHalf == 0 || nBottomHalf == 0 {
t.Fatal("Both lengths of stack-halves should be non-zero")
}
// The bottom half will always have the 50 non-runtime frames along with these 3 frames:
// * main.main()
// * "runtime.main"
// * "runtime.goexit"
// hence we need to decrement 3 counted lines.
if nTopHalf != nBottomHalf-3 {
t.Errorf("len(topHalfStackTraces)=%d len(bottomHalfStackTraces)-3=%d yet must be equal\n", nTopHalf, nBottomHalf-3)
}

// 4. Next, prune out the:
// func...
// line...
// pairs in both of the halves.
prunes := []struct {
src *string
matches []string
}{
{src: &topHalf, matches: topHalfStackTraces},
{src: &bottomHalf, matches: bottomHalfStackTraces},
}

for _, prune := range prunes {
str := *prune.src
for _, match := range prune.matches {
index := strings.Index(str, match)
str = str[:index] + str[index+len(match):]
}
*prune.src = str
}

// 5. Now match and prune out the remaining patterns in the top and bottom halves.
// We aren't touching the bottom stack since its patterns are already matched
// by the:
// func...
// line...
// pairs
topPartPrunables := []string{
"^\\s*runtime: goroutine stack exceeds 10000-byte limit\n",
"\\s*fatal error: stack overflow\n",
"\\s*runtime stack:\n",
"\\s*runtime.throw[^\n]+\n\t.+:\\d+ [^\n]+",
"\\s+runtime\\.newstack[^\n]+\n\t.+:\\d+ [^\n]+",
"\\s+runtime.morestack[^\n]+\n\t.+:\\d+ [^\n]+",
"\\s+goroutine 1 \\[running\\]:",
}

for _, pat := range topPartPrunables {
reg := regexp.MustCompile(pat)
matches := reg.FindAllString(topHalf, -1)
if len(matches) == 0 {
t.Errorf("top stack traces do not contain pattern: %q", reg)
} else if len(matches) != 1 {
t.Errorf("inconsistent state got %d matches want only 1", len(matches))
} else {
match := matches[0]
idx := strings.Index(topHalf, match)
topHalf = topHalf[:idx] + topHalf[idx+len(match):]
}
}

// 6. At the end we should only be left with
// newlines in both the top and bottom halves.
topHalf = strings.TrimSpace(topHalf)
bottomHalf = strings.TrimSpace(bottomHalf)
if topHalf != "" && bottomHalf != "" {
t.Fatalf("len(topHalf)=%d len(bottomHalf)=%d\ntopHalf=\n%s\n\nbottomHalf=\n%s",
len(topHalf), len(bottomHalf), topHalf, bottomHalf)
}
}

func TestThreadExhaustion(t *testing.T) {
output := runTestProg(t, "testprog", "ThreadExhaustion")
want := "runtime: program exceeds 10-thread limit\nfatal error: thread exhaustion"
Expand Down
13 changes: 13 additions & 0 deletions src/runtime/testdata/testprog/deadlock.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func init() {
register("LockedDeadlock2", LockedDeadlock2)
register("GoexitDeadlock", GoexitDeadlock)
register("StackOverflow", StackOverflow)
register("StackOverflowTopAndBottomTraces", StackOverflowTopAndBottomTraces)
register("ThreadExhaustion", ThreadExhaustion)
register("RecursivePanic", RecursivePanic)
register("RecursivePanic2", RecursivePanic2)
Expand Down Expand Up @@ -85,6 +86,18 @@ func StackOverflow() {
f()
}

func StackOverflowTopAndBottomTraces() {
var fi, gi func()
fi = func() {
gi()
}
gi = func() {
fi()
}
debug.SetMaxStack(10000)
fi()
}

func ThreadExhaustion() {
debug.SetMaxThreads(10)
c := make(chan int)
Expand Down
Loading

0 comments on commit 3a81338

Please sign in to comment.