blob: 0bc1233b44dee9717891e3342c6e3234c2ef12f5 [file] [log] [blame]
// Copyright 2023 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 traceviewer
import (
"encoding/json"
"fmt"
"internal/trace"
"log"
"math"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
type MutatorUtilFunc func(trace.UtilFlags) ([][]trace.MutatorUtil, error)
func MMUHandlerFunc(ranges []Range, f MutatorUtilFunc) http.HandlerFunc {
mmu := &mmu{
cache: make(map[trace.UtilFlags]*mmuCacheEntry),
f: f,
ranges: ranges,
}
return func(w http.ResponseWriter, r *http.Request) {
switch r.FormValue("mode") {
case "plot":
mmu.HandlePlot(w, r)
return
case "details":
mmu.HandleDetails(w, r)
return
}
http.ServeContent(w, r, "", time.Time{}, strings.NewReader(templMMU))
}
}
var utilFlagNames = map[string]trace.UtilFlags{
"perProc": trace.UtilPerProc,
"stw": trace.UtilSTW,
"background": trace.UtilBackground,
"assist": trace.UtilAssist,
"sweep": trace.UtilSweep,
}
func requestUtilFlags(r *http.Request) trace.UtilFlags {
var flags trace.UtilFlags
for _, flagStr := range strings.Split(r.FormValue("flags"), "|") {
flags |= utilFlagNames[flagStr]
}
return flags
}
type mmuCacheEntry struct {
init sync.Once
util [][]trace.MutatorUtil
mmuCurve *trace.MMUCurve
err error
}
type mmu struct {
mu sync.Mutex
cache map[trace.UtilFlags]*mmuCacheEntry
f MutatorUtilFunc
ranges []Range
}
func (m *mmu) get(flags trace.UtilFlags) ([][]trace.MutatorUtil, *trace.MMUCurve, error) {
m.mu.Lock()
entry := m.cache[flags]
if entry == nil {
entry = new(mmuCacheEntry)
m.cache[flags] = entry
}
m.mu.Unlock()
entry.init.Do(func() {
util, err := m.f(flags)
if err != nil {
entry.err = err
} else {
entry.util = util
entry.mmuCurve = trace.NewMMUCurve(util)
}
})
return entry.util, entry.mmuCurve, entry.err
}
// HandlePlot serves the JSON data for the MMU plot.
func (m *mmu) HandlePlot(w http.ResponseWriter, r *http.Request) {
mu, mmuCurve, err := m.get(requestUtilFlags(r))
if err != nil {
http.Error(w, fmt.Sprintf("failed to produce MMU data: %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]any{"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('?mode=plot&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 = '?mode=details&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>
`
// HandleDetails serves details of an MMU graph at a particular window.
func (m *mmu) HandleDetails(w http.ResponseWriter, r *http.Request) {
_, mmuCurve, err := m.get(requestUtilFlags(r))
if err != nil {
http.Error(w, fmt.Sprintf("failed to produce MMU data: %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, m.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 (m *mmu) newLinkedUtilWindow(ui trace.UtilWindow, window time.Duration) linkedUtilWindow {
// Find the range containing this window.
var r Range
for _, r = range m.ranges {
if r.EndTime > ui.Time {
break
}
}
return linkedUtilWindow{ui, fmt.Sprintf("%s#%v:%v", r.URL(ViewProc), float64(ui.Time)/1e6, float64(ui.Time+int64(window))/1e6)}
}