blob: 1ae01af2c19f7cc043c0a19454df5c61d1edc019 [file] [log] [blame]
// 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()
}