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 }