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}}>&nbsp;</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 &regionFilter{
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