1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
98 func httpMMU(w http.ResponseWriter, r *http.Request) {
99 http.ServeContent(w, r, "", time.Time{}, strings.NewReader(templMMU))
100 }
101
102
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
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
127 xMax := xMin * 1e6
128
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
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
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
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
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
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