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