// Copyright 2023 The Go 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 trace import ( "fmt" "internal/diff" "reflect" "slices" "testing" "time" ) func TestMakeEvent(t *testing.T) { checkTime := func(t *testing.T, ev Event, want Time) { t.Helper() if ev.Time() != want { t.Errorf("expected time to be %d, got %d", want, ev.Time()) } } checkValid := func(t *testing.T, err error, valid bool) bool { t.Helper() if valid && err == nil { return true } if valid && err != nil { t.Errorf("expected no error, got %v", err) } else if !valid && err == nil { t.Errorf("expected error, got %v", err) } return false } type stackType string const ( schedStack stackType = "sched stack" stStack stackType = "state transition stack" ) checkStack := func(t *testing.T, got Stack, want Stack, which stackType) { t.Helper() diff := diff.Diff("want", []byte(want.String()), "got", []byte(got.String())) if len(diff) > 0 { t.Errorf("unexpected %s: %s", which, diff) } } stk1 := MakeStack([]StackFrame{ {PC: 1, Func: "foo", File: "foo.go", Line: 10}, {PC: 2, Func: "bar", File: "bar.go", Line: 20}, }) stk2 := MakeStack([]StackFrame{ {PC: 1, Func: "foo", File: "foo.go", Line: 10}, {PC: 2, Func: "bar", File: "bar.go", Line: 20}, }) t.Run("Metric", func(t *testing.T) { tests := []struct { name string metric string val uint64 stack Stack valid bool }{ {name: "gomaxprocs", metric: "/sched/gomaxprocs:threads", valid: true, val: 1, stack: NoStack}, {name: "gomaxprocs with stack", metric: "/sched/gomaxprocs:threads", valid: true, val: 1, stack: stk1}, {name: "heap objects", metric: "/memory/classes/heap/objects:bytes", valid: true, val: 2, stack: NoStack}, {name: "heap goal", metric: "/gc/heap/goal:bytes", valid: true, val: 3, stack: NoStack}, {name: "invalid metric", metric: "/test", valid: false, val: 4, stack: NoStack}, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ev, err := MakeEvent(EventConfig[Metric]{ Kind: EventMetric, Time: Time(42 + i), Details: Metric{Name: test.metric, Value: Uint64Value(test.val)}, Stack: test.stack, }) if !checkValid(t, err, test.valid) { return } checkTime(t, ev, Time(42+i)) checkStack(t, ev.Stack(), test.stack, schedStack) got := ev.Metric() if got.Name != test.metric { t.Errorf("expected name to be %q, got %q", test.metric, got.Name) } if got.Value.Uint64() != test.val { t.Errorf("expected value to be %d, got %d", test.val, got.Value.Uint64()) } }) } }) t.Run("Label", func(t *testing.T) { ev, err := MakeEvent(EventConfig[Label]{ Kind: EventLabel, Time: 42, Details: Label{Label: "test", Resource: MakeResourceID(GoID(23))}, }) if !checkValid(t, err, true) { return } label := ev.Label() if label.Label != "test" { t.Errorf("expected label to be test, got %q", label.Label) } if label.Resource.Kind != ResourceGoroutine { t.Errorf("expected label resource to be goroutine, got %d", label.Resource.Kind) } if label.Resource.id != 23 { t.Errorf("expected label resource to be 23, got %d", label.Resource.id) } checkTime(t, ev, 42) }) t.Run("Range", func(t *testing.T) { tests := []struct { kind EventKind name string scope ResourceID valid bool }{ {kind: EventRangeBegin, name: "GC concurrent mark phase", scope: ResourceID{}, valid: true}, {kind: EventRangeActive, name: "GC concurrent mark phase", scope: ResourceID{}, valid: true}, {kind: EventRangeEnd, name: "GC concurrent mark phase", scope: ResourceID{}, valid: true}, {kind: EventMetric, name: "GC concurrent mark phase", scope: ResourceID{}, valid: false}, {kind: EventRangeBegin, name: "GC concurrent mark phase - INVALID", scope: ResourceID{}, valid: false}, {kind: EventRangeBegin, name: "GC incremental sweep", scope: MakeResourceID(ProcID(1)), valid: true}, {kind: EventRangeActive, name: "GC incremental sweep", scope: MakeResourceID(ProcID(2)), valid: true}, {kind: EventRangeEnd, name: "GC incremental sweep", scope: MakeResourceID(ProcID(3)), valid: true}, {kind: EventMetric, name: "GC incremental sweep", scope: MakeResourceID(ProcID(4)), valid: false}, {kind: EventRangeBegin, name: "GC incremental sweep - INVALID", scope: MakeResourceID(ProcID(5)), valid: false}, {kind: EventRangeBegin, name: "GC mark assist", scope: MakeResourceID(GoID(1)), valid: true}, {kind: EventRangeActive, name: "GC mark assist", scope: MakeResourceID(GoID(2)), valid: true}, {kind: EventRangeEnd, name: "GC mark assist", scope: MakeResourceID(GoID(3)), valid: true}, {kind: EventMetric, name: "GC mark assist", scope: MakeResourceID(GoID(4)), valid: false}, {kind: EventRangeBegin, name: "GC mark assist - INVALID", scope: MakeResourceID(GoID(5)), valid: false}, {kind: EventRangeBegin, name: "stop-the-world (for a good reason)", scope: MakeResourceID(GoID(1)), valid: true}, {kind: EventRangeActive, name: "stop-the-world (for a good reason)", scope: MakeResourceID(GoID(2)), valid: false}, {kind: EventRangeEnd, name: "stop-the-world (for a good reason)", scope: MakeResourceID(GoID(3)), valid: true}, {kind: EventMetric, name: "stop-the-world (for a good reason)", scope: MakeResourceID(GoID(4)), valid: false}, {kind: EventRangeBegin, name: "stop-the-world (for a good reason) - INVALID", scope: MakeResourceID(GoID(5)), valid: false}, } for i, test := range tests { name := fmt.Sprintf("%s/%s/%s", test.kind, test.name, test.scope) t.Run(name, func(t *testing.T) { ev, err := MakeEvent(EventConfig[Range]{ Time: Time(42 + i), Kind: test.kind, Details: Range{Name: test.name, Scope: test.scope}, }) if !checkValid(t, err, test.valid) { return } got := ev.Range() if got.Name != test.name { t.Errorf("expected name to be %q, got %q", test.name, got.Name) } if ev.Kind() != test.kind { t.Errorf("expected kind to be %s, got %s", test.kind, ev.Kind()) } if got.Scope.String() != test.scope.String() { t.Errorf("expected scope to be %s, got %s", test.scope.String(), got.Scope.String()) } checkTime(t, ev, Time(42+i)) }) } }) t.Run("GoroutineTransition", func(t *testing.T) { const anotherG = 999 // indicates hat sched g is different from transition g tests := []struct { name string g GoID stack Stack stG GoID from GoState to GoState reason string stStack Stack valid bool }{ { name: "EvGoCreate", g: anotherG, stack: stk1, stG: 1, from: GoNotExist, to: GoRunnable, reason: "", stStack: stk2, valid: true, }, { name: "EvGoCreateBlocked", g: anotherG, stack: stk1, stG: 2, from: GoNotExist, to: GoWaiting, reason: "", stStack: stk2, valid: true, }, { name: "EvGoCreateSyscall", g: anotherG, stack: NoStack, stG: 3, from: GoNotExist, to: GoSyscall, reason: "", stStack: NoStack, valid: true, }, { name: "EvGoStart", g: anotherG, stack: NoStack, stG: 4, from: GoRunnable, to: GoRunning, reason: "", stStack: NoStack, valid: true, }, { name: "EvGoDestroy", g: 5, stack: NoStack, stG: 5, from: GoRunning, to: GoNotExist, reason: "", stStack: NoStack, valid: true, }, { name: "EvGoDestroySyscall", g: 6, stack: NoStack, stG: 6, from: GoSyscall, to: GoNotExist, reason: "", stStack: NoStack, valid: true, }, { name: "EvGoStop", g: 7, stack: stk1, stG: 7, from: GoRunning, to: GoRunnable, reason: "preempted", stStack: stk1, valid: true, }, { name: "EvGoBlock", g: 8, stack: stk1, stG: 8, from: GoRunning, to: GoWaiting, reason: "blocked", stStack: stk1, valid: true, }, { name: "EvGoUnblock", g: 9, stack: stk1, stG: anotherG, from: GoWaiting, to: GoRunnable, reason: "", stStack: NoStack, valid: true, }, // N.b. EvGoUnblock, EvGoSwitch and EvGoSwitchDestroy cannot be // distinguished from each other in Event form, so MakeEvent only // produces EvGoUnblock events for Waiting -> Runnable transitions. { name: "EvGoSyscallBegin", g: 10, stack: stk1, stG: 10, from: GoRunning, to: GoSyscall, reason: "", stStack: stk1, valid: true, }, { name: "EvGoSyscallEnd", g: 11, stack: NoStack, stG: 11, from: GoSyscall, to: GoRunning, reason: "", stStack: NoStack, valid: true, }, { name: "EvGoSyscallEndBlocked", g: 12, stack: NoStack, stG: 12, from: GoSyscall, to: GoRunnable, reason: "", stStack: NoStack, valid: true, }, // TODO(felixge): Use coverage testsing to check if we need all these GoStatus/GoStatusStack cases { name: "GoStatus Undetermined->Waiting", g: anotherG, stack: NoStack, stG: 13, from: GoUndetermined, to: GoWaiting, reason: "", stStack: NoStack, valid: true, }, { name: "GoStatus Undetermined->Running", g: anotherG, stack: NoStack, stG: 14, from: GoUndetermined, to: GoRunning, reason: "", stStack: NoStack, valid: true, }, { name: "GoStatusStack Undetermined->Waiting", g: anotherG, stack: stk1, stG: 15, from: GoUndetermined, to: GoWaiting, reason: "", stStack: stk1, valid: true, }, { name: "GoStatusStack Undetermined->Runnable", g: anotherG, stack: stk1, stG: 16, from: GoUndetermined, to: GoRunnable, reason: "", stStack: stk1, valid: true, }, { name: "GoStatus Runnable->Runnable", g: anotherG, stack: NoStack, stG: 17, from: GoRunnable, to: GoRunnable, reason: "", stStack: NoStack, valid: true, }, { name: "GoStatus Runnable->Running", g: anotherG, stack: NoStack, stG: 18, from: GoRunnable, to: GoRunning, reason: "", stStack: NoStack, valid: true, }, { name: "invalid NotExits->NotExists", g: anotherG, stack: stk1, stG: 18, from: GoNotExist, to: GoNotExist, reason: "", stStack: NoStack, valid: false, }, { name: "invalid Running->Undetermined", g: anotherG, stack: stk1, stG: 19, from: GoRunning, to: GoUndetermined, reason: "", stStack: NoStack, valid: false, }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { st := MakeGoStateTransition(test.stG, test.from, test.to) st.Stack = test.stStack st.Reason = test.reason ev, err := MakeEvent(EventConfig[StateTransition]{ Kind: EventStateTransition, Time: Time(42 + i), Goroutine: test.g, Stack: test.stack, Details: st, }) if !checkValid(t, err, test.valid) { return } checkStack(t, ev.Stack(), test.stack, schedStack) if ev.Goroutine() != test.g { t.Errorf("expected goroutine to be %d, got %d", test.g, ev.Goroutine()) } got := ev.StateTransition() if got.Resource.Goroutine() != test.stG { t.Errorf("expected resource to be %d, got %d", test.stG, got.Resource.Goroutine()) } from, to := got.Goroutine() if from != test.from { t.Errorf("from got=%s want=%s", from, test.from) } if to != test.to { t.Errorf("to got=%s want=%s", to, test.to) } if got.Reason != test.reason { t.Errorf("expected reason to be %s, got %s", test.reason, got.Reason) } checkStack(t, got.Stack, test.stStack, stStack) checkTime(t, ev, Time(42+i)) }) } }) t.Run("ProcTransition", func(t *testing.T) { tests := []struct { name string proc ProcID schedProc ProcID from ProcState to ProcState valid bool }{ {name: "ProcStart", proc: 1, schedProc: 99, from: ProcIdle, to: ProcRunning, valid: true}, {name: "ProcStop", proc: 2, schedProc: 2, from: ProcRunning, to: ProcIdle, valid: true}, {name: "ProcSteal", proc: 3, schedProc: 99, from: ProcRunning, to: ProcIdle, valid: true}, {name: "ProcSteal lost info", proc: 4, schedProc: 99, from: ProcIdle, to: ProcIdle, valid: true}, {name: "ProcStatus", proc: 5, schedProc: 99, from: ProcUndetermined, to: ProcRunning, valid: true}, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { st := MakeProcStateTransition(test.proc, test.from, test.to) ev, err := MakeEvent(EventConfig[StateTransition]{ Kind: EventStateTransition, Time: Time(42 + i), Proc: test.schedProc, Details: st, }) if !checkValid(t, err, test.valid) { return } checkTime(t, ev, Time(42+i)) gotSt := ev.StateTransition() from, to := gotSt.Proc() if from != test.from { t.Errorf("from got=%s want=%s", from, test.from) } if to != test.to { t.Errorf("to got=%s want=%s", to, test.to) } if ev.Proc() != test.schedProc { t.Errorf("expected proc to be %d, got %d", test.schedProc, ev.Proc()) } if gotSt.Resource.Proc() != test.proc { t.Errorf("expected resource to be %d, got %d", test.proc, gotSt.Resource.Proc()) } }) } }) t.Run("Sync", func(t *testing.T) { tests := []struct { name string kind EventKind n int clock *ClockSnapshot batches map[string][]ExperimentalBatch valid bool }{ { name: "invalid kind", n: 1, valid: false, }, { name: "N", kind: EventSync, n: 1, batches: map[string][]ExperimentalBatch{}, valid: true, }, { name: "N+ClockSnapshot", kind: EventSync, n: 1, batches: map[string][]ExperimentalBatch{}, clock: &ClockSnapshot{ Trace: 1, Wall: time.Unix(59, 123456789), Mono: 2, }, valid: true, }, { name: "N+Batches", kind: EventSync, n: 1, batches: map[string][]ExperimentalBatch{ "AllocFree": {{Thread: 1, Data: []byte{1, 2, 3}}}, }, valid: true, }, { name: "unknown experiment", kind: EventSync, n: 1, batches: map[string][]ExperimentalBatch{ "does-not-exist": {{Thread: 1, Data: []byte{1, 2, 3}}}, }, valid: false, }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ev, err := MakeEvent(EventConfig[Sync]{ Kind: test.kind, Time: Time(42 + i), Details: Sync{N: test.n, ClockSnapshot: test.clock, ExperimentalBatches: test.batches}, }) if !checkValid(t, err, test.valid) { return } got := ev.Sync() checkTime(t, ev, Time(42+i)) if got.N != test.n { t.Errorf("expected N to be %d, got %d", test.n, got.N) } if test.clock != nil && got.ClockSnapshot == nil { t.Fatalf("expected ClockSnapshot to be non-nil") } else if test.clock == nil && got.ClockSnapshot != nil { t.Fatalf("expected ClockSnapshot to be nil") } else if test.clock != nil && got.ClockSnapshot != nil { if got.ClockSnapshot.Trace != test.clock.Trace { t.Errorf("expected ClockSnapshot.Trace to be %d, got %d", test.clock.Trace, got.ClockSnapshot.Trace) } if !got.ClockSnapshot.Wall.Equal(test.clock.Wall) { t.Errorf("expected ClockSnapshot.Wall to be %s, got %s", test.clock.Wall, got.ClockSnapshot.Wall) } if got.ClockSnapshot.Mono != test.clock.Mono { t.Errorf("expected ClockSnapshot.Mono to be %d, got %d", test.clock.Mono, got.ClockSnapshot.Mono) } } if !reflect.DeepEqual(got.ExperimentalBatches, test.batches) { t.Errorf("expected ExperimentalBatches to be %#v, got %#v", test.batches, got.ExperimentalBatches) } }) } }) t.Run("Task", func(t *testing.T) { tests := []struct { name string kind EventKind id TaskID parent TaskID typ string valid bool }{ {name: "no task", kind: EventTaskBegin, id: NoTask, parent: 1, typ: "type-0", valid: false}, {name: "invalid kind", kind: EventMetric, id: 1, parent: 2, typ: "type-1", valid: false}, {name: "EvUserTaskBegin", kind: EventTaskBegin, id: 2, parent: 3, typ: "type-2", valid: true}, {name: "EvUserTaskEnd", kind: EventTaskEnd, id: 3, parent: 4, typ: "type-3", valid: true}, {name: "no parent", kind: EventTaskBegin, id: 4, parent: NoTask, typ: "type-4", valid: true}, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ev, err := MakeEvent(EventConfig[Task]{ Kind: test.kind, Time: Time(42 + i), Details: Task{ID: test.id, Parent: test.parent, Type: test.typ}, }) if !checkValid(t, err, test.valid) { return } checkTime(t, ev, Time(42+i)) got := ev.Task() if got.ID != test.id { t.Errorf("expected ID to be %d, got %d", test.id, got.ID) } if got.Parent != test.parent { t.Errorf("expected Parent to be %d, got %d", test.parent, got.Parent) } if got.Type != test.typ { t.Errorf("expected Type to be %s, got %s", test.typ, got.Type) } }) } }) t.Run("Region", func(t *testing.T) { tests := []struct { name string kind EventKind task TaskID typ string valid bool }{ {name: "invalid kind", kind: EventMetric, task: 1, typ: "type-1", valid: false}, {name: "EvUserRegionBegin", kind: EventRegionBegin, task: 2, typ: "type-2", valid: true}, {name: "EvUserRegionEnd", kind: EventRegionEnd, task: 3, typ: "type-3", valid: true}, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ev, err := MakeEvent(EventConfig[Region]{ Kind: test.kind, Time: Time(42 + i), Details: Region{Task: test.task, Type: test.typ}, }) if !checkValid(t, err, test.valid) { return } checkTime(t, ev, Time(42+i)) got := ev.Region() if got.Task != test.task { t.Errorf("expected Task to be %d, got %d", test.task, got.Task) } if got.Type != test.typ { t.Errorf("expected Type to be %s, got %s", test.typ, got.Type) } }) } }) t.Run("Log", func(t *testing.T) { tests := []struct { name string kind EventKind task TaskID category string message string valid bool }{ {name: "invalid kind", kind: EventMetric, task: 1, category: "category-1", message: "message-1", valid: false}, {name: "basic", kind: EventLog, task: 2, category: "category-2", message: "message-2", valid: true}, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ev, err := MakeEvent(EventConfig[Log]{ Kind: test.kind, Time: Time(42 + i), Details: Log{Task: test.task, Category: test.category, Message: test.message}, }) if !checkValid(t, err, test.valid) { return } checkTime(t, ev, Time(42+i)) got := ev.Log() if got.Task != test.task { t.Errorf("expected Task to be %d, got %d", test.task, got.Task) } if got.Category != test.category { t.Errorf("expected Category to be %s, got %s", test.category, got.Category) } if got.Message != test.message { t.Errorf("expected Message to be %s, got %s", test.message, got.Message) } }) } }) t.Run("StackSample", func(t *testing.T) { tests := []struct { name string kind EventKind stack Stack valid bool }{ {name: "invalid kind", kind: EventMetric, stack: stk1, valid: false}, {name: "basic", kind: EventStackSample, stack: stk1, valid: true}, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ev, err := MakeEvent(EventConfig[StackSample]{ Kind: test.kind, Time: Time(42 + i), Stack: test.stack, // N.b. Details defaults to StackSample{}, so we can // omit it here. }) if !checkValid(t, err, test.valid) { return } checkTime(t, ev, Time(42+i)) got := ev.Stack() checkStack(t, got, test.stack, schedStack) }) } }) } func TestMakeStack(t *testing.T) { frames := []StackFrame{ {PC: 1, Func: "foo", File: "foo.go", Line: 10}, {PC: 2, Func: "bar", File: "bar.go", Line: 20}, } got := slices.Collect(MakeStack(frames).Frames()) if len(got) != len(frames) { t.Errorf("got=%d want=%d", len(got), len(frames)) } for i := range got { if got[i] != frames[i] { t.Errorf("got=%v want=%v", got[i], frames[i]) } } } func TestPanicEvent(t *testing.T) { // Use a sync event for this because it doesn't have any extra metadata. ev := syncEvent(nil, 0, 0) mustPanic(t, func() { _ = ev.Range() }) mustPanic(t, func() { _ = ev.Metric() }) mustPanic(t, func() { _ = ev.Log() }) mustPanic(t, func() { _ = ev.Task() }) mustPanic(t, func() { _ = ev.Region() }) mustPanic(t, func() { _ = ev.Label() }) mustPanic(t, func() { _ = ev.RangeAttributes() }) } func mustPanic(t *testing.T, f func()) { defer func() { if r := recover(); r == nil { t.Fatal("failed to panic") } }() f() }