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