blob: e4ca613678adff809e8eddffcfc7908e74db1c78 [file] [log] [blame]
// 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 (
"cmp"
"log"
"math"
"net/http"
"slices"
"strconv"
"time"
"internal/trace"
"internal/trace/traceviewer"
tracev2 "internal/trace/v2"
)
func JSONTraceHandler(parsed *parsedTrace) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
opts := defaultGenOpts()
switch r.FormValue("view") {
case "thread":
opts.mode = traceviewer.ModeThreadOriented
}
if goids := r.FormValue("goid"); goids != "" {
// Render trace focused on a particular goroutine.
id, err := strconv.ParseUint(goids, 10, 64)
if err != nil {
log.Printf("failed to parse goid parameter %q: %v", goids, err)
return
}
goid := tracev2.GoID(id)
g, ok := parsed.summary.Goroutines[goid]
if !ok {
log.Printf("failed to find goroutine %d", goid)
return
}
opts.mode = traceviewer.ModeGoroutineOriented
if g.StartTime != 0 {
opts.startTime = g.StartTime.Sub(parsed.startTime())
} else {
opts.startTime = 0
}
if g.EndTime != 0 {
opts.endTime = g.EndTime.Sub(parsed.startTime())
} else { // The goroutine didn't end.
opts.endTime = parsed.endTime().Sub(parsed.startTime())
}
opts.focusGoroutine = goid
opts.goroutines = trace.RelatedGoroutinesV2(parsed.events, goid)
} else if taskids := r.FormValue("focustask"); taskids != "" {
taskid, err := strconv.ParseUint(taskids, 10, 64)
if err != nil {
log.Printf("failed to parse focustask parameter %q: %v", taskids, err)
return
}
task, ok := parsed.summary.Tasks[tracev2.TaskID(taskid)]
if !ok || (task.Start == nil && task.End == nil) {
log.Printf("failed to find task with id %d", taskid)
return
}
opts.setTask(parsed, task)
} else if taskids := r.FormValue("taskid"); taskids != "" {
taskid, err := strconv.ParseUint(taskids, 10, 64)
if err != nil {
log.Printf("failed to parse taskid parameter %q: %v", taskids, err)
return
}
task, ok := parsed.summary.Tasks[tracev2.TaskID(taskid)]
if !ok {
log.Printf("failed to find task with id %d", taskid)
return
}
// This mode is goroutine-oriented.
opts.mode = traceviewer.ModeGoroutineOriented
opts.setTask(parsed, task)
// Pick the goroutine to orient ourselves around by just
// trying to pick the earliest event in the task that makes
// any sense. Though, we always want the start if that's there.
var firstEv *tracev2.Event
if task.Start != nil {
firstEv = task.Start
} else {
for _, logEv := range task.Logs {
if firstEv == nil || logEv.Time() < firstEv.Time() {
firstEv = logEv
}
}
if task.End != nil && (firstEv == nil || task.End.Time() < firstEv.Time()) {
firstEv = task.End
}
}
if firstEv == nil || firstEv.Goroutine() == tracev2.NoGoroutine {
log.Printf("failed to find task with id %d", taskid)
return
}
// Set the goroutine filtering options.
goid := firstEv.Goroutine()
opts.focusGoroutine = goid
goroutines := make(map[tracev2.GoID]struct{})
for _, task := range opts.tasks {
// Find only directly involved goroutines.
for id := range task.Goroutines {
goroutines[id] = struct{}{}
}
}
opts.goroutines = goroutines
}
// Parse start and end options. Both or none must be present.
start := int64(0)
end := int64(math.MaxInt64)
if startStr, endStr := r.FormValue("start"), r.FormValue("end"); startStr != "" && endStr != "" {
var err error
start, err = strconv.ParseInt(startStr, 10, 64)
if err != nil {
log.Printf("failed to parse start parameter %q: %v", startStr, err)
return
}
end, err = strconv.ParseInt(endStr, 10, 64)
if err != nil {
log.Printf("failed to parse end parameter %q: %v", endStr, err)
return
}
}
c := traceviewer.ViewerDataTraceConsumer(w, start, end)
if err := generateTrace(parsed, opts, c); err != nil {
log.Printf("failed to generate trace: %v", err)
}
})
}
// traceContext is a wrapper around a traceviewer.Emitter with some additional
// information that's useful to most parts of trace viewer JSON emission.
type traceContext struct {
*traceviewer.Emitter
startTime tracev2.Time
endTime tracev2.Time
}
// elapsed returns the elapsed time between the trace time and the start time
// of the trace.
func (ctx *traceContext) elapsed(now tracev2.Time) time.Duration {
return now.Sub(ctx.startTime)
}
type genOpts struct {
mode traceviewer.Mode
startTime time.Duration
endTime time.Duration
// Used if mode != 0.
focusGoroutine tracev2.GoID
goroutines map[tracev2.GoID]struct{} // Goroutines to be displayed for goroutine-oriented or task-oriented view. goroutines[0] is the main goroutine.
tasks []*trace.UserTaskSummary
}
// setTask sets a task to focus on.
func (opts *genOpts) setTask(parsed *parsedTrace, task *trace.UserTaskSummary) {
opts.mode |= traceviewer.ModeTaskOriented
if task.Start != nil {
opts.startTime = task.Start.Time().Sub(parsed.startTime())
} else { // The task started before the trace did.
opts.startTime = 0
}
if task.End != nil {
opts.endTime = task.End.Time().Sub(parsed.startTime())
} else { // The task didn't end.
opts.endTime = parsed.endTime().Sub(parsed.startTime())
}
opts.tasks = task.Descendents()
slices.SortStableFunc(opts.tasks, func(a, b *trace.UserTaskSummary) int {
aStart, bStart := parsed.startTime(), parsed.startTime()
if a.Start != nil {
aStart = a.Start.Time()
}
if b.Start != nil {
bStart = b.Start.Time()
}
if a.Start != b.Start {
return cmp.Compare(aStart, bStart)
}
// Break ties with the end time.
aEnd, bEnd := parsed.endTime(), parsed.endTime()
if a.End != nil {
aEnd = a.End.Time()
}
if b.End != nil {
bEnd = b.End.Time()
}
return cmp.Compare(aEnd, bEnd)
})
}
func defaultGenOpts() *genOpts {
return &genOpts{
startTime: time.Duration(0),
endTime: time.Duration(math.MaxInt64),
}
}
func generateTrace(parsed *parsedTrace, opts *genOpts, c traceviewer.TraceConsumer) error {
ctx := &traceContext{
Emitter: traceviewer.NewEmitter(c, opts.startTime, opts.endTime),
startTime: parsed.events[0].Time(),
endTime: parsed.events[len(parsed.events)-1].Time(),
}
defer ctx.Flush()
var g generator
if opts.mode&traceviewer.ModeGoroutineOriented != 0 {
g = newGoroutineGenerator(ctx, opts.focusGoroutine, opts.goroutines)
} else if opts.mode&traceviewer.ModeThreadOriented != 0 {
g = newThreadGenerator()
} else {
g = newProcGenerator()
}
runGenerator(ctx, g, parsed, opts)
return nil
}