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