Skip to content

Commit d6b158d

Browse files
committed
stack: compress shared stacks for clearer output
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]>
1 parent 5a91bd3 commit d6b158d

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

util/stack/compress.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package stack
2+
3+
import (
4+
"slices"
5+
)
6+
7+
func compressStacks(st []*Stack) []*Stack {
8+
if len(st) == 0 {
9+
return nil
10+
}
11+
12+
slices.SortFunc(st, func(a, b *Stack) int {
13+
return len(b.Frames) - len(a.Frames)
14+
})
15+
16+
out := []*Stack{st[0]}
17+
18+
loop0:
19+
for _, st := range st[1:] {
20+
maxIdx := -1
21+
for _, prev := range out {
22+
idx := subFrames(st.Frames, prev.Frames)
23+
if idx == -1 {
24+
continue
25+
}
26+
// full match, potentially skip all
27+
if idx == len(st.Frames)-1 {
28+
if st.Pid == prev.Pid && st.Version == prev.Version && slices.Compare(st.Cmdline, st.Cmdline) == 0 {
29+
continue loop0
30+
}
31+
}
32+
if idx > maxIdx {
33+
maxIdx = idx
34+
}
35+
}
36+
37+
if maxIdx > 0 {
38+
st.Frames = st.Frames[:len(st.Frames)-maxIdx]
39+
}
40+
out = append(out, st)
41+
}
42+
43+
return out
44+
}
45+
46+
func subFrames(a, b []*Frame) int {
47+
idx := -1
48+
i := len(a) - 1
49+
j := len(b) - 1
50+
for i >= 0 {
51+
if j < 0 {
52+
break
53+
}
54+
if a[i].Equal(b[j]) {
55+
idx++
56+
i--
57+
j--
58+
} else {
59+
break
60+
}
61+
}
62+
return idx
63+
}
64+
65+
func (a *Frame) Equal(b *Frame) bool {
66+
return a.File == b.File && a.Line == b.Line && a.Name == b.Name
67+
}

util/stack/compress_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package stack
2+
3+
import (
4+
"testing"
5+
6+
"github.com/pkg/errors"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func testcall1() error {
11+
return errors.Errorf("error1")
12+
}
13+
14+
func testcall2() error {
15+
return errors.WithStack(testcall1())
16+
}
17+
18+
func testcall3() error {
19+
err := testcall2()
20+
// this is from a different line
21+
return errors.WithStack(err)
22+
}
23+
24+
func TestCompressStacks(t *testing.T) {
25+
err := testcall2()
26+
st := Traces(err)
27+
28+
// full trace match, shorter is removed
29+
require.Len(t, st, 1)
30+
require.GreaterOrEqual(t, len(st[0].Frames), 2)
31+
32+
f := st[0].Frames
33+
require.Contains(t, f[0].Name, "testcall1")
34+
require.Contains(t, f[1].Name, "testcall2")
35+
}
36+
37+
func TestCompressMultiStacks(t *testing.T) {
38+
err := testcall3()
39+
st := Traces(err)
40+
41+
require.Len(t, st, 2)
42+
require.GreaterOrEqual(t, len(st[0].Frames), 4)
43+
44+
f1 := st[0].Frames
45+
require.Contains(t, f1[0].Name, "testcall1")
46+
require.Contains(t, f1[1].Name, "testcall2")
47+
require.Contains(t, f1[2].Name, "testcall3")
48+
49+
f2 := st[1].Frames
50+
require.Contains(t, f2[0].Name, "testcall3")
51+
// next line is shared and everything after is removed
52+
require.Len(t, f2, 2)
53+
require.Equal(t, f1[3], f2[1])
54+
}

util/stack/stack.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func Helper() {
4444
}
4545

4646
func Traces(err error) []*Stack {
47+
return compressStacks(traces(err))
48+
}
49+
50+
func traces(err error) []*Stack {
4751
var st []*Stack
4852

4953
switch e := err.(type) {

0 commit comments

Comments
 (0)