pax_global_header00006660000000000000000000000064152067423030014514gustar00rootroot0000000000000052 comment=9df33087c79a9e5a83f52f293c2e2b630d2affb1 golang-github-ysmood-gotrace-0.6.0/000077500000000000000000000000001520674230300172005ustar00rootroot00000000000000golang-github-ysmood-gotrace-0.6.0/.github/000077500000000000000000000000001520674230300205405ustar00rootroot00000000000000golang-github-ysmood-gotrace-0.6.0/.github/workflows/000077500000000000000000000000001520674230300225755ustar00rootroot00000000000000golang-github-ysmood-gotrace-0.6.0/.github/workflows/go.yml000066400000000000000000000012211520674230300237210ustar00rootroot00000000000000name: Go on: [push] env: GODEBUG: tracebackancestors=1000 jobs: test: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v2 with: go-version: 1.17 - uses: actions/checkout@v2 - run: | go run github.com/ysmood/golangci-lint@latest go test -race -coverprofile=coverage.out go run github.com/ysmood/got/cmd/check-cov@latest other-platforms: strategy: matrix: os: [windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/setup-go@v2 with: go-version: 1.15 - uses: actions/checkout@v2 - run: go test -racegolang-github-ysmood-gotrace-0.6.0/.gitignore000066400000000000000000000000051520674230300211630ustar00rootroot00000000000000*.outgolang-github-ysmood-gotrace-0.6.0/.golangci.yml000066400000000000000000000003111520674230300215570ustar00rootroot00000000000000run: skip-dirs-use-default: false linters: enable: - gofmt - revive - gocyclo - misspell linters-settings: gocyclo: min-complexity: 15 issues: exclude-use-default: false golang-github-ysmood-gotrace-0.6.0/LICENSE000066400000000000000000000020511520674230300202030ustar00rootroot00000000000000The MIT License Copyright 2020 Yad Smood Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.golang-github-ysmood-gotrace-0.6.0/README.md000066400000000000000000000005351520674230300204620ustar00rootroot00000000000000# Overview A lib for monitoring runtime goroutine stack. Such as wait for goroutines to exit, leak detection, etc. ## Features - `context.Context` first design - Concurrent leak detection - No dependencies and 100% test coverage - Provides handy low-level APIs to extend the lib ## Guides To get started check the [examples](examples_test.go). golang-github-ysmood-gotrace-0.6.0/examples_test.go000066400000000000000000000047561520674230300224200ustar00rootroot00000000000000package gotrace_test import ( "context" "fmt" "os" "runtime" "strings" "testing" "time" "github.com/ysmood/gotrace" ) func TestExample(t *testing.T) { // Supports parallel leak detection t.Parallel() // Make sure no leak after the test gotrace.CheckLeak(t, 0) go func() { time.Sleep(100 * time.Millisecond) }() } func ExampleGet() { list := gotrace.Get(true) fmt.Println("id of current:", list[0].GoroutineID) fmt.Println("caller of current:", list[0].Stacks[2].Func) // Output: // // id of current: 1 // caller of current: github.com/ysmood/gotrace_test.ExampleGet } func ExampleIgnoreCurrent() { ignore := gotrace.IgnoreCurrent() go func() { time.Sleep(time.Second) }() start := time.Now() gotrace.Wait(context.TODO(), ignore) end := time.Since(start) if end > time.Second { fmt.Println("waited for 1 second") } // Output: waited for 1 second } func ExampleIgnoreFuncs() { ignoreCurrent := gotrace.IgnoreCurrent() ignore := gotrace.IgnoreFuncs("internal/poll.runtime_pollWait") go func() { time.Sleep(time.Second) }() start := time.Now() gotrace.Wait(context.TODO(), ignore, ignoreCurrent) end := time.Since(start) if end > time.Second { fmt.Println("waited for 1 second") } // Output: waited for 1 second } func ExampleCombineIgnores() { ignore := gotrace.CombineIgnores( gotrace.IgnoreCurrent(), func(t *gotrace.Trace) bool { return strings.Contains(t.Raw, "ExampleCombineIgnores.func2") }, ) go func() { time.Sleep(2 * time.Second) }() go func() { time.Sleep(time.Second) }() start := time.Now() gotrace.Wait(context.TODO(), ignore) end := time.Since(start) if time.Second < end && end < 2*time.Second { fmt.Println("only waits for the second goroutine") } // Output: only waits for the second goroutine } func ExampleTraces_String() { go func() { time.Sleep(time.Second) }() traces := gotrace.Wait(gotrace.Timeout(0)) str := fmt.Sprintf("%v %v", traces[0], traces) fmt.Println(strings.Contains(str, "gotrace_test.ExampleTraces_String")) // Output: true } func ExampleSignal() { // Skip the test for Windows because it can't send signal programatically. if runtime.GOOS == "windows" { fmt.Println("true") return } go func() { traces := gotrace.Wait(gotrace.Signal()) fmt.Println(strings.Contains(traces.String(), "gotrace_test.ExampleSignal")) }() time.Sleep(100 * time.Millisecond) p, _ := os.FindProcess(os.Getpid()) _ = p.Signal(os.Interrupt) time.Sleep(100 * time.Millisecond) // Output: true } golang-github-ysmood-gotrace-0.6.0/go.mod000066400000000000000000000000521520674230300203030ustar00rootroot00000000000000module github.com/ysmood/gotrace go 1.15 golang-github-ysmood-gotrace-0.6.0/gotrace.go000066400000000000000000000047151520674230300211620ustar00rootroot00000000000000package gotrace import ( "crypto/md5" "fmt" "regexp" "strconv" "strings" ) // Stack info type Stack struct { Func string Loc string } // Trace of one goroutine type Trace struct { Raw string GoroutineID int64 GoroutineAncestorIDs []int64 // Need GODEBUG="tracebackancestors=N" to be set WaitReason string // https://github.com/golang/go/blob/874b3132a84cf76da6a48978826c04c380a37a50/src/runtime/runtime2.go#L997 Stacks []Stack typeKey string // hash sum of WaitReason and Stacks } // String interface for fmt func (t Trace) String() string { return t.Raw } // HasParent goroutine id func (t Trace) HasParent(id int64) bool { for _, pid := range t.GoroutineAncestorIDs { if pid == id { return true } } return false } var regGoroutine = regexp.MustCompile(`^goroutine (\d+) \[(.+)\]:`) var regParent = regexp.MustCompile(`^\[originating from goroutine (\d+)\]:`) var regFunc = regexp.MustCompile(`^(?:created by )?([^()]+)`) var regLoc = regexp.MustCompile(`^\t(.*)( \+0x\w+)?$`) // Get the Trace of the calling goroutine. // If all is true, all other goroutines' Traces will be appended into the result too. func Get(all bool) Traces { rawList := strings.Split(GetStack(all), "\n\n") list := []*Trace{} for _, raw := range rawList { lines := strings.Split(raw, "\n") t := &Trace{ Raw: raw, Stacks: []Stack{}, GoroutineAncestorIDs: []int64{}, } typeKey := md5.New() l := len(lines) - 3 if l > -1 && lines[l] == "...additional frames elided..." { lines = append(lines[:l], lines[l+1:]...) } ancestor := false for i := 0; i < len(lines); i++ { l := lines[i] if i == 0 { ms := regGoroutine.FindStringSubmatch(l) id, _ := strconv.ParseInt(ms[1], 10, 64) t.GoroutineID = id t.WaitReason = ms[2] _, _ = typeKey.Write([]byte(t.WaitReason)) continue } else if i != 0 && l[len(l)-1] == ':' { ancestor = true ms := regParent.FindStringSubmatch(l) id, _ := strconv.ParseInt(ms[1], 10, 64) t.GoroutineAncestorIDs = append(t.GoroutineAncestorIDs, id) } if ancestor { continue } s := Stack{ regFunc.FindStringSubmatch(l)[1], regLoc.FindStringSubmatch(lines[i+1])[1], } t.Stacks = append(t.Stacks, s) _, _ = typeKey.Write([]byte(s.Func)) _, _ = typeKey.Write([]byte(s.Loc)) i++ } t.typeKey = fmt.Sprintf("%x", typeKey.Sum(nil)) list = append(list, t) } return list } golang-github-ysmood-gotrace-0.6.0/leak.go000066400000000000000000000035341520674230300204500ustar00rootroot00000000000000package gotrace import ( "context" "fmt" "log" "os" "runtime" "time" ) // Exit to os.Exit var Exit = os.Exit // CheckWithContext if there's goroutine leak func CheckWithContext(ctx context.Context, ignores ...Ignore) error { if traces := Wait(ctx, ignores...); traces.Any() { return fmt.Errorf("leaking goroutines: %s", traces) } return nil } // Check if there's goroutine leak func Check(timeout time.Duration, ignores ...Ignore) error { ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout(timeout)) defer cancel() return CheckWithContext(ctx, ignores...) } // M interface for testing.M type M interface { Run() int } // T interface for testing.T type T interface { Helper() Fail() Failed() bool Cleanup(f func()) Logf(format string, args ...interface{}) } // CheckMainLeak reports error if goroutines are leaking after all tests are done. Default timeout is 3s. // It's powerful but less accurate than Check, if you only use CheckMainLeak it will be hard to tell which test // is the cause of the leak. func CheckMainLeak(m M, timeout time.Duration, ignores ...Ignore) { code := m.Run() if code != 0 { Exit(code) return } if err := Check(timeout, ignores...); err != nil { _, file, line, _ := runtime.Caller(1) log.Printf("%s:%d %v\n", file, line, err) Exit(1) } } // CheckLeak reports error if the test is leaking goroutine. // Default timeout is 3s. Default ignore is gotrace.IgnoreNonChildren() . func CheckLeak(t T, timeout time.Duration, ignores ...Ignore) { t.Helper() if len(ignores) == 0 { ignores = []Ignore{IgnoreNonChildren()} } t.Cleanup(func() { t.Helper() if t.Failed() { return } if err := Check(timeout, ignores...); err != nil { t.Logf("%v", err) t.Fail() } }) } func defaultTimeout(t time.Duration) time.Duration { if t <= 0 { return 3 * time.Second } return t } golang-github-ysmood-gotrace-0.6.0/leak_test.go000066400000000000000000000046661520674230300215160ustar00rootroot00000000000000package gotrace_test import ( "bytes" "fmt" "log" "os" "strings" "sync" "testing" "time" "github.com/ysmood/gotrace" ) func TestAncestors(t *testing.T) { wg := sync.WaitGroup{} wg.Add(1) wait := make(chan int) go func() { <-wait }() go func() { go func() { go func() { <-wait }() }() time.Sleep(100 * time.Millisecond) id := gotrace.Get(false)[0].GoroutineID ts := gotrace.Get(true).Filter(gotrace.IgnoreNonChildren()) if len(ts) != 1 && ts[0].GoroutineID != id { t.Fail() } wg.Done() }() for _, trace := range gotrace.Get(true) { if trace.GoroutineID != 1 && !trace.HasParent(1) { t.Fail() } } wg.Wait() close(wait) } func TestCheckLeak(t *testing.T) { t.Parallel() m := &mockT{out: bytes.NewBuffer(nil)} wait := make(chan int) go func() { <-wait }() gotrace.CheckLeak(m, 100*time.Millisecond) if !m.Failed() { t.Log("m should fail") t.Fail() } if !strings.Contains(m.out.String(), "leaking goroutines") { t.Log("should find the leak") t.Fail() } close(wait) } func TestCheckLeakAlreadyFailed(t *testing.T) { m := &mockT{failed: true} wait := make(chan int) go func() { <-wait }() gotrace.CheckLeak(m, time.Millisecond) close(wait) } type mockT struct { failed bool out *bytes.Buffer } func (m *mockT) Helper() {} func (m *mockT) Fail() { m.failed = true } func (m *mockT) Failed() bool { return m.failed } func (m *mockT) Cleanup(f func()) { f() } func (m *mockT) Logf(format string, args ...interface{}) { fmt.Fprintf(m.out, format, args...) } func TestCheckMainLeak(t *testing.T) { old := gotrace.Exit t.Cleanup(func() { gotrace.Exit = old log.SetOutput(os.Stderr) }) gotrace.Exit = func(code int) {} buf := bytes.NewBuffer(nil) log.SetOutput(buf) wait := make(chan int) go func() { <-wait }() gotrace.CheckMainLeak(&mockM{}, time.Millisecond) if !strings.Contains(buf.String(), "leaking goroutines") { t.Fail() } close(wait) } func TestCheckMainLeakAlreadyFailed(t *testing.T) { old := gotrace.Exit t.Cleanup(func() { gotrace.Exit = old log.SetOutput(os.Stderr) }) gotrace.Exit = func(code int) {} buf := bytes.NewBuffer(nil) log.SetOutput(buf) wait := make(chan int) go func() { <-wait }() gotrace.CheckMainLeak(&mockM{code: 1}, time.Millisecond) t.Log(buf.String()) if buf.String() != "" { t.Fail() } close(wait) } type mockM struct { code int } func (m *mockM) Run() int { return m.code } golang-github-ysmood-gotrace-0.6.0/utils.go000066400000000000000000000101611520674230300206660ustar00rootroot00000000000000package gotrace import ( "context" "fmt" "os" "os/signal" "regexp" "runtime" "time" ) // GetStack of current runtime var GetStack = func(all bool) string { for i := 1024 * 1024; ; i *= 2 { buf := make([]byte, i) if n := runtime.Stack(buf, all); n < i { return string(buf[:n-1]) } } } // Ignore returns true to ignore t type Ignore func(t *Trace) bool // IgnoreCurrent running goroutines func IgnoreCurrent() Ignore { return IgnoreList(Get(true)) } // TraceAncestorsEnabled returns true if GODEBUG="tracebackancestors=N" is set var TraceAncestorsEnabled = regexp.MustCompile(`tracebackancestors=\d+`).MatchString(os.Getenv("GODEBUG")) // IgnoreNonChildren goroutines func IgnoreNonChildren() Ignore { if !TraceAncestorsEnabled { panic(`You must set GODEBUG="tracebackancestors=N", N should be a big enough integer, such as 1000`) } id := Get(false)[0].GoroutineID return func(t *Trace) bool { return !t.HasParent(id) } } // IgnoreList of traces func IgnoreList(list Traces) Ignore { return func(t *Trace) bool { for _, item := range list { if t.GoroutineID == item.GoroutineID { return true } } return false } } // IgnoreFuncs ignores a Trace if it's first Stack's Func equals one of the names. func IgnoreFuncs(names ...string) Ignore { return func(t *Trace) bool { for _, name := range names { if t.Stacks[0].Func == name { return true } } return false } } // CombineIgnores into one func CombineIgnores(list ...Ignore) Ignore { return func(t *Trace) bool { for _, i := range list { if i(t) { return true } } return false } } // Backoff is the default algorithm for sleep backoff var Backoff = func(t time.Duration) time.Duration { const maxSleep = 300 * time.Millisecond if t == 0 { return time.Microsecond } t *= 2 if t > maxSleep { return maxSleep } return t } // Wait uses Backoff for WaitWithBackoff func Wait(ctx context.Context, ignores ...Ignore) (remain Traces) { return WaitWithBackoff(ctx, Backoff, ignores...) } // WaitWithBackoff algorithm. Wait for other goroutines that are not ignored to exit. It returns the ones that are still active. // It keeps counting the active goroutines that are not ignored, if the number is zero return. func WaitWithBackoff(ctx context.Context, backoff func(time.Duration) time.Duration, ignores ...Ignore) (remain Traces) { sleep := backoff(0) ignore := CombineIgnores(ignores...) for { remain = Get(true)[1:].Filter(ignore) if len(remain) == 0 { return } sleep = backoff(sleep) tmr := time.NewTimer(sleep) select { case <-ctx.Done(): tmr.Stop() return case <-tmr.C: } } } var nullCancel = func() {} // Timeout shortcut for context.WithTimeout(context.Background(), d) func Timeout(d time.Duration) context.Context { ctx, c := context.WithTimeout(context.Background(), d) nullCancel() nullCancel = c return ctx } // Signal to cancel the returned context, default signal is CTRL+C . func Signal(signals ...os.Signal) context.Context { ctx, cancel := context.WithCancel(context.Background()) c := make(chan os.Signal, 1) if len(signals) == 0 { signals = append(signals, os.Interrupt) } signal.Notify(c, signals...) go func() { <-c signal.Stop(c) cancel() }() return ctx } // Traces of goroutines type Traces []*Trace // Any item exists in the list func (list Traces) Any() bool { return len(list) > 0 } // Filter returns the remain Traces that are ignored func (list Traces) Filter(ignore Ignore) Traces { remain := Traces{} for _, s := range list { if ignore(s) { continue } remain = append(remain, s) } return remain } // String interface for fmt. It will merge similar trace together and print counts. func (list Traces) String() string { type group struct { count int trace *Trace } // group by stack groups := map[string]*group{} for _, t := range list { if g, has := groups[t.typeKey]; has { g.count++ } else { groups[t.typeKey] = &group{count: 1, trace: t} } } out := "" for _, g := range groups { if g.count > 1 { out += fmt.Sprintf("[%d] ", g.count) + g.trace.Raw + "\n\n" } else { out += g.trace.Raw + "\n\n" } } return out } golang-github-ysmood-gotrace-0.6.0/utils_test.go000066400000000000000000000032661520674230300217350ustar00rootroot00000000000000package gotrace_test import ( "context" "testing" "time" "github.com/ysmood/gotrace" ) func TestMergeStackInfo(t *testing.T) { wait := make(chan int) fn := func(int) { <-wait } ig := gotrace.IgnoreCurrent() for i := range "..." { go fn(i) } ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) defer cancel() info := gotrace.Wait(ctx, ig).String() if info[:3] != "[3]" { t.Fail() } close(wait) } func TestGetElided(t *testing.T) { old := gotrace.GetStack defer func() { gotrace.GetStack = old }() gotrace.GetStack = func(all bool) string { return `goroutine 117 [runnable]: reflect.resolveTypeOff(0x7cd5a0, 0x45f40, 0x7bef40) D:/Go16/src/runtime/runtime1.go:504 +0x3a reflect.(*rtype).typeOff(...) D:/Go16/src/reflect/type.go:690 reflect.(*rtype).ptrTo(0x7cd5a0, 0x6) D:/Go16/src/reflect/type.go:1384 +0x36c reflect.Value.Addr(0x7cd5a0, 0xc0001f6380, 0x198, 0xa489e0, 0x7cd5a0, 0x198) D:/Go16/src/reflect/value.go:276 +0x3d encoding/json.(*decodeState).array(0xc0000a42c0, 0x7c1360, 0xc00013a1f8, 0x197, 0xc0000a42e8, 0x5b) D:/Go16/src/encoding/json/decode.go:558 +0x1b4 ...additional frames elided... created by main.testFn.func2 F:/test/fold/go-rod/debugg.go:518 +0x65e` } if gotrace.Get(true)[0].Stacks[5].Func != "main.testFn.func2" { t.Fail() } } func TestIgnoreNonChildrenPanic(t *testing.T) { old := gotrace.TraceAncestorsEnabled gotrace.TraceAncestorsEnabled = false t.Cleanup(func() { gotrace.TraceAncestorsEnabled = old }) defer func() { r := recover() if r.(string) != `You must set GODEBUG="tracebackancestors=N", N should be a big enough integer, such as 1000` { t.Fail() } }() gotrace.IgnoreNonChildren() }