1<!DOCTYPE html> 2<html> 3<!-- 4Copyright 2016 the V8 project authors. All rights reserved. Use of this source 5code is governed by a BSD-style license that can be found in the LICENSE file. 6--> 7 8<head> 9<meta charset="utf-8"> 10<title>V8 Parse Processor</title> 11<style> 12 html { 13 font-family: monospace; 14 } 15 16 .parse { 17 background-color: red; 18 border: 1px red solid; 19 } 20 21 .preparse { 22 background-color: orange; 23 border: 1px orange solid; 24 } 25 26 .resolution { 27 background-color: green; 28 border: 1px green solid; 29 } 30 31 .execution { 32 background-color: black; 33 border-left: 2px black solid; 34 z-index: -1; 35 } 36 37 .script { 38 margin-top: 1em; 39 overflow: visible; 40 clear: both; 41 border-top: 2px black dotted; 42 } 43 .script h3 { 44 height: 20px; 45 margin-bottom: 0.5em; 46 white-space: nowrap; 47 } 48 49 .script-details { 50 float: left; 51 } 52 53 .chart { 54 float: left; 55 margin-right: 2em; 56 } 57 58 .funktion-list { 59 float: left; 60 height: 400px; 61 } 62 63 .funktion-list > ul { 64 height: 80%; 65 overflow-y: scroll; 66 } 67 68 .funktion { 69 } 70 71 .script-size { 72 display: inline-flex; 73 background-color: #505050; 74 border-radius: 3px; 75 padding: 3px; 76 margin: 2px; 77 white-space: nowrap; 78 overflow: hidden; 79 text-decoration: none; 80 color: white; 81 transition: auto ease-in-out 0.8s; 82 max-width: 500px; 83 } 84 .script-size:hover { 85 max-width: 100000px !important; 86 transition: auto ease-in-out 0.8s; 87 } 88 .script-size.eval { 89 background-color: #ee6300fc; 90 } 91 .script-size.streaming { 92 background-color: #008aff; 93 } 94 .script-size.deserialized { 95 background-color: #1fad00fc; 96 } 97 98 .script-details { 99 padding-right: 5px; 100 margin-right: 4px; 101 } 102 /* all but the last need a border */ 103 .script-details:nth-last-child(n+2) { 104 border-right: 1px white solid; 105 } 106 107 .script-details.id { 108 min-width: 2em; 109 text-align: right; 110 } 111</style> 112<script src="https://www.gstatic.com/charts/loader.js"></script> 113<script type="module"> 114 115import { ParseProcessor, kSecondsToMillis, BYTES, PERCENT } from "./parse-processor.mjs"; 116 117google.charts.load('current', {packages: ['corechart']}); 118 119function $(query) { 120 return document.querySelector(query); 121} 122 123window.addEventListener('DOMContentLoaded', (event) => { 124 $("#uploadInput").focus(); 125}); 126 127document.loadFile = function() { 128 let files = $('#uploadInput').files; 129 130 let file = files[0]; 131 let reader = new FileReader(); 132 133 reader.onload = function(evt) { 134 const kTimerName = 'parse log file'; 135 console.time(kTimerName); 136 let parseProcessor = new ParseProcessor(); 137 parseProcessor.processString(this.result); 138 console.timeEnd(kTimerName); 139 renderParseResults(parseProcessor); 140 document.parseProcessor = parseProcessor; 141 } 142 reader.readAsText(file); 143} 144 145function createNode(tag, classNames) { 146 let node = document.createElement(tag); 147 if (classNames) { 148 if (Array.isArray(classNames)) { 149 node.classList.add(...classNames); 150 } else { 151 node.className = classNames; 152 } 153 } 154 return node; 155} 156 157function div(...args) { 158 return createNode('div', ...args); 159} 160 161function h1(string) { 162 let node = createNode('h1'); 163 node.appendChild(text(string)); 164 return node; 165} 166 167function h3(string, ...args) { 168 let node = createNode('h3', ...args); 169 if (string) node.appendChild(text(string)); 170 return node; 171} 172 173function a(href, string, ...args) { 174 let link = createNode('a', ...args); 175 if (href.length) link.href = href; 176 if (string) link.appendChild(text(string)); 177 return link; 178} 179 180function text(string) { 181 return document.createTextNode(string); 182} 183 184function delay(t) { 185 return new Promise(resolve => setTimeout(resolve, t)); 186} 187 188function renderParseResults(parseProcessor) { 189 let result = $('#result'); 190 // clear out all existing result pages; 191 result.innerHTML = ''; 192 const start = parseProcessor.firstEventTimestamp; 193 const end = parseProcessor.lastEventTimestamp; 194 renderScript(result, parseProcessor.totalScript, start, end); 195 // Build up the graphs lazily to keep the page responsive. 196 parseProcessor.scripts.forEach( 197 script => renderScript(result, script, start, end)); 198 renderScriptSizes(parseProcessor); 199 // Install an intersection observer to lazily load the graphs when the script 200 // div becomes visible for the first time. 201 var io = new IntersectionObserver((entries, observer) => { 202 entries.forEach(entry => { 203 if (entry.intersectionRatio == 0) return; 204 console.assert(!entry.target.querySelector('.graph')); 205 let target = entry.target; 206 appendGraph(target.script, target, start, end); 207 observer.unobserve(entry.target); 208 }); 209 }, {rootMargin: '400px'}); 210 document.querySelectorAll('.script').forEach(div => io.observe(div)); 211} 212 213const kTimeFactor = 10; 214const kHeight = 20; 215const kFunktionTopOffset = 50; 216 217function renderScript(result, script, start, end) { 218 // Filter out empty scripts. 219 if (script.isEmpty() || script.lastParseEvent == 0) return; 220 221 let scriptDiv = div('script'); 222 scriptDiv.script = script; 223 224 let scriptTitle = h3(); 225 let anchor = a("", 'Script #' + script.id); 226 anchor.name = "script"+script.id 227 scriptTitle.appendChild(anchor); 228 scriptDiv.appendChild(scriptTitle); 229 if (script.file) scriptTitle.appendChild(a(script.file, script.file)); 230 let summary = createNode('pre', 'script-details'); 231 summary.appendChild(text(script.summary)); 232 scriptDiv.appendChild(summary); 233 result.appendChild(scriptDiv); 234} 235 236function renderScriptSizes(parseProcessor) { 237 let scriptsDiv = $('#scripts'); 238 parseProcessor.scripts.forEach( 239 script => { 240 let scriptDiv = a(`#script${script.id}`, '', 'script-size'); 241 let scriptId = div('script-details'); 242 scriptId.classList.add('id'); 243 scriptId.innerText = `id=${script.id}`; 244 scriptDiv.appendChild(scriptId); 245 let scriptSize = div('script-details'); 246 scriptSize.innerText = BYTES(script.bytesTotal); 247 scriptDiv.appendChild(scriptSize); 248 let scriptUrl = div('script-details'); 249 if (script.isEval) { 250 scriptUrl.innerText = "eval"; 251 scriptDiv.classList.add('eval'); 252 } else { 253 scriptUrl.innerText = script.file.split("/").pop(); 254 } 255 if (script.isStreamingCompiled ) { 256 scriptDiv.classList.add('streaming'); 257 } else if (script.deserializationTimestamp > 0) { 258 scriptDiv.classList.add('deserialized'); 259 } 260 scriptDiv.appendChild(scriptUrl); 261 scriptDiv.style.maxWidth = `${script.bytesTotal * 0.001}px`; 262 scriptsDiv.appendChild(scriptDiv); 263 }); 264} 265 266const kMaxTime = 120 * kSecondsToMillis; 267// Resolution of the graphs 268const kTimeIncrement = 1; 269const kSelectionTimespan = 2; 270// TODO(cbruni): support compilation cache hit. 271class Series { 272 constructor(metricName, description, color, lineStyle, isArea=false) { 273 this.metricName = metricName; 274 this.description = description; 275 this.color = color; 276 this.lineStyle = lineStyle; 277 this.isArea = isArea; 278 } 279} 280const series = [ 281 new Series('firstParseEvent', 'Any Parse', '#4D4D4D', undefined, true), 282 new Series('execution', '1st Exec', '#fff700',undefined, true), 283 new Series('firstCompileEvent', 'Any Compile', '#5DA5DA', undefined, true), 284 285 new Series('compile', 'Eager Compile', '#FAA43A'), 286 new Series('lazyCompile', 'Lazy Compile','#FAA43A', 'dash'), 287 288 new Series('parse', 'Parsing', '#F17CB0'), 289 new Series('preparse', 'Preparse', '#B2912F'), 290 new Series('resolution', 'Preparse with Var. Resolution', '#B276B2'), 291 292 new Series('deserialization', 'Deserialization', '#DECF3F'), 293 294 new Series('baseline', 'Baseline', '#606611', 'dash'), 295 new Series('optimize', 'Optimize', '#F15854'), 296]; 297const metricNames = series.map(each => each.metricName); 298// Display cumulative values (useuful for bytes). 299const kCumulative = true; 300// Include durations in the graphs. 301const kUseDuration = false; 302 303 304function appendGraph(script, parentNode, start, end) { 305 const timerLabel = 'graph script=' + script.id; 306 // TODO(cbruni): add support for network events 307 308 console.time(timerLabel); 309 let data = new google.visualization.DataTable(); 310 data.addColumn('number', 'Duration'); 311 // The series are interleave bytes processed, time spent and thus have two 312 // different vAxes. 313 let seriesOptions = []; 314 series.forEach(series => { 315 // Add the bytes column. 316 data.addColumn('number', series.description); 317 let options = {targetAxisIndex: 0, color: series.color}; 318 if (series.isArea) options.type = 'area'; 319 if (series.lineStyle === 'dash') options.lineDashStyle = [4, 4]; 320 seriesOptions.push(options) 321 // Add the time column. 322 if (kUseDuration) { 323 data.addColumn('number', series.description + ' Duration'); 324 seriesOptions.push( 325 {targetAxisIndex: 1, color: series.color, lineDashStyle: [3, 2]}); 326 } 327 }); 328 329 const maxTime = Math.min(kMaxTime, end); 330 console.time('metrics'); 331 let metricValues = 332 script.getAccumulatedTimeMetrics(metricNames , 0, maxTime, kTimeIncrement, 333 kCumulative, kUseDuration); 334 console.timeEnd('metrics'); 335 // Make sure that the series added to the graph matches the returned values. 336 console.assert(metricValues[0].length == seriesOptions.length + 1); 337 data.addRows(metricValues); 338 339 let options = { 340 explorer: { 341 actions: ['dragToZoom', 'rightClickToReset'], 342 maxZoomIn: 0.01 343 }, 344 hAxis: { 345 format: '#,###.##s' 346 }, 347 vAxes: { 348 0: {title: 'Bytes Touched', format: 'short'}, 349 1: {title: 'Duration', format: '#,###ms'} 350 }, 351 height: 400, 352 width: 1000, 353 chartArea: {left: 70, top: 0, right: 160, height: "90%"}, 354 // The first series should be a area chart (total bytes touched), 355 series: seriesOptions, 356 // everthing else is a line. 357 seriesType: 'line' 358 }; 359 let graphNode = createNode('div', 'chart'); 360 let listNode = createNode('div', 'funktion-list'); 361 parentNode.appendChild(graphNode); 362 parentNode.appendChild(listNode); 363 let chart = new google.visualization.ComboChart(graphNode); 364 google.visualization.events.addListener(chart, 'select', 365 () => selectGraphPointHandler(chart, data, script, parentNode)); 366 chart.draw(data, options); 367 // Add event listeners 368 console.timeEnd(timerLabel); 369} 370 371function selectGraphPointHandler(chart, data, script, parentNode) { 372 let selection = chart.getSelection(); 373 if (selection.length <= 0) return; 374 // Display a list of funktions with events at the given time. 375 let {row, column} = selection[0]; 376 if (row === null|| column === null) return; 377 const kEntrySize = kUseDuration ? 2 : 1; 378 let [metric, description] = series[((column-1)/ kEntrySize) | 0]; 379 let time = data.getValue(row, 0); 380 let funktions = script.getFunktionsAtTime( 381 time * kSecondsToMillis, kSelectionTimespan, metric); 382 let oldList = parentNode.querySelector('.funktion-list'); 383 parentNode.replaceChild( 384 createFunktionList(metric, description, time, funktions), oldList); 385} 386 387function createFunktionList(metric, description, time, funktions) { 388 let container = createNode('div', 'funktion-list'); 389 container.appendChild(h3('Changes of "' + description + '" at ' + 390 time + 's: ' + funktions.length)); 391 let listNode = createNode('ul'); 392 funktions.forEach(funktion => { 393 let node = createNode('li', 'funktion'); 394 node.funktion = funktion; 395 node.appendChild(text(funktion.toString(false) + " ")); 396 let script = funktion.script; 397 if (script) { 398 node.appendChild(a("#script" + script.id, "in script " + script.id)); 399 } 400 listNode.appendChild(node); 401 }); 402 container.appendChild(listNode); 403 return container; 404} 405</script> 406</head> 407 408<body> 409 <h1>BEHOLD, THIS IS PARSEROR!</h1> 410 411 <h2>Usage</h2> 412 Run your script with <code>--log-function-events</code> and upload <code>v8.log</code> on this page:<br/> 413 <code>/path/to/d8 --log-function-events your_script.js</code> 414 415 <h2>Data</h2> 416 <form name="fileForm"> 417 <p> 418 <input id="uploadInput" type="file" name="files" onchange="loadFile();" accept=".log"> trace entries: <span id="count">0</span> 419 </p> 420 </form> 421 422 423 <h2>Scripts</h2> 424 <div id="scripts"></div> 425 426 <h2>Result</h2> 427 <div id="result"></div> 428</body> 429 430</html> 431