| // Copyright 2023 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 trace |
| |
| import ( |
| "cmp" |
| "fmt" |
| "html/template" |
| "internal/trace" |
| "internal/trace/traceviewer" |
| tracev2 "internal/trace/v2" |
| "log" |
| "net/http" |
| "slices" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| // GoroutinesHandlerFunc returns a HandlerFunc that serves list of goroutine groups. |
| func GoroutinesHandlerFunc(summaries map[tracev2.GoID]*trace.GoroutineSummary) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| // goroutineGroup describes a group of goroutines grouped by start PC. |
| type goroutineGroup struct { |
| ID uint64 // Unique identifier (PC). |
| Name string // Start function. |
| N int // Total number of goroutines in this group. |
| ExecTime time.Duration // Total execution time of all goroutines in this group. |
| } |
| // Accumulate groups by PC. |
| groupsByPC := make(map[uint64]goroutineGroup) |
| for _, summary := range summaries { |
| group := groupsByPC[summary.PC] |
| group.ID = summary.PC |
| group.Name = summary.Name |
| group.N++ |
| group.ExecTime += summary.ExecTime |
| groupsByPC[summary.PC] = group |
| } |
| var groups []goroutineGroup |
| for pc, group := range groupsByPC { |
| group.ID = pc |
| // If goroutine didn't run during the trace (no sampled PC), |
| // the v.ID and v.Name will be zero value. |
| if group.ID == 0 && group.Name == "" { |
| group.Name = "(Inactive, no stack trace sampled)" |
| } |
| groups = append(groups, group) |
| } |
| slices.SortFunc(groups, func(a, b goroutineGroup) int { |
| return cmp.Compare(b.ExecTime, a.ExecTime) |
| }) |
| w.Header().Set("Content-Type", "text/html;charset=utf-8") |
| if err := templGoroutines.Execute(w, groups); err != nil { |
| log.Printf("failed to execute template: %v", err) |
| return |
| } |
| } |
| } |
| |
| var templGoroutines = template.Must(template.New("").Parse(` |
| <html> |
| <style>` + traceviewer.CommonStyle + ` |
| table { |
| border-collapse: collapse; |
| } |
| td, |
| th { |
| border: 1px solid black; |
| padding-left: 8px; |
| padding-right: 8px; |
| padding-top: 4px; |
| padding-bottom: 4px; |
| } |
| </style> |
| <body> |
| <h1>Goroutines</h1> |
| Below is a table of all goroutines in the trace grouped by start location and sorted by the total execution time of the group.<br> |
| <br> |
| Click a start location to view more details about that group.<br> |
| <br> |
| <table> |
| <tr> |
| <th>Start location</th> |
| <th>Count</th> |
| <th>Total execution time</th> |
| </tr> |
| {{range $}} |
| <tr> |
| <td><code><a href="/goroutine?id={{.ID}}">{{.Name}}</a></code></td> |
| <td>{{.N}}</td> |
| <td>{{.ExecTime}}</td> |
| </tr> |
| {{end}} |
| </table> |
| </body> |
| </html> |
| `)) |
| |
| // GoroutineHandler creates a handler that serves information about |
| // goroutines in a particular group. |
| func GoroutineHandler(summaries map[tracev2.GoID]*trace.GoroutineSummary) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| 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 |
| } |
| |
| type goroutine struct { |
| *trace.GoroutineSummary |
| NonOverlappingStats map[string]time.Duration |
| HasRangeTime bool |
| } |
| |
| // Collect all the goroutines in the group. |
| var ( |
| goroutines []goroutine |
| name string |
| totalExecTime, execTime time.Duration |
| maxTotalTime time.Duration |
| ) |
| validNonOverlappingStats := make(map[string]struct{}) |
| validRangeStats := make(map[string]struct{}) |
| for _, summary := range summaries { |
| totalExecTime += summary.ExecTime |
| |
| if summary.PC != pc { |
| continue |
| } |
| nonOverlappingStats := summary.NonOverlappingStats() |
| for name := range nonOverlappingStats { |
| validNonOverlappingStats[name] = struct{}{} |
| } |
| var totalRangeTime time.Duration |
| for name, dt := range summary.RangeTime { |
| validRangeStats[name] = struct{}{} |
| totalRangeTime += dt |
| } |
| goroutines = append(goroutines, goroutine{ |
| GoroutineSummary: summary, |
| NonOverlappingStats: nonOverlappingStats, |
| HasRangeTime: totalRangeTime != 0, |
| }) |
| name = summary.Name |
| execTime += summary.ExecTime |
| if maxTotalTime < summary.TotalTime { |
| maxTotalTime = summary.TotalTime |
| } |
| } |
| |
| // Compute the percent of total execution time these goroutines represent. |
| execTimePercent := "" |
| if totalExecTime > 0 { |
| execTimePercent = fmt.Sprintf("%.2f%%", float64(execTime)/float64(totalExecTime)*100) |
| } |
| |
| // Sort. |
| sortBy := r.FormValue("sortby") |
| if _, ok := validNonOverlappingStats[sortBy]; ok { |
| slices.SortFunc(goroutines, func(a, b goroutine) int { |
| return cmp.Compare(b.NonOverlappingStats[sortBy], a.NonOverlappingStats[sortBy]) |
| }) |
| } else { |
| // Sort by total time by default. |
| slices.SortFunc(goroutines, func(a, b goroutine) int { |
| return cmp.Compare(b.TotalTime, a.TotalTime) |
| }) |
| } |
| |
| // Write down all the non-overlapping stats and sort them. |
| allNonOverlappingStats := make([]string, 0, len(validNonOverlappingStats)) |
| for name := range validNonOverlappingStats { |
| allNonOverlappingStats = append(allNonOverlappingStats, name) |
| } |
| slices.SortFunc(allNonOverlappingStats, func(a, b string) int { |
| if a == b { |
| return 0 |
| } |
| if a == "Execution time" { |
| return -1 |
| } |
| if b == "Execution time" { |
| return 1 |
| } |
| return cmp.Compare(a, b) |
| }) |
| |
| // Write down all the range stats and sort them. |
| allRangeStats := make([]string, 0, len(validRangeStats)) |
| for name := range validRangeStats { |
| allRangeStats = append(allRangeStats, name) |
| } |
| sort.Strings(allRangeStats) |
| |
| err = templGoroutine.Execute(w, struct { |
| Name string |
| PC uint64 |
| N int |
| ExecTimePercent string |
| MaxTotal time.Duration |
| Goroutines []goroutine |
| NonOverlappingStats []string |
| RangeStats []string |
| }{ |
| Name: name, |
| PC: pc, |
| N: len(goroutines), |
| ExecTimePercent: execTimePercent, |
| MaxTotal: maxTotalTime, |
| Goroutines: goroutines, |
| NonOverlappingStats: allNonOverlappingStats, |
| RangeStats: allRangeStats, |
| }) |
| if err != nil { |
| http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) |
| return |
| } |
| } |
| } |
| |
| func stat2Color(statName string) string { |
| color := "#636363" |
| if strings.HasPrefix(statName, "Block time") { |
| color = "#d01c8b" |
| } |
| switch statName { |
| case "Sched wait time": |
| color = "#2c7bb6" |
| case "Syscall execution time": |
| color = "#7b3294" |
| case "Execution time": |
| color = "#d7191c" |
| } |
| return color |
| } |
| |
| var templGoroutine = template.Must(template.New("").Funcs(template.FuncMap{ |
| "percent": func(dividend, divisor time.Duration) template.HTML { |
| if divisor == 0 { |
| return "" |
| } |
| return template.HTML(fmt.Sprintf("(%.1f%%)", float64(dividend)/float64(divisor)*100)) |
| }, |
| "headerStyle": func(statName string) template.HTMLAttr { |
| return template.HTMLAttr(fmt.Sprintf("style=\"background-color: %s;\"", stat2Color(statName))) |
| }, |
| "barStyle": func(statName string, dividend, divisor time.Duration) template.HTMLAttr { |
| width := "0" |
| if divisor != 0 { |
| width = fmt.Sprintf("%.2f%%", float64(dividend)/float64(divisor)*100) |
| } |
| return template.HTMLAttr(fmt.Sprintf("style=\"width: %s; background-color: %s;\"", width, stat2Color(statName))) |
| }, |
| }).Parse(` |
| <!DOCTYPE html> |
| <title>Goroutines: {{.Name}}</title> |
| <style>` + traceviewer.CommonStyle + ` |
| th { |
| background-color: #050505; |
| color: #fff; |
| } |
| th.link { |
| cursor: pointer; |
| } |
| table { |
| border-collapse: collapse; |
| } |
| td, |
| th { |
| padding-left: 8px; |
| padding-right: 8px; |
| padding-top: 4px; |
| padding-bottom: 4px; |
| } |
| .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; |
| } |
| </style> |
| |
| <script> |
| function reloadTable(key, value) { |
| let params = new URLSearchParams(window.location.search); |
| params.set(key, value); |
| window.location.search = params.toString(); |
| } |
| </script> |
| |
| <h1>Goroutines</h1> |
| |
| Table of contents |
| <ul> |
| <li><a href="#summary">Summary</a></li> |
| <li><a href="#breakdown">Breakdown</a></li> |
| <li><a href="#ranges">Special ranges</a></li> |
| </ul> |
| |
| <h3 id="summary">Summary</h3> |
| |
| <table class="summary"> |
| <tr> |
| <td>Goroutine start location:</td> |
| <td><code>{{.Name}}</code></td> |
| </tr> |
| <tr> |
| <td>Count:</td> |
| <td>{{.N}}</td> |
| </tr> |
| <tr> |
| <td>Execution Time:</td> |
| <td>{{.ExecTimePercent}} of total program execution time </td> |
| </tr> |
| <tr> |
| <td>Network wait profile:</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 profile:</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>Syscall profile:</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 profile:</td> |
| <td> <a href="/sched?id={{.PC}}">graph</a> <a href="/sched?id={{.PC}}&raw=1" download="sched.profile">(download)</a></td> |
| </tr> |
| </table> |
| |
| <h3 id="breakdown">Breakdown</h3> |
| |
| The table below breaks down where each goroutine is spent its time during the |
| traced period. |
| All of the columns except total time are non-overlapping. |
| <br> |
| <br> |
| |
| <table class="details"> |
| <tr> |
| <th> Goroutine</th> |
| <th class="link" onclick="reloadTable('sortby', 'Total time')"> Total</th> |
| <th></th> |
| {{range $.NonOverlappingStats}} |
| <th class="link" onclick="reloadTable('sortby', '{{.}}')" {{headerStyle .}}> {{.}}</th> |
| {{end}} |
| </tr> |
| {{range .Goroutines}} |
| <tr> |
| <td> <a href="/trace?goid={{.ID}}">{{.ID}}</a> </td> |
| <td> {{ .TotalTime.String }} </td> |
| <td> |
| <div class="stacked-bar-graph"> |
| {{$Goroutine := .}} |
| {{range $.NonOverlappingStats}} |
| {{$Time := index $Goroutine.NonOverlappingStats .}} |
| {{if $Time}} |
| <span {{barStyle . $Time $.MaxTotal}}> </span> |
| {{end}} |
| {{end}} |
| </div> |
| </td> |
| {{$Goroutine := .}} |
| {{range $.NonOverlappingStats}} |
| {{$Time := index $Goroutine.NonOverlappingStats .}} |
| <td> {{$Time.String}}</td> |
| {{end}} |
| </tr> |
| {{end}} |
| </table> |
| |
| <h3 id="ranges">Special ranges</h3> |
| |
| The table below describes how much of the traced period each goroutine spent in |
| certain special time ranges. |
| If a goroutine has spent no time in any special time ranges, it is excluded from |
| the table. |
| For example, how much time it spent helping the GC. Note that these times do |
| overlap with the times from the first table. |
| In general the goroutine may not be executing in these special time ranges. |
| For example, it may have blocked while trying to help the GC. |
| This must be taken into account when interpreting the data. |
| <br> |
| <br> |
| |
| <table class="details"> |
| <tr> |
| <th> Goroutine</th> |
| <th> Total</th> |
| {{range $.RangeStats}} |
| <th {{headerStyle .}}> {{.}}</th> |
| {{end}} |
| </tr> |
| {{range .Goroutines}} |
| {{if .HasRangeTime}} |
| <tr> |
| <td> <a href="/trace?goid={{.ID}}">{{.ID}}</a> </td> |
| <td> {{ .TotalTime.String }} </td> |
| {{$Goroutine := .}} |
| {{range $.RangeStats}} |
| {{$Time := index $Goroutine.RangeTime .}} |
| <td> {{$Time.String}}</td> |
| {{end}} |
| </tr> |
| {{end}} |
| {{end}} |
| </table> |
| `)) |