From: Felix Geisendörfer Date: Wed, 2 Jul 2025 09:26:17 +0000 (+0200) Subject: internal/trace: support event constructor for testing X-Git-Tag: go1.26rc1~97 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=54b82e944ebccb612f4f300c66f9a0230b43b24c;p=gostls13.git internal/trace: support event constructor for testing Implement the new APIs described in #74826. Closes #74826 Change-Id: I6a6a6964229548e9d54e7af95185011e183ee50b Reviewed-on: https://go-review.googlesource.com/c/go/+/691815 Reviewed-by: Michael Knyszek Reviewed-by: Cherry Mui LUCI-TryBot-Result: Go LUCI --- diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index b02db785c1..295c69425e 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -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. diff --git a/src/internal/trace/base.go b/src/internal/trace/base.go index 1f17daa5f5..a452622973 100644 --- a/src/internal/trace/base.go +++ b/src/internal/trace/base.go @@ -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. diff --git a/src/internal/trace/event.go b/src/internal/trace/event.go index b78e523294..a891472962 100644 --- a/src/internal/trace/event.go +++ b/src/internal/trace/event.go @@ -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() } diff --git a/src/internal/trace/event_test.go b/src/internal/trace/event_test.go index d39d6b75bd..1ae01af2c1 100644 --- a/src/internal/trace/event_test.go +++ b/src/internal/trace/event_test.go @@ -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. diff --git a/src/internal/trace/resources.go b/src/internal/trace/resources.go index 24db2f8d77..951159ddbd 100644 --- a/src/internal/trace/resources.go +++ b/src/internal/trace/resources.go @@ -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), diff --git a/src/internal/trace/value.go b/src/internal/trace/value.go index fc2808e597..41eeb50d87 100644 --- a/src/internal/trace/value.go +++ b/src/internal/trace/value.go @@ -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))} }