github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/trace/v2/gen.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package trace 6 7 import ( 8 "fmt" 9 "strings" 10 11 "github.com/go-asm/go/trace" 12 "github.com/go-asm/go/trace/traceviewer" 13 tracev2 "github.com/go-asm/go/trace/v2" 14 ) 15 16 // generator is an interface for generating a JSON trace for the trace viewer 17 // from a trace. Each method in this interface is a handler for a kind of event 18 // that is interesting to render in the UI via the JSON trace. 19 type generator interface { 20 // Global parts. 21 Sync() // Notifies the generator of an EventSync event. 22 StackSample(ctx *traceContext, ev *tracev2.Event) 23 GlobalRange(ctx *traceContext, ev *tracev2.Event) 24 GlobalMetric(ctx *traceContext, ev *tracev2.Event) 25 26 // Goroutine parts. 27 GoroutineLabel(ctx *traceContext, ev *tracev2.Event) 28 GoroutineRange(ctx *traceContext, ev *tracev2.Event) 29 GoroutineTransition(ctx *traceContext, ev *tracev2.Event) 30 31 // Proc parts. 32 ProcRange(ctx *traceContext, ev *tracev2.Event) 33 ProcTransition(ctx *traceContext, ev *tracev2.Event) 34 35 // User annotations. 36 Log(ctx *traceContext, ev *tracev2.Event) 37 38 // Finish indicates the end of the trace and finalizes generation. 39 Finish(ctx *traceContext) 40 } 41 42 // runGenerator produces a trace into ctx by running the generator over the parsed trace. 43 func runGenerator(ctx *traceContext, g generator, parsed *parsedTrace, opts *genOpts) { 44 for i := range parsed.events { 45 ev := &parsed.events[i] 46 47 switch ev.Kind() { 48 case tracev2.EventSync: 49 g.Sync() 50 case tracev2.EventStackSample: 51 g.StackSample(ctx, ev) 52 case tracev2.EventRangeBegin, tracev2.EventRangeActive, tracev2.EventRangeEnd: 53 r := ev.Range() 54 switch r.Scope.Kind { 55 case tracev2.ResourceGoroutine: 56 g.GoroutineRange(ctx, ev) 57 case tracev2.ResourceProc: 58 g.ProcRange(ctx, ev) 59 case tracev2.ResourceNone: 60 g.GlobalRange(ctx, ev) 61 } 62 case tracev2.EventMetric: 63 g.GlobalMetric(ctx, ev) 64 case tracev2.EventLabel: 65 l := ev.Label() 66 if l.Resource.Kind == tracev2.ResourceGoroutine { 67 g.GoroutineLabel(ctx, ev) 68 } 69 case tracev2.EventStateTransition: 70 switch ev.StateTransition().Resource.Kind { 71 case tracev2.ResourceProc: 72 g.ProcTransition(ctx, ev) 73 case tracev2.ResourceGoroutine: 74 g.GoroutineTransition(ctx, ev) 75 } 76 case tracev2.EventLog: 77 g.Log(ctx, ev) 78 } 79 } 80 for i, task := range opts.tasks { 81 emitTask(ctx, task, i) 82 if opts.mode&traceviewer.ModeGoroutineOriented != 0 { 83 for _, region := range task.Regions { 84 emitRegion(ctx, region) 85 } 86 } 87 } 88 g.Finish(ctx) 89 } 90 91 // emitTask emits information about a task into the trace viewer's event stream. 92 // 93 // sortIndex sets the order in which this task will appear related to other tasks, 94 // lowest first. 95 func emitTask(ctx *traceContext, task *trace.UserTaskSummary, sortIndex int) { 96 // Collect information about the task. 97 var startStack, endStack tracev2.Stack 98 var startG, endG tracev2.GoID 99 startTime, endTime := ctx.startTime, ctx.endTime 100 if task.Start != nil { 101 startStack = task.Start.Stack() 102 startG = task.Start.Goroutine() 103 startTime = task.Start.Time() 104 } 105 if task.End != nil { 106 endStack = task.End.Stack() 107 endG = task.End.Goroutine() 108 endTime = task.End.Time() 109 } 110 arg := struct { 111 ID uint64 `json:"id"` 112 StartG uint64 `json:"start_g,omitempty"` 113 EndG uint64 `json:"end_g,omitempty"` 114 }{ 115 ID: uint64(task.ID), 116 StartG: uint64(startG), 117 EndG: uint64(endG), 118 } 119 120 // Emit the task slice and notify the emitter of the task. 121 ctx.Task(uint64(task.ID), fmt.Sprintf("T%d %s", task.ID, task.Name), sortIndex) 122 ctx.TaskSlice(traceviewer.SliceEvent{ 123 Name: task.Name, 124 Ts: ctx.elapsed(startTime), 125 Dur: endTime.Sub(startTime), 126 Resource: uint64(task.ID), 127 Stack: ctx.Stack(viewerFrames(startStack)), 128 EndStack: ctx.Stack(viewerFrames(endStack)), 129 Arg: arg, 130 }) 131 // Emit an arrow from the parent to the child. 132 if task.Parent != nil && task.Start != nil && task.Start.Kind() == tracev2.EventTaskBegin { 133 ctx.TaskArrow(traceviewer.ArrowEvent{ 134 Name: "newTask", 135 Start: ctx.elapsed(task.Start.Time()), 136 End: ctx.elapsed(task.Start.Time()), 137 FromResource: uint64(task.Parent.ID), 138 ToResource: uint64(task.ID), 139 FromStack: ctx.Stack(viewerFrames(task.Start.Stack())), 140 }) 141 } 142 } 143 144 // emitRegion emits goroutine-based slice events to the UI. The caller 145 // must be emitting for a goroutine-oriented trace. 146 // 147 // TODO(mknyszek): Make regions part of the regular generator loop and 148 // treat them like ranges so that we can emit regions in traces oriented 149 // by proc or thread. 150 func emitRegion(ctx *traceContext, region *trace.UserRegionSummary) { 151 if region.Name == "" { 152 return 153 } 154 // Collect information about the region. 155 var startStack, endStack tracev2.Stack 156 goroutine := tracev2.NoGoroutine 157 startTime, endTime := ctx.startTime, ctx.endTime 158 if region.Start != nil { 159 startStack = region.Start.Stack() 160 startTime = region.Start.Time() 161 goroutine = region.Start.Goroutine() 162 } 163 if region.End != nil { 164 endStack = region.End.Stack() 165 endTime = region.End.Time() 166 goroutine = region.End.Goroutine() 167 } 168 if goroutine == tracev2.NoGoroutine { 169 return 170 } 171 arg := struct { 172 TaskID uint64 `json:"taskid"` 173 }{ 174 TaskID: uint64(region.TaskID), 175 } 176 ctx.AsyncSlice(traceviewer.AsyncSliceEvent{ 177 SliceEvent: traceviewer.SliceEvent{ 178 Name: region.Name, 179 Ts: ctx.elapsed(startTime), 180 Dur: endTime.Sub(startTime), 181 Resource: uint64(goroutine), 182 Stack: ctx.Stack(viewerFrames(startStack)), 183 EndStack: ctx.Stack(viewerFrames(endStack)), 184 Arg: arg, 185 }, 186 Category: "Region", 187 Scope: fmt.Sprintf("%x", region.TaskID), 188 TaskColorIndex: uint64(region.TaskID), 189 }) 190 } 191 192 // Building blocks for generators. 193 194 // stackSampleGenerator implements a generic handler for stack sample events. 195 // The provided resource is the resource the stack sample should count against. 196 type stackSampleGenerator[R resource] struct { 197 // getResource is a function to extract a resource ID from a stack sample event. 198 getResource func(*tracev2.Event) R 199 } 200 201 // StackSample implements a stack sample event handler. It expects ev to be one such event. 202 func (g *stackSampleGenerator[R]) StackSample(ctx *traceContext, ev *tracev2.Event) { 203 id := g.getResource(ev) 204 if id == R(noResource) { 205 // We have nowhere to put this in the UI. 206 return 207 } 208 ctx.Instant(traceviewer.InstantEvent{ 209 Name: "CPU profile sample", 210 Ts: ctx.elapsed(ev.Time()), 211 Resource: uint64(id), 212 Stack: ctx.Stack(viewerFrames(ev.Stack())), 213 }) 214 } 215 216 // globalRangeGenerator implements a generic handler for EventRange* events that pertain 217 // to tracev2.ResourceNone (the global scope). 218 type globalRangeGenerator struct { 219 ranges map[string]activeRange 220 seenSync bool 221 } 222 223 // Sync notifies the generator of an EventSync event. 224 func (g *globalRangeGenerator) Sync() { 225 g.seenSync = true 226 } 227 228 // GlobalRange implements a handler for EventRange* events whose Scope.Kind is ResourceNone. 229 // It expects ev to be one such event. 230 func (g *globalRangeGenerator) GlobalRange(ctx *traceContext, ev *tracev2.Event) { 231 if g.ranges == nil { 232 g.ranges = make(map[string]activeRange) 233 } 234 r := ev.Range() 235 switch ev.Kind() { 236 case tracev2.EventRangeBegin: 237 g.ranges[r.Name] = activeRange{ev.Time(), ev.Stack()} 238 case tracev2.EventRangeActive: 239 // If we've seen a Sync event, then Active events are always redundant. 240 if !g.seenSync { 241 // Otherwise, they extend back to the start of the trace. 242 g.ranges[r.Name] = activeRange{ctx.startTime, ev.Stack()} 243 } 244 case tracev2.EventRangeEnd: 245 // Only emit GC events, because we have nowhere to 246 // put other events. 247 ar := g.ranges[r.Name] 248 if strings.Contains(r.Name, "GC") { 249 ctx.Slice(traceviewer.SliceEvent{ 250 Name: r.Name, 251 Ts: ctx.elapsed(ar.time), 252 Dur: ev.Time().Sub(ar.time), 253 Resource: trace.GCP, 254 Stack: ctx.Stack(viewerFrames(ar.stack)), 255 EndStack: ctx.Stack(viewerFrames(ev.Stack())), 256 }) 257 } 258 delete(g.ranges, r.Name) 259 } 260 } 261 262 // Finish flushes any outstanding ranges at the end of the trace. 263 func (g *globalRangeGenerator) Finish(ctx *traceContext) { 264 for name, ar := range g.ranges { 265 if !strings.Contains(name, "GC") { 266 continue 267 } 268 ctx.Slice(traceviewer.SliceEvent{ 269 Name: name, 270 Ts: ctx.elapsed(ar.time), 271 Dur: ctx.endTime.Sub(ar.time), 272 Resource: trace.GCP, 273 Stack: ctx.Stack(viewerFrames(ar.stack)), 274 }) 275 } 276 } 277 278 // globalMetricGenerator implements a generic handler for Metric events. 279 type globalMetricGenerator struct { 280 } 281 282 // GlobalMetric implements an event handler for EventMetric events. ev must be one such event. 283 func (g *globalMetricGenerator) GlobalMetric(ctx *traceContext, ev *tracev2.Event) { 284 m := ev.Metric() 285 switch m.Name { 286 case "/memory/classes/heap/objects:bytes": 287 ctx.HeapAlloc(ctx.elapsed(ev.Time()), m.Value.Uint64()) 288 case "/gc/heap/goal:bytes": 289 ctx.HeapGoal(ctx.elapsed(ev.Time()), m.Value.Uint64()) 290 case "/sched/gomaxprocs:threads": 291 ctx.Gomaxprocs(m.Value.Uint64()) 292 } 293 } 294 295 // procRangeGenerator implements a generic handler for EventRange* events whose Scope.Kind is 296 // ResourceProc. 297 type procRangeGenerator struct { 298 ranges map[tracev2.Range]activeRange 299 seenSync bool 300 } 301 302 // Sync notifies the generator of an EventSync event. 303 func (g *procRangeGenerator) Sync() { 304 g.seenSync = true 305 } 306 307 // ProcRange implements a handler for EventRange* events whose Scope.Kind is ResourceProc. 308 // It expects ev to be one such event. 309 func (g *procRangeGenerator) ProcRange(ctx *traceContext, ev *tracev2.Event) { 310 if g.ranges == nil { 311 g.ranges = make(map[tracev2.Range]activeRange) 312 } 313 r := ev.Range() 314 switch ev.Kind() { 315 case tracev2.EventRangeBegin: 316 g.ranges[r] = activeRange{ev.Time(), ev.Stack()} 317 case tracev2.EventRangeActive: 318 // If we've seen a Sync event, then Active events are always redundant. 319 if !g.seenSync { 320 // Otherwise, they extend back to the start of the trace. 321 g.ranges[r] = activeRange{ctx.startTime, ev.Stack()} 322 } 323 case tracev2.EventRangeEnd: 324 // Emit proc-based ranges. 325 ar := g.ranges[r] 326 ctx.Slice(traceviewer.SliceEvent{ 327 Name: r.Name, 328 Ts: ctx.elapsed(ar.time), 329 Dur: ev.Time().Sub(ar.time), 330 Resource: uint64(r.Scope.Proc()), 331 Stack: ctx.Stack(viewerFrames(ar.stack)), 332 EndStack: ctx.Stack(viewerFrames(ev.Stack())), 333 }) 334 delete(g.ranges, r) 335 } 336 } 337 338 // Finish flushes any outstanding ranges at the end of the trace. 339 func (g *procRangeGenerator) Finish(ctx *traceContext) { 340 for r, ar := range g.ranges { 341 ctx.Slice(traceviewer.SliceEvent{ 342 Name: r.Name, 343 Ts: ctx.elapsed(ar.time), 344 Dur: ctx.endTime.Sub(ar.time), 345 Resource: uint64(r.Scope.Proc()), 346 Stack: ctx.Stack(viewerFrames(ar.stack)), 347 }) 348 } 349 } 350 351 // activeRange represents an active EventRange* range. 352 type activeRange struct { 353 time tracev2.Time 354 stack tracev2.Stack 355 } 356 357 // completedRange represents a completed EventRange* range. 358 type completedRange struct { 359 name string 360 startTime tracev2.Time 361 endTime tracev2.Time 362 startStack tracev2.Stack 363 endStack tracev2.Stack 364 arg any 365 } 366 367 type logEventGenerator[R resource] struct { 368 // getResource is a function to extract a resource ID from a Log event. 369 getResource func(*tracev2.Event) R 370 } 371 372 // Log implements a log event handler. It expects ev to be one such event. 373 func (g *logEventGenerator[R]) Log(ctx *traceContext, ev *tracev2.Event) { 374 id := g.getResource(ev) 375 if id == R(noResource) { 376 // We have nowhere to put this in the UI. 377 return 378 } 379 380 // Construct the name to present. 381 log := ev.Log() 382 name := log.Message 383 if log.Category != "" { 384 name = "[" + log.Category + "] " + name 385 } 386 387 // Emit an instant event. 388 ctx.Instant(traceviewer.InstantEvent{ 389 Name: name, 390 Ts: ctx.elapsed(ev.Time()), 391 Category: "user event", 392 Resource: uint64(id), 393 Stack: ctx.Stack(viewerFrames(ev.Stack())), 394 }) 395 }