Skip to content

Commit

Permalink
Show number of runnable/total goroutines
Browse files Browse the repository at this point in the history
This commit adds a rudimentary graph for the total and runnable
goroutine counts.

It only calculates the metric point once every 1<<20 nanoseconds,
so about once every 1 millisecond.
Other than being a power of two so that we don't need division,
this is is an arbitrary choice, I haven't checked the performance
impact if the value would be different.
  • Loading branch information
martin-sucha committed Apr 24, 2024
1 parent c167bd1 commit 2c512c3
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 13 deletions.
27 changes: 20 additions & 7 deletions cmd/gotraceui/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
34 changes: 28 additions & 6 deletions cmd/gotraceui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1581,6 +1583,25 @@ 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/goroutine/runnable:goroutine"],
Style: PlotFilled,
Color: colors[colorStateReady],
},
PlotSeries{
Name: "Total",
Points: pt.Metrics["/gotraceui/goroutine/total:goroutine"],
Style: PlotStaircase,
Color: oklch(70.59, 0.102, 139.64),
},
)

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) {
Expand Down Expand Up @@ -1626,9 +1647,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
}

Expand Down
74 changes: 74 additions & 0 deletions trace/ptrace/ptrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,64 @@ func rangeActualScope(r exptrace.Range) rangeScope {
}
}

const (
runningGaugeStep = 1 << 20
runningGaugeMask = runningGaugeStep - 1
)

// runningGauge tracks maximum value of a gauge and creates a time series that can be rendered.
type runningGauge struct {
// initial is the initial value of the gauge.
initial int64
// current is the current value of the gauge minus the initial value.
current int64
// currentMax is the maximum value of the gauge in the current time interval minus the initial value.
currentMax int64
// currentInterval is the end time of the last data interval.
currentInterval exptrace.Time
// series is a list of points that capture the currentMax at a given time.
// While collecting data, the series does not include the initial value.
series []Point
}

// addInitial adds to the initial value.
func (rg *runningGauge) addInitial(v int64) {
rg.initial += v
}

// 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) {
interval := t &^ runningGaugeMask
switch {
case rg.currentInterval == 0:
rg.currentInterval = interval
case interval > rg.currentInterval:
rg.series = append(rg.series, Point{rg.currentInterval - runningGaugeStep, uint64(rg.currentMax)})
rg.currentInterval = interval
rg.currentMax = rg.current
}
rg.current += v
if rg.current > rg.currentMax {
rg.currentMax = rg.current
}
}

func (rg *runningGauge) finalize() []Point {
if rg.currentInterval != 0 {
rg.series = append(rg.series, Point{rg.currentInterval - runningGaugeStep, uint64(rg.currentMax)})
}
for i := range rg.series {
rg.series[i].Value += uint64(rg.initial)
}
return rg.series
}

type goroutineMetrics struct {
totalGoroutines runningGauge
runnableGoroutines 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.
Expand Down Expand Up @@ -476,6 +534,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 {
Expand All @@ -490,6 +549,8 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error

if tr.Events.Len() == 0 {
traceStart = ev.Time()
gm.totalGoroutines.add(traceStart, 0)
gm.runnableGoroutines.add(traceStart, 0)
}

evID := EventID(tr.Events.Len())
Expand Down Expand Up @@ -709,9 +770,17 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error
}

if from == exptrace.GoUndetermined {
gm.totalGoroutines.addInitial(1)
s.Start = traceStart
}

if from == exptrace.GoRunnable {
gm.runnableGoroutines.add(ev.Time(), -1)
}
if to == exptrace.GoRunnable {
gm.runnableGoroutines.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
// we need to update the "running without G" span in the P.
Expand Down Expand Up @@ -768,6 +837,7 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error
switch to {
case exptrace.GoNotExist:
g.End = container.Some(ev.Time())
gm.totalGoroutines.add(ev.Time(), -1)
continue
case exptrace.GoRunnable:
if from == exptrace.GoNotExist {
Expand All @@ -778,6 +848,7 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error
// ev.Goroutine is the goroutine that's creating us, versus g, which is the
// created goroutine.
addEventToCurrentSpan(ev.Goroutine(), evID)
gm.totalGoroutines.add(ev.Time(), 1)
} else {
if trans.Reason == "runtime.GoSched" || trans.Reason == "runtime.Gosched" {
s.State = StateInactive
Expand Down Expand Up @@ -873,6 +944,9 @@ func processEvents(r *exptrace.Reader, tr *Trace, progress func(float64)) error
}
}

tr.Metrics["/gotraceui/goroutine/total:goroutine"] = gm.totalGoroutines.finalize()
tr.Metrics["/gotraceui/goroutine/runnable:goroutine"] = gm.runnableGoroutines.finalize()

return nil
}

Expand Down

0 comments on commit 2c512c3

Please sign in to comment.