v8/tools/parse-processor.html
Camillo Bruni 53c3e10482 [tools] Fix parse-processor
- Update parse processor to use new async log-reader functions
- Fix some typos
- Add more desciptions to the output
- Update bytes and time formatting to use common helper.mjs functions

Bug: v8:13146
Change-Id: Idf58a394aa493b7f50ad5282533c1b6d326117be
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3810233
Reviewed-by: Victor Gomes <victorgomes@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#82206}
2022-08-04 19:00:33 +00:00

431 lines
12 KiB
HTML

<!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 = async function(evt) {
const kTimerName = 'parse log file';
console.time(kTimerName);
let parseProcessor = new ParseProcessor();
await 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>