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>
FMT, internal/trace/version, io, sort, encoding/binary
< 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;
# cmd/trace dependencies.
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.
//
// This is intended to be called only once after insertions are done.
package trace
import (
+ "errors"
"fmt"
+ "io"
"iter"
"math"
+ "regexp"
+ "slices"
"strconv"
"strings"
"time"
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.
//
// If two Stacks are equal then their Frames are guaranteed to be identical. If they are not
}
}
+// 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
// a lack of a stack trace.
var NoStack = Stack{}
}
if strings.HasSuffix(e.Args[i], "string") {
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.
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.
type Event struct {
table *evTable
switch e.base.typ {
case tracev2.EvProcsChange:
m.Name = "/sched/gomaxprocs:threads"
- m.Value = uint64Value(e.base.args[0])
+ m.Value = Uint64Value(e.base.args[0])
case tracev2.EvHeapAlloc:
m.Name = "/memory/classes/heap/objects:bytes"
- m.Value = uint64Value(e.base.args[0])
+ m.Value = Uint64Value(e.base.args[0])
case tracev2.EvHeapGoal:
m.Name = "/gc/heap/goal:bytes"
- m.Value = uint64Value(e.base.args[0])
+ m.Value = Uint64Value(e.base.args[0])
default:
panic(fmt.Sprintf("internal error: unexpected wire-format event type for Metric kind: %d", e.base.typ))
}
return []RangeAttribute{
{
Name: "bytes swept",
- Value: uint64Value(e.base.args[0]),
+ Value: Uint64Value(e.base.args[0]),
},
{
Name: "bytes reclaimed",
- Value: uint64Value(e.base.args[1]),
+ Value: Uint64Value(e.base.args[1]),
},
}
}
var s StateTransition
switch e.base.typ {
case tracev2.EvProcStart:
- s = procStateTransition(ProcID(e.base.args[0]), ProcIdle, ProcRunning)
+ s = MakeProcStateTransition(ProcID(e.base.args[0]), ProcIdle, ProcRunning)
case tracev2.EvProcStop:
- s = procStateTransition(e.ctx.P, ProcRunning, ProcIdle)
+ s = MakeProcStateTransition(e.ctx.P, ProcRunning, ProcIdle)
case tracev2.EvProcSteal:
// N.B. ordering.advance populates e.base.extra.
beforeState := ProcRunning
// transition.
beforeState = ProcIdle
}
- s = procStateTransition(ProcID(e.base.args[0]), beforeState, ProcIdle)
+ s = MakeProcStateTransition(ProcID(e.base.args[0]), beforeState, ProcIdle)
case tracev2.EvProcStatus:
// 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:
status := GoRunnable
if e.base.typ == tracev2.EvGoCreateBlocked {
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])}
case tracev2.EvGoCreateSyscall:
- s = goStateTransition(GoID(e.base.args[0]), GoNotExist, GoSyscall)
+ s = MakeGoStateTransition(GoID(e.base.args[0]), GoNotExist, GoSyscall)
case tracev2.EvGoStart:
- s = goStateTransition(GoID(e.base.args[0]), GoRunnable, GoRunning)
+ s = MakeGoStateTransition(GoID(e.base.args[0]), GoRunnable, GoRunning)
case tracev2.EvGoDestroy:
- s = goStateTransition(e.ctx.G, GoRunning, GoNotExist)
+ s = MakeGoStateTransition(e.ctx.G, GoRunning, GoNotExist)
case tracev2.EvGoDestroySyscall:
- s = goStateTransition(e.ctx.G, GoSyscall, GoNotExist)
+ s = MakeGoStateTransition(e.ctx.G, GoSyscall, GoNotExist)
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.Stack = e.Stack() // This event references the resource the event happened on.
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.Stack = e.Stack() // This event references the resource the event happened on.
case tracev2.EvGoUnblock, tracev2.EvGoSwitch, tracev2.EvGoSwitchDestroy:
// N.B. GoSwitch and GoSwitchDestroy both emit additional events, but
// the first thing they both do is unblock the goroutine they name,
// 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:
- 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.
case tracev2.EvGoSyscallEnd:
- s = goStateTransition(e.ctx.G, GoSyscall, GoRunning)
+ s = MakeGoStateTransition(e.ctx.G, GoSyscall, GoRunning)
case tracev2.EvGoSyscallEndBlocked:
- s = goStateTransition(e.ctx.G, GoSyscall, GoRunnable)
+ s = MakeGoStateTransition(e.ctx.G, GoSyscall, GoRunnable)
case tracev2.EvGoStatus, tracev2.EvGoStatusStack:
packedStatus := e.base.args[2]
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:
panic(fmt.Sprintf("internal error: unexpected wire-format event type for StateTransition kind: %d", e.base.typ))
}
tracev2.GoSyscall: GoSyscall,
}
+var goState2Tracev2GoStatus = [...]tracev2.GoStatus{
+ GoRunnable: tracev2.GoRunnable,
+ GoRunning: tracev2.GoRunning,
+ GoWaiting: tracev2.GoWaiting,
+ GoSyscall: tracev2.GoSyscall,
+}
+
var tracev2ProcStatus2ProcState = [...]ProcState{
tracev2.ProcRunning: ProcRunning,
tracev2.ProcIdle: 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.
//
// The format of the string is intended for debugging and is subject to change.
if s.Stack != NoStack {
fmt.Fprintln(&sb)
fmt.Fprintln(&sb, "TransitionStack=")
- for f := range 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)
- }
+ printStack(&sb, "\t", s.Stack.Frames())
}
case EventExperimental:
r := e.Experimental()
if stk := e.Stack(); stk != NoStack {
fmt.Fprintln(&sb)
fmt.Fprintln(&sb, "Stack=")
- for f := range 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)
- }
+ printStack(&sb, "\t", stk.Frames())
}
return sb.String()
}
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) {
// Use a sync event for this because it doesn't have any extra metadata.
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{
Resource: ResourceID{Kind: ResourceGoroutine, id: int64(id)},
oldState: uint8(from),
}
}
-func procStateTransition(id ProcID, from, to ProcState) StateTransition {
+// MakeProcStateTransition creates a proc state transition.
+func MakeProcStateTransition(id ProcID, from, to ProcState) StateTransition {
return StateTransition{
Resource: ResourceID{Kind: ResourceProc, id: int64(id)},
oldState: uint8(from),
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}
}
-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))}
}