github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/trace/v2/goroutines.go (about)

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