Skip to content

Commit 971f593

Browse files
mknyszekgopherbot
authored andcommitted
cmd/trace/v2: add thread-oriented mode for v2 traces
This is a nice-to-have that's now straightforward to do with the new trace format. This change adds a new query variable passed to the /trace endpoint called "view," which indicates the type of view to use. It is orthogonal with task-related views. Unfortunately a goroutine-based view isn't included because it's too likely to cause the browser tab to crash. For #60773. For #63960. Change-Id: Ifbcb8f2d58ffd425819bdb09c586819cb786478d Reviewed-on: https://go-review.googlesource.com/c/go/+/543695 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Pratt <mpratt@google.com> Auto-Submit: Michael Knyszek <mknyszek@google.com>
1 parent 90ba445 commit 971f593

File tree

9 files changed

+273
-20
lines changed

9 files changed

+273
-20
lines changed

src/cmd/trace/main.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ func main() {
146146
http.HandleFunc("/mmu", traceviewer.MMUHandlerFunc(ranges, mutatorUtil))
147147

148148
// Install main handler.
149-
http.Handle("/", traceviewer.MainHandler(ranges))
149+
http.Handle("/", traceviewer.MainHandler([]traceviewer.View{
150+
{Type: traceviewer.ViewProc, Ranges: ranges},
151+
}))
150152

151153
// Start http server.
152154
err = http.Serve(ln, nil)

src/cmd/trace/v2/gstate.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import (
1414
)
1515

1616
// resource is a generic constraint interface for resource IDs.
17-
type resource interface{ tracev2.GoID | tracev2.ProcID }
17+
type resource interface {
18+
tracev2.GoID | tracev2.ProcID | tracev2.ThreadID
19+
}
1820

1921
// noResource indicates the lack of a resource.
2022
const noResource = -1
@@ -214,20 +216,29 @@ func (gs *gState[R]) blockedSyscallEnd(ts tracev2.Time, stack tracev2.Stack, ctx
214216

215217
// unblock indicates that the goroutine gs represents has been unblocked.
216218
func (gs *gState[R]) unblock(ts tracev2.Time, stack tracev2.Stack, resource R, ctx *traceContext) {
217-
// Unblocking goroutine.
218219
name := "unblock"
219220
viewerResource := uint64(resource)
221+
if gs.startBlockReason != "" {
222+
name = fmt.Sprintf("%s (%s)", name, gs.startBlockReason)
223+
}
220224
if strings.Contains(gs.startBlockReason, "network") {
221-
// Emit an unblock instant event for the "Network" lane.
225+
// Attribute the network instant to the nebulous "NetpollP" if
226+
// resource isn't a thread, because there's a good chance that
227+
// resource isn't going to be valid in this case.
228+
//
229+
// TODO(mknyszek): Handle this invalidness in a more general way.
230+
if _, ok := any(resource).(tracev2.ThreadID); !ok {
231+
// Emit an unblock instant event for the "Network" lane.
232+
viewerResource = trace.NetpollP
233+
}
222234
ctx.Instant(traceviewer.InstantEvent{
223235
Name: name,
224236
Ts: ctx.elapsed(ts),
225-
Resource: trace.NetpollP,
237+
Resource: viewerResource,
226238
Stack: ctx.Stack(viewerFrames(stack)),
227239
})
228-
gs.startBlockReason = ""
229-
viewerResource = trace.NetpollP
230240
}
241+
gs.startBlockReason = ""
231242
if viewerResource != 0 {
232243
gs.setStartCause(ts, name, viewerResource, stack)
233244
}

src/cmd/trace/v2/jsontrace.go

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ func JSONTraceHandler(parsed *parsedTrace) http.Handler {
2222
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2323
opts := defaultGenOpts()
2424

25+
switch r.FormValue("view") {
26+
case "thread":
27+
opts.mode = traceviewer.ModeThreadOriented
28+
}
2529
if goids := r.FormValue("goid"); goids != "" {
2630
// Render trace focused on a particular goroutine.
2731

@@ -163,6 +167,8 @@ func generateTrace(parsed *parsedTrace, opts *genOpts, c traceviewer.TraceConsum
163167
var g generator
164168
if opts.mode&traceviewer.ModeGoroutineOriented != 0 {
165169
g = newGoroutineGenerator(ctx, opts.focusGoroutine, opts.goroutines)
170+
} else if opts.mode&traceviewer.ModeThreadOriented != 0 {
171+
g = newThreadGenerator()
166172
} else {
167173
g = newProcGenerator()
168174
}

src/cmd/trace/v2/jsontrace_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func checkNetworkUnblock(t *testing.T, data format.Data) {
159159
count := 0
160160
var netBlockEv *format.Event
161161
for _, e := range data.Events {
162-
if e.TID == tracev1.NetpollP && e.Name == "unblock" && e.Phase == "I" && e.Scope == "t" {
162+
if e.TID == tracev1.NetpollP && e.Name == "unblock (network)" && e.Phase == "I" && e.Scope == "t" {
163163
count++
164164
netBlockEv = e
165165
}

src/cmd/trace/v2/main.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,13 @@ func Main(traceFile, httpAddr, pprof string, debug int) error {
6767
mux := http.NewServeMux()
6868

6969
// Main endpoint.
70-
mux.Handle("/", traceviewer.MainHandler(ranges))
70+
mux.Handle("/", traceviewer.MainHandler([]traceviewer.View{
71+
{Type: traceviewer.ViewProc, Ranges: ranges},
72+
// N.B. Use the same ranges for threads. It takes a long time to compute
73+
// the split a second time, but the makeup of the events are similar enough
74+
// that this is still a good split.
75+
{Type: traceviewer.ViewThread, Ranges: ranges},
76+
}))
7177

7278
// Catapult handlers.
7379
mux.Handle("/trace", traceviewer.TraceHandler())

src/cmd/trace/v2/threadgen.go

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
"internal/trace/traceviewer"
10+
"internal/trace/traceviewer/format"
11+
tracev2 "internal/trace/v2"
12+
)
13+
14+
var _ generator = &threadGenerator{}
15+
16+
type threadGenerator struct {
17+
globalRangeGenerator
18+
globalMetricGenerator
19+
stackSampleGenerator[tracev2.ThreadID]
20+
21+
gStates map[tracev2.GoID]*gState[tracev2.ThreadID]
22+
threads map[tracev2.ThreadID]struct{}
23+
}
24+
25+
func newThreadGenerator() *threadGenerator {
26+
tg := new(threadGenerator)
27+
tg.stackSampleGenerator.getResource = func(ev *tracev2.Event) tracev2.ThreadID {
28+
return ev.Thread()
29+
}
30+
tg.gStates = make(map[tracev2.GoID]*gState[tracev2.ThreadID])
31+
tg.threads = make(map[tracev2.ThreadID]struct{})
32+
return tg
33+
}
34+
35+
func (g *threadGenerator) Sync() {
36+
g.globalRangeGenerator.Sync()
37+
}
38+
39+
func (g *threadGenerator) GoroutineLabel(ctx *traceContext, ev *tracev2.Event) {
40+
l := ev.Label()
41+
g.gStates[l.Resource.Goroutine()].setLabel(l.Label)
42+
}
43+
44+
func (g *threadGenerator) GoroutineRange(ctx *traceContext, ev *tracev2.Event) {
45+
r := ev.Range()
46+
switch ev.Kind() {
47+
case tracev2.EventRangeBegin:
48+
g.gStates[r.Scope.Goroutine()].rangeBegin(ev.Time(), r.Name, ev.Stack())
49+
case tracev2.EventRangeActive:
50+
g.gStates[r.Scope.Goroutine()].rangeActive(r.Name)
51+
case tracev2.EventRangeEnd:
52+
gs := g.gStates[r.Scope.Goroutine()]
53+
gs.rangeEnd(ev.Time(), r.Name, ev.Stack(), ctx)
54+
}
55+
}
56+
57+
func (g *threadGenerator) GoroutineTransition(ctx *traceContext, ev *tracev2.Event) {
58+
if ev.Thread() != tracev2.NoThread {
59+
if _, ok := g.threads[ev.Thread()]; !ok {
60+
g.threads[ev.Thread()] = struct{}{}
61+
}
62+
}
63+
64+
st := ev.StateTransition()
65+
goID := st.Resource.Goroutine()
66+
67+
// If we haven't seen this goroutine before, create a new
68+
// gState for it.
69+
gs, ok := g.gStates[goID]
70+
if !ok {
71+
gs = newGState[tracev2.ThreadID](goID)
72+
g.gStates[goID] = gs
73+
}
74+
// If we haven't already named this goroutine, try to name it.
75+
gs.augmentName(st.Stack)
76+
77+
// Handle the goroutine state transition.
78+
from, to := st.Goroutine()
79+
if from == to {
80+
// Filter out no-op events.
81+
return
82+
}
83+
if from.Executing() && !to.Executing() {
84+
if to == tracev2.GoWaiting {
85+
// Goroutine started blocking.
86+
gs.block(ev.Time(), ev.Stack(), st.Reason, ctx)
87+
} else {
88+
gs.stop(ev.Time(), ev.Stack(), ctx)
89+
}
90+
}
91+
if !from.Executing() && to.Executing() {
92+
start := ev.Time()
93+
if from == tracev2.GoUndetermined {
94+
// Back-date the event to the start of the trace.
95+
start = ctx.startTime
96+
}
97+
gs.start(start, ev.Thread(), ctx)
98+
}
99+
100+
if from == tracev2.GoWaiting {
101+
// Goroutine was unblocked.
102+
gs.unblock(ev.Time(), ev.Stack(), ev.Thread(), ctx)
103+
}
104+
if from == tracev2.GoNotExist && to == tracev2.GoRunnable {
105+
// Goroutine was created.
106+
gs.created(ev.Time(), ev.Thread(), ev.Stack())
107+
}
108+
if from == tracev2.GoSyscall {
109+
// Exiting syscall.
110+
gs.syscallEnd(ev.Time(), to != tracev2.GoRunning, ctx)
111+
}
112+
113+
// Handle syscalls.
114+
if to == tracev2.GoSyscall {
115+
start := ev.Time()
116+
if from == tracev2.GoUndetermined {
117+
// Back-date the event to the start of the trace.
118+
start = ctx.startTime
119+
}
120+
// Write down that we've entered a syscall. Note: we might have no P here
121+
// if we're in a cgo callback or this is a transition from GoUndetermined
122+
// (i.e. the G has been blocked in a syscall).
123+
gs.syscallBegin(start, ev.Thread(), ev.Stack())
124+
}
125+
126+
// Note down the goroutine transition.
127+
_, inMarkAssist := gs.activeRanges["GC mark assist"]
128+
ctx.GoroutineTransition(ctx.elapsed(ev.Time()), viewerGState(from, inMarkAssist), viewerGState(to, inMarkAssist))
129+
}
130+
131+
func (g *threadGenerator) ProcTransition(ctx *traceContext, ev *tracev2.Event) {
132+
if ev.Thread() != tracev2.NoThread {
133+
if _, ok := g.threads[ev.Thread()]; !ok {
134+
g.threads[ev.Thread()] = struct{}{}
135+
}
136+
}
137+
138+
type procArg struct {
139+
Proc uint64 `json:"proc,omitempty"`
140+
}
141+
st := ev.StateTransition()
142+
viewerEv := traceviewer.InstantEvent{
143+
Resource: uint64(ev.Thread()),
144+
Stack: ctx.Stack(viewerFrames(ev.Stack())),
145+
Arg: procArg{Proc: uint64(st.Resource.Proc())},
146+
}
147+
148+
from, to := st.Proc()
149+
if from == to {
150+
// Filter out no-op events.
151+
return
152+
}
153+
if to.Executing() {
154+
start := ev.Time()
155+
if from == tracev2.ProcUndetermined {
156+
start = ctx.startTime
157+
}
158+
viewerEv.Name = "proc start"
159+
viewerEv.Arg = format.ThreadIDArg{ThreadID: uint64(ev.Thread())}
160+
viewerEv.Ts = ctx.elapsed(start)
161+
// TODO(mknyszek): We don't have a state machine for threads, so approximate
162+
// running threads with running Ps.
163+
ctx.IncThreadStateCount(ctx.elapsed(start), traceviewer.ThreadStateRunning, 1)
164+
}
165+
if from.Executing() {
166+
start := ev.Time()
167+
viewerEv.Name = "proc stop"
168+
viewerEv.Ts = ctx.elapsed(start)
169+
// TODO(mknyszek): We don't have a state machine for threads, so approximate
170+
// running threads with running Ps.
171+
ctx.IncThreadStateCount(ctx.elapsed(start), traceviewer.ThreadStateRunning, -1)
172+
}
173+
// TODO(mknyszek): Consider modeling procs differently and have them be
174+
// transition to and from NotExist when GOMAXPROCS changes. We can emit
175+
// events for this to clearly delineate GOMAXPROCS changes.
176+
177+
if viewerEv.Name != "" {
178+
ctx.Instant(viewerEv)
179+
}
180+
}
181+
182+
func (g *threadGenerator) ProcRange(ctx *traceContext, ev *tracev2.Event) {
183+
// TODO(mknyszek): Extend procRangeGenerator to support rendering proc ranges on threads.
184+
}
185+
186+
func (g *threadGenerator) Finish(ctx *traceContext) {
187+
ctx.SetResourceType("OS THREADS")
188+
189+
// Finish off global ranges.
190+
g.globalRangeGenerator.Finish(ctx)
191+
192+
// Finish off all the goroutine slices.
193+
for _, gs := range g.gStates {
194+
gs.finish(ctx)
195+
}
196+
197+
// Name all the threads to the emitter.
198+
for id := range g.threads {
199+
ctx.Resource(uint64(id), fmt.Sprintf("Thread %d", id))
200+
}
201+
}

src/internal/trace/traceviewer/emitter.go

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ type Mode int
282282
const (
283283
ModeGoroutineOriented Mode = 1 << iota
284284
ModeTaskOriented
285+
ModeThreadOriented // Mutually exclusive with ModeGoroutineOriented.
285286
)
286287

287288
// NewEmitter returns a new Emitter that writes to c. The rangeStart and

src/internal/trace/traceviewer/http.go

+36-10
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import (
1212
"strings"
1313
)
1414

15-
func MainHandler(ranges []Range) http.Handler {
15+
func MainHandler(views []View) http.Handler {
1616
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
17-
if err := templMain.Execute(w, ranges); err != nil {
17+
if err := templMain.Execute(w, views); err != nil {
1818
http.Error(w, err.Error(), http.StatusInternalServerError)
1919
return
2020
}
@@ -70,25 +70,32 @@ var templMain = template.Must(template.New("").Parse(`
7070
</p>
7171
7272
<h2>Event timelines for running goroutines</h2>
73-
{{if $}}
73+
{{range $i, $view := $}}
74+
{{if $view.Ranges}}
75+
{{if eq $i 0}}
7476
<p>
7577
Large traces are split into multiple sections of equal data size
7678
(not duration) to avoid overwhelming the visualizer.
7779
</p>
80+
{{end}}
7881
<ul>
79-
{{range $e := $}}
80-
<li><a href="{{$e.URL}}">View trace ({{$e.Name}})</a></li>
82+
{{range $index, $e := $view.Ranges}}
83+
<li><a href="{{$view.URL $index}}">View trace by {{$view.Type}} ({{$e.Name}})</a></li>
8184
{{end}}
8285
</ul>
8386
{{else}}
8487
<ul>
85-
<li><a href="/trace">View trace</a></li>
88+
<li><a href="{{$view.URL -1}}">View trace by {{$view.Type}}</a></li>
8689
</ul>
8790
{{end}}
91+
{{end}}
8892
<p>
89-
This view displays a timeline for each of the GOMAXPROCS logical
90-
processors, showing which goroutine (if any) was running on that
93+
This view displays a series of timelines for a type of resource.
94+
The "by proc" view consists of a timeline for each of the GOMAXPROCS
95+
logical processors, showing which goroutine (if any) was running on that
9196
logical processor at each moment.
97+
The "by thread" view (if available) consists of a similar timeline for each
98+
OS thread.
9299
93100
Each goroutine has an identifying number (e.g. G123), main function,
94101
and color.
@@ -237,6 +244,25 @@ var templMain = template.Must(template.New("").Parse(`
237244
</html>
238245
`))
239246

247+
type View struct {
248+
Type ViewType
249+
Ranges []Range
250+
}
251+
252+
type ViewType string
253+
254+
const (
255+
ViewProc ViewType = "proc"
256+
ViewThread ViewType = "thread"
257+
)
258+
259+
func (v View) URL(rangeIdx int) string {
260+
if rangeIdx < 0 {
261+
return fmt.Sprintf("/trace?view=%s", v.Type)
262+
}
263+
return v.Ranges[rangeIdx].URL(v.Type)
264+
}
265+
240266
type Range struct {
241267
Name string
242268
Start int
@@ -245,8 +271,8 @@ type Range struct {
245271
EndTime int64
246272
}
247273

248-
func (r Range) URL() string {
249-
return fmt.Sprintf("/trace?start=%d&end=%d", r.Start, r.End)
274+
func (r Range) URL(viewType ViewType) string {
275+
return fmt.Sprintf("/trace?view=%s&start=%d&end=%d", viewType, r.Start, r.End)
250276
}
251277

252278
func TraceHandler() http.Handler {

0 commit comments

Comments
 (0)