github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/trace/v2/jsontrace_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package trace
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"io"
    11  	"net/http/httptest"
    12  	"os"
    13  	"path/filepath"
    14  	"slices"
    15  	"strconv"
    16  	"strings"
    17  	"testing"
    18  	"time"
    19  
    20  	tracev1 "github.com/go-asm/go/trace"
    21  
    22  	"github.com/go-asm/go/trace/traceviewer/format"
    23  	"github.com/go-asm/go/trace/v2/raw"
    24  )
    25  
    26  func TestJSONTraceHandler(t *testing.T) {
    27  	testPaths, err := filepath.Glob("./testdata/*.test")
    28  	if err != nil {
    29  		t.Fatalf("discovering tests: %v", err)
    30  	}
    31  	for _, testPath := range testPaths {
    32  		t.Run(filepath.Base(testPath), func(t *testing.T) {
    33  			parsed := getTestTrace(t, testPath)
    34  			data := recordJSONTraceHandlerResponse(t, parsed)
    35  			// TODO(mknyszek): Check that there's one at most goroutine per proc at any given time.
    36  			checkExecutionTimes(t, data)
    37  			checkPlausibleHeapMetrics(t, data)
    38  			// TODO(mknyszek): Check for plausible thread and goroutine metrics.
    39  			checkMetaNamesEmitted(t, data, "process_name", []string{"STATS", "PROCS"})
    40  			checkMetaNamesEmitted(t, data, "thread_name", []string{"GC", "Network", "Timers", "Syscalls", "Proc 0"})
    41  			checkProcStartStop(t, data)
    42  			checkSyscalls(t, data)
    43  			checkNetworkUnblock(t, data)
    44  			// TODO(mknyszek): Check for flow events.
    45  		})
    46  	}
    47  }
    48  
    49  func checkSyscalls(t *testing.T, data format.Data) {
    50  	data = filterViewerTrace(data,
    51  		filterEventName("syscall"),
    52  		filterStackRootFunc("main.blockingSyscall"))
    53  	if len(data.Events) <= 1 {
    54  		t.Errorf("got %d events, want > 1", len(data.Events))
    55  	}
    56  	data = filterViewerTrace(data, filterBlocked("yes"))
    57  	if len(data.Events) != 1 {
    58  		t.Errorf("got %d events, want 1", len(data.Events))
    59  	}
    60  }
    61  
    62  type eventFilterFn func(*format.Event, *format.Data) bool
    63  
    64  func filterEventName(name string) eventFilterFn {
    65  	return func(e *format.Event, _ *format.Data) bool {
    66  		return e.Name == name
    67  	}
    68  }
    69  
    70  // filterGoRoutineName returns an event filter that returns true if the event's
    71  // goroutine name is equal to name.
    72  func filterGoRoutineName(name string) eventFilterFn {
    73  	return func(e *format.Event, _ *format.Data) bool {
    74  		return parseGoroutineName(e) == name
    75  	}
    76  }
    77  
    78  // parseGoroutineName returns the goroutine name from the event's name field.
    79  // E.g. if e.Name is "G42 main.cpu10", this returns "main.cpu10".
    80  func parseGoroutineName(e *format.Event) string {
    81  	parts := strings.SplitN(e.Name, " ", 2)
    82  	if len(parts) != 2 || !strings.HasPrefix(parts[0], "G") {
    83  		return ""
    84  	}
    85  	return parts[1]
    86  }
    87  
    88  // filterBlocked returns an event filter that returns true if the event's
    89  // "blocked" argument is equal to blocked.
    90  func filterBlocked(blocked string) eventFilterFn {
    91  	return func(e *format.Event, _ *format.Data) bool {
    92  		m, ok := e.Arg.(map[string]any)
    93  		if !ok {
    94  			return false
    95  		}
    96  		return m["blocked"] == blocked
    97  	}
    98  }
    99  
   100  // filterStackRootFunc returns an event filter that returns true if the function
   101  // at the root of the stack trace is named name.
   102  func filterStackRootFunc(name string) eventFilterFn {
   103  	return func(e *format.Event, data *format.Data) bool {
   104  		frames := stackFrames(data, e.Stack)
   105  		rootFrame := frames[len(frames)-1]
   106  		return strings.HasPrefix(rootFrame, name+":")
   107  	}
   108  }
   109  
   110  // filterViewerTrace returns a copy of data with only the events that pass all
   111  // of the given filters.
   112  func filterViewerTrace(data format.Data, fns ...eventFilterFn) (filtered format.Data) {
   113  	filtered = data
   114  	filtered.Events = nil
   115  	for _, e := range data.Events {
   116  		keep := true
   117  		for _, fn := range fns {
   118  			keep = keep && fn(e, &filtered)
   119  		}
   120  		if keep {
   121  			filtered.Events = append(filtered.Events, e)
   122  		}
   123  	}
   124  	return
   125  }
   126  
   127  func stackFrames(data *format.Data, stackID int) (frames []string) {
   128  	for {
   129  		frame, ok := data.Frames[strconv.Itoa(stackID)]
   130  		if !ok {
   131  			return
   132  		}
   133  		frames = append(frames, frame.Name)
   134  		stackID = frame.Parent
   135  	}
   136  }
   137  
   138  func checkProcStartStop(t *testing.T, data format.Data) {
   139  	procStarted := map[uint64]bool{}
   140  	for _, e := range data.Events {
   141  		if e.Name == "proc start" {
   142  			if procStarted[e.TID] == true {
   143  				t.Errorf("proc started twice: %d", e.TID)
   144  			}
   145  			procStarted[e.TID] = true
   146  		}
   147  		if e.Name == "proc stop" {
   148  			if procStarted[e.TID] == false {
   149  				t.Errorf("proc stopped twice: %d", e.TID)
   150  			}
   151  			procStarted[e.TID] = false
   152  		}
   153  	}
   154  	if got, want := len(procStarted), 8; got != want {
   155  		t.Errorf("wrong number of procs started/stopped got=%d want=%d", got, want)
   156  	}
   157  }
   158  
   159  func checkNetworkUnblock(t *testing.T, data format.Data) {
   160  	count := 0
   161  	var netBlockEv *format.Event
   162  	for _, e := range data.Events {
   163  		if e.TID == tracev1.NetpollP && e.Name == "unblock (network)" && e.Phase == "I" && e.Scope == "t" {
   164  			count++
   165  			netBlockEv = e
   166  		}
   167  	}
   168  	if netBlockEv == nil {
   169  		t.Error("failed to find a network unblock")
   170  	}
   171  	if count == 0 {
   172  		t.Errorf("found zero network block events, want at least one")
   173  	}
   174  	// TODO(mknyszek): Check for the flow of this event to some slice event of a goroutine running.
   175  }
   176  
   177  func checkExecutionTimes(t *testing.T, data format.Data) {
   178  	cpu10 := sumExecutionTime(filterViewerTrace(data, filterGoRoutineName("main.cpu10")))
   179  	cpu20 := sumExecutionTime(filterViewerTrace(data, filterGoRoutineName("main.cpu20")))
   180  	if cpu10 <= 0 || cpu20 <= 0 || cpu10 >= cpu20 {
   181  		t.Errorf("bad execution times: cpu10=%v, cpu20=%v", cpu10, cpu20)
   182  	}
   183  }
   184  
   185  func checkMetaNamesEmitted(t *testing.T, data format.Data, category string, want []string) {
   186  	t.Helper()
   187  	names := metaEventNameArgs(category, data)
   188  	for _, wantName := range want {
   189  		if !slices.Contains(names, wantName) {
   190  			t.Errorf("%s: names=%v, want %q", category, names, wantName)
   191  		}
   192  	}
   193  }
   194  
   195  func metaEventNameArgs(category string, data format.Data) (names []string) {
   196  	for _, e := range data.Events {
   197  		if e.Name == category && e.Phase == "M" {
   198  			names = append(names, e.Arg.(map[string]any)["name"].(string))
   199  		}
   200  	}
   201  	return
   202  }
   203  
   204  func checkPlausibleHeapMetrics(t *testing.T, data format.Data) {
   205  	hms := heapMetrics(data)
   206  	var nonZeroAllocated, nonZeroNextGC bool
   207  	for _, hm := range hms {
   208  		if hm.Allocated > 0 {
   209  			nonZeroAllocated = true
   210  		}
   211  		if hm.NextGC > 0 {
   212  			nonZeroNextGC = true
   213  		}
   214  	}
   215  
   216  	if !nonZeroAllocated {
   217  		t.Errorf("nonZeroAllocated=%v, want true", nonZeroAllocated)
   218  	}
   219  	if !nonZeroNextGC {
   220  		t.Errorf("nonZeroNextGC=%v, want true", nonZeroNextGC)
   221  	}
   222  }
   223  
   224  func heapMetrics(data format.Data) (metrics []format.HeapCountersArg) {
   225  	for _, e := range data.Events {
   226  		if e.Phase == "C" && e.Name == "Heap" {
   227  			j, _ := json.Marshal(e.Arg)
   228  			var metric format.HeapCountersArg
   229  			json.Unmarshal(j, &metric)
   230  			metrics = append(metrics, metric)
   231  		}
   232  	}
   233  	return
   234  }
   235  
   236  func recordJSONTraceHandlerResponse(t *testing.T, parsed *parsedTrace) format.Data {
   237  	h := JSONTraceHandler(parsed)
   238  	recorder := httptest.NewRecorder()
   239  	r := httptest.NewRequest("GET", "/jsontrace", nil)
   240  	h.ServeHTTP(recorder, r)
   241  
   242  	var data format.Data
   243  	if err := json.Unmarshal(recorder.Body.Bytes(), &data); err != nil {
   244  		t.Fatal(err)
   245  	}
   246  	return data
   247  }
   248  
   249  func sumExecutionTime(data format.Data) (sum time.Duration) {
   250  	for _, e := range data.Events {
   251  		sum += time.Duration(e.Dur) * time.Microsecond
   252  	}
   253  	return
   254  }
   255  
   256  func getTestTrace(t *testing.T, testPath string) *parsedTrace {
   257  	t.Helper()
   258  
   259  	// First read in the text trace and write it out as bytes.
   260  	f, err := os.Open(testPath)
   261  	if err != nil {
   262  		t.Fatalf("failed to open test %s: %v", testPath, err)
   263  	}
   264  	r, err := raw.NewTextReader(f)
   265  	if err != nil {
   266  		t.Fatalf("failed to read test %s: %v", testPath, err)
   267  	}
   268  	var trace bytes.Buffer
   269  	w, err := raw.NewWriter(&trace, r.Version())
   270  	if err != nil {
   271  		t.Fatalf("failed to write out test %s: %v", testPath, err)
   272  	}
   273  	for {
   274  		ev, err := r.ReadEvent()
   275  		if err == io.EOF {
   276  			break
   277  		}
   278  		if err != nil {
   279  			t.Fatalf("failed to read test %s: %v", testPath, err)
   280  		}
   281  		if err := w.WriteEvent(ev); err != nil {
   282  			t.Fatalf("failed to write out test %s: %v", testPath, err)
   283  		}
   284  	}
   285  
   286  	// Parse the test trace.
   287  	parsed, err := parseTrace(&trace)
   288  	if err != nil {
   289  		t.Fatalf("failed to parse trace: %v", err)
   290  	}
   291  	return parsed
   292  }