| // 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 |
| } |