// Copyright 2017 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Minimum mutator utilization (MMU) graphing. // TODO: // // In worst window list, show break-down of GC utilization sources // (STW, assist, etc). Probably requires a different MutatorUtil // representation. // // When a window size is selected, show a second plot of the mutator // utilization distribution for that window size. // // Render plot progressively so rough outline is visible quickly even // for very complex MUTs. Start by computing just a few window sizes // and then add more window sizes. // // Consider using sampling to compute an approximate MUT. This would // work by sampling the mutator utilization at randomly selected // points in time in the trace to build an empirical distribution. We // could potentially put confidence intervals on these estimates and // render this progressively as we refine the distributions. package main import ( "encoding/json" "fmt" "internal/trace" "log" "math" "net/http" "strconv" "strings" "sync" "time" ) func init() { http.HandleFunc("/mmu", httpMMU) http.HandleFunc("/mmuPlot", httpMMUPlot) http.HandleFunc("/mmuDetails", httpMMUDetails) } var utilFlagNames = map[string]trace.UtilFlags{ "perProc": trace.UtilPerProc, "stw": trace.UtilSTW, "background": trace.UtilBackground, "assist": trace.UtilAssist, "sweep": trace.UtilSweep, } type mmuCacheEntry struct { init sync.Once util [][]trace.MutatorUtil mmuCurve *trace.MMUCurve err error } var mmuCache struct { m map[trace.UtilFlags]*mmuCacheEntry lock sync.Mutex } func init() { mmuCache.m = make(map[trace.UtilFlags]*mmuCacheEntry) } func getMMUCurve(r *http.Request) ([][]trace.MutatorUtil, *trace.MMUCurve, error) { var flags trace.UtilFlags for _, flagStr := range strings.Split(r.FormValue("flags"), "|") { flags |= utilFlagNames[flagStr] } mmuCache.lock.Lock() c := mmuCache.m[flags] if c == nil { c = new(mmuCacheEntry) mmuCache.m[flags] = c } mmuCache.lock.Unlock() c.init.Do(func() { events, err := parseEvents() if err != nil { c.err = err } else { c.util = trace.MutatorUtilization(events, flags) c.mmuCurve = trace.NewMMUCurve(c.util) } }) return c.util, c.mmuCurve, c.err } // httpMMU serves the MMU plot page. func httpMMU(w http.ResponseWriter, r *http.Request) { http.ServeContent(w, r, "", time.Time{}, strings.NewReader(templMMU)) } // httpMMUPlot serves the JSON data for the MMU plot. func httpMMUPlot(w http.ResponseWriter, r *http.Request) { mu, mmuCurve, err := getMMUCurve(r) if err != nil { http.Error(w, fmt.Sprintf("failed to parse events: %v", err), http.StatusInternalServerError) return } var quantiles []float64 for _, flagStr := range strings.Split(r.FormValue("flags"), "|") { if flagStr == "mut" { quantiles = []float64{0, 1 - .999, 1 - .99, 1 - .95} break } } // Find a nice starting point for the plot. xMin := time.Second for xMin > 1 { if mmu := mmuCurve.MMU(xMin); mmu < 0.0001 { break } xMin /= 1000 } // Cover six orders of magnitude. xMax := xMin * 1e6 // But no more than the length of the trace. minEvent, maxEvent := mu[0][0].Time, mu[0][len(mu[0])-1].Time for _, mu1 := range mu[1:] { if mu1[0].Time < minEvent { minEvent = mu1[0].Time } if mu1[len(mu1)-1].Time > maxEvent { maxEvent = mu1[len(mu1)-1].Time } } if maxMax := time.Duration(maxEvent - minEvent); xMax > maxMax { xMax = maxMax } // Compute MMU curve. logMin, logMax := math.Log(float64(xMin)), math.Log(float64(xMax)) const samples = 100 plot := make([][]float64, samples) for i := 0; i < samples; i++ { window := time.Duration(math.Exp(float64(i)/(samples-1)*(logMax-logMin) + logMin)) if quantiles == nil { plot[i] = make([]float64, 2) plot[i][1] = mmuCurve.MMU(window) } else { plot[i] = make([]float64, 1+len(quantiles)) copy(plot[i][1:], mmuCurve.MUD(window, quantiles)) } plot[i][0] = float64(window) } // Create JSON response. err = json.NewEncoder(w).Encode(map[string]interface{}{"xMin": int64(xMin), "xMax": int64(xMax), "quantiles": quantiles, "curve": plot}) if err != nil { log.Printf("failed to serialize response: %v", err) return } } var templMMU = `<!doctype html> <html> <head> <meta charset="utf-8"> <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script type="text/javascript"> google.charts.load('current', {'packages':['corechart']}); var chartsReady = false; google.charts.setOnLoadCallback(function() { chartsReady = true; refreshChart(); }); var chart; var curve; function niceDuration(ns) { if (ns < 1e3) { return ns + 'ns'; } else if (ns < 1e6) { return ns / 1e3 + 'µs'; } else if (ns < 1e9) { return ns / 1e6 + 'ms'; } else { return ns / 1e9 + 's'; } } function niceQuantile(q) { return 'p' + q*100; } function mmuFlags() { var flags = ""; $("#options input").each(function(i, elt) { if (elt.checked) flags += "|" + elt.id; }); return flags.substr(1); } function refreshChart() { if (!chartsReady) return; var container = $('#mmu_chart'); container.css('opacity', '.5'); refreshChart.count++; var seq = refreshChart.count; $.getJSON('/mmuPlot?flags=' + mmuFlags()) .fail(function(xhr, status, error) { alert('failed to load plot: ' + status); }) .done(function(result) { if (refreshChart.count === seq) drawChart(result); }); } refreshChart.count = 0; function drawChart(plotData) { curve = plotData.curve; var data = new google.visualization.DataTable(); data.addColumn('number', 'Window duration'); data.addColumn('number', 'Minimum mutator utilization'); if (plotData.quantiles) { for (var i = 1; i < plotData.quantiles.length; i++) { data.addColumn('number', niceQuantile(1 - plotData.quantiles[i]) + ' MU'); } } data.addRows(curve); for (var i = 0; i < curve.length; i++) { data.setFormattedValue(i, 0, niceDuration(curve[i][0])); } var options = { chart: { title: 'Minimum mutator utilization', }, hAxis: { title: 'Window duration', scaleType: 'log', ticks: [], }, vAxis: { title: 'Minimum mutator utilization', minValue: 0.0, maxValue: 1.0, }, legend: { position: 'none' }, focusTarget: 'category', width: 900, height: 500, chartArea: { width: '80%', height: '80%' }, }; for (var v = plotData.xMin; v <= plotData.xMax; v *= 10) { options.hAxis.ticks.push({v:v, f:niceDuration(v)}); } if (plotData.quantiles) { options.vAxis.title = 'Mutator utilization'; options.legend.position = 'in'; } var container = $('#mmu_chart'); container.empty(); container.css('opacity', ''); chart = new google.visualization.LineChart(container[0]); chart = new google.visualization.LineChart(document.getElementById('mmu_chart')); chart.draw(data, options); google.visualization.events.addListener(chart, 'select', selectHandler); $('#details').empty(); } function selectHandler() { var items = chart.getSelection(); if (items.length === 0) { return; } var details = $('#details'); details.empty(); var windowNS = curve[items[0].row][0]; var url = '/mmuDetails?window=' + windowNS + '&flags=' + mmuFlags(); $.getJSON(url) .fail(function(xhr, status, error) { details.text(status + ': ' + url + ' could not be loaded'); }) .done(function(worst) { details.text('Lowest mutator utilization in ' + niceDuration(windowNS) + ' windows:'); for (var i = 0; i < worst.length; i++) { details.append($('<br/>')); var text = worst[i].MutatorUtil.toFixed(3) + ' at time ' + niceDuration(worst[i].Time); details.append($('<a/>').text(text).attr('href', worst[i].URL)); } }); } $.when($.ready).then(function() { $("#options input").click(refreshChart); }); </script> <style> .help { display: inline-block; position: relative; width: 1em; height: 1em; border-radius: 50%; color: #fff; background: #555; text-align: center; cursor: help; } .help > span { display: none; } .help:hover > span { display: block; position: absolute; left: 1.1em; top: 1.1em; background: #555; text-align: left; width: 20em; padding: 0.5em; border-radius: 0.5em; z-index: 5; } </style> </head> <body> <div style="position: relative"> <div id="mmu_chart" style="width: 900px; height: 500px; display: inline-block; vertical-align: top">Loading plot...</div> <div id="options" style="display: inline-block; vertical-align: top"> <p> <b>View</b><br/> <input type="radio" name="view" id="system" checked><label for="system">System</label> <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/> <input type="radio" name="view" id="perProc"><label for="perProc">Per-goroutine</label> <span class="help">?<span>Consider per-goroutine utilization. When even one goroutine is interrupted by GC, mutator utilization is 0.</span></span><br/> </p> <p> <b>Include</b><br/> <input type="checkbox" id="stw" checked><label for="stw">STW</label> <span class="help">?<span>Stop-the-world stops all goroutines simultaneously.</span></span><br/> <input type="checkbox" id="background" checked><label for="background">Background workers</label> <span class="help">?<span>Background workers are GC-specific goroutines. 25% of the CPU is dedicated to background workers during GC.</span></span><br/> <input type="checkbox" id="assist" checked><label for="assist">Mark assist</label> <span class="help">?<span>Mark assists are performed by allocation to prevent the mutator from outpacing GC.</span></span><br/> <input type="checkbox" id="sweep"><label for="sweep">Sweep</label> <span class="help">?<span>Sweep reclaims unused memory between GCs. (Enabling this may be very slow.).</span></span><br/> </p> <p> <b>Display</b><br/> <input type="checkbox" id="mut"><label for="mut">Show percentiles</label> <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/> </p> </div> </div> <div id="details">Select a point for details.</div> </body> </html> ` // httpMMUDetails serves details of an MMU graph at a particular window. func httpMMUDetails(w http.ResponseWriter, r *http.Request) { _, mmuCurve, err := getMMUCurve(r) if err != nil { http.Error(w, fmt.Sprintf("failed to parse events: %v", err), http.StatusInternalServerError) return } windowStr := r.FormValue("window") window, err := strconv.ParseUint(windowStr, 10, 64) if err != nil { http.Error(w, fmt.Sprintf("failed to parse window parameter %q: %v", windowStr, err), http.StatusBadRequest) return } worst := mmuCurve.Examples(time.Duration(window), 10) // Construct a link for each window. var links []linkedUtilWindow for _, ui := range worst { links = append(links, newLinkedUtilWindow(ui, time.Duration(window))) } err = json.NewEncoder(w).Encode(links) if err != nil { log.Printf("failed to serialize trace: %v", err) return } } type linkedUtilWindow struct { trace.UtilWindow URL string } func newLinkedUtilWindow(ui trace.UtilWindow, window time.Duration) linkedUtilWindow { // Find the range containing this window. var r Range for _, r = range ranges { if r.EndTime > ui.Time { break } } return linkedUtilWindow{ui, fmt.Sprintf("%s#%v:%v", r.URL(), float64(ui.Time)/1e6, float64(ui.Time+int64(window))/1e6)} }