From 427894835b11e37e899adb21347ad591f2c47ffc Mon Sep 17 00:00:00 2001 From: Martin Sucha Date: Tue, 23 Apr 2024 12:07:32 +0200 Subject: [PATCH] Show number of running, runnable and blocked goroutines --- cmd/gotraceui/canvas.go | 27 +++++++++++++++------ cmd/gotraceui/main.go | 40 ++++++++++++++++++++++++++----- trace/ptrace/ptrace.go | 53 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/cmd/gotraceui/canvas.go b/cmd/gotraceui/canvas.go index a3d6cbf..83b962c 100644 --- a/cmd/gotraceui/canvas.go +++ b/cmd/gotraceui/canvas.go @@ -132,7 +132,8 @@ type Canvas struct { scrollbar widget.Scrollbar axis Axis - memoryGraph Plot + memoryGraph Plot + goroutineGraph Plot // State for dragging the canvas drag struct { @@ -209,7 +210,7 @@ func NewCanvasInto(cv *Canvas, dwin *DebugWindow, t *Trace) { *cv = Canvas{ resizeMemoryTimelines: component.Resize{ Axis: layout.Vertical, - Ratio: 0.1, + Ratio: 0.2, }, axis: Axis{cv: cv, anchor: AxisAnchorCenter}, trace: t, @@ -924,13 +925,25 @@ func (cv *Canvas) Layout(win *theme.Window, gtx layout.Context) layout.Dimension func(gtx layout.Context) layout.Dimensions { return theme.Resize(win.Theme, &cv.resizeMemoryTimelines).Layout(win, gtx, - // Memory graph func(win *theme.Window, gtx layout.Context) layout.Dimensions { - defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() - cv.drag.drag.Add(gtx.Ops) + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions { + // Memory graph + defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() + cv.drag.drag.Add(gtx.Ops) + + dims := cv.memoryGraph.Layout(win, gtx, cv) + return dims + }), + layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions { + // Goroutine graph + defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() + cv.drag.drag.Add(gtx.Ops) - dims := cv.memoryGraph.Layout(win, gtx, cv) - return dims + dims := cv.goroutineGraph.Layout(win, gtx, cv) + return dims + }), + ) }, // Timelines and scrollbar diff --git a/cmd/gotraceui/main.go b/cmd/gotraceui/main.go index 80e279f..bbc6c21 100644 --- a/cmd/gotraceui/main.go +++ b/cmd/gotraceui/main.go @@ -1111,6 +1111,7 @@ func (mwin *MainWindow) showFileOpenDialog() { func (mwin *MainWindow) loadTraceImpl(res loadTraceResult) { NewCanvasInto(&mwin.canvas, mwin.debugWindow, res.trace) mwin.canvas.memoryGraph = res.plot + mwin.canvas.goroutineGraph = res.goroutinePlot mwin.canvas.timelines = append(mwin.canvas.timelines, res.timelines...) for _, tl := range res.timelines { @@ -1436,9 +1437,10 @@ func main() { } type loadTraceResult struct { - trace *Trace - plot Plot - timelines []*Timeline + trace *Trace + plot Plot + goroutinePlot Plot + timelines []*Timeline } type progresser interface { @@ -1581,6 +1583,31 @@ func loadTrace(f io.Reader, p progresser, cv *Canvas) (loadTraceResult, error) { }, ) + gg := Plot{ + Name: "Goroutine count", + Unit: "goroutines", + } + gg.AddSeries( + PlotSeries{ + Name: "Runnable", + Points: pt.Metrics["/gotraceui/sched/goroutines/runnable:goroutines"], + Style: PlotStaircase, + Color: colors[colorStateReady], + }, + PlotSeries{ + Name: "Running", + Points: pt.Metrics["/gotraceui/sched/goroutines/running:goroutines"], + Style: PlotStaircase, + Color: colors[colorStateActive], + }, + PlotSeries{ + Name: "Waiting", + Points: pt.Metrics["/gotraceui/sched/goroutines/waiting:goroutines"], + Style: PlotStaircase, + Color: colors[colorStateBlocked], + }, + ) + var goroot, gopath string for _, fn := range tr.Functions { if strings.HasPrefix(fn.Func, "runtime.") && strings.Count(fn.Func, ".") == 1 && strings.Contains(fn.File, filepath.Join("go", "src", "runtime")) && !strings.ContainsRune(fn.Func, os.PathSeparator) { @@ -1626,9 +1653,10 @@ func loadTrace(f io.Reader, p progresser, cv *Canvas) (loadTraceResult, error) { tr.TimeOffset = -tr.Start() return loadTraceResult{ - trace: tr, - plot: mg, - timelines: timelines, + trace: tr, + plot: mg, + goroutinePlot: gg, + timelines: timelines, }, nil } diff --git a/trace/ptrace/ptrace.go b/trace/ptrace/ptrace.go index 8b255b7..c893a78 100644 --- a/trace/ptrace/ptrace.go +++ b/trace/ptrace/ptrace.go @@ -438,6 +438,31 @@ func rangeActualScope(r exptrace.Range) rangeScope { } } +// runningGauge builds up a gauge over time. +type runningGauge struct { + // current is the current value of the gauge. + current int64 + // points capture the current value over time. + points []Point +} + +// add adds a value to the gauge at time t. +// It assumes that t will not decrease with subsequent calls. +func (rg *runningGauge) add(t exptrace.Time, v int64) { + rg.current += v + if idx := len(rg.points) - 1; idx >= 0 && rg.points[idx].When == t { + rg.points[idx].Value = uint64(rg.current) + } else { + rg.points = append(rg.points, Point{When: t, Value: uint64(rg.current)}) + } +} + +type goroutineMetrics struct { + runnableGoroutines runningGauge + runningGoroutines runningGauge + blockedGoroutines runningGauge +} + func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error { // OPT(dh): evaluate reading all events in one pass, then preallocating []Span slices based on the number // of events we saw for Ps and Gs. @@ -476,6 +501,7 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error synced := false userRegionDepths := map[exptrace.GoID]int{} var traceStart exptrace.Time + var gm goroutineMetrics for { ev, err := r.ReadEvent() if err != nil { @@ -490,6 +516,9 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error if tr.Events.Len() == 0 { traceStart = ev.Time() + gm.runningGoroutines.add(traceStart, 0) + gm.runnableGoroutines.add(traceStart, 0) + gm.blockedGoroutines.add(traceStart, 0) } evID := EventID(tr.Events.Len()) @@ -711,6 +740,26 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error if from == exptrace.GoUndetermined { s.Start = traceStart } + switch { + case from == exptrace.GoRunnable: + gm.runnableGoroutines.add(ev.Time(), -1) + case to == exptrace.GoRunnable: + gm.runnableGoroutines.add(ev.Time(), 1) + } + switch { + case from == exptrace.GoRunning: + gm.runningGoroutines.add(ev.Time(), -1) + case to == exptrace.GoRunning: + gm.runningGoroutines.add(ev.Time(), 1) + } + fromIsBlocked := from == exptrace.GoSyscall || from == exptrace.GoWaiting + toIsBlocked := to == exptrace.GoSyscall || to == exptrace.GoWaiting + switch { + case fromIsBlocked && !toIsBlocked: + gm.blockedGoroutines.add(ev.Time(), -1) + case !fromIsBlocked && toIsBlocked: + gm.blockedGoroutines.add(ev.Time(), 1) + } // XXX actually, for from == exptrace.GoUndetermined, we still need to do omst of the work to update P // spans. a proc may start, followed by a goroutine going from undetermined->running on that proc, and @@ -873,6 +922,10 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error } } + tr.Metrics["/gotraceui/sched/goroutines/runnable:goroutines"] = gm.runnableGoroutines.points + tr.Metrics["/gotraceui/sched/goroutines/running:goroutines"] = gm.runningGoroutines.points + tr.Metrics["/gotraceui/sched/goroutines/waiting:goroutines"] = gm.blockedGoroutines.points + return nil }