]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/trace: beautify goroutine page
authorHana (Hyang-Ah) Kim <hyangah@gmail.com>
Thu, 22 Mar 2018 16:51:13 +0000 (12:51 -0400)
committerHyang-Ah Hana Kim <hyangah@gmail.com>
Mon, 26 Mar 2018 15:36:56 +0000 (15:36 +0000)
- 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>
src/cmd/trace/goroutines.go

index d0d428cbe2f4d490f0f0176d5d29c1f76086ecf9..548871a82c20ad8c8df9f5eb4a7c8129ab863658 100644 (file)
@@ -10,10 +10,13 @@ import (
        "fmt"
        "html/template"
        "internal/trace"
+       "log"
        "net/http"
+       "reflect"
        "sort"
        "strconv"
        "sync"
+       "time"
 )
 
 func init() {
@@ -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
@@ -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)
        }
-       sort.Sort(glist)
-       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)
+               return
+       }
 }
 
 var templGoroutines = template.Must(template.New("").Parse(`
@@ -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)
                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 gdescList
+       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
+               }
        }
-       sort.Sort(glist)
+
+       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)
                return
        }
 }
 
-var templGoroutine = template.Must(template.New("").Parse(`
-<html>
-<body>
-<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
+       },
+}).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> 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></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> {{.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>
+    <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}}
+        </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>
-</body>
-</html>
 `))