Source file src/internal/trace/traceviewer/mmu.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  // Minimum mutator utilization (MMU) graphing.
     6  
     7  // TODO:
     8  //
     9  // In worst window list, show break-down of GC utilization sources
    10  // (STW, assist, etc). Probably requires a different MutatorUtil
    11  // representation.
    12  //
    13  // When a window size is selected, show a second plot of the mutator
    14  // utilization distribution for that window size.
    15  //
    16  // Render plot progressively so rough outline is visible quickly even
    17  // for very complex MUTs. Start by computing just a few window sizes
    18  // and then add more window sizes.
    19  //
    20  // Consider using sampling to compute an approximate MUT. This would
    21  // work by sampling the mutator utilization at randomly selected
    22  // points in time in the trace to build an empirical distribution. We
    23  // could potentially put confidence intervals on these estimates and
    24  // render this progressively as we refine the distributions.
    25  
    26  package traceviewer
    27  
    28  import (
    29  	"encoding/json"
    30  	"fmt"
    31  	"internal/trace"
    32  	"log"
    33  	"math"
    34  	"net/http"
    35  	"strconv"
    36  	"strings"
    37  	"sync"
    38  	"time"
    39  )
    40  
    41  type MutatorUtilFunc func(trace.UtilFlags) ([][]trace.MutatorUtil, error)
    42  
    43  func MMUHandlerFunc(ranges []Range, f MutatorUtilFunc) http.HandlerFunc {
    44  	mmu := &mmu{
    45  		cache:  make(map[trace.UtilFlags]*mmuCacheEntry),
    46  		f:      f,
    47  		ranges: ranges,
    48  	}
    49  	return func(w http.ResponseWriter, r *http.Request) {
    50  		switch r.FormValue("mode") {
    51  		case "plot":
    52  			mmu.HandlePlot(w, r)
    53  			return
    54  		case "details":
    55  			mmu.HandleDetails(w, r)
    56  			return
    57  		}
    58  		http.ServeContent(w, r, "", time.Time{}, strings.NewReader(templMMU))
    59  	}
    60  }
    61  
    62  var utilFlagNames = map[string]trace.UtilFlags{
    63  	"perProc":    trace.UtilPerProc,
    64  	"stw":        trace.UtilSTW,
    65  	"background": trace.UtilBackground,
    66  	"assist":     trace.UtilAssist,
    67  	"sweep":      trace.UtilSweep,
    68  }
    69  
    70  func requestUtilFlags(r *http.Request) trace.UtilFlags {
    71  	var flags trace.UtilFlags
    72  	for _, flagStr := range strings.Split(r.FormValue("flags"), "|") {
    73  		flags |= utilFlagNames[flagStr]
    74  	}
    75  	return flags
    76  }
    77  
    78  type mmuCacheEntry struct {
    79  	init     sync.Once
    80  	util     [][]trace.MutatorUtil
    81  	mmuCurve *trace.MMUCurve
    82  	err      error
    83  }
    84  
    85  type mmu struct {
    86  	mu     sync.Mutex
    87  	cache  map[trace.UtilFlags]*mmuCacheEntry
    88  	f      MutatorUtilFunc
    89  	ranges []Range
    90  }
    91  
    92  func (m *mmu) get(flags trace.UtilFlags) ([][]trace.MutatorUtil, *trace.MMUCurve, error) {
    93  	m.mu.Lock()
    94  	entry := m.cache[flags]
    95  	if entry == nil {
    96  		entry = new(mmuCacheEntry)
    97  		m.cache[flags] = entry
    98  	}
    99  	m.mu.Unlock()
   100  
   101  	entry.init.Do(func() {
   102  		util, err := m.f(flags)
   103  		if err != nil {
   104  			entry.err = err
   105  		} else {
   106  			entry.util = util
   107  			entry.mmuCurve = trace.NewMMUCurve(util)
   108  		}
   109  	})
   110  	return entry.util, entry.mmuCurve, entry.err
   111  }
   112  
   113  // HandlePlot serves the JSON data for the MMU plot.
   114  func (m *mmu) HandlePlot(w http.ResponseWriter, r *http.Request) {
   115  	mu, mmuCurve, err := m.get(requestUtilFlags(r))
   116  	if err != nil {
   117  		http.Error(w, fmt.Sprintf("failed to produce MMU data: %v", err), http.StatusInternalServerError)
   118  		return
   119  	}
   120  
   121  	var quantiles []float64
   122  	for _, flagStr := range strings.Split(r.FormValue("flags"), "|") {
   123  		if flagStr == "mut" {
   124  			quantiles = []float64{0, 1 - .999, 1 - .99, 1 - .95}
   125  			break
   126  		}
   127  	}
   128  
   129  	// Find a nice starting point for the plot.
   130  	xMin := time.Second
   131  	for xMin > 1 {
   132  		if mmu := mmuCurve.MMU(xMin); mmu < 0.0001 {
   133  			break
   134  		}
   135  		xMin /= 1000
   136  	}
   137  	// Cover six orders of magnitude.
   138  	xMax := xMin * 1e6
   139  	// But no more than the length of the trace.
   140  	minEvent, maxEvent := mu[0][0].Time, mu[0][len(mu[0])-1].Time
   141  	for _, mu1 := range mu[1:] {
   142  		if mu1[0].Time < minEvent {
   143  			minEvent = mu1[0].Time
   144  		}
   145  		if mu1[len(mu1)-1].Time > maxEvent {
   146  			maxEvent = mu1[len(mu1)-1].Time
   147  		}
   148  	}
   149  	if maxMax := time.Duration(maxEvent - minEvent); xMax > maxMax {
   150  		xMax = maxMax
   151  	}
   152  	// Compute MMU curve.
   153  	logMin, logMax := math.Log(float64(xMin)), math.Log(float64(xMax))
   154  	const samples = 100
   155  	plot := make([][]float64, samples)
   156  	for i := 0; i < samples; i++ {
   157  		window := time.Duration(math.Exp(float64(i)/(samples-1)*(logMax-logMin) + logMin))
   158  		if quantiles == nil {
   159  			plot[i] = make([]float64, 2)
   160  			plot[i][1] = mmuCurve.MMU(window)
   161  		} else {
   162  			plot[i] = make([]float64, 1+len(quantiles))
   163  			copy(plot[i][1:], mmuCurve.MUD(window, quantiles))
   164  		}
   165  		plot[i][0] = float64(window)
   166  	}
   167  
   168  	// Create JSON response.
   169  	err = json.NewEncoder(w).Encode(map[string]any{"xMin": int64(xMin), "xMax": int64(xMax), "quantiles": quantiles, "curve": plot})
   170  	if err != nil {
   171  		log.Printf("failed to serialize response: %v", err)
   172  		return
   173  	}
   174  }
   175  
   176  var templMMU = `<!doctype html>
   177  <html>
   178    <head>
   179      <meta charset="utf-8">
   180      <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
   181      <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
   182      <script type="text/javascript">
   183        google.charts.load('current', {'packages':['corechart']});
   184        var chartsReady = false;
   185        google.charts.setOnLoadCallback(function() { chartsReady = true; refreshChart(); });
   186  
   187        var chart;
   188        var curve;
   189  
   190        function niceDuration(ns) {
   191            if (ns < 1e3) { return ns + 'ns'; }
   192            else if (ns < 1e6) { return ns / 1e3 + 'µs'; }
   193            else if (ns < 1e9) { return ns / 1e6 + 'ms'; }
   194            else { return ns / 1e9 + 's'; }
   195        }
   196  
   197        function niceQuantile(q) {
   198          return 'p' + q*100;
   199        }
   200  
   201        function mmuFlags() {
   202          var flags = "";
   203          $("#options input").each(function(i, elt) {
   204            if (elt.checked)
   205              flags += "|" + elt.id;
   206          });
   207          return flags.substr(1);
   208        }
   209  
   210        function refreshChart() {
   211          if (!chartsReady) return;
   212          var container = $('#mmu_chart');
   213          container.css('opacity', '.5');
   214          refreshChart.count++;
   215          var seq = refreshChart.count;
   216          $.getJSON('?mode=plot&flags=' + mmuFlags())
   217           .fail(function(xhr, status, error) {
   218             alert('failed to load plot: ' + status);
   219           })
   220           .done(function(result) {
   221             if (refreshChart.count === seq)
   222               drawChart(result);
   223           });
   224        }
   225        refreshChart.count = 0;
   226  
   227        function drawChart(plotData) {
   228          curve = plotData.curve;
   229          var data = new google.visualization.DataTable();
   230          data.addColumn('number', 'Window duration');
   231          data.addColumn('number', 'Minimum mutator utilization');
   232          if (plotData.quantiles) {
   233            for (var i = 1; i < plotData.quantiles.length; i++) {
   234              data.addColumn('number', niceQuantile(1 - plotData.quantiles[i]) + ' MU');
   235            }
   236          }
   237          data.addRows(curve);
   238          for (var i = 0; i < curve.length; i++) {
   239            data.setFormattedValue(i, 0, niceDuration(curve[i][0]));
   240          }
   241  
   242          var options = {
   243            chart: {
   244              title: 'Minimum mutator utilization',
   245            },
   246            hAxis: {
   247              title: 'Window duration',
   248              scaleType: 'log',
   249              ticks: [],
   250            },
   251            vAxis: {
   252              title: 'Minimum mutator utilization',
   253              minValue: 0.0,
   254              maxValue: 1.0,
   255            },
   256            legend: { position: 'none' },
   257            focusTarget: 'category',
   258            width: 900,
   259            height: 500,
   260            chartArea: { width: '80%', height: '80%' },
   261          };
   262          for (var v = plotData.xMin; v <= plotData.xMax; v *= 10) {
   263            options.hAxis.ticks.push({v:v, f:niceDuration(v)});
   264          }
   265          if (plotData.quantiles) {
   266            options.vAxis.title = 'Mutator utilization';
   267            options.legend.position = 'in';
   268          }
   269  
   270          var container = $('#mmu_chart');
   271          container.empty();
   272          container.css('opacity', '');
   273          chart = new google.visualization.LineChart(container[0]);
   274          chart = new google.visualization.LineChart(document.getElementById('mmu_chart'));
   275          chart.draw(data, options);
   276  
   277          google.visualization.events.addListener(chart, 'select', selectHandler);
   278          $('#details').empty();
   279        }
   280  
   281        function selectHandler() {
   282          var items = chart.getSelection();
   283          if (items.length === 0) {
   284            return;
   285          }
   286          var details = $('#details');
   287          details.empty();
   288          var windowNS = curve[items[0].row][0];
   289          var url = '?mode=details&window=' + windowNS + '&flags=' + mmuFlags();
   290          $.getJSON(url)
   291           .fail(function(xhr, status, error) {
   292              details.text(status + ': ' + url + ' could not be loaded');
   293           })
   294           .done(function(worst) {
   295              details.text('Lowest mutator utilization in ' + niceDuration(windowNS) + ' windows:');
   296              for (var i = 0; i < worst.length; i++) {
   297                details.append($('<br>'));
   298                var text = worst[i].MutatorUtil.toFixed(3) + ' at time ' + niceDuration(worst[i].Time);
   299                details.append($('<a/>').text(text).attr('href', worst[i].URL));
   300              }
   301           });
   302        }
   303  
   304        $.when($.ready).then(function() {
   305          $("#options input").click(refreshChart);
   306        });
   307      </script>
   308      <style>
   309        .help {
   310          display: inline-block;
   311          position: relative;
   312          width: 1em;
   313          height: 1em;
   314          border-radius: 50%;
   315          color: #fff;
   316          background: #555;
   317          text-align: center;
   318          cursor: help;
   319        }
   320        .help > span {
   321          display: none;
   322        }
   323        .help:hover > span {
   324          display: block;
   325          position: absolute;
   326          left: 1.1em;
   327          top: 1.1em;
   328          background: #555;
   329          text-align: left;
   330          width: 20em;
   331          padding: 0.5em;
   332          border-radius: 0.5em;
   333          z-index: 5;
   334        }
   335      </style>
   336    </head>
   337    <body>
   338      <div style="position: relative">
   339        <div id="mmu_chart" style="width: 900px; height: 500px; display: inline-block; vertical-align: top">Loading plot...</div>
   340        <div id="options" style="display: inline-block; vertical-align: top">
   341          <p>
   342            <b>View</b><br>
   343            <input type="radio" name="view" id="system" checked><label for="system">System</label>
   344            <span class="help">?<span>Consider whole system utilization. For example, if one of four procs is available to the mutator, mutator utilization will be 0.25. This is the standard definition of an MMU.</span></span><br>
   345            <input type="radio" name="view" id="perProc"><label for="perProc">Per-goroutine</label>
   346            <span class="help">?<span>Consider per-goroutine utilization. When even one goroutine is interrupted by GC, mutator utilization is 0.</span></span><br>
   347          </p>
   348          <p>
   349            <b>Include</b><br>
   350            <input type="checkbox" id="stw" checked><label for="stw">STW</label>
   351            <span class="help">?<span>Stop-the-world stops all goroutines simultaneously.</span></span><br>
   352            <input type="checkbox" id="background" checked><label for="background">Background workers</label>
   353            <span class="help">?<span>Background workers are GC-specific goroutines. 25% of the CPU is dedicated to background workers during GC.</span></span><br>
   354            <input type="checkbox" id="assist" checked><label for="assist">Mark assist</label>
   355            <span class="help">?<span>Mark assists are performed by allocation to prevent the mutator from outpacing GC.</span></span><br>
   356            <input type="checkbox" id="sweep"><label for="sweep">Sweep</label>
   357            <span class="help">?<span>Sweep reclaims unused memory between GCs. (Enabling this may be very slow.).</span></span><br>
   358          </p>
   359          <p>
   360            <b>Display</b><br>
   361            <input type="checkbox" id="mut"><label for="mut">Show percentiles</label>
   362            <span class="help">?<span>Display percentile mutator utilization in addition to minimum. E.g., p99 MU drops the worst 1% of windows.</span></span><br>
   363          </p>
   364        </div>
   365      </div>
   366      <div id="details">Select a point for details.</div>
   367    </body>
   368  </html>
   369  `
   370  
   371  // HandleDetails serves details of an MMU graph at a particular window.
   372  func (m *mmu) HandleDetails(w http.ResponseWriter, r *http.Request) {
   373  	_, mmuCurve, err := m.get(requestUtilFlags(r))
   374  	if err != nil {
   375  		http.Error(w, fmt.Sprintf("failed to produce MMU data: %v", err), http.StatusInternalServerError)
   376  		return
   377  	}
   378  
   379  	windowStr := r.FormValue("window")
   380  	window, err := strconv.ParseUint(windowStr, 10, 64)
   381  	if err != nil {
   382  		http.Error(w, fmt.Sprintf("failed to parse window parameter %q: %v", windowStr, err), http.StatusBadRequest)
   383  		return
   384  	}
   385  	worst := mmuCurve.Examples(time.Duration(window), 10)
   386  
   387  	// Construct a link for each window.
   388  	var links []linkedUtilWindow
   389  	for _, ui := range worst {
   390  		links = append(links, m.newLinkedUtilWindow(ui, time.Duration(window)))
   391  	}
   392  
   393  	err = json.NewEncoder(w).Encode(links)
   394  	if err != nil {
   395  		log.Printf("failed to serialize trace: %v", err)
   396  		return
   397  	}
   398  }
   399  
   400  type linkedUtilWindow struct {
   401  	trace.UtilWindow
   402  	URL string
   403  }
   404  
   405  func (m *mmu) newLinkedUtilWindow(ui trace.UtilWindow, window time.Duration) linkedUtilWindow {
   406  	// Find the range containing this window.
   407  	var r Range
   408  	for _, r = range m.ranges {
   409  		if r.EndTime > ui.Time {
   410  			break
   411  		}
   412  	}
   413  	return linkedUtilWindow{ui, fmt.Sprintf("%s#%v:%v", r.URL(ViewProc), float64(ui.Time)/1e6, float64(ui.Time+int64(window))/1e6)}
   414  }
   415  

View as plain text