diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 13:14:23 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 13:14:23 +0000 |
commit | 73df946d56c74384511a194dd01dbe099584fd1a (patch) | |
tree | fd0bcea490dd81327ddfbb31e215439672c9a068 /src/cmd/trace/trace.go | |
parent | Initial commit. (diff) | |
download | golang-1.16-73df946d56c74384511a194dd01dbe099584fd1a.tar.xz golang-1.16-73df946d56c74384511a194dd01dbe099584fd1a.zip |
Adding upstream version 1.16.10.upstream/1.16.10upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cmd/trace/trace.go')
-rw-r--r-- | src/cmd/trace/trace.go | 1273 |
1 files changed, 1273 insertions, 0 deletions
diff --git a/src/cmd/trace/trace.go b/src/cmd/trace/trace.go new file mode 100644 index 0000000..30c80f0 --- /dev/null +++ b/src/cmd/trace/trace.go @@ -0,0 +1,1273 @@ +// Copyright 2014 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 main + +import ( + "cmd/internal/traceviewer" + "encoding/json" + "fmt" + "internal/trace" + "io" + "log" + "math" + "net/http" + "path/filepath" + "runtime" + "runtime/debug" + "sort" + "strconv" + "strings" + "time" +) + +func init() { + http.HandleFunc("/trace", httpTrace) + http.HandleFunc("/jsontrace", httpJsonTrace) + http.HandleFunc("/trace_viewer_html", httpTraceViewerHTML) + http.HandleFunc("/webcomponents.min.js", webcomponentsJS) +} + +// httpTrace serves either whole trace (goid==0) or trace for goid goroutine. +func httpTrace(w http.ResponseWriter, r *http.Request) { + _, err := parseTrace() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + html := strings.ReplaceAll(templTrace, "{{PARAMS}}", r.Form.Encode()) + w.Write([]byte(html)) + +} + +// https://chromium.googlesource.com/catapult/+/9508452e18f130c98499cb4c4f1e1efaedee8962/tracing/docs/embedding-trace-viewer.md +// This is almost verbatim copy of https://chromium-review.googlesource.com/c/catapult/+/2062938/2/tracing/bin/index.html +var templTrace = ` +<html> +<head> +<script src="/webcomponents.min.js"></script> +<script> +'use strict'; + +function onTraceViewerImportFail() { + document.addEventListener('DOMContentLoaded', function() { + document.body.textContent = + '/trace_viewer_full.html is missing. File a bug in https://golang.org/issue'; + }); +} +</script> + +<link rel="import" href="/trace_viewer_html" + onerror="onTraceViewerImportFail(event)"> + +<style type="text/css"> + html, body { + box-sizing: border-box; + overflow: hidden; + margin: 0px; + padding: 0; + width: 100%; + height: 100%; + } + #trace-viewer { + width: 100%; + height: 100%; + } + #trace-viewer:focus { + outline: none; + } +</style> +<script> +'use strict'; +(function() { + var viewer; + var url; + var model; + + function load() { + var req = new XMLHttpRequest(); + var isBinary = /[.]gz$/.test(url) || /[.]zip$/.test(url); + req.overrideMimeType('text/plain; charset=x-user-defined'); + req.open('GET', url, true); + if (isBinary) + req.responseType = 'arraybuffer'; + + req.onreadystatechange = function(event) { + if (req.readyState !== 4) + return; + + window.setTimeout(function() { + if (req.status === 200) + onResult(isBinary ? req.response : req.responseText); + else + onResultFail(req.status); + }, 0); + }; + req.send(null); + } + + function onResultFail(err) { + var overlay = new tr.ui.b.Overlay(); + overlay.textContent = err + ': ' + url + ' could not be loaded'; + overlay.title = 'Failed to fetch data'; + overlay.visible = true; + } + + function onResult(result) { + model = new tr.Model(); + var opts = new tr.importer.ImportOptions(); + opts.shiftWorldToZero = false; + var i = new tr.importer.Import(model, opts); + var p = i.importTracesWithProgressDialog([result]); + p.then(onModelLoaded, onImportFail); + } + + function onModelLoaded() { + viewer.model = model; + viewer.viewTitle = "trace"; + + if (!model || model.bounds.isEmpty) + return; + var sel = window.location.hash.substr(1); + if (sel === '') + return; + var parts = sel.split(':'); + var range = new (tr.b.Range || tr.b.math.Range)(); + range.addValue(parseFloat(parts[0])); + range.addValue(parseFloat(parts[1])); + viewer.trackView.viewport.interestRange.set(range); + } + + function onImportFail(err) { + var overlay = new tr.ui.b.Overlay(); + overlay.textContent = tr.b.normalizeException(err).message; + overlay.title = 'Import error'; + overlay.visible = true; + } + + document.addEventListener('WebComponentsReady', function() { + var container = document.createElement('track-view-container'); + container.id = 'track_view_container'; + + viewer = document.createElement('tr-ui-timeline-view'); + viewer.track_view_container = container; + Polymer.dom(viewer).appendChild(container); + + viewer.id = 'trace-viewer'; + viewer.globalMode = true; + Polymer.dom(document.body).appendChild(viewer); + + url = '/jsontrace?{{PARAMS}}'; + load(); + }); +}()); +</script> +</head> +<body> +</body> +</html> +` + +// httpTraceViewerHTML serves static part of trace-viewer. +// This URL is queried from templTrace HTML. +func httpTraceViewerHTML(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, filepath.Join(runtime.GOROOT(), "misc", "trace", "trace_viewer_full.html")) +} + +func webcomponentsJS(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, filepath.Join(runtime.GOROOT(), "misc", "trace", "webcomponents.min.js")) +} + +// httpJsonTrace serves json trace, requested from within templTrace HTML. +func httpJsonTrace(w http.ResponseWriter, r *http.Request) { + defer debug.FreeOSMemory() + defer reportMemoryUsage("after httpJsonTrace") + // This is an AJAX handler, so instead of http.Error we use log.Printf to log errors. + res, err := parseTrace() + if err != nil { + log.Printf("failed to parse trace: %v", err) + return + } + + params := &traceParams{ + parsed: res, + endTime: math.MaxInt64, + } + + if goids := r.FormValue("goid"); goids != "" { + // If goid argument is present, we are rendering a trace for this particular goroutine. + goid, err := strconv.ParseUint(goids, 10, 64) + if err != nil { + log.Printf("failed to parse goid parameter %q: %v", goids, err) + return + } + analyzeGoroutines(res.Events) + g, ok := gs[goid] + if !ok { + log.Printf("failed to find goroutine %d", goid) + return + } + params.mode = modeGoroutineOriented + params.startTime = g.StartTime + if g.EndTime != 0 { + params.endTime = g.EndTime + } else { // The goroutine didn't end. + params.endTime = lastTimestamp() + } + params.maing = goid + params.gs = trace.RelatedGoroutines(res.Events, goid) + } 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 + } + annotRes, _ := analyzeAnnotations() + task, ok := annotRes.tasks[taskid] + if !ok || len(task.events) == 0 { + log.Printf("failed to find task with id %d", taskid) + return + } + goid := task.events[0].G + params.mode = modeGoroutineOriented | modeTaskOriented + params.startTime = task.firstTimestamp() - 1 + params.endTime = task.lastTimestamp() + 1 + params.maing = goid + params.tasks = task.descendants() + gs := map[uint64]bool{} + for _, t := range params.tasks { + // find only directly involved goroutines + for k, v := range t.RelatedGoroutines(res.Events, 0) { + gs[k] = v + } + } + params.gs = gs + } 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 + } + annotRes, _ := analyzeAnnotations() + task, ok := annotRes.tasks[taskid] + if !ok || len(task.events) == 0 { + log.Printf("failed to find task with id %d", taskid) + return + } + params.mode = modeTaskOriented + params.startTime = task.firstTimestamp() - 1 + params.endTime = task.lastTimestamp() + 1 + params.tasks = task.descendants() + } + + start := int64(0) + end := int64(math.MaxInt64) + if startStr, endStr := r.FormValue("start"), r.FormValue("end"); startStr != "" && endStr != "" { + // If start/end arguments are present, we are rendering a range of the trace. + 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 := viewerDataTraceConsumer(w, start, end) + if err := generateTrace(params, c); err != nil { + log.Printf("failed to generate trace: %v", err) + return + } +} + +type Range struct { + Name string + Start int + End int + StartTime int64 + EndTime int64 +} + +func (r Range) URL() string { + return fmt.Sprintf("/trace?start=%d&end=%d", r.Start, r.End) +} + +// splitTrace splits the trace into a number of ranges, +// each resulting in approx 100MB of json output +// (trace viewer can hardly handle more). +func splitTrace(res trace.ParseResult) []Range { + params := &traceParams{ + parsed: res, + endTime: math.MaxInt64, + } + s, c := splittingTraceConsumer(100 << 20) // 100M + if err := generateTrace(params, c); err != nil { + dief("%v\n", err) + } + return s.Ranges +} + +type splitter struct { + Ranges []Range +} + +func splittingTraceConsumer(max int) (*splitter, traceConsumer) { + type eventSz struct { + Time float64 + Sz int + } + + var ( + data = traceviewer.Data{Frames: make(map[string]traceviewer.Frame)} + + sizes []eventSz + cw countingWriter + ) + + s := new(splitter) + + return s, traceConsumer{ + consumeTimeUnit: func(unit string) { + data.TimeUnit = unit + }, + consumeViewerEvent: func(v *traceviewer.Event, required bool) { + if required { + // Store required events inside data + // so flush can include them in the required + // part of the trace. + data.Events = append(data.Events, v) + return + } + enc := json.NewEncoder(&cw) + enc.Encode(v) + sizes = append(sizes, eventSz{v.Time, cw.size + 1}) // +1 for ",". + cw.size = 0 + }, + consumeViewerFrame: func(k string, v traceviewer.Frame) { + data.Frames[k] = v + }, + flush: func() { + // Calculate size of the mandatory part of the trace. + // This includes stack traces and thread names. + cw.size = 0 + enc := json.NewEncoder(&cw) + enc.Encode(data) + minSize := cw.size + + // Then calculate size of each individual event + // and group them into ranges. + sum := minSize + start := 0 + for i, ev := range sizes { + if sum+ev.Sz > max { + startTime := time.Duration(sizes[start].Time * 1000) + endTime := time.Duration(ev.Time * 1000) + ranges = append(ranges, Range{ + Name: fmt.Sprintf("%v-%v", startTime, endTime), + Start: start, + End: i + 1, + StartTime: int64(startTime), + EndTime: int64(endTime), + }) + start = i + 1 + sum = minSize + } else { + sum += ev.Sz + 1 + } + } + if len(ranges) <= 1 { + s.Ranges = nil + return + } + + if end := len(sizes) - 1; start < end { + ranges = append(ranges, Range{ + Name: fmt.Sprintf("%v-%v", time.Duration(sizes[start].Time*1000), time.Duration(sizes[end].Time*1000)), + Start: start, + End: end, + StartTime: int64(sizes[start].Time * 1000), + EndTime: int64(sizes[end].Time * 1000), + }) + } + s.Ranges = ranges + }, + } +} + +type countingWriter struct { + size int +} + +func (cw *countingWriter) Write(data []byte) (int, error) { + cw.size += len(data) + return len(data), nil +} + +type traceParams struct { + parsed trace.ParseResult + mode traceviewMode + startTime int64 + endTime int64 + maing uint64 // for goroutine-oriented view, place this goroutine on the top row + gs map[uint64]bool // Goroutines to be displayed for goroutine-oriented or task-oriented view + tasks []*taskDesc // Tasks to be displayed. tasks[0] is the top-most task +} + +type traceviewMode uint + +const ( + modeGoroutineOriented traceviewMode = 1 << iota + modeTaskOriented +) + +type traceContext struct { + *traceParams + consumer traceConsumer + frameTree frameNode + frameSeq int + arrowSeq uint64 + gcount uint64 + + heapStats, prevHeapStats heapStats + threadStats, prevThreadStats threadStats + gstates, prevGstates [gStateCount]int64 + + regionID int // last emitted region id. incremented in each emitRegion call. +} + +type heapStats struct { + heapAlloc uint64 + nextGC uint64 +} + +type threadStats struct { + insyscallRuntime int64 // system goroutine in syscall + insyscall int64 // user goroutine in syscall + prunning int64 // thread running P +} + +type frameNode struct { + id int + children map[uint64]frameNode +} + +type gState int + +const ( + gDead gState = iota + gRunnable + gRunning + gWaiting + gWaitingGC + + gStateCount +) + +type gInfo struct { + state gState // current state + name string // name chosen for this goroutine at first EvGoStart + isSystemG bool + start *trace.Event // most recent EvGoStart + markAssist *trace.Event // if non-nil, the mark assist currently running. +} + +type NameArg struct { + Name string `json:"name"` +} + +type TaskArg struct { + ID uint64 `json:"id"` + StartG uint64 `json:"start_g,omitempty"` + EndG uint64 `json:"end_g,omitempty"` +} + +type RegionArg struct { + TaskID uint64 `json:"taskid,omitempty"` +} + +type SortIndexArg struct { + Index int `json:"sort_index"` +} + +type traceConsumer struct { + consumeTimeUnit func(unit string) + consumeViewerEvent func(v *traceviewer.Event, required bool) + consumeViewerFrame func(key string, f traceviewer.Frame) + flush func() +} + +const ( + procsSection = 0 // where Goroutines or per-P timelines are presented. + statsSection = 1 // where counters are presented. + tasksSection = 2 // where Task hierarchy & timeline is presented. +) + +// generateTrace generates json trace for trace-viewer: +// https://github.com/google/trace-viewer +// Trace format is described at: +// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/view +// If mode==goroutineMode, generate trace for goroutine goid, otherwise whole trace. +// startTime, endTime determine part of the trace that we are interested in. +// gset restricts goroutines that are included in the resulting trace. +func generateTrace(params *traceParams, consumer traceConsumer) error { + defer consumer.flush() + + ctx := &traceContext{traceParams: params} + ctx.frameTree.children = make(map[uint64]frameNode) + ctx.consumer = consumer + + ctx.consumer.consumeTimeUnit("ns") + maxProc := 0 + ginfos := make(map[uint64]*gInfo) + stacks := params.parsed.Stacks + + getGInfo := func(g uint64) *gInfo { + info, ok := ginfos[g] + if !ok { + info = &gInfo{} + ginfos[g] = info + } + return info + } + + // Since we make many calls to setGState, we record a sticky + // error in setGStateErr and check it after every event. + var setGStateErr error + setGState := func(ev *trace.Event, g uint64, oldState, newState gState) { + info := getGInfo(g) + if oldState == gWaiting && info.state == gWaitingGC { + // For checking, gWaiting counts as any gWaiting*. + oldState = info.state + } + if info.state != oldState && setGStateErr == nil { + setGStateErr = fmt.Errorf("expected G %d to be in state %d, but got state %d", g, oldState, newState) + } + ctx.gstates[info.state]-- + ctx.gstates[newState]++ + info.state = newState + } + + for _, ev := range ctx.parsed.Events { + // Handle state transitions before we filter out events. + switch ev.Type { + case trace.EvGoStart, trace.EvGoStartLabel: + setGState(ev, ev.G, gRunnable, gRunning) + info := getGInfo(ev.G) + info.start = ev + case trace.EvProcStart: + ctx.threadStats.prunning++ + case trace.EvProcStop: + ctx.threadStats.prunning-- + case trace.EvGoCreate: + newG := ev.Args[0] + info := getGInfo(newG) + if info.name != "" { + return fmt.Errorf("duplicate go create event for go id=%d detected at offset %d", newG, ev.Off) + } + + stk, ok := stacks[ev.Args[1]] + if !ok || len(stk) == 0 { + return fmt.Errorf("invalid go create event: missing stack information for go id=%d at offset %d", newG, ev.Off) + } + + fname := stk[0].Fn + info.name = fmt.Sprintf("G%v %s", newG, fname) + info.isSystemG = isSystemGoroutine(fname) + + ctx.gcount++ + setGState(ev, newG, gDead, gRunnable) + case trace.EvGoEnd: + ctx.gcount-- + setGState(ev, ev.G, gRunning, gDead) + case trace.EvGoUnblock: + setGState(ev, ev.Args[0], gWaiting, gRunnable) + case trace.EvGoSysExit: + setGState(ev, ev.G, gWaiting, gRunnable) + if getGInfo(ev.G).isSystemG { + ctx.threadStats.insyscallRuntime-- + } else { + ctx.threadStats.insyscall-- + } + case trace.EvGoSysBlock: + setGState(ev, ev.G, gRunning, gWaiting) + if getGInfo(ev.G).isSystemG { + ctx.threadStats.insyscallRuntime++ + } else { + ctx.threadStats.insyscall++ + } + case trace.EvGoSched, trace.EvGoPreempt: + setGState(ev, ev.G, gRunning, gRunnable) + case trace.EvGoStop, + trace.EvGoSleep, trace.EvGoBlock, trace.EvGoBlockSend, trace.EvGoBlockRecv, + trace.EvGoBlockSelect, trace.EvGoBlockSync, trace.EvGoBlockCond, trace.EvGoBlockNet: + setGState(ev, ev.G, gRunning, gWaiting) + case trace.EvGoBlockGC: + setGState(ev, ev.G, gRunning, gWaitingGC) + case trace.EvGCMarkAssistStart: + getGInfo(ev.G).markAssist = ev + case trace.EvGCMarkAssistDone: + getGInfo(ev.G).markAssist = nil + case trace.EvGoWaiting: + setGState(ev, ev.G, gRunnable, gWaiting) + case trace.EvGoInSyscall: + // Cancel out the effect of EvGoCreate at the beginning. + setGState(ev, ev.G, gRunnable, gWaiting) + if getGInfo(ev.G).isSystemG { + ctx.threadStats.insyscallRuntime++ + } else { + ctx.threadStats.insyscall++ + } + case trace.EvHeapAlloc: + ctx.heapStats.heapAlloc = ev.Args[0] + case trace.EvNextGC: + ctx.heapStats.nextGC = ev.Args[0] + } + if setGStateErr != nil { + return setGStateErr + } + if ctx.gstates[gRunnable] < 0 || ctx.gstates[gRunning] < 0 || ctx.threadStats.insyscall < 0 || ctx.threadStats.insyscallRuntime < 0 { + return fmt.Errorf("invalid state after processing %v: runnable=%d running=%d insyscall=%d insyscallRuntime=%d", ev, ctx.gstates[gRunnable], ctx.gstates[gRunning], ctx.threadStats.insyscall, ctx.threadStats.insyscallRuntime) + } + + // Ignore events that are from uninteresting goroutines + // or outside of the interesting timeframe. + if ctx.gs != nil && ev.P < trace.FakeP && !ctx.gs[ev.G] { + continue + } + if !withinTimeRange(ev, ctx.startTime, ctx.endTime) { + continue + } + + if ev.P < trace.FakeP && ev.P > maxProc { + maxProc = ev.P + } + + // Emit trace objects. + switch ev.Type { + case trace.EvProcStart: + if ctx.mode&modeGoroutineOriented != 0 { + continue + } + ctx.emitInstant(ev, "proc start", "") + case trace.EvProcStop: + if ctx.mode&modeGoroutineOriented != 0 { + continue + } + ctx.emitInstant(ev, "proc stop", "") + case trace.EvGCStart: + ctx.emitSlice(ev, "GC") + case trace.EvGCDone: + case trace.EvGCSTWStart: + if ctx.mode&modeGoroutineOriented != 0 { + continue + } + ctx.emitSlice(ev, fmt.Sprintf("STW (%s)", ev.SArgs[0])) + case trace.EvGCSTWDone: + case trace.EvGCMarkAssistStart: + // Mark assists can continue past preemptions, so truncate to the + // whichever comes first. We'll synthesize another slice if + // necessary in EvGoStart. + markFinish := ev.Link + goFinish := getGInfo(ev.G).start.Link + fakeMarkStart := *ev + text := "MARK ASSIST" + if markFinish == nil || markFinish.Ts > goFinish.Ts { + fakeMarkStart.Link = goFinish + text = "MARK ASSIST (unfinished)" + } + ctx.emitSlice(&fakeMarkStart, text) + case trace.EvGCSweepStart: + slice := ctx.makeSlice(ev, "SWEEP") + if done := ev.Link; done != nil && done.Args[0] != 0 { + slice.Arg = struct { + Swept uint64 `json:"Swept bytes"` + Reclaimed uint64 `json:"Reclaimed bytes"` + }{done.Args[0], done.Args[1]} + } + ctx.emit(slice) + case trace.EvGoStart, trace.EvGoStartLabel: + info := getGInfo(ev.G) + if ev.Type == trace.EvGoStartLabel { + ctx.emitSlice(ev, ev.SArgs[0]) + } else { + ctx.emitSlice(ev, info.name) + } + if info.markAssist != nil { + // If we're in a mark assist, synthesize a new slice, ending + // either when the mark assist ends or when we're descheduled. + markFinish := info.markAssist.Link + goFinish := ev.Link + fakeMarkStart := *ev + text := "MARK ASSIST (resumed, unfinished)" + if markFinish != nil && markFinish.Ts < goFinish.Ts { + fakeMarkStart.Link = markFinish + text = "MARK ASSIST (resumed)" + } + ctx.emitSlice(&fakeMarkStart, text) + } + case trace.EvGoCreate: + ctx.emitArrow(ev, "go") + case trace.EvGoUnblock: + ctx.emitArrow(ev, "unblock") + case trace.EvGoSysCall: + ctx.emitInstant(ev, "syscall", "") + case trace.EvGoSysExit: + ctx.emitArrow(ev, "sysexit") + case trace.EvUserLog: + ctx.emitInstant(ev, formatUserLog(ev), "user event") + case trace.EvUserTaskCreate: + ctx.emitInstant(ev, "task start", "user event") + case trace.EvUserTaskEnd: + ctx.emitInstant(ev, "task end", "user event") + } + // Emit any counter updates. + ctx.emitThreadCounters(ev) + ctx.emitHeapCounters(ev) + ctx.emitGoroutineCounters(ev) + } + + ctx.emitSectionFooter(statsSection, "STATS", 0) + + if ctx.mode&modeTaskOriented != 0 { + ctx.emitSectionFooter(tasksSection, "TASKS", 1) + } + + if ctx.mode&modeGoroutineOriented != 0 { + ctx.emitSectionFooter(procsSection, "G", 2) + } else { + ctx.emitSectionFooter(procsSection, "PROCS", 2) + } + + ctx.emitFooter(&traceviewer.Event{Name: "thread_name", Phase: "M", PID: procsSection, TID: trace.GCP, Arg: &NameArg{"GC"}}) + ctx.emitFooter(&traceviewer.Event{Name: "thread_sort_index", Phase: "M", PID: procsSection, TID: trace.GCP, Arg: &SortIndexArg{-6}}) + + ctx.emitFooter(&traceviewer.Event{Name: "thread_name", Phase: "M", PID: procsSection, TID: trace.NetpollP, Arg: &NameArg{"Network"}}) + ctx.emitFooter(&traceviewer.Event{Name: "thread_sort_index", Phase: "M", PID: procsSection, TID: trace.NetpollP, Arg: &SortIndexArg{-5}}) + + ctx.emitFooter(&traceviewer.Event{Name: "thread_name", Phase: "M", PID: procsSection, TID: trace.TimerP, Arg: &NameArg{"Timers"}}) + ctx.emitFooter(&traceviewer.Event{Name: "thread_sort_index", Phase: "M", PID: procsSection, TID: trace.TimerP, Arg: &SortIndexArg{-4}}) + + ctx.emitFooter(&traceviewer.Event{Name: "thread_name", Phase: "M", PID: procsSection, TID: trace.SyscallP, Arg: &NameArg{"Syscalls"}}) + ctx.emitFooter(&traceviewer.Event{Name: "thread_sort_index", Phase: "M", PID: procsSection, TID: trace.SyscallP, Arg: &SortIndexArg{-3}}) + + // Display rows for Ps if we are in the default trace view mode (not goroutine-oriented presentation) + if ctx.mode&modeGoroutineOriented == 0 { + for i := 0; i <= maxProc; i++ { + ctx.emitFooter(&traceviewer.Event{Name: "thread_name", Phase: "M", PID: procsSection, TID: uint64(i), Arg: &NameArg{fmt.Sprintf("Proc %v", i)}}) + ctx.emitFooter(&traceviewer.Event{Name: "thread_sort_index", Phase: "M", PID: procsSection, TID: uint64(i), Arg: &SortIndexArg{i}}) + } + } + + // Display task and its regions if we are in task-oriented presentation mode. + if ctx.mode&modeTaskOriented != 0 { + // sort tasks based on the task start time. + sortedTask := make([]*taskDesc, 0, len(ctx.tasks)) + for _, task := range ctx.tasks { + sortedTask = append(sortedTask, task) + } + sort.SliceStable(sortedTask, func(i, j int) bool { + ti, tj := sortedTask[i], sortedTask[j] + if ti.firstTimestamp() == tj.firstTimestamp() { + return ti.lastTimestamp() < tj.lastTimestamp() + } + return ti.firstTimestamp() < tj.firstTimestamp() + }) + + for i, task := range sortedTask { + ctx.emitTask(task, i) + + // If we are in goroutine-oriented mode, we draw regions. + // TODO(hyangah): add this for task/P-oriented mode (i.e., focustask view) too. + if ctx.mode&modeGoroutineOriented != 0 { + for _, s := range task.regions { + ctx.emitRegion(s) + } + } + } + } + + // Display goroutine rows if we are either in goroutine-oriented mode. + if ctx.mode&modeGoroutineOriented != 0 { + for k, v := range ginfos { + if !ctx.gs[k] { + continue + } + ctx.emitFooter(&traceviewer.Event{Name: "thread_name", Phase: "M", PID: procsSection, TID: k, Arg: &NameArg{v.name}}) + } + // Row for the main goroutine (maing) + ctx.emitFooter(&traceviewer.Event{Name: "thread_sort_index", Phase: "M", PID: procsSection, TID: ctx.maing, Arg: &SortIndexArg{-2}}) + // Row for GC or global state (specified with G=0) + ctx.emitFooter(&traceviewer.Event{Name: "thread_sort_index", Phase: "M", PID: procsSection, TID: 0, Arg: &SortIndexArg{-1}}) + } + + return nil +} + +func (ctx *traceContext) emit(e *traceviewer.Event) { + ctx.consumer.consumeViewerEvent(e, false) +} + +func (ctx *traceContext) emitFooter(e *traceviewer.Event) { + ctx.consumer.consumeViewerEvent(e, true) +} +func (ctx *traceContext) emitSectionFooter(sectionID uint64, name string, priority int) { + ctx.emitFooter(&traceviewer.Event{Name: "process_name", Phase: "M", PID: sectionID, Arg: &NameArg{name}}) + ctx.emitFooter(&traceviewer.Event{Name: "process_sort_index", Phase: "M", PID: sectionID, Arg: &SortIndexArg{priority}}) +} + +func (ctx *traceContext) time(ev *trace.Event) float64 { + // Trace viewer wants timestamps in microseconds. + return float64(ev.Ts) / 1000 +} + +func withinTimeRange(ev *trace.Event, s, e int64) bool { + if evEnd := ev.Link; evEnd != nil { + return ev.Ts <= e && evEnd.Ts >= s + } + return ev.Ts >= s && ev.Ts <= e +} + +func tsWithinRange(ts, s, e int64) bool { + return s <= ts && ts <= e +} + +func (ctx *traceContext) proc(ev *trace.Event) uint64 { + if ctx.mode&modeGoroutineOriented != 0 && ev.P < trace.FakeP { + return ev.G + } else { + return uint64(ev.P) + } +} + +func (ctx *traceContext) emitSlice(ev *trace.Event, name string) { + ctx.emit(ctx.makeSlice(ev, name)) +} + +func (ctx *traceContext) makeSlice(ev *trace.Event, name string) *traceviewer.Event { + // If ViewerEvent.Dur is not a positive value, + // trace viewer handles it as a non-terminating time interval. + // Avoid it by setting the field with a small value. + durationUsec := ctx.time(ev.Link) - ctx.time(ev) + if ev.Link.Ts-ev.Ts <= 0 { + durationUsec = 0.0001 // 0.1 nanoseconds + } + sl := &traceviewer.Event{ + Name: name, + Phase: "X", + Time: ctx.time(ev), + Dur: durationUsec, + TID: ctx.proc(ev), + Stack: ctx.stack(ev.Stk), + EndStack: ctx.stack(ev.Link.Stk), + } + + // grey out non-overlapping events if the event is not a global event (ev.G == 0) + if ctx.mode&modeTaskOriented != 0 && ev.G != 0 { + // include P information. + if t := ev.Type; t == trace.EvGoStart || t == trace.EvGoStartLabel { + type Arg struct { + P int + } + sl.Arg = &Arg{P: ev.P} + } + // grey out non-overlapping events. + overlapping := false + for _, task := range ctx.tasks { + if _, overlapped := task.overlappingDuration(ev); overlapped { + overlapping = true + break + } + } + if !overlapping { + sl.Cname = colorLightGrey + } + } + return sl +} + +func (ctx *traceContext) emitTask(task *taskDesc, sortIndex int) { + taskRow := uint64(task.id) + taskName := task.name + durationUsec := float64(task.lastTimestamp()-task.firstTimestamp()) / 1e3 + + ctx.emitFooter(&traceviewer.Event{Name: "thread_name", Phase: "M", PID: tasksSection, TID: taskRow, Arg: &NameArg{fmt.Sprintf("T%d %s", task.id, taskName)}}) + ctx.emit(&traceviewer.Event{Name: "thread_sort_index", Phase: "M", PID: tasksSection, TID: taskRow, Arg: &SortIndexArg{sortIndex}}) + ts := float64(task.firstTimestamp()) / 1e3 + sl := &traceviewer.Event{ + Name: taskName, + Phase: "X", + Time: ts, + Dur: durationUsec, + PID: tasksSection, + TID: taskRow, + Cname: pickTaskColor(task.id), + } + targ := TaskArg{ID: task.id} + if task.create != nil { + sl.Stack = ctx.stack(task.create.Stk) + targ.StartG = task.create.G + } + if task.end != nil { + sl.EndStack = ctx.stack(task.end.Stk) + targ.EndG = task.end.G + } + sl.Arg = targ + ctx.emit(sl) + + if task.create != nil && task.create.Type == trace.EvUserTaskCreate && task.create.Args[1] != 0 { + ctx.arrowSeq++ + ctx.emit(&traceviewer.Event{Name: "newTask", Phase: "s", TID: task.create.Args[1], ID: ctx.arrowSeq, Time: ts, PID: tasksSection}) + ctx.emit(&traceviewer.Event{Name: "newTask", Phase: "t", TID: taskRow, ID: ctx.arrowSeq, Time: ts, PID: tasksSection}) + } +} + +func (ctx *traceContext) emitRegion(s regionDesc) { + if s.Name == "" { + return + } + + if !tsWithinRange(s.firstTimestamp(), ctx.startTime, ctx.endTime) && + !tsWithinRange(s.lastTimestamp(), ctx.startTime, ctx.endTime) { + return + } + + ctx.regionID++ + regionID := ctx.regionID + + id := s.TaskID + scopeID := fmt.Sprintf("%x", id) + name := s.Name + + sl0 := &traceviewer.Event{ + Category: "Region", + Name: name, + Phase: "b", + Time: float64(s.firstTimestamp()) / 1e3, + TID: s.G, // only in goroutine-oriented view + ID: uint64(regionID), + Scope: scopeID, + Cname: pickTaskColor(s.TaskID), + } + if s.Start != nil { + sl0.Stack = ctx.stack(s.Start.Stk) + } + ctx.emit(sl0) + + sl1 := &traceviewer.Event{ + Category: "Region", + Name: name, + Phase: "e", + Time: float64(s.lastTimestamp()) / 1e3, + TID: s.G, + ID: uint64(regionID), + Scope: scopeID, + Cname: pickTaskColor(s.TaskID), + Arg: RegionArg{TaskID: s.TaskID}, + } + if s.End != nil { + sl1.Stack = ctx.stack(s.End.Stk) + } + ctx.emit(sl1) +} + +type heapCountersArg struct { + Allocated uint64 + NextGC uint64 +} + +func (ctx *traceContext) emitHeapCounters(ev *trace.Event) { + if ctx.prevHeapStats == ctx.heapStats { + return + } + diff := uint64(0) + if ctx.heapStats.nextGC > ctx.heapStats.heapAlloc { + diff = ctx.heapStats.nextGC - ctx.heapStats.heapAlloc + } + if tsWithinRange(ev.Ts, ctx.startTime, ctx.endTime) { + ctx.emit(&traceviewer.Event{Name: "Heap", Phase: "C", Time: ctx.time(ev), PID: 1, Arg: &heapCountersArg{ctx.heapStats.heapAlloc, diff}}) + } + ctx.prevHeapStats = ctx.heapStats +} + +type goroutineCountersArg struct { + Running uint64 + Runnable uint64 + GCWaiting uint64 +} + +func (ctx *traceContext) emitGoroutineCounters(ev *trace.Event) { + if ctx.prevGstates == ctx.gstates { + return + } + if tsWithinRange(ev.Ts, ctx.startTime, ctx.endTime) { + ctx.emit(&traceviewer.Event{Name: "Goroutines", Phase: "C", Time: ctx.time(ev), PID: 1, Arg: &goroutineCountersArg{uint64(ctx.gstates[gRunning]), uint64(ctx.gstates[gRunnable]), uint64(ctx.gstates[gWaitingGC])}}) + } + ctx.prevGstates = ctx.gstates +} + +type threadCountersArg struct { + Running int64 + InSyscall int64 +} + +func (ctx *traceContext) emitThreadCounters(ev *trace.Event) { + if ctx.prevThreadStats == ctx.threadStats { + return + } + if tsWithinRange(ev.Ts, ctx.startTime, ctx.endTime) { + ctx.emit(&traceviewer.Event{Name: "Threads", Phase: "C", Time: ctx.time(ev), PID: 1, Arg: &threadCountersArg{ + Running: ctx.threadStats.prunning, + InSyscall: ctx.threadStats.insyscall}}) + } + ctx.prevThreadStats = ctx.threadStats +} + +func (ctx *traceContext) emitInstant(ev *trace.Event, name, category string) { + if !tsWithinRange(ev.Ts, ctx.startTime, ctx.endTime) { + return + } + + cname := "" + if ctx.mode&modeTaskOriented != 0 { + taskID, isUserAnnotation := isUserAnnotationEvent(ev) + + show := false + for _, task := range ctx.tasks { + if isUserAnnotation && task.id == taskID || task.overlappingInstant(ev) { + show = true + break + } + } + // grey out or skip if non-overlapping instant. + if !show { + if isUserAnnotation { + return // don't display unrelated user annotation events. + } + cname = colorLightGrey + } + } + var arg interface{} + if ev.Type == trace.EvProcStart { + type Arg struct { + ThreadID uint64 + } + arg = &Arg{ev.Args[0]} + } + ctx.emit(&traceviewer.Event{ + Name: name, + Category: category, + Phase: "I", + Scope: "t", + Time: ctx.time(ev), + TID: ctx.proc(ev), + Stack: ctx.stack(ev.Stk), + Cname: cname, + Arg: arg}) +} + +func (ctx *traceContext) emitArrow(ev *trace.Event, name string) { + if ev.Link == nil { + // The other end of the arrow is not captured in the trace. + // For example, a goroutine was unblocked but was not scheduled before trace stop. + return + } + if ctx.mode&modeGoroutineOriented != 0 && (!ctx.gs[ev.Link.G] || ev.Link.Ts < ctx.startTime || ev.Link.Ts > ctx.endTime) { + return + } + + if ev.P == trace.NetpollP || ev.P == trace.TimerP || ev.P == trace.SyscallP { + // Trace-viewer discards arrows if they don't start/end inside of a slice or instant. + // So emit a fake instant at the start of the arrow. + ctx.emitInstant(&trace.Event{P: ev.P, Ts: ev.Ts}, "unblock", "") + } + + color := "" + if ctx.mode&modeTaskOriented != 0 { + overlapping := false + // skip non-overlapping arrows. + for _, task := range ctx.tasks { + if _, overlapped := task.overlappingDuration(ev); overlapped { + overlapping = true + break + } + } + if !overlapping { + return + } + } + + ctx.arrowSeq++ + ctx.emit(&traceviewer.Event{Name: name, Phase: "s", TID: ctx.proc(ev), ID: ctx.arrowSeq, Time: ctx.time(ev), Stack: ctx.stack(ev.Stk), Cname: color}) + ctx.emit(&traceviewer.Event{Name: name, Phase: "t", TID: ctx.proc(ev.Link), ID: ctx.arrowSeq, Time: ctx.time(ev.Link), Cname: color}) +} + +func (ctx *traceContext) stack(stk []*trace.Frame) int { + return ctx.buildBranch(ctx.frameTree, stk) +} + +// buildBranch builds one branch in the prefix tree rooted at ctx.frameTree. +func (ctx *traceContext) buildBranch(parent frameNode, stk []*trace.Frame) int { + if len(stk) == 0 { + return parent.id + } + last := len(stk) - 1 + frame := stk[last] + stk = stk[:last] + + node, ok := parent.children[frame.PC] + if !ok { + ctx.frameSeq++ + node.id = ctx.frameSeq + node.children = make(map[uint64]frameNode) + parent.children[frame.PC] = node + ctx.consumer.consumeViewerFrame(strconv.Itoa(node.id), traceviewer.Frame{Name: fmt.Sprintf("%v:%v", frame.Fn, frame.Line), Parent: parent.id}) + } + return ctx.buildBranch(node, stk) +} + +func isSystemGoroutine(entryFn string) bool { + // This mimics runtime.isSystemGoroutine as closely as + // possible. + return entryFn != "runtime.main" && strings.HasPrefix(entryFn, "runtime.") +} + +// firstTimestamp returns the timestamp of the first event record. +func firstTimestamp() int64 { + res, _ := parseTrace() + if len(res.Events) > 0 { + return res.Events[0].Ts + } + return 0 +} + +// lastTimestamp returns the timestamp of the last event record. +func lastTimestamp() int64 { + res, _ := parseTrace() + if n := len(res.Events); n > 1 { + return res.Events[n-1].Ts + } + return 0 +} + +type jsonWriter struct { + w io.Writer + enc *json.Encoder +} + +func viewerDataTraceConsumer(w io.Writer, start, end int64) traceConsumer { + frames := make(map[string]traceviewer.Frame) + enc := json.NewEncoder(w) + written := 0 + index := int64(-1) + + io.WriteString(w, "{") + return traceConsumer{ + consumeTimeUnit: func(unit string) { + io.WriteString(w, `"displayTimeUnit":`) + enc.Encode(unit) + io.WriteString(w, ",") + }, + consumeViewerEvent: func(v *traceviewer.Event, required bool) { + index++ + if !required && (index < start || index > end) { + // not in the range. Skip! + return + } + if written == 0 { + io.WriteString(w, `"traceEvents": [`) + } + if written > 0 { + io.WriteString(w, ",") + } + enc.Encode(v) + // TODO: get rid of the extra \n inserted by enc.Encode. + // Same should be applied to splittingTraceConsumer. + written++ + }, + consumeViewerFrame: func(k string, v traceviewer.Frame) { + frames[k] = v + }, + flush: func() { + io.WriteString(w, `], "stackFrames":`) + enc.Encode(frames) + io.WriteString(w, `}`) + }, + } +} + +// Mapping from more reasonable color names to the reserved color names in +// https://github.com/catapult-project/catapult/blob/master/tracing/tracing/base/color_scheme.html#L50 +// The chrome trace viewer allows only those as cname values. +const ( + colorLightMauve = "thread_state_uninterruptible" // 182, 125, 143 + colorOrange = "thread_state_iowait" // 255, 140, 0 + colorSeafoamGreen = "thread_state_running" // 126, 200, 148 + colorVistaBlue = "thread_state_runnable" // 133, 160, 210 + colorTan = "thread_state_unknown" // 199, 155, 125 + colorIrisBlue = "background_memory_dump" // 0, 180, 180 + colorMidnightBlue = "light_memory_dump" // 0, 0, 180 + colorDeepMagenta = "detailed_memory_dump" // 180, 0, 180 + colorBlue = "vsync_highlight_color" // 0, 0, 255 + colorGrey = "generic_work" // 125, 125, 125 + colorGreen = "good" // 0, 125, 0 + colorDarkGoldenrod = "bad" // 180, 125, 0 + colorPeach = "terrible" // 180, 0, 0 + colorBlack = "black" // 0, 0, 0 + colorLightGrey = "grey" // 221, 221, 221 + colorWhite = "white" // 255, 255, 255 + colorYellow = "yellow" // 255, 255, 0 + colorOlive = "olive" // 100, 100, 0 + colorCornflowerBlue = "rail_response" // 67, 135, 253 + colorSunsetOrange = "rail_animation" // 244, 74, 63 + colorTangerine = "rail_idle" // 238, 142, 0 + colorShamrockGreen = "rail_load" // 13, 168, 97 + colorGreenishYellow = "startup" // 230, 230, 0 + colorDarkGrey = "heap_dump_stack_frame" // 128, 128, 128 + colorTawny = "heap_dump_child_node_arrow" // 204, 102, 0 + colorLemon = "cq_build_running" // 255, 255, 119 + colorLime = "cq_build_passed" // 153, 238, 102 + colorPink = "cq_build_failed" // 238, 136, 136 + colorSilver = "cq_build_abandoned" // 187, 187, 187 + colorManzGreen = "cq_build_attempt_runnig" // 222, 222, 75 + colorKellyGreen = "cq_build_attempt_passed" // 108, 218, 35 + colorAnotherGrey = "cq_build_attempt_failed" // 187, 187, 187 +) + +var colorForTask = []string{ + colorLightMauve, + colorOrange, + colorSeafoamGreen, + colorVistaBlue, + colorTan, + colorMidnightBlue, + colorIrisBlue, + colorDeepMagenta, + colorGreen, + colorDarkGoldenrod, + colorPeach, + colorOlive, + colorCornflowerBlue, + colorSunsetOrange, + colorTangerine, + colorShamrockGreen, + colorTawny, + colorLemon, + colorLime, + colorPink, + colorSilver, + colorManzGreen, + colorKellyGreen, +} + +func pickTaskColor(id uint64) string { + idx := id % uint64(len(colorForTask)) + return colorForTask[idx] +} |