github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/trace/v2/regions.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 package trace 6 7 import ( 8 "cmp" 9 "fmt" 10 "html/template" 11 "net/http" 12 "net/url" 13 "slices" 14 "sort" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/go-asm/go/trace" 20 "github.com/go-asm/go/trace/traceviewer" 21 tracev2 "github.com/go-asm/go/trace/v2" 22 ) 23 24 // UserTasksHandlerFunc returns a HandlerFunc that reports all regions found in the trace. 25 func UserRegionsHandlerFunc(t *parsedTrace) http.HandlerFunc { 26 return func(w http.ResponseWriter, r *http.Request) { 27 // Summarize all the regions. 28 summary := make(map[regionFingerprint]regionStats) 29 for _, g := range t.summary.Goroutines { 30 for _, r := range g.Regions { 31 id := fingerprintRegion(r) 32 stats, ok := summary[id] 33 if !ok { 34 stats.regionFingerprint = id 35 } 36 stats.add(t, r) 37 summary[id] = stats 38 } 39 } 40 // Sort regions by PC and name. 41 userRegions := make([]regionStats, 0, len(summary)) 42 for _, stats := range summary { 43 userRegions = append(userRegions, stats) 44 } 45 slices.SortFunc(userRegions, func(a, b regionStats) int { 46 if c := cmp.Compare(a.Type, b.Type); c != 0 { 47 return c 48 } 49 return cmp.Compare(a.Frame.PC, b.Frame.PC) 50 }) 51 // Emit table. 52 err := templUserRegionTypes.Execute(w, userRegions) 53 if err != nil { 54 http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) 55 return 56 } 57 } 58 } 59 60 // regionFingerprint is a way to categorize regions that goes just one step beyond the region's Type 61 // by including the top stack frame. 62 type regionFingerprint struct { 63 Frame tracev2.StackFrame 64 Type string 65 } 66 67 func fingerprintRegion(r *trace.UserRegionSummary) regionFingerprint { 68 return regionFingerprint{ 69 Frame: regionTopStackFrame(r), 70 Type: r.Name, 71 } 72 } 73 74 func regionTopStackFrame(r *trace.UserRegionSummary) tracev2.StackFrame { 75 var frame tracev2.StackFrame 76 if r.Start != nil && r.Start.Stack() != tracev2.NoStack { 77 r.Start.Stack().Frames(func(f tracev2.StackFrame) bool { 78 frame = f 79 return false 80 }) 81 } 82 return frame 83 } 84 85 type regionStats struct { 86 regionFingerprint 87 Histogram traceviewer.TimeHistogram 88 } 89 90 func (s *regionStats) UserRegionURL() func(min, max time.Duration) string { 91 return func(min, max time.Duration) string { 92 return fmt.Sprintf("/userregion?type=%s&pc=%x&latmin=%v&latmax=%v", template.URLQueryEscaper(s.Type), s.Frame.PC, template.URLQueryEscaper(min), template.URLQueryEscaper(max)) 93 } 94 } 95 96 func (s *regionStats) add(t *parsedTrace, region *trace.UserRegionSummary) { 97 s.Histogram.Add(regionInterval(t, region).duration()) 98 } 99 100 var templUserRegionTypes = template.Must(template.New("").Parse(` 101 <!DOCTYPE html> 102 <title>Regions</title> 103 <style>` + traceviewer.CommonStyle + ` 104 .histoTime { 105 width: 20%; 106 white-space:nowrap; 107 } 108 th { 109 background-color: #050505; 110 color: #fff; 111 } 112 table { 113 border-collapse: collapse; 114 } 115 td, 116 th { 117 padding-left: 8px; 118 padding-right: 8px; 119 padding-top: 4px; 120 padding-bottom: 4px; 121 } 122 </style> 123 <body> 124 <h1>Regions</h1> 125 126 Below is a table containing a summary of all the user-defined regions in the trace. 127 Regions are grouped by the region type and the point at which the region started. 128 The rightmost column of the table contains a latency histogram for each region group. 129 Note that this histogram only counts regions that began and ended within the traced 130 period. 131 However, the "Count" column includes all regions, including those that only started 132 or ended during the traced period. 133 Regions that were active through the trace period were not recorded, and so are not 134 accounted for at all. 135 Click on the links to explore a breakdown of time spent for each region by goroutine 136 and user-defined task. 137 <br> 138 <br> 139 140 <table border="1" sortable="1"> 141 <tr> 142 <th>Region type</th> 143 <th>Count</th> 144 <th>Duration distribution (complete tasks)</th> 145 </tr> 146 {{range $}} 147 <tr> 148 <td><pre>{{printf "%q" .Type}}<br>{{.Frame.Func}} @ {{printf "0x%x" .Frame.PC}}<br>{{.Frame.File}}:{{.Frame.Line}}</pre></td> 149 <td><a href="/userregion?type={{.Type}}&pc={{.Frame.PC | printf "%x"}}">{{.Histogram.Count}}</a></td> 150 <td>{{.Histogram.ToHTML (.UserRegionURL)}}</td> 151 </tr> 152 {{end}} 153 </table> 154 </body> 155 </html> 156 `)) 157 158 // UserRegionHandlerFunc returns a HandlerFunc that presents the details of the selected regions. 159 func UserRegionHandlerFunc(t *parsedTrace) http.HandlerFunc { 160 return func(w http.ResponseWriter, r *http.Request) { 161 // Construct the filter from the request. 162 filter, err := newRegionFilter(r) 163 if err != nil { 164 http.Error(w, err.Error(), http.StatusBadRequest) 165 return 166 } 167 168 // Collect all the regions with their goroutines. 169 type region struct { 170 *trace.UserRegionSummary 171 Goroutine tracev2.GoID 172 NonOverlappingStats map[string]time.Duration 173 HasRangeTime bool 174 } 175 var regions []region 176 var maxTotal time.Duration 177 validNonOverlappingStats := make(map[string]struct{}) 178 validRangeStats := make(map[string]struct{}) 179 for _, g := range t.summary.Goroutines { 180 for _, r := range g.Regions { 181 if !filter.match(t, r) { 182 continue 183 } 184 nonOverlappingStats := r.NonOverlappingStats() 185 for name := range nonOverlappingStats { 186 validNonOverlappingStats[name] = struct{}{} 187 } 188 var totalRangeTime time.Duration 189 for name, dt := range r.RangeTime { 190 validRangeStats[name] = struct{}{} 191 totalRangeTime += dt 192 } 193 regions = append(regions, region{ 194 UserRegionSummary: r, 195 Goroutine: g.ID, 196 NonOverlappingStats: nonOverlappingStats, 197 HasRangeTime: totalRangeTime != 0, 198 }) 199 if maxTotal < r.TotalTime { 200 maxTotal = r.TotalTime 201 } 202 } 203 } 204 205 // Sort. 206 sortBy := r.FormValue("sortby") 207 if _, ok := validNonOverlappingStats[sortBy]; ok { 208 slices.SortFunc(regions, func(a, b region) int { 209 return cmp.Compare(b.NonOverlappingStats[sortBy], a.NonOverlappingStats[sortBy]) 210 }) 211 } else { 212 // Sort by total time by default. 213 slices.SortFunc(regions, func(a, b region) int { 214 return cmp.Compare(b.TotalTime, a.TotalTime) 215 }) 216 } 217 218 // Write down all the non-overlapping stats and sort them. 219 allNonOverlappingStats := make([]string, 0, len(validNonOverlappingStats)) 220 for name := range validNonOverlappingStats { 221 allNonOverlappingStats = append(allNonOverlappingStats, name) 222 } 223 slices.SortFunc(allNonOverlappingStats, func(a, b string) int { 224 if a == b { 225 return 0 226 } 227 if a == "Execution time" { 228 return -1 229 } 230 if b == "Execution time" { 231 return 1 232 } 233 return cmp.Compare(a, b) 234 }) 235 236 // Write down all the range stats and sort them. 237 allRangeStats := make([]string, 0, len(validRangeStats)) 238 for name := range validRangeStats { 239 allRangeStats = append(allRangeStats, name) 240 } 241 sort.Strings(allRangeStats) 242 243 err = templUserRegionType.Execute(w, struct { 244 MaxTotal time.Duration 245 Regions []region 246 Name string 247 Filter *regionFilter 248 NonOverlappingStats []string 249 RangeStats []string 250 }{ 251 MaxTotal: maxTotal, 252 Regions: regions, 253 Name: filter.name, 254 Filter: filter, 255 NonOverlappingStats: allNonOverlappingStats, 256 RangeStats: allRangeStats, 257 }) 258 if err != nil { 259 http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) 260 return 261 } 262 } 263 } 264 265 var templUserRegionType = template.Must(template.New("").Funcs(template.FuncMap{ 266 "headerStyle": func(statName string) template.HTMLAttr { 267 return template.HTMLAttr(fmt.Sprintf("style=\"background-color: %s;\"", stat2Color(statName))) 268 }, 269 "barStyle": func(statName string, dividend, divisor time.Duration) template.HTMLAttr { 270 width := "0" 271 if divisor != 0 { 272 width = fmt.Sprintf("%.2f%%", float64(dividend)/float64(divisor)*100) 273 } 274 return template.HTMLAttr(fmt.Sprintf("style=\"width: %s; background-color: %s;\"", width, stat2Color(statName))) 275 }, 276 "filterParams": func(f *regionFilter) template.URL { 277 return template.URL(f.params.Encode()) 278 }, 279 }).Parse(` 280 <!DOCTYPE html> 281 <title>Regions: {{.Name}}</title> 282 <style>` + traceviewer.CommonStyle + ` 283 th { 284 background-color: #050505; 285 color: #fff; 286 } 287 th.link { 288 cursor: pointer; 289 } 290 table { 291 border-collapse: collapse; 292 } 293 td, 294 th { 295 padding-left: 8px; 296 padding-right: 8px; 297 padding-top: 4px; 298 padding-bottom: 4px; 299 } 300 .details tr:hover { 301 background-color: #f2f2f2; 302 } 303 .details td { 304 text-align: right; 305 border: 1px solid #000; 306 } 307 .details td.id { 308 text-align: left; 309 } 310 .stacked-bar-graph { 311 width: 300px; 312 height: 10px; 313 color: #414042; 314 white-space: nowrap; 315 font-size: 5px; 316 } 317 .stacked-bar-graph span { 318 display: inline-block; 319 width: 100%; 320 height: 100%; 321 box-sizing: border-box; 322 float: left; 323 padding: 0; 324 } 325 </style> 326 327 <script> 328 function reloadTable(key, value) { 329 let params = new URLSearchParams(window.location.search); 330 params.set(key, value); 331 window.location.search = params.toString(); 332 } 333 </script> 334 335 <h1>Regions: {{.Name}}</h1> 336 337 Table of contents 338 <ul> 339 <li><a href="#summary">Summary</a></li> 340 <li><a href="#breakdown">Breakdown</a></li> 341 <li><a href="#ranges">Special ranges</a></li> 342 </ul> 343 344 <h3 id="summary">Summary</h3> 345 346 {{ with $p := filterParams .Filter}} 347 <table class="summary"> 348 <tr> 349 <td>Network wait profile:</td> 350 <td> <a href="/regionio?{{$p}}">graph</a> <a href="/regionio?{{$p}}&raw=1" download="io.profile">(download)</a></td> 351 </tr> 352 <tr> 353 <td>Sync block profile:</td> 354 <td> <a href="/regionblock?{{$p}}">graph</a> <a href="/regionblock?{{$p}}&raw=1" download="block.profile">(download)</a></td> 355 </tr> 356 <tr> 357 <td>Syscall profile:</td> 358 <td> <a href="/regionsyscall?{{$p}}">graph</a> <a href="/regionsyscall?{{$p}}&raw=1" download="syscall.profile">(download)</a></td> 359 </tr> 360 <tr> 361 <td>Scheduler wait profile:</td> 362 <td> <a href="/regionsched?{{$p}}">graph</a> <a href="/regionsched?{{$p}}&raw=1" download="sched.profile">(download)</a></td> 363 </tr> 364 </table> 365 {{ end }} 366 367 <h3 id="breakdown">Breakdown</h3> 368 369 The table below breaks down where each goroutine is spent its time during the 370 traced period. 371 All of the columns except total time are non-overlapping. 372 <br> 373 <br> 374 375 <table class="details"> 376 <tr> 377 <th> Goroutine </th> 378 <th> Task </th> 379 <th class="link" onclick="reloadTable('sortby', 'Total time')"> Total</th> 380 <th></th> 381 {{range $.NonOverlappingStats}} 382 <th class="link" onclick="reloadTable('sortby', '{{.}}')" {{headerStyle .}}> {{.}}</th> 383 {{end}} 384 </tr> 385 {{range .Regions}} 386 <tr> 387 <td> <a href="/trace?goid={{.Goroutine}}">{{.Goroutine}}</a> </td> 388 <td> {{if .TaskID}}<a href="/trace?focustask={{.TaskID}}">{{.TaskID}}</a>{{end}} </td> 389 <td> {{ .TotalTime.String }} </td> 390 <td> 391 <div class="stacked-bar-graph"> 392 {{$Region := .}} 393 {{range $.NonOverlappingStats}} 394 {{$Time := index $Region.NonOverlappingStats .}} 395 {{if $Time}} 396 <span {{barStyle . $Time $.MaxTotal}}> </span> 397 {{end}} 398 {{end}} 399 </div> 400 </td> 401 {{$Region := .}} 402 {{range $.NonOverlappingStats}} 403 {{$Time := index $Region.NonOverlappingStats .}} 404 <td> {{$Time.String}}</td> 405 {{end}} 406 </tr> 407 {{end}} 408 </table> 409 410 <h3 id="ranges">Special ranges</h3> 411 412 The table below describes how much of the traced period each goroutine spent in 413 certain special time ranges. 414 If a goroutine has spent no time in any special time ranges, it is excluded from 415 the table. 416 For example, how much time it spent helping the GC. Note that these times do 417 overlap with the times from the first table. 418 In general the goroutine may not be executing in these special time ranges. 419 For example, it may have blocked while trying to help the GC. 420 This must be taken into account when interpreting the data. 421 <br> 422 <br> 423 424 <table class="details"> 425 <tr> 426 <th> Goroutine</th> 427 <th> Task </th> 428 <th> Total</th> 429 {{range $.RangeStats}} 430 <th {{headerStyle .}}> {{.}}</th> 431 {{end}} 432 </tr> 433 {{range .Regions}} 434 {{if .HasRangeTime}} 435 <tr> 436 <td> <a href="/trace?goid={{.Goroutine}}">{{.Goroutine}}</a> </td> 437 <td> {{if .TaskID}}<a href="/trace?focustask={{.TaskID}}">{{.TaskID}}</a>{{end}} </td> 438 <td> {{ .TotalTime.String }} </td> 439 {{$Region := .}} 440 {{range $.RangeStats}} 441 {{$Time := index $Region.RangeTime .}} 442 <td> {{$Time.String}}</td> 443 {{end}} 444 </tr> 445 {{end}} 446 {{end}} 447 </table> 448 `)) 449 450 // regionFilter represents a region filter specified by a user of cmd/trace. 451 type regionFilter struct { 452 name string 453 params url.Values 454 cond []func(*parsedTrace, *trace.UserRegionSummary) bool 455 } 456 457 // match returns true if a region, described by its ID and summary, matches 458 // the filter. 459 func (f *regionFilter) match(t *parsedTrace, s *trace.UserRegionSummary) bool { 460 for _, c := range f.cond { 461 if !c(t, s) { 462 return false 463 } 464 } 465 return true 466 } 467 468 // newRegionFilter creates a new region filter from URL query variables. 469 func newRegionFilter(r *http.Request) (*regionFilter, error) { 470 if err := r.ParseForm(); err != nil { 471 return nil, err 472 } 473 474 var name []string 475 var conditions []func(*parsedTrace, *trace.UserRegionSummary) bool 476 filterParams := make(url.Values) 477 478 param := r.Form 479 if typ, ok := param["type"]; ok && len(typ) > 0 { 480 name = append(name, fmt.Sprintf("%q", typ[0])) 481 conditions = append(conditions, func(_ *parsedTrace, r *trace.UserRegionSummary) bool { 482 return r.Name == typ[0] 483 }) 484 filterParams.Add("type", typ[0]) 485 } 486 if pc, err := strconv.ParseUint(r.FormValue("pc"), 16, 64); err == nil { 487 encPC := fmt.Sprintf("0x%x", pc) 488 name = append(name, "@ "+encPC) 489 conditions = append(conditions, func(_ *parsedTrace, r *trace.UserRegionSummary) bool { 490 return regionTopStackFrame(r).PC == pc 491 }) 492 filterParams.Add("pc", encPC) 493 } 494 495 if lat, err := time.ParseDuration(r.FormValue("latmin")); err == nil { 496 name = append(name, fmt.Sprintf("(latency >= %s)", lat)) 497 conditions = append(conditions, func(t *parsedTrace, r *trace.UserRegionSummary) bool { 498 return regionInterval(t, r).duration() >= lat 499 }) 500 filterParams.Add("latmin", lat.String()) 501 } 502 if lat, err := time.ParseDuration(r.FormValue("latmax")); err == nil { 503 name = append(name, fmt.Sprintf("(latency <= %s)", lat)) 504 conditions = append(conditions, func(t *parsedTrace, r *trace.UserRegionSummary) bool { 505 return regionInterval(t, r).duration() <= lat 506 }) 507 filterParams.Add("latmax", lat.String()) 508 } 509 510 return ®ionFilter{ 511 name: strings.Join(name, " "), 512 cond: conditions, 513 params: filterParams, 514 }, nil 515 } 516 517 func regionInterval(t *parsedTrace, s *trace.UserRegionSummary) interval { 518 var i interval 519 if s.Start != nil { 520 i.start = s.Start.Time() 521 } else { 522 i.start = t.startTime() 523 } 524 if s.End != nil { 525 i.end = s.End.Time() 526 } else { 527 i.end = t.endTime() 528 } 529 return i 530 }