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
5package trace_test
6
7import (
8	"internal/trace"
9	"internal/trace/testtrace"
10	"io"
11	"testing"
12)
13
14func TestSummarizeGoroutinesTrace(t *testing.T) {
15	summaries := summarizeTraceTest(t, "testdata/tests/go122-gc-stress.test").Goroutines
16	var (
17		hasSchedWaitTime    bool
18		hasSyncBlockTime    bool
19		hasGCMarkAssistTime bool
20	)
21
22	assertContainsGoroutine(t, summaries, "runtime.gcBgMarkWorker")
23	assertContainsGoroutine(t, summaries, "main.main.func1")
24
25	for _, summary := range summaries {
26		basicGoroutineSummaryChecks(t, summary)
27		hasSchedWaitTime = hasSchedWaitTime || summary.SchedWaitTime > 0
28		if dt, ok := summary.BlockTimeByReason["sync"]; ok && dt > 0 {
29			hasSyncBlockTime = true
30		}
31		if dt, ok := summary.RangeTime["GC mark assist"]; ok && dt > 0 {
32			hasGCMarkAssistTime = true
33		}
34	}
35	if !hasSchedWaitTime {
36		t.Error("missing sched wait time")
37	}
38	if !hasSyncBlockTime {
39		t.Error("missing sync block time")
40	}
41	if !hasGCMarkAssistTime {
42		t.Error("missing GC mark assist time")
43	}
44}
45
46func TestSummarizeGoroutinesRegionsTrace(t *testing.T) {
47	summaries := summarizeTraceTest(t, "testdata/tests/go122-annotations.test").Goroutines
48	type region struct {
49		startKind trace.EventKind
50		endKind   trace.EventKind
51	}
52	wantRegions := map[string]region{
53		// N.B. "pre-existing region" never even makes it into the trace.
54		//
55		// TODO(mknyszek): Add test case for end-without-a-start, which can happen at
56		// a generation split only.
57		"":                     {trace.EventStateTransition, trace.EventStateTransition}, // Task inheritance marker.
58		"task0 region":         {trace.EventRegionBegin, trace.EventBad},
59		"region0":              {trace.EventRegionBegin, trace.EventRegionEnd},
60		"region1":              {trace.EventRegionBegin, trace.EventRegionEnd},
61		"unended region":       {trace.EventRegionBegin, trace.EventStateTransition},
62		"post-existing region": {trace.EventRegionBegin, trace.EventBad},
63	}
64	for _, summary := range summaries {
65		basicGoroutineSummaryChecks(t, summary)
66		for _, region := range summary.Regions {
67			want, ok := wantRegions[region.Name]
68			if !ok {
69				continue
70			}
71			checkRegionEvents(t, want.startKind, want.endKind, summary.ID, region)
72			delete(wantRegions, region.Name)
73		}
74	}
75	if len(wantRegions) != 0 {
76		t.Errorf("failed to find regions: %#v", wantRegions)
77	}
78}
79
80func TestSummarizeTasksTrace(t *testing.T) {
81	summaries := summarizeTraceTest(t, "testdata/tests/go122-annotations-stress.test").Tasks
82	type task struct {
83		name       string
84		parent     *trace.TaskID
85		children   []trace.TaskID
86		logs       []trace.Log
87		goroutines []trace.GoID
88	}
89	parent := func(id trace.TaskID) *trace.TaskID {
90		p := new(trace.TaskID)
91		*p = id
92		return p
93	}
94	wantTasks := map[trace.TaskID]task{
95		trace.BackgroundTask: {
96			// The background task (0) is never any task's parent.
97			logs: []trace.Log{
98				{Task: trace.BackgroundTask, Category: "log", Message: "before do"},
99				{Task: trace.BackgroundTask, Category: "log", Message: "before do"},
100			},
101			goroutines: []trace.GoID{1},
102		},
103		1: {
104			// This started before tracing started and has no parents.
105			// Task 2 is technically a child, but we lost that information.
106			children: []trace.TaskID{3, 7, 16},
107			logs: []trace.Log{
108				{Task: 1, Category: "log", Message: "before do"},
109				{Task: 1, Category: "log", Message: "before do"},
110			},
111			goroutines: []trace.GoID{1},
112		},
113		2: {
114			// This started before tracing started and its parent is technically (1), but that information was lost.
115			children: []trace.TaskID{8, 17},
116			logs: []trace.Log{
117				{Task: 2, Category: "log", Message: "before do"},
118				{Task: 2, Category: "log", Message: "before do"},
119			},
120			goroutines: []trace.GoID{1},
121		},
122		3: {
123			parent:   parent(1),
124			children: []trace.TaskID{10, 19},
125			logs: []trace.Log{
126				{Task: 3, Category: "log", Message: "before do"},
127				{Task: 3, Category: "log", Message: "before do"},
128			},
129			goroutines: []trace.GoID{1},
130		},
131		4: {
132			// Explicitly, no parent.
133			children: []trace.TaskID{12, 21},
134			logs: []trace.Log{
135				{Task: 4, Category: "log", Message: "before do"},
136				{Task: 4, Category: "log", Message: "before do"},
137			},
138			goroutines: []trace.GoID{1},
139		},
140		12: {
141			parent:   parent(4),
142			children: []trace.TaskID{13},
143			logs: []trace.Log{
144				// TODO(mknyszek): This is computed asynchronously in the trace,
145				// which makes regenerating this test very annoying, since it will
146				// likely break this test. Resolve this by making the order not matter.
147				{Task: 12, Category: "log2", Message: "do"},
148				{Task: 12, Category: "log", Message: "fanout region4"},
149				{Task: 12, Category: "log", Message: "fanout region0"},
150				{Task: 12, Category: "log", Message: "fanout region1"},
151				{Task: 12, Category: "log", Message: "fanout region2"},
152				{Task: 12, Category: "log", Message: "before do"},
153				{Task: 12, Category: "log", Message: "fanout region3"},
154			},
155			goroutines: []trace.GoID{1, 5, 6, 7, 8, 9},
156		},
157		13: {
158			// Explicitly, no children.
159			parent: parent(12),
160			logs: []trace.Log{
161				{Task: 13, Category: "log2", Message: "do"},
162			},
163			goroutines: []trace.GoID{7},
164		},
165	}
166	for id, summary := range summaries {
167		want, ok := wantTasks[id]
168		if !ok {
169			continue
170		}
171		if id != summary.ID {
172			t.Errorf("ambiguous task %d (or %d?): field likely set incorrectly", id, summary.ID)
173		}
174
175		// Check parent.
176		if want.parent != nil {
177			if summary.Parent == nil {
178				t.Errorf("expected parent %d for task %d without a parent", *want.parent, id)
179			} else if summary.Parent.ID != *want.parent {
180				t.Errorf("bad parent for task %d: want %d, got %d", id, *want.parent, summary.Parent.ID)
181			}
182		} else if summary.Parent != nil {
183			t.Errorf("unexpected parent %d for task %d", summary.Parent.ID, id)
184		}
185
186		// Check children.
187		gotChildren := make(map[trace.TaskID]struct{})
188		for _, child := range summary.Children {
189			gotChildren[child.ID] = struct{}{}
190		}
191		for _, wantChild := range want.children {
192			if _, ok := gotChildren[wantChild]; ok {
193				delete(gotChildren, wantChild)
194			} else {
195				t.Errorf("expected child task %d for task %d not found", wantChild, id)
196			}
197		}
198		if len(gotChildren) != 0 {
199			for child := range gotChildren {
200				t.Errorf("unexpected child task %d for task %d", child, id)
201			}
202		}
203
204		// Check logs.
205		if len(want.logs) != len(summary.Logs) {
206			t.Errorf("wanted %d logs for task %d, got %d logs instead", len(want.logs), id, len(summary.Logs))
207		} else {
208			for i := range want.logs {
209				if want.logs[i] != summary.Logs[i].Log() {
210					t.Errorf("log mismatch: want %#v, got %#v", want.logs[i], summary.Logs[i].Log())
211				}
212			}
213		}
214
215		// Check goroutines.
216		if len(want.goroutines) != len(summary.Goroutines) {
217			t.Errorf("wanted %d goroutines for task %d, got %d goroutines instead", len(want.goroutines), id, len(summary.Goroutines))
218		} else {
219			for _, goid := range want.goroutines {
220				g, ok := summary.Goroutines[goid]
221				if !ok {
222					t.Errorf("want goroutine %d for task %d, not found", goid, id)
223					continue
224				}
225				if g.ID != goid {
226					t.Errorf("goroutine summary for %d does not match task %d listing of %d", g.ID, id, goid)
227				}
228			}
229		}
230
231		// Marked as seen.
232		delete(wantTasks, id)
233	}
234	if len(wantTasks) != 0 {
235		t.Errorf("failed to find tasks: %#v", wantTasks)
236	}
237}
238
239func assertContainsGoroutine(t *testing.T, summaries map[trace.GoID]*trace.GoroutineSummary, name string) {
240	for _, summary := range summaries {
241		if summary.Name == name {
242			return
243		}
244	}
245	t.Errorf("missing goroutine %s", name)
246}
247
248func basicGoroutineSummaryChecks(t *testing.T, summary *trace.GoroutineSummary) {
249	if summary.ID == trace.NoGoroutine {
250		t.Error("summary found for no goroutine")
251		return
252	}
253	if (summary.StartTime != 0 && summary.CreationTime > summary.StartTime) ||
254		(summary.StartTime != 0 && summary.EndTime != 0 && summary.StartTime > summary.EndTime) {
255		t.Errorf("bad summary creation/start/end times for G %d: creation=%d start=%d end=%d", summary.ID, summary.CreationTime, summary.StartTime, summary.EndTime)
256	}
257	if (summary.PC != 0 && summary.Name == "") || (summary.PC == 0 && summary.Name != "") {
258		t.Errorf("bad name and/or PC for G %d: pc=0x%x name=%q", summary.ID, summary.PC, summary.Name)
259	}
260	basicGoroutineExecStatsChecks(t, &summary.GoroutineExecStats)
261	for _, region := range summary.Regions {
262		basicGoroutineExecStatsChecks(t, &region.GoroutineExecStats)
263	}
264}
265
266func summarizeTraceTest(t *testing.T, testPath string) *trace.Summary {
267	trc, _, err := testtrace.ParseFile(testPath)
268	if err != nil {
269		t.Fatalf("malformed test %s: bad trace file: %v", testPath, err)
270	}
271	// Create the analysis state.
272	s := trace.NewSummarizer()
273
274	// Create a reader.
275	r, err := trace.NewReader(trc)
276	if err != nil {
277		t.Fatalf("failed to create trace reader for %s: %v", testPath, err)
278	}
279	// Process the trace.
280	for {
281		ev, err := r.ReadEvent()
282		if err == io.EOF {
283			break
284		}
285		if err != nil {
286			t.Fatalf("failed to process trace %s: %v", testPath, err)
287		}
288		s.Event(&ev)
289	}
290	return s.Finalize()
291}
292
293func checkRegionEvents(t *testing.T, wantStart, wantEnd trace.EventKind, goid trace.GoID, region *trace.UserRegionSummary) {
294	switch wantStart {
295	case trace.EventBad:
296		if region.Start != nil {
297			t.Errorf("expected nil region start event, got\n%s", region.Start.String())
298		}
299	case trace.EventStateTransition, trace.EventRegionBegin:
300		if region.Start == nil {
301			t.Error("expected non-nil region start event, got nil")
302		}
303		kind := region.Start.Kind()
304		if kind != wantStart {
305			t.Errorf("wanted region start event %s, got %s", wantStart, kind)
306		}
307		if kind == trace.EventRegionBegin {
308			if region.Start.Region().Type != region.Name {
309				t.Errorf("region name mismatch: event has %s, summary has %s", region.Start.Region().Type, region.Name)
310			}
311		} else {
312			st := region.Start.StateTransition()
313			if st.Resource.Kind != trace.ResourceGoroutine {
314				t.Errorf("found region start event for the wrong resource: %s", st.Resource)
315			}
316			if st.Resource.Goroutine() != goid {
317				t.Errorf("found region start event for the wrong resource: wanted goroutine %d, got %s", goid, st.Resource)
318			}
319			if old, _ := st.Goroutine(); old != trace.GoNotExist && old != trace.GoUndetermined {
320				t.Errorf("expected transition from GoNotExist or GoUndetermined, got transition from %s instead", old)
321			}
322		}
323	default:
324		t.Errorf("unexpected want start event type: %s", wantStart)
325	}
326
327	switch wantEnd {
328	case trace.EventBad:
329		if region.End != nil {
330			t.Errorf("expected nil region end event, got\n%s", region.End.String())
331		}
332	case trace.EventStateTransition, trace.EventRegionEnd:
333		if region.End == nil {
334			t.Error("expected non-nil region end event, got nil")
335		}
336		kind := region.End.Kind()
337		if kind != wantEnd {
338			t.Errorf("wanted region end event %s, got %s", wantEnd, kind)
339		}
340		if kind == trace.EventRegionEnd {
341			if region.End.Region().Type != region.Name {
342				t.Errorf("region name mismatch: event has %s, summary has %s", region.End.Region().Type, region.Name)
343			}
344		} else {
345			st := region.End.StateTransition()
346			if st.Resource.Kind != trace.ResourceGoroutine {
347				t.Errorf("found region end event for the wrong resource: %s", st.Resource)
348			}
349			if st.Resource.Goroutine() != goid {
350				t.Errorf("found region end event for the wrong resource: wanted goroutine %d, got %s", goid, st.Resource)
351			}
352			if _, new := st.Goroutine(); new != trace.GoNotExist {
353				t.Errorf("expected transition to GoNotExist, got transition to %s instead", new)
354			}
355		}
356	default:
357		t.Errorf("unexpected want end event type: %s", wantEnd)
358	}
359}
360
361func basicGoroutineExecStatsChecks(t *testing.T, stats *trace.GoroutineExecStats) {
362	if stats.ExecTime < 0 {
363		t.Error("found negative ExecTime")
364	}
365	if stats.SchedWaitTime < 0 {
366		t.Error("found negative SchedWaitTime")
367	}
368	if stats.SyscallTime < 0 {
369		t.Error("found negative SyscallTime")
370	}
371	if stats.SyscallBlockTime < 0 {
372		t.Error("found negative SyscallBlockTime")
373	}
374	if stats.TotalTime < 0 {
375		t.Error("found negative TotalTime")
376	}
377	for reason, dt := range stats.BlockTimeByReason {
378		if dt < 0 {
379			t.Errorf("found negative BlockTimeByReason for %s", reason)
380		}
381	}
382	for name, dt := range stats.RangeTime {
383		if dt < 0 {
384			t.Errorf("found negative RangeTime for range %s", name)
385		}
386	}
387}
388
389func TestRelatedGoroutinesV2Trace(t *testing.T) {
390	testPath := "testdata/tests/go122-gc-stress.test"
391	trc, _, err := testtrace.ParseFile(testPath)
392	if err != nil {
393		t.Fatalf("malformed test %s: bad trace file: %v", testPath, err)
394	}
395
396	// Create a reader.
397	r, err := trace.NewReader(trc)
398	if err != nil {
399		t.Fatalf("failed to create trace reader for %s: %v", testPath, err)
400	}
401
402	// Collect all the events.
403	var events []trace.Event
404	for {
405		ev, err := r.ReadEvent()
406		if err == io.EOF {
407			break
408		}
409		if err != nil {
410			t.Fatalf("failed to process trace %s: %v", testPath, err)
411		}
412		events = append(events, ev)
413	}
414
415	// Test the function.
416	targetg := trace.GoID(86)
417	got := trace.RelatedGoroutinesV2(events, targetg)
418	want := map[trace.GoID]struct{}{
419		trace.GoID(86):  struct{}{}, // N.B. Result includes target.
420		trace.GoID(71):  struct{}{},
421		trace.GoID(25):  struct{}{},
422		trace.GoID(122): struct{}{},
423	}
424	for goid := range got {
425		if _, ok := want[goid]; ok {
426			delete(want, goid)
427		} else {
428			t.Errorf("unexpected goroutine %d found in related goroutines for %d in test %s", goid, targetg, testPath)
429		}
430	}
431	if len(want) != 0 {
432		for goid := range want {
433			t.Errorf("failed to find related goroutine %d for goroutine %d in test %s", goid, targetg, testPath)
434		}
435	}
436}
437