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

View as plain text