Skip to content

Commit 64c12ba

Browse files
committed
cmd/trace/v2: add support for goroutine filtering
This change adds support for the trace?goid=<goid> endpoint to the trace tool for v2 traces. In effect, this change actually implements a per-goroutine view. I tried to add a link to the main page to enable a "view by goroutines" view without filtering, but the web trace viewer broke the browser tab when there were a few hundred goroutines. The risk of a browser hang probably isn't worth the cases where this is nice, especially since filtering by goroutine already works. Unfortunate, but c'est l'vie. Might be worth revisiting if we change out the web viewer in the future. For #60773. For #63960. Change-Id: I8e29f4ab8346af6708fd8824505c30f2c43db796 Reviewed-on: https://go-review.googlesource.com/c/go/+/543595 TryBot-Bypass: Michael Knyszek <[email protected]> Reviewed-by: Michael Pratt <[email protected]> Auto-Submit: Michael Knyszek <[email protected]>
1 parent 7105216 commit 64c12ba

File tree

5 files changed

+249
-24
lines changed

5 files changed

+249
-24
lines changed

src/cmd/trace/v2/goroutinegen.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
tracev2 "internal/trace/v2"
9+
)
10+
11+
var _ generator = &goroutineGenerator{}
12+
13+
type goroutineGenerator struct {
14+
globalRangeGenerator
15+
globalMetricGenerator
16+
stackSampleGenerator[tracev2.GoID]
17+
18+
gStates map[tracev2.GoID]*gState[tracev2.GoID]
19+
focus tracev2.GoID
20+
filter map[tracev2.GoID]struct{}
21+
}
22+
23+
func newGoroutineGenerator(ctx *traceContext, focus tracev2.GoID, filter map[tracev2.GoID]struct{}) *goroutineGenerator {
24+
gg := new(goroutineGenerator)
25+
gg.stackSampleGenerator.getResource = func(ev *tracev2.Event) tracev2.GoID {
26+
return ev.Goroutine()
27+
}
28+
gg.gStates = make(map[tracev2.GoID]*gState[tracev2.GoID])
29+
gg.focus = focus
30+
gg.filter = filter
31+
32+
// Enable a filter on the emitter.
33+
if filter != nil {
34+
ctx.SetResourceFilter(func(resource uint64) bool {
35+
_, ok := filter[tracev2.GoID(resource)]
36+
return ok
37+
})
38+
}
39+
return gg
40+
}
41+
42+
func (g *goroutineGenerator) Sync() {
43+
g.globalRangeGenerator.Sync()
44+
}
45+
46+
func (g *goroutineGenerator) GoroutineLabel(ctx *traceContext, ev *tracev2.Event) {
47+
l := ev.Label()
48+
g.gStates[l.Resource.Goroutine()].setLabel(l.Label)
49+
}
50+
51+
func (g *goroutineGenerator) GoroutineRange(ctx *traceContext, ev *tracev2.Event) {
52+
r := ev.Range()
53+
switch ev.Kind() {
54+
case tracev2.EventRangeBegin:
55+
g.gStates[r.Scope.Goroutine()].rangeBegin(ev.Time(), r.Name, ev.Stack())
56+
case tracev2.EventRangeActive:
57+
g.gStates[r.Scope.Goroutine()].rangeActive(r.Name)
58+
case tracev2.EventRangeEnd:
59+
gs := g.gStates[r.Scope.Goroutine()]
60+
gs.rangeEnd(ev.Time(), r.Name, ev.Stack(), ctx)
61+
}
62+
}
63+
64+
func (g *goroutineGenerator) GoroutineTransition(ctx *traceContext, ev *tracev2.Event) {
65+
st := ev.StateTransition()
66+
goID := st.Resource.Goroutine()
67+
68+
// If we haven't seen this goroutine before, create a new
69+
// gState for it.
70+
gs, ok := g.gStates[goID]
71+
if !ok {
72+
gs = newGState[tracev2.GoID](goID)
73+
g.gStates[goID] = gs
74+
}
75+
76+
// Try to augment the name of the goroutine.
77+
gs.augmentName(st.Stack)
78+
79+
// Handle the goroutine state transition.
80+
from, to := st.Goroutine()
81+
if from == to {
82+
// Filter out no-op events.
83+
return
84+
}
85+
if from.Executing() && !to.Executing() {
86+
if to == tracev2.GoWaiting {
87+
// Goroutine started blocking.
88+
gs.block(ev.Time(), ev.Stack(), st.Reason, ctx)
89+
} else {
90+
gs.stop(ev.Time(), ev.Stack(), ctx)
91+
}
92+
}
93+
if !from.Executing() && to.Executing() {
94+
start := ev.Time()
95+
if from == tracev2.GoUndetermined {
96+
// Back-date the event to the start of the trace.
97+
start = ctx.startTime
98+
}
99+
gs.start(start, goID, ctx)
100+
}
101+
102+
if from == tracev2.GoWaiting {
103+
// Goroutine unblocked.
104+
gs.unblock(ev.Time(), ev.Stack(), ev.Goroutine(), ctx)
105+
}
106+
if from == tracev2.GoNotExist && to == tracev2.GoRunnable {
107+
// Goroutine was created.
108+
gs.created(ev.Time(), ev.Goroutine(), ev.Stack())
109+
}
110+
if from == tracev2.GoSyscall && to != tracev2.GoRunning {
111+
// Exiting blocked syscall.
112+
gs.syscallEnd(ev.Time(), true, ctx)
113+
gs.blockedSyscallEnd(ev.Time(), ev.Stack(), ctx)
114+
} else if from == tracev2.GoSyscall {
115+
// Check if we're exiting a syscall in a non-blocking way.
116+
gs.syscallEnd(ev.Time(), false, ctx)
117+
}
118+
119+
// Handle syscalls.
120+
if to == tracev2.GoSyscall {
121+
start := ev.Time()
122+
if from == tracev2.GoUndetermined {
123+
// Back-date the event to the start of the trace.
124+
start = ctx.startTime
125+
}
126+
// Write down that we've entered a syscall. Note: we might have no G or P here
127+
// if we're in a cgo callback or this is a transition from GoUndetermined
128+
// (i.e. the G has been blocked in a syscall).
129+
gs.syscallBegin(start, goID, ev.Stack())
130+
}
131+
132+
// Note down the goroutine transition.
133+
_, inMarkAssist := gs.activeRanges["GC mark assist"]
134+
ctx.GoroutineTransition(ctx.elapsed(ev.Time()), viewerGState(from, inMarkAssist), viewerGState(to, inMarkAssist))
135+
}
136+
137+
func (g *goroutineGenerator) ProcRange(ctx *traceContext, ev *tracev2.Event) {
138+
// TODO(mknyszek): Extend procRangeGenerator to support rendering proc ranges
139+
// that overlap with a goroutine's execution.
140+
}
141+
142+
func (g *goroutineGenerator) ProcTransition(ctx *traceContext, ev *tracev2.Event) {
143+
// Not needed. All relevant information for goroutines can be derived from goroutine transitions.
144+
}
145+
146+
func (g *goroutineGenerator) Finish(ctx *traceContext, endTime tracev2.Time) {
147+
ctx.SetResourceType("G")
148+
149+
// Finish off global ranges.
150+
g.globalRangeGenerator.Finish(ctx, endTime)
151+
152+
// Finish off all the goroutine slices.
153+
for id, gs := range g.gStates {
154+
gs.finish(endTime, ctx)
155+
156+
// Tell the emitter about the goroutines we want to render.
157+
ctx.Resource(uint64(id), gs.name())
158+
}
159+
160+
// Set the goroutine to focus on.
161+
if g.focus != tracev2.NoGoroutine {
162+
ctx.Focus(uint64(g.focus))
163+
}
164+
}

src/cmd/trace/v2/jsontrace.go

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,44 @@ import (
1111
"strconv"
1212
"time"
1313

14+
"internal/trace"
1415
"internal/trace/traceviewer"
1516
tracev2 "internal/trace/v2"
1617
)
1718

1819
func JSONTraceHandler(parsed *parsedTrace) http.Handler {
1920
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21+
opts := defaultGenOpts()
22+
23+
if goids := r.FormValue("goid"); goids != "" {
24+
// Render trace focused on a particular goroutine.
25+
26+
id, err := strconv.ParseUint(goids, 10, 64)
27+
if err != nil {
28+
log.Printf("failed to parse goid parameter %q: %v", goids, err)
29+
return
30+
}
31+
goid := tracev2.GoID(id)
32+
g, ok := parsed.summary.Goroutines[goid]
33+
if !ok {
34+
log.Printf("failed to find goroutine %d", goid)
35+
return
36+
}
37+
opts.mode = traceviewer.ModeGoroutineOriented
38+
if g.StartTime != 0 {
39+
opts.startTime = g.StartTime.Sub(parsed.startTime())
40+
} else {
41+
opts.startTime = 0
42+
}
43+
if g.EndTime != 0 {
44+
opts.endTime = g.EndTime.Sub(parsed.startTime())
45+
} else { // The goroutine didn't end.
46+
opts.endTime = parsed.endTime().Sub(parsed.startTime())
47+
}
48+
opts.focusGoroutine = goid
49+
opts.goroutines = trace.RelatedGoroutinesV2(parsed.events, goid)
50+
}
51+
2052
// Parse start and end options. Both or none must be present.
2153
start := int64(0)
2254
end := int64(math.MaxInt64)
@@ -36,7 +68,7 @@ func JSONTraceHandler(parsed *parsedTrace) http.Handler {
3668
}
3769

3870
c := traceviewer.ViewerDataTraceConsumer(w, start, end)
39-
if err := generateTrace(parsed, c); err != nil {
71+
if err := generateTrace(parsed, opts, c); err != nil {
4072
log.Printf("failed to generate trace: %v", err)
4173
}
4274
})
@@ -55,13 +87,36 @@ func (ctx *traceContext) elapsed(now tracev2.Time) time.Duration {
5587
return now.Sub(ctx.startTime)
5688
}
5789

58-
func generateTrace(parsed *parsedTrace, c traceviewer.TraceConsumer) error {
90+
type genOpts struct {
91+
mode traceviewer.Mode
92+
startTime time.Duration
93+
endTime time.Duration
94+
95+
// Used if mode != 0.
96+
focusGoroutine tracev2.GoID
97+
goroutines map[tracev2.GoID]struct{} // Goroutines to be displayed for goroutine-oriented or task-oriented view. goroutines[0] is the main goroutine.
98+
}
99+
100+
func defaultGenOpts() *genOpts {
101+
return &genOpts{
102+
startTime: time.Duration(0),
103+
endTime: time.Duration(math.MaxInt64),
104+
}
105+
}
106+
107+
func generateTrace(parsed *parsedTrace, opts *genOpts, c traceviewer.TraceConsumer) error {
59108
ctx := &traceContext{
60-
Emitter: traceviewer.NewEmitter(c, 0, time.Duration(0), time.Duration(math.MaxInt64)),
109+
Emitter: traceviewer.NewEmitter(c, 0, opts.startTime, opts.endTime),
61110
startTime: parsed.events[0].Time(),
62111
}
63112
defer ctx.Flush()
64113

65-
runGenerator(ctx, newProcGenerator(), parsed)
114+
var g generator
115+
if opts.mode&traceviewer.ModeGoroutineOriented != 0 {
116+
g = newGoroutineGenerator(ctx, opts.focusGoroutine, opts.goroutines)
117+
} else {
118+
g = newProcGenerator()
119+
}
120+
runGenerator(ctx, g, parsed)
66121
return nil
67122
}

src/cmd/trace/v2/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func splitTrace(parsed *parsedTrace) ([]traceviewer.Range, error) {
145145
// TODO(mknyszek): Split traces by generation by doing a quick first pass over the
146146
// trace to identify all the generation boundaries.
147147
s, c := traceviewer.SplittingTraceConsumer(100 << 20) // 100 MiB
148-
if err := generateTrace(parsed, c); err != nil {
148+
if err := generateTrace(parsed, defaultGenOpts(), c); err != nil {
149149
return nil, err
150150
}
151151
return s.Ranges, nil

src/internal/trace/summary.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package trace
66

77
import (
88
tracev2 "internal/trace/v2"
9-
"io"
109
"sort"
1110
"time"
1211
)
@@ -601,11 +600,7 @@ func (s *Summarizer) Finalize() *Summary {
601600
// RelatedGoroutinesV2 finds a set of goroutines related to goroutine goid for v2 traces.
602601
// The association is based on whether they have synchronized with each other in the Go
603602
// scheduler (one has unblocked another).
604-
func RelatedGoroutinesV2(trace io.Reader, goid tracev2.GoID) (map[tracev2.GoID]struct{}, error) {
605-
r, err := tracev2.NewReader(trace)
606-
if err != nil {
607-
return nil, err
608-
}
603+
func RelatedGoroutinesV2(events []tracev2.Event, goid tracev2.GoID) map[tracev2.GoID]struct{} {
609604
// Process all the events, looking for transitions of goroutines
610605
// out of GoWaiting. If there was an active goroutine when this
611606
// happened, then we know that active goroutine unblocked another.
@@ -615,14 +610,7 @@ func RelatedGoroutinesV2(trace io.Reader, goid tracev2.GoID) (map[tracev2.GoID]s
615610
operand tracev2.GoID
616611
}
617612
var unblockEdges []unblockEdge
618-
for {
619-
ev, err := r.ReadEvent()
620-
if err == io.EOF {
621-
break
622-
}
623-
if err != nil {
624-
return nil, err
625-
}
613+
for _, ev := range events {
626614
if ev.Goroutine() == tracev2.NoGoroutine {
627615
continue
628616
}
@@ -660,5 +648,5 @@ func RelatedGoroutinesV2(trace io.Reader, goid tracev2.GoID) (map[tracev2.GoID]s
660648
}
661649
gmap = gmap1
662650
}
663-
return gmap, nil
651+
return gmap
664652
}

src/internal/trace/summary_test.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,15 +375,33 @@ func basicGoroutineExecStatsChecks(t *testing.T, stats *GoroutineExecStats) {
375375

376376
func TestRelatedGoroutinesV2Trace(t *testing.T) {
377377
testPath := "v2/testdata/tests/go122-gc-stress.test"
378-
r, _, err := testtrace.ParseFile(testPath)
378+
trace, _, err := testtrace.ParseFile(testPath)
379379
if err != nil {
380380
t.Fatalf("malformed test %s: bad trace file: %v", testPath, err)
381381
}
382-
targetg := tracev2.GoID(86)
383-
got, err := RelatedGoroutinesV2(r, targetg)
382+
383+
// Create a reader.
384+
r, err := tracev2.NewReader(trace)
384385
if err != nil {
385-
t.Fatalf("failed to find related goroutines for %s: %v", testPath, err)
386+
t.Fatalf("failed to create trace reader for %s: %v", testPath, err)
387+
}
388+
389+
// Collect all the events.
390+
var events []tracev2.Event
391+
for {
392+
ev, err := r.ReadEvent()
393+
if err == io.EOF {
394+
break
395+
}
396+
if err != nil {
397+
t.Fatalf("failed to process trace %s: %v", testPath, err)
398+
}
399+
events = append(events, ev)
386400
}
401+
402+
// Test the function.
403+
targetg := tracev2.GoID(86)
404+
got := RelatedGoroutinesV2(events, targetg)
387405
want := map[tracev2.GoID]struct{}{
388406
tracev2.GoID(86): struct{}{}, // N.B. Result includes target.
389407
tracev2.GoID(71): struct{}{},

0 commit comments

Comments
 (0)