Skip to content


cmd/trace: beautify goroutine page
Browse files Browse the repository at this point in the history
- Summary: also includes links to pprof data.
- Sortable table: sorting is done on server-side. The intention is
  that later, I want to add pagination feature and limit the page
  size the browser has to handle.
- Stacked horizontal bar graph to present total time breakdown.
- Human-friendly time representation.
- No dependency on external fancy javascript libraries to allow
  it to function without an internet connection.

Change-Id: I91e5c26746e59ad0329dfb61e096e11f768c7b73
Run-TryBot: Hyang-Ah Hana Kim <>
TryBot-Result: Gobot Gobot <>
Reviewed-by: Andrew Bonventre <>
Reviewed-by: Heschi Kreinick <>
  • Loading branch information
hyangah committed Mar 26, 2018
1 parent d2dd2e1 commit ea1f483
Showing 1 changed file with 175 additions and 59 deletions.
234 changes: 175 additions & 59 deletions src/cmd/trace/goroutines.go
Expand Up @@ -10,10 +10,13 @@ import (

func init() {
Expand All @@ -29,34 +32,6 @@ type gtype struct {
ExecTime int64 // Total execution time of all goroutines in this group.

type gtypeList []gtype

func (l gtypeList) Len() int {
return len(l)

func (l gtypeList) Less(i, j int) bool {
return l[i].ExecTime > l[j].ExecTime

func (l gtypeList) Swap(i, j int) {
l[i], l[j] = l[j], l[i]

type gdescList []*trace.GDesc

func (l gdescList) Len() int {
return len(l)

func (l gdescList) Less(i, j int) bool {
return l[i].TotalTime > l[j].TotalTime

func (l gdescList) Swap(i, j int) {
l[i], l[j] = l[j], l[i]

var (
gsInit sync.Once
gs map[uint64]*trace.GDesc
Expand Down Expand Up @@ -86,13 +61,17 @@ func httpGoroutines(w http.ResponseWriter, r *http.Request) {
gs1.ExecTime += g.ExecTime
gss[g.PC] = gs1
var glist gtypeList
var glist []gtype
for k, v := range gss {
v.ID = k
glist = append(glist, v)
templGoroutines.Execute(w, glist)
sort.Slice(glist, func(i, j int) bool { return glist[i].ExecTime > glist[j].ExecTime })
w.Header().Set("Content-Type", "text/html;charset=utf-8")
if err := templGoroutines.Execute(w, glist); err != nil {
log.Printf("failed to execute template: %v", err)

var templGoroutines = template.Must(template.New("").Parse(`
Expand All @@ -108,64 +87,201 @@ Goroutines: <br>

// httpGoroutine serves list of goroutines in a particular group.
func httpGoroutine(w http.ResponseWriter, r *http.Request) {
// TODO(hyangah): support format=csv (raw data)

events, err := parseEvents()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)

pc, err := strconv.ParseUint(r.FormValue("id"), 10, 64)
if err != nil {
http.Error(w, fmt.Sprintf("failed to parse id parameter '%v': %v", r.FormValue("id"), err), http.StatusInternalServerError)
var glist gdescList
var (
glist []*trace.GDesc
name string
totalExecTime, execTime int64
maxTotalTime int64

for _, g := range gs {
totalExecTime += g.ExecTime

if g.PC != pc {
glist = append(glist, g)
name = g.Name
execTime += g.ExecTime
if maxTotalTime < g.TotalTime {
maxTotalTime = g.TotalTime

execTimePercent := ""
if totalExecTime > 0 {
execTimePercent = fmt.Sprintf("%.2f%%", float64(execTime)/float64(totalExecTime)*100)

sortby := r.FormValue("sortby")
_, ok := reflect.TypeOf(trace.GDesc{}).FieldByNameFunc(func(s string) bool {
return s == sortby
if !ok {
sortby = "TotalTime"

sort.Slice(glist, func(i, j int) bool {
ival := reflect.ValueOf(glist[i]).Elem().FieldByName(sortby).Int()
jval := reflect.ValueOf(glist[j]).Elem().FieldByName(sortby).Int()
return ival > jval

err = templGoroutine.Execute(w, struct {
PC uint64
GList gdescList
}{pc, glist})
Name string
PC uint64
N int
ExecTimePercent string
MaxTotal int64
GList []*trace.GDesc
Name: name,
PC: pc,
N: len(glist),
ExecTimePercent: execTimePercent,
MaxTotal: maxTotalTime,
GList: glist})
if err != nil {
http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)

var templGoroutine = template.Must(template.New("").Parse(`
<table border="1" sortable="1">
var templGoroutine = template.Must(template.New("").Funcs(template.FuncMap{
"prettyDuration": func(nsec int64) template.HTML {
d := time.Duration(nsec) * time.Nanosecond
return template.HTML(niceDuration(d))
"percent": func(dividened, divisor int64) template.HTML {
if divisor == 0 {
return ""
return template.HTML(fmt.Sprintf("(%.1f%%)", float64(dividened)/float64(divisor)*100))
"barLen": func(dividened, divisor int64) template.HTML {
if divisor == 0 {
return "0"
return template.HTML(fmt.Sprintf("%.2f%%", float64(dividened)/float64(divisor)*100))
"unknownTime": func(desc *trace.GDesc) int64 {
sum := desc.ExecTime + desc.IOTime + desc.BlockTime + desc.SyscallTime + desc.SchedWaitTime
if sum < desc.TotalTime {
return desc.TotalTime - sum
return 0
<!DOCTYPE html>
<title>Goroutine {{.Name}}</title>
th {
background-color: #050505;
color: #fff;
table {
border-collapse: collapse;
.details tr:hover {
background-color: #f2f2f2;
.details td {
text-align: right;
border: 1px solid black;
.details {
text-align: left;
.stacked-bar-graph {
width: 300px;
height: 10px;
color: #414042;
white-space: nowrap;
font-size: 5px;
.stacked-bar-graph span {
display: inline-block;
width: 100%;
height: 100%;
box-sizing: border-box;
float: left;
padding: 0;
.unknown-time { background-color: #636363; }
.exec-time { background-color: #d7191c; }
.io-time { background-color: #fdae61; }
.block-time { background-color: #d01c8b; }
.syscall-time { background-color: #7b3294; }
.sched-time { background-color: #2c7bb6; }
function reloadTable(key, value) {
let params = new URLSearchParams(;
params.set(key, value); = params.toString();
<table class="summary">
<tr><td>Goroutine Name:</td><td>{{.Name}}</td></tr>
<tr><td>Number of Goroutines:</td><td>{{.N}}</td></tr>
<tr><td>Execution Time:</td><td>{{.ExecTimePercent}} of total program execution time </td> </tr>
<tr><td>Network Wait Time:</td><td> <a href="/io?id={{.PC}}">graph</a><a href="/io?id={{.PC}}&raw=1" download="io.profile">(download)</a></td></tr>
<tr><td>Sync Block Time:</td><td> <a href="/block?id={{.PC}}">graph</a><a href="/block?id={{.PC}}&raw=1" download="block.profile">(download)</a></td></tr>
<tr><td>Blocking Syscall Time:</td><td> <a href="/syscall?id={{.PC}}">graph</a><a href="/syscall?id={{.PC}}&raw=1" download="syscall.profile">(download)</a></td></tr>
<tr><td>Scheduler Wait Time:</td><td> <a href="/sched?id={{.PC}}">graph</a><a href="/sched?id={{.PC}}&raw=1" download="sched.profile">(download)</a></td></tr>
<table class="details">
<th> Goroutine </th>
<th> Total time, ns </th>
<th> Execution time, ns </th>
<th> <a href="/io?id={{.PC}}">Network wait time, ns</a><a href="/io?id={{.PC}}&raw=1" download="io.profile">⬇</a> </th>
<th> <a href="/block?id={{.PC}}">Sync block time, ns</a><a href="/block?id={{.PC}}&raw=1" download="block.profile">⬇</a> </th>
<th> <a href="/syscall?id={{.PC}}">Blocking syscall time, ns</a><a href="/syscall?id={{.PC}}&raw=1" download="syscall.profile">⬇</a> </th>
<th> <a href="/sched?id={{.PC}}">Scheduler wait time, ns</a><a href="/sched?id={{.PC}}&raw=1" download="sched.profile">⬇</a> </th>
<th> GC sweeping time, ns </th>
<th> GC pause time, ns </th>
<th> Goroutine</th>
<th onclick="reloadTable('sortby', 'TotalTime')"> Total</th>
<th onclick="reloadTable('sortby', 'ExecTime')" class="exec-time"> Execution</th>
<th onclick="reloadTable('sortby', 'IOTime')" class="io-time"> Network wait</th>
<th onclick="reloadTable('sortby', 'BlockTime')" class="block-time"> Sync block </th>
<th onclick="reloadTable('sortby', 'SyscallTime')" class="syscall-time"> Blocking syscall</th>
<th onclick="reloadTable('sortby', 'SchedWaitTime')" class="sched-time"> Scheduler wait</th>
<th onclick="reloadTable('sortby', 'SweepTime')"> GC sweeping</th>
<th onclick="reloadTable('sortby', 'GCTime')"> GC pause</th>
{{range .GList}}
<td> <a href="/trace?goid={{.ID}}">{{.ID}}</a> </td>
<td> {{.TotalTime}} </td>
<td> {{.ExecTime}} </td>
<td> {{.IOTime}} </td>
<td> {{.BlockTime}} </td>
<td> {{.SyscallTime}} </td>
<td> {{.SchedWaitTime}} </td>
<td> {{.SweepTime}} </td>
<td> {{.GCTime}} </td>
<td> {{prettyDuration .TotalTime}} </td>
<div class="stacked-bar-graph">
{{if unknownTime .}}<span style="width:{{barLen (unknownTime .) $.MaxTotal}}" class="unknown-time">&nbsp;</span>{{end}}
{{if .ExecTime}}<span style="width:{{barLen .ExecTime $.MaxTotal}}" class="exec-time">&nbsp;</span>{{end}}
{{if .IOTime}}<span style="width:{{barLen .IOTime $.MaxTotal}}" class="io-time">&nbsp;</span>{{end}}
{{if .BlockTime}}<span style="width:{{barLen .BlockTime $.MaxTotal}}" class="block-time">&nbsp;</span>{{end}}
{{if .SyscallTime}}<span style="width:{{barLen .SyscallTime $.MaxTotal}}" class="syscall-time">&nbsp;</span>{{end}}
{{if .SchedWaitTime}}<span style="width:{{barLen .SchedWaitTime $.MaxTotal}}" class="sched-time">&nbsp;</span>{{end}}
<td> {{prettyDuration .ExecTime}}</td>
<td> {{prettyDuration .IOTime}}</td>
<td> {{prettyDuration .BlockTime}}</td>
<td> {{prettyDuration .SyscallTime}}</td>
<td> {{prettyDuration .SchedWaitTime}}</td>
<td> {{prettyDuration .SweepTime}} {{percent .SweepTime .TotalTime}}</td>
<td> {{prettyDuration .GCTime}} {{percent .GCTime .TotalTime}}</td>

0 comments on commit ea1f483

Please sign in to comment.