Source file src/cmd/trace/goroutines.go

     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  
     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  // GoroutinesHandlerFunc returns a HandlerFunc that serves list of goroutine groups.
    24  func 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  
    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  // GoroutineHandler creates a handler that serves information about
    96  // goroutines in a particular group.
    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  		// 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  
   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}}>&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  
   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