blob: b71fecfe8d0803a20140832b84cbd2f9a52b1c32 [file] [log] [blame]
// Copyright 2015 The Vanadium 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 goroutines
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"runtime"
"strconv"
"strings"
"time"
)
var goroutineHeaderRE = regexp.MustCompile(`^goroutine (\d+) \[([^\]]+)\]:$`)
var stackFileRE = regexp.MustCompile(`^\s+([^:]+):(\d+)(?: \+0x([0-9A-Fa-f]+))?$`)
var ignoredGoroutines = []string{
"runtime.ensureSigM",
"sync.(*WaitGroup).Done",
"security.newOpenSSLSigner.func",
"security.unmarshalPublicKeyImpl.func",
"net/http.(*Transport).dialConn",
"net/http.(*Transport).getConn.func2",
}
type Goroutine struct {
ID int
State string
Stack []*Frame
Creator *Frame
}
// Get gets a set of currently running goroutines and parses them into a
// structured representation.
func Get() ([]*Goroutine, error) {
bufsize, read := 1<<20, 0
buf := make([]byte, bufsize)
for {
read = runtime.Stack(buf, true)
if read < bufsize {
buf = buf[:read]
break
}
bufsize *= 2
buf = make([]byte, bufsize)
}
return Parse(buf, true)
}
// Parse parses a stack trace into a structure representation.
func Parse(buf []byte, ignore bool) ([]*Goroutine, error) {
scanner := bufio.NewScanner(bytes.NewReader(buf))
var out []*Goroutine
for scanner.Scan() {
if len(scanner.Bytes()) == 0 {
continue
}
g, err := parseGoroutine(scanner)
if err != nil {
return out, fmt.Errorf("Error %v parsing trace:\n%s", err, string(buf))
}
if !ignore || !shouldIgnore(g) {
out = append(out, g)
}
}
return out, scanner.Err()
}
func shouldIgnore(g *Goroutine) bool {
for _, ignored := range ignoredGoroutines {
if c := g.Creator; c != nil && strings.Contains(c.Call, ignored) {
return true
}
for _, f := range g.Stack {
if strings.Contains(f.Call, ignored) {
return true
}
}
}
return false
}
func parseGoroutine(scanner *bufio.Scanner) (*Goroutine, error) {
g := &Goroutine{}
matches := goroutineHeaderRE.FindSubmatch(scanner.Bytes())
if len(matches) != 3 {
return nil, fmt.Errorf("Could not parse goroutine header from: %s", scanner.Text())
}
id, err := strconv.ParseInt(string(matches[1]), 10, 64)
if err != nil {
return nil, err
}
g.ID = int(id)
g.State = string(matches[2])
for scanner.Scan() {
if len(scanner.Bytes()) == 0 {
break
}
frame, err := parseFrame(scanner)
if err != nil {
return nil, err
}
if strings.HasPrefix(frame.Call, "created by ") {
frame.Call = frame.Call[len("created by "):]
g.Creator = frame
break
}
g.Stack = append(g.Stack, frame)
}
return g, nil
}
func (g *Goroutine) writeTo(w io.Writer) {
fmt.Fprintf(w, "goroutine %d [%s]:\n", g.ID, g.State)
for _, f := range g.Stack {
f.writeTo(w)
}
if g.Creator != nil {
fmt.Fprint(w, "created by ")
g.Creator.writeTo(w)
}
}
// Frame represents a single stack frame.
type Frame struct {
Call string
File string
Line int
Offset int
}
func parseFrame(scanner *bufio.Scanner) (*Frame, error) {
f := &Frame{Call: scanner.Text()}
if !scanner.Scan() {
return nil, fmt.Errorf("Frame lacked a second line %s", f.Call)
}
matches := stackFileRE.FindSubmatch(scanner.Bytes())
if len(matches) < 4 {
return nil, fmt.Errorf("Could not parse file reference from %s", scanner.Text())
}
f.File = string(matches[1])
line, err := strconv.ParseInt(string(matches[2]), 10, 64)
if err != nil {
return nil, err
}
f.Line = int(line)
if len(matches[3]) > 0 {
offset, err := strconv.ParseInt(string(matches[3]), 16, 64)
if err != nil {
return nil, err
}
f.Offset = int(offset)
}
return f, nil
}
func (f *Frame) writeTo(w io.Writer) {
fmt.Fprintln(w, f.Call)
if f.Offset != 0 {
fmt.Fprintf(w, "\t%s:%d +0x%x\n", f.File, f.Line, f.Offset)
} else {
fmt.Fprintf(w, "\t%s:%d\n", f.File, f.Line)
}
}
// Format formats Goroutines back into the normal string representation.
func Format(gs ...*Goroutine) []byte {
var buf bytes.Buffer
for i, g := range gs {
if i != 0 {
buf.WriteRune('\n')
}
g.writeTo(&buf)
}
return buf.Bytes()
}
// ErrorReporter is used by NoLeaks to report errors. testing.T implements
// this interface and is normally passed.
type ErrorReporter interface {
Errorf(format string, args ...interface{})
}
// NoLeaks helps test that a test isn't leaving extra goroutines after it finishes.
//
// The normal way to use it is:
// func TestFoo(t *testing.T) {
// defer goroutines.NoLeaks(t, time.Second)()
//
// ... Normal test code here ...
//
// }
//
// The test will fail if there are goroutines running at the end of the test
// that weren't running at the beginning.
// Since testing for goroutines being finished can be racy, the detector
// can wait the specified duration for the set of goroutines to return to the
// initial set.
func NoLeaks(t ErrorReporter, wait time.Duration) func() {
gs, err := Get()
if err != nil {
return func() {} // If we can't parse correctly we let the test pass.
}
bycreator := map[string]int{}
for _, g := range gs {
key := ""
if g.Creator != nil {
key = g.Creator.Call
}
bycreator[key]++
}
return func() {
var left []*Goroutine
backoff := 10 * time.Millisecond
start := time.Now()
until := start.Add(wait)
for {
cgs, err := Get()
if err != nil {
return // If we can't parse correctly we let the test pass.
}
left = left[:0]
cbycreator := map[string]int{}
for _, g := range cgs {
key := ""
if g.Creator != nil {
key = g.Creator.Call
}
cbycreator[key]++
if cbycreator[key] > bycreator[key] {
left = append(left, g)
}
}
if len(left) == 0 {
return
}
if time.Now().After(until) {
t.Errorf("%d extra Goroutines outstanding:\n %s", len(left),
string(Format(left...)))
return
}
time.Sleep(backoff)
if backoff = backoff * 2; backoff > time.Second {
backoff = time.Second
}
}
}
}