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

View as plain text