parse-processor.html 11.9 KB
Newer Older
1
<!DOCTYPE html>
2 3 4 5 6 7 8
<html>
<!--
Copyright 2016 the V8 project authors. All rights reserved.  Use of this source
code is governed by a BSD-style license that can be found in the LICENSE file.
-->

<head>
9 10
<meta charset="utf-8">
<title>V8 Parse Processor</title>
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
<style>
  html {
    font-family: monospace;
  }

  .parse {
    background-color: red;
    border: 1px red solid;
  }

  .preparse {
    background-color: orange;
    border: 1px orange solid;
  }

  .resolution {
    background-color: green;
    border: 1px green solid;
  }

  .execution {
    background-color: black;
    border-left: 2px black solid;
    z-index: -1;
  }

  .script {
    margin-top: 1em;
    overflow: visible;
    clear: both;
      border-top: 2px black dotted;
  }
  .script h3 {
    height: 20px;
    margin-bottom: 0.5em;
    white-space: nowrap;
  }

  .script-details {
    float: left;
  }

  .chart {
    float: left;
    margin-right: 2em;
  }

  .funktion-list {
    float: left;
    height: 400px;
  }

  .funktion-list > ul {
    height: 80%;
    overflow-y: scroll;
  }

  .funktion {
  }

71
  .script-size {
72
    display: inline-flex;
73 74 75 76 77 78 79 80
    background-color: #505050;
    border-radius: 3px;
    padding: 3px;
    margin: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-decoration: none;
    color: white;
81 82 83 84 85 86
    transition: auto ease-in-out 0.8s;
    max-width: 500px;
  }
  .script-size:hover {
    max-width: 100000px !important;
    transition: auto ease-in-out 0.8s;
87 88 89 90
  }
  .script-size.eval {
    background-color: #ee6300fc;
  }
91
  .script-size.streaming {
92 93
    background-color: #008aff;
  }
94 95 96
  .script-size.deserialized {
    background-color: #1fad00fc;
  }
97

98 99 100
  .script-details {
    padding-right: 5px;
    margin-right: 4px;
101
  }
102 103 104 105 106 107 108 109
  /* all but the last need a border  */
  .script-details:nth-last-child(n+2) {
    border-right: 1px white solid;
  }

  .script-details.id {
    min-width: 2em;
    text-align: right;
110
  }
111
</style>
112
<script src="https://www.gstatic.com/charts/loader.js"></script>
113 114
<script type="module">

115
import { ParseProcessor, kSecondsToMillis, BYTES, PERCENT } from "./parse-processor.mjs";
116

117 118 119 120 121 122
google.charts.load('current', {packages: ['corechart']});

function $(query) {
  return document.querySelector(query);
}

123 124 125 126 127
window.addEventListener('DOMContentLoaded', (event) => {
  $("#uploadInput").focus();
});

document.loadFile = function() {
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
  let files = $('#uploadInput').files;

  let file = files[0];
  let reader = new FileReader();

  reader.onload = function(evt) {
    const kTimerName = 'parse log file';
    console.time(kTimerName);
    let parseProcessor = new ParseProcessor();
    parseProcessor.processString(this.result);
    console.timeEnd(kTimerName);
    renderParseResults(parseProcessor);
    document.parseProcessor = parseProcessor;
  }
  reader.readAsText(file);
}

function createNode(tag, classNames) {
  let node = document.createElement(tag);
  if (classNames) {
    if (Array.isArray(classNames)) {
      node.classList.add(...classNames);
    } else {
      node.className = classNames;
    }
  }
  return node;
}

function div(...args) {
  return createNode('div', ...args);
}

function h1(string) {
  let node = createNode('h1');
  node.appendChild(text(string));
  return node;
}

function h3(string, ...args) {
  let node = createNode('h3', ...args);
  if (string) node.appendChild(text(string));
  return node;
}

function a(href, string, ...args) {
  let link = createNode('a', ...args);
  if (href.length) link.href = href;
  if (string) link.appendChild(text(string));
  return link;
}

function text(string) {
  return document.createTextNode(string);
}

function delay(t) {
185
  return new Promise(resolve => setTimeout(resolve, t));
186 187 188 189 190 191
}

function renderParseResults(parseProcessor) {
  let result = $('#result');
  // clear out all existing result pages;
  result.innerHTML = '';
192 193
  const start = parseProcessor.firstEventTimestamp;
  const end = parseProcessor.lastEventTimestamp;
194 195 196 197
  renderScript(result, parseProcessor.totalScript, start, end);
  // Build up the graphs lazily to keep the page responsive.
  parseProcessor.scripts.forEach(
      script => renderScript(result, script, start, end));
198
  renderScriptSizes(parseProcessor);
199 200 201 202 203 204 205 206 207 208
  // Install an intersection observer to lazily load the graphs when the script
  // div becomes visible for the first time.
  var io = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.intersectionRatio == 0) return;
      console.assert(!entry.target.querySelector('.graph'));
      let target = entry.target;
      appendGraph(target.script, target, start, end);
      observer.unobserve(entry.target);
    });
209
  }, {rootMargin: '400px'});
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
  document.querySelectorAll('.script').forEach(div => io.observe(div));
}

const kTimeFactor = 10;
const kHeight = 20;
const kFunktionTopOffset = 50;

function renderScript(result, script, start, end) {
  // Filter out empty scripts.
  if (script.isEmpty() || script.lastParseEvent == 0) return;

  let scriptDiv = div('script');
  scriptDiv.script = script;

  let scriptTitle = h3();
225
  let anchor = a("", 'Script #' + script.id);
226 227 228
  anchor.name = "script"+script.id
  scriptTitle.appendChild(anchor);
  scriptDiv.appendChild(scriptTitle);
229
  if (script.file) scriptTitle.appendChild(a(script.file, script.file));
230 231 232 233
  let summary = createNode('pre', 'script-details');
  summary.appendChild(text(script.summary));
  scriptDiv.appendChild(summary);
  result.appendChild(scriptDiv);
234 235 236 237 238 239
}

function renderScriptSizes(parseProcessor) {
  let scriptsDiv = $('#scripts');
  parseProcessor.scripts.forEach(
    script => {
240
      let scriptDiv = a(`#script${script.id}`, '', 'script-size');
241 242
      let scriptId = div('script-details');
      scriptId.classList.add('id');
243
      scriptId.innerText = `id=${script.id}`;
244
      scriptDiv.appendChild(scriptId);
245 246 247 248
      let scriptSize = div('script-details');
      scriptSize.innerText = BYTES(script.bytesTotal);
      scriptDiv.appendChild(scriptSize);
      let scriptUrl = div('script-details');
249 250 251 252 253 254
      if (script.isEval) {
        scriptUrl.innerText = "eval";
        scriptDiv.classList.add('eval');
      } else {
        scriptUrl.innerText = script.file.split("/").pop();
      }
255 256 257 258
      if (script.isStreamingCompiled ) {
        scriptDiv.classList.add('streaming');
      } else if (script.deserializationTimestamp > 0) {
        scriptDiv.classList.add('deserialized');
259 260
      }
      scriptDiv.appendChild(scriptUrl);
261
      scriptDiv.style.maxWidth = `${script.bytesTotal * 0.001}px`;
262 263
      scriptsDiv.appendChild(scriptDiv);
    });
264 265 266 267 268 269
}

const kMaxTime = 120 * kSecondsToMillis;
// Resolution of the graphs
const kTimeIncrement = 1;
const kSelectionTimespan = 2;
270
// TODO(cbruni): support compilation cache hit.
271 272 273 274 275 276 277 278 279
class Series {
  constructor(metricName, description, color, lineStyle, isArea=false) {
    this.metricName = metricName;
    this.description = description;
    this.color = color;
    this.lineStyle = lineStyle;
    this.isArea = isArea;
  }
}
280
const series = [
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
    new Series('firstParseEvent', 'Any Parse', '#4D4D4D', undefined, true),
    new Series('execution', '1st Exec', '#fff700',undefined, true),
    new Series('firstCompileEvent', 'Any Compile', '#5DA5DA', undefined, true),

    new Series('compile', 'Eager Compile', '#FAA43A'),
    new Series('lazyCompile', 'Lazy Compile','#FAA43A', 'dash'),

    new Series('parse', 'Parsing', '#F17CB0'),
    new Series('preparse', 'Preparse', '#B2912F'),
    new Series('resolution', 'Preparse with Var. Resolution', '#B276B2'),

    new Series('deserialization', 'Deserialization', '#DECF3F'),

    new Series('baseline', 'Baseline', '#606611', 'dash'),
    new Series('optimize', 'Optimize', '#F15854'),
296
];
297
const metricNames = series.map(each => each.metricName);
298 299 300 301
// Display cumulative values (useuful for bytes).
const kCumulative = true;
// Include durations in the graphs.
const kUseDuration = false;
302 303 304 305 306 307 308 309


function appendGraph(script, parentNode, start, end) {
  const timerLabel = 'graph script=' + script.id;
  // TODO(cbruni): add support for network events

  console.time(timerLabel);
  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 317 318 319
    data.addColumn('number', series.description);
    let options = {targetAxisIndex: 0, color: series.color};
    if (series.isArea) options.type = 'area';
    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 330 331
  });

  const maxTime = Math.min(kMaxTime, end);
  console.time('metrics');
  let metricValues =
332 333
    script.getAccumulatedTimeMetrics(metricNames , 0, maxTime, kTimeIncrement,
        kCumulative, kUseDuration);
334
  console.timeEnd('metrics');
335
  // Make sure that the series added to the graph matches the returned values.
336 337 338 339 340 341 342 343 344 345 346 347 348
  console.assert(metricValues[0].length == seriesOptions.length + 1);
  data.addRows(metricValues);

  let options = {
    explorer: {
      actions: ['dragToZoom', 'rightClickToReset'],
      maxZoomIn: 0.01
    },
    hAxis: {
      format: '#,###.##s'
    },
    vAxes: {
      0: {title: 'Bytes Touched', format: 'short'},
349
      1: {title: 'Duration', format: '#,###ms'}
350 351 352
    },
    height: 400,
    width: 1000,
353
    chartArea: {left: 70, top: 0, right: 160, height: "90%"},
354 355 356 357 358 359 360 361 362 363 364 365 366
    // The first series should be a area chart (total bytes touched),
    series: seriesOptions,
    // everthing else is a line.
    seriesType: 'line'
  };
  let graphNode = createNode('div', 'chart');
  let listNode = createNode('div', 'funktion-list');
  parentNode.appendChild(graphNode);
  parentNode.appendChild(listNode);
  let chart = new google.visualization.ComboChart(graphNode);
  google.visualization.events.addListener(chart, 'select',
      () => selectGraphPointHandler(chart, data, script, parentNode));
  chart.draw(data, options);
367
  // Add event listeners
368 369 370 371 372 373 374 375 376
  console.timeEnd(timerLabel);
}

function selectGraphPointHandler(chart, data, script, parentNode) {
  let selection = chart.getSelection();
  if (selection.length <= 0) return;
  // Display a list of funktions with events at the given time.
  let {row, column} = selection[0];
  if (row === null|| column === null) return;
377 378
  const kEntrySize = kUseDuration ? 2 : 1;
  let [metric, description] = series[((column-1)/ kEntrySize) | 0];
379 380
  let time = data.getValue(row, 0);
  let funktions = script.getFunktionsAtTime(
381
        time * kSecondsToMillis, kSelectionTimespan, metric);
382
  let oldList = parentNode.querySelector('.funktion-list');
383 384
  parentNode.replaceChild(
      createFunktionList(metric, description, time, funktions), oldList);
385 386
}

387
function createFunktionList(metric, description, time, funktions) {
388
  let container = createNode('div', 'funktion-list');
389
  container.appendChild(h3('Changes of "' + description + '" at ' +
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
        time + 's: ' + funktions.length));
  let listNode = createNode('ul');
  funktions.forEach(funktion => {
    let node = createNode('li', 'funktion');
    node.funktion = funktion;
    node.appendChild(text(funktion.toString(false) + " "));
    let script = funktion.script;
    if (script) {
      node.appendChild(a("#script" + script.id, "in script " + script.id));
    }
    listNode.appendChild(node);
  });
  container.appendChild(listNode);
  return container;
}
</script>
</head>

408
<body>
409 410 411 412 413 414 415 416 417
  <h1>BEHOLD, THIS IS PARSEROR!</h1>

  <h2>Usage</h2>
  Run your script with <code>--log-function-events</code> and upload <code>v8.log</code> on this page:<br/>
  <code>/path/to/d8 --log-function-events your_script.js</code>

  <h2>Data</h2>
  <form name="fileForm">
    <p>
418
      <input id="uploadInput" type="file" name="files" onchange="loadFile();" accept=".log"> trace entries: <span id="count">0</span>
419 420 421
    </p>
  </form>

422 423 424 425

  <h2>Scripts</h2>
  <div id="scripts"></div>

426 427 428 429 430
  <h2>Result</h2>
  <div id="result"></div>
</body>

</html>