Source file src/cmd/trace/mmu.go

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

View as plain text