github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/trace/v2/regions.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  package trace
     6  
     7  import (
     8  	"cmp"
     9  	"fmt"
    10  	"html/template"
    11  	"net/http"
    12  	"net/url"
    13  	"slices"
    14  	"sort"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/go-asm/go/trace"
    20  	"github.com/go-asm/go/trace/traceviewer"
    21  	tracev2 "github.com/go-asm/go/trace/v2"
    22  )
    23  
    24  // UserTasksHandlerFunc returns a HandlerFunc that reports all regions found in the trace.
    25  func UserRegionsHandlerFunc(t *parsedTrace) http.HandlerFunc {
    26  	return func(w http.ResponseWriter, r *http.Request) {
    27  		// Summarize all the regions.
    28  		summary := make(map[regionFingerprint]regionStats)
    29  		for _, g := range t.summary.Goroutines {
    30  			for _, r := range g.Regions {
    31  				id := fingerprintRegion(r)
    32  				stats, ok := summary[id]
    33  				if !ok {
    34  					stats.regionFingerprint = id
    35  				}
    36  				stats.add(t, r)
    37  				summary[id] = stats
    38  			}
    39  		}
    40  		// Sort regions by PC and name.
    41  		userRegions := make([]regionStats, 0, len(summary))
    42  		for _, stats := range summary {
    43  			userRegions = append(userRegions, stats)
    44  		}
    45  		slices.SortFunc(userRegions, func(a, b regionStats) int {
    46  			if c := cmp.Compare(a.Type, b.Type); c != 0 {
    47  				return c
    48  			}
    49  			return cmp.Compare(a.Frame.PC, b.Frame.PC)
    50  		})
    51  		// Emit table.
    52  		err := templUserRegionTypes.Execute(w, userRegions)
    53  		if err != nil {
    54  			http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)
    55  			return
    56  		}
    57  	}
    58  }
    59  
    60  // regionFingerprint is a way to categorize regions that goes just one step beyond the region's Type
    61  // by including the top stack frame.
    62  type regionFingerprint struct {
    63  	Frame tracev2.StackFrame
    64  	Type  string
    65  }
    66  
    67  func fingerprintRegion(r *trace.UserRegionSummary) regionFingerprint {
    68  	return regionFingerprint{
    69  		Frame: regionTopStackFrame(r),
    70  		Type:  r.Name,
    71  	}
    72  }
    73  
    74  func regionTopStackFrame(r *trace.UserRegionSummary) tracev2.StackFrame {
    75  	var frame tracev2.StackFrame
    76  	if r.Start != nil && r.Start.Stack() != tracev2.NoStack {
    77  		r.Start.Stack().Frames(func(f tracev2.StackFrame) bool {
    78  			frame = f
    79  			return false
    80  		})
    81  	}
    82  	return frame
    83  }
    84  
    85  type regionStats struct {
    86  	regionFingerprint
    87  	Histogram traceviewer.TimeHistogram
    88  }
    89  
    90  func (s *regionStats) UserRegionURL() func(min, max time.Duration) string {
    91  	return func(min, max time.Duration) string {
    92  		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))
    93  	}
    94  }
    95  
    96  func (s *regionStats) add(t *parsedTrace, region *trace.UserRegionSummary) {
    97  	s.Histogram.Add(regionInterval(t, region).duration())
    98  }
    99  
   100  var templUserRegionTypes = template.Must(template.New("").Parse(`
   101  <!DOCTYPE html>
   102  <title>Regions</title>
   103  <style>` + traceviewer.CommonStyle + `
   104  .histoTime {
   105    width: 20%;
   106    white-space:nowrap;
   107  }
   108  th {
   109    background-color: #050505;
   110    color: #fff;
   111  }
   112  table {
   113    border-collapse: collapse;
   114  }
   115  td,
   116  th {
   117    padding-left: 8px;
   118    padding-right: 8px;
   119    padding-top: 4px;
   120    padding-bottom: 4px;
   121  }
   122  </style>
   123  <body>
   124  <h1>Regions</h1>
   125  
   126  Below is a table containing a summary of all the user-defined regions in the trace.
   127  Regions are grouped by the region type and the point at which the region started.
   128  The rightmost column of the table contains a latency histogram for each region group.
   129  Note that this histogram only counts regions that began and ended within the traced
   130  period.
   131  However, the "Count" column includes all regions, including those that only started
   132  or ended during the traced period.
   133  Regions that were active through the trace period were not recorded, and so are not
   134  accounted for at all.
   135  Click on the links to explore a breakdown of time spent for each region by goroutine
   136  and user-defined task.
   137  <br>
   138  <br>
   139  
   140  <table border="1" sortable="1">
   141  <tr>
   142  <th>Region type</th>
   143  <th>Count</th>
   144  <th>Duration distribution (complete tasks)</th>
   145  </tr>
   146  {{range $}}
   147    <tr>
   148      <td><pre>{{printf "%q" .Type}}<br>{{.Frame.Func}} @ {{printf "0x%x" .Frame.PC}}<br>{{.Frame.File}}:{{.Frame.Line}}</pre></td>
   149      <td><a href="/userregion?type={{.Type}}&pc={{.Frame.PC | printf "%x"}}">{{.Histogram.Count}}</a></td>
   150      <td>{{.Histogram.ToHTML (.UserRegionURL)}}</td>
   151    </tr>
   152  {{end}}
   153  </table>
   154  </body>
   155  </html>
   156  `))
   157  
   158  // UserRegionHandlerFunc returns a HandlerFunc that presents the details of the selected regions.
   159  func UserRegionHandlerFunc(t *parsedTrace) http.HandlerFunc {
   160  	return func(w http.ResponseWriter, r *http.Request) {
   161  		// Construct the filter from the request.
   162  		filter, err := newRegionFilter(r)
   163  		if err != nil {
   164  			http.Error(w, err.Error(), http.StatusBadRequest)
   165  			return
   166  		}
   167  
   168  		// Collect all the regions with their goroutines.
   169  		type region struct {
   170  			*trace.UserRegionSummary
   171  			Goroutine           tracev2.GoID
   172  			NonOverlappingStats map[string]time.Duration
   173  			HasRangeTime        bool
   174  		}
   175  		var regions []region
   176  		var maxTotal time.Duration
   177  		validNonOverlappingStats := make(map[string]struct{})
   178  		validRangeStats := make(map[string]struct{})
   179  		for _, g := range t.summary.Goroutines {
   180  			for _, r := range g.Regions {
   181  				if !filter.match(t, r) {
   182  					continue
   183  				}
   184  				nonOverlappingStats := r.NonOverlappingStats()
   185  				for name := range nonOverlappingStats {
   186  					validNonOverlappingStats[name] = struct{}{}
   187  				}
   188  				var totalRangeTime time.Duration
   189  				for name, dt := range r.RangeTime {
   190  					validRangeStats[name] = struct{}{}
   191  					totalRangeTime += dt
   192  				}
   193  				regions = append(regions, region{
   194  					UserRegionSummary:   r,
   195  					Goroutine:           g.ID,
   196  					NonOverlappingStats: nonOverlappingStats,
   197  					HasRangeTime:        totalRangeTime != 0,
   198  				})
   199  				if maxTotal < r.TotalTime {
   200  					maxTotal = r.TotalTime
   201  				}
   202  			}
   203  		}
   204  
   205  		// Sort.
   206  		sortBy := r.FormValue("sortby")
   207  		if _, ok := validNonOverlappingStats[sortBy]; ok {
   208  			slices.SortFunc(regions, func(a, b region) int {
   209  				return cmp.Compare(b.NonOverlappingStats[sortBy], a.NonOverlappingStats[sortBy])
   210  			})
   211  		} else {
   212  			// Sort by total time by default.
   213  			slices.SortFunc(regions, func(a, b region) int {
   214  				return cmp.Compare(b.TotalTime, a.TotalTime)
   215  			})
   216  		}
   217  
   218  		// Write down all the non-overlapping stats and sort them.
   219  		allNonOverlappingStats := make([]string, 0, len(validNonOverlappingStats))
   220  		for name := range validNonOverlappingStats {
   221  			allNonOverlappingStats = append(allNonOverlappingStats, name)
   222  		}
   223  		slices.SortFunc(allNonOverlappingStats, func(a, b string) int {
   224  			if a == b {
   225  				return 0
   226  			}
   227  			if a == "Execution time" {
   228  				return -1
   229  			}
   230  			if b == "Execution time" {
   231  				return 1
   232  			}
   233  			return cmp.Compare(a, b)
   234  		})
   235  
   236  		// Write down all the range stats and sort them.
   237  		allRangeStats := make([]string, 0, len(validRangeStats))
   238  		for name := range validRangeStats {
   239  			allRangeStats = append(allRangeStats, name)
   240  		}
   241  		sort.Strings(allRangeStats)
   242  
   243  		err = templUserRegionType.Execute(w, struct {
   244  			MaxTotal            time.Duration
   245  			Regions             []region
   246  			Name                string
   247  			Filter              *regionFilter
   248  			NonOverlappingStats []string
   249  			RangeStats          []string
   250  		}{
   251  			MaxTotal:            maxTotal,
   252  			Regions:             regions,
   253  			Name:                filter.name,
   254  			Filter:              filter,
   255  			NonOverlappingStats: allNonOverlappingStats,
   256  			RangeStats:          allRangeStats,
   257  		})
   258  		if err != nil {
   259  			http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)
   260  			return
   261  		}
   262  	}
   263  }
   264  
   265  var templUserRegionType = template.Must(template.New("").Funcs(template.FuncMap{
   266  	"headerStyle": func(statName string) template.HTMLAttr {
   267  		return template.HTMLAttr(fmt.Sprintf("style=\"background-color: %s;\"", stat2Color(statName)))
   268  	},
   269  	"barStyle": func(statName string, dividend, divisor time.Duration) template.HTMLAttr {
   270  		width := "0"
   271  		if divisor != 0 {
   272  			width = fmt.Sprintf("%.2f%%", float64(dividend)/float64(divisor)*100)
   273  		}
   274  		return template.HTMLAttr(fmt.Sprintf("style=\"width: %s; background-color: %s;\"", width, stat2Color(statName)))
   275  	},
   276  	"filterParams": func(f *regionFilter) template.URL {
   277  		return template.URL(f.params.Encode())
   278  	},
   279  }).Parse(`
   280  <!DOCTYPE html>
   281  <title>Regions: {{.Name}}</title>
   282  <style>` + traceviewer.CommonStyle + `
   283  th {
   284    background-color: #050505;
   285    color: #fff;
   286  }
   287  th.link {
   288    cursor: pointer;
   289  }
   290  table {
   291    border-collapse: collapse;
   292  }
   293  td,
   294  th {
   295    padding-left: 8px;
   296    padding-right: 8px;
   297    padding-top: 4px;
   298    padding-bottom: 4px;
   299  }
   300  .details tr:hover {
   301    background-color: #f2f2f2;
   302  }
   303  .details td {
   304    text-align: right;
   305    border: 1px solid #000;
   306  }
   307  .details td.id {
   308    text-align: left;
   309  }
   310  .stacked-bar-graph {
   311    width: 300px;
   312    height: 10px;
   313    color: #414042;
   314    white-space: nowrap;
   315    font-size: 5px;
   316  }
   317  .stacked-bar-graph span {
   318    display: inline-block;
   319    width: 100%;
   320    height: 100%;
   321    box-sizing: border-box;
   322    float: left;
   323    padding: 0;
   324  }
   325  </style>
   326  
   327  <script>
   328  function reloadTable(key, value) {
   329    let params = new URLSearchParams(window.location.search);
   330    params.set(key, value);
   331    window.location.search = params.toString();
   332  }
   333  </script>
   334  
   335  <h1>Regions: {{.Name}}</h1>
   336  
   337  Table of contents
   338  <ul>
   339  	<li><a href="#summary">Summary</a></li>
   340  	<li><a href="#breakdown">Breakdown</a></li>
   341  	<li><a href="#ranges">Special ranges</a></li>
   342  </ul>
   343  
   344  <h3 id="summary">Summary</h3>
   345  
   346  {{ with $p := filterParams .Filter}}
   347  <table class="summary">
   348  	<tr>
   349  		<td>Network wait profile:</td>
   350  		<td> <a href="/regionio?{{$p}}">graph</a> <a href="/regionio?{{$p}}&raw=1" download="io.profile">(download)</a></td>
   351  	</tr>
   352  	<tr>
   353  		<td>Sync block profile:</td>
   354  		<td> <a href="/regionblock?{{$p}}">graph</a> <a href="/regionblock?{{$p}}&raw=1" download="block.profile">(download)</a></td>
   355  	</tr>
   356  	<tr>
   357  		<td>Syscall profile:</td>
   358  		<td> <a href="/regionsyscall?{{$p}}">graph</a> <a href="/regionsyscall?{{$p}}&raw=1" download="syscall.profile">(download)</a></td>
   359  	</tr>
   360  	<tr>
   361  		<td>Scheduler wait profile:</td>
   362  		<td> <a href="/regionsched?{{$p}}">graph</a> <a href="/regionsched?{{$p}}&raw=1" download="sched.profile">(download)</a></td>
   363  	</tr>
   364  </table>
   365  {{ end }}
   366  
   367  <h3 id="breakdown">Breakdown</h3>
   368  
   369  The table below breaks down where each goroutine is spent its time during the
   370  traced period.
   371  All of the columns except total time are non-overlapping.
   372  <br>
   373  <br>
   374  
   375  <table class="details">
   376  <tr>
   377  <th> Goroutine </th>
   378  <th> Task </th>
   379  <th class="link" onclick="reloadTable('sortby', 'Total time')"> Total</th>
   380  <th></th>
   381  {{range $.NonOverlappingStats}}
   382  <th class="link" onclick="reloadTable('sortby', '{{.}}')" {{headerStyle .}}> {{.}}</th>
   383  {{end}}
   384  </tr>
   385  {{range .Regions}}
   386  	<tr>
   387  		<td> <a href="/trace?goid={{.Goroutine}}">{{.Goroutine}}</a> </td>
   388  		<td> {{if .TaskID}}<a href="/trace?focustask={{.TaskID}}">{{.TaskID}}</a>{{end}} </td>
   389  		<td> {{ .TotalTime.String }} </td>
   390  		<td>
   391  			<div class="stacked-bar-graph">
   392  			{{$Region := .}}
   393  			{{range $.NonOverlappingStats}}
   394  				{{$Time := index $Region.NonOverlappingStats .}}
   395  				{{if $Time}}
   396  					<span {{barStyle . $Time $.MaxTotal}}>&nbsp;</span>
   397  				{{end}}
   398  			{{end}}
   399  			</div>
   400  		</td>
   401  		{{$Region := .}}
   402  		{{range $.NonOverlappingStats}}
   403  			{{$Time := index $Region.NonOverlappingStats .}}
   404  			<td> {{$Time.String}}</td>
   405  		{{end}}
   406  	</tr>
   407  {{end}}
   408  </table>
   409  
   410  <h3 id="ranges">Special ranges</h3>
   411  
   412  The table below describes how much of the traced period each goroutine spent in
   413  certain special time ranges.
   414  If a goroutine has spent no time in any special time ranges, it is excluded from
   415  the table.
   416  For example, how much time it spent helping the GC. Note that these times do
   417  overlap with the times from the first table.
   418  In general the goroutine may not be executing in these special time ranges.
   419  For example, it may have blocked while trying to help the GC.
   420  This must be taken into account when interpreting the data.
   421  <br>
   422  <br>
   423  
   424  <table class="details">
   425  <tr>
   426  <th> Goroutine</th>
   427  <th> Task </th>
   428  <th> Total</th>
   429  {{range $.RangeStats}}
   430  <th {{headerStyle .}}> {{.}}</th>
   431  {{end}}
   432  </tr>
   433  {{range .Regions}}
   434  	{{if .HasRangeTime}}
   435  		<tr>
   436  			<td> <a href="/trace?goid={{.Goroutine}}">{{.Goroutine}}</a> </td>
   437  			<td> {{if .TaskID}}<a href="/trace?focustask={{.TaskID}}">{{.TaskID}}</a>{{end}} </td>
   438  			<td> {{ .TotalTime.String }} </td>
   439  			{{$Region := .}}
   440  			{{range $.RangeStats}}
   441  				{{$Time := index $Region.RangeTime .}}
   442  				<td> {{$Time.String}}</td>
   443  			{{end}}
   444  		</tr>
   445  	{{end}}
   446  {{end}}
   447  </table>
   448  `))
   449  
   450  // regionFilter represents a region filter specified by a user of cmd/trace.
   451  type regionFilter struct {
   452  	name   string
   453  	params url.Values
   454  	cond   []func(*parsedTrace, *trace.UserRegionSummary) bool
   455  }
   456  
   457  // match returns true if a region, described by its ID and summary, matches
   458  // the filter.
   459  func (f *regionFilter) match(t *parsedTrace, s *trace.UserRegionSummary) bool {
   460  	for _, c := range f.cond {
   461  		if !c(t, s) {
   462  			return false
   463  		}
   464  	}
   465  	return true
   466  }
   467  
   468  // newRegionFilter creates a new region filter from URL query variables.
   469  func newRegionFilter(r *http.Request) (*regionFilter, error) {
   470  	if err := r.ParseForm(); err != nil {
   471  		return nil, err
   472  	}
   473  
   474  	var name []string
   475  	var conditions []func(*parsedTrace, *trace.UserRegionSummary) bool
   476  	filterParams := make(url.Values)
   477  
   478  	param := r.Form
   479  	if typ, ok := param["type"]; ok && len(typ) > 0 {
   480  		name = append(name, fmt.Sprintf("%q", typ[0]))
   481  		conditions = append(conditions, func(_ *parsedTrace, r *trace.UserRegionSummary) bool {
   482  			return r.Name == typ[0]
   483  		})
   484  		filterParams.Add("type", typ[0])
   485  	}
   486  	if pc, err := strconv.ParseUint(r.FormValue("pc"), 16, 64); err == nil {
   487  		encPC := fmt.Sprintf("0x%x", pc)
   488  		name = append(name, "@ "+encPC)
   489  		conditions = append(conditions, func(_ *parsedTrace, r *trace.UserRegionSummary) bool {
   490  			return regionTopStackFrame(r).PC == pc
   491  		})
   492  		filterParams.Add("pc", encPC)
   493  	}
   494  
   495  	if lat, err := time.ParseDuration(r.FormValue("latmin")); err == nil {
   496  		name = append(name, fmt.Sprintf("(latency >= %s)", lat))
   497  		conditions = append(conditions, func(t *parsedTrace, r *trace.UserRegionSummary) bool {
   498  			return regionInterval(t, r).duration() >= lat
   499  		})
   500  		filterParams.Add("latmin", lat.String())
   501  	}
   502  	if lat, err := time.ParseDuration(r.FormValue("latmax")); err == nil {
   503  		name = append(name, fmt.Sprintf("(latency <= %s)", lat))
   504  		conditions = append(conditions, func(t *parsedTrace, r *trace.UserRegionSummary) bool {
   505  			return regionInterval(t, r).duration() <= lat
   506  		})
   507  		filterParams.Add("latmax", lat.String())
   508  	}
   509  
   510  	return &regionFilter{
   511  		name:   strings.Join(name, " "),
   512  		cond:   conditions,
   513  		params: filterParams,
   514  	}, nil
   515  }
   516  
   517  func regionInterval(t *parsedTrace, s *trace.UserRegionSummary) interval {
   518  	var i interval
   519  	if s.Start != nil {
   520  		i.start = s.Start.Time()
   521  	} else {
   522  		i.start = t.startTime()
   523  	}
   524  	if s.End != nil {
   525  		i.end = s.End.Time()
   526  	} else {
   527  		i.end = t.endTime()
   528  	}
   529  	return i
   530  }