internal/trace: support event constructor for testing

Implement the new APIs described in #74826.

Closes #74826

Change-Id: I6a6a6964229548e9d54e7af95185011e183ee50b
Reviewed-on: https://go-review.googlesource.com/c/go/+/691815
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Felix Geisendörfer 2025-07-02 11:26:17 +02:00
parent eb63ef9d66
commit 54b82e944e
6 changed files with 1243 additions and 37 deletions

View file

@ -766,7 +766,7 @@ var depsRules = `
FMT, internal/trace/version, io, sort, encoding/binary FMT, internal/trace/version, io, sort, encoding/binary
< internal/trace/internal/tracev1; < internal/trace/internal/tracev1;
FMT, encoding/binary, internal/trace/version, internal/trace/internal/tracev1, container/heap, math/rand FMT, encoding/binary, internal/trace/version, internal/trace/internal/tracev1, container/heap, math/rand, regexp
< internal/trace; < internal/trace;
# cmd/trace dependencies. # cmd/trace dependencies.

View file

@ -108,6 +108,16 @@ func (d *dataTable[EI, E]) insert(id EI, data E) error {
return nil return nil
} }
// append adds a new element to the data table and returns its ID.
func (d *dataTable[EI, E]) append(data E) EI {
if d.sparse == nil {
d.sparse = make(map[EI]E)
}
id := EI(len(d.sparse)) + 1
d.sparse[id] = data
return id
}
// compactify attempts to compact sparse into dense. // compactify attempts to compact sparse into dense.
// //
// This is intended to be called only once after insertions are done. // This is intended to be called only once after insertions are done.

View file

@ -5,9 +5,13 @@
package trace package trace
import ( import (
"errors"
"fmt" "fmt"
"io"
"iter" "iter"
"math" "math"
"regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -256,6 +260,19 @@ type Log struct {
Message string Message string
} }
// StackSample is used to construct StackSample events via MakeEvent. There are
// no details associated with it, use EventConfig.Stack instead.
type StackSample struct{}
// MakeStack create a stack from a list of stack frames.
func MakeStack(frames []StackFrame) Stack {
// TODO(felixge): support evTable reuse.
tbl := &evTable{pcs: make(map[uint64]frame)}
tbl.strings.compactify()
tbl.stacks.compactify()
return Stack{table: tbl, id: addStack(tbl, frames)}
}
// Stack represents a stack. It's really a handle to a stack and it's trivially comparable. // Stack represents a stack. It's really a handle to a stack and it's trivially comparable.
// //
// If two Stacks are equal then their Frames are guaranteed to be identical. If they are not // If two Stacks are equal then their Frames are guaranteed to be identical. If they are not
@ -287,6 +304,22 @@ func (s Stack) Frames() iter.Seq[StackFrame] {
} }
} }
// String returns the stack as a human-readable string.
//
// The format of the string is intended for debugging and is subject to change.
func (s Stack) String() string {
var sb strings.Builder
printStack(&sb, "", s.Frames())
return sb.String()
}
func printStack(w io.Writer, prefix string, frames iter.Seq[StackFrame]) {
for f := range frames {
fmt.Fprintf(w, "%s%s @ 0x%x\n", prefix, f.Func, f.PC)
fmt.Fprintf(w, "%s\t%s:%d\n", prefix, f.File, f.Line)
}
}
// NoStack is a sentinel value that can be compared against any Stack value, indicating // NoStack is a sentinel value that can be compared against any Stack value, indicating
// a lack of a stack trace. // a lack of a stack trace.
var NoStack = Stack{} var NoStack = Stack{}
@ -332,9 +365,9 @@ func (e ExperimentalEvent) ArgValue(i int) Value {
} }
if strings.HasSuffix(e.Args[i], "string") { if strings.HasSuffix(e.Args[i], "string") {
s := e.table.strings.mustGet(stringID(e.argValues[i])) s := e.table.strings.mustGet(stringID(e.argValues[i]))
return stringValue(s) return StringValue(s)
} }
return uint64Value(e.argValues[i]) return Uint64Value(e.argValues[i])
} }
// ExperimentalBatch represents a packet of unparsed data along with metadata about that packet. // ExperimentalBatch represents a packet of unparsed data along with metadata about that packet.
@ -346,6 +379,416 @@ type ExperimentalBatch struct {
Data []byte Data []byte
} }
type EventDetails interface {
Metric | Label | Range | StateTransition | Sync | Task | Region | Log | StackSample
}
// EventConfig holds the data for constructing a trace event.
type EventConfig[T EventDetails] struct {
// Time is the timestamp of the event.
Time Time
// Kind is the kind of the event.
Kind EventKind
// Goroutine is the goroutine ID of the event.
Goroutine GoID
// Proc is the proc ID of the event.
Proc ProcID
// Thread is the thread ID of the event.
Thread ThreadID
// Stack is the stack of the event.
Stack Stack
// Details is the kind specific details of the event.
Details T
}
// MakeEvent creates a new trace event from the given configuration.
func MakeEvent[T EventDetails](c EventConfig[T]) (e Event, err error) {
// TODO(felixge): make the evTable reusable.
e = Event{
table: &evTable{pcs: make(map[uint64]frame), sync: sync{freq: 1}},
base: baseEvent{time: c.Time},
ctx: schedCtx{G: c.Goroutine, P: c.Proc, M: c.Thread},
}
defer func() {
// N.b. evSync is not in tracev2.Specs()
if err != nil || e.base.typ == evSync {
return
}
spec := tracev2.Specs()[e.base.typ]
if len(spec.StackIDs) > 0 && c.Stack != NoStack {
// The stack for the main execution context is always the
// first stack listed in StackIDs. Subtract one from this
// because we've peeled away the timestamp argument.
e.base.args[spec.StackIDs[0]-1] = uint64(addStack(e.table, slices.Collect(c.Stack.Frames())))
}
e.table.strings.compactify()
e.table.stacks.compactify()
}()
var defaultKind EventKind
switch c.Kind {
case defaultKind:
return Event{}, fmt.Errorf("the Kind field must be provided")
case EventMetric:
if m, ok := any(c.Details).(Metric); ok {
return makeMetricEvent(e, m)
}
case EventLabel:
if l, ok := any(c.Details).(Label); ok {
return makeLabelEvent(e, l)
}
case EventRangeBegin, EventRangeActive, EventRangeEnd:
if r, ok := any(c.Details).(Range); ok {
return makeRangeEvent(e, c.Kind, r)
}
case EventStateTransition:
if t, ok := any(c.Details).(StateTransition); ok {
return makeStateTransitionEvent(e, t)
}
case EventSync:
if s, ok := any(c.Details).(Sync); ok {
return makeSyncEvent(e, s)
}
case EventTaskBegin, EventTaskEnd:
if t, ok := any(c.Details).(Task); ok {
return makeTaskEvent(e, c.Kind, t)
}
case EventRegionBegin, EventRegionEnd:
if r, ok := any(c.Details).(Region); ok {
return makeRegionEvent(e, c.Kind, r)
}
case EventLog:
if l, ok := any(c.Details).(Log); ok {
return makeLogEvent(e, l)
}
case EventStackSample:
if _, ok := any(c.Details).(StackSample); ok {
return makeStackSampleEvent(e, c.Stack)
}
}
return Event{}, fmt.Errorf("the Kind field %s is incompatible with Details type %T", c.Kind, c.Details)
}
func makeMetricEvent(e Event, m Metric) (Event, error) {
if m.Value.Kind() != ValueUint64 {
return Event{}, fmt.Errorf("metric value must be a uint64, got: %s", m.Value.String())
}
switch m.Name {
case "/sched/gomaxprocs:threads":
e.base.typ = tracev2.EvProcsChange
case "/memory/classes/heap/objects:bytes":
e.base.typ = tracev2.EvHeapAlloc
case "/gc/heap/goal:bytes":
e.base.typ = tracev2.EvHeapGoal
default:
return Event{}, fmt.Errorf("unknown metric name: %s", m.Name)
}
e.base.args[0] = uint64(m.Value.Uint64())
return e, nil
}
func makeLabelEvent(e Event, l Label) (Event, error) {
if l.Resource.Kind != ResourceGoroutine {
return Event{}, fmt.Errorf("resource must be a goroutine: %s", l.Resource)
}
e.base.typ = tracev2.EvGoLabel
e.base.args[0] = uint64(e.table.strings.append(l.Label))
// TODO(felixge): check against sched ctx and return error on mismatch
e.ctx.G = l.Resource.Goroutine()
return e, nil
}
var stwRangeRegexp = regexp.MustCompile(`^stop-the-world \((.*)\)$`)
// TODO(felixge): should this ever manipulate the e ctx? Or just report mismatches?
func makeRangeEvent(e Event, kind EventKind, r Range) (Event, error) {
// TODO(felixge): Should we add dedicated range kinds rather than using
// string names?
switch r.Name {
case "GC concurrent mark phase":
if r.Scope.Kind != ResourceNone {
return Event{}, fmt.Errorf("unexpected scope: %s", r.Scope)
}
switch kind {
case EventRangeBegin:
e.base.typ = tracev2.EvGCBegin
case EventRangeActive:
e.base.typ = tracev2.EvGCActive
case EventRangeEnd:
e.base.typ = tracev2.EvGCEnd
default:
return Event{}, fmt.Errorf("unexpected range kind: %s", kind)
}
case "GC incremental sweep":
if r.Scope.Kind != ResourceProc {
return Event{}, fmt.Errorf("unexpected scope: %s", r.Scope)
}
switch kind {
case EventRangeBegin:
e.base.typ = tracev2.EvGCSweepBegin
e.ctx.P = r.Scope.Proc()
case EventRangeActive:
e.base.typ = tracev2.EvGCSweepActive
e.base.args[0] = uint64(r.Scope.Proc())
case EventRangeEnd:
e.base.typ = tracev2.EvGCSweepEnd
// TODO(felixge): check against sched ctx and return error on mismatch
e.ctx.P = r.Scope.Proc()
default:
return Event{}, fmt.Errorf("unexpected range kind: %s", kind)
}
case "GC mark assist":
if r.Scope.Kind != ResourceGoroutine {
return Event{}, fmt.Errorf("unexpected scope: %s", r.Scope)
}
switch kind {
case EventRangeBegin:
e.base.typ = tracev2.EvGCMarkAssistBegin
e.ctx.G = r.Scope.Goroutine()
case EventRangeActive:
e.base.typ = tracev2.EvGCMarkAssistActive
e.base.args[0] = uint64(r.Scope.Goroutine())
case EventRangeEnd:
e.base.typ = tracev2.EvGCMarkAssistEnd
// TODO(felixge): check against sched ctx and return error on mismatch
e.ctx.G = r.Scope.Goroutine()
default:
return Event{}, fmt.Errorf("unexpected range kind: %s", kind)
}
default:
match := stwRangeRegexp.FindStringSubmatch(r.Name)
if len(match) != 2 {
return Event{}, fmt.Errorf("unexpected range name: %s", r.Name)
}
if r.Scope.Kind != ResourceGoroutine {
return Event{}, fmt.Errorf("unexpected scope: %s", r.Scope)
}
switch kind {
case EventRangeBegin:
e.base.typ = tracev2.EvSTWBegin
// TODO(felixge): check against sched ctx and return error on mismatch
e.ctx.G = r.Scope.Goroutine()
case EventRangeEnd:
e.base.typ = tracev2.EvSTWEnd
// TODO(felixge): check against sched ctx and return error on mismatch
e.ctx.G = r.Scope.Goroutine()
default:
return Event{}, fmt.Errorf("unexpected range kind: %s", kind)
}
e.base.args[0] = uint64(e.table.strings.append(match[1]))
}
return e, nil
}
func makeStateTransitionEvent(e Event, t StateTransition) (Event, error) {
switch t.Resource.Kind {
case ResourceProc:
from, to := ProcState(t.oldState), ProcState(t.newState)
switch {
case from == ProcIdle && to == ProcIdle:
// TODO(felixge): Could this also be a ProcStatus event?
e.base.typ = tracev2.EvProcSteal
e.base.args[0] = uint64(t.Resource.Proc())
e.base.extra(version.Go122)[0] = uint64(tracev2.ProcSyscallAbandoned)
case from == ProcIdle && to == ProcRunning:
e.base.typ = tracev2.EvProcStart
e.base.args[0] = uint64(t.Resource.Proc())
case from == ProcRunning && to == ProcIdle:
e.base.typ = tracev2.EvProcStop
if t.Resource.Proc() != e.ctx.P {
e.base.typ = tracev2.EvProcSteal
e.base.args[0] = uint64(t.Resource.Proc())
}
default:
e.base.typ = tracev2.EvProcStatus
e.base.args[0] = uint64(t.Resource.Proc())
e.base.args[1] = uint64(procState2Tracev2ProcStatus[to])
e.base.extra(version.Go122)[0] = uint64(procState2Tracev2ProcStatus[from])
return e, nil
}
case ResourceGoroutine:
from, to := GoState(t.oldState), GoState(t.newState)
stack := slices.Collect(t.Stack.Frames())
goroutine := t.Resource.Goroutine()
if (from == GoUndetermined || from == to) && from != GoNotExist {
e.base.typ = tracev2.EvGoStatus
if len(stack) > 0 {
e.base.typ = tracev2.EvGoStatusStack
}
e.base.args[0] = uint64(goroutine)
e.base.args[2] = uint64(from)<<32 | uint64(goState2Tracev2GoStatus[to])
} else {
switch from {
case GoNotExist:
switch to {
case GoWaiting:
e.base.typ = tracev2.EvGoCreateBlocked
e.base.args[0] = uint64(goroutine)
e.base.args[1] = uint64(addStack(e.table, stack))
case GoRunnable:
e.base.typ = tracev2.EvGoCreate
e.base.args[0] = uint64(goroutine)
e.base.args[1] = uint64(addStack(e.table, stack))
case GoSyscall:
e.base.typ = tracev2.EvGoCreateSyscall
e.base.args[0] = uint64(goroutine)
default:
return Event{}, fmt.Errorf("unexpected transition: %s -> %s", from, to)
}
case GoRunnable:
e.base.typ = tracev2.EvGoStart
e.base.args[0] = uint64(goroutine)
case GoRunning:
switch to {
case GoNotExist:
e.base.typ = tracev2.EvGoDestroy
e.ctx.G = goroutine
case GoRunnable:
e.base.typ = tracev2.EvGoStop
e.ctx.G = goroutine
e.base.args[0] = uint64(e.table.strings.append(t.Reason))
case GoWaiting:
e.base.typ = tracev2.EvGoBlock
e.ctx.G = goroutine
e.base.args[0] = uint64(e.table.strings.append(t.Reason))
case GoSyscall:
e.base.typ = tracev2.EvGoSyscallBegin
e.ctx.G = goroutine
default:
return Event{}, fmt.Errorf("unexpected transition: %s -> %s", from, to)
}
case GoSyscall:
switch to {
case GoNotExist:
e.base.typ = tracev2.EvGoDestroySyscall
e.ctx.G = goroutine
case GoRunning:
e.base.typ = tracev2.EvGoSyscallEnd
e.ctx.G = goroutine
case GoRunnable:
e.base.typ = tracev2.EvGoSyscallEndBlocked
e.ctx.G = goroutine
default:
return Event{}, fmt.Errorf("unexpected transition: %s -> %s", from, to)
}
case GoWaiting:
switch to {
case GoRunnable:
e.base.typ = tracev2.EvGoUnblock
e.base.args[0] = uint64(goroutine)
default:
return Event{}, fmt.Errorf("unexpected transition: %s -> %s", from, to)
}
default:
return Event{}, fmt.Errorf("unexpected transition: %s -> %s", from, to)
}
}
default:
return Event{}, fmt.Errorf("unsupported state transition resource: %s", t.Resource)
}
return e, nil
}
func makeSyncEvent(e Event, s Sync) (Event, error) {
e.base.typ = evSync
e.base.args[0] = uint64(s.N)
if e.table.expBatches == nil {
e.table.expBatches = make(map[tracev2.Experiment][]ExperimentalBatch)
}
for name, batches := range s.ExperimentalBatches {
var found bool
for id, exp := range tracev2.Experiments() {
if exp == name {
found = true
e.table.expBatches[tracev2.Experiment(id)] = batches
break
}
}
if !found {
return Event{}, fmt.Errorf("unknown experiment: %s", name)
}
}
if s.ClockSnapshot != nil {
e.table.hasClockSnapshot = true
e.table.snapWall = s.ClockSnapshot.Wall
e.table.snapMono = s.ClockSnapshot.Mono
// N.b. MakeEvent sets e.table.freq to 1.
e.table.snapTime = timestamp(s.ClockSnapshot.Trace)
}
return e, nil
}
func makeTaskEvent(e Event, kind EventKind, t Task) (Event, error) {
if t.ID == NoTask {
return Event{}, errors.New("task ID cannot be NoTask")
}
e.base.args[0] = uint64(t.ID)
switch kind {
case EventTaskBegin:
e.base.typ = tracev2.EvUserTaskBegin
e.base.args[1] = uint64(t.Parent)
e.base.args[2] = uint64(e.table.strings.append(t.Type))
case EventTaskEnd:
e.base.typ = tracev2.EvUserTaskEnd
e.base.extra(version.Go122)[0] = uint64(t.Parent)
e.base.extra(version.Go122)[1] = uint64(e.table.addExtraString(t.Type))
default:
// TODO(felixge): also do this for ranges?
panic("unexpected task kind")
}
return e, nil
}
func makeRegionEvent(e Event, kind EventKind, r Region) (Event, error) {
e.base.args[0] = uint64(r.Task)
e.base.args[1] = uint64(e.table.strings.append(r.Type))
switch kind {
case EventRegionBegin:
e.base.typ = tracev2.EvUserRegionBegin
case EventRegionEnd:
e.base.typ = tracev2.EvUserRegionEnd
default:
panic("unexpected region kind")
}
return e, nil
}
func makeLogEvent(e Event, l Log) (Event, error) {
e.base.typ = tracev2.EvUserLog
e.base.args[0] = uint64(l.Task)
e.base.args[1] = uint64(e.table.strings.append(l.Category))
e.base.args[2] = uint64(e.table.strings.append(l.Message))
return e, nil
}
func makeStackSampleEvent(e Event, s Stack) (Event, error) {
e.base.typ = tracev2.EvCPUSample
frames := slices.Collect(s.Frames())
e.base.args[0] = uint64(addStack(e.table, frames))
return e, nil
}
func addStack(table *evTable, frames []StackFrame) stackID {
var pcs []uint64
for _, f := range frames {
table.pcs[f.PC] = frame{
pc: f.PC,
funcID: table.strings.append(f.Func),
fileID: table.strings.append(f.File),
line: f.Line,
}
pcs = append(pcs, f.PC)
}
return table.stacks.append(stack{pcs: pcs})
}
// Event represents a single event in the trace. // Event represents a single event in the trace.
type Event struct { type Event struct {
table *evTable table *evTable
@ -435,13 +878,13 @@ func (e Event) Metric() Metric {
switch e.base.typ { switch e.base.typ {
case tracev2.EvProcsChange: case tracev2.EvProcsChange:
m.Name = "/sched/gomaxprocs:threads" m.Name = "/sched/gomaxprocs:threads"
m.Value = uint64Value(e.base.args[0]) m.Value = Uint64Value(e.base.args[0])
case tracev2.EvHeapAlloc: case tracev2.EvHeapAlloc:
m.Name = "/memory/classes/heap/objects:bytes" m.Name = "/memory/classes/heap/objects:bytes"
m.Value = uint64Value(e.base.args[0]) m.Value = Uint64Value(e.base.args[0])
case tracev2.EvHeapGoal: case tracev2.EvHeapGoal:
m.Name = "/gc/heap/goal:bytes" m.Name = "/gc/heap/goal:bytes"
m.Value = uint64Value(e.base.args[0]) m.Value = Uint64Value(e.base.args[0])
default: default:
panic(fmt.Sprintf("internal error: unexpected wire-format event type for Metric kind: %d", e.base.typ)) panic(fmt.Sprintf("internal error: unexpected wire-format event type for Metric kind: %d", e.base.typ))
} }
@ -516,11 +959,11 @@ func (e Event) RangeAttributes() []RangeAttribute {
return []RangeAttribute{ return []RangeAttribute{
{ {
Name: "bytes swept", Name: "bytes swept",
Value: uint64Value(e.base.args[0]), Value: Uint64Value(e.base.args[0]),
}, },
{ {
Name: "bytes reclaimed", Name: "bytes reclaimed",
Value: uint64Value(e.base.args[1]), Value: Uint64Value(e.base.args[1]),
}, },
} }
} }
@ -594,9 +1037,9 @@ func (e Event) StateTransition() StateTransition {
var s StateTransition var s StateTransition
switch e.base.typ { switch e.base.typ {
case tracev2.EvProcStart: case tracev2.EvProcStart:
s = procStateTransition(ProcID(e.base.args[0]), ProcIdle, ProcRunning) s = MakeProcStateTransition(ProcID(e.base.args[0]), ProcIdle, ProcRunning)
case tracev2.EvProcStop: case tracev2.EvProcStop:
s = procStateTransition(e.ctx.P, ProcRunning, ProcIdle) s = MakeProcStateTransition(e.ctx.P, ProcRunning, ProcIdle)
case tracev2.EvProcSteal: case tracev2.EvProcSteal:
// N.B. ordering.advance populates e.base.extra. // N.B. ordering.advance populates e.base.extra.
beforeState := ProcRunning beforeState := ProcRunning
@ -607,49 +1050,50 @@ func (e Event) StateTransition() StateTransition {
// transition. // transition.
beforeState = ProcIdle beforeState = ProcIdle
} }
s = procStateTransition(ProcID(e.base.args[0]), beforeState, ProcIdle) s = MakeProcStateTransition(ProcID(e.base.args[0]), beforeState, ProcIdle)
case tracev2.EvProcStatus: case tracev2.EvProcStatus:
// N.B. ordering.advance populates e.base.extra. // N.B. ordering.advance populates e.base.extra.
s = procStateTransition(ProcID(e.base.args[0]), ProcState(e.base.extra(version.Go122)[0]), tracev2ProcStatus2ProcState[e.base.args[1]]) s = MakeProcStateTransition(ProcID(e.base.args[0]), ProcState(e.base.extra(version.Go122)[0]), tracev2ProcStatus2ProcState[e.base.args[1]])
case tracev2.EvGoCreate, tracev2.EvGoCreateBlocked: case tracev2.EvGoCreate, tracev2.EvGoCreateBlocked:
status := GoRunnable status := GoRunnable
if e.base.typ == tracev2.EvGoCreateBlocked { if e.base.typ == tracev2.EvGoCreateBlocked {
status = GoWaiting status = GoWaiting
} }
s = goStateTransition(GoID(e.base.args[0]), GoNotExist, status) s = MakeGoStateTransition(GoID(e.base.args[0]), GoNotExist, status)
s.Stack = Stack{table: e.table, id: stackID(e.base.args[1])} s.Stack = Stack{table: e.table, id: stackID(e.base.args[1])}
case tracev2.EvGoCreateSyscall: case tracev2.EvGoCreateSyscall:
s = goStateTransition(GoID(e.base.args[0]), GoNotExist, GoSyscall) s = MakeGoStateTransition(GoID(e.base.args[0]), GoNotExist, GoSyscall)
case tracev2.EvGoStart: case tracev2.EvGoStart:
s = goStateTransition(GoID(e.base.args[0]), GoRunnable, GoRunning) s = MakeGoStateTransition(GoID(e.base.args[0]), GoRunnable, GoRunning)
case tracev2.EvGoDestroy: case tracev2.EvGoDestroy:
s = goStateTransition(e.ctx.G, GoRunning, GoNotExist) s = MakeGoStateTransition(e.ctx.G, GoRunning, GoNotExist)
case tracev2.EvGoDestroySyscall: case tracev2.EvGoDestroySyscall:
s = goStateTransition(e.ctx.G, GoSyscall, GoNotExist) s = MakeGoStateTransition(e.ctx.G, GoSyscall, GoNotExist)
case tracev2.EvGoStop: case tracev2.EvGoStop:
s = goStateTransition(e.ctx.G, GoRunning, GoRunnable) s = MakeGoStateTransition(e.ctx.G, GoRunning, GoRunnable)
s.Reason = e.table.strings.mustGet(stringID(e.base.args[0])) s.Reason = e.table.strings.mustGet(stringID(e.base.args[0]))
s.Stack = e.Stack() // This event references the resource the event happened on. s.Stack = e.Stack() // This event references the resource the event happened on.
case tracev2.EvGoBlock: case tracev2.EvGoBlock:
s = goStateTransition(e.ctx.G, GoRunning, GoWaiting) s = MakeGoStateTransition(e.ctx.G, GoRunning, GoWaiting)
s.Reason = e.table.strings.mustGet(stringID(e.base.args[0])) s.Reason = e.table.strings.mustGet(stringID(e.base.args[0]))
s.Stack = e.Stack() // This event references the resource the event happened on. s.Stack = e.Stack() // This event references the resource the event happened on.
case tracev2.EvGoUnblock, tracev2.EvGoSwitch, tracev2.EvGoSwitchDestroy: case tracev2.EvGoUnblock, tracev2.EvGoSwitch, tracev2.EvGoSwitchDestroy:
// N.B. GoSwitch and GoSwitchDestroy both emit additional events, but // N.B. GoSwitch and GoSwitchDestroy both emit additional events, but
// the first thing they both do is unblock the goroutine they name, // the first thing they both do is unblock the goroutine they name,
// identically to an unblock event (even their arguments match). // identically to an unblock event (even their arguments match).
s = goStateTransition(GoID(e.base.args[0]), GoWaiting, GoRunnable) s = MakeGoStateTransition(GoID(e.base.args[0]), GoWaiting, GoRunnable)
case tracev2.EvGoSyscallBegin: case tracev2.EvGoSyscallBegin:
s = goStateTransition(e.ctx.G, GoRunning, GoSyscall) s = MakeGoStateTransition(e.ctx.G, GoRunning, GoSyscall)
s.Stack = e.Stack() // This event references the resource the event happened on. s.Stack = e.Stack() // This event references the resource the event happened on.
case tracev2.EvGoSyscallEnd: case tracev2.EvGoSyscallEnd:
s = goStateTransition(e.ctx.G, GoSyscall, GoRunning) s = MakeGoStateTransition(e.ctx.G, GoSyscall, GoRunning)
case tracev2.EvGoSyscallEndBlocked: case tracev2.EvGoSyscallEndBlocked:
s = goStateTransition(e.ctx.G, GoSyscall, GoRunnable) s = MakeGoStateTransition(e.ctx.G, GoSyscall, GoRunnable)
case tracev2.EvGoStatus, tracev2.EvGoStatusStack: case tracev2.EvGoStatus, tracev2.EvGoStatusStack:
packedStatus := e.base.args[2] packedStatus := e.base.args[2]
from, to := packedStatus>>32, packedStatus&((1<<32)-1) from, to := packedStatus>>32, packedStatus&((1<<32)-1)
s = goStateTransition(GoID(e.base.args[0]), GoState(from), tracev2GoStatus2GoState[to]) s = MakeGoStateTransition(GoID(e.base.args[0]), GoState(from), tracev2GoStatus2GoState[to])
s.Stack = e.Stack() // This event references the resource the event happened on.
default: default:
panic(fmt.Sprintf("internal error: unexpected wire-format event type for StateTransition kind: %d", e.base.typ)) panic(fmt.Sprintf("internal error: unexpected wire-format event type for StateTransition kind: %d", e.base.typ))
} }
@ -793,6 +1237,13 @@ var tracev2GoStatus2GoState = [...]GoState{
tracev2.GoSyscall: GoSyscall, tracev2.GoSyscall: GoSyscall,
} }
var goState2Tracev2GoStatus = [...]tracev2.GoStatus{
GoRunnable: tracev2.GoRunnable,
GoRunning: tracev2.GoRunning,
GoWaiting: tracev2.GoWaiting,
GoSyscall: tracev2.GoSyscall,
}
var tracev2ProcStatus2ProcState = [...]ProcState{ var tracev2ProcStatus2ProcState = [...]ProcState{
tracev2.ProcRunning: ProcRunning, tracev2.ProcRunning: ProcRunning,
tracev2.ProcIdle: ProcIdle, tracev2.ProcIdle: ProcIdle,
@ -800,6 +1251,12 @@ var tracev2ProcStatus2ProcState = [...]ProcState{
tracev2.ProcSyscallAbandoned: ProcIdle, tracev2.ProcSyscallAbandoned: ProcIdle,
} }
var procState2Tracev2ProcStatus = [...]tracev2.ProcStatus{
ProcRunning: tracev2.ProcRunning,
ProcIdle: tracev2.ProcIdle,
// TODO(felixge): how to map ProcSyscall and ProcSyscallAbandoned?
}
// String returns the event as a human-readable string. // String returns the event as a human-readable string.
// //
// The format of the string is intended for debugging and is subject to change. // The format of the string is intended for debugging and is subject to change.
@ -857,10 +1314,7 @@ func (e Event) String() string {
if s.Stack != NoStack { if s.Stack != NoStack {
fmt.Fprintln(&sb) fmt.Fprintln(&sb)
fmt.Fprintln(&sb, "TransitionStack=") fmt.Fprintln(&sb, "TransitionStack=")
for f := range s.Stack.Frames() { printStack(&sb, "\t", s.Stack.Frames())
fmt.Fprintf(&sb, "\t%s @ 0x%x\n", f.Func, f.PC)
fmt.Fprintf(&sb, "\t\t%s:%d\n", f.File, f.Line)
}
} }
case EventExperimental: case EventExperimental:
r := e.Experimental() r := e.Experimental()
@ -886,10 +1340,7 @@ func (e Event) String() string {
if stk := e.Stack(); stk != NoStack { if stk := e.Stack(); stk != NoStack {
fmt.Fprintln(&sb) fmt.Fprintln(&sb)
fmt.Fprintln(&sb, "Stack=") fmt.Fprintln(&sb, "Stack=")
for f := range stk.Frames() { printStack(&sb, "\t", stk.Frames())
fmt.Fprintf(&sb, "\t%s @ 0x%x\n", f.Func, f.PC)
fmt.Fprintf(&sb, "\t\t%s:%d\n", f.File, f.Line)
}
} }
return sb.String() return sb.String()
} }

View file

@ -4,7 +4,748 @@
package trace package trace
import "testing" 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) { func TestPanicEvent(t *testing.T) {
// Use a sync event for this because it doesn't have any extra metadata. // Use a sync event for this because it doesn't have any extra metadata.

View file

@ -228,7 +228,8 @@ type StateTransition struct {
newState uint8 newState uint8
} }
func goStateTransition(id GoID, from, to GoState) StateTransition { // MakeGoStateTransition creates a goroutine state transition.
func MakeGoStateTransition(id GoID, from, to GoState) StateTransition {
return StateTransition{ return StateTransition{
Resource: ResourceID{Kind: ResourceGoroutine, id: int64(id)}, Resource: ResourceID{Kind: ResourceGoroutine, id: int64(id)},
oldState: uint8(from), oldState: uint8(from),
@ -236,7 +237,8 @@ func goStateTransition(id GoID, from, to GoState) StateTransition {
} }
} }
func procStateTransition(id ProcID, from, to ProcState) StateTransition { // MakeProcStateTransition creates a proc state transition.
func MakeProcStateTransition(id ProcID, from, to ProcState) StateTransition {
return StateTransition{ return StateTransition{
Resource: ResourceID{Kind: ResourceProc, id: int64(id)}, Resource: ResourceID{Kind: ResourceProc, id: int64(id)},
oldState: uint8(from), oldState: uint8(from),

View file

@ -58,10 +58,12 @@ func (v Value) String() string {
return "Value{Bad}" return "Value{Bad}"
} }
func uint64Value(x uint64) Value { // Uint64Value creates a value of kind ValueUint64.
func Uint64Value(x uint64) Value {
return Value{kind: ValueUint64, scalar: x} return Value{kind: ValueUint64, scalar: x}
} }
func stringValue(s string) Value { // StringValue creates a value of kind ValueString.
func StringValue(s string) Value {
return Value{kind: ValueString, scalar: uint64(len(s)), pointer: unsafe.Pointer(unsafe.StringData(s))} return Value{kind: ValueString, scalar: uint64(len(s)), pointer: unsafe.Pointer(unsafe.StringData(s))}
} }