...

Source file src/github.com/onsi/gomega/gleak/goroutine/goroutine.go

Documentation: github.com/onsi/gomega/gleak/goroutine

     1  package goroutine
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"strconv"
     9  	"strings"
    10  )
    11  
    12  // Goroutine represents information about a single goroutine, such as its unique
    13  // ID, state, backtrace, creator, and more.
    14  //
    15  // Go's runtime assigns unique IDs to goroutines, also called "goid" in Go
    16  // runtime parlance. These IDs start with 1 for the main goroutine and only
    17  // increase (unless you manage to create 2**64-2 goroutines during the lifetime
    18  // of your tests so that the goids wrap around). Due to runtime-internal
    19  // optimizations, not all IDs might be used, so that there might be gaps. But
    20  // IDs are never reused, so they're fine as unique goroutine identities.
    21  //
    22  // The size of a goidsis always 64bits, even on 32bit architectures (if you
    23  // like, you might want to double-check for yourself in runtime/runtime2.go and
    24  // runtime/proc.go).
    25  //
    26  // A Goroutine's State field starts with one of the following strings:
    27  //   - "idle"
    28  //   - "runnable"
    29  //   - "running"
    30  //   - "syscall"
    31  //   - ("waiting" ... see below)
    32  //   - ("dead" ... these goroutines should never appear in dumps)
    33  //   - "copystack"
    34  //   - "preempted"
    35  //   - ("???" ... something IS severely broken.)
    36  //
    37  // In case a goroutine is in waiting state, the State field instead starts with
    38  // one of the following strings, never showing a lonely "waiting" string, but
    39  // rather one of the reasons for waiting:
    40  //   - "chan receive"
    41  //   - "chan send"
    42  //   - "select"
    43  //   - "sleep"
    44  //   - "finalizer wait"
    45  //   - ...quite some more waiting states.
    46  //
    47  // The State description may next contain "(scan)", separated by a single blank
    48  // from the preceding goroutine state text.
    49  //
    50  // If a goroutine is blocked from more than at least a minute, then the state
    51  // description next contains the string "X minutes", where X is the number of
    52  // minutes blocked. This text is separated by a "," and a blank from the
    53  // preceding information.
    54  //
    55  // Finally, OS thread-locked goroutines finally contain "locked to thread" in
    56  // their State description, again separated by a "," and a blank from the
    57  // preceding information.
    58  //
    59  // Please note that the State field never contains the opening and closing
    60  // square brackets as used in plain stack dumps.
    61  type Goroutine struct {
    62  	ID              uint64 // unique goroutine ID ("goid" in Go's runtime parlance)
    63  	State           string // goroutine state, such as "running"
    64  	TopFunction     string // topmost function on goroutine's stack
    65  	CreatorFunction string // name of function creating this goroutine, if any
    66  	BornAt          string // location where the goroutine was started from, if any; format "file-path:line-number"
    67  	Backtrace       string // goroutine's backtrace (of the stack)
    68  }
    69  
    70  // String returns a short textual description of this goroutine, but without the
    71  // potentially lengthy and ugly backtrace details.
    72  func (g Goroutine) String() string {
    73  	s := fmt.Sprintf("Goroutine ID: %d, state: %s, top function: %s",
    74  		g.ID, g.State, g.TopFunction)
    75  	if g.CreatorFunction == "" {
    76  		return s
    77  	}
    78  	s += fmt.Sprintf(", created by: %s, at: %s",
    79  		g.CreatorFunction, g.BornAt)
    80  	return s
    81  }
    82  
    83  // GomegaString returns the Gomega struct representation of a Goroutine, but
    84  // without a potentially rather lengthy backtrace. This Gomega object value
    85  // dumps getting happily truncated as to become more or less useless.
    86  func (g Goroutine) GomegaString() string {
    87  	return fmt.Sprintf(
    88  		"{ID: %d, State: %q, TopFunction: %q, CreatorFunction: %q, BornAt: %q}",
    89  		g.ID, g.State, g.TopFunction, g.CreatorFunction, g.BornAt)
    90  }
    91  
    92  // Goroutines returns information about all goroutines.
    93  func Goroutines() []Goroutine {
    94  	return goroutines(true)
    95  }
    96  
    97  // Current returns information about the current goroutine in which it is
    98  // called. Please note that the topmost function name will always be
    99  // runtime.Stack.
   100  func Current() Goroutine {
   101  	return goroutines(false)[0]
   102  }
   103  
   104  // goroutines is an internal wrapper around dumping either only the stack of the
   105  // current goroutine of the caller or dumping the stacks of all goroutines, and
   106  // then parsing the dump into separate Goroutine descriptions.
   107  func goroutines(all bool) []Goroutine {
   108  	return parseStack(stacks(all))
   109  }
   110  
   111  // parseStack parses the stack dump of one or multiple goroutines, as returned
   112  // by runtime.Stack() and then returns a list of Goroutine descriptions based on
   113  // the dump.
   114  func parseStack(stacks []byte) []Goroutine {
   115  	gs := []Goroutine{}
   116  	r := bufio.NewReader(bytes.NewReader(stacks))
   117  	for {
   118  		// We expect a line describing a new "goroutine", everything else is a
   119  		// failure. And yes, if we get an EOF already with this line, bail out.
   120  		line, err := r.ReadString('\n')
   121  		if err == io.EOF {
   122  			break
   123  		}
   124  		g := new(line)
   125  		// Read the rest ... that is, the backtrace for this goroutine.
   126  		g.TopFunction, g.Backtrace = parseGoroutineBacktrace(r)
   127  		if strings.HasSuffix(g.Backtrace, "\n\n") {
   128  			g.Backtrace = g.Backtrace[:len(g.Backtrace)-1]
   129  		}
   130  		g.CreatorFunction, g.BornAt = findCreator(g.Backtrace)
   131  		gs = append(gs, g)
   132  	}
   133  	return gs
   134  }
   135  
   136  // new takes a goroutine line from a stack dump and returns a Goroutine object
   137  // based on the information contained in the dump.
   138  func new(s string) Goroutine {
   139  	s = strings.TrimSuffix(s, ":\n")
   140  	fields := strings.SplitN(s, " ", 3)
   141  	if len(fields) != 3 {
   142  		panic(fmt.Sprintf("invalid stack header: %q", s))
   143  	}
   144  	id, err := strconv.ParseUint(fields[1], 10, 64)
   145  	if err != nil {
   146  		panic(fmt.Sprintf("invalid stack header ID: %q, header: %q", fields[1], s))
   147  	}
   148  	state := strings.TrimSuffix(strings.TrimPrefix(fields[2], "["), "]")
   149  	return Goroutine{ID: id, State: state}
   150  }
   151  
   152  // Beginning of line indicating the creator of a Goroutine, if any. This
   153  // indication is missing for the main goroutine as it appeared in a big bang or
   154  // something similar.
   155  const backtraceGoroutineCreator = "created by "
   156  
   157  // findCreator solves the great mystery of Gokind, answering the question of who
   158  // created this goroutine? Given a backtrace, that is.
   159  func findCreator(backtrace string) (creator, location string) {
   160  	pos := strings.LastIndex(backtrace, backtraceGoroutineCreator)
   161  	if pos < 0 {
   162  		return
   163  	}
   164  	// Split the "created by ..." line from the following line giving us the
   165  	// (indented) file name:line number and the hex offset of the call location
   166  	// within the function.
   167  	details := strings.SplitN(backtrace[pos+len(backtraceGoroutineCreator):], "\n", 3)
   168  	if len(details) < 2 {
   169  		return
   170  	}
   171  	// Split off the call location hex offset which is of no use to us, and only
   172  	// keep the file path and line number information. This will be useful for
   173  	// diagnosis, when dumping leaked goroutines.
   174  	offsetpos := strings.LastIndex(details[1], " +0x")
   175  	if offsetpos < 0 {
   176  		return
   177  	}
   178  	location = strings.TrimSpace(details[1][:offsetpos])
   179  	creator = details[0]
   180  	if offsetpos := strings.LastIndex(creator, " in goroutine "); offsetpos >= 0 {
   181  		creator = creator[:offsetpos]
   182  	}
   183  	return
   184  }
   185  
   186  // Beginning of header line introducing a (new) goroutine in a backtrace.
   187  const backtraceGoroutineHeader = "goroutine "
   188  
   189  // Length of the header line prefix introducing a (new) goroutine in a
   190  // backtrace.
   191  const backtraceGoroutineHeaderLen = len(backtraceGoroutineHeader)
   192  
   193  // parseGoroutineBacktrace reads from reader r the backtrace information until
   194  // the end or until the next goroutine header is seen. This next goroutine
   195  // header is NOT consumed so that callers can still read the next header from
   196  // the reader.
   197  func parseGoroutineBacktrace(r *bufio.Reader) (topFn string, backtrace string) {
   198  	bt := bytes.Buffer{}
   199  	// Read backtrace information belonging to this goroutine until we meet
   200  	// another goroutine header.
   201  	for {
   202  		header, err := r.Peek(backtraceGoroutineHeaderLen)
   203  		if string(header) == backtraceGoroutineHeader {
   204  			// next goroutine header is up for read, so we're done with parsing
   205  			// the backtrace of this goroutine.
   206  			break
   207  		}
   208  		if err != nil && err != io.EOF {
   209  			// There is some serious problem with the stack dump, so we
   210  			// decidedly panic now.
   211  			panic("parsing backtrace failed: " + err.Error())
   212  		}
   213  		line, err := r.ReadString('\n')
   214  		if err != nil && err != io.EOF {
   215  			// There is some serious problem with the stack dump, so we
   216  			// decidedly panic now.
   217  			panic("parsing backtrace failed: " + err.Error())
   218  		}
   219  		// The first line after a goroutine header lists the "topmost" function.
   220  		if topFn == "" {
   221  			line := /*sic!*/ strings.TrimSpace(line)
   222  			idx := strings.LastIndex(line, "(")
   223  			if idx <= 0 {
   224  				panic(fmt.Sprintf("invalid function call stack entry: %q", line))
   225  			}
   226  			topFn = line[:idx]
   227  		}
   228  		// Always append the line read to the goroutine's backtrace.
   229  		bt.WriteString(line)
   230  		if err == io.EOF {
   231  			// we're reached the end of the stack dump, so that's it.
   232  			break
   233  		}
   234  	}
   235  	return topFn, bt.String()
   236  }
   237  

View as plain text