Source file
src/cmd/trace/goroutines.go
1
2
3
4
5
6
7 package main
8
9 import (
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
24 func GoroutinesHandlerFunc(summaries map[trace.GoID]*trace.GoroutineSummary) http.HandlerFunc {
25 return func(w http.ResponseWriter, r *http.Request) {
26
27 type goroutineGroup struct {
28 Name string
29 N int
30 ExecTime time.Duration
31 }
32
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
56 var templGoroutines = template.Must(template.New("").Parse(`
57 <html>
58 <style>` + traceviewer.CommonStyle + `
59 table {
60 border-collapse: collapse;
61 }
62 td,
63 th {
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>
73 Below 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>
75 Click 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
96
97 func 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
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
144 execTimePercent := ""
145 if totalExecTime > 0 {
146 execTimePercent = fmt.Sprintf("%.2f%%", float64(execTime)/float64(totalExecTime)*100)
147 }
148
149
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
157 slices.SortFunc(goroutines, func(a, b goroutine) int {
158 return cmp.Compare(b.TotalTime, a.TotalTime)
159 })
160 }
161
162
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
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
211 func 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
227 var 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 + `
248 th {
249 background-color: #050505;
250 color: #fff;
251 }
252 th.link {
253 cursor: pointer;
254 }
255 table {
256 border-collapse: collapse;
257 }
258 td,
259 th {
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>
293 function 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
302 Table 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
344 The table below breaks down where each goroutine is spent its time during the
345 traced period.
346 All 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}}> </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
385 The table below describes how much of the traced period each goroutine spent in
386 certain special time ranges.
387 If a goroutine has spent no time in any special time ranges, it is excluded from
388 the table.
389 For example, how much time it spent helping the GC. Note that these times do
390 overlap with the times from the first table.
391 In general the goroutine may not be executing in these special time ranges.
392 For example, it may have blocked while trying to help the GC.
393 This 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
View as plain text