| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package stack |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "runtime" |
| "sort" |
| ) |
| |
| // Capture get the current stack traces from the runtime. |
| func Capture() Dump { |
| buf := make([]byte, 2<<20) |
| buf = buf[:runtime.Stack(buf, true)] |
| scanner := NewScanner(bytes.NewReader(buf)) |
| dump, _ := Parse(scanner) |
| return dump |
| } |
| |
| // Summarize a dump for easier consumption. |
| // This collates goroutines with equivalent stacks. |
| func Summarize(dump Dump) Summary { |
| s := Summary{ |
| Total: len(dump), |
| } |
| for _, gr := range dump { |
| s.addGoroutine(gr) |
| } |
| return s |
| } |
| |
| // Process and input stream to an output stream, summarizing any stacks that |
| // are detected in place. |
| func Process(out io.Writer, in io.Reader) error { |
| scanner := NewScanner(in) |
| for { |
| dump, err := Parse(scanner) |
| summary := Summarize(dump) |
| switch { |
| case len(dump) > 0: |
| fmt.Fprintf(out, "%+v\n\n", summary) |
| case err != nil: |
| return err |
| case scanner.Done(): |
| return scanner.Err() |
| default: |
| // must have been a line that is not part of a dump |
| fmt.Fprintln(out, scanner.Next()) |
| } |
| } |
| } |
| |
| // Diff calculates the delta between two dumps. |
| func Diff(before, after Dump) Delta { |
| result := Delta{} |
| processed := make(map[int]bool) |
| for _, gr := range before { |
| processed[gr.ID] = false |
| } |
| for _, gr := range after { |
| if _, found := processed[gr.ID]; found { |
| result.Shared = append(result.Shared, gr) |
| } else { |
| result.After = append(result.After, gr) |
| } |
| processed[gr.ID] = true |
| } |
| for _, gr := range before { |
| if done := processed[gr.ID]; !done { |
| result.Before = append(result.Before, gr) |
| } |
| } |
| return result |
| } |
| |
| // TODO: do we want to allow contraction of stacks before comparison? |
| func (s *Summary) addGoroutine(gr Goroutine) { |
| index := sort.Search(len(s.Calls), func(i int) bool { |
| return !s.Calls[i].Stack.less(gr.Stack) |
| }) |
| if index >= len(s.Calls) || !s.Calls[index].Stack.equal(gr.Stack) { |
| // insert new stack, first increase the length |
| s.Calls = append(s.Calls, Call{}) |
| // move the top part upward to make space |
| copy(s.Calls[index+1:], s.Calls[index:]) |
| // insert the new call |
| s.Calls[index] = Call{ |
| Stack: gr.Stack, |
| } |
| } |
| // merge the goroutine into the matched call |
| s.Calls[index].merge(gr) |
| } |
| |
| // TODO: do we want other grouping strategies? |
| func (c *Call) merge(gr Goroutine) { |
| for i := range c.Groups { |
| canditate := &c.Groups[i] |
| if canditate.State == gr.State { |
| canditate.Goroutines = append(canditate.Goroutines, gr) |
| return |
| } |
| } |
| c.Groups = append(c.Groups, Group{ |
| State: gr.State, |
| Goroutines: []Goroutine{gr}, |
| }) |
| } |