Skip to content

Commit

Permalink
stack: compress shared stacks for clearer output
Browse files Browse the repository at this point in the history
Combines the shared parts of stacktraces so they
take up less space. For example if error is wrapped
from similar codepath to the main error, the main error
gets the stacktrace and wrapping gets only to stacktrace
up to the point where same frame exists in main error as well.

This also means we need to be less careful about WithStack as
if error already has a longer stack from current position, it
would be ignored.

Signed-off-by: Tonis Tiigi <[email protected]>
  • Loading branch information
tonistiigi committed Jul 23, 2024
1 parent 5a91bd3 commit d6b158d
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 0 deletions.
67 changes: 67 additions & 0 deletions util/stack/compress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package stack

import (
"slices"
)

func compressStacks(st []*Stack) []*Stack {
if len(st) == 0 {
return nil
}

slices.SortFunc(st, func(a, b *Stack) int {
return len(b.Frames) - len(a.Frames)
})

out := []*Stack{st[0]}

loop0:
for _, st := range st[1:] {
maxIdx := -1
for _, prev := range out {
idx := subFrames(st.Frames, prev.Frames)
if idx == -1 {
continue
}
// full match, potentially skip all
if idx == len(st.Frames)-1 {
if st.Pid == prev.Pid && st.Version == prev.Version && slices.Compare(st.Cmdline, st.Cmdline) == 0 {
continue loop0
}
}
if idx > maxIdx {
maxIdx = idx
}
}

if maxIdx > 0 {
st.Frames = st.Frames[:len(st.Frames)-maxIdx]
}
out = append(out, st)
}

return out
}

func subFrames(a, b []*Frame) int {
idx := -1
i := len(a) - 1
j := len(b) - 1
for i >= 0 {
if j < 0 {
break
}
if a[i].Equal(b[j]) {
idx++
i--
j--
} else {
break
}
}
return idx
}

func (a *Frame) Equal(b *Frame) bool {
return a.File == b.File && a.Line == b.Line && a.Name == b.Name
}
54 changes: 54 additions & 0 deletions util/stack/compress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package stack

import (
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)

func testcall1() error {
return errors.Errorf("error1")
}

func testcall2() error {
return errors.WithStack(testcall1())
}

func testcall3() error {
err := testcall2()
// this is from a different line
return errors.WithStack(err)
}

func TestCompressStacks(t *testing.T) {
err := testcall2()
st := Traces(err)

// full trace match, shorter is removed
require.Len(t, st, 1)
require.GreaterOrEqual(t, len(st[0].Frames), 2)

f := st[0].Frames
require.Contains(t, f[0].Name, "testcall1")
require.Contains(t, f[1].Name, "testcall2")
}

func TestCompressMultiStacks(t *testing.T) {
err := testcall3()
st := Traces(err)

require.Len(t, st, 2)
require.GreaterOrEqual(t, len(st[0].Frames), 4)

f1 := st[0].Frames
require.Contains(t, f1[0].Name, "testcall1")
require.Contains(t, f1[1].Name, "testcall2")
require.Contains(t, f1[2].Name, "testcall3")

f2 := st[1].Frames
require.Contains(t, f2[0].Name, "testcall3")
// next line is shared and everything after is removed
require.Len(t, f2, 2)
require.Equal(t, f1[3], f2[1])
}
4 changes: 4 additions & 0 deletions util/stack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func Helper() {
}

func Traces(err error) []*Stack {
return compressStacks(traces(err))
}

func traces(err error) []*Stack {
var st []*Stack

switch e := err.(type) {
Expand Down

0 comments on commit d6b158d

Please sign in to comment.