├── go.mod ├── main.go └── util ├── stack.go └── stack_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/whyrusleeping/stackparse 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "text/tabwriter" 13 | "time" 14 | 15 | util "github.com/whyrusleeping/stackparse/util" 16 | ) 17 | 18 | func printHelp() { 19 | helpstr := ` 20 | To filter out goroutines from the trace, use the following flags: 21 | --frame-match=FOO or --fm=FOO 22 | print only stacks with frames that contain 'FOO' 23 | --frame-not-match=FOO or --fnm=FOO 24 | print only stacks with no frames containing 'FOO' 25 | --wait-more-than=10m 26 | print only stacks that have been blocked for more than ten minutes 27 | --wait-less-than=10m 28 | print only stacks that have been blocked for less than ten minutes 29 | --state-match=FOO 30 | print only stacks whose state matches 'FOO' 31 | --state-not-match=FOO 32 | print only stacks whose state matches 'FOO' 33 | 34 | Output is by default sorted by waittime ascending, to change this use: 35 | --sort=[stacksize,goronum,waittime] 36 | 37 | To print a summary of the goroutines in the stack trace, use: 38 | --summary 39 | 40 | If your stacks have some prefix to them (like a systemd log prefix) trim it with: 41 | --line-prefix=prefixRegex 42 | 43 | To print the output in JSON format, use: 44 | --json or -j 45 | ` 46 | fmt.Println(helpstr) 47 | } 48 | 49 | func main() { 50 | if len(os.Args) < 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { 51 | fmt.Printf("usage: %s \n", os.Args[0]) 52 | printHelp() 53 | return 54 | } 55 | 56 | var filters []util.Filter 57 | var compfunc util.StackCompFunc = util.CompWaitTime 58 | outputType := "full" 59 | formatType := "default" 60 | fname := "-" 61 | 62 | var linePrefix string 63 | 64 | var repl bool 65 | 66 | // parse flags 67 | for _, a := range os.Args[1:] { 68 | if strings.HasPrefix(a, "-") { 69 | parts := strings.Split(a, "=") 70 | var key string 71 | var val string 72 | key = parts[0] 73 | if len(parts) == 2 { 74 | val = parts[1] 75 | } 76 | 77 | switch key { 78 | case "--frame-match", "--fm": 79 | filters = append(filters, util.HasFrameMatching(val)) 80 | case "--wait-more-than": 81 | d, err := time.ParseDuration(val) 82 | if err != nil { 83 | fmt.Println(err) 84 | os.Exit(1) 85 | } 86 | filters = append(filters, util.TimeGreaterThan(d)) 87 | case "--wait-less-than": 88 | d, err := time.ParseDuration(val) 89 | if err != nil { 90 | fmt.Println(err) 91 | os.Exit(1) 92 | } 93 | filters = append(filters, util.Negate(util.TimeGreaterThan(d))) 94 | case "--frame-not-match", "--fnm": 95 | filters = append(filters, util.Negate(util.HasFrameMatching(val))) 96 | case "--state-match": 97 | filters = append(filters, util.MatchState(val)) 98 | case "--state-not-match": 99 | filters = append(filters, util.Negate(util.MatchState(val))) 100 | case "--sort": 101 | switch parts[1] { 102 | case "goronum": 103 | compfunc = util.CompGoroNum 104 | case "stacksize": 105 | compfunc = util.CompDepth 106 | case "waittime": 107 | compfunc = util.CompWaitTime 108 | default: 109 | fmt.Println("unknown sorting parameter: ", val) 110 | fmt.Println("options: goronum, stacksize, waittime (default)") 111 | os.Exit(1) 112 | } 113 | case "--line-prefix": 114 | linePrefix = val 115 | 116 | case "--repl": 117 | repl = true 118 | case "--output": 119 | switch val { 120 | case "full", "top", "summary": 121 | outputType = val 122 | default: 123 | fmt.Println("unrecognized output type: ", parts[1]) 124 | fmt.Println("valid options are: full, top, summary, json") 125 | os.Exit(1) 126 | } 127 | case "--summary", "-s": 128 | outputType = "summary" 129 | case "--json", "-j": 130 | formatType = "json" 131 | case "--suspicious", "--sus": 132 | outputType = "sus" 133 | } 134 | } else { 135 | fname = a 136 | } 137 | } 138 | 139 | var r io.Reader 140 | if fname == "-" { 141 | r = os.Stdin 142 | } else { 143 | fi, err := os.Open(fname) 144 | if err != nil { 145 | fmt.Println(err) 146 | os.Exit(1) 147 | } 148 | defer fi.Close() 149 | 150 | r = fi 151 | } 152 | 153 | stacks, err := util.ParseStacks(r, linePrefix) 154 | if err != nil { 155 | fmt.Println(err) 156 | os.Exit(1) 157 | } 158 | 159 | sorter := util.StackSorter{ 160 | Stacks: stacks, 161 | CompFunc: compfunc, 162 | } 163 | 164 | sort.Sort(sorter) 165 | 166 | var f formatter 167 | switch formatType { 168 | case "default": 169 | f = &defaultFormatter{} 170 | case "json": 171 | f = &jsonFormatter{} 172 | } 173 | 174 | stacks = util.ApplyFilters(stacks, filters) 175 | 176 | var formatErr error 177 | 178 | switch outputType { 179 | case "full": 180 | formatErr = f.formatStacks(os.Stdout, stacks) 181 | case "summary": 182 | formatErr = f.formatSummaries(os.Stdout, summarize(stacks)) 183 | case "sus": 184 | suspiciousCheck(util.ApplyFilters(stacks, filters)) 185 | default: 186 | fmt.Println("unrecognized output type: ", outputType) 187 | os.Exit(1) 188 | } 189 | 190 | if formatErr != nil { 191 | fmt.Println(err) 192 | os.Exit(1) 193 | } 194 | 195 | if repl { 196 | runRepl(util.ApplyFilters(stacks, filters)) 197 | } 198 | } 199 | 200 | type summary struct { 201 | Function string 202 | Count int 203 | } 204 | 205 | type formatter interface { 206 | formatSummaries(io.Writer, []summary) error 207 | formatStacks(io.Writer, []*util.Stack) error 208 | } 209 | 210 | type defaultFormatter struct{} 211 | 212 | func (t *defaultFormatter) formatSummaries(w io.Writer, summaries []summary) error { 213 | tw := tabwriter.NewWriter(w, 8, 4, 2, ' ', 0) 214 | for _, s := range summaries { 215 | fmt.Fprintf(tw, "%s\t%d\n", s.Function, s.Count) 216 | } 217 | tw.Flush() 218 | return nil 219 | } 220 | 221 | func (t *defaultFormatter) formatStacks(w io.Writer, stacks []*util.Stack) error { 222 | for _, s := range stacks { 223 | fmt.Fprintln(w, s.String()) 224 | } 225 | return nil 226 | } 227 | 228 | type jsonFormatter struct{} 229 | 230 | func (j *jsonFormatter) formatSummaries(w io.Writer, summaries []summary) error { 231 | return json.NewEncoder(w).Encode(summaries) 232 | } 233 | 234 | func (j *jsonFormatter) formatStacks(w io.Writer, stacks []*util.Stack) error { 235 | return json.NewEncoder(w).Encode(stacks) 236 | } 237 | 238 | func summarize(stacks []*util.Stack) []summary { 239 | counts := make(map[string]int) 240 | 241 | var filtered []*util.Stack 242 | 243 | for _, s := range stacks { 244 | f := s.Frames[0].Function 245 | if counts[f] == 0 { 246 | filtered = append(filtered, s) 247 | } 248 | counts[f]++ 249 | } 250 | 251 | sort.Sort(util.StackSorter{ 252 | Stacks: filtered, 253 | CompFunc: func(a, b *util.Stack) bool { 254 | return counts[a.Frames[0].Function] < counts[b.Frames[0].Function] 255 | }, 256 | }) 257 | 258 | var summaries []summary 259 | for _, s := range filtered { 260 | summaries = append(summaries, summary{ 261 | Function: s.Frames[0].Function, 262 | Count: counts[s.Frames[0].Function], 263 | }) 264 | } 265 | return summaries 266 | } 267 | 268 | type framecount struct { 269 | frameKey string 270 | count int 271 | } 272 | 273 | // work in progress, trying to come up with an algorithm to point out suspicious stacks 274 | func suspiciousCheck(stacks []*util.Stack) { 275 | sharedFrames := make(map[string][]*util.Stack) 276 | 277 | for _, s := range stacks { 278 | for _, f := range s.Frames { 279 | fk := f.FrameKey() 280 | sharedFrames[fk] = append(sharedFrames[fk], s) 281 | } 282 | } 283 | 284 | var fcs []framecount 285 | for k, v := range sharedFrames { 286 | fcs = append(fcs, framecount{ 287 | frameKey: k, 288 | count: len(v), 289 | }) 290 | } 291 | 292 | sort.Slice(fcs, func(i, j int) bool { 293 | return fcs[i].count > fcs[j].count 294 | }) 295 | 296 | for i := 0; i < 20; i++ { 297 | fmt.Printf("%s - %d\n", fcs[i].frameKey, fcs[i].count) 298 | } 299 | 300 | for i := 0; i < 5; i++ { 301 | fmt.Println("-------- FRAME SUS STAT ------") 302 | fmt.Printf("%s - %d\n", fcs[i].frameKey, fcs[i].count) 303 | 304 | sf := sharedFrames[fcs[i].frameKey] 305 | 306 | printUnique(sf) 307 | } 308 | } 309 | 310 | func printUnique(stacks []*util.Stack) { 311 | var ftypes []*util.Stack 312 | var bucketed [][]*util.Stack 313 | for _, s := range stacks { 314 | var found bool 315 | for x, ft := range ftypes { 316 | if s.Sameish(ft) { 317 | bucketed[x] = append(bucketed[x], s) 318 | found = true 319 | } 320 | } 321 | 322 | if !found { 323 | ftypes = append(ftypes, s) 324 | bucketed = append(bucketed, []*util.Stack{s}) 325 | } 326 | } 327 | 328 | for x, ft := range ftypes { 329 | fmt.Println("count: ", len(bucketed[x])) 330 | fmt.Println("average wait: ", compWaitStats(bucketed[x]).String()) 331 | fmt.Println(ft.String()) 332 | fmt.Println() 333 | } 334 | } 335 | 336 | type waitStats struct { 337 | Average time.Duration 338 | Max time.Duration 339 | Min time.Duration 340 | Median time.Duration 341 | } 342 | 343 | func (ws waitStats) String() string { 344 | return fmt.Sprintf("av/min/max/med: %s/%s/%s/%s\n", ws.Average, ws.Min, ws.Max, ws.Median) 345 | } 346 | 347 | func compWaitStats(stacks []*util.Stack) waitStats { 348 | var durations []time.Duration 349 | var min, max, sum time.Duration 350 | for _, s := range stacks { 351 | if min == 0 || s.WaitTime < min { 352 | min = s.WaitTime 353 | } 354 | if s.WaitTime > max { 355 | max = s.WaitTime 356 | } 357 | 358 | sum += s.WaitTime 359 | durations = append(durations, s.WaitTime) 360 | } 361 | 362 | sort.Slice(durations, func(i, j int) bool { 363 | return durations[i] < durations[j] 364 | }) 365 | 366 | return waitStats{ 367 | Average: sum / time.Duration(len(durations)), 368 | Max: max, 369 | Min: min, 370 | Median: durations[len(durations)/2], 371 | } 372 | } 373 | 374 | func frameStat(stacks []*util.Stack) { 375 | frames := make(map[string]int) 376 | 377 | for _, s := range stacks { 378 | for _, f := range s.Frames { 379 | frames[fmt.Sprintf("%s:%d\n%s", f.File, f.Line, f.Function)]++ 380 | } 381 | } 382 | 383 | type frameCount struct { 384 | Line string 385 | Count int 386 | } 387 | 388 | var fcs []frameCount 389 | for k, v := range frames { 390 | fcs = append(fcs, frameCount{ 391 | Line: k, 392 | Count: v, 393 | }) 394 | } 395 | 396 | sort.Slice(fcs, func(i, j int) bool { 397 | return fcs[i].Count < fcs[j].Count 398 | }) 399 | 400 | for _, fc := range fcs { 401 | fmt.Printf("%s\t%d\n", fc.Line, fc.Count) 402 | } 403 | } 404 | 405 | func runRepl(input []*util.Stack) { 406 | bynumber := make(map[int]*util.Stack) 407 | for _, i := range input { 408 | bynumber[i.Number] = i 409 | } 410 | 411 | stk := [][]*util.Stack{input} 412 | ops := []string{"."} 413 | 414 | cur := input 415 | 416 | f := &defaultFormatter{} 417 | 418 | scan := bufio.NewScanner(os.Stdin) 419 | fmt.Print("stackparse> ") 420 | for scan.Scan() { 421 | parts := strings.Split(scan.Text(), " ") 422 | switch parts[0] { 423 | case "fm", "frame-match": 424 | 425 | var filters []util.Filter 426 | for _, p := range parts[1:] { 427 | filters = append(filters, util.HasFrameMatching(strings.TrimSpace(p))) 428 | } 429 | 430 | cur = util.ApplyFilters(cur, filters) 431 | stk = append(stk, cur) 432 | ops = append(ops, scan.Text()) 433 | 434 | case "fnm", "frame-not-match": 435 | var filters []util.Filter 436 | for _, p := range parts[1:] { 437 | filters = append(filters, util.Negate(util.HasFrameMatching(strings.TrimSpace(p)))) 438 | } 439 | 440 | cur = util.ApplyFilters(cur, filters) 441 | stk = append(stk, cur) 442 | ops = append(ops, scan.Text()) 443 | case "s", "summary", "sum": 444 | err := f.formatSummaries(os.Stdout, summarize(cur)) 445 | if err != nil { 446 | fmt.Println(err) 447 | } 448 | case "show", "p", "print": 449 | if len(parts) > 1 { 450 | num, err := strconv.Atoi(parts[1]) 451 | if err != nil { 452 | fmt.Println(err) 453 | goto end 454 | } 455 | s, ok := bynumber[num] 456 | if !ok { 457 | fmt.Println("no stack found with that number") 458 | goto end 459 | } 460 | f.formatStacks(os.Stdout, []*util.Stack{s}) 461 | } else { 462 | f.formatStacks(os.Stdout, cur) 463 | } 464 | case "diff": 465 | for i, op := range ops { 466 | fmt.Printf("%d (%d): %s\n", i, len(stk[i]), op) 467 | } 468 | case "pop": 469 | if len(stk) > 1 { 470 | stk = stk[:len(stk)-1] 471 | ops = ops[:len(ops)-1] 472 | 473 | cur = stk[len(stk)-1] 474 | } 475 | case "sus": 476 | // WIP!!! 477 | suspiciousCheck(cur) 478 | case "framestat": 479 | frameStat(cur) 480 | case "unique", "uu": 481 | printUnique(cur) 482 | } 483 | 484 | end: 485 | fmt.Print("stackparse> ") 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /util/stack.go: -------------------------------------------------------------------------------- 1 | package stacks 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "runtime/debug" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type Stack struct { 15 | Number int 16 | State string 17 | WaitTime time.Duration 18 | Frames []Frame 19 | ThreadLocked bool 20 | CreatedBy CreatedBy 21 | FramesElided bool 22 | } 23 | 24 | func (s *Stack) String() string { 25 | sb := strings.Builder{} 26 | state := s.State 27 | waitTime := int(s.WaitTime.Minutes()) 28 | if waitTime != 0 { 29 | state += ", " + fmt.Sprintf("%d minutes", waitTime) 30 | } 31 | sb.WriteString(fmt.Sprintf("goroutine %d [%s]:\n", s.Number, state)) 32 | for _, f := range s.Frames { 33 | sb.WriteString(f.String()) 34 | sb.WriteRune('\n') 35 | } 36 | sb.WriteString(s.CreatedBy.String()) 37 | sb.WriteRune('\n') 38 | return sb.String() 39 | } 40 | 41 | type Frame struct { 42 | Function string 43 | Params []string 44 | File string 45 | Line int64 46 | Entry int64 47 | } 48 | 49 | func (f *Frame) String() string { 50 | sb := strings.Builder{} 51 | sb.WriteString(fmt.Sprintf("%s(%s)\n", f.Function, strings.Join(f.Params, ", "))) 52 | sb.WriteString(fmt.Sprintf("\t%s:%d", f.File, f.Line)) 53 | if f.Entry != 0 { 54 | sb.WriteString(fmt.Sprintf(" %+#x", f.Entry)) 55 | } 56 | return sb.String() 57 | } 58 | 59 | type CreatedBy struct { 60 | Function string 61 | File string 62 | Line int64 63 | Entry int64 64 | } 65 | 66 | func (c *CreatedBy) String() string { 67 | sb := strings.Builder{} 68 | sb.WriteString(fmt.Sprintf("created by %s\n", c.Function)) 69 | sb.WriteString(fmt.Sprintf("\t%s:%d", c.File, c.Line)) 70 | if c.Entry != 0 { 71 | sb.WriteString(fmt.Sprintf(" %+#x", c.Entry)) 72 | } 73 | return sb.String() 74 | } 75 | 76 | func (f *Frame) FrameKey() string { 77 | return fmt.Sprintf("%s", f.Function) 78 | } 79 | 80 | func (s *Stack) Sameish(os *Stack) bool { 81 | if len(s.Frames) != len(os.Frames) { 82 | return false 83 | } 84 | 85 | for i := 0; i < len(s.Frames); i++ { 86 | if s.Frames[i].Function != os.Frames[i].Function { 87 | return false 88 | } 89 | } 90 | 91 | return true 92 | } 93 | 94 | type Filter func(s *Stack) bool 95 | 96 | func HasFrameMatching(pattern string) Filter { 97 | return func(s *Stack) bool { 98 | for _, f := range s.Frames { 99 | if strings.Contains(f.Function, pattern) || strings.Contains(fmt.Sprintf("%s:%d", f.File, f.Line), pattern) { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | } 106 | 107 | func MatchState(st string) Filter { 108 | return func(s *Stack) bool { 109 | return s.State == st 110 | } 111 | } 112 | 113 | func TimeGreaterThan(d time.Duration) Filter { 114 | return func(s *Stack) bool { 115 | return s.WaitTime >= d 116 | } 117 | } 118 | 119 | func Negate(f Filter) Filter { 120 | return func(s *Stack) bool { 121 | return !f(s) 122 | } 123 | } 124 | 125 | func ApplyFilters(stacks []*Stack, filters []Filter) []*Stack { 126 | var out []*Stack 127 | 128 | next: 129 | for _, s := range stacks { 130 | for _, f := range filters { 131 | if !f(s) { 132 | continue next 133 | } 134 | } 135 | out = append(out, s) 136 | } 137 | return out 138 | } 139 | 140 | func ParseStacks(r io.Reader, linePrefix string) (_ []*Stack, _err error) { 141 | var re *regexp.Regexp 142 | 143 | if linePrefix != "" { 144 | r, err := regexp.Compile(linePrefix) 145 | if err != nil { 146 | return nil, fmt.Errorf("failed to compile line prefix regexp") 147 | } 148 | re = r 149 | } 150 | 151 | // Catch parsing errors and recover. There's no reason to crash the entire parser. 152 | // Also report the line number where the error happened. 153 | lineNo := 0 154 | defer func() { 155 | if r := recover(); r != nil { 156 | _err = fmt.Errorf("line %d: [panic] %s\n%s", lineNo, r, debug.Stack()) 157 | } else if _err != nil { 158 | _err = fmt.Errorf("line %d: %w", lineNo, _err) 159 | } 160 | }() 161 | 162 | var cur *Stack 163 | var stacks []*Stack 164 | var frame *Frame 165 | scan := bufio.NewScanner(r) 166 | for scan.Scan() { 167 | lineNo++ 168 | line := scan.Text() 169 | if re != nil { 170 | pref := re.Find([]byte(line)) 171 | if len(pref) == len(line) { 172 | line = "" 173 | } else { 174 | line = line[len(pref):] 175 | line = strings.TrimSpace(line) 176 | } 177 | } 178 | 179 | if strings.HasPrefix(line, "goroutine") { 180 | if cur != nil { 181 | stacks = append(stacks, cur) 182 | cur = nil 183 | } 184 | 185 | parts := strings.Split(line, " ") 186 | num, err := strconv.Atoi(parts[1]) 187 | if err != nil { 188 | return nil, fmt.Errorf("unexpected formatting: %s", line) 189 | } 190 | 191 | var timev time.Duration 192 | state := strings.Split(strings.Trim(strings.Join(parts[2:], " "), "[]:"), ",") 193 | locked := false 194 | // The first field is always the state. The second and 195 | // third are the time and whether or not it's locked to 196 | // the current thread. However, either or both of these fields can be omitted. 197 | for _, s := range state[1:] { 198 | if s == " locked to thread" { 199 | locked = true 200 | continue 201 | } 202 | timeparts := strings.Fields(state[1]) 203 | if len(timeparts) != 2 { 204 | return nil, fmt.Errorf("weirdly formatted time string: %q", state[1]) 205 | } 206 | 207 | val, err := strconv.Atoi(timeparts[0]) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | timev = time.Duration(val) * time.Minute 213 | } 214 | 215 | cur = &Stack{ 216 | Number: num, 217 | State: state[0], 218 | WaitTime: timev, 219 | ThreadLocked: locked, 220 | } 221 | continue 222 | } 223 | if line == "" { 224 | // This can happen when we get random empty lines. 225 | if cur != nil { 226 | stacks = append(stacks, cur) 227 | } 228 | cur = nil 229 | continue 230 | } 231 | 232 | if strings.HasPrefix(line, "created by") { 233 | fn := strings.TrimPrefix(line, "created by ") 234 | if !scan.Scan() { 235 | return nil, fmt.Errorf("no file info after 'created by' line on line %d", lineNo) 236 | } 237 | file, line, entry, err := parseEntryLine(scan.Text()) 238 | if err != nil { 239 | return nil, err 240 | } 241 | cur.CreatedBy = CreatedBy{ 242 | Function: fn, 243 | File: file, 244 | Line: line, 245 | Entry: entry, 246 | } 247 | } else if frame == nil { 248 | if strings.Contains(line, "...additional frames elided...") { 249 | cur.FramesElided = true 250 | continue 251 | } 252 | 253 | frame = &Frame{ 254 | Function: line, 255 | } 256 | 257 | n := strings.LastIndexByte(line, '(') 258 | if n > -1 { 259 | frame.Function = line[:n] 260 | frame.Params = strings.Split(line[n+1:len(line)-1], ", ") 261 | } 262 | 263 | } else { 264 | file, line, entry, err := parseEntryLine(line) 265 | if err != nil { 266 | return nil, err 267 | } 268 | frame.File = file 269 | frame.Line = line 270 | frame.Entry = entry 271 | cur.Frames = append(cur.Frames, *frame) 272 | frame = nil 273 | } 274 | } 275 | if cur != nil { 276 | stacks = append(stacks, cur) 277 | } 278 | 279 | return stacks, nil 280 | } 281 | 282 | func parseEntryLine(s string) (file string, line int64, entry int64, err error) { 283 | parts := strings.Split(s, ":") 284 | file = strings.Trim(parts[0], " \t\n") 285 | if len(parts) != 2 { 286 | return "", 0, 0, fmt.Errorf("expected a colon: %q", line) 287 | } 288 | 289 | lineAndEntry := strings.Split(parts[1], " ") 290 | line, err = strconv.ParseInt(lineAndEntry[0], 0, 64) 291 | if err != nil { 292 | return "", 0, 0, fmt.Errorf("error parsing line number: %s", lineAndEntry[0]) 293 | } 294 | if len(lineAndEntry) > 1 { 295 | entry, err = strconv.ParseInt(lineAndEntry[1], 0, 64) 296 | if err != nil { 297 | return "", 0, 0, fmt.Errorf("error parsing entry offset: %s", lineAndEntry[1]) 298 | } 299 | } 300 | return 301 | } 302 | 303 | type StackCompFunc func(a, b *Stack) bool 304 | type StackSorter struct { 305 | Stacks []*Stack 306 | CompFunc StackCompFunc 307 | } 308 | 309 | func (ss StackSorter) Len() int { 310 | return len(ss.Stacks) 311 | } 312 | 313 | func (ss StackSorter) Less(i, j int) bool { 314 | return ss.CompFunc(ss.Stacks[i], ss.Stacks[j]) 315 | } 316 | 317 | func (ss StackSorter) Swap(i, j int) { 318 | ss.Stacks[i], ss.Stacks[j] = ss.Stacks[j], ss.Stacks[i] 319 | } 320 | 321 | func CompWaitTime(a, b *Stack) bool { 322 | return a.WaitTime < b.WaitTime 323 | } 324 | 325 | func CompDepth(a, b *Stack) bool { 326 | return len(a.Frames) < len(b.Frames) 327 | } 328 | 329 | func CompGoroNum(a, b *Stack) bool { 330 | return a.Number < b.Number 331 | } 332 | -------------------------------------------------------------------------------- /util/stack_test.go: -------------------------------------------------------------------------------- 1 | package stacks 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestParseStacks(t *testing.T) { 12 | cases := []struct { 13 | name string 14 | input string 15 | linePrefix string 16 | 17 | expected []*Stack 18 | }{ 19 | { 20 | name: "single goroutine", 21 | input: ` 22 | goroutine 85751948 [semacquire, 25 minutes]: 23 | sync.runtime_Semacquire(0xc099422a74) 24 | /usr/local/go/src/runtime/sema.go:56 +0x45 25 | sync.(*WaitGroup).Wait(0xc099422a74) 26 | /usr/local/go/src/sync/waitgroup.go:130 +0x65 27 | github.com/libp2p/go-libp2p-swarm.(*Swarm).notifyAll(0xc000783380, 0xc01a77d0c0) 28 | pkg/mod/github.com/libp2p/go-libp2p-swarm@v0.5.3/swarm.go:553 +0x13e 29 | github.com/libp2p/go-libp2p-swarm.(*Conn).doClose.func1(0xc06f36f4d0) 30 | pkg/mod/github.com/libp2p/go-libp2p-swarm@v0.5.3/swarm_conn.go:84 +0xa7 31 | created by github.com/libp2p/go-libp2p-swarm.(*Conn).doClose 32 | pkg/mod/github.com/libp2p/go-libp2p-swarm@v0.5.3/swarm_conn.go:79 +0x16a 33 | 34 | `, 35 | expected: []*Stack{ 36 | { 37 | Number: 85751948, 38 | State: "semacquire", 39 | WaitTime: 25 * time.Minute, 40 | Frames: []Frame{ 41 | { 42 | Function: "sync.runtime_Semacquire", 43 | Params: []string{"0xc099422a74"}, 44 | File: "/usr/local/go/src/runtime/sema.go", 45 | Line: 56, 46 | Entry: 69, 47 | }, 48 | { 49 | Function: "sync.(*WaitGroup).Wait", 50 | Params: []string{"0xc099422a74"}, 51 | File: "/usr/local/go/src/sync/waitgroup.go", 52 | Line: 130, 53 | Entry: 101, 54 | }, 55 | { 56 | Function: "github.com/libp2p/go-libp2p-swarm.(*Swarm).notifyAll", 57 | Params: []string{ 58 | "0xc000783380", 59 | "0xc01a77d0c0", 60 | }, 61 | File: "pkg/mod/github.com/libp2p/go-libp2p-swarm@v0.5.3/swarm.go", 62 | Line: 553, 63 | Entry: 318, 64 | }, 65 | { 66 | Function: "github.com/libp2p/go-libp2p-swarm.(*Conn).doClose.func1", 67 | Params: []string{"0xc06f36f4d0"}, 68 | File: "pkg/mod/github.com/libp2p/go-libp2p-swarm@v0.5.3/swarm_conn.go", 69 | Line: 84, 70 | Entry: 167, 71 | }, 72 | }, 73 | ThreadLocked: false, 74 | CreatedBy: CreatedBy{ 75 | Function: "github.com/libp2p/go-libp2p-swarm.(*Conn).doClose", 76 | File: "pkg/mod/github.com/libp2p/go-libp2p-swarm@v0.5.3/swarm_conn.go", 77 | Line: 79, 78 | Entry: 362, 79 | }, 80 | }, 81 | }, 82 | }, 83 | } 84 | 85 | for _, c := range cases { 86 | t.Run(c.name, func(t *testing.T) { 87 | reader := strings.NewReader(c.input) 88 | stacks, err := ParseStacks(reader, c.linePrefix) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | if !reflect.DeepEqual(c.expected, stacks) { 94 | expectedJSON, err := json.MarshalIndent(c.expected, "", " ") 95 | if err != nil { 96 | panic(err) 97 | } 98 | gotJSON, err := json.MarshalIndent(stacks, "", " ") 99 | if err != nil { 100 | panic(err) 101 | } 102 | t.Fatalf("expected:\n%v\ngot:\n%v", string(expectedJSON), string(gotJSON)) 103 | } 104 | }) 105 | } 106 | 107 | } 108 | --------------------------------------------------------------------------------