@@ -34,10 +34,15 @@ const _defaultBufferSize = 64 * 1024 // 64 KiB
34
34
35
35
// Stack represents a single Goroutine's stack.
36
36
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.
39
41
firstFunction string
40
42
43
+ // A set of all functions in the stack,
44
+ allFunctions map [string ]struct {}
45
+
41
46
// Full, raw stack trace.
42
47
fullStack string
43
48
}
@@ -62,6 +67,13 @@ func (s Stack) FirstFunction() string {
62
67
return s .firstFunction
63
68
}
64
69
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
+
65
77
func (s Stack ) String () string {
66
78
return fmt .Sprintf (
67
79
"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) {
126
138
firstFunction string
127
139
fullStack bytes.Buffer
128
140
)
141
+ funcs := make (map [string ]struct {})
129
142
for p .scan .Scan () {
130
143
line := p .scan .Text ()
131
-
132
144
if strings .HasPrefix (line , "goroutine " ) {
133
145
// If we see the goroutine header,
134
146
// it's the end of this stack.
@@ -140,19 +152,74 @@ func (p *stackParser) parseStack(line string) (Stack, error) {
140
152
fullStack .WriteString (line )
141
153
fullStack .WriteByte ('\n' ) // scanner trims the newline
142
154
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 ()
148
195
}
149
196
}
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
+ }
150
216
}
151
217
152
218
return Stack {
153
219
id : id ,
154
220
state : state ,
155
221
firstFunction : firstFunction ,
222
+ allFunctions : funcs ,
156
223
fullStack : fullStack .String (),
157
224
}, nil
158
225
}
@@ -176,12 +243,35 @@ func getStackBuffer(all bool) []byte {
176
243
}
177
244
}
178
245
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 ]
183
268
}
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
185
275
}
186
276
187
277
// parseGoStackHeader parses a stack header that looks like:
0 commit comments