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>
This commit is contained in:
Michael Anthony Knyszek 2023-11-20 07:28:37 +00:00 committed by Gopher Robot
parent 90ba445253
commit 971f59399f
9 changed files with 273 additions and 20 deletions

View File

@ -146,7 +146,9 @@ func main() {
http.HandleFunc("/mmu", traceviewer.MMUHandlerFunc(ranges, mutatorUtil)) http.HandleFunc("/mmu", traceviewer.MMUHandlerFunc(ranges, mutatorUtil))
// Install main handler. // Install main handler.
http.Handle("/", traceviewer.MainHandler(ranges)) http.Handle("/", traceviewer.MainHandler([]traceviewer.View{
{Type: traceviewer.ViewProc, Ranges: ranges},
}))
// Start http server. // Start http server.
err = http.Serve(ln, nil) err = http.Serve(ln, nil)

View File

@ -14,7 +14,9 @@ import (
) )
// resource is a generic constraint interface for resource IDs. // resource is a generic constraint interface for resource IDs.
type resource interface{ tracev2.GoID | tracev2.ProcID } type resource interface {
tracev2.GoID | tracev2.ProcID | tracev2.ThreadID
}
// noResource indicates the lack of a resource. // noResource indicates the lack of a resource.
const noResource = -1 const noResource = -1
@ -214,20 +216,29 @@ func (gs *gState[R]) blockedSyscallEnd(ts tracev2.Time, stack tracev2.Stack, ctx
// unblock indicates that the goroutine gs represents has been unblocked. // unblock indicates that the goroutine gs represents has been unblocked.
func (gs *gState[R]) unblock(ts tracev2.Time, stack tracev2.Stack, resource R, ctx *traceContext) { func (gs *gState[R]) unblock(ts tracev2.Time, stack tracev2.Stack, resource R, ctx *traceContext) {
// Unblocking goroutine.
name := "unblock" name := "unblock"
viewerResource := uint64(resource) viewerResource := uint64(resource)
if gs.startBlockReason != "" {
name = fmt.Sprintf("%s (%s)", name, gs.startBlockReason)
}
if strings.Contains(gs.startBlockReason, "network") { if strings.Contains(gs.startBlockReason, "network") {
// Emit an unblock instant event for the "Network" lane. // Attribute the network instant to the nebulous "NetpollP" if
// resource isn't a thread, because there's a good chance that
// resource isn't going to be valid in this case.
//
// TODO(mknyszek): Handle this invalidness in a more general way.
if _, ok := any(resource).(tracev2.ThreadID); !ok {
// Emit an unblock instant event for the "Network" lane.
viewerResource = trace.NetpollP
}
ctx.Instant(traceviewer.InstantEvent{ ctx.Instant(traceviewer.InstantEvent{
Name: name, Name: name,
Ts: ctx.elapsed(ts), Ts: ctx.elapsed(ts),
Resource: trace.NetpollP, Resource: viewerResource,
Stack: ctx.Stack(viewerFrames(stack)), Stack: ctx.Stack(viewerFrames(stack)),
}) })
gs.startBlockReason = ""
viewerResource = trace.NetpollP
} }
gs.startBlockReason = ""
if viewerResource != 0 { if viewerResource != 0 {
gs.setStartCause(ts, name, viewerResource, stack) gs.setStartCause(ts, name, viewerResource, stack)
} }

View File

@ -22,6 +22,10 @@ func JSONTraceHandler(parsed *parsedTrace) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
opts := defaultGenOpts() opts := defaultGenOpts()
switch r.FormValue("view") {
case "thread":
opts.mode = traceviewer.ModeThreadOriented
}
if goids := r.FormValue("goid"); goids != "" { if goids := r.FormValue("goid"); goids != "" {
// Render trace focused on a particular goroutine. // Render trace focused on a particular goroutine.
@ -163,6 +167,8 @@ func generateTrace(parsed *parsedTrace, opts *genOpts, c traceviewer.TraceConsum
var g generator var g generator
if opts.mode&traceviewer.ModeGoroutineOriented != 0 { if opts.mode&traceviewer.ModeGoroutineOriented != 0 {
g = newGoroutineGenerator(ctx, opts.focusGoroutine, opts.goroutines) g = newGoroutineGenerator(ctx, opts.focusGoroutine, opts.goroutines)
} else if opts.mode&traceviewer.ModeThreadOriented != 0 {
g = newThreadGenerator()
} else { } else {
g = newProcGenerator() g = newProcGenerator()
} }

View File

@ -159,7 +159,7 @@ func checkNetworkUnblock(t *testing.T, data format.Data) {
count := 0 count := 0
var netBlockEv *format.Event var netBlockEv *format.Event
for _, e := range data.Events { for _, e := range data.Events {
if e.TID == tracev1.NetpollP && e.Name == "unblock" && e.Phase == "I" && e.Scope == "t" { if e.TID == tracev1.NetpollP && e.Name == "unblock (network)" && e.Phase == "I" && e.Scope == "t" {
count++ count++
netBlockEv = e netBlockEv = e
} }

View File

@ -67,7 +67,13 @@ func Main(traceFile, httpAddr, pprof string, debug int) error {
mux := http.NewServeMux() mux := http.NewServeMux()
// Main endpoint. // Main endpoint.
mux.Handle("/", traceviewer.MainHandler(ranges)) mux.Handle("/", traceviewer.MainHandler([]traceviewer.View{
{Type: traceviewer.ViewProc, Ranges: ranges},
// N.B. Use the same ranges for threads. It takes a long time to compute
// the split a second time, but the makeup of the events are similar enough
// that this is still a good split.
{Type: traceviewer.ViewThread, Ranges: ranges},
}))
// Catapult handlers. // Catapult handlers.
mux.Handle("/trace", traceviewer.TraceHandler()) mux.Handle("/trace", traceviewer.TraceHandler())

View File

@ -0,0 +1,201 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package trace
import (
"fmt"
"internal/trace/traceviewer"
"internal/trace/traceviewer/format"
tracev2 "internal/trace/v2"
)
var _ generator = &threadGenerator{}
type threadGenerator struct {
globalRangeGenerator
globalMetricGenerator
stackSampleGenerator[tracev2.ThreadID]
gStates map[tracev2.GoID]*gState[tracev2.ThreadID]
threads map[tracev2.ThreadID]struct{}
}
func newThreadGenerator() *threadGenerator {
tg := new(threadGenerator)
tg.stackSampleGenerator.getResource = func(ev *tracev2.Event) tracev2.ThreadID {
return ev.Thread()
}
tg.gStates = make(map[tracev2.GoID]*gState[tracev2.ThreadID])
tg.threads = make(map[tracev2.ThreadID]struct{})
return tg
}
func (g *threadGenerator) Sync() {
g.globalRangeGenerator.Sync()
}
func (g *threadGenerator) GoroutineLabel(ctx *traceContext, ev *tracev2.Event) {
l := ev.Label()
g.gStates[l.Resource.Goroutine()].setLabel(l.Label)
}
func (g *threadGenerator) GoroutineRange(ctx *traceContext, ev *tracev2.Event) {
r := ev.Range()
switch ev.Kind() {
case tracev2.EventRangeBegin:
g.gStates[r.Scope.Goroutine()].rangeBegin(ev.Time(), r.Name, ev.Stack())
case tracev2.EventRangeActive:
g.gStates[r.Scope.Goroutine()].rangeActive(r.Name)
case tracev2.EventRangeEnd:
gs := g.gStates[r.Scope.Goroutine()]
gs.rangeEnd(ev.Time(), r.Name, ev.Stack(), ctx)
}
}
func (g *threadGenerator) GoroutineTransition(ctx *traceContext, ev *tracev2.Event) {
if ev.Thread() != tracev2.NoThread {
if _, ok := g.threads[ev.Thread()]; !ok {
g.threads[ev.Thread()] = struct{}{}
}
}
st := ev.StateTransition()
goID := st.Resource.Goroutine()
// If we haven't seen this goroutine before, create a new
// gState for it.
gs, ok := g.gStates[goID]
if !ok {
gs = newGState[tracev2.ThreadID](goID)
g.gStates[goID] = gs
}
// If we haven't already named this goroutine, try to name it.
gs.augmentName(st.Stack)
// Handle the goroutine state transition.
from, to := st.Goroutine()
if from == to {
// Filter out no-op events.
return
}
if from.Executing() && !to.Executing() {
if to == tracev2.GoWaiting {
// Goroutine started blocking.
gs.block(ev.Time(), ev.Stack(), st.Reason, ctx)
} else {
gs.stop(ev.Time(), ev.Stack(), ctx)
}
}
if !from.Executing() && to.Executing() {
start := ev.Time()
if from == tracev2.GoUndetermined {
// Back-date the event to the start of the trace.
start = ctx.startTime
}
gs.start(start, ev.Thread(), ctx)
}
if from == tracev2.GoWaiting {
// Goroutine was unblocked.
gs.unblock(ev.Time(), ev.Stack(), ev.Thread(), ctx)
}
if from == tracev2.GoNotExist && to == tracev2.GoRunnable {
// Goroutine was created.
gs.created(ev.Time(), ev.Thread(), ev.Stack())
}
if from == tracev2.GoSyscall {
// Exiting syscall.
gs.syscallEnd(ev.Time(), to != tracev2.GoRunning, ctx)
}
// Handle syscalls.
if to == tracev2.GoSyscall {
start := ev.Time()
if from == tracev2.GoUndetermined {
// Back-date the event to the start of the trace.
start = ctx.startTime
}
// Write down that we've entered a syscall. Note: we might have no P here
// if we're in a cgo callback or this is a transition from GoUndetermined
// (i.e. the G has been blocked in a syscall).
gs.syscallBegin(start, ev.Thread(), ev.Stack())
}
// Note down the goroutine transition.
_, inMarkAssist := gs.activeRanges["GC mark assist"]
ctx.GoroutineTransition(ctx.elapsed(ev.Time()), viewerGState(from, inMarkAssist), viewerGState(to, inMarkAssist))
}
func (g *threadGenerator) ProcTransition(ctx *traceContext, ev *tracev2.Event) {
if ev.Thread() != tracev2.NoThread {
if _, ok := g.threads[ev.Thread()]; !ok {
g.threads[ev.Thread()] = struct{}{}
}
}
type procArg struct {
Proc uint64 `json:"proc,omitempty"`
}
st := ev.StateTransition()
viewerEv := traceviewer.InstantEvent{
Resource: uint64(ev.Thread()),
Stack: ctx.Stack(viewerFrames(ev.Stack())),
Arg: procArg{Proc: uint64(st.Resource.Proc())},
}
from, to := st.Proc()
if from == to {
// Filter out no-op events.
return
}
if to.Executing() {
start := ev.Time()
if from == tracev2.ProcUndetermined {
start = ctx.startTime
}
viewerEv.Name = "proc start"
viewerEv.Arg = format.ThreadIDArg{ThreadID: uint64(ev.Thread())}
viewerEv.Ts = ctx.elapsed(start)
// TODO(mknyszek): We don't have a state machine for threads, so approximate
// running threads with running Ps.
ctx.IncThreadStateCount(ctx.elapsed(start), traceviewer.ThreadStateRunning, 1)
}
if from.Executing() {
start := ev.Time()
viewerEv.Name = "proc stop"
viewerEv.Ts = ctx.elapsed(start)
// TODO(mknyszek): We don't have a state machine for threads, so approximate
// running threads with running Ps.
ctx.IncThreadStateCount(ctx.elapsed(start), traceviewer.ThreadStateRunning, -1)
}
// TODO(mknyszek): Consider modeling procs differently and have them be
// transition to and from NotExist when GOMAXPROCS changes. We can emit
// events for this to clearly delineate GOMAXPROCS changes.
if viewerEv.Name != "" {
ctx.Instant(viewerEv)
}
}
func (g *threadGenerator) ProcRange(ctx *traceContext, ev *tracev2.Event) {
// TODO(mknyszek): Extend procRangeGenerator to support rendering proc ranges on threads.
}
func (g *threadGenerator) Finish(ctx *traceContext) {
ctx.SetResourceType("OS THREADS")
// Finish off global ranges.
g.globalRangeGenerator.Finish(ctx)
// Finish off all the goroutine slices.
for _, gs := range g.gStates {
gs.finish(ctx)
}
// Name all the threads to the emitter.
for id := range g.threads {
ctx.Resource(uint64(id), fmt.Sprintf("Thread %d", id))
}
}

View File

@ -282,6 +282,7 @@ type Mode int
const ( const (
ModeGoroutineOriented Mode = 1 << iota ModeGoroutineOriented Mode = 1 << iota
ModeTaskOriented ModeTaskOriented
ModeThreadOriented // Mutually exclusive with ModeGoroutineOriented.
) )
// NewEmitter returns a new Emitter that writes to c. The rangeStart and // NewEmitter returns a new Emitter that writes to c. The rangeStart and

View File

@ -12,9 +12,9 @@ import (
"strings" "strings"
) )
func MainHandler(ranges []Range) http.Handler { func MainHandler(views []View) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if err := templMain.Execute(w, ranges); err != nil { if err := templMain.Execute(w, views); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@ -70,25 +70,32 @@ var templMain = template.Must(template.New("").Parse(`
</p> </p>
<h2>Event timelines for running goroutines</h2> <h2>Event timelines for running goroutines</h2>
{{if $}} {{range $i, $view := $}}
{{if $view.Ranges}}
{{if eq $i 0}}
<p> <p>
Large traces are split into multiple sections of equal data size Large traces are split into multiple sections of equal data size
(not duration) to avoid overwhelming the visualizer. (not duration) to avoid overwhelming the visualizer.
</p> </p>
{{end}}
<ul> <ul>
{{range $e := $}} {{range $index, $e := $view.Ranges}}
<li><a href="{{$e.URL}}">View trace ({{$e.Name}})</a></li> <li><a href="{{$view.URL $index}}">View trace by {{$view.Type}} ({{$e.Name}})</a></li>
{{end}} {{end}}
</ul> </ul>
{{else}} {{else}}
<ul> <ul>
<li><a href="/trace">View trace</a></li> <li><a href="{{$view.URL -1}}">View trace by {{$view.Type}}</a></li>
</ul> </ul>
{{end}} {{end}}
{{end}}
<p> <p>
This view displays a timeline for each of the GOMAXPROCS logical This view displays a series of timelines for a type of resource.
processors, showing which goroutine (if any) was running on that The "by proc" view consists of a timeline for each of the GOMAXPROCS
logical processors, showing which goroutine (if any) was running on that
logical processor at each moment. logical processor at each moment.
The "by thread" view (if available) consists of a similar timeline for each
OS thread.
Each goroutine has an identifying number (e.g. G123), main function, Each goroutine has an identifying number (e.g. G123), main function,
and color. and color.
@ -237,6 +244,25 @@ var templMain = template.Must(template.New("").Parse(`
</html> </html>
`)) `))
type View struct {
Type ViewType
Ranges []Range
}
type ViewType string
const (
ViewProc ViewType = "proc"
ViewThread ViewType = "thread"
)
func (v View) URL(rangeIdx int) string {
if rangeIdx < 0 {
return fmt.Sprintf("/trace?view=%s", v.Type)
}
return v.Ranges[rangeIdx].URL(v.Type)
}
type Range struct { type Range struct {
Name string Name string
Start int Start int
@ -245,8 +271,8 @@ type Range struct {
EndTime int64 EndTime int64
} }
func (r Range) URL() string { func (r Range) URL(viewType ViewType) string {
return fmt.Sprintf("/trace?start=%d&end=%d", r.Start, r.End) return fmt.Sprintf("/trace?view=%s&start=%d&end=%d", viewType, r.Start, r.End)
} }
func TraceHandler() http.Handler { func TraceHandler() http.Handler {

View File

@ -410,5 +410,5 @@ func (m *mmu) newLinkedUtilWindow(ui trace.UtilWindow, window time.Duration) lin
break break
} }
} }
return linkedUtilWindow{ui, fmt.Sprintf("%s#%v:%v", r.URL(), float64(ui.Time)/1e6, float64(ui.Time+int64(window))/1e6)} return linkedUtilWindow{ui, fmt.Sprintf("%s#%v:%v", r.URL(ViewProc), float64(ui.Time)/1e6, float64(ui.Time+int64(window))/1e6)}
} }