<!DOCTYPE html> <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> <meta charset="utf-8"> <title>V8 Parse Processor</title> <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 { } .script-size { display: inline-flex; background-color: #505050; border-radius: 3px; padding: 3px; margin: 2px; white-space: nowrap; overflow: hidden; text-decoration: none; color: white; transition: auto ease-in-out 0.8s; max-width: 500px; } .script-size:hover { max-width: 100000px !important; transition: auto ease-in-out 0.8s; } .script-size.eval { background-color: #ee6300fc; } .script-size.streaming { background-color: #008aff; } .script-size.deserialized { background-color: #1fad00fc; } .script-details { padding-right: 5px; margin-right: 4px; } /* 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; } </style> <script src="https://www.gstatic.com/charts/loader.js"></script> <script type="module"> import { ParseProcessor, kSecondsToMillis, BYTES, PERCENT } from "./parse-processor.mjs"; google.charts.load('current', {packages: ['corechart']}); function $(query) { return document.querySelector(query); } window.addEventListener('DOMContentLoaded', (event) => { $("#uploadInput").focus(); }); document.loadFile = function() { 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) { return new Promise(resolve => setTimeout(resolve, t)); } function renderParseResults(parseProcessor) { let result = $('#result'); // clear out all existing result pages; result.innerHTML = ''; const start = parseProcessor.firstEventTimestamp; const end = parseProcessor.lastEventTimestamp; 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)); renderScriptSizes(parseProcessor); // 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); }); }, {rootMargin: '400px'}); 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(); let anchor = a("", 'Script #' + script.id); anchor.name = "script"+script.id scriptTitle.appendChild(anchor); scriptDiv.appendChild(scriptTitle); if (script.file) scriptTitle.appendChild(a(script.file, script.file)); let summary = createNode('pre', 'script-details'); summary.appendChild(text(script.summary)); scriptDiv.appendChild(summary); result.appendChild(scriptDiv); } function renderScriptSizes(parseProcessor) { let scriptsDiv = $('#scripts'); parseProcessor.scripts.forEach( script => { let scriptDiv = a(`#script${script.id}`, '', 'script-size'); let scriptId = div('script-details'); scriptId.classList.add('id'); scriptId.innerText = `id=${script.id}`; scriptDiv.appendChild(scriptId); let scriptSize = div('script-details'); scriptSize.innerText = BYTES(script.bytesTotal); scriptDiv.appendChild(scriptSize); let scriptUrl = div('script-details'); if (script.isEval) { scriptUrl.innerText = "eval"; scriptDiv.classList.add('eval'); } else { scriptUrl.innerText = script.file.split("/").pop(); } if (script.isStreamingCompiled ) { scriptDiv.classList.add('streaming'); } else if (script.deserializationTimestamp > 0) { scriptDiv.classList.add('deserialized'); } scriptDiv.appendChild(scriptUrl); scriptDiv.style.maxWidth = `${script.bytesTotal * 0.001}px`; scriptsDiv.appendChild(scriptDiv); }); } const kMaxTime = 120 * kSecondsToMillis; // Resolution of the graphs const kTimeIncrement = 1; const kSelectionTimespan = 2; // TODO(cbruni): support compilation cache hit. class Series { constructor(metricName, description, color, lineStyle, isArea=false) { this.metricName = metricName; this.description = description; this.color = color; this.lineStyle = lineStyle; this.isArea = isArea; } } const series = [ 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'), ]; const metricNames = series.map(each => each.metricName); // Display cumulative values (useuful for bytes). const kCumulative = true; // Include durations in the graphs. const kUseDuration = false; 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(); data.addColumn('number', 'Duration'); // The series are interleave bytes processed, time spent and thus have two // different vAxes. let seriesOptions = []; series.forEach(series => { // Add the bytes column. 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]; seriesOptions.push(options) // Add the time column. if (kUseDuration) { data.addColumn('number', series.description + ' Duration'); seriesOptions.push( {targetAxisIndex: 1, color: series.color, lineDashStyle: [3, 2]}); } }); const maxTime = Math.min(kMaxTime, end); console.time('metrics'); let metricValues = script.getAccumulatedTimeMetrics(metricNames , 0, maxTime, kTimeIncrement, kCumulative, kUseDuration); console.timeEnd('metrics'); // Make sure that the series added to the graph matches the returned values. 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'}, 1: {title: 'Duration', format: '#,###ms'} }, height: 400, width: 1000, chartArea: {left: 70, top: 0, right: 160, height: "90%"}, // 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); // Add event listeners 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; const kEntrySize = kUseDuration ? 2 : 1; let [metric, description] = series[((column-1)/ kEntrySize) | 0]; let time = data.getValue(row, 0); let funktions = script.getFunktionsAtTime( time * kSecondsToMillis, kSelectionTimespan, metric); let oldList = parentNode.querySelector('.funktion-list'); parentNode.replaceChild( createFunktionList(metric, description, time, funktions), oldList); } function createFunktionList(metric, description, time, funktions) { let container = createNode('div', 'funktion-list'); container.appendChild(h3('Changes of "' + description + '" at ' + 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> <body> <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> <input id="uploadInput" type="file" name="files" onchange="loadFile();" accept=".log"> trace entries: <span id="count">0</span> </p> </form> <h2>Scripts</h2> <div id="scripts"></div> <h2>Result</h2> <div id="result"></div> </body> </html>