• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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