mirror of https://github.com/miguelmota/cointop
go mod tidy
parent
9a906c3a68
commit
55ab27095d
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015 Marc-Antoine Ruel
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,106 +0,0 @@
|
||||
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package stack
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Similarity is the level at which two call lines arguments must match to be
|
||||
// considered similar enough to coalesce them.
|
||||
type Similarity int
|
||||
|
||||
const (
|
||||
// ExactFlags requires same bits (e.g. Locked).
|
||||
ExactFlags Similarity = iota
|
||||
// ExactLines requests the exact same arguments on the call line.
|
||||
ExactLines
|
||||
// AnyPointer considers different pointers a similar call line.
|
||||
AnyPointer
|
||||
// AnyValue accepts any value as similar call line.
|
||||
AnyValue
|
||||
)
|
||||
|
||||
// Aggregate merges similar goroutines into buckets.
|
||||
//
|
||||
// The buckets are ordered in library provided order of relevancy. You can
|
||||
// reorder at your choosing.
|
||||
func Aggregate(goroutines []*Goroutine, similar Similarity) []*Bucket {
|
||||
type count struct {
|
||||
ids []int
|
||||
first bool
|
||||
}
|
||||
b := map[*Signature]*count{}
|
||||
// O(n²). Fix eventually.
|
||||
for _, routine := range goroutines {
|
||||
found := false
|
||||
for key, c := range b {
|
||||
// When a match is found, this effectively drops the other goroutine ID.
|
||||
if key.similar(&routine.Signature, similar) {
|
||||
found = true
|
||||
c.ids = append(c.ids, routine.ID)
|
||||
c.first = c.first || routine.First
|
||||
if !key.equal(&routine.Signature) {
|
||||
// Almost but not quite equal. There's different pointers passed
|
||||
// around but the same values. Zap out the different values.
|
||||
newKey := key.merge(&routine.Signature)
|
||||
b[newKey] = c
|
||||
delete(b, key)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
// Create a copy of the Signature, since it will be mutated.
|
||||
key := &Signature{}
|
||||
*key = routine.Signature
|
||||
b[key] = &count{ids: []int{routine.ID}, first: routine.First}
|
||||
}
|
||||
}
|
||||
out := make(buckets, 0, len(b))
|
||||
for signature, c := range b {
|
||||
sort.Ints(c.ids)
|
||||
out = append(out, &Bucket{Signature: *signature, IDs: c.ids, First: c.first})
|
||||
}
|
||||
sort.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Bucket is a stack trace signature and the list of goroutines that fits this
|
||||
// signature.
|
||||
type Bucket struct {
|
||||
// Signature is the generalized signature for this bucket.
|
||||
Signature
|
||||
// IDs is the ID of each Goroutine with this Signature.
|
||||
IDs []int
|
||||
// First is true if this Bucket contains the first goroutine, e.g. the one
|
||||
// Signature that likely generated the panic() call, if any.
|
||||
First bool
|
||||
}
|
||||
|
||||
// less does reverse sort.
|
||||
func (b *Bucket) less(r *Bucket) bool {
|
||||
if b.First || r.First {
|
||||
return b.First
|
||||
}
|
||||
return b.Signature.less(&r.Signature)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// buckets is a list of Bucket sorted by repeation count.
|
||||
type buckets []*Bucket
|
||||
|
||||
func (b buckets) Len() int {
|
||||
return len(b)
|
||||
}
|
||||
|
||||
func (b buckets) Less(i, j int) bool {
|
||||
return b[i].less(b[j])
|
||||
}
|
||||
|
||||
func (b buckets) Swap(i, j int) {
|
||||
b[j], b[i] = b[i], b[j]
|
||||
}
|
@ -1,935 +0,0 @@
|
||||
// Copyright 2018 Marc-Antoine Ruel. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
//go:generate go get golang.org/x/tools/cmd/stringer
|
||||
//go:generate stringer -type state
|
||||
|
||||
package stack
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Context is a parsing context.
|
||||
//
|
||||
// It contains the deduced GOROOT and GOPATH, if guesspaths is true.
|
||||
type Context struct {
|
||||
// Goroutines is the Goroutines found.
|
||||
//
|
||||
// They are in the order that they were printed.
|
||||
Goroutines []*Goroutine
|
||||
|
||||
// GOROOT is the GOROOT as detected in the traceback, not the on the host.
|
||||
//
|
||||
// It can be empty if no root was determined, for example the traceback
|
||||
// contains only non-stdlib source references.
|
||||
//
|
||||
// Empty is guesspaths was false.
|
||||
GOROOT string
|
||||
// GOPATHs is the GOPATH as detected in the traceback, with the value being
|
||||
// the corresponding path mapped to the host.
|
||||
//
|
||||
// It can be empty if only stdlib code is in the traceback or if no local
|
||||
// sources were matched up. In the general case there is only one entry in
|
||||
// the map.
|
||||
//
|
||||
// Nil is guesspaths was false.
|
||||
GOPATHs map[string]string
|
||||
|
||||
// localGomoduleRoot is the root directory containing go.mod. It is
|
||||
// considered to be the primary project containing the main executable. It is
|
||||
// initialized by findRoots().
|
||||
//
|
||||
// It only works with stack traces created in the local file system.
|
||||
localGomoduleRoot string
|
||||
// gomodImportPath is set to the relative import path that localGomoduleRoot
|
||||
// represents.
|
||||
gomodImportPath string
|
||||
|
||||
// localgoroot is GOROOT with "/" as path separator. No trailing "/".
|
||||
localgoroot string
|
||||
// localgopaths is GOPATH with "/" as path separator. No trailing "/".
|
||||
localgopaths []string
|
||||
}
|
||||
|
||||
// ParseDump processes the output from runtime.Stack().
|
||||
//
|
||||
// Returns nil *Context if no stack trace was detected.
|
||||
//
|
||||
// It pipes anything not detected as a panic stack trace from r into out. It
|
||||
// assumes there is junk before the actual stack trace. The junk is streamed to
|
||||
// out.
|
||||
//
|
||||
// If guesspaths is false, no guessing of GOROOT and GOPATH is done, and Call
|
||||
// entites do not have LocalSrcPath and IsStdlib filled in. If true, be warned
|
||||
// that file presence is done, which means some level of disk I/O.
|
||||
func ParseDump(r io.Reader, out io.Writer, guesspaths bool) (*Context, error) {
|
||||
goroutines, err := parseDump(r, out)
|
||||
if len(goroutines) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
c := &Context{
|
||||
Goroutines: goroutines,
|
||||
localgoroot: strings.Replace(runtime.GOROOT(), "\\", "/", -1),
|
||||
localgopaths: getGOPATHs(),
|
||||
}
|
||||
nameArguments(goroutines)
|
||||
// Corresponding local values on the host for Context.
|
||||
if guesspaths {
|
||||
c.findRoots()
|
||||
for _, r := range c.Goroutines {
|
||||
// Note that this is important to call it even if
|
||||
// c.GOROOT == c.localgoroot.
|
||||
r.updateLocations(c.GOROOT, c.localgoroot, c.localGomoduleRoot, c.gomodImportPath, c.GOPATHs)
|
||||
}
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// Private stuff.
|
||||
|
||||
func parseDump(r io.Reader, out io.Writer) ([]*Goroutine, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(scanLines)
|
||||
// Do not enable race detection parsing yet, since it cannot be returned in
|
||||
// Context at the moment.
|
||||
s := scanningState{}
|
||||
for scanner.Scan() {
|
||||
line, err := s.scan(scanner.Text())
|
||||
if line != "" {
|
||||
_, _ = io.WriteString(out, line)
|
||||
}
|
||||
if err != nil {
|
||||
return s.goroutines, err
|
||||
}
|
||||
}
|
||||
return s.goroutines, scanner.Err()
|
||||
}
|
||||
|
||||
// scanLines is similar to bufio.ScanLines except that it:
|
||||
// - doesn't drop '\n'
|
||||
// - doesn't strip '\r'
|
||||
// - returns when the data is bufio.MaxScanTokenSize bytes
|
||||
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||
return i + 1, data[0 : i+1], nil
|
||||
}
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
if len(data) >= bufio.MaxScanTokenSize {
|
||||
// Returns the line even if it is not at EOF nor has a '\n', otherwise the
|
||||
// scanner will return bufio.ErrTooLong which is definitely not what we
|
||||
// want.
|
||||
return len(data), data, nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
const (
|
||||
lockedToThread = "locked to thread"
|
||||
elided = "...additional frames elided..."
|
||||
// gotRaceHeader1, normal
|
||||
raceHeaderFooter = "=================="
|
||||
// gotRaceHeader2
|
||||
raceHeader = "WARNING: DATA RACE"
|
||||
)
|
||||
|
||||
// These are effectively constants.
|
||||
var (
|
||||
// gotRoutineHeader
|
||||
reRoutineHeader = regexp.MustCompile("^([ \t]*)goroutine (\\d+) \\[([^\\]]+)\\]\\:$")
|
||||
reMinutes = regexp.MustCompile(`^(\d+) minutes$`)
|
||||
|
||||
// gotUnavail
|
||||
reUnavail = regexp.MustCompile("^(?:\t| +)goroutine running on other thread; stack unavailable")
|
||||
|
||||
// gotFileFunc, gotRaceOperationFile, gotRaceGoroutineFile
|
||||
// See gentraceback() in src/runtime/traceback.go for more information.
|
||||
// - Sometimes the source file comes up as "<autogenerated>". It is the
|
||||
// compiler than generated these, not the runtime.
|
||||
// - The tab may be replaced with spaces when a user copy-paste it, handle
|
||||
// this transparently.
|
||||
// - "runtime.gopanic" is explicitly replaced with "panic" by gentraceback().
|
||||
// - The +0x123 byte offset is printed when frame.pc > _func.entry. _func is
|
||||
// generated by the linker.
|
||||
// - The +0x123 byte offset is not included with generated code, e.g. unnamed
|
||||
// functions "func·006()" which is generally go func() { ... }()
|
||||
// statements. Since the _func is generated at runtime, it's probably why
|
||||
// _func.entry is not set.
|
||||
// - C calls may have fp=0x123 sp=0x123 appended. I think it normally happens
|
||||
// when a signal is not correctly handled. It is printed with m.throwing>0.
|
||||
// These are discarded.
|
||||
// - For cgo, the source file may be "??".
|
||||
reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\<autogenerated\\>|.+\\.(?:c|go|s))\\:(\\d+)(?:| \\+0x[0-9a-f]+)(?:| fp=0x[0-9a-f]+ sp=0x[0-9a-f]+(?:| pc=0x[0-9a-f]+))$")
|
||||
|
||||
// gotCreated
|
||||
// Sadly, it doesn't note the goroutine number so we could cascade them per
|
||||
// parenthood.
|
||||
reCreated = regexp.MustCompile("^created by (.+)$")
|
||||
|
||||
// gotFunc, gotRaceOperationFunc, gotRaceGoroutineFunc
|
||||
reFunc = regexp.MustCompile(`^(.+)\((.*)\)$`)
|
||||
|
||||
// Race:
|
||||
// See https://github.com/llvm/llvm-project/blob/master/compiler-rt/lib/tsan/rtl/tsan_report.cpp
|
||||
// for the code generating these messages. Please note only the block in
|
||||
// #else // #if !SANITIZER_GO
|
||||
// is used.
|
||||
// TODO(maruel): " [failed to restore the stack]\n\n"
|
||||
// TODO(maruel): "Global var %s of size %zu at %p declared at %s:%zu\n"
|
||||
|
||||
// gotRaceOperationHeader
|
||||
reRaceOperationHeader = regexp.MustCompile(`^(Read|Write) at (0x[0-9a-f]+) by goroutine (\d+):$`)
|
||||
|
||||
// gotRaceOperationHeader
|
||||
reRacePreviousOperationHeader = regexp.MustCompile(`^Previous (read|write) at (0x[0-9a-f]+) by goroutine (\d+):$`)
|
||||
|
||||
// gotRaceGoroutineHeader
|
||||
reRaceGoroutine = regexp.MustCompile(`^Goroutine (\d+) \((running|finished)\) created at:$`)
|
||||
|
||||
// TODO(maruel): Use it.
|
||||
//reRacePreviousOperationMainHeader = regexp.MustCompile("^Previous (read|write) at (0x[0-9a-f]+) by main goroutine:$")
|
||||
)
|
||||
|
||||
// state is the state of the scan to detect and process a stack trace.
|
||||
type state int
|
||||
|
||||
// Initial state is normal. Other states are when a stack trace is detected.
|
||||
const (
|
||||
// Outside a stack trace.
|
||||
// to: gotRoutineHeader, raceHeader1
|
||||
normal state = iota
|
||||
|
||||
// Panic stack trace:
|
||||
|
||||
// Signature: ""
|
||||
// An empty line between goroutines.
|
||||
// from: gotFileCreated, gotFileFunc
|
||||
// to: gotRoutineHeader, normal
|
||||
betweenRoutine
|
||||
// Regexp: reRoutineHeader
|
||||
// Signature: "goroutine 1 [running]:"
|
||||
// Goroutine header was found.
|
||||
// from: normal
|
||||
// to: gotUnavail, gotFunc
|
||||
gotRoutineHeader
|
||||
// Regexp: reFunc
|
||||
// Signature: "main.main()"
|
||||
// Function call line was found.
|
||||
// from: gotRoutineHeader
|
||||
// to: gotFileFunc
|
||||
gotFunc
|
||||
// Regexp: reCreated
|
||||
// Signature: "created by main.glob..func4"
|
||||
// Goroutine creation line was found.
|
||||
// from: gotFileFunc
|
||||
// to: gotFileCreated
|
||||
gotCreated
|
||||
// Regexp: reFile
|
||||
// Signature: "\t/foo/bar/baz.go:116 +0x35"
|
||||
// File header was found.
|
||||
// from: gotFunc
|
||||
// to: gotFunc, gotCreated, betweenRoutine, normal
|
||||
gotFileFunc
|
||||
// Regexp: reFile
|
||||
// Signature: "\t/foo/bar/baz.go:116 +0x35"
|
||||
// File header was found.
|
||||
// from: gotCreated
|
||||
// to: betweenRoutine, normal
|
||||
gotFileCreated
|
||||
// Regexp: reUnavail
|
||||
// Signature: "goroutine running on other thread; stack unavailable"
|
||||
// State when the goroutine stack is instead is reUnavail.
|
||||
// from: gotRoutineHeader
|
||||
// to: betweenRoutine, gotCreated
|
||||
gotUnavail
|
||||
|
||||
// Race detector:
|
||||
|
||||
// Constant: raceHeaderFooter
|
||||
// Signature: "=================="
|
||||
// from: normal
|
||||
// to: normal, gotRaceHeader2
|
||||
gotRaceHeader1
|
||||
// Constant: raceHeader
|
||||
// Signature: "WARNING: DATA RACE"
|
||||
// from: gotRaceHeader1
|
||||
// to: normal, gotRaceOperationHeader
|
||||
gotRaceHeader2
|
||||
// Regexp: reRaceOperationHeader, reRacePreviousOperationHeader
|
||||
// Signature: "Read at 0x00c0000e4030 by goroutine 7:"
|
||||
// A race operation was found.
|
||||
// from: gotRaceHeader2
|
||||
// to: normal, gotRaceOperationFunc
|
||||
gotRaceOperationHeader
|
||||
// Regexp: reFunc
|
||||
// Signature: " main.panicRace.func1()"
|
||||
// Function that caused the race.
|
||||
// from: gotRaceOperationHeader
|
||||
// to: normal, gotRaceOperationFile
|
||||
gotRaceOperationFunc
|
||||
// Regexp: reFile
|
||||
// Signature: "\t/foo/bar/baz.go:116 +0x35"
|
||||
// File header that caused the race.
|
||||
// from: gotRaceOperationFunc
|
||||
// to: normal, betweenRaceOperations, gotRaceOperationFunc
|
||||
gotRaceOperationFile
|
||||
// Signature: ""
|
||||
// Empty line between race operations or just after.
|
||||
// from: gotRaceOperationFile
|
||||
// to: normal, gotRaceOperationHeader, gotRaceGoroutineHeader
|
||||
betweenRaceOperations
|
||||
|
||||
// Regexp: reRaceGoroutine
|
||||
// Signature: "Goroutine 7 (running) created at:"
|
||||
// Goroutine header.
|
||||
// from: betweenRaceOperations, betweenRaceGoroutines
|
||||
// to: normal, gotRaceOperationHeader
|
||||
gotRaceGoroutineHeader
|
||||
// Regexp: reFunc
|
||||
// Signature: " main.panicRace.func1()"
|
||||
// Function that caused the race.
|
||||
// from: gotRaceGoroutineHeader
|
||||
// to: normal, gotRaceGoroutineFile
|
||||
gotRaceGoroutineFunc
|
||||
// Regexp: reFile
|
||||
// Signature: "\t/foo/bar/baz.go:116 +0x35"
|
||||
// File header that caused the race.
|
||||
// from: gotRaceGoroutineFunc
|
||||
// to: normal, betweenRaceGoroutines
|
||||
gotRaceGoroutineFile
|
||||
// Signature: ""
|
||||
// Empty line between race stack traces.
|
||||
// from: gotRaceGoroutineFile
|
||||
// to: normal, gotRaceGoroutineHeader
|
||||
betweenRaceGoroutines
|
||||
)
|
||||
|
||||
// raceOp is one of the detected data race operation as detected by the race
|
||||
// detector.
|
||||
type raceOp struct {
|
||||
write bool
|
||||
addr uint64
|
||||
id int
|
||||
create Stack
|
||||
}
|
||||
|
||||
// scanningState is the state of the scan to detect and process a stack trace
|
||||
// and stores the traces found.
|
||||
type scanningState struct {
|
||||
// Determines if race detection is enabled. Currently false since scan()
|
||||
// would swallow the race detector output, but the data is not part of
|
||||
// Context yet.
|
||||
raceDetectionEnabled bool
|
||||
|
||||
// goroutines contains all the goroutines found.
|
||||
goroutines []*Goroutine
|
||||
|
||||
state state
|
||||
prefix string
|
||||
races map[int]*raceOp
|
||||
goroutineID int
|
||||
}
|
||||
|
||||
// scan scans one line, updates goroutines and move to the next state.
|
||||
//
|
||||
// TODO(maruel): Handle corrupted stack cases:
|
||||
// - missed stack barrier
|
||||
// - found next stack barrier at 0x123; expected
|
||||
// - runtime: unexpected return pc for FUNC_NAME called from 0x123
|
||||
func (s *scanningState) scan(line string) (string, error) {
|
||||
/* This is very useful to debug issues in the state machine.
|
||||
defer func() {
|
||||
log.Printf("scan(%q) -> %s", line, s.state)
|
||||
}()
|
||||
//*/
|
||||
var cur *Goroutine
|
||||
if len(s.goroutines) != 0 {
|
||||
cur = s.goroutines[len(s.goroutines)-1]
|
||||
}
|
||||
trimmed := line
|
||||
if strings.HasSuffix(line, "\r\n") {
|
||||
trimmed = line[:len(line)-2]
|
||||
} else if strings.HasSuffix(line, "\n") {
|
||||
trimmed = line[:len(line)-1]
|
||||
} else {
|
||||
// There's two cases:
|
||||
// - It's the end of the stream and it's not terminating with EOL character.
|
||||
// - The line is longer than bufio.MaxScanTokenSize
|
||||
if s.state == normal {
|
||||
return line, nil
|
||||
}
|
||||
// Let it flow. It's possible the last line was trimmed and we still want to parse it.
|
||||
}
|
||||
|
||||
if trimmed != "" && s.prefix != "" {
|
||||
// This can only be the case if s.state != normal or the line is empty.
|
||||
if !strings.HasPrefix(trimmed, s.prefix) {
|
||||
prefix := s.prefix
|
||||
s.state = normal
|
||||
s.prefix = ""
|
||||
return "", fmt.Errorf("inconsistent indentation: %q, expected %q", trimmed, prefix)
|
||||
}
|
||||
trimmed = trimmed[len(s.prefix):]
|
||||
}
|
||||
|
||||
switch s.state {
|
||||
case normal:
|
||||
// We could look for '^panic:' but this is more risky, there can be a lot
|
||||
// of junk between this and the stack dump.
|
||||
fallthrough
|
||||
|
||||
case betweenRoutine:
|
||||
// Look for a goroutine header.
|
||||
if match := reRoutineHeader.FindStringSubmatch(trimmed); match != nil {
|
||||
if id, err := strconv.Atoi(match[2]); err == nil {
|
||||
// See runtime/traceback.go.
|
||||
// "<state>, \d+ minutes, locked to thread"
|
||||
items := strings.Split(match[3], ", ")
|
||||
sleep := 0
|
||||
locked := false
|
||||
for i := 1; i < len(items); i++ {
|
||||
if items[i] == lockedToThread {
|
||||
locked = true
|
||||
continue
|
||||
}
|
||||
// Look for duration, if any.
|
||||
if match2 := reMinutes.FindStringSubmatch(items[i]); match2 != nil {
|
||||
sleep, _ = strconv.Atoi(match2[1])
|
||||
}
|
||||
}
|
||||
g := &Goroutine{
|
||||
Signature: Signature{
|
||||
State: items[0],
|
||||
SleepMin: sleep,
|
||||
SleepMax: sleep,
|
||||
Locked: locked,
|
||||
},
|
||||
ID: id,
|
||||
First: len(s.goroutines) == 0,
|
||||
}
|
||||
// Increase performance by always allocating 4 goroutines minimally.
|
||||
if s.goroutines == nil {
|
||||
s.goroutines = make([]*Goroutine, 0, 4)
|
||||
}
|
||||
s.goroutines = append(s.goroutines, g)
|
||||
s.state = gotRoutineHeader
|
||||
s.prefix = match[1]
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
// Switch to race detection mode.
|
||||
if s.raceDetectionEnabled && trimmed == raceHeaderFooter {
|
||||
// TODO(maruel): We should buffer it in case the next line is not a
|
||||
// WARNING so we can output it back.
|
||||
s.state = gotRaceHeader1
|
||||
return "", nil
|
||||
}
|
||||
// Fallthrough.
|
||||
s.state = normal
|
||||
s.prefix = ""
|
||||
return line, nil
|
||||
|
||||
case gotRoutineHeader:
|
||||
if reUnavail.MatchString(trimmed) {
|
||||
// Generate a fake stack entry.
|
||||
cur.Stack.Calls = []Call{{SrcPath: "<unavailable>"}}
|
||||
// Next line is expected to be an empty line.
|
||||
s.state = gotUnavail
|
||||
return "", nil
|
||||
}
|
||||
c := Call{}
|
||||
if found, err := parseFunc(&c, trimmed); found {
|
||||
// Increase performance by always allocating 4 calls minimally.
|
||||
if cur.Stack.Calls == nil {
|
||||
cur.Stack.Calls = make([]Call, 0, 4)
|
||||
}
|
||||
cur.Stack.Calls = append(cur.Stack.Calls, c)
|
||||
s.state = gotFunc
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("expected a function after a goroutine header, got: %q", strings.TrimSpace(trimmed))
|
||||
|
||||
case gotFunc:
|
||||
// cur.Stack.Calls is guaranteed to have at least one item.
|
||||
if found, err := parseFile(&cur.Stack.Calls[len(cur.Stack.Calls)-1], trimmed); err != nil {
|
||||
return "", err
|
||||
} else if !found {
|
||||
return "", fmt.Errorf("expected a file after a function, got: %q", strings.TrimSpace(trimmed))
|
||||
}
|
||||
s.state = gotFileFunc
|
||||
return "", nil
|
||||
|
||||
case gotCreated:
|
||||
if found, err := parseFile(&cur.CreatedBy, trimmed); err != nil {
|
||||
return "", err
|
||||
} else if !found {
|
||||
return "", fmt.Errorf("expected a file after a created line, got: %q", trimmed)
|
||||
}
|
||||
s.state = gotFileCreated
|
||||
return "", nil
|
||||
|
||||
case gotFileFunc:
|
||||
if match := reCreated.FindStringSubmatch(trimmed); match != nil {
|
||||
cur.CreatedBy.Func.Raw = match[1]
|
||||
s.state = gotCreated
|
||||
return "", nil
|
||||
}
|
||||
if elided == trimmed {
|
||||
cur.Stack.Elided = true
|
||||
// TODO(maruel): New state.
|
||||
return "", nil
|
||||
}
|
||||
c := Call{}
|
||||
if found, err := parseFunc(&c, trimmed); found {
|
||||
// Increase performance by always allocating 4 calls minimally.
|
||||
if cur.Stack.Calls == nil {
|
||||
cur.Stack.Calls = make([]Call, 0, 4)
|
||||
}
|
||||
cur.Stack.Calls = append(cur.Stack.Calls, c)
|
||||
s.state = gotFunc
|
||||
return "", err
|
||||
}
|
||||
if trimmed == "" {
|
||||
s.state = betweenRoutine
|
||||
return "", nil
|
||||
}
|
||||
// Back to normal state.
|
||||
s.state = normal
|
||||
s.prefix = ""
|
||||
return line, nil
|
||||
|
||||
case gotFileCreated:
|
||||
if trimmed == "" {
|
||||
s.state = betweenRoutine
|
||||
return "", nil
|
||||
}
|
||||
s.state = normal
|
||||
s.prefix = ""
|
||||
return line, nil
|
||||
|
||||
case gotUnavail:
|
||||
if trimmed == "" {
|
||||
s.state = betweenRoutine
|
||||
return "", nil
|
||||
}
|
||||
if match := reCreated.FindStringSubmatch(trimmed); match != nil {
|
||||
cur.CreatedBy.Func.Raw = match[1]
|
||||
s.state = gotCreated
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("expected empty line after unavailable stack, got: %q", strings.TrimSpace(trimmed))
|
||||
|
||||
// Race detector.
|
||||
|
||||
case gotRaceHeader1:
|
||||
if raceHeader == trimmed {
|
||||
// TODO(maruel): We should buffer it in case the next line is not a
|
||||
// WARNING so we can output it back.
|
||||
s.state = gotRaceHeader2
|
||||
return "", nil
|
||||
}
|
||||
s.state = normal
|
||||
return line, nil
|
||||
|
||||
case gotRaceHeader2:
|
||||
if match := reRaceOperationHeader.FindStringSubmatch(trimmed); match != nil {
|
||||
w := match[1] == "Write"
|
||||
addr, err := strconv.ParseUint(match[2], 0, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse address on line: %q", strings.TrimSpace(trimmed))
|
||||
}
|
||||
id, err := strconv.Atoi(match[3])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed))
|
||||
}
|
||||
if s.races != nil {
|
||||
panic("internal failure; expected s.races to be nil")
|
||||
}
|
||||
if s.goroutines != nil {
|
||||
panic("internal failure; expected s.goroutines to be nil")
|
||||
}
|
||||
s.races = make(map[int]*raceOp, 4)
|
||||
s.races[id] = &raceOp{write: w, addr: addr, id: id}
|
||||
s.goroutines = append(make([]*Goroutine, 0, 4), &Goroutine{ID: id, First: true})
|
||||
s.goroutineID = id
|
||||
s.state = gotRaceOperationHeader
|
||||
return "", nil
|
||||
}
|
||||
s.state = normal
|
||||
return line, nil
|
||||
|
||||
case gotRaceOperationHeader:
|
||||
c := Call{}
|
||||
if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); found {
|
||||
// Increase performance by always allocating 4 calls minimally.
|
||||
if cur.Stack.Calls == nil {
|
||||
cur.Stack.Calls = make([]Call, 0, 4)
|
||||
}
|
||||
cur.Stack.Calls = append(cur.Stack.Calls, c)
|
||||
s.state = gotRaceOperationFunc
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("expected a function after a race operation, got: %q", trimmed)
|
||||
|
||||
case gotRaceOperationFunc:
|
||||
if found, err := parseFile(&cur.Stack.Calls[len(cur.Stack.Calls)-1], trimmed); err != nil {
|
||||
return "", err
|
||||
} else if !found {
|
||||
return "", fmt.Errorf("expected a file after a race function, got: %q", trimmed)
|
||||
}
|
||||
s.state = gotRaceOperationFile
|
||||
return "", nil
|
||||
|
||||
case gotRaceOperationFile:
|
||||
if trimmed == "" {
|
||||
s.state = betweenRaceOperations
|
||||
return "", nil
|
||||
}
|
||||
c := Call{}
|
||||
if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); found {
|
||||
cur.Stack.Calls = append(cur.Stack.Calls, c)
|
||||
s.state = gotRaceOperationFunc
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("expected an empty line after a race file, got: %q", trimmed)
|
||||
|
||||
case betweenRaceOperations:
|
||||
// Look for other previous race data operations.
|
||||
if match := reRacePreviousOperationHeader.FindStringSubmatch(trimmed); match != nil {
|
||||
w := match[1] == "write"
|
||||
addr, err := strconv.ParseUint(match[2], 0, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse address on line: %q", strings.TrimSpace(trimmed))
|
||||
}
|
||||
id, err := strconv.Atoi(match[3])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed))
|
||||
}
|
||||
s.goroutineID = id
|
||||
s.races[s.goroutineID] = &raceOp{write: w, addr: addr, id: id}
|
||||
s.goroutines = append(s.goroutines, &Goroutine{ID: id})
|
||||
s.state = gotRaceOperationHeader
|
||||
return "", nil
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case betweenRaceGoroutines:
|
||||
if match := reRaceGoroutine.FindStringSubmatch(trimmed); match != nil {
|
||||
id, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed))
|
||||
}
|
||||
found := false
|
||||
for _, g := range s.goroutines {
|
||||
if g.ID == id {
|
||||
g.State = match[2]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "", fmt.Errorf("unexpected goroutine ID on line: %q", strings.TrimSpace(trimmed))
|
||||
}
|
||||
s.goroutineID = id
|
||||
s.state = gotRaceGoroutineHeader
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("expected an operator or goroutine, got: %q", trimmed)
|
||||
|
||||
// Race stack traces
|
||||
|
||||
case gotRaceGoroutineFunc:
|
||||
c := s.races[s.goroutineID].create.Calls
|
||||
if found, err := parseFile(&c[len(c)-1], trimmed); err != nil {
|
||||
return "", err
|
||||
} else if !found {
|
||||
return "", fmt.Errorf("expected a file after a race function, got: %q", trimmed)
|
||||
}
|
||||
// TODO(maruel): Set s.goroutines[].CreatedBy.
|
||||
s.state = gotRaceGoroutineFile
|
||||
return "", nil
|
||||
|
||||
case gotRaceGoroutineFile:
|
||||
if trimmed == "" {
|
||||
s.state = betweenRaceGoroutines
|
||||
return "", nil
|
||||
}
|
||||
if trimmed == raceHeaderFooter {
|
||||
// Done.
|
||||
s.state = normal
|
||||
return "", nil
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case gotRaceGoroutineHeader:
|
||||
c := Call{}
|
||||
if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); found {
|
||||
// TODO(maruel): Set s.goroutines[].CreatedBy.
|
||||
s.races[s.goroutineID].create.Calls = append(s.races[s.goroutineID].create.Calls, c)
|
||||
s.state = gotRaceGoroutineFunc
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("expected a function after a race operation or a race file, got: %q", trimmed)
|
||||
|
||||
default:
|
||||
return "", errors.New("internal error")
|
||||
}
|
||||
}
|
||||
|
||||
// parseFunc only return an error if also returning a Call.
|
||||
//
|
||||
// Uses reFunc.
|
||||
func parseFunc(c *Call, line string) (bool, error) {
|
||||
if match := reFunc.FindStringSubmatch(line); match != nil {
|
||||
c.Func.Raw = match[1]
|
||||
for _, a := range strings.Split(match[2], ", ") {
|
||||
if a == "..." {
|
||||
c.Args.Elided = true
|
||||
continue
|
||||
}
|
||||
if a == "" {
|
||||
// Remaining values were dropped.
|
||||
break
|
||||
}
|
||||
v, err := strconv.ParseUint(a, 0, 64)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line))
|
||||
}
|
||||
// Increase performance by always allocating 4 values minimally.
|
||||
if c.Args.Values == nil {
|
||||
c.Args.Values = make([]Arg, 0, 4)
|
||||
}
|
||||
c.Args.Values = append(c.Args.Values, Arg{Value: v})
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// parseFile only return an error if also processing a Call.
|
||||
//
|
||||
// Uses reFile.
|
||||
func parseFile(c *Call, line string) (bool, error) {
|
||||
if match := reFile.FindStringSubmatch(line); match != nil {
|
||||
num, err := strconv.Atoi(match[2])
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line))
|
||||
}
|
||||
c.SrcPath = match[1]
|
||||
c.Line = num
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// hasSrcPrefix returns true if any of s is the prefix of p.
|
||||
func hasSrcPrefix(p string, s map[string]string) bool {
|
||||
for prefix := range s {
|
||||
if strings.HasPrefix(p, prefix+"/src/") || strings.HasPrefix(p, prefix+"/pkg/mod/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getFiles returns all the source files deduped and ordered.
|
||||
func getFiles(goroutines []*Goroutine) []string {
|
||||
files := map[string]struct{}{}
|
||||
for _, g := range goroutines {
|
||||
for _, c := range g.Stack.Calls {
|
||||
files[c.SrcPath] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(files))
|
||||
for f := range files {
|
||||
out = append(out, f)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// splitPath splits a path using "/" as separator into its components.
|
||||
//
|
||||
// The first item has its initial path separator kept.
|
||||
func splitPath(p string) []string {
|
||||
if p == "" {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
s := ""
|
||||
for _, c := range p {
|
||||
if c != '/' || (len(out) == 0 && strings.Count(s, "/") == len(s)) {
|
||||
s += string(c)
|
||||
} else if s != "" {
|
||||
out = append(out, s)
|
||||
s = ""
|
||||
}
|
||||
}
|
||||
if s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// isFile returns true if the path is a valid file.
|
||||
func isFile(p string) bool {
|
||||
// TODO(maruel): Is it faster to open the file or to stat it? Worth a perf
|
||||
// test on Windows.
|
||||
i, err := os.Stat(p)
|
||||
return err == nil && !i.IsDir()
|
||||
}
|
||||
|
||||
// rootedIn returns a root if the file split in parts is rooted in root.
|
||||
//
|
||||
// Uses "/" as path separator.
|
||||
func rootedIn(root string, parts []string) string {
|
||||
//log.Printf("rootIn(%s, %v)", root, parts)
|
||||
for i := 1; i < len(parts); i++ {
|
||||
suffix := pathJoin(parts[i:]...)
|
||||
if isFile(pathJoin(root, suffix)) {
|
||||
return pathJoin(parts[:i]...)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// reModule find the module line in a go.mod file. It works even on CRLF file.
|
||||
var reModule = regexp.MustCompile(`(?m)^module\s+([^\n\r]+)\r?$`)
|
||||
|
||||
// isGoModule returns the string to the directory containing a go.mod/go.sum
|
||||
// files pair, and the go import path it represents, if found.
|
||||
func isGoModule(parts []string) (string, string) {
|
||||
for i := len(parts); i > 0; i-- {
|
||||
prefix := pathJoin(parts[:i]...)
|
||||
if isFile(pathJoin(prefix, "go.sum")) {
|
||||
b, err := ioutil.ReadFile(pathJoin(prefix, "go.mod"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if match := reModule.FindSubmatch(b); match != nil {
|
||||
return prefix, string(match[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// findRoots sets member GOROOT, GOPATHs and localGomoduleRoot.
|
||||
//
|
||||
// This causes disk I/O as it checks for file presence.
|
||||
//
|
||||
// Returns the number of missing files.
|
||||
func (c *Context) findRoots() int {
|
||||
c.GOPATHs = map[string]string{}
|
||||
missing := 0
|
||||
for _, f := range getFiles(c.Goroutines) {
|
||||
// TODO(maruel): Could a stack dump have mixed cases? I think it's
|
||||
// possible, need to confirm and handle.
|
||||
//log.Printf(" Analyzing %s", f)
|
||||
|
||||
// First checks skip file I/O.
|
||||
if c.GOROOT != "" && strings.HasPrefix(f, c.GOROOT+"/src/") {
|
||||
// stdlib.
|
||||
continue
|
||||
}
|
||||
if hasSrcPrefix(f, c.GOPATHs) {
|
||||
// $GOPATH/src or go.mod dependency in $GOPATH/pkg/mod.
|
||||
continue
|
||||
}
|
||||
|
||||
// At this point, disk will be looked up.
|
||||
parts := splitPath(f)
|
||||
if c.GOROOT == "" {
|
||||
if r := rootedIn(c.localgoroot+"/src", parts); r != "" {
|
||||
c.GOROOT = r[:len(r)-4]
|
||||
//log.Printf("Found GOROOT=%s", c.GOROOT)
|
||||
continue
|
||||
}
|
||||
}
|
||||
found := false
|
||||
for _, l := range c.localgopaths {
|
||||
if r := rootedIn(l+"/src", parts); r != "" {
|
||||
//log.Printf("Found GOPATH=%s", r[:len(r)-4])
|
||||
c.GOPATHs[r[:len(r)-4]] = l
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if r := rootedIn(l+"/pkg/mod", parts); r != "" {
|
||||
//log.Printf("Found GOPATH=%s", r[:len(r)-8])
|
||||
c.GOPATHs[r[:len(r)-8]] = l
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// If the source is not found, it's probably a go module.
|
||||
if !found {
|
||||
if c.localGomoduleRoot == "" && len(parts) > 1 {
|
||||
// Search upward looking for a go.mod/go.sum pair.
|
||||
c.localGomoduleRoot, c.gomodImportPath = isGoModule(parts[:len(parts)-1])
|
||||
}
|
||||
if c.localGomoduleRoot != "" && strings.HasPrefix(f, c.localGomoduleRoot+"/") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
// If the source is not found, just too bad.
|
||||
//log.Printf("Failed to find locally: %s", f)
|
||||
missing++
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
// getGOPATHs returns parsed GOPATH or its default, using "/" as path separator.
|
||||
func getGOPATHs() []string {
|
||||
var out []string
|
||||
if gp := os.Getenv("GOPATH"); gp != "" {
|
||||
for _, v := range filepath.SplitList(gp) {
|
||||
// Disallow non-absolute paths?
|
||||
if v != "" {
|
||||
v = strings.Replace(v, "\\", "/", -1)
|
||||
// Trim trailing "/".
|
||||
if l := len(v); v[l-1] == '/' {
|
||||
v = v[:l-1]
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
homeDir := ""
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
homeDir = os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
panic(fmt.Sprintf("Could not get current user or $HOME: %s\n", err.Error()))
|
||||
}
|
||||
} else {
|
||||
homeDir = u.HomeDir
|
||||
}
|
||||
out = []string{strings.Replace(homeDir+"/go", "\\", "/", -1)}
|
||||
}
|
||||
return out
|
||||
}
|
@ -1,302 +0,0 @@
|
||||
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// This file contains the code to process sources, to be able to deduct the
|
||||
// original types.
|
||||
|
||||
package stack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// cache is a cache of sources on the file system.
|
||||
type cache struct {
|
||||
files map[string][]byte
|
||||
parsed map[string]*parsedFile
|
||||
}
|
||||
|
||||
// Augment processes source files to improve calls to be more descriptive.
|
||||
//
|
||||
// It modifies goroutines in place. It requires calling ParseDump() with
|
||||
// guesspaths set to true to work properly.
|
||||
func Augment(goroutines []*Goroutine) {
|
||||
c := &cache{}
|
||||
for _, g := range goroutines {
|
||||
c.augmentGoroutine(g)
|
||||
}
|
||||
}
|
||||
|
||||
// augmentGoroutine processes source files to improve call to be more
|
||||
// descriptive.
|
||||
//
|
||||
// It modifies the routine.
|
||||
func (c *cache) augmentGoroutine(goroutine *Goroutine) {
|
||||
if c.files == nil {
|
||||
c.files = map[string][]byte{}
|
||||
}
|
||||
if c.parsed == nil {
|
||||
c.parsed = map[string]*parsedFile{}
|
||||
}
|
||||
// For each call site, look at the next call and populate it. Then we can
|
||||
// walk back and reformat things.
|
||||
for i := range goroutine.Stack.Calls {
|
||||
c.load(goroutine.Stack.Calls[i].LocalSrcPath)
|
||||
}
|
||||
|
||||
// Once all loaded, we can look at the next call when available.
|
||||
for i := 0; i < len(goroutine.Stack.Calls)-1; i++ {
|
||||
// Get the AST from the previous call and process the call line with it.
|
||||
if f := c.getFuncAST(&goroutine.Stack.Calls[i]); f != nil {
|
||||
processCall(&goroutine.Stack.Calls[i], f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private stuff.
|
||||
|
||||
// load loads a source file and parses the AST tree. Failures are ignored.
|
||||
func (c *cache) load(fileName string) {
|
||||
if fileName == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := c.parsed[fileName]; ok {
|
||||
return
|
||||
}
|
||||
c.parsed[fileName] = nil
|
||||
if !strings.HasSuffix(fileName, ".go") {
|
||||
// Ignore C and assembly.
|
||||
c.files[fileName] = nil
|
||||
return
|
||||
}
|
||||
//log.Printf("load(%s)", fileName)
|
||||
if _, ok := c.files[fileName]; !ok {
|
||||
var err error
|
||||
if c.files[fileName], err = ioutil.ReadFile(fileName); err != nil {
|
||||
log.Printf("Failed to read %s: %s", fileName, err)
|
||||
c.files[fileName] = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
src := c.files[fileName]
|
||||
parsed, err := parser.ParseFile(fset, fileName, src, 0)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse %s: %s", fileName, err)
|
||||
return
|
||||
}
|
||||
// Convert the line number into raw file offset.
|
||||
offsets := []int{0, 0}
|
||||
start := 0
|
||||
for l := 1; start < len(src); l++ {
|
||||
start += bytes.IndexByte(src[start:], '\n') + 1
|
||||
offsets = append(offsets, start)
|
||||
}
|
||||
c.parsed[fileName] = &parsedFile{offsets, parsed}
|
||||
}
|
||||
|
||||
func (c *cache) getFuncAST(call *Call) *ast.FuncDecl {
|
||||
if p := c.parsed[call.LocalSrcPath]; p != nil {
|
||||
return p.getFuncAST(call.Func.Name(), call.Line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type parsedFile struct {
|
||||
lineToByteOffset []int
|
||||
parsed *ast.File
|
||||
}
|
||||
|
||||
// getFuncAST gets the callee site function AST representation for the code
|
||||
// inside the function f at line l.
|
||||
func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl) {
|
||||
if len(p.lineToByteOffset) <= l {
|
||||
// The line number in the stack trace line does not exist in the file. That
|
||||
// can only mean that the sources on disk do not match the sources used to
|
||||
// build the binary.
|
||||
// TODO(maruel): This should be surfaced, so that source parsing is
|
||||
// completely ignored.
|
||||
return
|
||||
}
|
||||
|
||||
// Walk the AST to find the lineToByteOffset that fits the line number.
|
||||
var lastFunc *ast.FuncDecl
|
||||
// Inspect() goes depth first. This means for example that a function like:
|
||||
// func a() {
|
||||
// b := func() {}
|
||||
// c()
|
||||
// }
|
||||
//
|
||||
// Were we are looking at the c() call can return confused values. It is
|
||||
// important to look at the actual ast.Node hierarchy.
|
||||
ast.Inspect(p.parsed, func(n ast.Node) bool {
|
||||
if d != nil {
|
||||
return false
|
||||
}
|
||||
if n == nil {
|
||||
return true
|
||||
}
|
||||
if int(n.Pos()) >= p.lineToByteOffset[l] {
|
||||
// We are expecting a ast.CallExpr node. It can be harder to figure out
|
||||
// when there are multiple calls on a single line, as the stack trace
|
||||
// doesn't have file byte offset information, only line based.
|
||||
// gofmt will always format to one function call per line but there can
|
||||
// be edge cases, like:
|
||||
// a = A{Foo(), Bar()}
|
||||
d = lastFunc
|
||||
//p.processNode(call, n)
|
||||
return false
|
||||
} else if f, ok := n.(*ast.FuncDecl); ok {
|
||||
lastFunc = f
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func name(n ast.Node) string {
|
||||
switch t := n.(type) {
|
||||
case *ast.InterfaceType:
|
||||
return "interface{}"
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.SelectorExpr:
|
||||
return t.Sel.Name
|
||||
case *ast.StarExpr:
|
||||
return "*" + name(t.X)
|
||||
default:
|
||||
return "<unknown>"
|
||||
}
|
||||
}
|
||||
|
||||
// fieldToType returns the type name and whether if it's an ellipsis.
|
||||
func fieldToType(f *ast.Field) (string, bool) {
|
||||
switch arg := f.Type.(type) {
|
||||
case *ast.ArrayType:
|
||||
return "[]" + name(arg.Elt), false
|
||||
case *ast.Ellipsis:
|
||||
return name(arg.Elt), true
|
||||
case *ast.FuncType:
|
||||
// Do not print the function signature to not overload the trace.
|
||||
return "func", false
|
||||
case *ast.Ident:
|
||||
return arg.Name, false
|
||||
case *ast.InterfaceType:
|
||||
return "interface{}", false
|
||||
case *ast.SelectorExpr:
|
||||
return arg.Sel.Name, false
|
||||
case *ast.StarExpr:
|
||||
return "*" + name(arg.X), false
|
||||
case *ast.MapType:
|
||||
return fmt.Sprintf("map[%s]%s", name(arg.Key), name(arg.Value)), false
|
||||
case *ast.ChanType:
|
||||
return fmt.Sprintf("chan %s", name(arg.Value)), false
|
||||
default:
|
||||
// TODO(maruel): Implement anything missing.
|
||||
return "<unknown>", false
|
||||
}
|
||||
}
|
||||
|
||||
// extractArgumentsType returns the name of the type of each input argument.
|
||||
func extractArgumentsType(f *ast.FuncDecl) ([]string, bool) {
|
||||
var fields []*ast.Field
|
||||
if f.Recv != nil {
|
||||
if len(f.Recv.List) != 1 {
|
||||
panic("Expect only one receiver; please fix panicparse's code")
|
||||
}
|
||||
// If it is an object receiver (vs a pointer receiver), its address is not
|
||||
// printed in the stack trace so it needs to be ignored.
|
||||
if _, ok := f.Recv.List[0].Type.(*ast.StarExpr); ok {
|
||||
fields = append(fields, f.Recv.List[0])
|
||||
}
|
||||
}
|
||||
var types []string
|
||||
extra := false
|
||||
for _, arg := range append(fields, f.Type.Params.List...) {
|
||||
// Assert that extra is only set on the last item of fields?
|
||||
var t string
|
||||
t, extra = fieldToType(arg)
|
||||
mult := len(arg.Names)
|
||||
if mult == 0 {
|
||||
mult = 1
|
||||
}
|
||||
for i := 0; i < mult; i++ {
|
||||
types = append(types, t)
|
||||
}
|
||||
}
|
||||
return types, extra
|
||||
}
|
||||
|
||||
// processCall walks the function and populate call accordingly.
|
||||
func processCall(call *Call, f *ast.FuncDecl) {
|
||||
values := make([]uint64, len(call.Args.Values))
|
||||
for i := range call.Args.Values {
|
||||
values[i] = call.Args.Values[i].Value
|
||||
}
|
||||
index := 0
|
||||
pop := func() uint64 {
|
||||
if len(values) != 0 {
|
||||
x := values[0]
|
||||
values = values[1:]
|
||||
index++
|
||||
return x
|
||||
}
|
||||
return 0
|
||||
}
|
||||
popName := func() string {
|
||||
n := call.Args.Values[index].Name
|
||||
v := pop()
|
||||
if len(n) == 0 {
|
||||
return fmt.Sprintf("0x%x", v)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
types, extra := extractArgumentsType(f)
|
||||
for i := 0; len(values) != 0; i++ {
|
||||
var t string
|
||||
if i >= len(types) {
|
||||
if !extra {
|
||||
// These are unexpected value! Print them as hex.
|
||||
call.Args.Processed = append(call.Args.Processed, popName())
|
||||
continue
|
||||
}
|
||||
t = types[len(types)-1]
|
||||
} else {
|
||||
t = types[i]
|
||||
}
|
||||
switch t {
|
||||
case "float32":
|
||||
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%g", math.Float32frombits(uint32(pop()))))
|
||||
case "float64":
|
||||
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%g", math.Float64frombits(pop())))
|
||||
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
|
||||
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%d", pop()))
|
||||
case "string":
|
||||
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%s(%s, len=%d)", t, popName(), pop()))
|
||||
default:
|
||||
if strings.HasPrefix(t, "*") {
|
||||
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%s(%s)", t, popName()))
|
||||
} else if strings.HasPrefix(t, "[]") {
|
||||
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%s(%s len=%d cap=%d)", t, popName(), pop(), pop()))
|
||||
} else {
|
||||
// Assumes it's an interface. For now, discard the object value, which
|
||||
// is probably not a good idea.
|
||||
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%s(%s)", t, popName()))
|
||||
pop()
|
||||
}
|
||||
}
|
||||
if len(values) == 0 && call.Args.Elided {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
@ -1,733 +0,0 @@
|
||||
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// Package stack analyzes stack dump of Go processes and simplifies it.
|
||||
//
|
||||
// It is mostly useful on servers will large number of identical goroutines,
|
||||
// making the crash dump harder to read than strictly necessary.
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Func is a function call as read in a goroutine stack trace.
|
||||
//
|
||||
// Go stack traces print a mangled function call, this wrapper unmangle the
|
||||
// string before printing and adds other filtering methods.
|
||||
//
|
||||
// The main caveat is that for calls in package main, the package import URL is
|
||||
// left out.
|
||||
type Func struct {
|
||||
Raw string
|
||||
}
|
||||
|
||||
// String return the fully qualified package import path dot function/method
|
||||
// name.
|
||||
//
|
||||
// It returns the unmangled form of .Raw.
|
||||
func (f *Func) String() string {
|
||||
s, _ := url.QueryUnescape(f.Raw)
|
||||
return s
|
||||
}
|
||||
|
||||
// Name returns the function name.
|
||||
//
|
||||
// Methods are fully qualified, including the struct type.
|
||||
func (f *Func) Name() string {
|
||||
// This works even on Windows as filepath.Base() splits also on "/".
|
||||
// TODO(maruel): This code will fail on a source file with a dot in its name.
|
||||
parts := strings.SplitN(filepath.Base(f.Raw), ".", 2)
|
||||
if len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
// importPath returns the fully qualified package import URL as a guess from
|
||||
// the function signature.
|
||||
//
|
||||
// Not exported because Call.ImportPath() should be called instead, as this
|
||||
// function can't return the import path for package main.
|
||||
func (f *Func) importPath() string {
|
||||
i := strings.LastIndexByte(f.Raw, '/')
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
j := strings.IndexByte(f.Raw[i:], '.')
|
||||
if j == -1 {
|
||||
return ""
|
||||
}
|
||||
s, _ := url.QueryUnescape(f.Raw[:i+j])
|
||||
return s
|
||||
}
|
||||
|
||||
// PkgName returns the guessed package name for this function reference.
|
||||
//
|
||||
// Since the package name can differ from the package import path, the result
|
||||
// is incorrect when there's a mismatch between the directory name containing
|
||||
// the package and the package name.
|
||||
func (f *Func) PkgName() string {
|
||||
parts := strings.SplitN(filepath.Base(f.Raw), ".", 2)
|
||||
if len(parts) == 1 {
|
||||
return ""
|
||||
}
|
||||
s, _ := url.QueryUnescape(parts[0])
|
||||
return s
|
||||
}
|
||||
|
||||
// PkgDotName returns "<package>.<func>" format.
|
||||
//
|
||||
// Since the package name can differ from the package import path, the result
|
||||
// is incorrect when there's a mismatch between the directory name containing
|
||||
// the package and the package name.
|
||||
func (f *Func) PkgDotName() string {
|
||||
parts := strings.SplitN(filepath.Base(f.Raw), ".", 2)
|
||||
s, _ := url.QueryUnescape(parts[0])
|
||||
if len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
if s != "" || parts[1] != "" {
|
||||
return s + "." + parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsExported returns true if the function is exported.
|
||||
func (f *Func) IsExported() bool {
|
||||
name := f.Name()
|
||||
// TODO(maruel): Something like serverHandler.ServeHTTP in package net/host
|
||||
// should not be considered exported. We need something similar to the
|
||||
// decoding done in symbol() in internal/htmlstack.
|
||||
parts := strings.Split(name, ".")
|
||||
r, _ := utf8.DecodeRuneInString(parts[len(parts)-1])
|
||||
if unicode.ToUpper(r) == r {
|
||||
return true
|
||||
}
|
||||
return f.PkgName() == "main" && name == "main"
|
||||
}
|
||||
|
||||
// Arg is an argument on a Call.
|
||||
type Arg struct {
|
||||
Value uint64 // Value is the raw value as found in the stack trace
|
||||
Name string // Name is a pseudo name given to the argument
|
||||
}
|
||||
|
||||
const (
|
||||
// Assumes all values are above 4MiB and positive are pointers; assuming that
|
||||
// above half the memory is kernel memory.
|
||||
//
|
||||
// This is not always true but this should be good enough to help
|
||||
// implementing AnyPointer.
|
||||
pointerFloor = 4 * 1024 * 1024
|
||||
// Assume the stack was generated with the same bitness (32 vs 64) than the
|
||||
// code processing it.
|
||||
pointerCeiling = uint64((^uint(0)) >> 1)
|
||||
)
|
||||
|
||||
// IsPtr returns true if we guess it's a pointer. It's only a guess, it can be
|
||||
// easily be confused by a bitmask.
|
||||
func (a *Arg) IsPtr() bool {
|
||||
return a.Value > pointerFloor && a.Value < pointerCeiling
|
||||
}
|
||||
|
||||
const zeroToNine = "0123456789"
|
||||
|
||||
// String prints the argument as the name if present, otherwise as the value.
|
||||
func (a *Arg) String() string {
|
||||
if a.Name != "" {
|
||||
return a.Name
|
||||
}
|
||||
if a.Value < uint64(len(zeroToNine)) {
|
||||
return zeroToNine[a.Value : a.Value+1]
|
||||
}
|
||||
return fmt.Sprintf("0x%x", a.Value)
|
||||
}
|
||||
|
||||
// similar returns true if the two Arg are equal or almost but not quite equal.
|
||||
func (a *Arg) similar(r *Arg, similar Similarity) bool {
|
||||
switch similar {
|
||||
case ExactFlags, ExactLines:
|
||||
return *a == *r
|
||||
case AnyValue:
|
||||
return true
|
||||
case AnyPointer:
|
||||
if a.IsPtr() != r.IsPtr() {
|
||||
return false
|
||||
}
|
||||
return a.IsPtr() || a.Value == r.Value
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Args is a series of function call arguments.
|
||||
type Args struct {
|
||||
// Values is the arguments as shown on the stack trace. They are mangled via
|
||||
// simplification.
|
||||
Values []Arg
|
||||
// Processed is the arguments generated from processing the source files. It
|
||||
// can have a length lower than Values.
|
||||
Processed []string
|
||||
// Elided when set means there was a trailing ", ...".
|
||||
Elided bool
|
||||
}
|
||||
|
||||
func (a *Args) String() string {
|
||||
var v []string
|
||||
if len(a.Processed) != 0 {
|
||||
v = a.Processed
|
||||
} else {
|
||||
v = make([]string, 0, len(a.Values))
|
||||
for _, item := range a.Values {
|
||||
v = append(v, item.String())
|
||||
}
|
||||
}
|
||||
if a.Elided {
|
||||
v = append(v, "...")
|
||||
}
|
||||
return strings.Join(v, ", ")
|
||||
}
|
||||
|
||||
// equal returns true only if both arguments are exactly equal.
|
||||
func (a *Args) equal(r *Args) bool {
|
||||
if a.Elided != r.Elided || len(a.Values) != len(r.Values) {
|
||||
return false
|
||||
}
|
||||
for i, l := range a.Values {
|
||||
if l != r.Values[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// similar returns true if the two Args are equal or almost but not quite
|
||||
// equal.
|
||||
func (a *Args) similar(r *Args, similar Similarity) bool {
|
||||
if a.Elided != r.Elided || len(a.Values) != len(r.Values) {
|
||||
return false
|
||||
}
|
||||
for i := range a.Values {
|
||||
if !a.Values[i].similar(&r.Values[i], similar) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// merge merges two similar Args, zapping out differences.
|
||||
func (a *Args) merge(r *Args) Args {
|
||||
out := Args{
|
||||
Values: make([]Arg, len(a.Values)),
|
||||
Elided: a.Elided,
|
||||
}
|
||||
for i, l := range a.Values {
|
||||
if l != r.Values[i] {
|
||||
out.Values[i].Name = "*"
|
||||
out.Values[i].Value = l.Value
|
||||
} else {
|
||||
out.Values[i] = l
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Call is an item in the stack trace.
|
||||
type Call struct {
|
||||
// SrcPath is the full path name of the source file as seen in the trace.
|
||||
SrcPath string
|
||||
// LocalSrcPath is the full path name of the source file as seen in the host,
|
||||
// if found.
|
||||
LocalSrcPath string
|
||||
// Line is the line number.
|
||||
Line int
|
||||
// Func is the fully qualified function name (encoded).
|
||||
Func Func
|
||||
// Args is the call arguments.
|
||||
Args Args
|
||||
|
||||
// The following are only set if guesspaths is set to true in ParseDump().
|
||||
// IsStdlib is true if it is a Go standard library function. This includes
|
||||
// the 'go test' generated main executable.
|
||||
IsStdlib bool
|
||||
// RelSrcPath is the relative path to GOROOT or GOPATH. Only set when
|
||||
// Augment() is called.
|
||||
RelSrcPath string
|
||||
}
|
||||
|
||||
// equal returns true only if both calls are exactly equal.
|
||||
func (c *Call) equal(r *Call) bool {
|
||||
return c.SrcPath == r.SrcPath && c.Line == r.Line && c.Func == r.Func && c.Args.equal(&r.Args)
|
||||
}
|
||||
|
||||
// similar returns true if the two Call are equal or almost but not quite
|
||||
// equal.
|
||||
func (c *Call) similar(r *Call, similar Similarity) bool {
|
||||
return c.SrcPath == r.SrcPath && c.Line == r.Line && c.Func == r.Func && c.Args.similar(&r.Args, similar)
|
||||
}
|
||||
|
||||
// merge merges two similar Call, zapping out differences.
|
||||
func (c *Call) merge(r *Call) Call {
|
||||
return Call{
|
||||
SrcPath: c.SrcPath,
|
||||
LocalSrcPath: c.LocalSrcPath,
|
||||
Line: c.Line,
|
||||
Func: c.Func,
|
||||
Args: c.Args.merge(&r.Args),
|
||||
IsStdlib: c.IsStdlib,
|
||||
RelSrcPath: c.RelSrcPath,
|
||||
}
|
||||
}
|
||||
|
||||
// SrcName returns the base file name of the source file.
|
||||
func (c *Call) SrcName() string {
|
||||
return filepath.Base(c.SrcPath)
|
||||
}
|
||||
|
||||
// SrcLine returns "source.go:line", including only the base file name.
|
||||
//
|
||||
// Deprecated: Format it yourself, will be removed in v2.
|
||||
func (c *Call) SrcLine() string {
|
||||
return fmt.Sprintf("%s:%d", c.SrcName(), c.Line)
|
||||
}
|
||||
|
||||
// FullSrcLine returns "/path/to/source.go:line".
|
||||
//
|
||||
// This file path is mutated to look like the local path.
|
||||
//
|
||||
// Deprecated: Format it yourself, will be removed in v2.
|
||||
func (c *Call) FullSrcLine() string {
|
||||
return fmt.Sprintf("%s:%d", c.SrcPath, c.Line)
|
||||
}
|
||||
|
||||
// PkgSrc returns one directory plus the file name of the source file.
|
||||
//
|
||||
// Since the package name can differ from the package import path, the result
|
||||
// is incorrect when there's a mismatch between the directory name containing
|
||||
// the package and the package name.
|
||||
func (c *Call) PkgSrc() string {
|
||||
return pathJoin(filepath.Base(filepath.Dir(c.SrcPath)), c.SrcName())
|
||||
}
|
||||
|
||||
// IsPkgMain returns true if it is in the main package.
|
||||
func (c *Call) IsPkgMain() bool {
|
||||
return c.Func.PkgName() == "main"
|
||||
}
|
||||
|
||||
// ImportPath returns the fully qualified package import path.
|
||||
//
|
||||
// In the case of package "main", it returns the underlying path to the main
|
||||
// package instead of "main" if guesspaths=true was specified to ParseDump().
|
||||
func (c *Call) ImportPath() string {
|
||||
// In case guesspath=true was passed to ParseDump().
|
||||
if c.RelSrcPath != "" {
|
||||
if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
|
||||
return c.RelSrcPath[:i]
|
||||
}
|
||||
}
|
||||
// Fallback to best effort.
|
||||
if !c.IsPkgMain() {
|
||||
return c.Func.importPath()
|
||||
}
|
||||
// In package main, it can only work well if guesspath=true was used. Return
|
||||
// an empty string instead of garbagge.
|
||||
return ""
|
||||
}
|
||||
|
||||
const testMainSrc = "_test" + string(os.PathSeparator) + "_testmain.go"
|
||||
|
||||
// updateLocations initializes LocalSrcPath, RelSrcPath and IsStdlib.
|
||||
//
|
||||
// goroot, localgoroot, localgomod, gomodImportPath and gopaths are expected to
|
||||
// be in "/" format even on Windows. They must not have a trailing "/".
|
||||
func (c *Call) updateLocations(goroot, localgoroot, localgomod, gomodImportPath string, gopaths map[string]string) {
|
||||
if c.SrcPath == "" {
|
||||
return
|
||||
}
|
||||
// Check GOROOT first.
|
||||
if goroot != "" {
|
||||
if prefix := goroot + "/src/"; strings.HasPrefix(c.SrcPath, prefix) {
|
||||
// Replace remote GOROOT with local GOROOT.
|
||||
c.RelSrcPath = c.SrcPath[len(prefix):]
|
||||
c.LocalSrcPath = pathJoin(localgoroot, "src", c.RelSrcPath)
|
||||
c.IsStdlib = true
|
||||
goto done
|
||||
}
|
||||
}
|
||||
// Check GOPATH.
|
||||
// TODO(maruel): Sort for deterministic behavior?
|
||||
for prefix, dest := range gopaths {
|
||||
if p := prefix + "/src/"; strings.HasPrefix(c.SrcPath, p) {
|
||||
c.RelSrcPath = c.SrcPath[len(p):]
|
||||
c.LocalSrcPath = pathJoin(dest, "src", c.RelSrcPath)
|
||||
goto done
|
||||
}
|
||||
// For modules, the path has to be altered, as it contains the version.
|
||||
if p := prefix + "/pkg/mod/"; strings.HasPrefix(c.SrcPath, p) {
|
||||
c.RelSrcPath = c.SrcPath[len(p):]
|
||||
c.LocalSrcPath = pathJoin(dest, "pkg/mod", c.RelSrcPath)
|
||||
goto done
|
||||
}
|
||||
}
|
||||
// Go module path detection only works with stack traces created on the local
|
||||
// file system.
|
||||
if localgomod != "" {
|
||||
if prefix := localgomod + "/"; strings.HasPrefix(c.SrcPath, prefix) {
|
||||
c.RelSrcPath = gomodImportPath + "/" + c.SrcPath[len(prefix):]
|
||||
c.LocalSrcPath = c.SrcPath
|
||||
goto done
|
||||
}
|
||||
}
|
||||
done:
|
||||
if !c.IsStdlib {
|
||||
// Consider _test/_testmain.go as stdlib since it's injected by "go test".
|
||||
c.IsStdlib = c.PkgSrc() == testMainSrc
|
||||
}
|
||||
}
|
||||
|
||||
// Stack is a call stack.
|
||||
type Stack struct {
|
||||
// Calls is the call stack. First is original function, last is leaf
|
||||
// function.
|
||||
Calls []Call
|
||||
// Elided is set when there's >100 items in Stack, currently hardcoded in
|
||||
// package runtime.
|
||||
Elided bool
|
||||
}
|
||||
|
||||
// equal returns true on if both call stacks are exactly equal.
|
||||
func (s *Stack) equal(r *Stack) bool {
|
||||
if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided {
|
||||
return false
|
||||
}
|
||||
for i := range s.Calls {
|
||||
if !s.Calls[i].equal(&r.Calls[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// similar returns true if the two Stack are equal or almost but not quite
|
||||
// equal.
|
||||
func (s *Stack) similar(r *Stack, similar Similarity) bool {
|
||||
if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided {
|
||||
return false
|
||||
}
|
||||
for i := range s.Calls {
|
||||
if !s.Calls[i].similar(&r.Calls[i], similar) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// merge merges two similar Stack, zapping out differences.
|
||||
func (s *Stack) merge(r *Stack) *Stack {
|
||||
// Assumes similar stacks have the same length.
|
||||
out := &Stack{
|
||||
Calls: make([]Call, len(s.Calls)),
|
||||
Elided: s.Elided,
|
||||
}
|
||||
for i := range s.Calls {
|
||||
out.Calls[i] = s.Calls[i].merge(&r.Calls[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// less compares two Stack, where the ones that are less are more
|
||||
// important, so they come up front.
|
||||
//
|
||||
// A Stack with more private functions is 'less' so it is at the top.
|
||||
// Inversely, a Stack with only public functions is 'more' so it is at the
|
||||
// bottom.
|
||||
func (s *Stack) less(r *Stack) bool {
|
||||
lStdlib := 0
|
||||
lPrivate := 0
|
||||
for _, c := range s.Calls {
|
||||
if c.IsStdlib {
|
||||
lStdlib++
|
||||
} else {
|
||||
lPrivate++
|
||||
}
|
||||
}
|
||||
rStdlib := 0
|
||||
rPrivate := 0
|
||||
for _, s := range r.Calls {
|
||||
if s.IsStdlib {
|
||||
rStdlib++
|
||||
} else {
|
||||
rPrivate++
|
||||
}
|
||||
}
|
||||
if lPrivate > rPrivate {
|
||||
return true
|
||||
}
|
||||
if lPrivate < rPrivate {
|
||||
return false
|
||||
}
|
||||
if lStdlib > rStdlib {
|
||||
return false
|
||||
}
|
||||
if lStdlib < rStdlib {
|
||||
return true
|
||||
}
|
||||
|
||||
// Stack lengths are the same.
|
||||
for x := range s.Calls {
|
||||
if s.Calls[x].Func.Raw < r.Calls[x].Func.Raw {
|
||||
return true
|
||||
}
|
||||
if s.Calls[x].Func.Raw > r.Calls[x].Func.Raw {
|
||||
return true
|
||||
}
|
||||
if s.Calls[x].PkgSrc() < r.Calls[x].PkgSrc() {
|
||||
return true
|
||||
}
|
||||
if s.Calls[x].PkgSrc() > r.Calls[x].PkgSrc() {
|
||||
return true
|
||||
}
|
||||
if s.Calls[x].Line < r.Calls[x].Line {
|
||||
return true
|
||||
}
|
||||
if s.Calls[x].Line > r.Calls[x].Line {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Stack) updateLocations(goroot, localgoroot, localgomod, gomodImportPath string, gopaths map[string]string) {
|
||||
for i := range s.Calls {
|
||||
s.Calls[i].updateLocations(goroot, localgoroot, localgomod, gomodImportPath, gopaths)
|
||||
}
|
||||
}
|
||||
|
||||
// Signature represents the signature of one or multiple goroutines.
|
||||
//
|
||||
// It is effectively the stack trace plus the goroutine internal bits, like
|
||||
// it's state, if it is thread locked, which call site created this goroutine,
|
||||
// etc.
|
||||
type Signature struct {
|
||||
// State is the goroutine state at the time of the snapshot.
|
||||
//
|
||||
// Use git grep 'gopark(|unlock)\(' to find them all plus everything listed
|
||||
// in runtime/traceback.go. Valid values includes:
|
||||
// - chan send, chan receive, select
|
||||
// - finalizer wait, mark wait (idle),
|
||||
// - Concurrent GC wait, GC sweep wait, force gc (idle)
|
||||
// - IO wait, panicwait
|
||||
// - semacquire, semarelease
|
||||
// - sleep, timer goroutine (idle)
|
||||
// - trace reader (blocked)
|
||||
// Stuck cases:
|
||||
// - chan send (nil chan), chan receive (nil chan), select (no cases)
|
||||
// Runnable states:
|
||||
// - idle, runnable, running, syscall, waiting, dead, enqueue, copystack,
|
||||
// Scan states:
|
||||
// - scan, scanrunnable, scanrunning, scansyscall, scanwaiting, scandead,
|
||||
// scanenqueue
|
||||
State string
|
||||
// Createdby is the goroutine which created this one, if applicable.
|
||||
CreatedBy Call
|
||||
// SleepMin is the wait time in minutes, if applicable.
|
||||
SleepMin int
|
||||
// SleepMax is the wait time in minutes, if applicable.
|
||||
SleepMax int
|
||||
// Stack is the call stack.
|
||||
Stack Stack
|
||||
// Locked is set if the goroutine was locked to an OS thread.
|
||||
Locked bool
|
||||
}
|
||||
|
||||
// equal returns true only if both signatures are exactly equal.
|
||||
func (s *Signature) equal(r *Signature) bool {
|
||||
if s.State != r.State || !s.CreatedBy.equal(&r.CreatedBy) || s.Locked != r.Locked || s.SleepMin != r.SleepMin || s.SleepMax != r.SleepMax {
|
||||
return false
|
||||
}
|
||||
return s.Stack.equal(&r.Stack)
|
||||
}
|
||||
|
||||
// similar returns true if the two Signature are equal or almost but not quite
|
||||
// equal.
|
||||
func (s *Signature) similar(r *Signature, similar Similarity) bool {
|
||||
if s.State != r.State || !s.CreatedBy.similar(&r.CreatedBy, similar) {
|
||||
return false
|
||||
}
|
||||
if similar == ExactFlags && s.Locked != r.Locked {
|
||||
return false
|
||||
}
|
||||
return s.Stack.similar(&r.Stack, similar)
|
||||
}
|
||||
|
||||
// merge merges two similar Signature, zapping out differences.
|
||||
func (s *Signature) merge(r *Signature) *Signature {
|
||||
min := s.SleepMin
|
||||
if r.SleepMin < min {
|
||||
min = r.SleepMin
|
||||
}
|
||||
max := s.SleepMax
|
||||
if r.SleepMax > max {
|
||||
max = r.SleepMax
|
||||
}
|
||||
return &Signature{
|
||||
State: s.State, // Drop right side.
|
||||
CreatedBy: s.CreatedBy, // Drop right side.
|
||||
SleepMin: min,
|
||||
SleepMax: max,
|
||||
Stack: *s.Stack.merge(&r.Stack),
|
||||
Locked: s.Locked || r.Locked, // TODO(maruel): This is weirdo.
|
||||
}
|
||||
}
|
||||
|
||||
// less compares two Signature, where the ones that are less are more
|
||||
// important, so they come up front. A Signature with more private functions is
|
||||
// 'less' so it is at the top. Inversely, a Signature with only public
|
||||
// functions is 'more' so it is at the bottom.
|
||||
func (s *Signature) less(r *Signature) bool {
|
||||
if s.Stack.less(&r.Stack) {
|
||||
return true
|
||||
}
|
||||
if r.Stack.less(&s.Stack) {
|
||||
return false
|
||||
}
|
||||
if s.Locked && !r.Locked {
|
||||
return true
|
||||
}
|
||||
if r.Locked && !s.Locked {
|
||||
return false
|
||||
}
|
||||
if s.State < r.State {
|
||||
return true
|
||||
}
|
||||
if s.State > r.State {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SleepString returns a string "N-M minutes" if the goroutine(s) slept for a
|
||||
// long time.
|
||||
//
|
||||
// Returns an empty string otherwise.
|
||||
func (s *Signature) SleepString() string {
|
||||
if s.SleepMax == 0 {
|
||||
return ""
|
||||
}
|
||||
if s.SleepMin != s.SleepMax {
|
||||
return fmt.Sprintf("%d~%d minutes", s.SleepMin, s.SleepMax)
|
||||
}
|
||||
return fmt.Sprintf("%d minutes", s.SleepMax)
|
||||
}
|
||||
|
||||
// CreatedByString return a short context about the origin of this goroutine
|
||||
// signature.
|
||||
//
|
||||
// Deprecated: Format it yourself, will be removed in v2.
|
||||
func (s *Signature) CreatedByString(fullPath bool) string {
|
||||
created := s.CreatedBy.Func.PkgDotName()
|
||||
if created == "" {
|
||||
return ""
|
||||
}
|
||||
created += " @ "
|
||||
if fullPath {
|
||||
created += s.CreatedBy.FullSrcLine()
|
||||
} else {
|
||||
created += s.CreatedBy.SrcLine()
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
func (s *Signature) updateLocations(goroot, localgoroot, localgomod, gomodImportPath string, gopaths map[string]string) {
|
||||
s.CreatedBy.updateLocations(goroot, localgoroot, localgomod, gomodImportPath, gopaths)
|
||||
s.Stack.updateLocations(goroot, localgoroot, localgomod, gomodImportPath, gopaths)
|
||||
}
|
||||
|
||||
// Goroutine represents the state of one goroutine, including the stack trace.
|
||||
type Goroutine struct {
|
||||
// Signature is the stack trace, internal bits, state, which call site
|
||||
// created it, etc.
|
||||
Signature
|
||||
// ID is the goroutine id.
|
||||
ID int
|
||||
// First is the goroutine first printed, normally the one that crashed.
|
||||
First bool
|
||||
}
|
||||
|
||||
// Private stuff.
|
||||
|
||||
// nameArguments is a post-processing step where Args are 'named' with numbers.
|
||||
func nameArguments(goroutines []*Goroutine) {
|
||||
// Set a name for any pointer occurring more than once.
|
||||
type object struct {
|
||||
args []*Arg
|
||||
inPrimary bool
|
||||
}
|
||||
objects := map[uint64]object{}
|
||||
// Enumerate all the arguments.
|
||||
for i := range goroutines {
|
||||
for j := range goroutines[i].Stack.Calls {
|
||||
for k := range goroutines[i].Stack.Calls[j].Args.Values {
|
||||
arg := goroutines[i].Stack.Calls[j].Args.Values[k]
|
||||
if arg.IsPtr() {
|
||||
objects[arg.Value] = object{
|
||||
args: append(objects[arg.Value].args, &goroutines[i].Stack.Calls[j].Args.Values[k]),
|
||||
inPrimary: objects[arg.Value].inPrimary || i == 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// CreatedBy.Args is never set.
|
||||
}
|
||||
order := make(uint64Slice, 0, len(objects)/2)
|
||||
for k, obj := range objects {
|
||||
if len(obj.args) > 1 && obj.inPrimary {
|
||||
order = append(order, k)
|
||||
}
|
||||
}
|
||||
sort.Sort(order)
|
||||
nextID := 1
|
||||
for _, k := range order {
|
||||
for _, arg := range objects[k].args {
|
||||
arg.Name = fmt.Sprintf("#%d", nextID)
|
||||
}
|
||||
nextID++
|
||||
}
|
||||
|
||||
// Now do the rest. This is done so the output is deterministic.
|
||||
order = make(uint64Slice, 0, len(objects))
|
||||
for k := range objects {
|
||||
order = append(order, k)
|
||||
}
|
||||
sort.Sort(order)
|
||||
for _, k := range order {
|
||||
// Process the remaining pointers, they were not referenced by primary
|
||||
// thread so will have higher IDs.
|
||||
if objects[k].inPrimary {
|
||||
continue
|
||||
}
|
||||
for _, arg := range objects[k].args {
|
||||
arg.Name = fmt.Sprintf("#%d", nextID)
|
||||
}
|
||||
nextID++
|
||||
}
|
||||
}
|
||||
|
||||
func pathJoin(s ...string) string {
|
||||
return strings.Join(s, "/")
|
||||
}
|
||||
|
||||
type uint64Slice []uint64
|
||||
|
||||
func (a uint64Slice) Len() int { return len(a) }
|
||||
func (a uint64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a uint64Slice) Less(i, j int) bool { return a[i] < a[j] }
|
@ -1,40 +0,0 @@
|
||||
// Code generated by "stringer -type state"; DO NOT EDIT.
|
||||
|
||||
package stack
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[normal-0]
|
||||
_ = x[betweenRoutine-1]
|
||||
_ = x[gotRoutineHeader-2]
|
||||
_ = x[gotFunc-3]
|
||||
_ = x[gotCreated-4]
|
||||
_ = x[gotFileFunc-5]
|
||||
_ = x[gotFileCreated-6]
|
||||
_ = x[gotUnavail-7]
|
||||
_ = x[gotRaceHeader1-8]
|
||||
_ = x[gotRaceHeader2-9]
|
||||
_ = x[gotRaceOperationHeader-10]
|
||||
_ = x[gotRaceOperationFunc-11]
|
||||
_ = x[gotRaceOperationFile-12]
|
||||
_ = x[betweenRaceOperations-13]
|
||||
_ = x[gotRaceGoroutineHeader-14]
|
||||
_ = x[gotRaceGoroutineFunc-15]
|
||||
_ = x[gotRaceGoroutineFile-16]
|
||||
_ = x[betweenRaceGoroutines-17]
|
||||
}
|
||||
|
||||
const _state_name = "normalbetweenRoutinegotRoutineHeadergotFuncgotCreatedgotFileFuncgotFileCreatedgotUnavailgotRaceHeader1gotRaceHeader2gotRaceOperationHeadergotRaceOperationFuncgotRaceOperationFilebetweenRaceOperationsgotRaceGoroutineHeadergotRaceGoroutineFuncgotRaceGoroutineFilebetweenRaceGoroutines"
|
||||
|
||||
var _state_index = [...]uint16{0, 6, 20, 36, 43, 53, 64, 78, 88, 102, 116, 138, 158, 178, 199, 221, 241, 261, 282}
|
||||
|
||||
func (i state) String() string {
|
||||
if i < 0 || i >= state(len(_state_index)-1) {
|
||||
return "state(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _state_name[_state_index[i]:_state_index[i+1]]
|
||||
}
|
Loading…
Reference in New Issue