mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	ResourceCheck was previously using cmp.Diff on multiline goroutine stacks The produced output was difficult to read for a number of reasons: - the goroutines were sorted by count, and a changing count caused them to jump around - diffs would be in the middle of stacks Instead, we now parse the pprof/goroutines?debug=1 format goroutines and only diff whole stacks. Updates #1253 Signed-off-by: Paul Scott <paul@tailscale.com>
		
			
				
	
	
		
			257 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			257 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package tstest
 | |
| 
 | |
| import (
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| )
 | |
| 
 | |
| func TestPrintGoroutines(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name string
 | |
| 		in   string
 | |
| 		want string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "empty",
 | |
| 			in:   "goroutine profile: total 0\n",
 | |
| 			want: "goroutine profile: total 0",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "single goroutine",
 | |
| 			in: `goroutine profile: total 1
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 			want: `goroutine profile: total 1
 | |
| 
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multiple goroutines sorted",
 | |
| 			in: `goroutine profile: total 14
 | |
| 7 @ 0x47bc0e 0x413705 0x4132b2 0x10fda4d 0x483da1
 | |
| #   0x10fda4c   github.com/user/pkg.RoutineA+0x16c    pkg/a.go:443
 | |
| 
 | |
| 7 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| `,
 | |
| 			want: `goroutine profile: total 14
 | |
| 
 | |
| 7 @ 0x47bc0e 0x413705 0x4132b2 0x10fda4d 0x483da1
 | |
| #   0x10fda4c   github.com/user/pkg.RoutineA+0x16c    pkg/a.go:443
 | |
| 
 | |
| 7 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| `,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			got := string(printGoroutines(parseGoroutines([]byte(tt.in))))
 | |
| 			if got != tt.want {
 | |
| 				t.Errorf("printGoroutines() = %q, want %q, diff:\n%s", got, tt.want, cmp.Diff(tt.want, got))
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestDiffPprofGoroutines(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name string
 | |
| 		x, y string
 | |
| 		want string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "no difference",
 | |
| 			x: `goroutine profile: total 1
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261`,
 | |
| 			y: `goroutine profile: total 1
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 			want: "",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "different counts",
 | |
| 			x: `goroutine profile: total 1
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 			y: `goroutine profile: total 2
 | |
| 2 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 			want: `- goroutine profile: total 1
 | |
| + goroutine profile: total 2
 | |
| 
 | |
| - 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| + 2 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
|   #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "new goroutine",
 | |
| 			x: `goroutine profile: total 1
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 			y: `goroutine profile: total 2
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| 
 | |
| 1 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| `,
 | |
| 			want: `- goroutine profile: total 1
 | |
| + goroutine profile: total 2
 | |
| 
 | |
| + 1 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| + #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| `,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "removed goroutine",
 | |
| 			x: `goroutine profile: total 2
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| 
 | |
| 1 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| `,
 | |
| 			y: `goroutine profile: total 1
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 			want: `- goroutine profile: total 2
 | |
| + goroutine profile: total 1
 | |
| 
 | |
| - 1 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| - #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| `,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "removed many goroutine",
 | |
| 			x: `goroutine profile: total 2
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| 
 | |
| 1 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| `,
 | |
| 			y: `goroutine profile: total 0`,
 | |
| 			want: `- goroutine profile: total 2
 | |
| + goroutine profile: total 0
 | |
| 
 | |
| - 1 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| - #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| 
 | |
| - 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| - #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "invalid input x",
 | |
| 			x:    "invalid",
 | |
| 			y:    "goroutine profile: total 0\n",
 | |
| 			want: "- invalid\n+ goroutine profile: total 0\n",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "invalid input y",
 | |
| 			x:    "goroutine profile: total 0\n",
 | |
| 			y:    "invalid",
 | |
| 			want: "- goroutine profile: total 0\n+ invalid\n",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			got := diffGoroutines(
 | |
| 				parseGoroutines([]byte(tt.x)),
 | |
| 				parseGoroutines([]byte(tt.y)),
 | |
| 			)
 | |
| 			if got != tt.want {
 | |
| 				t.Errorf("diffPprofGoroutines() diff:\ngot:\n%s\nwant:\n%s\ndiff (-want +got):\n%s", got, tt.want, cmp.Diff(tt.want, got))
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestParseGoroutines(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name       string
 | |
| 		in         string
 | |
| 		wantHeader string
 | |
| 		wantCount  int
 | |
| 	}{
 | |
| 		{
 | |
| 			name:       "empty profile",
 | |
| 			in:         "goroutine profile: total 0\n",
 | |
| 			wantHeader: "goroutine profile: total 0",
 | |
| 			wantCount:  0,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "single goroutine",
 | |
| 			in: `goroutine profile: total 1
 | |
| 1 @ 0x47bc0e 0x458e57 0x847587 0x483da1
 | |
| #   0x847586    database/sql.(*DB).connectionOpener+0x86    database/sql/sql.go:1261
 | |
| `,
 | |
| 			wantHeader: "goroutine profile: total 1",
 | |
| 			wantCount:  1,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multiple goroutines",
 | |
| 			in: `goroutine profile: total 14
 | |
| 7 @ 0x47bc0e 0x413705 0x4132b2 0x10fda4d 0x483da1
 | |
| #   0x10fda4c   github.com/user/pkg.RoutineA+0x16c    pkg/a.go:443
 | |
| 
 | |
| 7 @ 0x47bc0e 0x458e57 0x754927 0x483da1
 | |
| #   0x754926    net/http.(*persistConn).writeLoop+0xe6  net/http/transport.go:2596
 | |
| `,
 | |
| 			wantHeader: "goroutine profile: total 14",
 | |
| 			wantCount:  2,
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "invalid format",
 | |
| 			in:         "invalid",
 | |
| 			wantHeader: "invalid",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := parseGoroutines([]byte(tt.in))
 | |
| 
 | |
| 			if got := string(g.head); got != tt.wantHeader {
 | |
| 				t.Errorf("parseGoroutines() header = %q, want %q", got, tt.wantHeader)
 | |
| 			}
 | |
| 			if got := len(g.goroutines); got != tt.wantCount {
 | |
| 				t.Errorf("parseGoroutines() goroutine count = %d, want %d", got, tt.wantCount)
 | |
| 			}
 | |
| 
 | |
| 			// Verify that the sort field is correctly reversed
 | |
| 			for _, g := range g.goroutines {
 | |
| 				original := strings.Fields(string(g.header))
 | |
| 				sorted := strings.Fields(string(g.sort))
 | |
| 				if len(original) != len(sorted) {
 | |
| 					t.Errorf("sort field has different number of words: got %d, want %d", len(sorted), len(original))
 | |
| 					continue
 | |
| 				}
 | |
| 				for i := 0; i < len(original); i++ {
 | |
| 					if original[i] != sorted[len(sorted)-1-i] {
 | |
| 						t.Errorf("sort field word mismatch at position %d: got %q, want %q", i, sorted[len(sorted)-1-i], original[i])
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |