Source file src/cmd/trace/regions.go

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

View as plain text