diff --git a/util/stack/compress.go b/util/stack/compress.go new file mode 100644 index 000000000000..6d9a78f34702 --- /dev/null +++ b/util/stack/compress.go @@ -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 +} diff --git a/util/stack/compress_test.go b/util/stack/compress_test.go new file mode 100644 index 000000000000..d43c6618b15a --- /dev/null +++ b/util/stack/compress_test.go @@ -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]) +} diff --git a/util/stack/stack.go b/util/stack/stack.go index 2fec18cb7a6d..458a1a7817e1 100644 --- a/util/stack/stack.go +++ b/util/stack/stack.go @@ -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) {