1
2
3
4
5
6
7
8
9 package testtrace
10
11 import (
12 "errors"
13 "fmt"
14 "golang.org/x/exp/trace"
15 "slices"
16 "strings"
17 )
18
19
20 type Validator struct {
21 lastTs trace.Time
22 gs map[trace.GoID]*goState
23 ps map[trace.ProcID]*procState
24 ms map[trace.ThreadID]*schedContext
25 ranges map[trace.ResourceID][]string
26 tasks map[trace.TaskID]string
27 seenSync bool
28 Go121 bool
29 }
30
31 type schedContext struct {
32 M trace.ThreadID
33 P trace.ProcID
34 G trace.GoID
35 }
36
37 type goState struct {
38 state trace.GoState
39 binding *schedContext
40 }
41
42 type procState struct {
43 state trace.ProcState
44 binding *schedContext
45 }
46
47
48 func NewValidator() *Validator {
49 return &Validator{
50 gs: make(map[trace.GoID]*goState),
51 ps: make(map[trace.ProcID]*procState),
52 ms: make(map[trace.ThreadID]*schedContext),
53 ranges: make(map[trace.ResourceID][]string),
54 tasks: make(map[trace.TaskID]string),
55 }
56 }
57
58
59
60
61 func (v *Validator) Event(ev trace.Event) error {
62 e := new(errAccumulator)
63
64
65 if v.lastTs != 0 {
66 if ev.Time() <= v.lastTs {
67 e.Errorf("timestamp out-of-order for %+v", ev)
68 } else {
69 v.lastTs = ev.Time()
70 }
71 } else {
72 v.lastTs = ev.Time()
73 }
74
75
76 checkStack(e, ev.Stack())
77
78 switch ev.Kind() {
79 case trace.EventSync:
80
81 v.seenSync = true
82 case trace.EventMetric:
83 m := ev.Metric()
84 if !strings.Contains(m.Name, ":") {
85
86 e.Errorf("invalid metric name %q", m.Name)
87 }
88
89 if m.Value.Kind() == trace.ValueBad {
90 e.Errorf("invalid value")
91 }
92 switch m.Value.Kind() {
93 case trace.ValueUint64:
94
95 _ = m.Value.Uint64()
96 }
97 case trace.EventLabel:
98 l := ev.Label()
99
100
101 if l.Label == "" {
102 e.Errorf("invalid label %q", l.Label)
103 }
104
105
106 if l.Resource.Kind == trace.ResourceNone {
107 e.Errorf("label resource none")
108 }
109 switch l.Resource.Kind {
110 case trace.ResourceGoroutine:
111 id := l.Resource.Goroutine()
112 if _, ok := v.gs[id]; !ok {
113 e.Errorf("label for invalid goroutine %d", id)
114 }
115 case trace.ResourceProc:
116 id := l.Resource.Proc()
117 if _, ok := v.ps[id]; !ok {
118 e.Errorf("label for invalid proc %d", id)
119 }
120 case trace.ResourceThread:
121 id := l.Resource.Thread()
122 if _, ok := v.ms[id]; !ok {
123 e.Errorf("label for invalid thread %d", id)
124 }
125 }
126 case trace.EventStackSample:
127
128
129
130 case trace.EventStateTransition:
131
132
133
134
135
136
137 tr := ev.StateTransition()
138 checkStack(e, tr.Stack)
139 switch tr.Resource.Kind {
140 case trace.ResourceGoroutine:
141
142 id := tr.Resource.Goroutine()
143 old, new := tr.Goroutine()
144 if new == trace.GoUndetermined {
145 e.Errorf("transition to undetermined state for goroutine %d", id)
146 }
147 if v.seenSync && old == trace.GoUndetermined {
148 e.Errorf("undetermined goroutine %d after first global sync", id)
149 }
150 if new == trace.GoNotExist && v.hasAnyRange(trace.MakeResourceID(id)) {
151 e.Errorf("goroutine %d died with active ranges", id)
152 }
153 state, ok := v.gs[id]
154 if ok {
155 if old != state.state {
156 e.Errorf("bad old state for goroutine %d: got %s, want %s", id, old, state.state)
157 }
158 state.state = new
159 } else {
160 if old != trace.GoUndetermined && old != trace.GoNotExist {
161 e.Errorf("bad old state for unregistered goroutine %d: %s", id, old)
162 }
163 state = &goState{state: new}
164 v.gs[id] = state
165 }
166
167 if new.Executing() {
168 ctx := v.getOrCreateThread(e, ev, ev.Thread())
169 if ctx != nil {
170 if ctx.G != trace.NoGoroutine && ctx.G != id {
171 e.Errorf("tried to run goroutine %d when one was already executing (%d) on thread %d", id, ctx.G, ev.Thread())
172 }
173 ctx.G = id
174 state.binding = ctx
175 }
176 } else if old.Executing() && !new.Executing() {
177 if tr.Stack != ev.Stack() {
178
179
180 e.Errorf("StateTransition.Stack doesn't match Event.Stack")
181 }
182 ctx := state.binding
183 if ctx != nil {
184 if ctx.G != id {
185 e.Errorf("tried to stop goroutine %d when it wasn't currently executing (currently executing %d) on thread %d", id, ctx.G, ev.Thread())
186 }
187 ctx.G = trace.NoGoroutine
188 state.binding = nil
189 } else {
190 e.Errorf("stopping goroutine %d not bound to any active context", id)
191 }
192 }
193 case trace.ResourceProc:
194
195 id := tr.Resource.Proc()
196 old, new := tr.Proc()
197 if new == trace.ProcUndetermined {
198 e.Errorf("transition to undetermined state for proc %d", id)
199 }
200 if v.seenSync && old == trace.ProcUndetermined {
201 e.Errorf("undetermined proc %d after first global sync", id)
202 }
203 if new == trace.ProcNotExist && v.hasAnyRange(trace.MakeResourceID(id)) {
204 e.Errorf("proc %d died with active ranges", id)
205 }
206 state, ok := v.ps[id]
207 if ok {
208 if old != state.state {
209 e.Errorf("bad old state for proc %d: got %s, want %s", id, old, state.state)
210 }
211 state.state = new
212 } else {
213 if old != trace.ProcUndetermined && old != trace.ProcNotExist {
214 e.Errorf("bad old state for unregistered proc %d: %s", id, old)
215 }
216 state = &procState{state: new}
217 v.ps[id] = state
218 }
219
220 if new.Executing() {
221 ctx := v.getOrCreateThread(e, ev, ev.Thread())
222 if ctx != nil {
223 if ctx.P != trace.NoProc && ctx.P != id {
224 e.Errorf("tried to run proc %d when one was already executing (%d) on thread %d", id, ctx.P, ev.Thread())
225 }
226 ctx.P = id
227 state.binding = ctx
228 }
229 } else if old.Executing() && !new.Executing() {
230 ctx := state.binding
231 if ctx != nil {
232 if ctx.P != id {
233 e.Errorf("tried to stop proc %d when it wasn't currently executing (currently executing %d) on thread %d", id, ctx.P, ctx.M)
234 }
235 ctx.P = trace.NoProc
236 state.binding = nil
237 } else {
238 e.Errorf("stopping proc %d not bound to any active context", id)
239 }
240 }
241 }
242 case trace.EventRangeBegin, trace.EventRangeActive, trace.EventRangeEnd:
243
244 r := ev.Range()
245 switch ev.Kind() {
246 case trace.EventRangeBegin:
247 if v.hasRange(r.Scope, r.Name) {
248 e.Errorf("already active range %q on %v begun again", r.Name, r.Scope)
249 }
250 v.addRange(r.Scope, r.Name)
251 case trace.EventRangeActive:
252 if !v.hasRange(r.Scope, r.Name) {
253 v.addRange(r.Scope, r.Name)
254 }
255 case trace.EventRangeEnd:
256 if !v.hasRange(r.Scope, r.Name) {
257 e.Errorf("inactive range %q on %v ended", r.Name, r.Scope)
258 }
259 v.deleteRange(r.Scope, r.Name)
260 }
261 case trace.EventTaskBegin:
262
263 t := ev.Task()
264 if t.ID == trace.NoTask || t.ID == trace.BackgroundTask {
265
266 e.Errorf("found invalid task ID for task of type %s", t.Type)
267 }
268 if t.Parent == trace.BackgroundTask {
269
270 e.Errorf("found background task as the parent for task of type %s", t.Type)
271 }
272
273 v.tasks[t.ID] = t.Type
274 case trace.EventTaskEnd:
275
276
277
278 t := ev.Task()
279 if typ, ok := v.tasks[t.ID]; ok {
280 if t.Type != typ {
281 e.Errorf("task end type %q doesn't match task start type %q for task %d", t.Type, typ, t.ID)
282 }
283 delete(v.tasks, t.ID)
284 }
285 case trace.EventLog:
286
287
288
289
290
291 _ = ev.Log()
292 }
293 return e.Errors()
294 }
295
296 func (v *Validator) hasRange(r trace.ResourceID, name string) bool {
297 ranges, ok := v.ranges[r]
298 return ok && slices.Contains(ranges, name)
299 }
300
301 func (v *Validator) addRange(r trace.ResourceID, name string) {
302 ranges, _ := v.ranges[r]
303 ranges = append(ranges, name)
304 v.ranges[r] = ranges
305 }
306
307 func (v *Validator) hasAnyRange(r trace.ResourceID) bool {
308 ranges, ok := v.ranges[r]
309 return ok && len(ranges) != 0
310 }
311
312 func (v *Validator) deleteRange(r trace.ResourceID, name string) {
313 ranges, ok := v.ranges[r]
314 if !ok {
315 return
316 }
317 i := slices.Index(ranges, name)
318 if i < 0 {
319 return
320 }
321 v.ranges[r] = slices.Delete(ranges, i, i+1)
322 }
323
324 func (v *Validator) getOrCreateThread(e *errAccumulator, ev trace.Event, m trace.ThreadID) *schedContext {
325 lenient := func() bool {
326
327
328
329 if !v.Go121 {
330 return false
331 }
332 if ev.Kind() != trace.EventStateTransition {
333 return false
334 }
335 tr := ev.StateTransition()
336 if tr.Resource.Kind != trace.ResourceGoroutine {
337 return false
338 }
339 from, to := tr.Goroutine()
340 return from == trace.GoUndetermined && to == trace.GoSyscall
341 }
342 if m == trace.NoThread && !lenient() {
343 e.Errorf("must have thread, but thread ID is none")
344 return nil
345 }
346 s, ok := v.ms[m]
347 if !ok {
348 s = &schedContext{M: m, P: trace.NoProc, G: trace.NoGoroutine}
349 v.ms[m] = s
350 return s
351 }
352 return s
353 }
354
355 func checkStack(e *errAccumulator, stk trace.Stack) {
356
357 i := 0
358 stk.Frames(func(f trace.StackFrame) bool {
359 if i == 0 {
360
361
362
363 return true
364 }
365 if f.Func == "" || f.File == "" || f.PC == 0 || f.Line == 0 {
366 e.Errorf("invalid stack frame %#v: missing information", f)
367 }
368 i++
369 return true
370 })
371 }
372
373 type errAccumulator struct {
374 errs []error
375 }
376
377 func (e *errAccumulator) Errorf(f string, args ...any) {
378 e.errs = append(e.errs, fmt.Errorf(f, args...))
379 }
380
381 func (e *errAccumulator) Errors() error {
382 return errors.Join(e.errs...)
383 }
384
View as plain text