tailscale/tstest/resource.go

118 lines
2.5 KiB
Go

// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tstest
import (
"bytes"
"compress/gzip"
"io"
"runtime"
"runtime/pprof"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"tailscale.com/tstest/profilepb"
)
func ResourceCheck(tb testing.TB) {
tb.Helper()
startN, startStacks := goroutines()
tb.Cleanup(func() {
if tb.Failed() {
// Something else went wrong.
return
}
// Goroutines might be still exiting.
for i := 0; i < 100; i++ {
if runtime.NumGoroutine() <= startN {
return
}
time.Sleep(5 * time.Millisecond)
}
filteredGoroutines := numGoroutines(tb, pprof.Lookup("goroutine"))
if filteredGoroutines <= startN {
return
}
endN, endStacks := goroutines()
tb.Logf("goroutine diff:\n%v\n", cmp.Diff(startStacks, endStacks))
tb.Fatalf("goroutine count: expected %d, got %d (%d filtered)\n", startN, endN, filteredGoroutines)
})
}
func goroutines() (int, []byte) {
p := pprof.Lookup("goroutine")
b := new(bytes.Buffer)
p.WriteTo(b, 1)
return p.Count(), b.Bytes()
}
var ignoredGoroutineStarts = map[string]bool{
"net.cgoIPLookup": true,
}
func numGoroutines(tb testing.TB, p *pprof.Profile) int {
b := new(bytes.Buffer)
p.WriteTo(b, 0) // gzip-compressed protobuf format
zr, err := gzip.NewReader(b)
if err != nil {
tb.Logf("error creating gzip.Reader: %v", err)
return -1
}
pb, err := io.ReadAll(zr)
if err != nil {
tb.Logf("error decompressing profile: %v", err)
return -1
}
var prof profilepb.Profile
if err := proto.Unmarshal(pb, &prof); err != nil {
tb.Logf("error parsing profile: %v", err)
return -1
}
var (
functions = make(map[uint64]*profilepb.Function)
locations = make(map[uint64]*profilepb.Location)
)
for _, f := range prof.Function {
functions[f.Id] = f
}
for _, m := range prof.Location {
locations[m.Id] = m
}
var num int64
for _, sample := range prof.Sample {
skip := false
for _, locid := range sample.LocationId {
loc := locations[locid]
for _, line := range loc.Line {
fn := functions[line.FunctionId]
fname := prof.StringTable[fn.Name]
if ignoredGoroutineStarts[fname] {
tb.Logf("skipping goroutine: %s", fname)
skip = true
break
}
}
if skip {
break
}
}
if skip {
continue
}
for _, val := range sample.Value {
num += val
}
}
return int(num)
}