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