1// Copyright 2014 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Goroutine-related profiles. 6 7package main 8 9import ( 10 "cmp" 11 "fmt" 12 "html/template" 13 "internal/trace" 14 "internal/trace/traceviewer" 15 "log" 16 "net/http" 17 "slices" 18 "sort" 19 "strings" 20 "time" 21) 22 23// GoroutinesHandlerFunc returns a HandlerFunc that serves list of goroutine groups. 24func GoroutinesHandlerFunc(summaries map[trace.GoID]*trace.GoroutineSummary) http.HandlerFunc { 25 return func(w http.ResponseWriter, r *http.Request) { 26 // goroutineGroup describes a group of goroutines grouped by name. 27 type goroutineGroup struct { 28 Name string // Start function. 29 N int // Total number of goroutines in this group. 30 ExecTime time.Duration // Total execution time of all goroutines in this group. 31 } 32 // Accumulate groups by Name. 33 groupsByName := make(map[string]goroutineGroup) 34 for _, summary := range summaries { 35 group := groupsByName[summary.Name] 36 group.Name = summary.Name 37 group.N++ 38 group.ExecTime += summary.ExecTime 39 groupsByName[summary.Name] = group 40 } 41 var groups []goroutineGroup 42 for _, group := range groupsByName { 43 groups = append(groups, group) 44 } 45 slices.SortFunc(groups, func(a, b goroutineGroup) int { 46 return cmp.Compare(b.ExecTime, a.ExecTime) 47 }) 48 w.Header().Set("Content-Type", "text/html;charset=utf-8") 49 if err := templGoroutines.Execute(w, groups); err != nil { 50 log.Printf("failed to execute template: %v", err) 51 return 52 } 53 } 54} 55 56var templGoroutines = template.Must(template.New("").Parse(` 57<html> 58<style>` + traceviewer.CommonStyle + ` 59table { 60 border-collapse: collapse; 61} 62td, 63th { 64 border: 1px solid black; 65 padding-left: 8px; 66 padding-right: 8px; 67 padding-top: 4px; 68 padding-bottom: 4px; 69} 70</style> 71<body> 72<h1>Goroutines</h1> 73Below is a table of all goroutines in the trace grouped by start location and sorted by the total execution time of the group.<br> 74<br> 75Click a start location to view more details about that group.<br> 76<br> 77<table> 78 <tr> 79 <th>Start location</th> 80 <th>Count</th> 81 <th>Total execution time</th> 82 </tr> 83{{range $}} 84 <tr> 85 <td><code><a href="/goroutine?name={{.Name}}">{{or .Name "(Inactive, no stack trace sampled)"}}</a></code></td> 86 <td>{{.N}}</td> 87 <td>{{.ExecTime}}</td> 88 </tr> 89{{end}} 90</table> 91</body> 92</html> 93`)) 94 95// GoroutineHandler creates a handler that serves information about 96// goroutines in a particular group. 97func GoroutineHandler(summaries map[trace.GoID]*trace.GoroutineSummary) http.HandlerFunc { 98 return func(w http.ResponseWriter, r *http.Request) { 99 goroutineName := r.FormValue("name") 100 101 type goroutine struct { 102 *trace.GoroutineSummary 103 NonOverlappingStats map[string]time.Duration 104 HasRangeTime bool 105 } 106 107 // Collect all the goroutines in the group. 108 var ( 109 goroutines []goroutine 110 name string 111 totalExecTime, execTime time.Duration 112 maxTotalTime time.Duration 113 ) 114 validNonOverlappingStats := make(map[string]struct{}) 115 validRangeStats := make(map[string]struct{}) 116 for _, summary := range summaries { 117 totalExecTime += summary.ExecTime 118 119 if summary.Name != goroutineName { 120 continue 121 } 122 nonOverlappingStats := summary.NonOverlappingStats() 123 for name := range nonOverlappingStats { 124 validNonOverlappingStats[name] = struct{}{} 125 } 126 var totalRangeTime time.Duration 127 for name, dt := range summary.RangeTime { 128 validRangeStats[name] = struct{}{} 129 totalRangeTime += dt 130 } 131 goroutines = append(goroutines, goroutine{ 132 GoroutineSummary: summary, 133 NonOverlappingStats: nonOverlappingStats, 134 HasRangeTime: totalRangeTime != 0, 135 }) 136 name = summary.Name 137 execTime += summary.ExecTime 138 if maxTotalTime < summary.TotalTime { 139 maxTotalTime = summary.TotalTime 140 } 141 } 142 143 // Compute the percent of total execution time these goroutines represent. 144 execTimePercent := "" 145 if totalExecTime > 0 { 146 execTimePercent = fmt.Sprintf("%.2f%%", float64(execTime)/float64(totalExecTime)*100) 147 } 148 149 // Sort. 150 sortBy := r.FormValue("sortby") 151 if _, ok := validNonOverlappingStats[sortBy]; ok { 152 slices.SortFunc(goroutines, func(a, b goroutine) int { 153 return cmp.Compare(b.NonOverlappingStats[sortBy], a.NonOverlappingStats[sortBy]) 154 }) 155 } else { 156 // Sort by total time by default. 157 slices.SortFunc(goroutines, func(a, b goroutine) int { 158 return cmp.Compare(b.TotalTime, a.TotalTime) 159 }) 160 } 161 162 // Write down all the non-overlapping stats and sort them. 163 allNonOverlappingStats := make([]string, 0, len(validNonOverlappingStats)) 164 for name := range validNonOverlappingStats { 165 allNonOverlappingStats = append(allNonOverlappingStats, name) 166 } 167 slices.SortFunc(allNonOverlappingStats, func(a, b string) int { 168 if a == b { 169 return 0 170 } 171 if a == "Execution time" { 172 return -1 173 } 174 if b == "Execution time" { 175 return 1 176 } 177 return cmp.Compare(a, b) 178 }) 179 180 // Write down all the range stats and sort them. 181 allRangeStats := make([]string, 0, len(validRangeStats)) 182 for name := range validRangeStats { 183 allRangeStats = append(allRangeStats, name) 184 } 185 sort.Strings(allRangeStats) 186 187 err := templGoroutine.Execute(w, struct { 188 Name string 189 N int 190 ExecTimePercent string 191 MaxTotal time.Duration 192 Goroutines []goroutine 193 NonOverlappingStats []string 194 RangeStats []string 195 }{ 196 Name: name, 197 N: len(goroutines), 198 ExecTimePercent: execTimePercent, 199 MaxTotal: maxTotalTime, 200 Goroutines: goroutines, 201 NonOverlappingStats: allNonOverlappingStats, 202 RangeStats: allRangeStats, 203 }) 204 if err != nil { 205 http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) 206 return 207 } 208 } 209} 210 211func stat2Color(statName string) string { 212 color := "#636363" 213 if strings.HasPrefix(statName, "Block time") { 214 color = "#d01c8b" 215 } 216 switch statName { 217 case "Sched wait time": 218 color = "#2c7bb6" 219 case "Syscall execution time": 220 color = "#7b3294" 221 case "Execution time": 222 color = "#d7191c" 223 } 224 return color 225} 226 227var templGoroutine = template.Must(template.New("").Funcs(template.FuncMap{ 228 "percent": func(dividend, divisor time.Duration) template.HTML { 229 if divisor == 0 { 230 return "" 231 } 232 return template.HTML(fmt.Sprintf("(%.1f%%)", float64(dividend)/float64(divisor)*100)) 233 }, 234 "headerStyle": func(statName string) template.HTMLAttr { 235 return template.HTMLAttr(fmt.Sprintf("style=\"background-color: %s;\"", stat2Color(statName))) 236 }, 237 "barStyle": func(statName string, dividend, divisor time.Duration) template.HTMLAttr { 238 width := "0" 239 if divisor != 0 { 240 width = fmt.Sprintf("%.2f%%", float64(dividend)/float64(divisor)*100) 241 } 242 return template.HTMLAttr(fmt.Sprintf("style=\"width: %s; background-color: %s;\"", width, stat2Color(statName))) 243 }, 244}).Parse(` 245<!DOCTYPE html> 246<title>Goroutines: {{.Name}}</title> 247<style>` + traceviewer.CommonStyle + ` 248th { 249 background-color: #050505; 250 color: #fff; 251} 252th.link { 253 cursor: pointer; 254} 255table { 256 border-collapse: collapse; 257} 258td, 259th { 260 padding-left: 8px; 261 padding-right: 8px; 262 padding-top: 4px; 263 padding-bottom: 4px; 264} 265.details tr:hover { 266 background-color: #f2f2f2; 267} 268.details td { 269 text-align: right; 270 border: 1px solid black; 271} 272.details td.id { 273 text-align: left; 274} 275.stacked-bar-graph { 276 width: 300px; 277 height: 10px; 278 color: #414042; 279 white-space: nowrap; 280 font-size: 5px; 281} 282.stacked-bar-graph span { 283 display: inline-block; 284 width: 100%; 285 height: 100%; 286 box-sizing: border-box; 287 float: left; 288 padding: 0; 289} 290</style> 291 292<script> 293function reloadTable(key, value) { 294 let params = new URLSearchParams(window.location.search); 295 params.set(key, value); 296 window.location.search = params.toString(); 297} 298</script> 299 300<h1>Goroutines</h1> 301 302Table of contents 303<ul> 304 <li><a href="#summary">Summary</a></li> 305 <li><a href="#breakdown">Breakdown</a></li> 306 <li><a href="#ranges">Special ranges</a></li> 307</ul> 308 309<h3 id="summary">Summary</h3> 310 311<table class="summary"> 312 <tr> 313 <td>Goroutine start location:</td> 314 <td><code>{{.Name}}</code></td> 315 </tr> 316 <tr> 317 <td>Count:</td> 318 <td>{{.N}}</td> 319 </tr> 320 <tr> 321 <td>Execution Time:</td> 322 <td>{{.ExecTimePercent}} of total program execution time </td> 323 </tr> 324 <tr> 325 <td>Network wait profile:</td> 326 <td> <a href="/io?name={{.Name}}">graph</a> <a href="/io?name={{.Name}}&raw=1" download="io.profile">(download)</a></td> 327 </tr> 328 <tr> 329 <td>Sync block profile:</td> 330 <td> <a href="/block?name={{.Name}}">graph</a> <a href="/block?name={{.Name}}&raw=1" download="block.profile">(download)</a></td> 331 </tr> 332 <tr> 333 <td>Syscall profile:</td> 334 <td> <a href="/syscall?name={{.Name}}">graph</a> <a href="/syscall?name={{.Name}}&raw=1" download="syscall.profile">(download)</a></td> 335 </tr> 336 <tr> 337 <td>Scheduler wait profile:</td> 338 <td> <a href="/sched?name={{.Name}}">graph</a> <a href="/sched?name={{.Name}}&raw=1" download="sched.profile">(download)</a></td> 339 </tr> 340</table> 341 342<h3 id="breakdown">Breakdown</h3> 343 344The table below breaks down where each goroutine is spent its time during the 345traced period. 346All of the columns except total time are non-overlapping. 347<br> 348<br> 349 350<table class="details"> 351<tr> 352<th> Goroutine</th> 353<th class="link" onclick="reloadTable('sortby', 'Total time')"> Total</th> 354<th></th> 355{{range $.NonOverlappingStats}} 356<th class="link" onclick="reloadTable('sortby', '{{.}}')" {{headerStyle .}}> {{.}}</th> 357{{end}} 358</tr> 359{{range .Goroutines}} 360 <tr> 361 <td> <a href="/trace?goid={{.ID}}">{{.ID}}</a> </td> 362 <td> {{ .TotalTime.String }} </td> 363 <td> 364 <div class="stacked-bar-graph"> 365 {{$Goroutine := .}} 366 {{range $.NonOverlappingStats}} 367 {{$Time := index $Goroutine.NonOverlappingStats .}} 368 {{if $Time}} 369 <span {{barStyle . $Time $.MaxTotal}}> </span> 370 {{end}} 371 {{end}} 372 </div> 373 </td> 374 {{$Goroutine := .}} 375 {{range $.NonOverlappingStats}} 376 {{$Time := index $Goroutine.NonOverlappingStats .}} 377 <td> {{$Time.String}}</td> 378 {{end}} 379 </tr> 380{{end}} 381</table> 382 383<h3 id="ranges">Special ranges</h3> 384 385The table below describes how much of the traced period each goroutine spent in 386certain special time ranges. 387If a goroutine has spent no time in any special time ranges, it is excluded from 388the table. 389For example, how much time it spent helping the GC. Note that these times do 390overlap with the times from the first table. 391In general the goroutine may not be executing in these special time ranges. 392For example, it may have blocked while trying to help the GC. 393This must be taken into account when interpreting the data. 394<br> 395<br> 396 397<table class="details"> 398<tr> 399<th> Goroutine</th> 400<th> Total</th> 401{{range $.RangeStats}} 402<th {{headerStyle .}}> {{.}}</th> 403{{end}} 404</tr> 405{{range .Goroutines}} 406 {{if .HasRangeTime}} 407 <tr> 408 <td> <a href="/trace?goid={{.ID}}">{{.ID}}</a> </td> 409 <td> {{ .TotalTime.String }} </td> 410 {{$Goroutine := .}} 411 {{range $.RangeStats}} 412 {{$Time := index $Goroutine.RangeTime .}} 413 <td> {{$Time.String}}</td> 414 {{end}} 415 </tr> 416 {{end}} 417{{end}} 418</table> 419`)) 420