mirror of
https://github.com/golang/go
synced 2024-11-08 12:56:16 -07:00
ea1f483240
- 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 Reviewed-on: https://go-review.googlesource.com/102156 Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Andrew Bonventre <andybons@golang.org> Reviewed-by: Heschi Kreinick <heschi@google.com>
288 lines
8.5 KiB
Go
288 lines
8.5 KiB
Go
// 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.
|
|
|
|
// Goroutine-related profiles.
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"internal/trace"
|
|
"log"
|
|
"net/http"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
func init() {
|
|
http.HandleFunc("/goroutines", httpGoroutines)
|
|
http.HandleFunc("/goroutine", httpGoroutine)
|
|
}
|
|
|
|
// gtype describes a group of goroutines grouped by start PC.
|
|
type gtype struct {
|
|
ID uint64 // Unique identifier (PC).
|
|
Name string // Start function.
|
|
N int // Total number of goroutines in this group.
|
|
ExecTime int64 // Total execution time of all goroutines in this group.
|
|
}
|
|
|
|
var (
|
|
gsInit sync.Once
|
|
gs map[uint64]*trace.GDesc
|
|
)
|
|
|
|
// analyzeGoroutines generates statistics about execution of all goroutines and stores them in gs.
|
|
func analyzeGoroutines(events []*trace.Event) {
|
|
gsInit.Do(func() {
|
|
gs = trace.GoroutineStats(events)
|
|
})
|
|
}
|
|
|
|
// httpGoroutines serves list of goroutine groups.
|
|
func httpGoroutines(w http.ResponseWriter, r *http.Request) {
|
|
events, err := parseEvents()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
analyzeGoroutines(events)
|
|
gss := make(map[uint64]gtype)
|
|
for _, g := range gs {
|
|
gs1 := gss[g.PC]
|
|
gs1.ID = g.PC
|
|
gs1.Name = g.Name
|
|
gs1.N++
|
|
gs1.ExecTime += g.ExecTime
|
|
gss[g.PC] = gs1
|
|
}
|
|
var glist []gtype
|
|
for k, v := range gss {
|
|
v.ID = k
|
|
glist = append(glist, v)
|
|
}
|
|
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)
|
|
return
|
|
}
|
|
}
|
|
|
|
var templGoroutines = template.Must(template.New("").Parse(`
|
|
<html>
|
|
<body>
|
|
Goroutines: <br>
|
|
{{range $}}
|
|
<a href="/goroutine?id={{.ID}}">{{.Name}}</a> N={{.N}} <br>
|
|
{{end}}
|
|
</body>
|
|
</html>
|
|
`))
|
|
|
|
// 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)
|
|
return
|
|
}
|
|
|
|
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)
|
|
return
|
|
}
|
|
analyzeGoroutines(events)
|
|
var (
|
|
glist []*trace.GDesc
|
|
name string
|
|
totalExecTime, execTime int64
|
|
maxTotalTime int64
|
|
)
|
|
|
|
for _, g := range gs {
|
|
totalExecTime += g.ExecTime
|
|
|
|
if g.PC != pc {
|
|
continue
|
|
}
|
|
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 {
|
|
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)
|
|
return
|
|
}
|
|
}
|
|
|
|
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
|
|
},
|
|
}).Parse(`
|
|
<!DOCTYPE html>
|
|
<title>Goroutine {{.Name}}</title>
|
|
<style>
|
|
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 td.id {
|
|
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; }
|
|
</style>
|
|
|
|
<script>
|
|
function reloadTable(key, value) {
|
|
let params = new URLSearchParams(window.location.search);
|
|
params.set(key, value);
|
|
window.location.search = params.toString();
|
|
}
|
|
</script>
|
|
|
|
<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>
|
|
<p>
|
|
<table class="details">
|
|
<tr>
|
|
<th> Goroutine</th>
|
|
<th onclick="reloadTable('sortby', 'TotalTime')"> Total</th>
|
|
<th></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>
|
|
</tr>
|
|
{{range .GList}}
|
|
<tr>
|
|
<td> <a href="/trace?goid={{.ID}}">{{.ID}}</a> </td>
|
|
<td> {{prettyDuration .TotalTime}} </td>
|
|
<td>
|
|
<div class="stacked-bar-graph">
|
|
{{if unknownTime .}}<span style="width:{{barLen (unknownTime .) $.MaxTotal}}" class="unknown-time"> </span>{{end}}
|
|
{{if .ExecTime}}<span style="width:{{barLen .ExecTime $.MaxTotal}}" class="exec-time"> </span>{{end}}
|
|
{{if .IOTime}}<span style="width:{{barLen .IOTime $.MaxTotal}}" class="io-time"> </span>{{end}}
|
|
{{if .BlockTime}}<span style="width:{{barLen .BlockTime $.MaxTotal}}" class="block-time"> </span>{{end}}
|
|
{{if .SyscallTime}}<span style="width:{{barLen .SyscallTime $.MaxTotal}}" class="syscall-time"> </span>{{end}}
|
|
{{if .SchedWaitTime}}<span style="width:{{barLen .SchedWaitTime $.MaxTotal}}" class="sched-time"> </span>{{end}}
|
|
</div>
|
|
</td>
|
|
<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>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
`))
|