Skip to content

Commit 90ba445

Browse files
committed
cmd/trace/v2: add support for a task-oriented procs-based view
This change implements support for the trace?focustask=<taskid> endpoint in the trace tool for v2 traces. Note: the one missing feature in v2 vs. v1 is that the "irrelevant" (but still rendered) events are not grayed out. This basically includes events that overlapped with events that overlapped with other events that were in the task time period, but aren't themselves directly associated. This is probably fine -- the UI already puts a very obvious focus on the period of time the selected task was running. For #60773. For #63960. Change-Id: I5c78a220ae816e331b74cb67c01c5cd98be40dd4 Reviewed-on: https://go-review.googlesource.com/c/go/+/543596 Auto-Submit: Michael Knyszek <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Michael Pratt <[email protected]>
1 parent 64c12ba commit 90ba445

File tree

8 files changed

+249
-33
lines changed

8 files changed

+249
-33
lines changed

src/cmd/trace/trace.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ type SortIndexArg struct {
210210
func generateTrace(params *traceParams, consumer traceviewer.TraceConsumer) error {
211211
emitter := traceviewer.NewEmitter(
212212
consumer,
213-
params.mode,
214213
time.Duration(params.startTime),
215214
time.Duration(params.endTime),
216215
)
@@ -565,8 +564,7 @@ func (ctx *traceContext) emitTask(task *taskDesc, sortIndex int) {
565564
taskName := task.name
566565
durationUsec := float64(task.lastTimestamp()-task.firstTimestamp()) / 1e3
567566

568-
ctx.emitFooter(&format.Event{Name: "thread_name", Phase: "M", PID: format.TasksSection, TID: taskRow, Arg: &NameArg{fmt.Sprintf("T%d %s", task.id, taskName)}})
569-
ctx.emit(&format.Event{Name: "thread_sort_index", Phase: "M", PID: format.TasksSection, TID: taskRow, Arg: &SortIndexArg{sortIndex}})
567+
ctx.emitter.Task(taskRow, taskName, sortIndex)
570568
ts := float64(task.firstTimestamp()) / 1e3
571569
sl := &format.Event{
572570
Name: taskName,

src/cmd/trace/v2/gen.go

+64-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package trace
66

77
import (
8+
"fmt"
89
"internal/trace"
910
"internal/trace/traceviewer"
1011
tracev2 "internal/trace/v2"
@@ -31,11 +32,11 @@ type generator interface {
3132
ProcTransition(ctx *traceContext, ev *tracev2.Event)
3233

3334
// Finish indicates the end of the trace and finalizes generation.
34-
Finish(ctx *traceContext, endTime tracev2.Time)
35+
Finish(ctx *traceContext)
3536
}
3637

3738
// runGenerator produces a trace into ctx by running the generator over the parsed trace.
38-
func runGenerator(ctx *traceContext, g generator, parsed *parsedTrace) {
39+
func runGenerator(ctx *traceContext, g generator, parsed *parsedTrace, opts *genOpts) {
3940
for i := range parsed.events {
4041
ev := &parsed.events[i]
4142

@@ -70,7 +71,63 @@ func runGenerator(ctx *traceContext, g generator, parsed *parsedTrace) {
7071
}
7172
}
7273
}
73-
g.Finish(ctx, parsed.events[len(parsed.events)-1].Time())
74+
for i, task := range opts.tasks {
75+
emitTask(ctx, task, i)
76+
}
77+
g.Finish(ctx)
78+
}
79+
80+
// emitTask emits information about a task into the trace viewer's event stream.
81+
//
82+
// sortIndex sets the order in which this task will appear related to other tasks,
83+
// lowest first.
84+
func emitTask(ctx *traceContext, task *trace.UserTaskSummary, sortIndex int) {
85+
// Collect information about the task.
86+
var startStack, endStack tracev2.Stack
87+
var startG, endG tracev2.GoID
88+
startTime, endTime := ctx.startTime, ctx.endTime
89+
if task.Start != nil {
90+
startStack = task.Start.Stack()
91+
startG = task.Start.Goroutine()
92+
startTime = task.Start.Time()
93+
}
94+
if task.End != nil {
95+
endStack = task.End.Stack()
96+
endG = task.End.Goroutine()
97+
endTime = task.End.Time()
98+
}
99+
arg := struct {
100+
ID uint64 `json:"id"`
101+
StartG uint64 `json:"start_g,omitempty"`
102+
EndG uint64 `json:"end_g,omitempty"`
103+
}{
104+
ID: uint64(task.ID),
105+
StartG: uint64(startG),
106+
EndG: uint64(endG),
107+
}
108+
109+
// Emit the task slice and notify the emitter of the task.
110+
ctx.Task(uint64(task.ID), fmt.Sprintf("T%d %s", task.ID, task.Name), sortIndex)
111+
ctx.TaskSlice(traceviewer.SliceEvent{
112+
Name: task.Name,
113+
Ts: ctx.elapsed(startTime),
114+
Dur: endTime.Sub(startTime),
115+
Resource: uint64(task.ID),
116+
Stack: ctx.Stack(viewerFrames(startStack)),
117+
EndStack: ctx.Stack(viewerFrames(endStack)),
118+
Arg: arg,
119+
})
120+
// Emit an arrow from the parent to the child.
121+
if task.Parent != nil && task.Start != nil && task.Start.Kind() == tracev2.EventTaskBegin {
122+
ctx.TaskArrow(traceviewer.ArrowEvent{
123+
Name: "newTask",
124+
Start: ctx.elapsed(task.Start.Time()),
125+
End: ctx.elapsed(task.Start.Time()),
126+
FromResource: uint64(task.Parent.ID),
127+
ToResource: uint64(task.ID),
128+
FromStack: ctx.Stack(viewerFrames(task.Start.Stack())),
129+
})
130+
}
74131
}
75132

76133
// Building blocks for generators.
@@ -144,15 +201,15 @@ func (g *globalRangeGenerator) GlobalRange(ctx *traceContext, ev *tracev2.Event)
144201
}
145202

146203
// Finish flushes any outstanding ranges at the end of the trace.
147-
func (g *globalRangeGenerator) Finish(ctx *traceContext, endTime tracev2.Time) {
204+
func (g *globalRangeGenerator) Finish(ctx *traceContext) {
148205
for name, ar := range g.ranges {
149206
if !strings.Contains(name, "GC") {
150207
continue
151208
}
152209
ctx.Slice(traceviewer.SliceEvent{
153210
Name: name,
154211
Ts: ctx.elapsed(ar.time),
155-
Dur: endTime.Sub(ar.time),
212+
Dur: ctx.endTime.Sub(ar.time),
156213
Resource: trace.GCP,
157214
Stack: ctx.Stack(viewerFrames(ar.stack)),
158215
})
@@ -220,12 +277,12 @@ func (g *procRangeGenerator) ProcRange(ctx *traceContext, ev *tracev2.Event) {
220277
}
221278

222279
// Finish flushes any outstanding ranges at the end of the trace.
223-
func (g *procRangeGenerator) Finish(ctx *traceContext, endTime tracev2.Time) {
280+
func (g *procRangeGenerator) Finish(ctx *traceContext) {
224281
for r, ar := range g.ranges {
225282
ctx.Slice(traceviewer.SliceEvent{
226283
Name: r.Name,
227284
Ts: ctx.elapsed(ar.time),
228-
Dur: endTime.Sub(ar.time),
285+
Dur: ctx.endTime.Sub(ar.time),
229286
Resource: uint64(r.Scope.Proc()),
230287
Stack: ctx.Stack(viewerFrames(ar.stack)),
231288
})

src/cmd/trace/v2/goroutinegen.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,15 @@ func (g *goroutineGenerator) ProcTransition(ctx *traceContext, ev *tracev2.Event
143143
// Not needed. All relevant information for goroutines can be derived from goroutine transitions.
144144
}
145145

146-
func (g *goroutineGenerator) Finish(ctx *traceContext, endTime tracev2.Time) {
146+
func (g *goroutineGenerator) Finish(ctx *traceContext) {
147147
ctx.SetResourceType("G")
148148

149149
// Finish off global ranges.
150-
g.globalRangeGenerator.Finish(ctx, endTime)
150+
g.globalRangeGenerator.Finish(ctx)
151151

152152
// Finish off all the goroutine slices.
153153
for id, gs := range g.gStates {
154-
gs.finish(endTime, ctx)
154+
gs.finish(ctx)
155155

156156
// Tell the emitter about the goroutines we want to render.
157157
ctx.Resource(uint64(id), gs.name())

src/cmd/trace/v2/gstate.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -305,10 +305,10 @@ func (gs *gState[R]) stop(ts tracev2.Time, stack tracev2.Stack, ctx *traceContex
305305
// This must only be used once the trace has been fully processed and no
306306
// further events will be processed. This method may leave the gState in
307307
// an inconsistent state.
308-
func (gs *gState[R]) finish(ts tracev2.Time, ctx *traceContext) {
308+
func (gs *gState[R]) finish(ctx *traceContext) {
309309
if gs.executing != R(noResource) {
310-
gs.syscallEnd(ts, false, ctx)
311-
gs.stop(ts, tracev2.NoStack, ctx)
310+
gs.syscallEnd(ctx.endTime, false, ctx)
311+
gs.stop(ctx.endTime, tracev2.NoStack, ctx)
312312
}
313313
}
314314

src/cmd/trace/v2/jsontrace.go

+51-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
package trace
66

77
import (
8+
"cmp"
89
"log"
910
"math"
1011
"net/http"
12+
"slices"
1113
"strconv"
1214
"time"
1315

@@ -47,6 +49,50 @@ func JSONTraceHandler(parsed *parsedTrace) http.Handler {
4749
}
4850
opts.focusGoroutine = goid
4951
opts.goroutines = trace.RelatedGoroutinesV2(parsed.events, goid)
52+
} else if taskids := r.FormValue("focustask"); taskids != "" {
53+
taskid, err := strconv.ParseUint(taskids, 10, 64)
54+
if err != nil {
55+
log.Printf("failed to parse focustask parameter %q: %v", taskids, err)
56+
return
57+
}
58+
task, ok := parsed.summary.Tasks[tracev2.TaskID(taskid)]
59+
if !ok || (task.Start == nil && task.End == nil) {
60+
log.Printf("failed to find task with id %d", taskid)
61+
return
62+
}
63+
opts.mode = traceviewer.ModeTaskOriented
64+
if task.Start != nil {
65+
opts.startTime = task.Start.Time().Sub(parsed.startTime())
66+
} else { // The task started before the trace did.
67+
opts.startTime = 0
68+
}
69+
if task.End != nil {
70+
opts.endTime = task.End.Time().Sub(parsed.startTime())
71+
} else { // The task didn't end.
72+
opts.endTime = parsed.endTime().Sub(parsed.startTime())
73+
}
74+
opts.tasks = task.Descendents()
75+
slices.SortStableFunc(opts.tasks, func(a, b *trace.UserTaskSummary) int {
76+
aStart, bStart := parsed.startTime(), parsed.startTime()
77+
if a.Start != nil {
78+
aStart = a.Start.Time()
79+
}
80+
if b.Start != nil {
81+
bStart = b.Start.Time()
82+
}
83+
if a.Start != b.Start {
84+
return cmp.Compare(aStart, bStart)
85+
}
86+
// Break ties with the end time.
87+
aEnd, bEnd := parsed.endTime(), parsed.endTime()
88+
if a.End != nil {
89+
aEnd = a.End.Time()
90+
}
91+
if b.End != nil {
92+
bEnd = b.End.Time()
93+
}
94+
return cmp.Compare(aEnd, bEnd)
95+
})
5096
}
5197

5298
// Parse start and end options. Both or none must be present.
@@ -79,6 +125,7 @@ func JSONTraceHandler(parsed *parsedTrace) http.Handler {
79125
type traceContext struct {
80126
*traceviewer.Emitter
81127
startTime tracev2.Time
128+
endTime tracev2.Time
82129
}
83130

84131
// elapsed returns the elapsed time between the trace time and the start time
@@ -95,6 +142,7 @@ type genOpts struct {
95142
// Used if mode != 0.
96143
focusGoroutine tracev2.GoID
97144
goroutines map[tracev2.GoID]struct{} // Goroutines to be displayed for goroutine-oriented or task-oriented view. goroutines[0] is the main goroutine.
145+
tasks []*trace.UserTaskSummary
98146
}
99147

100148
func defaultGenOpts() *genOpts {
@@ -106,8 +154,9 @@ func defaultGenOpts() *genOpts {
106154

107155
func generateTrace(parsed *parsedTrace, opts *genOpts, c traceviewer.TraceConsumer) error {
108156
ctx := &traceContext{
109-
Emitter: traceviewer.NewEmitter(c, 0, opts.startTime, opts.endTime),
157+
Emitter: traceviewer.NewEmitter(c, opts.startTime, opts.endTime),
110158
startTime: parsed.events[0].Time(),
159+
endTime: parsed.events[len(parsed.events)-1].Time(),
111160
}
112161
defer ctx.Flush()
113162

@@ -117,6 +166,6 @@ func generateTrace(parsed *parsedTrace, opts *genOpts, c traceviewer.TraceConsum
117166
} else {
118167
g = newProcGenerator()
119168
}
120-
runGenerator(ctx, g, parsed)
169+
runGenerator(ctx, g, parsed, opts)
121170
return nil
122171
}

src/cmd/trace/v2/procgen.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,18 @@ func (g *procGenerator) ProcTransition(ctx *traceContext, ev *tracev2.Event) {
188188
}
189189
}
190190

191-
func (g *procGenerator) Finish(ctx *traceContext, endTime tracev2.Time) {
191+
func (g *procGenerator) Finish(ctx *traceContext) {
192192
ctx.SetResourceType("PROCS")
193193

194194
// Finish off ranges first. It doesn't really matter for the global ranges,
195195
// but the proc ranges need to either be a subset of a goroutine slice or
196196
// their own slice entirely. If the former, it needs to end first.
197-
g.procRangeGenerator.Finish(ctx, endTime)
198-
g.globalRangeGenerator.Finish(ctx, endTime)
197+
g.procRangeGenerator.Finish(ctx)
198+
g.globalRangeGenerator.Finish(ctx)
199199

200200
// Finish off all the goroutine slices.
201201
for _, gs := range g.gStates {
202-
gs.finish(endTime, ctx)
202+
gs.finish(ctx)
203203
}
204204

205205
// Name all the procs to the emitter.

src/internal/trace/summary.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type UserTaskSummary struct {
5050
// Task begin event. An EventTaskBegin event or nil.
5151
Start *tracev2.Event
5252

53-
// End end event. Normally EventTaskEnd event or nil,
53+
// End end event. Normally EventTaskEnd event or nil.
5454
End *tracev2.Event
5555

5656
// Logs is a list of tracev2.EventLog events associated with the task.
@@ -69,6 +69,16 @@ func (s *UserTaskSummary) Complete() bool {
6969
return s.Start != nil && s.End != nil
7070
}
7171

72+
// Descendents returns a slice consisting of itself (always the first task returned),
73+
// and the transitive closure of all of its children.
74+
func (s *UserTaskSummary) Descendents() []*UserTaskSummary {
75+
descendents := []*UserTaskSummary{s}
76+
for _, child := range s.Children {
77+
descendents = append(descendents, child.Descendents()...)
78+
}
79+
return descendents
80+
}
81+
7282
// UserRegionSummary represents a region and goroutine execution stats
7383
// while the region was active. (For v2 traces.)
7484
type UserRegionSummary struct {

0 commit comments

Comments
 (0)