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