// 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> `))