blob: da85494f14c1bcd48e8b4f27bbc16fd76ded1914 [file] [log] [blame]
<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">
<style>
body {
font-family: arial;
}
table {
display: table;
border-spacing: 0px;
}
tr {
border-spacing: 0px;
padding: 10px;
}
td,
th {
padding: 3px 10px 3px 5px;
}
.inline {
display: inline-block;
vertical-align: top;
}
h2,
h3 {
margin-bottom: 0px;
}
.hidden {
display: none;
}
.view {
display: table;
}
.column {
display: table-cell;
border-right: 1px black dotted;
min-width: 200px;
}
.column .header {
padding: 0 10px 0 10px
}
#column {
display: none;
}
.list {
width: 100%;
}
select {
width: 100%
}
.list tbody {
cursor: pointer;
}
.list tr:nth-child(even) {
background-color: #EFEFEF;
}
.list tr:nth-child(even).selected {
background-color: #DDD;
}
.list tr.child {
display: none;
}
.list tr.child.visible {
display: table-row;
}
.list .child .name {
padding-left: 20px;
}
.list .parent td {
border-top: 1px solid #AAA;
}
.list .total {
font-weight: bold
}
.list tr.parent {
background-color: #FFF;
}
.list tr.parent.selected {
background-color: #DDD;
}
tr.selected {
background-color: #DDD;
}
.list .position {
text-align: right;
display: none;
}
.list div.toggle {
cursor: pointer;
}
#column_0 .position {
display: table-cell;
}
#column_0 .name {
display: table-cell;
}
.list .name {
display: none;
white-space: nowrap;
}
.value {
text-align: right;
}
.selectedVersion {
font-weight: bold;
}
#baseline {
width: auto;
}
.compareSelector {
padding-bottom: 20px;
}
.pageDetailTable tbody {
cursor: pointer
}
.pageDetailTable tfoot td {
border-top: 1px grey solid;
}
#popover {
position: absolute;
transform: translateY(-50%) translateX(40px);
box-shadow: -2px 10px 44px -10px #000;
border-radius: 5px;
z-index: 1;
background-color: #FFF;
display: none;
white-space: nowrap;
}
#popover table {
position: relative;
z-index: 1;
text-align: right;
margin: 10px;
}
#popover td {
padding: 3px 0px 3px 5px;
white-space: nowrap;
}
.popoverArrow {
background-color: #FFF;
position: absolute;
width: 30px;
height: 30px;
transform: translateY(-50%)rotate(45deg);
top: 50%;
left: -10px;
z-index: 0;
}
#popover .name {
padding: 5px;
font-weight: bold;
text-align: center;
}
#popover table .compare {
display: none
}
#popover table.compare .compare {
display: table-cell;
}
#popover .compare .time,
#popover .compare .version {
padding-left: 10px;
}
</style>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
"use strict"
google.charts.load('current', {packages: ['corechart']});
// Did anybody say monkeypatching?
if (!NodeList.prototype.forEach) {
NodeList.prototype.forEach = function(func) {
for (var i = 0; i < this.length; i++) {
func(this[i]);
}
}
}
var versions;
var selectedPage;
var baselineVersion;
var selectedEntry;
function initialize() {
var original = $("column");
var view = document.createElement('div');
view.id = 'view';
var i = 0;
versions.forEach((version) => {
if (!version.enabled) return;
// add column
var column = original.cloneNode(true);
column.id = "column_" + i;
// Fill in all versions
var select = column.querySelector(".version");
select.id = "selectVersion_" + i;
// add all select options
versions.forEach((version) => {
if (!version.enabled) return;
var option = document.createElement("option");
option.textContent = version.name;
option.version = version;
select.appendChild(option);
});
// Fill in all page versions
select = column.querySelector(".pageVersion");
select.id = "select_" + i;
// add all pages
versions.forEach((version) => {
if (!version.enabled) return;
var optgroup = document.createElement("optgroup");
optgroup.label = version.name;
optgroup.version = version;
version.pages.forEach((page) => {
var option = document.createElement("option");
option.textContent = page.name;
option.page = page;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
});
view.appendChild(column);
i++;
});
var oldView = $('view');
oldView.parentNode.replaceChild(view, oldView);
var select = $('baseline');
removeAllChildren(select);
select.appendChild(document.createElement('option'));
versions.forEach((version) => {
var option = document.createElement("option");
option.textContent = version.name;
option.version = version;
select.appendChild(option);
});
var versionSelectorList = $('results').querySelector('.versionSelector ul');
removeAllChildren(versionSelectorList);
versions.forEach((version) => {
var li = document.createElement('li');
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = version.enabled;
checkbox.version = version;
checkbox.addEventListener('click', handleToggleVersionEnable);
li.appendChild(checkbox);
li.appendChild(document.createTextNode(version.name));
versionSelectorList.appendChild(li);
});
$('results').querySelectorAll('#results > .hidden').forEach((node) => {
toggleCssClass(node, 'hidden', false);
});
}
function showPage(firstPage) {
var changeSelectedEntry = selectedEntry !== undefined
&& selectedEntry.page === selectedPage;
selectedPage = firstPage;
selectedPage.sort();
showPageInColumn(firstPage, 0);
// Show the other versions of this page in the following columns.
var pageVersions = versions.pageVersions(firstPage.name);
var index = 1;
pageVersions.forEach((page) => {
if (page !== firstPage) {
showPageInColumn(page, index);
index++;
}
});
if (changeSelectedEntry) {
showEntryDetail(selectedPage.getEntry(selectedEntry));
} else {
showImpactList(selectedPage);
}
}
function showPageInColumn(page, columnIndex) {
page.sort();
var showDiff = (baselineVersion === undefined && columnIndex !== 0) ||
(baselineVersion !== undefined && page.version !== baselineVersion);
var diffStatus = (td, a, b) => {};
if (showDiff) {
if (baselineVersion !== undefined) {
diffStatus = (td, a, b) => {
if (a == 0) return;
td.style.color = a < 0 ? '#FF0000' : '#00BB00';
};
} else {
diffStatus = (td, a, b) => {
if (a == b) return;
var color;
var ratio = a / b;
if (ratio > 1) {
ratio = Math.min(Math.round((ratio - 1) * 255 * 10), 200);
color = '#' + ratio.toString(16) + "0000";
} else {
ratio = Math.min(Math.round((1 - ratio) * 255 * 10), 200);
color = '#00' + ratio.toString(16) + "00";
}
td.style.color = color;
}
}
}
var column = $('column_' + columnIndex);
var select = $('select_' + columnIndex);
// Find the matching option
selectOption(select, (i, option) => {
return option.page == page
});
var table = column.querySelector("table");
var oldTbody = table.querySelector('tbody');
var tbody = document.createElement('tbody');
var referencePage = selectedPage;
page.forEachSorted(selectedPage, (parentEntry, entry, referenceEntry) => {
// Filter out entries that do not exist in the first column for the default
// view.
if (baselineVersion === undefined && referenceEntry &&
referenceEntry.time == 0) {
return;
}
var tr = document.createElement('tr');
tbody.appendChild(tr);
tr.entry = entry;
tr.parentEntry = parentEntry;
tr.className = parentEntry === undefined ? 'parent' : 'child';
// Don't show entries that do not exist on the current page or if we
// compare against the current page
if (entry !== undefined && page.version !== baselineVersion) {
// If we show a diff, use the baselineVersion as the referenceEntry
if (baselineVersion !== undefined) {
var baselineEntry = baselineVersion.getEntry(entry);
if (baselineEntry !== undefined) referenceEntry = baselineEntry
}
if (!parentEntry) {
var node = td(tr, '<div class="toggle">►</div>', 'position');
node.firstChild.addEventListener('click', handleToggleGroup);
} else {
td(tr, entry.position == 0 ? '' : entry.position, 'position');
}
td(tr, entry.name, 'name ' + entry.cssClass());
diffStatus(
td(tr, ms(entry.time), 'value time'),
entry.time, referenceEntry.time);
diffStatus(
td(tr, percent(entry.timePercent), 'value time'),
entry.time, referenceEntry.time);
diffStatus(
td(tr, count(entry.count), 'value count'),
entry.count, referenceEntry.count);
} else if (baselineVersion !== undefined && referenceEntry
&& page.version !== baselineVersion) {
// Show comparison of entry that does not exist on the current page.
tr.entry = referenceEntry;
td(tr, '-', 'position');
td(tr, referenceEntry.name, 'name');
diffStatus(
td(tr, ms(referenceEntry.time), 'value time'),
referenceEntry.time, 0);
diffStatus(
td(tr, percent(referenceEntry.timePercent), 'value time'),
referenceEntry.timePercent, 0);
diffStatus(
td(tr, count(referenceEntry.count), 'value count'),
referenceEntry.count, 0);
} else {
// Display empty entry / baseline entry
var showBaselineEntry = entry !== undefined;
if (showBaselineEntry) {
if (!parentEntry) {
var node = td(tr, '<div class="toggle">►</div>', 'position');
node.firstChild.addEventListener('click', handleToggleGroup);
} else {
td(tr, entry.position == 0 ? '' : entry.position, 'position');
}
td(tr, entry.name, 'name');
td(tr, ms(entry.time, false), 'value time');
td(tr, percent(entry.timePercent, false), 'value time');
td(tr, count(entry.count, false), 'value count');
} else {
td(tr, '-', 'position');
td(tr, '-', 'name');
td(tr, '-', 'value time');
td(tr, '-', 'value time');
td(tr, '-', 'value count');
}
}
});
table.replaceChild(tbody, oldTbody);
var versionSelect = column.querySelector('select.version');
selectOption(versionSelect, (index, option) => {
return option.version == page.version
});
}
function selectEntry(entry, updateSelectedPage) {
if (updateSelectedPage) {
entry = selectedPage.version.getEntry(entry);
}
var rowIndex;
var needsPageSwitch = updateSelectedPage && entry.page != selectedPage;
// If clicked in the detail row change the first column to that page.
if (needsPageSwitch) showPage(entry.page);
var childNodes = $('column_0').querySelector('.list tbody').childNodes;
for (var i = 0; i < childNodes.length; i++) {
if (childNodes[i].entry.name == entry.name) {
rowIndex = i;
break;
}
}
var firstEntry = childNodes[rowIndex].entry;
if (rowIndex) {
if (firstEntry.parent) showGroup(firstEntry.parent);
}
// Deselect all
$('view').querySelectorAll('.list tbody tr').forEach((tr) => {
toggleCssClass(tr, 'selected', false);
});
// Select the entry row
$('view').querySelectorAll("tbody").forEach((body) => {
var row = body.childNodes[rowIndex];
if (!row) return;
toggleCssClass(row, 'selected', row.entry && row.entry.name ==
firstEntry.name);
});
if (updateSelectedPage) {
entry = selectedEntry.page.version.getEntry(entry);
}
selectedEntry = entry;
showEntryDetail(entry);
}
function showEntryDetail(entry) {
var table, tbody, entries;
table = $('detailView').querySelector('.versionDetailTable');
tbody = document.createElement('tbody');
if (entry !== undefined) {
$('detailView').querySelector('.versionDetail h3 span').innerHTML =
entry.name;
entries = versions.pageVersions(entry.page.name).map(
(page) => {
return page.get(entry.name)
});
entries.sort((a, b) => {
return a.time - b.time
});
entries.forEach((pageEntry) => {
if (pageEntry === undefined) return;
var tr = document.createElement('tr');
if (pageEntry == entry) tr.className += 'selected';
tr.entry = pageEntry;
td(tr, pageEntry.page.version.name, 'version');
td(tr, pageEntry.position, 'value position');
td(tr, ms(pageEntry.time), 'value time');
td(tr, percent(pageEntry.timePercent), 'value time');
td(tr, count(pageEntry.count), 'value count');
tbody.appendChild(tr);
});
}
table.replaceChild(tbody, table.querySelector('tbody'));
table = $('detailView').querySelector('.pageDetailTable');
tbody = document.createElement('tbody');
if (entry !== undefined) {
var version = entry.page.version;
$('detailView').querySelector('.pageDetail h3 span').innerHTML =
version.name;
entries = version.pages.map(
(page) => {
return page.get(entry.name)
});
entries.sort((a, b) => {
var cmp = b.timePercent - a.timePercent;
if (cmp.toFixed(1) == 0) return b.time - a.time;
return cmp
});
entries.forEach((pageEntry) => {
if (pageEntry === undefined) return;
var tr = document.createElement('tr');
if (pageEntry === entry) tr.className += 'selected';
tr.entry = pageEntry;
td(tr, pageEntry.page.name, 'name');
td(tr, pageEntry.position, 'value position');
td(tr, ms(pageEntry.time), 'value time');
td(tr, percent(pageEntry.timePercent), 'value time');
td(tr, count(pageEntry.count), 'value count');
tbody.appendChild(tr);
});
// show the total for all pages
var tds = table.querySelectorAll('tfoot td');
tds[2].innerHTML = ms(entry.getTimeImpact());
// Only show the percentage total if we are in diff mode:
tds[3].innerHTML = percent(entry.getTimePercentImpact());
tds[4].innerHTML = count(entry.getCountImpact());
}
table.replaceChild(tbody, table.querySelector('tbody'));
showImpactList(entry.page);
showPageGraphs(entry.page);
}
function showImpactList(page) {
var impactView = $('detailView').querySelector('.impactView');
impactView.querySelector('h3 span').innerHTML = page.version.name;
var table = impactView.querySelector('table');
var tbody = document.createElement('tbody');
var version = page.version;
var entries = version.allEntries();
if (selectedEntry !== undefined && selectedEntry.isGroup) {
impactView.querySelector('h3 span').innerHTML += " " + selectedEntry.name;
entries = entries.filter((entry) => {
return entry.name == selectedEntry.name ||
(entry.parent && entry.parent.name == selectedEntry.name)
});
}
var isCompareView = baselineVersion !== undefined;
entries = entries.filter((entry) => {
if (isCompareView) {
var impact = entry.getTimeImpact();
return impact < -1 || 1 < impact
}
return entry.getTimePercentImpact() > 0.1;
});
entries.sort((a, b) => {
var cmp = b.getTimePercentImpact() - a.getTimePercentImpact();
if (isCompareView || cmp.toFixed(1) == 0) {
return b.getTimeImpact() - a.getTimeImpact();
}
return cmp
});
entries.forEach((entry) => {
var tr = document.createElement('tr');
tr.entry = entry;
td(tr, entry.name, 'name');
td(tr, ms(entry.getTimeImpact()), 'value time');
var percentImpact = entry.getTimePercentImpact();
td(tr, percentImpact > 1000 ? '-' : percent(percentImpact), 'value time');
var topPages = entry.getPagesByPercentImpact().slice(0, 3)
.map((each) => {
return each.name + ' (' + percent(each.getEntry(entry).timePercent) +
')'
});
td(tr, topPages.join(', '), 'name');
tbody.appendChild(tr);
});
table.replaceChild(tbody, table.querySelector('tbody'));
}
var selectedGroup;
function showPageGraphs(page) {
var groups = page.groups.filter(each => each.name != page.total.name);
if (selectedGroup == undefined) {
selectedGroup = groups[0];
} else {
groups = groups.filter(each => each.name != selectedGroup.name);
groups.unshift(selectedGroup);
}
var dataTable = new google.visualization.DataTable();
dataTable.addColumn('string', 'Name');
groups.forEach(group => {
var column = dataTable.addColumn('number', group.name);
dataTable.setColumnProperty(column, 'group', group);
});
// Calculate the average row
var row = ['Average'];
groups.forEach((group) => {
row.push(group.getTimeImpact());
});
dataTable.addRow(row);
// Sort the pages by the selected group.
var pages = page.version.pages.slice();
pages.sort((a, b) => {
return b.getEntry(selectedGroup).timePercent - a.getEntry(selectedGroup).timePercent;
});
// Calculate the entries for the pages
pages.forEach((page) => {
row = [page.name];
groups.forEach((group) => {
row.push(page.getEntry(group).time);
});
dataTable.addRow(row);
});
var height = 1000/27*page.version.pages.length;
var options = {
title: 'Page Comparison for Version ' + page.version.name,
isStacked: 'percent',
height: height ,
hAxis: {
title: '% Time',
minValue: 0,
},
vAxis: {
}
};
var chart = new google.visualization.BarChart($('pageGraphs'));
chart.draw(dataTable, options);
google.visualization.events.addListener(chart, 'select', selectHandler);
function selectHandler() {
var column = chart.getSelection()[0].column;
if (column === undefined) return;
selectedGroup = dataTable.getColumnProperty(column, 'group');
showPageGraphs(selectedEntry.page);
}
}
function showGroup(entry) {
toggleGroup(entry, true);
}
function toggleGroup(group, show) {
$('view').querySelectorAll(".child").forEach((tr) => {
var entry = tr.parentEntry;
if (!entry) return;
if (entry.name !== group.name) return;
toggleCssClass(tr, 'visible', show);
});
}
function showPopover(entry) {
var popover = $('popover');
popover.querySelector('td.name').innerHTML = entry.name;
popover.querySelector('td.page').innerHTML = entry.page.name;
setPopoverDetail(popover, entry, '');
popover.querySelector('table').className = "";
if (baselineVersion !== undefined) {
entry = baselineVersion.getEntry(entry);
setPopoverDetail(popover, entry, '.compare');
popover.querySelector('table').className = "compare";
}
}
function setPopoverDetail(popover, entry, prefix) {
var node = (name) => popover.querySelector(prefix + name);
if (entry == undefined) {
node('.version').innerHTML = baselineVersion.name;
node('.time').innerHTML = '-';
node('.timeVariance').innerHTML = '-';
node('.percent').innerHTML = '-';
node('.percentVariance').innerHTML = '-';
node('.count').innerHTML = '-';
node('.countVariance').innerHTML = '-';
node('.timeImpact').innerHTML = '-';
node('.timePercentImpact').innerHTML = '-';
} else {
node('.version').innerHTML = entry.page.version.name;
node('.time').innerHTML = ms(entry._time, false);
node('.timeVariance').innerHTML
= percent(entry.timeVariancePercent, false);
node('.percent').innerHTML = percent(entry.timePercent, false);
node('.percentVariance').innerHTML
= percent(entry.timePercentVariancePercent, false);
node('.count').innerHTML = count(entry._count, false);
node('.countVariance').innerHTML
= percent(entry.timeVariancePercent, false);
node('.timeImpact').innerHTML
= ms(entry.getTimeImpact(false), false);
node('.timePercentImpact').innerHTML
= percent(entry.getTimeImpactVariancePercent(false), false);
}
}
// ===========================================================================
// Helpers
function $(id) {
return document.getElementById(id)
}
function removeAllChildren(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
function selectOption(select, match) {
var options = select.options;
for (var i = 0; i < options.length; i++) {
if (match(i, options[i])) {
select.selectedIndex = i;
return;
}
}
}
function td(tr, content, className) {
var td = document.createElement("td");
td.innerHTML = content;
td.className = className
tr.appendChild(td);
return td
}
function nodeIndex(node) {
var children = node.parentNode.childNodes,
i = 0;
for (; i < children.length; i++) {
if (children[i] == node) {
return i;
}
}
return -1;
}
function toggleCssClass(node, cssClass, toggleState) {
var index = -1;
var classes;
if (node.className != undefined) {
classes = node.className.split(' ');
index = classes.indexOf(cssClass);
}
if (index == -1) {
if (toggleState === false) return;
node.className += ' ' + cssClass;
return;
}
if (toggleState === true) return;
classes.splice(index, 1);
node.className = classes.join(' ');
}
function diffSign(value, digits, unit, showDiff) {
if (showDiff === false || baselineVersion == undefined) {
return value.toFixed(digits) + unit;
}
return (value >= 0 ? '+' : '') + value.toFixed(digits) + unit + 'Δ';
}
function ms(value, showDiff) {
return diffSign(value, 1, 'ms', showDiff);
}
function count(value, showDiff) {
return diffSign(value, 0, '#', showDiff);
}
function percent(value, showDiff) {
return diffSign(value, 1, '%', showDiff);
}
// =========================================================================
// EventHandlers
function handleBodyLoad() {
$('uploadInput').focus();
}
function handleLoadFile() {
var files = document.getElementById("uploadInput").files;
var file = files[0];
var reader = new FileReader();
reader.onload = function(evt) {
versions = Versions.fromJSON(JSON.parse(this.result));
initialize()
showPage(versions.versions[0].pages[0]);
}
reader.readAsText(file);
}
function handleToggleGroup(event) {
var group = event.target.parentNode.parentNode.entry;
toggleGroup(selectedPage.get(group.name));
}
function handleSelectPage(select, event) {
var option = select.options[select.selectedIndex];
if (select.id == "select_0") {
showPage(option.page);
} else {
var columnIndex = select.id.split('_')[1];
showPageInColumn(option.page, columnIndex);
}
}
function handleSelectVersion(select, event) {
var option = select.options[select.selectedIndex];
var version = option.version;
if (select.id == "selectVersion_0") {
var page = version.get(selectedPage.name);
showPage(page);
} else {
var columnIndex = select.id.split('_')[1];
var pageSelect = $('select_' + columnIndex);
var page = pageSelect.options[select.selectedIndex].page;
page = version.get(page.name);
showPageInColumn(page, columnIndex);
}
}
function handleSelectDetailRow(table, event) {
if (event.target.tagName != 'TD') return;
var tr = event.target.parentNode;
if (tr.tagName != 'TR') return;
if (tr.entry === undefined) return;
selectEntry(tr.entry, true);
}
function handleSelectRow(table, event, fromDetail) {
if (event.target.tagName != 'TD') return;
var tr = event.target.parentNode;
if (tr.tagName != 'TR') return;
if (tr.entry === undefined) return;
selectEntry(tr.entry, false);
}
function handleSelectBaseline(select, event) {
var option = select.options[select.selectedIndex];
baselineVersion = option.version
showPage(selectedPage);
if (selectedEntry === undefined) return;
selectEntry(selectedEntry, true);
}
function handleUpdatePopover(event) {
var popover = $('popover');
popover.style.left = event.pageX + 'px';
popover.style.top = event.pageY + 'px';
popover.style.display = event.shiftKey ? 'block' : 'none';
var target = event.target;
while (target.entry === undefined) {
target = target.parentNode;
if (!target) return;
}
showPopover(target.entry);
}
function handleToggleVersionEnable(event) {
var version = this.version;
if (version === undefined) return;
version.enabled = this.checked;
initialize();
var page = selectedPage;
if (page === undefined || !page.version.enabled) {
page = versions.getEnabledPage(page.name);
}
showPage(page);
}
// ===========================================================================
class Versions {
constructor() {
this.versions = [];
}
add(version) {
this.versions.push(version)
}
pageVersions(name) {
var result = [];
this.versions.forEach((version) => {
if (!version.enabled) return;
var page = version.get(name);
if (page !== undefined) result.push(page);
});
return result;
}
get length() {
return this.versions.length
}
get(index) {
return this.versions[index]
};
forEach(f) {
this.versions.forEach(f);
}
sort() {
this.versions.sort((a, b) => {
if (a.name > b.name) return 1;
if (a.name < b.name) return -1;
return 0
})
}
getEnabledPage(name) {
for (var i = 0; i < this.versions.length; i++) {
var version = this.versions[i];
if (!version.enabled) continue;
var page = version.get(name);
if (page !== undefined) return page;
}
}
}
Versions.fromJSON = function(json) {
var versions = new Versions();
for (var version in json) {
versions.add(Version.fromJSON(version, json[version]));
}
versions.sort();
return versions;
}
class Version {
constructor(name) {
this.name = name;
this.enabled = true;
this.pages = [];
}
add(page) {
this.pages.push(page);
}
indexOf(name) {
for (var i = 0; i < this.pages.length; i++) {
if (this.pages[i].name == name) return i;
}
return -1;
}
get(name) {
var index = this.indexOf(name);
if (0 <= index) return this.pages[index];
return undefined
}
get length() {
return this.versions.length
}
getEntry(entry) {
if (entry === undefined) return undefined;
var page = this.get(entry.page.name);
if (page === undefined) return undefined;
return page.get(entry.name);
}
forEachEntry(fun) {
this.pages.forEach((page) => {
page.forEach(fun);
});
}
allEntries() {
var map = new Map();
this.forEachEntry((group, entry) => {
if (!map.has(entry.name)) map.set(entry.name, entry);
});
return Array.from(map.values());
}
getTotalValue(name, property) {
if (name === undefined) name = this.pages[0].total.name;
var sum = 0;
this.pages.forEach((page) => {
var entry = page.get(name);
if (entry !== undefined) sum += entry[property];
});
return sum;
}
getTotalTime(name, showDiff) {
return this.getTotalValue(name, showDiff === false ? '_time' : 'time');
}
getTotalTimePercent(name, showDiff) {
if (baselineVersion === undefined) {
// Return the overall average percent of the given entry name.
return this.getTotalValue(name, 'time') /
this.getTotalTime('Group-Total') * 100;
}
// Otherwise return the difference to the sum of the baseline version.
var baselineValue = baselineVersion.getTotalTime(name, false);
return this.getTotalValue(name, '_time') / baselineValue * 100;
}
getTotalTimeVariance(name, showDiff) {
// Calculate the overall error for a given entry name
var sum = 0;
this.pages.forEach((page) => {
var entry = page.get(name);
if (entry === undefined) return;
sum += entry.timeVariance * entry.timeVariance;
});
return Math.sqrt(sum);
}
getTotalTimeVariancePercent(name, showDiff) {
return this.getTotalTimeVariance(name, showDiff) /
this.getTotalTime(name, showDiff) * 100;
}
getTotalCount(name, showDiff) {
return this.getTotalValue(name, showDiff === false ? '_count' : 'count');
}
getPagesByPercentImpact(name) {
var sortedPages =
this.pages.filter((each) => {
return each.get(name) !== undefined
});
sortedPages.sort((a, b) => {
return b.get(name).timePercent - a.get(name).timePercent;
});
return sortedPages;
}
sort() {
this.pages.sort((a, b) => {
if (a.name > b.name) return 1;
if (a.name < b.name) return -1;
return 0
})
}
}
Version.fromJSON = function(name, data) {
var version = new Version(name);
for (var page in data) {
version.add(Page.fromJSON(version, page, data[page]));
}
version.sort();
return version;
}
class Page {
constructor(version, name) {
this.name = name;
this.total = new GroupedEntry('Total', /.*Total.*/);
this.unclassified = new UnclassifiedEntry(this)
this.groups = [
this.total,
new GroupedEntry('IC', /.*IC.*/),
new GroupedEntry('Optimize',
/StackGuard|.*Optimize.*|.*Deoptimize.*|Recompile.*/),
new GroupedEntry('Compile', /.*Compile.*|Parse.*/),
new GroupedEntry('Callback', /.*Callback$/),
new GroupedEntry('API', /.*API.*/),
new GroupedEntry('GC', /GC|AllocateInTargetSpace/),
new GroupedEntry('JavaScript', /JS_Execution/),
this.unclassified
];
this.entryDict = new Map();
this.groups.forEach((entry) => {
entry.page = this;
this.entryDict.set(entry.name, entry);
});
this.version = version;
}
add(entry) {
entry.page = this;
this.entryDict.set(entry.name, entry);
var added = false;
this.groups.forEach((group) => {
if (!added) added = group.add(entry);
});
if (added) return;
this.unclassified.push(entry);
}
get(name) {
return this.entryDict.get(name)
}
getEntry(entry) {
if (entry === undefined) return undefined;
return this.get(entry.name);
}
get length() {
return this.versions.length
}
forEachSorted(referencePage, func) {
// Iterate over all the entries in the order they appear on the reference page.
referencePage.forEach((parent, referenceEntry) => {
var entry;
if (parent) parent = this.entryDict.get(parent.name);
if (referenceEntry) entry = this.entryDict.get(referenceEntry.name);
func(parent, entry, referenceEntry);
});
}
forEach(fun) {
this.forEachGroup((group) => {
fun(undefined, group);
group.forEach((entry) => {
fun(group, entry)
});
});
}
forEachGroup(fun) {
this.groups.forEach(fun)
}
sort() {
this.groups.sort((a, b) => {
return b.time - a.time;
});
this.groups.forEach((group) => {
group.sort()
});
}
}
Page.fromJSON = function(version, name, data) {
if (name.indexOf('www.') == 0) {
name = name.substring(4);
}
var page = new Page(version, name);
for (var i = 0; i < data.length; i++) {
page.add(Entry.fromJSON(i, data[data.length - i - 1]));
}
page.sort();
return page
}
class Entry {
constructor(position, name, time, timeVariance, timeVariancePercent,
count,
countVariance, countVariancePercent) {
this.position = position;
this.name = name;
this._time = time;
this._timeVariance = timeVariance;
this._timeVariancePercent = timeVariancePercent;
this._count = count;
this.countVariance = countVariance;
this.countVariancePercent = countVariancePercent;
this.page = undefined;
this.parent = undefined;
}
getCompareWithBaseline(value, property) {
if (baselineVersion == undefined) return value;
var baselineEntry = baselineVersion.getEntry(this);
if (!baselineEntry) return value;
if (baselineVersion === this.page.version) return value;
return value - baselineEntry[property];
}
cssClass() {
return ''
}
get time() {
return this.getCompareWithBaseline(this._time, '_time');
}
get count() {
return this.getCompareWithBaseline(this._count, '_count');
}
get timePercent() {
var value = this._time / this.page.total._time * 100;
if (baselineVersion == undefined) return value;
var baselineEntry = baselineVersion.getEntry(this);
if (!baselineEntry) return value;
if (baselineVersion === this.page.version) return value;
return (this._time - baselineEntry._time) / this.page.total._time *
100;
}
get timePercentVariancePercent() {
// Get the absolute values for the percentages
return this.timeVariance / this.page.total._time * 100;
}
getTimeImpact(showDiff) {
return this.page.version.getTotalTime(this.name, showDiff);
}
getTimeImpactVariancePercent(showDiff) {
return this.page.version.getTotalTimeVariancePercent(this.name, showDiff);
}
getTimePercentImpact(showDiff) {
return this.page.version.getTotalTimePercent(this.name, showDiff);
}
getCountImpact(showDiff) {
return this.page.version.getTotalCount(this.name, showDiff);
}
getPagesByPercentImpact() {
return this.page.version.getPagesByPercentImpact(this.name);
}
get isGroup() {
return false
}
get timeVariance() {
return this._timeVariance
}
get timeVariancePercent() {
return this._timeVariancePercent
}
}
Entry.fromJSON = function(position, data) {
return new Entry(position, ...data);
}
class GroupedEntry extends Entry {
constructor(name, regexp) {
super(0, 'Group-' + name, 0, 0, 0, 0, 0, 0);
this.regexp = regexp;
this.entries = [];
}
add(entry) {
if (!entry.name.match(this.regexp)) return false;
this._time += entry.time;
this._count += entry.count;
// TODO: sum up variance
this.entries.push(entry);
entry.parent = this;
return true;
}
forEach(fun) {
if (baselineVersion === undefined) {
this.entries.forEach(fun);
return;
}
// If we have a baslineVersion to compare against show also all entries from the
// other group.
var tmpEntries = baselineVersion.getEntry(this)
.entries.filter((entry) => {
return this.page.get(entry.name) == undefined
});
// The compared entries are sorted by absolute impact.
tmpEntries = tmpEntries.map((entry) => {
var tmpEntry = new Entry(0, entry.name, 0, 0, 0, 0, 0, 0);
tmpEntry.page = this.page;
return tmpEntry;
});
tmpEntries = tmpEntries.concat(this.entries);
tmpEntries.sort((a, b) => {
return a.time - b.time
});
tmpEntries.forEach(fun);
}
sort() {
this.entries.sort((a, b) => {
return b.time - a.time;
});
}
cssClass() {
if (this.page.total == this) return 'total';
return '';
}
get isGroup() {
return true
}
getVarianceForProperty(property) {
var sum = 0;
this.entries.forEach((entry) => {
sum += entry[property + 'Variance'] * entry[property +
'Variance'];
});
return Math.sqrt(sum);
}
get timeVariancePercent() {
if (this._time == 0) return 0;
return this.getVarianceForProperty('time') / this._time * 100
}
get timeVariance() {
return this.getVarianceForProperty('time')
}
}
class UnclassifiedEntry extends GroupedEntry {
constructor(page) {
super('Runtime');
this.page = page;
this._time = undefined;
this._count = undefined;
}
add(entry) {
this.entries.push(entry);
entry.parent = this;
return true;
}
forEachPageGroup(fun) {
this.page.forEachGroup((group) => {
if (group == this) return;
if (group == this.page.total) return;
fun(group);
});
}
get time() {
if (this._time === undefined) {
this._time = this.page.total._time;
this.forEachPageGroup((group) => {
this._time -= group._time;
});
}
return this.getCompareWithBaseline(this._time, '_time');
}
get count() {
if (this._count === undefined) {
this._count = this.page.total._count;
this.forEachPageGroup((group) => {
this._count -= group._count;
});
}
return this.getCompareWithBaseline(this._count, '_count');
}
}
</script>
</head>
<body onmousemove="handleUpdatePopover(event)" onload="handleBodyLoad()">
<h1>Runtime Stats Komparator</h1>
<div id="results">
<div class="inline">
<h2>Data</h2>
<form name="fileForm">
<p>
<input id="uploadInput" type="file" name="files" onchange="handleLoadFile();" accept=".json">
</p>
</form>
</div>
<div class="inline hidden">
<h2>Result</h2>
<div class="compareSelector inline">
Compare against:&nbsp;<select id="baseline" onchange="handleSelectBaseline(this, event)"></select><br/>
<span style="color: #060">Green</span> the selected version above performs
better on this measurement.
</div>
<div class="versionSelector inline">
Select Versions:
<ul></ul>
</div>
</div>
<div id="view">
</div>
<div id="detailView" class="hidden">
<h2></h2>
<div class="versionDetail inline">
<h3>Version Comparison for <span></span></h3>
<table class="versionDetailTable" onclick="handleSelectDetailRow(this, event);">
<thead>
<tr>
<th class="version">Version&nbsp;</th>
<th class="position">Pos.&nbsp;</th>
<th class="value time">Time▴&nbsp;</th>
<th class="value time">Percent&nbsp;</th>
<th class="value count">Count&nbsp;</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="pageDetail inline">
<h3>Page Comparison for <span></span></h3>
<table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
<thead>
<tr>
<th class="page">Page&nbsp;</th>
<th class="position">Pos.&nbsp;</th>
<th class="value time">Time&nbsp;</th>
<th class="value time">Percent▾&nbsp;</th>
<th class="value count">Count&nbsp;</th>
</tr>
</thead>
<tfoot>
<tr>
<td class="page">Total:</td>
<td class="position"></td>
<td class="value time"></td>
<td class="value time"></td>
<td class="value count"></td>
</tr>
</tfoot>
<tbody></tbody>
</table>
</div>
<div class="impactView inline">
<h3>Impact list for <span></span></h3>
<table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
<thead>
<tr>
<th class="page">Name&nbsp;</th>
<th class="value time">Time&nbsp;</th>
<th class="value time">Percent▾&nbsp;</th>
<th class="">Top Pages</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="pageGraphs" class="hidden">
</div>
<div id="column" class="column">
<div class="header">
<select class="version" onchange="handleSelectVersion(this, event);"></select>
<select class="pageVersion" onchange="handleSelectPage(this, event);"></select>
</div>
<table class="list" onclick="handleSelectRow(this, event);">
<thead>
<tr>
<th class="position">Pos.&nbsp;</th>
<th class="name">Name&nbsp;</th>
<th class="value time">Time&nbsp;</th>
<th class="value time">Percent&nbsp;</th>
<th class="value count">Count&nbsp;</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="inline">
<h2>Usage</h2>
<ol>
<li>Install scipy, e.g. <code>sudo aptitude install python-scipy</code>
<li>Build chrome with the <a href="https://codereview.chromium.org/1923893002">extended runtime callstats</a>.</li>
<li>Run <code>callstats.py</code> with a web-page-replay archive:
<pre>$V8_DIR/tools/callstats.py run \
--replay-bin=$CHROME_SRC/third_party/webpagereplay/replay.py \
--replay-wpr=$INPUT_DIR/top25.wpr \
--js-flags="" \
--with-chrome=$CHROME_SRC/out/Release/chrome \
--sites-file=$INPUT_DIR/top25.json</pre>
</li>
<li>Move results file to a subdirectory: <code>mkdir $VERSION_DIR; mv *.txt $VERSION_DIR</code></li>
<li>Repeat from step 1 with a different configuration (e.g. <code>--js-flags="--nolazy"</code>).</li>
<li>Create the final results file: <code>./callstats.py json $VERSION_DIR1 $VERSION_DIR2 > result.json</code></li>
<li>Use <code>results.json</code> on this site.</code>
</ol>
</div>
<div id="popover">
<div class="popoverArrow"></div>
<table>
<tr>
<td class="name" colspan="6"></td>
</tr>
<tr>
<td>Page:</td>
<td class="page name" colspan="6"></td>
</tr>
<tr>
<td>Version:</td>
<td class="version name" colspan="3"></td>
<td class="compare version name" colspan="3"></td>
</tr>
<tr>
<td>Time:</td>
<td class="time"></td><td>±</td><td class="timeVariance"></td>
<td class="compare time"></td><td class="compare"> ± </td><td class="compare timeVariance"></td>
</tr>
<tr>
<td>Percent:</td>
<td class="percent"></td><td>±</td><td class="percentVariance"></td>
<td class="compare percent"></td><td class="compare"> ± </td><td class="compare percentVariance"></td>
</tr>
<tr>
<td>Count:</td>
<td class="count"></td><td>±</td><td class="countVariance"></td>
<td class="compare count"></td><td class="compare"> ± </td><td class="compare countVariance"></td>
</tr>
<tr>
<td>Overall Impact:</td>
<td class="timeImpact"></td><td>±</td><td class="timePercentImpact"></td>
<td class="compare timeImpact"></td><td class="compare"> ± </td><td class="compare timePercentImpact"></td>
</tr>
</table>
</div>
</body>
</html>