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