mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1810 lines
57 KiB
1810 lines
57 KiB
8 years ago
|
<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;
|
||
|
|
||
|
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.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'));
|
||
|
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', handleToggleVersionEnable);
|
||
|
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]);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
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.getPageVersions(firstPage);
|
||
|
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');
|
||
|
}
|
||
|
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, '-', '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 = 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.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);
|
||
|
}
|
||
|
|
||
|
function showVersionDetails(entry) {
|
||
|
var table, tbody, entries;
|
||
|
table = $('detailView').querySelector('.versionDetailTable');
|
||
|
tbody = document.createElement('tbody');
|
||
|
if (entry !== undefined) {
|
||
|
$('detailView').querySelector('.versionDetail h3 span').innerHTML =
|
||
|
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').innerHTML =
|
||
|
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].innerHTML = ms(entry.getTimeImpact(), showDiff);
|
||
|
// Only show the percentage total if we are in diff mode:
|
||
|
tds[2].innerHTML = percent(entry.getTimePercentImpact(), showDiff);
|
||
|
tds[3].innerHTML = '';
|
||
|
tds[4].innerHTML = count(entry.getCountImpact(), showDiff);
|
||
|
table.replaceChild(tbody, table.querySelector('tbody'));
|
||
|
}
|
||
|
|
||
|
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'));
|
||
|
}
|
||
|
|
||
|
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.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').innerHTML = 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').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('.percentPerEntry').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('.percentPerEntry').innerHTML
|
||
|
= percent(entry.timePercentPerEntry, 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);
|
||
|
}
|
||
|
}
|
||
|
</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.innerHTML = '?'
|
||
|
button.className = "codeSearch"
|
||
|
button.addEventListener('click', handleCodeSearch);
|
||
|
node.appendChild(button);
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
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 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();
|
||
|
}
|
||
|
|
||
|
function handleLoadFile() {
|
||
|
var files = document.getElementById("uploadInput").files;
|
||
|
var file = files[0];
|
||
|
var reader = new FileReader();
|
||
|
|
||
|
reader.onload = function(evt) {
|
||
|
pages = new Pages();
|
||
|
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[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 handleToggleVersionEnable(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);
|
||
|
}
|
||
|
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]
|
||
|
};
|
||
|
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;
|
||
|
}
|
||
|
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.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 = new GroupedEntry('Total', /.*Total.*/, '#BBB');
|
||
|
this.total.isTotal = true;
|
||
|
this.unclassified = new UnclassifiedEntry(this, "#000")
|
||
|
this.groups = [
|
||
|
this.total,
|
||
|
new GroupedEntry('IC', /.*IC.*/, "#3366CC"),
|
||
|
new GroupedEntry('Optimize',
|
||
|
/StackGuard|.*Optimize.*|.*Deoptimize.*|Recompile.*/, "#DC3912"),
|
||
|
new GroupedEntry('Compile', /.*Compile.*/, "#FFAA00"),
|
||
|
new GroupedEntry('Parse', /.*Parse.*/, "#FF6600"),
|
||
|
new GroupedEntry('Callback', /.*Callback$/, "#109618"),
|
||
|
new GroupedEntry('API', /.*API.*/, "#990099"),
|
||
|
new GroupedEntry('GC', /GC|AllocateInTargetSpace/, "#0099C6"),
|
||
|
new GroupedEntry('JavaScript', /JS_Execution/, "#DD4477"),
|
||
|
new GroupedEntry('Runtime', /.*/, "#88BB00"),
|
||
|
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
|
||
|
}
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
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;
|
||
|
}
|
||
|
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 GroupedEntry extends Entry {
|
||
|
constructor(name, regexp, color) {
|
||
|
super(0, 'Group-' + name, 0, 0, 0, 0, 0, 0);
|
||
|
this.regexp = regexp;
|
||
|
this.color = color;
|
||
|
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, color) {
|
||
|
super('Unclassified', undefined, color);
|
||
|
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>Version Selector</h2>
|
||
|
<div class="content hidden">
|
||
|
<ul></ul>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="pageSelector" class="inline toggleContentVisibility">
|
||
|
<h2>Page Selector</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>
|