Skip to content

Commit 91de685

Browse files
authored
stack: Parse all functions (#111)
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
1 parent 25cbb67 commit 91de685

File tree

7 files changed

+826
-19
lines changed

7 files changed

+826
-19
lines changed

internal/stack/stacks.go

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,15 @@ const _defaultBufferSize = 64 * 1024 // 64 KiB
3434

3535
// Stack represents a single Goroutine's stack.
3636
type Stack struct {
37-
id int
38-
state string
37+
id int
38+
state string // e.g. 'running', 'chan receive'
39+
40+
// The first function on the stack.
3941
firstFunction string
4042

43+
// A set of all functions in the stack,
44+
allFunctions map[string]struct{}
45+
4146
// Full, raw stack trace.
4247
fullStack string
4348
}
@@ -62,6 +67,13 @@ func (s Stack) FirstFunction() string {
6267
return s.firstFunction
6368
}
6469

70+
// HasFunction reports whether the stack has the given function
71+
// anywhere in it.
72+
func (s Stack) HasFunction(name string) bool {
73+
_, ok := s.allFunctions[name]
74+
return ok
75+
}
76+
6577
func (s Stack) String() string {
6678
return fmt.Sprintf(
6779
"Goroutine %v in state %v, with %v on top of the stack:\n%s",
@@ -126,9 +138,9 @@ func (p *stackParser) parseStack(line string) (Stack, error) {
126138
firstFunction string
127139
fullStack bytes.Buffer
128140
)
141+
funcs := make(map[string]struct{})
129142
for p.scan.Scan() {
130143
line := p.scan.Text()
131-
132144
if strings.HasPrefix(line, "goroutine ") {
133145
// If we see the goroutine header,
134146
// it's the end of this stack.
@@ -140,19 +152,74 @@ func (p *stackParser) parseStack(line string) (Stack, error) {
140152
fullStack.WriteString(line)
141153
fullStack.WriteByte('\n') // scanner trims the newline
142154

143-
// The first line after the header is the top of the stack.
144-
if firstFunction == "" {
145-
firstFunction, err = parseFirstFunc(line)
146-
if err != nil {
147-
return Stack{}, fmt.Errorf("extract function: %w", err)
155+
if len(line) == 0 {
156+
// Empty line usually marks the end of the stack
157+
// but we don't want to have to rely on that.
158+
// Just skip it.
159+
continue
160+
}
161+
162+
funcName, creator, err := parseFuncName(line)
163+
if err != nil {
164+
return Stack{}, fmt.Errorf("parse function: %w", err)
165+
}
166+
if !creator {
167+
// A function is part of a goroutine's stack
168+
// only if it's not a "created by" function.
169+
//
170+
// The creator function is part of a different stack.
171+
// We don't care about it right now.
172+
funcs[funcName] = struct{}{}
173+
if firstFunction == "" {
174+
firstFunction = funcName
175+
}
176+
177+
}
178+
179+
// The function name followed by a line in the form:
180+
//
181+
// <tab>example.com/path/to/package/file.go:123 +0x123
182+
//
183+
// We don't care about the position so we can skip this line.
184+
if p.scan.Scan() {
185+
// Be defensive:
186+
// Skip the line only if it starts with a tab.
187+
bs := p.scan.Bytes()
188+
if len(bs) > 0 && bs[0] == '\t' {
189+
fullStack.Write(bs)
190+
fullStack.WriteByte('\n')
191+
} else {
192+
// Put it back and let the next iteration handle it
193+
// if it doesn't start with a tab.
194+
p.scan.Unscan()
148195
}
149196
}
197+
198+
if creator {
199+
// The "created by" line is the last line of the stack.
200+
// We can stop parsing now.
201+
//
202+
// Note that if tracebackancestors=N is set,
203+
// there may be more a traceback of the creator function
204+
// following the "created by" line,
205+
// but it should not be considered part of this stack.
206+
// e.g.,
207+
//
208+
// created by testing.(*T).Run in goroutine 1
209+
// /usr/lib/go/src/testing/testing.go:1648 +0x3ad
210+
// [originating from goroutine 1]:
211+
// testing.(*T).Run(...)
212+
// /usr/lib/go/src/testing/testing.go:1649 +0x3ad
213+
//
214+
break
215+
}
150216
}
151217

152218
return Stack{
153219
id: id,
154220
state: state,
155221
firstFunction: firstFunction,
222+
allFunctions: funcs,
156223
fullStack: fullStack.String(),
157224
}, nil
158225
}
@@ -176,12 +243,35 @@ func getStackBuffer(all bool) []byte {
176243
}
177244
}
178245

179-
func parseFirstFunc(line string) (string, error) {
180-
line = strings.TrimSpace(line)
181-
if idx := strings.LastIndex(line, "("); idx > 0 {
182-
return line[:idx], nil
246+
// Parses a single function from the given line.
247+
// The line is in one of these formats:
248+
//
249+
// example.com/path/to/package.funcName(args...)
250+
// example.com/path/to/package.(*typeName).funcName(args...)
251+
// created by example.com/path/to/package.funcName
252+
// created by example.com/path/to/package.funcName in goroutine [...]
253+
//
254+
// Also reports whether the line was a "created by" line.
255+
func parseFuncName(line string) (name string, creator bool, err error) {
256+
if after, ok := strings.CutPrefix(line, "created by "); ok {
257+
// The function name is the part after "created by "
258+
// and before " in goroutine [...]".
259+
idx := strings.Index(after, " in goroutine")
260+
if idx >= 0 {
261+
after = after[:idx]
262+
}
263+
name = after
264+
creator = true
265+
} else if idx := strings.LastIndexByte(line, '('); idx >= 0 {
266+
// The function name is the part before the last '('.
267+
name = line[:idx]
183268
}
184-
return "", fmt.Errorf("no function found: %q", line)
269+
270+
if name == "" {
271+
return "", false, fmt.Errorf("no function found: %q", line)
272+
}
273+
274+
return name, creator, nil
185275
}
186276

187277
// parseGoStackHeader parses a stack header that looks like:

0 commit comments

Comments
 (0)