Skip to content

Commit

Permalink
stack: Parse all functions (#111)
Browse files Browse the repository at this point in the history
Adds support to the stack parser for reading the full list of functions
for a stack trace.

NOTE:
The function that created the goroutine
is NOT considered part of the stack.

We don't maintain the order of the functions
since that's not something we need at this time.
The functions are all placed in a set.

This unblocks #41 and allows implementing an
IgnoreAnyFunction option (similar to the stalled #80 PR).

Depends on #110
  • Loading branch information
abhinav committed Oct 23, 2023
1 parent 25cbb67 commit 91de685
Show file tree
Hide file tree
Showing 7 changed files with 826 additions and 19 deletions.
116 changes: 103 additions & 13 deletions internal/stack/stacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ const _defaultBufferSize = 64 * 1024 // 64 KiB

// Stack represents a single Goroutine's stack.
type Stack struct {
id int
state string
id int
state string // e.g. 'running', 'chan receive'

// The first function on the stack.
firstFunction string

// A set of all functions in the stack,
allFunctions map[string]struct{}

// Full, raw stack trace.
fullStack string
}
Expand All @@ -62,6 +67,13 @@ func (s Stack) FirstFunction() string {
return s.firstFunction
}

// HasFunction reports whether the stack has the given function
// anywhere in it.
func (s Stack) HasFunction(name string) bool {
_, ok := s.allFunctions[name]
return ok
}

func (s Stack) String() string {
return fmt.Sprintf(
"Goroutine %v in state %v, with %v on top of the stack:\n%s",
Expand Down Expand Up @@ -126,9 +138,9 @@ func (p *stackParser) parseStack(line string) (Stack, error) {
firstFunction string
fullStack bytes.Buffer
)
funcs := make(map[string]struct{})
for p.scan.Scan() {
line := p.scan.Text()

if strings.HasPrefix(line, "goroutine ") {
// If we see the goroutine header,
// it's the end of this stack.
Expand All @@ -140,19 +152,74 @@ func (p *stackParser) parseStack(line string) (Stack, error) {
fullStack.WriteString(line)
fullStack.WriteByte('\n') // scanner trims the newline

// The first line after the header is the top of the stack.
if firstFunction == "" {
firstFunction, err = parseFirstFunc(line)
if err != nil {
return Stack{}, fmt.Errorf("extract function: %w", err)
if len(line) == 0 {
// Empty line usually marks the end of the stack
// but we don't want to have to rely on that.
// Just skip it.
continue
}

funcName, creator, err := parseFuncName(line)
if err != nil {
return Stack{}, fmt.Errorf("parse function: %w", err)
}
if !creator {
// A function is part of a goroutine's stack
// only if it's not a "created by" function.
//
// The creator function is part of a different stack.
// We don't care about it right now.
funcs[funcName] = struct{}{}
if firstFunction == "" {
firstFunction = funcName
}

}

// The function name followed by a line in the form:
//
// <tab>example.com/path/to/package/file.go:123 +0x123
//
// We don't care about the position so we can skip this line.
if p.scan.Scan() {
// Be defensive:
// Skip the line only if it starts with a tab.
bs := p.scan.Bytes()
if len(bs) > 0 && bs[0] == '\t' {
fullStack.Write(bs)
fullStack.WriteByte('\n')
} else {
// Put it back and let the next iteration handle it
// if it doesn't start with a tab.
p.scan.Unscan()
}
}

if creator {
// The "created by" line is the last line of the stack.
// We can stop parsing now.
//
// Note that if tracebackancestors=N is set,
// there may be more a traceback of the creator function
// following the "created by" line,
// but it should not be considered part of this stack.
// e.g.,
//
// created by testing.(*T).Run in goroutine 1
// /usr/lib/go/src/testing/testing.go:1648 +0x3ad
// [originating from goroutine 1]:
// testing.(*T).Run(...)
// /usr/lib/go/src/testing/testing.go:1649 +0x3ad
//
break
}
}

return Stack{
id: id,
state: state,
firstFunction: firstFunction,
allFunctions: funcs,
fullStack: fullStack.String(),
}, nil
}
Expand All @@ -176,12 +243,35 @@ func getStackBuffer(all bool) []byte {
}
}

func parseFirstFunc(line string) (string, error) {
line = strings.TrimSpace(line)
if idx := strings.LastIndex(line, "("); idx > 0 {
return line[:idx], nil
// Parses a single function from the given line.
// The line is in one of these formats:
//
// example.com/path/to/package.funcName(args...)
// example.com/path/to/package.(*typeName).funcName(args...)
// created by example.com/path/to/package.funcName
// created by example.com/path/to/package.funcName in goroutine [...]
//
// Also reports whether the line was a "created by" line.
func parseFuncName(line string) (name string, creator bool, err error) {
if after, ok := strings.CutPrefix(line, "created by "); ok {
// The function name is the part after "created by "
// and before " in goroutine [...]".
idx := strings.Index(after, " in goroutine")
if idx >= 0 {
after = after[:idx]
}
name = after
creator = true
} else if idx := strings.LastIndexByte(line, '('); idx >= 0 {
// The function name is the part before the last '('.
name = line[:idx]
}
return "", fmt.Errorf("no function found: %q", line)

if name == "" {
return "", false, fmt.Errorf("no function found: %q", line)
}

return name, creator, nil
}

// parseGoStackHeader parses a stack header that looks like:
Expand Down

0 comments on commit 91de685

Please sign in to comment.