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 }