Skip to content

Commit

Permalink
stackutil improvements: (un-aggregated) snapshots, add gids to text o…
Browse files Browse the repository at this point in the history
…utput
  • Loading branch information
lukseven committed Nov 20, 2023
1 parent 9e991e1 commit d302972
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 22 deletions.
58 changes: 44 additions & 14 deletions util/stackutil/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package stackutil

import (
"bytes"
"io/ioutil"
"io"
"sort"
"strings"

"github.com/eluv-io/errors-go"
"github.com/maruel/panicparse/v2/stack"

"github.com/eluv-io/common-go/util/numberutil"
"github.com/eluv-io/common-go/util/sliceutil"
"github.com/eluv-io/errors-go"
"github.com/eluv-io/utc-go"
)

const (
Expand All @@ -18,32 +21,32 @@ const (

// AggregateStack is an aggregated stack dump - a wrapper around the "buckets" of the 3rd-party panicparse library.
type AggregateStack struct {
trace string
agg *stack.Aggregated
similarity stack.Similarity
Agg *stack.Aggregated
Similarity stack.Similarity
Timestamp utc.UTC
}

// Aggregate creates an aggregated stack dump object from the given stack trace. The aggregation will be performed more
// or less aggressively based on the provided similarity parameter.
func Aggregate(trace string, sim stack.Similarity) (*AggregateStack, error) {
in := bytes.NewBuffer([]byte(trace))

snapshot, _, err := stack.ScanSnapshot(in, ioutil.Discard, stack.DefaultOpts())
snapshot, _, err := stack.ScanSnapshot(in, io.Discard, stack.DefaultOpts())
if snapshot == nil {
return nil, errors.E("stack.Aggregate", errors.K.NotExist, err, "reason", "no stacktrace found")
}

agg := snapshot.Aggregate(sim)

return &AggregateStack{
trace: trace,
agg: agg,
similarity: sim,
Agg: agg,
Similarity: sim,
Timestamp: utc.Now(),
}, nil
}

func (a *AggregateStack) Buckets() []*stack.Bucket {
return a.agg.Buckets
return a.Agg.Buckets
}

func (a *AggregateStack) String() string {
Expand All @@ -52,7 +55,7 @@ func (a *AggregateStack) String() string {
}

func (a *AggregateStack) SimilarityString() string {
switch a.similarity {
switch a.Similarity {
case stack.ExactFlags:
return "identical"
case stack.ExactLines:
Expand All @@ -78,7 +81,7 @@ func (a *AggregateStack) AsText() (string, error) {
// AsHTML converts this stack to an HTML page.
func (a *AggregateStack) AsHTML() (string, error) {
out := &bytes.Buffer{}
err := a.agg.ToHTML(out, "")
err := a.Agg.ToHTML(out, "")
if err != nil {
return "", err
}
Expand All @@ -88,7 +91,7 @@ func (a *AggregateStack) AsHTML() (string, error) {
// SortByCount sorts the stack traces by the number of goroutines that were
// aggregated into the same stack trace.
func (a *AggregateStack) SortByCount(ascending bool) {
buckets := a.agg.Buckets
buckets := a.Agg.Buckets
sort.SliceStable(buckets, func(i, j int) bool {
return numberutil.LessInt(ascending, len(buckets[i].IDs), len(buckets[j].IDs), func() bool {
return numberutil.LessInt(ascending, buckets[i].SleepMax, buckets[j].SleepMax, func() bool {
Expand All @@ -100,7 +103,7 @@ func (a *AggregateStack) SortByCount(ascending bool) {

// SortBySleepTime sorts the stack traces by their sleep times.
func (a *AggregateStack) SortBySleepTime(ascending bool) {
buckets := a.agg.Buckets
buckets := a.Agg.Buckets
sort.SliceStable(buckets, func(i, j int) bool {
return numberutil.LessInt(ascending, buckets[i].SleepMax, buckets[j].SleepMax, func() bool {
return numberutil.LessInt(ascending, buckets[i].SleepMin, buckets[j].SleepMin, func() bool {
Expand All @@ -109,3 +112,30 @@ func (a *AggregateStack) SortBySleepTime(ascending bool) {
})
})
}

// Filter filters the AggregateStack by retaining the buckets that match the given "keep" function and removing all
// others. It returns the number of removed buckets.
func (a *AggregateStack) Filter(keep func(*stack.Bucket) bool) (removed int) {
remove := func(e *stack.Bucket) bool {
return !keep(e)
}
a.Agg.Buckets, removed = sliceutil.RemoveMatch(a.Agg.Buckets, remove)
return removed
}

// FilterText filters the AggregateStack by retaining (keep=true) or removing (keep=false) the buckets (aggregated call
// stacks) that match the given "match" strings. A bucket matches if at least one of its function calls or corresponding
// source filenames contains at least one of the "match" strings. The return value is the number of removed buckets.
func (a *AggregateStack) FilterText(keep bool, match ...string) (removed int) {
a.Agg.Buckets, removed = sliceutil.RemoveMatch(a.Agg.Buckets, func(e *stack.Bucket) bool {
for _, call := range e.Stack.Calls {
for _, s := range match {
if strings.Contains(call.SrcName, s) || strings.Contains(call.Func.Complete, s) {
return !keep
}
}
}
return keep
})
return removed
}
6 changes: 3 additions & 3 deletions util/stackutil/aggregate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package stackutil_test

import (
"fmt"
"io/ioutil"
"os"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -11,7 +11,7 @@ import (
)

func TestAggregate(t *testing.T) {
stackb, err := ioutil.ReadFile("testdata/stacktrace1.txt")
stackb, err := os.ReadFile("testdata/stacktrace1.txt")
require.NoError(t, err)
stack := string(stackb)

Expand All @@ -31,7 +31,7 @@ func TestAggregate(t *testing.T) {
}

func TestAggregateHTML(t *testing.T) {
stackb, err := ioutil.ReadFile("testdata/stacktrace1.txt")
stackb, err := os.ReadFile("testdata/stacktrace1.txt")
require.NoError(t, err)
stack := string(stackb)

Expand Down
12 changes: 7 additions & 5 deletions util/stackutil/text.go → util/stackutil/aggregate_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import (
"io"
"text/template"

"github.com/eluv-io/utc-go"
"github.com/maruel/panicparse/v2/stack"

"github.com/eluv-io/utc-go"
)

const textTmpl = `
Expand All @@ -20,7 +21,7 @@ const textTmpl = `
** Aggregate Stacktrace **
generated on: {{.Now.String}}
generated on: {{.Timestamp.String}}
aggregation mode: {{.Similarity}}
go-routines: {{.GoroutineCount}}
Expand All @@ -35,6 +36,7 @@ go-routines: {{.GoroutineCount}}
{{- end -}}
{{- if len .CreatedBy.Calls }} [Created by {{template "RenderCallNoSpace" index .CreatedBy.Calls 0 }}]
{{- end -}}
{{- range .IDs}} gid={{.}}{{- end}}
{{- range .Signature.Stack.Calls}}
{{- template "RenderCall" .}}
{{- end}}
Expand All @@ -54,11 +56,11 @@ func (a *AggregateStack) writeAsText(out io.Writer) error {
}
data := struct {
Buckets []*stack.Bucket
Now utc.UTC
Timestamp utc.UTC
SrcLineSize int
GoroutineCount int
Similarity string
}{a.agg.Buckets, utc.Now(), srcLineLen, goroutineCount, a.SimilarityString()}
}{a.Agg.Buckets, a.Timestamp, srcLineLen, goroutineCount, a.SimilarityString()}
return t.Execute(out, data)
}

Expand All @@ -67,7 +69,7 @@ func (a *AggregateStack) calcLengths(fullPath bool) (int, int, int) {
goroutineCount := 0
srcLen := 0
pkgLen := 0
for _, bucket := range a.agg.Buckets {
for _, bucket := range a.Agg.Buckets {
goroutineCount += len(bucket.IDs)
for _, line := range bucket.Signature.Stack.Calls {

Expand Down
116 changes: 116 additions & 0 deletions util/stackutil/snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package stackutil

import (
"bytes"
"io"
"sort"
"strings"

"github.com/maruel/panicparse/v2/stack"

"github.com/eluv-io/common-go/util/numberutil"
"github.com/eluv-io/common-go/util/sliceutil"
"github.com/eluv-io/errors-go"
"github.com/eluv-io/utc-go"
)

// NewSnapshot creates a snapshot by parsing stacktrace information from the given string. Returns an error if no
// stacktraces are found.
func NewSnapshot(trace string) (*Snapshot, error) {
in := bytes.NewBuffer([]byte(trace))
snapshot, _, err := stack.ScanSnapshot(in, io.Discard, stack.DefaultOpts())
if snapshot == nil {
return nil, errors.E("stack.CreateSnapshot", errors.K.NotExist, "error", err, "reason", "no stacktrace found")
}
s := &Snapshot{snapshot, utc.Now()}
s.SortByGID(true)
return s, nil
}

// ExtractSnapshots extracts all snapshot found in the given reader. In the case of an error, the snapshots found up to
// that point are returned.
func ExtractSnapshots(reader io.Reader) ([]*Snapshot, error) {
res := make([]*Snapshot, 0, 10)
for {
snapshot, _, err := stack.ScanSnapshot(reader, io.Discard, stack.DefaultOpts())
if snapshot == nil {
if err != io.EOF {
return res, errors.E("stack.ExtractSnapshots", errors.K.IO, err)
}
break
}
s := &Snapshot{snapshot, utc.Now()}
s.SortByGID(true)
res = append(res, s)
}
return res, nil
}

// Snapshot is a wrapper around github.com/maruel/panicparse/v2/stack.Snapshot and offers sorting, filtering
// and custom text marshalling.
type Snapshot struct {
*stack.Snapshot
Timestamp utc.UTC
}

// SortByGID sorts the goroutines by goroutine ID.
func (s *Snapshot) SortByGID(ascending bool) {
goroutines := s.Goroutines
sort.SliceStable(goroutines, func(i, j int) bool {
return numberutil.LessInt(ascending, goroutines[i].ID, goroutines[j].ID)
})
}

// AsText converts this stack to a text based form.
func (s *Snapshot) String() string {
res, _ := s.AsText()
return res
}

// AsText converts this stack to a text based form.
func (s *Snapshot) AsText() (string, error) {
out := &bytes.Buffer{}
err := s.writeAsText(out)
if err != nil {
return "", err
}
return out.String(), nil
}

// Filter filters the Snapshot by retaining the Goroutines that match the given "keep" function and removing all
// others. It returns the number of removed Goroutines.
func (s *Snapshot) Filter(keep func(goroutine *stack.Goroutine) bool) (removed int) {
remove := func(e *stack.Goroutine) bool {
return !keep(e)
}
s.Goroutines, removed = sliceutil.RemoveMatch(s.Goroutines, remove)
return removed
}

// FilterText filters the Snapshot by retaining (keep=true) or removing (keep=false) the Goroutines that match the given "match"
// strings. A Goroutine matches if at least one of its function calls or corresponding source
// filenames contains at least one of the "match" strings. The return value is the number of removed Goroutines.
func (s *Snapshot) FilterText(keep bool, match ...string) (removed int) {
s.Goroutines, removed = sliceutil.RemoveMatch(s.Goroutines, func(e *stack.Goroutine) bool {
for _, call := range e.Stack.Calls {
for _, s := range match {
if strings.Contains(call.SrcName, s) || strings.Contains(call.Func.Complete, s) {
return !keep
}
}
}
return keep
})
return removed
}

// Aggregate creates an aggregated stack dump from this snapshot. The aggregation is more or less aggressive based on
// the provided similarity parameter.
func (s *Snapshot) Aggregate(sim stack.Similarity) *AggregateStack {
agg := s.Snapshot.Aggregate(sim)
return &AggregateStack{
Agg: agg,
Similarity: sim,
Timestamp: s.Timestamp,
}
}
34 changes: 34 additions & 0 deletions util/stackutil/snapshot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package stackutil_test

import (
"fmt"
"os"
"testing"

"github.com/stretchr/testify/require"

"github.com/eluv-io/common-go/util/stackutil"
)

func TestSnapshot(t *testing.T) {
stackb, err := os.ReadFile("testdata/stacktrace1.txt")
require.NoError(t, err)
stack := string(stackb)

snapshot, err := stackutil.NewSnapshot(stack)
require.NoError(t, err)

require.Equal(t, 587, len(snapshot.Goroutines))

removed := snapshot.FilterText(true, "content-fabric")
require.Equal(t, 157, removed)
require.Equal(t, 430, len(snapshot.Goroutines))

removed = snapshot.FilterText(false, "qparts.")
require.Equal(t, 423, removed)
require.Equal(t, 7, len(snapshot.Goroutines))

text, err := snapshot.AsText()
require.NoError(t, err)
fmt.Println(text)
}
Loading

0 comments on commit d302972

Please sign in to comment.