d2c565215b
Previously the runtime stats tool would only show entries which were in the baseline version. This change adds any entries which exist in any version to the comparison table, which would otherwise not appear anywhere unless that version was selected as the baseline version. NOTRY=true Review-Url: https://codereview.chromium.org/2683863004 Cr-Commit-Position: refs/heads/master@{#43045}
1991 lines
63 KiB
HTML
1991 lines
63 KiB
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">
|
|
<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;
|
|
}
|
|
|
|
.codeSearch {
|
|
display: block-inline;
|
|
float: right;
|
|
border-radius: 5px;
|
|
background-color: #EEE;
|
|
width: 1em;
|
|
text-align: center;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
.graph,
|
|
.graph .content {
|
|
width: 100%;
|
|
}
|
|
|
|
.diff .hideDiff {
|
|
display: none;
|
|
}
|
|
.noDiff .hideNoDiff {
|
|
display: none;
|
|
}
|
|
</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 pages;
|
|
var selectedPage;
|
|
var baselineVersion;
|
|
var selectedEntry;
|
|
|
|
// Marker to programatically replace the defaultData.
|
|
var defaultData = /*default-data-start*/undefined/*default-data-end*/;
|
|
|
|
function initialize() {
|
|
// Initialize the stats table and toggle lists.
|
|
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.forEachPage((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);
|
|
});
|
|
initializeToggleList(versions.versions, $('versionSelector'));
|
|
initializeToggleList(pages.values(), $('pageSelector'));
|
|
initializeToggleList(Group.groups.values(), $('groupSelector'));
|
|
initializeToggleContentVisibility();
|
|
}
|
|
|
|
function initializeToggleList(items, node) {
|
|
var list = node.querySelector('ul');
|
|
removeAllChildren(list);
|
|
items = Array.from(items);
|
|
items.sort(NameComparator);
|
|
items.forEach((item) => {
|
|
var li = document.createElement('li');
|
|
var checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.checked = item.enabled;
|
|
checkbox.item = item;
|
|
checkbox.addEventListener('click', handleToggleVersionOrPageEnable);
|
|
li.appendChild(checkbox);
|
|
li.appendChild(document.createTextNode(item.name));
|
|
list.appendChild(li);
|
|
});
|
|
$('results').querySelectorAll('#results > .hidden').forEach((node) => {
|
|
toggleCssClass(node, 'hidden', false);
|
|
})
|
|
}
|
|
|
|
function initializeToggleContentVisibility() {
|
|
var nodes = document.querySelectorAll('.toggleContentVisibility');
|
|
nodes.forEach((node) => {
|
|
var content = node.querySelector('.content');
|
|
var header = node.querySelector('h1,h2,h3');
|
|
if (content === undefined || header === undefined) return;
|
|
if (header.querySelector('input') != undefined) return;
|
|
var checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.checked = content.className.indexOf('hidden') == -1;
|
|
checkbox.contentNode = content;
|
|
checkbox.addEventListener('click', handleToggleContentVisibility);
|
|
header.insertBefore(checkbox, header.childNodes[0]);
|
|
});
|
|
}
|
|
|
|
window.addEventListener('popstate', (event) => {
|
|
popHistoryState(event.state);
|
|
});
|
|
|
|
function popHistoryState(state) {
|
|
if (!state.version) return false;
|
|
if (!versions) return false;
|
|
var version = versions.getByName(state.version);
|
|
if (!version) return false;
|
|
var page = version.get(state.page);
|
|
if (!page) return false;
|
|
if (!state.entry) {
|
|
showPage(page);
|
|
} else {
|
|
var entry = page.get(state.entry);
|
|
if (!entry) {
|
|
showPage(page);
|
|
} else {
|
|
showEntry(entry);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function pushHistoryState() {
|
|
var selection = selectedEntry ? selectedEntry : selectedPage;
|
|
if (!selection) return;
|
|
var state = selection.urlParams();
|
|
// Don't push a history state if it didn't change.
|
|
if (JSON.stringify(window.history.state) === JSON.stringify(state)) return;
|
|
var params = "?";
|
|
for (var pairs of Object.entries(state)) {
|
|
params += encodeURIComponent(pairs[0]) + "="
|
|
+ encodeURIComponent(pairs[1]) + "&";
|
|
}
|
|
window.history.pushState(state, selection.toString(), params);
|
|
}
|
|
|
|
function showPage(firstPage) {
|
|
var changeSelectedEntry = selectedEntry !== undefined
|
|
&& selectedEntry.page === selectedPage;
|
|
pushHistoryState();
|
|
selectedPage = firstPage;
|
|
selectedPage.sort();
|
|
showPageInColumn(firstPage, 0);
|
|
// Show the other versions of this page in the following columns.
|
|
var pageVersions = versions.getPageVersions(firstPage);
|
|
var index = 1;
|
|
pageVersions.forEach((page) => {
|
|
if (page !== firstPage) {
|
|
showPageInColumn(page, index);
|
|
index++;
|
|
}
|
|
});
|
|
if (changeSelectedEntry) {
|
|
showEntryDetail(selectedPage.getEntry(selectedEntry));
|
|
}
|
|
showImpactList(selectedPage);
|
|
pushHistoryState();
|
|
}
|
|
|
|
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) => {
|
|
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');
|
|
}
|
|
addCodeSearchButton(entry,
|
|
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 = new Entry(0, referenceEntry.name);
|
|
tr.entry.page = page;
|
|
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, referenceEntry.name, '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 showEntry(entry) {
|
|
selectedEntry = entry;
|
|
selectEntry(entry, true);
|
|
}
|
|
|
|
function selectEntry(entry, updateSelectedPage) {
|
|
if (updateSelectedPage) {
|
|
entry = selectedPage.version.getEntry(entry);
|
|
}
|
|
var rowIndex = 0;
|
|
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 !== undefined &&
|
|
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) {
|
|
showVersionDetails(entry);
|
|
showPageDetails(entry);
|
|
showImpactList(entry.page);
|
|
showGraphs(entry.page);
|
|
pushHistoryState();
|
|
}
|
|
|
|
function showVersionDetails(entry) {
|
|
var table, tbody, entries;
|
|
table = $('detailView').querySelector('.versionDetailTable');
|
|
tbody = document.createElement('tbody');
|
|
if (entry !== undefined) {
|
|
$('detailView').querySelector('.versionDetail h3 span').textContent =
|
|
entry.name + ' in ' + entry.page.name;
|
|
entries = versions.getPageVersions(entry.page).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;
|
|
var isBaselineEntry = pageEntry.page.version == baselineVersion;
|
|
td(tr, pageEntry.page.version.name, 'version');
|
|
td(tr, ms(pageEntry.time, !isBaselineEntry), 'value time');
|
|
td(tr, percent(pageEntry.timePercent, !isBaselineEntry), 'value time');
|
|
td(tr, count(pageEntry.count, !isBaselineEntry), 'value count');
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
table.replaceChild(tbody, table.querySelector('tbody'));
|
|
}
|
|
|
|
function showPageDetails(entry) {
|
|
var table, tbody, entries;
|
|
table = $('detailView').querySelector('.pageDetailTable');
|
|
tbody = document.createElement('tbody');
|
|
if (entry === undefined) {
|
|
table.replaceChild(tbody, table.querySelector('tbody'));
|
|
return;
|
|
}
|
|
var version = entry.page.version;
|
|
var showDiff = version !== baselineVersion;
|
|
$('detailView').querySelector('.pageDetail h3 span').textContent =
|
|
version.name;
|
|
entries = version.pages.map((page) => {
|
|
if (!page.enabled) return;
|
|
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, ms(pageEntry.time, showDiff), 'value time');
|
|
td(tr, percent(pageEntry.timePercent, showDiff), 'value time');
|
|
td(tr, percent(pageEntry.timePercentPerEntry, showDiff),
|
|
'value time hideNoDiff');
|
|
td(tr, count(pageEntry.count, showDiff), 'value count');
|
|
tbody.appendChild(tr);
|
|
});
|
|
// show the total for all pages
|
|
var tds = table.querySelectorAll('tfoot td');
|
|
tds[1].textContent = ms(entry.getTimeImpact(), showDiff);
|
|
// Only show the percentage total if we are in diff mode:
|
|
tds[2].textContent = percent(entry.getTimePercentImpact(), showDiff);
|
|
tds[3].textContent = '';
|
|
tds[4].textContent = count(entry.getCountImpact(), showDiff);
|
|
table.replaceChild(tbody, table.querySelector('tbody'));
|
|
}
|
|
|
|
function showImpactList(page) {
|
|
var impactView = $('detailView').querySelector('.impactView');
|
|
impactView.querySelector('h3 span').textContent = 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').textContent += " " + 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'));
|
|
}
|
|
|
|
function showGraphs(page) {
|
|
var groups = page.groups.slice();
|
|
// Sort groups by the biggest impact
|
|
groups.sort((a, b) => {
|
|
return b.getTimeImpact() - a.getTimeImpact();
|
|
});
|
|
if (selectedGroup == undefined) {
|
|
selectedGroup = groups[0];
|
|
} else {
|
|
groups = groups.filter(each => each.enabled && each.name != selectedGroup.name);
|
|
groups.unshift(selectedGroup);
|
|
}
|
|
showPageGraph(groups, page);
|
|
showVersionGraph(groups, page);
|
|
showPageVersionGraph(groups, page);
|
|
}
|
|
|
|
function getGraphDataTable(groups) {
|
|
var dataTable = new google.visualization.DataTable();
|
|
dataTable.addColumn('string', 'Name');
|
|
groups.forEach(group => {
|
|
var column = dataTable.addColumn('number', group.name.substring(6));
|
|
dataTable.setColumnProperty(column, 'group', group);
|
|
});
|
|
return dataTable;
|
|
}
|
|
|
|
var selectedGroup;
|
|
function showPageGraph(groups, page) {
|
|
var isDiffView = baselineVersion !== undefined;
|
|
var dataTable = getGraphDataTable(groups);
|
|
// Calculate the average row
|
|
var row = ['Average'];
|
|
groups.forEach((group) => {
|
|
if (isDiffView) {
|
|
row.push(group.isTotal ? 0 : group.getAverageTimeImpact());
|
|
} else {
|
|
row.push(group.isTotal ? 0 : group.getTimeImpact());
|
|
}
|
|
});
|
|
dataTable.addRow(row);
|
|
// Sort the pages by the selected group.
|
|
var pages = page.version.pages.filter(page => page.enabled);
|
|
function sumDiff(page) {
|
|
var sum = 0;
|
|
groups.forEach(group => {
|
|
var value = group.getTimePercentImpact() -
|
|
page.getEntry(group).timePercent;
|
|
sum += value * value;
|
|
});
|
|
return sum;
|
|
}
|
|
if (isDiffView) {
|
|
pages.sort((a, b) => {
|
|
return b.getEntry(selectedGroup).time-
|
|
a.getEntry(selectedGroup).time;
|
|
});
|
|
} else {
|
|
pages.sort((a, b) => {
|
|
return b.getEntry(selectedGroup).timePercent -
|
|
a.getEntry(selectedGroup).timePercent;
|
|
});
|
|
}
|
|
// Sort by sum of squared distance to the average.
|
|
// pages.sort((a, b) => {
|
|
// return a.distanceFromTotalPercent() - b.distanceFromTotalPercent();
|
|
// });
|
|
// Calculate the entries for the pages
|
|
pages.forEach((page) => {
|
|
row = [page.name];
|
|
groups.forEach((group) => {
|
|
row.push(group.isTotal ? 0 : page.getEntry(group).time);
|
|
});
|
|
var rowIndex = dataTable.addRow(row);
|
|
dataTable.setRowProperty(rowIndex, 'page', page);
|
|
});
|
|
renderGraph('Pages for ' + page.version.name, groups, dataTable,
|
|
'pageGraph', isDiffView ? true : 'percent');
|
|
}
|
|
|
|
function showVersionGraph(groups, page) {
|
|
var dataTable = getGraphDataTable(groups);
|
|
var row;
|
|
var vs = versions.versions.filter(version => version.enabled);
|
|
vs.sort((a, b) => {
|
|
return b.getEntry(selectedGroup).getTimeImpact() -
|
|
a.getEntry(selectedGroup).getTimeImpact();
|
|
});
|
|
// Calculate the entries for the versions
|
|
vs.forEach((version) => {
|
|
row = [version.name];
|
|
groups.forEach((group) => {
|
|
row.push(group.isTotal ? 0 : version.getEntry(group).getTimeImpact());
|
|
});
|
|
var rowIndex = dataTable.addRow(row);
|
|
dataTable.setRowProperty(rowIndex, 'page', page);
|
|
});
|
|
renderGraph('Versions Total Time over all Pages', groups, dataTable,
|
|
'versionGraph', true);
|
|
}
|
|
|
|
function showPageVersionGraph(groups, page) {
|
|
var dataTable = getGraphDataTable(groups);
|
|
var row;
|
|
var vs = versions.getPageVersions(page);
|
|
vs.sort((a, b) => {
|
|
return b.getEntry(selectedGroup).time - a.getEntry(selectedGroup).time;
|
|
});
|
|
// Calculate the entries for the versions
|
|
vs.forEach((page) => {
|
|
row = [page.version.name];
|
|
groups.forEach((group) => {
|
|
row.push(group.isTotal ? 0 : page.getEntry(group).time);
|
|
});
|
|
var rowIndex = dataTable.addRow(row);
|
|
dataTable.setRowProperty(rowIndex, 'page', page);
|
|
});
|
|
renderGraph('Versions for ' + page.name, groups, dataTable,
|
|
'pageVersionGraph', true);
|
|
}
|
|
|
|
function renderGraph(title, groups, dataTable, id, isStacked) {
|
|
var isDiffView = baselineVersion !== undefined;
|
|
var formatter = new google.visualization.NumberFormat({
|
|
suffix: (isDiffView ? 'msΔ' : 'ms'),
|
|
negativeColor: 'red',
|
|
groupingSymbol: "'"
|
|
});
|
|
for (var i = 1; i < dataTable.getNumberOfColumns(); i++) {
|
|
formatter.format(dataTable, i);
|
|
}
|
|
var height = 85 + 28 * dataTable.getNumberOfRows();
|
|
var options = {
|
|
isStacked: isStacked,
|
|
height: height,
|
|
hAxis: {
|
|
minValue: 0,
|
|
},
|
|
animation:{
|
|
duration: 500,
|
|
easing: 'out',
|
|
},
|
|
vAxis: {
|
|
},
|
|
explorer: {
|
|
actions: ['dragToZoom', 'rightClickToReset'],
|
|
maxZoomIn: 0.01
|
|
},
|
|
legend: {position:'top', textStyle:{fontSize: '16px'}},
|
|
chartArea: {left:200, top:50, width:'98%', height:'80%'},
|
|
colors: groups.map(each => each.color)
|
|
};
|
|
var parentNode = $(id);
|
|
parentNode.querySelector('h2>span, h3>span').textContent = title;
|
|
var graphNode = parentNode.querySelector('.content');
|
|
|
|
var chart = graphNode.chart;
|
|
if (chart === undefined) {
|
|
chart = graphNode.chart = new google.visualization.BarChart(graphNode);
|
|
} else {
|
|
google.visualization.events.removeAllListeners(chart);
|
|
}
|
|
google.visualization.events.addListener(chart, 'select', selectHandler);
|
|
function getChartEntry(selection) {
|
|
if (!selection) return undefined;
|
|
var column = selection.column;
|
|
if (column == undefined) return undefined;
|
|
var selectedGroup = dataTable.getColumnProperty(column, 'group');
|
|
var row = selection.row;
|
|
if (row == null) return selectedGroup;
|
|
var page = dataTable.getRowProperty(row, 'page');
|
|
if (!page) return selectedGroup;
|
|
return page.getEntry(selectedGroup);
|
|
}
|
|
function selectHandler() {
|
|
selectedGroup = getChartEntry(chart.getSelection()[0])
|
|
if (!selectedGroup) return;
|
|
selectEntry(selectedGroup, true);
|
|
}
|
|
|
|
// Make our global tooltips work
|
|
google.visualization.events.addListener(chart, 'onmouseover', mouseOverHandler);
|
|
function mouseOverHandler(selection) {
|
|
graphNode.entry = getChartEntry(selection);
|
|
}
|
|
chart.draw(dataTable, options);
|
|
}
|
|
|
|
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').textContent = entry.name;
|
|
popover.querySelector('td.page').textContent = 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').textContent = baselineVersion.name;
|
|
node('.time').textContent = '-';
|
|
node('.timeVariance').textContent = '-';
|
|
node('.percent').textContent = '-';
|
|
node('.percentPerEntry').textContent = '-';
|
|
node('.percentVariance').textContent = '-';
|
|
node('.count').textContent = '-';
|
|
node('.countVariance').textContent = '-';
|
|
node('.timeImpact').textContent = '-';
|
|
node('.timePercentImpact').textContent = '-';
|
|
} else {
|
|
node('.version').textContent = entry.page.version.name;
|
|
node('.time').textContent = ms(entry._time, false);
|
|
node('.timeVariance').textContent
|
|
= percent(entry.timeVariancePercent, false);
|
|
node('.percent').textContent = percent(entry.timePercent, false);
|
|
node('.percentPerEntry').textContent
|
|
= percent(entry.timePercentPerEntry, false);
|
|
node('.percentVariance').textContent
|
|
= percent(entry.timePercentVariancePercent, false);
|
|
node('.count').textContent = count(entry._count, false);
|
|
node('.countVariance').textContent
|
|
= percent(entry.timeVariancePercent, false);
|
|
node('.timeImpact').textContent
|
|
= ms(entry.getTimeImpact(false), false);
|
|
node('.timePercentImpact').textContent
|
|
= percent(entry.getTimeImpactVariancePercent(false), false);
|
|
}
|
|
}
|
|
</script>
|
|
<script type="text/javascript">
|
|
"use strict"
|
|
// =========================================================================
|
|
// 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 addCodeSearchButton(entry, node) {
|
|
if (entry.isGroup) return;
|
|
var button = document.createElement("div");
|
|
button.textContent = '?'
|
|
button.className = "codeSearch"
|
|
button.addEventListener('click', handleCodeSearch);
|
|
node.appendChild(button);
|
|
return node;
|
|
}
|
|
|
|
function td(tr, content, className) {
|
|
var td = document.createElement("td");
|
|
if (content[0] == '<') {
|
|
td.innerHTML = content;
|
|
} else {
|
|
td.textContent = 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 NameComparator(a, b) {
|
|
if (a.name > b.name) return 1;
|
|
if (a.name < b.name) return -1;
|
|
return 0
|
|
}
|
|
|
|
function diffSign(value, digits, unit, showDiff) {
|
|
if (showDiff === false || baselineVersion == undefined) {
|
|
if (value === undefined) return '';
|
|
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);
|
|
}
|
|
|
|
</script>
|
|
<script type="text/javascript">
|
|
"use strict"
|
|
// =========================================================================
|
|
// EventHandlers
|
|
function handleBodyLoad() {
|
|
$('uploadInput').focus();
|
|
if (defaultData) {
|
|
handleLoadJSON(defaultData);
|
|
} else if (window.location.protocol !== 'file:') {
|
|
tryLoadDefaultResults();
|
|
}
|
|
}
|
|
|
|
function tryLoadDefaultResults() {
|
|
// Try to load a results.json file adjacent to this day.
|
|
var xhr = new XMLHttpRequest();
|
|
// The markers on the following line can be used to replace the url easily
|
|
// with scripts.
|
|
xhr.open('GET', /*results-url-start*/'results.json'/*results-url-end*/, true);
|
|
xhr.onreadystatechange = function(e) {
|
|
if(this.readyState !== XMLHttpRequest.DONE || this.status !== 200) return;
|
|
handleLoadText(this.responseText);
|
|
};
|
|
xhr.send();
|
|
}
|
|
|
|
function handleLoadFile() {
|
|
var files = document.getElementById("uploadInput").files;
|
|
var file = files[0];
|
|
var reader = new FileReader();
|
|
|
|
reader.onload = function(evt) {
|
|
handleLoadText(this.result);
|
|
}
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
function handleLoadText(text) {
|
|
handleLoadJSON(JSON.parse(text));
|
|
}
|
|
|
|
function getStateFromParams() {
|
|
var query = window.location.search.substr(1);
|
|
var result = {};
|
|
query.split("&").forEach((part) => {
|
|
var item = part.split("=");
|
|
var key = decodeURIComponent(item[0])
|
|
result[key] = decodeURIComponent(item[1]);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function fixSinglePageJSON(json) {
|
|
// Try to detect the single-version case, where we're missing the toplevel
|
|
// version object. The incoming JSON is of the form:
|
|
// {"Page 1": [... data points ... ], "Page 2": [...], ...}
|
|
// Instead of the default multi-page JSON:
|
|
// {"Version 1": { "Page 1": ..., ...}, "Version 2": {...}, ...}
|
|
// In this case insert a single "Default" version as top-level entry.
|
|
var firstProperty = (object) => {
|
|
for (var key in object) return key;
|
|
};
|
|
var maybePage = json[firstProperty(json)];
|
|
if (!Array.isArray(maybePage)) return json;
|
|
return {"Default": json}
|
|
}
|
|
|
|
function handleLoadJSON(json) {
|
|
json = fixSinglePageJSON(json);
|
|
var state = getStateFromParams();
|
|
pages = new Pages();
|
|
versions = Versions.fromJSON(json);
|
|
initialize()
|
|
showPage(versions.versions[0].pages[0]);
|
|
if (!popHistoryState(state)) {
|
|
selectEntry(selectedPage.total);
|
|
}
|
|
}
|
|
|
|
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[pageSelect.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;
|
|
var showingDiff = baselineVersion !== undefined;
|
|
var body = $('body');
|
|
toggleCssClass(body, 'diff', showingDiff);
|
|
toggleCssClass(body, 'noDiff', !showingDiff);
|
|
showPage(selectedPage);
|
|
if (selectedEntry === undefined) return;
|
|
selectEntry(selectedEntry, true);
|
|
}
|
|
|
|
function findEntry(event) {
|
|
var target = event.target;
|
|
while (target.entry === undefined) {
|
|
target = target.parentNode;
|
|
if (!target) return undefined;
|
|
}
|
|
return target.entry;
|
|
}
|
|
|
|
function handleUpdatePopover(event) {
|
|
var popover = $('popover');
|
|
popover.style.left = event.pageX + 'px';
|
|
popover.style.top = event.pageY + 'px';
|
|
popover.style.display = 'none';
|
|
popover.style.display = event.shiftKey ? 'block' : 'none';
|
|
var entry = findEntry(event);
|
|
if (entry === undefined) return;
|
|
showPopover(entry);
|
|
}
|
|
|
|
function handleToggleVersionOrPageEnable(event) {
|
|
var item = this.item ;
|
|
if (item === undefined) return;
|
|
item .enabled = this.checked;
|
|
initialize();
|
|
var page = selectedPage;
|
|
if (page === undefined || !page.version.enabled) {
|
|
page = versions.getEnabledPage(page.name);
|
|
}
|
|
if (!page.enabled) {
|
|
page = page.getNextPage();
|
|
}
|
|
showPage(page);
|
|
}
|
|
|
|
function handleToggleContentVisibility(event) {
|
|
var content = event.target.contentNode;
|
|
toggleCssClass(content, 'hidden');
|
|
}
|
|
|
|
function handleCodeSearch(event) {
|
|
var entry = findEntry(event);
|
|
if (entry === undefined) return;
|
|
var url = "https://cs.chromium.org/search/?sq=package:chromium&type=cs&q=";
|
|
name = entry.name;
|
|
if (name.startsWith("API_")) {
|
|
name = name.substring(4);
|
|
}
|
|
url += encodeURIComponent(name) + "+file:src/v8/src";
|
|
window.open(url,'_blank');
|
|
}
|
|
</script>
|
|
<script type="text/javascript">
|
|
"use strict"
|
|
// =========================================================================
|
|
class Versions {
|
|
constructor() {
|
|
this.versions = [];
|
|
}
|
|
add(version) {
|
|
this.versions.push(version)
|
|
}
|
|
getPageVersions(page) {
|
|
var result = [];
|
|
this.versions.forEach((version) => {
|
|
if (!version.enabled) return;
|
|
var versionPage = version.get(page.name);
|
|
if (versionPage !== undefined) result.push(versionPage);
|
|
});
|
|
return result;
|
|
}
|
|
get length() {
|
|
return this.versions.length
|
|
}
|
|
get(index) {
|
|
return this.versions[index]
|
|
}
|
|
getByName(name) {
|
|
return this.versions.find((each) => each.name == name);
|
|
}
|
|
forEach(f) {
|
|
this.versions.forEach(f);
|
|
}
|
|
sort() {
|
|
this.versions.sort(NameComparator);
|
|
}
|
|
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;
|
|
}
|
|
getNextPage(page) {
|
|
if (this.length == 0) return undefined;
|
|
return this.pages[(this.indexOf(page.name) + 1) % this.length];
|
|
}
|
|
get(name) {
|
|
var index = this.indexOf(name);
|
|
if (0 <= index) return this.pages[index];
|
|
return undefined
|
|
}
|
|
get length() {
|
|
return this.pages.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.forEachPage((page) => {
|
|
page.forEach(fun);
|
|
});
|
|
}
|
|
forEachPage(fun) {
|
|
this.pages.forEach((page) => {
|
|
if (!page.enabled) return;
|
|
fun(page);
|
|
})
|
|
}
|
|
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.forEachPage((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 || showDiff === false) {
|
|
// 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);
|
|
var total = this.getTotalValue(name, '_time');
|
|
return (total / baselineValue - 1) * 100;
|
|
}
|
|
getTotalTimeVariance(name, showDiff) {
|
|
// Calculate the overall error for a given entry name
|
|
var sum = 0;
|
|
this.forEachPage((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');
|
|
}
|
|
getAverageTimeImpact(name, showDiff) {
|
|
return this.getTotalTime(name, showDiff) / this.pages.length;
|
|
}
|
|
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(NameComparator)
|
|
}
|
|
}
|
|
Version.fromJSON = function(name, data) {
|
|
var version = new Version(name);
|
|
for (var pageName in data) {
|
|
version.add(PageVersion.fromJSON(version, pageName, data[pageName]));
|
|
}
|
|
version.sort();
|
|
return version;
|
|
}
|
|
|
|
class Pages extends Map {
|
|
get(name) {
|
|
if (name.indexOf('www.') == 0) {
|
|
name = name.substring(4);
|
|
}
|
|
if (!this.has(name)) {
|
|
this.set(name, new Page(name));
|
|
}
|
|
return super.get(name);
|
|
}
|
|
}
|
|
|
|
class Page {
|
|
constructor(name) {
|
|
this.name = name;
|
|
this.enabled = true;
|
|
this.versions = [];
|
|
}
|
|
add(page) {
|
|
this.versions.push(page);
|
|
}
|
|
}
|
|
|
|
class PageVersion {
|
|
constructor(version, page) {
|
|
this.page = page;
|
|
this.page.add(this);
|
|
this.total = Group.groups.get('total').entry();
|
|
this.total.isTotal = true;
|
|
this.unclassified = new UnclassifiedEntry(this)
|
|
this.groups = [
|
|
this.total,
|
|
Group.groups.get('ic').entry(),
|
|
Group.groups.get('optimize').entry(),
|
|
Group.groups.get('compile-background').entry(),
|
|
Group.groups.get('compile').entry(),
|
|
Group.groups.get('parse-background').entry(),
|
|
Group.groups.get('parse').entry(),
|
|
Group.groups.get('callback').entry(),
|
|
Group.groups.get('api').entry(),
|
|
Group.groups.get('gc').entry(),
|
|
Group.groups.get('javascript').entry(),
|
|
Group.groups.get('runtime').entry(),
|
|
this.unclassified
|
|
];
|
|
this.entryDict = new Map();
|
|
this.groups.forEach((entry) => {
|
|
entry.page = this;
|
|
this.entryDict.set(entry.name, entry);
|
|
});
|
|
this.version = version;
|
|
}
|
|
toString() {
|
|
return this.version.name + ": " + this.name;
|
|
}
|
|
urlParams() {
|
|
return { version: this.version.name, page: this.name};
|
|
}
|
|
add(entry) {
|
|
// Ignore accidentally added Group entries.
|
|
if (entry.name.startsWith(GroupedEntry.prefix)) return;
|
|
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
|
|
}
|
|
get name() { return this.page.name }
|
|
get enabled() { return this.page.enabled }
|
|
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()
|
|
});
|
|
}
|
|
distanceFromTotalPercent() {
|
|
var sum = 0;
|
|
this.groups.forEach(group => {
|
|
if (group == this.total) return;
|
|
var value = group.getTimePercentImpact() -
|
|
this.getEntry(group).timePercent;
|
|
sum += value * value;
|
|
});
|
|
return sum;
|
|
}
|
|
getNextPage() {
|
|
return this.version.getNextPage(this);
|
|
}
|
|
}
|
|
PageVersion.fromJSON = function(version, name, data) {
|
|
var page = new PageVersion(version, pages.get(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;
|
|
this.isTotal = false;
|
|
}
|
|
urlParams() {
|
|
var params = this.page.urlParams();
|
|
params.entry = this.name;
|
|
return params;
|
|
}
|
|
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 timePercentPerEntry() {
|
|
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 - 1) * 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);
|
|
}
|
|
getAverageTimeImpact(showDiff) {
|
|
return this.page.version.getAverageTimeImpact(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 Group {
|
|
constructor(name, regexp, color) {
|
|
this.name = name;
|
|
this.regexp = regexp;
|
|
this.color = color;
|
|
this.enabled = true;
|
|
}
|
|
entry() { return new GroupedEntry(this) };
|
|
}
|
|
Group.groups = new Map();
|
|
Group.add = function(name, group) {
|
|
this.groups.set(name, group);
|
|
return group;
|
|
}
|
|
Group.add('total', new Group('Total', /.*Total.*/, '#BBB'));
|
|
Group.add('ic', new Group('IC', /.*IC_.*/, "#3366CC"));
|
|
Group.add('optimize', new Group('Optimize',
|
|
/StackGuard|.*Optimize.*|.*Deoptimize.*|Recompile.*/, "#DC3912"));
|
|
Group.add('compile-background', new Group('Compile-Background',
|
|
/(.*CompileBackground.*)/, "#b9a720"));
|
|
Group.add('compile', new Group('Compile',
|
|
/(^Compile.*)|(.*_Compile.*)/, "#FFAA00"));
|
|
Group.add('parse-background',
|
|
new Group('Parse-Background', /.*ParseBackground.*/, "#af744d"));
|
|
Group.add('parse', new Group('Parse', /.*Parse.*/, "#FF6600"));
|
|
Group.add('callback', new Group('Blink C++', /.*Callback.*/, "#109618"));
|
|
Group.add('api', new Group('API', /.*API.*/, "#990099"));
|
|
Group.add('gc', new Group('GC', /GC|AllocateInTargetSpace/, "#0099C6"));
|
|
Group.add('javascript', new Group('JavaScript', /JS_Execution/, "#DD4477"));
|
|
Group.add('runtime', new Group('V8 C++', /.*/, "#88BB00"));
|
|
var group =
|
|
Group.add('unclassified', new Group('Unclassified', /.*/, "#000"));
|
|
group.enabled = false;
|
|
|
|
class GroupedEntry extends Entry {
|
|
constructor(group) {
|
|
super(0, GroupedEntry.prefix + group.name, 0, 0, 0, 0, 0, 0);
|
|
this.group = group;
|
|
this.entries = [];
|
|
}
|
|
get regexp() { return this.group.regexp }
|
|
get color() { return this.group.color }
|
|
get enabled() { return this.group.enabled }
|
|
add(entry) {
|
|
if (!this.regexp.test(entry.name)) 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) {
|
|
// Show also all entries which are in at least one version.
|
|
var dummyEntryNames = new Set();
|
|
versions.forEach((version) => {
|
|
var groupEntry = version.getEntry(this);
|
|
if (groupEntry != this) {
|
|
for (var entry of groupEntry.entries) {
|
|
if (this.page.get(entry.name) == undefined) {
|
|
dummyEntryNames.add(entry.name);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var tmpEntries = [];
|
|
for (var name of dummyEntryNames) {
|
|
var tmpEntry = new Entry(0, name, 0, 0, 0, 0, 0, 0);
|
|
tmpEntry.page = this.page;
|
|
tmpEntries.push(tmpEntry);
|
|
};
|
|
|
|
// Concatenate our real entries.
|
|
tmpEntries = tmpEntries.concat(this.entries);
|
|
|
|
// The compared entries are sorted by absolute impact.
|
|
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')
|
|
}
|
|
}
|
|
GroupedEntry.prefix = 'Group-';
|
|
|
|
class UnclassifiedEntry extends GroupedEntry {
|
|
constructor(page) {
|
|
super(Group.groups.get('unclassified'));
|
|
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 id="body" onmousemove="handleUpdatePopover(event)" onload="handleBodyLoad()" class="noDiff">
|
|
<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: <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>
|
|
|
|
<div id="versionSelector" class="inline toggleContentVisibility">
|
|
<h2>Versions</h2>
|
|
<div class="content hidden">
|
|
<ul></ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="pageSelector" class="inline toggleContentVisibility">
|
|
<h2>Pages</h2>
|
|
<div class="content hidden">
|
|
<ul></ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="groupSelector" class="inline toggleContentVisibility">
|
|
<h2>Groups</h2>
|
|
<div class="content hidden">
|
|
<ul></ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="view">
|
|
</div>
|
|
|
|
<div id="detailView" class="hidden">
|
|
<div class="versionDetail inline toggleContentVisibility">
|
|
<h3><span></span></h3>
|
|
<div class="content">
|
|
<table class="versionDetailTable" onclick="handleSelectDetailRow(this, event);">
|
|
<thead>
|
|
<tr>
|
|
<th class="version">Version </th>
|
|
<th class="position">Pos. </th>
|
|
<th class="value time">Time▴ </th>
|
|
<th class="value time">Percent </th>
|
|
<th class="value count">Count </th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="pageDetail inline toggleContentVisibility">
|
|
<h3>Page Comparison for <span></span></h3>
|
|
<div class="content">
|
|
<table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
|
|
<thead>
|
|
<tr>
|
|
<th class="page">Page </th>
|
|
<th class="value time">Time </th>
|
|
<th class="value time">Percent▾ </th>
|
|
<th class="value time hideNoDiff">%/Entry </th>
|
|
<th class="value count">Count </th>
|
|
</tr>
|
|
</thead>
|
|
<tfoot>
|
|
<tr>
|
|
<td class="page">Total:</td>
|
|
<td class="value time"></td>
|
|
<td class="value time"></td>
|
|
<td class="value time hideNoDiff"></td>
|
|
<td class="value count"></td>
|
|
</tr>
|
|
</tfoot>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="impactView inline toggleContentVisibility">
|
|
<h3>Impact list for <span></span></h3>
|
|
<div class="content">
|
|
<table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
|
|
<thead>
|
|
<tr>
|
|
<th class="page">Name </th>
|
|
<th class="value time">Time </th>
|
|
<th class="value time">Percent▾ </th>
|
|
<th class="">Top Pages</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="pageVersionGraph" class="graph hidden toggleContentVisibility">
|
|
<h3><span></span></h3>
|
|
<div class="content"></div>
|
|
</div>
|
|
<div id="pageGraph" class="graph hidden toggleContentVisibility">
|
|
<h3><span></span></h3>
|
|
<div class="content"></div>
|
|
</div>
|
|
<div id="versionGraph" class="graph hidden toggleContentVisibility">
|
|
<h3><span></span></h3>
|
|
<div class="content"></div>
|
|
</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. </th>
|
|
<th class="name">Name </th>
|
|
<th class="value time">Time </th>
|
|
<th class="value time">Percent </th>
|
|
<th class="value count">Count </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.</li>
|
|
<li>Check out a known working version of webpagereply:
|
|
<pre>git -C $CHROME_DIR/third_party/webpagereplay checkout 7dbd94752d1cde5536ffc623a9e10a51721eff1d</pre>
|
|
</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>Percent per Entry:</td>
|
|
<td class="percentPerEntry"></td><td colspan=2></td>
|
|
<td class="compare percentPerEntry"></td><td colspan=2></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>
|