]> Cypherpunks repositories - gostls13.git/commitdiff
internal/trace: support event constructor for testing
authorFelix Geisendörfer <felix.geisendoerfer@datadoghq.com>
Wed, 2 Jul 2025 09:26:17 +0000 (11:26 +0200)
committerFelix Geisendörfer <felix.geisendoerfer@datadoghq.com>
Wed, 26 Nov 2025 10:31:15 +0000 (02:31 -0800)
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>

src/go/build/deps_test.go
src/internal/trace/base.go
src/internal/trace/event.go
src/internal/trace/event_test.go
src/internal/trace/resources.go
src/internal/trace/value.go

index b02db785c13a34d4ea26b11eb50645bebea3b56d..295c69425ee52a58befa428597e62e9490d6a22c 100644 (file)
@@ -766,7 +766,7 @@ var depsRules = `
        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.
index 1f17daa5f5349cf3bcaf8423bf30412fddd4105c..a452622973eedfa910466396bca7b7dcb0f5a1d4 100644 (file)
@@ -108,6 +108,16 @@ func (d *dataTable[EI, E]) insert(id EI, data E) error {
        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.
index b78e52329469d31cb8e3ba4d67e59008f948b78d..a891472962234f98c6d64452a1f67281c190d0e6 100644 (file)
@@ -5,9 +5,13 @@
 package trace
 
 import (
+       "errors"
        "fmt"
+       "io"
        "iter"
        "math"
+       "regexp"
+       "slices"
        "strconv"
        "strings"
        "time"
@@ -256,6 +260,19 @@ type Log struct {
        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
@@ -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
 // a lack of a stack trace.
 var NoStack = Stack{}
@@ -332,9 +365,9 @@ func (e ExperimentalEvent) ArgValue(i int) Value {
        }
        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.
@@ -346,6 +379,416 @@ type ExperimentalBatch struct {
        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
@@ -435,13 +878,13 @@ func (e Event) Metric() Metric {
        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))
        }
@@ -516,11 +959,11 @@ func (e Event) RangeAttributes() []RangeAttribute {
        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]),
                },
        }
 }
@@ -594,9 +1037,9 @@ func (e Event) StateTransition() StateTransition {
        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
@@ -607,49 +1050,50 @@ func (e Event) StateTransition() StateTransition {
                        // 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))
        }
@@ -793,6 +1237,13 @@ var tracev2GoStatus2GoState = [...]GoState{
        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,
@@ -800,6 +1251,12 @@ var tracev2ProcStatus2ProcState = [...]ProcState{
        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.
@@ -857,10 +1314,7 @@ func (e Event) String() string {
                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()
@@ -886,10 +1340,7 @@ func (e Event) String() string {
        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()
 }
index d39d6b75bd761381b15016882f3dbba6a492b835..1ae01af2c19f7cc043c0a19454df5c61d1edc019 100644 (file)
@@ -4,7 +4,748 @@
 
 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.
index 24db2f8d77a66a5a02245ad6eb006211c286030a..951159ddbdcac5e0e25ef0ae1d7c86f4a56393de 100644 (file)
@@ -228,7 +228,8 @@ type StateTransition struct {
        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),
@@ -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{
                Resource: ResourceID{Kind: ResourceProc, id: int64(id)},
                oldState: uint8(from),
index fc2808e59753a82da1a55081b2b45959ecd9d3ff..41eeb50d87fe9a67d4b04864456c0d9d6bc019b6 100644 (file)
@@ -58,10 +58,12 @@ func (v Value) String() string {
        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))}
 }