github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/trace/traceviewer/mmu.go (about)

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