Home | History | Annotate | Download | only in tools
      1 <html>
      2 <!--
      3 Copyright 2016 the V8 project authors. All rights reserved.  Use of this source
      4 code is governed by a BSD-style license that can be found in the LICENSE file.
      5 -->
      6 
      7 <head>
      8   <meta charset="UTF-8">
      9   <style>
     10     body {
     11       font-family: arial;
     12     }
     13 
     14     table {
     15       display: table;
     16       border-spacing: 0px;
     17     }
     18 
     19     tr {
     20       border-spacing: 0px;
     21       padding: 10px;
     22     }
     23 
     24     td,
     25     th {
     26       padding: 3px 10px 3px 5px;
     27     }
     28 
     29     .inline {
     30       display: inline-block;
     31       vertical-align: top;
     32     }
     33 
     34     h2,
     35     h3 {
     36       margin-bottom: 0px;
     37     }
     38 
     39     .hidden {
     40       display: none;
     41     }
     42 
     43     .view {
     44       display: table;
     45     }
     46 
     47     .column {
     48       display: table-cell;
     49       border-right: 1px black dotted;
     50       min-width: 200px;
     51     }
     52 
     53     .column .header {
     54       padding: 0 10px 0 10px
     55     }
     56 
     57     #column {
     58       display: none;
     59     }
     60 
     61     .list {
     62       width: 100%;
     63     }
     64 
     65     select {
     66       width: 100%
     67     }
     68 
     69     .list tbody {
     70       cursor: pointer;
     71     }
     72 
     73     .list tr:nth-child(even) {
     74       background-color: #EFEFEF;
     75     }
     76 
     77     .list tr:nth-child(even).selected {
     78       background-color: #DDD;
     79     }
     80 
     81     .list tr.child {
     82       display: none;
     83     }
     84 
     85     .list tr.child.visible {
     86       display: table-row;
     87     }
     88 
     89     .list .child .name {
     90       padding-left: 20px;
     91     }
     92 
     93     .list .parent td {
     94       border-top: 1px solid #AAA;
     95     }
     96 
     97     .list .total {
     98       font-weight: bold
     99     }
    100 
    101     .list tr.parent {
    102       background-color: #FFF;
    103     }
    104 
    105     .list tr.parent.selected {
    106       background-color: #DDD;
    107     }
    108 
    109     tr.selected {
    110       background-color: #DDD;
    111     }
    112 
    113     .codeSearch {
    114       display: block-inline;
    115       float: right;
    116       border-radius: 5px;
    117       background-color: #EEE;
    118       width: 1em;
    119       text-align: center;
    120     }
    121 
    122     .list .position {
    123       text-align: right;
    124       display: none;
    125     }
    126 
    127     .list div.toggle {
    128       cursor: pointer;
    129     }
    130 
    131     #column_0 .position {
    132       display: table-cell;
    133     }
    134 
    135     #column_0 .name {
    136       display: table-cell;
    137     }
    138 
    139     .list .name {
    140       display: none;
    141       white-space: nowrap;
    142     }
    143 
    144     .value {
    145       text-align: right;
    146     }
    147 
    148     .selectedVersion {
    149       font-weight: bold;
    150     }
    151 
    152     #baseline {
    153       width: auto;
    154     }
    155 
    156     .compareSelector {
    157       padding-bottom: 20px;
    158     }
    159 
    160     .pageDetailTable tbody {
    161       cursor: pointer
    162     }
    163 
    164     .pageDetailTable tfoot td {
    165       border-top: 1px grey solid;
    166     }
    167 
    168     #popover {
    169       position: absolute;
    170       transform: translateY(-50%) translateX(40px);
    171       box-shadow: -2px 10px 44px -10px #000;
    172       border-radius: 5px;
    173       z-index: 1;
    174       background-color: #FFF;
    175       display: none;
    176       white-space: nowrap;
    177     }
    178 
    179     #popover table {
    180       position: relative;
    181       z-index: 1;
    182       text-align: right;
    183       margin: 10px;
    184     }
    185     #popover td {
    186       padding: 3px 0px 3px 5px;
    187       white-space: nowrap;
    188     }
    189 
    190     .popoverArrow {
    191       background-color: #FFF;
    192       position: absolute;
    193       width: 30px;
    194       height: 30px;
    195       transform: translateY(-50%)rotate(45deg);
    196       top: 50%;
    197       left: -10px;
    198       z-index: 0;
    199     }
    200 
    201     #popover .name {
    202       padding: 5px;
    203       font-weight: bold;
    204       text-align: center;
    205     }
    206 
    207     #popover table .compare {
    208       display: none
    209     }
    210 
    211     #popover table.compare .compare {
    212       display: table-cell;
    213     }
    214 
    215     #popover .compare .time,
    216     #popover .compare .version {
    217       padding-left: 10px;
    218     }
    219     .graph,
    220     .graph .content {
    221       width: 100%;
    222     }
    223 
    224     .diff .hideDiff {
    225       display: none;
    226     }
    227     .noDiff .hideNoDiff {
    228       display: none;
    229     }
    230   </style>
    231   <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
    232   <script type="text/javascript">
    233     "use strict"
    234     google.charts.load('current', {packages: ['corechart']});
    235 
    236     // Did anybody say monkeypatching?
    237     if (!NodeList.prototype.forEach) {
    238       NodeList.prototype.forEach = function(func) {
    239         for (var i = 0; i < this.length; i++) {
    240           func(this[i]);
    241         }
    242       }
    243     }
    244 
    245     var versions;
    246     var pages;
    247     var selectedPage;
    248     var baselineVersion;
    249     var selectedEntry;
    250 
    251     // Marker to programatically replace the defaultData.
    252     var defaultData = /*default-data-start*/undefined/*default-data-end*/;
    253 
    254     function initialize() {
    255       // Initialize the stats table and toggle lists.
    256       var original = $("column");
    257       var view = document.createElement('div');
    258       view.id = 'view';
    259       var i = 0;
    260       versions.forEach((version) =>  {
    261         if (!version.enabled) return;
    262         // add column
    263         var column = original.cloneNode(true);
    264         column.id = "column_" + i;
    265         // Fill in all versions
    266         var select = column.querySelector(".version");
    267         select.id = "selectVersion_" + i;
    268         // add all select options
    269         versions.forEach((version) => {
    270           if (!version.enabled) return;
    271           var option = document.createElement("option");
    272           option.textContent = version.name;
    273           option.version = version;
    274           select.appendChild(option);
    275         });
    276         // Fill in all page versions
    277         select = column.querySelector(".pageVersion");
    278         select.id = "select_" + i;
    279         // add all pages
    280         versions.forEach((version) => {
    281           if (!version.enabled) return;
    282           var optgroup = document.createElement("optgroup");
    283           optgroup.label = version.name;
    284           optgroup.version = version;
    285           version.forEachPage((page) => {
    286             var option = document.createElement("option");
    287             option.textContent = page.name;
    288             option.page = page;
    289             optgroup.appendChild(option);
    290           });
    291           select.appendChild(optgroup);
    292         });
    293         view.appendChild(column);
    294         i++;
    295       });
    296       var oldView = $('view');
    297       oldView.parentNode.replaceChild(view, oldView);
    298 
    299       var select = $('baseline');
    300       removeAllChildren(select);
    301       select.appendChild(document.createElement('option'));
    302       versions.forEach((version) => {
    303         var option = document.createElement("option");
    304         option.textContent = version.name;
    305         option.version = version;
    306         select.appendChild(option);
    307       });
    308       initializeToggleList(versions.versions, $('versionSelector'));
    309       initializeToggleList(pages.values(), $('pageSelector'));
    310       initializeToggleList(Group.groups.values(), $('groupSelector'));
    311       initializeToggleContentVisibility();
    312     }
    313 
    314     function initializeToggleList(items, node) {
    315       var list = node.querySelector('ul');
    316       removeAllChildren(list);
    317       items = Array.from(items);
    318       items.sort(NameComparator);
    319       items.forEach((item) => {
    320         var li = document.createElement('li');
    321         var checkbox = document.createElement('input');
    322         checkbox.type = 'checkbox';
    323         checkbox.checked = item.enabled;
    324         checkbox.item = item;
    325         checkbox.addEventListener('click', handleToggleVersionOrPageEnable);
    326         li.appendChild(checkbox);
    327         li.appendChild(document.createTextNode(item.name));
    328         list.appendChild(li);
    329       });
    330       $('results').querySelectorAll('#results > .hidden').forEach((node) => {
    331         toggleCssClass(node, 'hidden', false);
    332       })
    333     }
    334 
    335     function initializeToggleContentVisibility() {
    336       var nodes = document.querySelectorAll('.toggleContentVisibility');
    337       nodes.forEach((node) => {
    338         var content = node.querySelector('.content');
    339         var header = node.querySelector('h1,h2,h3');
    340         if (content === undefined || header === undefined) return;
    341         if (header.querySelector('input') != undefined) return;
    342         var checkbox = document.createElement('input');
    343         checkbox.type = 'checkbox';
    344         checkbox.checked = content.className.indexOf('hidden') == -1;
    345         checkbox.contentNode = content;
    346         checkbox.addEventListener('click', handleToggleContentVisibility);
    347         header.insertBefore(checkbox, header.childNodes[0]);
    348       });
    349     }
    350 
    351     window.addEventListener('popstate', (event) => {
    352       popHistoryState(event.state);
    353     });
    354 
    355     function popHistoryState(state) {
    356       if (!state.version) return false;
    357       if (!versions) return false;
    358       var version = versions.getByName(state.version);
    359       if (!version) return false;
    360       var page = version.get(state.page);
    361       if (!page) return false;
    362       if (!state.entry) {
    363         showEntry(page.total);
    364       } else {
    365         var entry = page.get(state.entry);
    366         if (!entry) {
    367           showEntry(page.total);
    368         } else {
    369           showEntry(entry);
    370         }
    371       }
    372       return true;
    373     }
    374 
    375     function pushHistoryState() {
    376       var selection = selectedEntry ? selectedEntry : selectedPage;
    377       if (!selection) return;
    378       var state = selection.urlParams();
    379       // Don't push a history state if it didn't change.
    380       if (JSON.stringify(window.history.state) === JSON.stringify(state)) return;
    381       var params = "?";
    382       for (var pairs of Object.entries(state)) {
    383         params += encodeURIComponent(pairs[0]) + "="
    384             + encodeURIComponent(pairs[1]) + "&";
    385       }
    386       window.history.pushState(state, selection.toString(), params);
    387     }
    388 
    389     function showSelectedEntryInPage(page) {
    390       if (!selectedEntry) return showPage(page);
    391       var entry = page.get(selectedEntry.name);
    392       if (!entry) return showPage(page);
    393       selectEntry(entry);
    394     }
    395 
    396     function showPage(firstPage) {
    397       var changeSelectedEntry = selectedEntry !== undefined
    398           && selectedEntry.page === selectedPage;
    399       selectedPage = firstPage;
    400       selectedPage.sort();
    401       showPageInColumn(firstPage, 0);
    402       // Show the other versions of this page in the following columns.
    403       var pageVersions = versions.getPageVersions(firstPage);
    404       var index = 1;
    405       pageVersions.forEach((page) => {
    406         if (page !== firstPage) {
    407           showPageInColumn(page, index);
    408           index++;
    409         }
    410       });
    411       if (changeSelectedEntry) {
    412         showEntryDetail(selectedPage.getEntry(selectedEntry));
    413       }
    414       showImpactList(selectedPage);
    415       pushHistoryState();
    416     }
    417 
    418     function showPageInColumn(page, columnIndex) {
    419       page.sort();
    420       var showDiff = (baselineVersion === undefined && columnIndex !== 0) ||
    421         (baselineVersion !== undefined && page.version !== baselineVersion);
    422       var diffStatus = (td, a, b) => {};
    423       if (showDiff) {
    424         if (baselineVersion !== undefined) {
    425           diffStatus = (td, a, b) => {
    426             if (a == 0) return;
    427             td.style.color = a < 0 ? '#FF0000' : '#00BB00';
    428           };
    429         } else {
    430           diffStatus = (td, a, b) => {
    431             if (a == b) return;
    432             var color;
    433             var ratio = a / b;
    434             if (ratio > 1) {
    435               ratio = Math.min(Math.round((ratio - 1) * 255 * 10), 200);
    436               color = '#' + ratio.toString(16) + "0000";
    437             } else {
    438               ratio = Math.min(Math.round((1 - ratio) * 255 * 10), 200);
    439               color = '#00' + ratio.toString(16) + "00";
    440             }
    441             td.style.color = color;
    442           }
    443         }
    444       }
    445 
    446       var column = $('column_' + columnIndex);
    447       var select = $('select_' + columnIndex);
    448       // Find the matching option
    449       selectOption(select, (i, option) => {
    450         return option.page == page
    451       });
    452       var table = column.querySelector("table");
    453       var oldTbody = table.querySelector('tbody');
    454       var tbody = document.createElement('tbody');
    455       var referencePage = selectedPage;
    456       page.forEachSorted(selectedPage, (parentEntry, entry, referenceEntry) => {
    457         var tr = document.createElement('tr');
    458         tbody.appendChild(tr);
    459         tr.entry = entry;
    460         tr.parentEntry = parentEntry;
    461         tr.className = parentEntry === undefined ? 'parent' : 'child';
    462         // Don't show entries that do not exist on the current page or if we
    463         // compare against the current page
    464         if (entry !== undefined && page.version !== baselineVersion) {
    465           // If we show a diff, use the baselineVersion as the referenceEntry
    466           if (baselineVersion !== undefined) {
    467             var baselineEntry = baselineVersion.getEntry(entry);
    468             if (baselineEntry !== undefined) referenceEntry = baselineEntry
    469           }
    470           if (!parentEntry) {
    471             var node = td(tr, '<div class="toggle"></div>', 'position');
    472             node.firstChild.addEventListener('click', handleToggleGroup);
    473           } else {
    474             td(tr, entry.position == 0 ? '' : entry.position, 'position');
    475           }
    476           addCodeSearchButton(entry,
    477               td(tr, entry.name, 'name ' + entry.cssClass()));
    478 
    479           diffStatus(
    480             td(tr, ms(entry.time), 'value time'),
    481             entry.time, referenceEntry.time);
    482           diffStatus(
    483             td(tr, percent(entry.timePercent), 'value time'),
    484             entry.time, referenceEntry.time);
    485           diffStatus(
    486             td(tr, count(entry.count), 'value count'),
    487             entry.count, referenceEntry.count);
    488         } else if (baselineVersion !== undefined && referenceEntry
    489             && page.version !== baselineVersion) {
    490           // Show comparison of entry that does not exist on the current page.
    491           tr.entry = new Entry(0, referenceEntry.name);
    492           tr.entry.page = page;
    493           td(tr, '-', 'position');
    494           td(tr, referenceEntry.name, 'name');
    495           diffStatus(
    496             td(tr, ms(referenceEntry.time), 'value time'),
    497             referenceEntry.time, 0);
    498           diffStatus(
    499             td(tr, percent(referenceEntry.timePercent), 'value time'),
    500             referenceEntry.timePercent, 0);
    501           diffStatus(
    502             td(tr, count(referenceEntry.count), 'value count'),
    503             referenceEntry.count, 0);
    504         } else {
    505           // Display empty entry / baseline entry
    506           var showBaselineEntry = entry !== undefined;
    507           if (showBaselineEntry) {
    508             if (!parentEntry) {
    509               var node = td(tr, '<div class="toggle"></div>', 'position');
    510               node.firstChild.addEventListener('click', handleToggleGroup);
    511             } else {
    512               td(tr, entry.position == 0 ? '' : entry.position, 'position');
    513             }
    514             td(tr, entry.name, 'name');
    515             td(tr, ms(entry.time, false), 'value time');
    516             td(tr, percent(entry.timePercent, false), 'value time');
    517             td(tr, count(entry.count, false), 'value count');
    518           } else {
    519             td(tr, '-', 'position');
    520             td(tr, referenceEntry.name, 'name');
    521             td(tr, '-', 'value time');
    522             td(tr, '-', 'value time');
    523             td(tr, '-', 'value count');
    524           }
    525         }
    526       });
    527       table.replaceChild(tbody, oldTbody);
    528       var versionSelect = column.querySelector('select.version');
    529       selectOption(versionSelect, (index, option) => {
    530         return option.version == page.version
    531       });
    532     }
    533 
    534     function showEntry(entry) {
    535       selectEntry(entry, true);
    536     }
    537 
    538     function selectEntry(entry, updateSelectedPage) {
    539       var needsPageSwitch = true;
    540       if (updateSelectedPage && selectedPage) {
    541         entry = selectedPage.version.getEntry(entry);
    542         needsPageSwitch = updateSelectedPage && entry.page != selectedPage;
    543       }
    544       var rowIndex = 0;
    545       // If clicked in the detail row change the first column to that page.
    546       if (needsPageSwitch) showPage(entry.page);
    547       var childNodes = $('column_0').querySelector('.list tbody').childNodes;
    548       for (var i = 0; i < childNodes.length; i++) {
    549         if (childNodes[i].entry !== undefined &&
    550             childNodes[i].entry.name == entry.name) {
    551           rowIndex = i;
    552           break;
    553         }
    554       }
    555       var firstEntry = childNodes[rowIndex].entry;
    556       if (rowIndex) {
    557         if (firstEntry.parent) showGroup(firstEntry.parent);
    558       }
    559       // Deselect all
    560       $('view').querySelectorAll('.list tbody tr').forEach((tr) => {
    561         toggleCssClass(tr, 'selected', false);
    562       });
    563       // Select the entry row
    564       $('view').querySelectorAll("tbody").forEach((body) => {
    565         var row = body.childNodes[rowIndex];
    566         if (!row) return;
    567         toggleCssClass(row, 'selected', row.entry && row.entry.name ==
    568           firstEntry.name);
    569       });
    570       if (updateSelectedPage && selectedEntry) {
    571         entry = selectedEntry.page.version.getEntry(entry);
    572       }
    573       if (entry !== selectedEntry) {
    574         selectedEntry = entry;
    575         showEntryDetail(entry);
    576       }
    577     }
    578 
    579     function showEntryDetail(entry) {
    580       showVersionDetails(entry);
    581       showPageDetails(entry);
    582       showImpactList(entry.page);
    583       showGraphs(entry.page);
    584       pushHistoryState();
    585     }
    586 
    587     function showVersionDetails(entry) {
    588       var table, tbody, entries;
    589       table = $('detailView').querySelector('.versionDetailTable');
    590       tbody = document.createElement('tbody');
    591       if (entry !== undefined) {
    592         $('detailView').querySelector('.versionDetail h3 span').textContent =
    593           entry.name + ' in ' + entry.page.name;
    594         entries = versions.getPageVersions(entry.page).map(
    595           (page) => {
    596             return page.get(entry.name)
    597           });
    598         entries.sort((a, b) => {
    599           return a.time - b.time
    600         });
    601         entries.forEach((pageEntry) => {
    602           if (pageEntry === undefined) return;
    603           var tr = document.createElement('tr');
    604           if (pageEntry == entry) tr.className += 'selected';
    605           tr.entry = pageEntry;
    606           var isBaselineEntry = pageEntry.page.version == baselineVersion;
    607           td(tr, pageEntry.page.version.name, 'version');
    608           td(tr, ms(pageEntry.time, !isBaselineEntry), 'value time');
    609           td(tr, percent(pageEntry.timePercent, !isBaselineEntry), 'value time');
    610           td(tr, count(pageEntry.count, !isBaselineEntry), 'value count');
    611           tbody.appendChild(tr);
    612         });
    613       }
    614       table.replaceChild(tbody, table.querySelector('tbody'));
    615     }
    616 
    617     function showPageDetails(entry) {
    618       var table, tbody, entries;
    619       table = $('detailView').querySelector('.pageDetailTable');
    620       tbody = document.createElement('tbody');
    621       if (entry === undefined) {
    622         table.replaceChild(tbody, table.querySelector('tbody'));
    623         return;
    624       }
    625       var version = entry.page.version;
    626       var showDiff = version !== baselineVersion;
    627       $('detailView').querySelector('.pageDetail h3 span').textContent =
    628         version.name;
    629       entries = version.pages.map((page) => {
    630           if (!page.enabled) return;
    631           return page.get(entry.name)
    632         });
    633       entries.sort((a, b) => {
    634         var cmp = b.timePercent - a.timePercent;
    635         if (cmp.toFixed(1) == 0) return b.time - a.time;
    636         return cmp
    637       });
    638       entries.forEach((pageEntry) => {
    639         if (pageEntry === undefined) return;
    640         var tr = document.createElement('tr');
    641         if (pageEntry === entry) tr.className += 'selected';
    642         tr.entry = pageEntry;
    643         td(tr, pageEntry.page.name, 'name');
    644         td(tr, ms(pageEntry.time, showDiff), 'value time');
    645         td(tr, percent(pageEntry.timePercent, showDiff), 'value time');
    646         td(tr, percent(pageEntry.timePercentPerEntry, showDiff),
    647             'value time hideNoDiff');
    648         td(tr, count(pageEntry.count, showDiff), 'value count');
    649         tbody.appendChild(tr);
    650       });
    651       // show the total for all pages
    652       var tds = table.querySelectorAll('tfoot td');
    653       tds[1].textContent = ms(entry.getTimeImpact(), showDiff);
    654       // Only show the percentage total if we are in diff mode:
    655       tds[2].textContent = percent(entry.getTimePercentImpact(), showDiff);
    656       tds[3].textContent = '';
    657       tds[4].textContent = count(entry.getCountImpact(), showDiff);
    658       table.replaceChild(tbody, table.querySelector('tbody'));
    659     }
    660 
    661     function showImpactList(page) {
    662       var impactView = $('detailView').querySelector('.impactView');
    663       impactView.querySelector('h3 span').textContent = page.version.name;
    664 
    665       var table = impactView.querySelector('table');
    666       var tbody = document.createElement('tbody');
    667       var version = page.version;
    668       var entries = version.allEntries();
    669       if (selectedEntry !== undefined && selectedEntry.isGroup) {
    670         impactView.querySelector('h3 span').textContent += " " + selectedEntry.name;
    671         entries = entries.filter((entry) => {
    672           return entry.name == selectedEntry.name ||
    673             (entry.parent && entry.parent.name == selectedEntry.name)
    674         });
    675       }
    676       var isCompareView = baselineVersion !== undefined;
    677       entries = entries.filter((entry) => {
    678         if (isCompareView) {
    679           var impact = entry.getTimeImpact();
    680           return impact < -1 || 1 < impact
    681         }
    682         return entry.getTimePercentImpact() > 0.01;
    683       });
    684       entries = entries.slice(0, 50);
    685       entries.sort((a, b) => {
    686         var cmp = b.getTimePercentImpact() - a.getTimePercentImpact();
    687         if (isCompareView || cmp.toFixed(1) == 0) {
    688           return b.getTimeImpact() - a.getTimeImpact();
    689         }
    690         return cmp
    691       });
    692       entries.forEach((entry) => {
    693         var tr = document.createElement('tr');
    694         tr.entry = entry;
    695         td(tr, entry.name, 'name');
    696         td(tr, ms(entry.getTimeImpact()), 'value time');
    697         var percentImpact = entry.getTimePercentImpact();
    698         td(tr, percentImpact > 1000 ? '-' : percent(percentImpact), 'value time');
    699         var topPages = entry.getPagesByPercentImpact().slice(0, 3)
    700           .map((each) => {
    701             return each.name + ' (' + percent(each.getEntry(entry).timePercent) +
    702               ')'
    703           });
    704         td(tr, topPages.join(', '), 'name');
    705         tbody.appendChild(tr);
    706       });
    707       table.replaceChild(tbody, table.querySelector('tbody'));
    708     }
    709 
    710     function showGraphs(page) {
    711       var groups = page.groups.filter(each => each.enabled);
    712       // Sort groups by the biggest impact
    713       groups.sort((a, b) => {
    714         return b.getTimeImpact() - a.getTimeImpact();
    715       });
    716       if (selectedGroup == undefined) {
    717         selectedGroup = groups[0];
    718       } else {
    719         groups = groups.filter(each => each.name != selectedGroup.name);
    720         groups.unshift(selectedGroup);
    721       }
    722       showPageGraph(groups, page);
    723       showVersionGraph(groups, page);
    724       showPageVersionGraph(groups, page);
    725     }
    726 
    727     function getGraphDataTable(groups) {
    728       var dataTable = new google.visualization.DataTable();
    729       dataTable.addColumn('string', 'Name');
    730       groups.forEach(group => {
    731         var column = dataTable.addColumn('number', group.name.substring(6));
    732         dataTable.setColumnProperty(column, 'group', group);
    733       });
    734       return dataTable;
    735     }
    736 
    737     var selectedGroup;
    738     function showPageGraph(groups, page) {
    739       var isDiffView = baselineVersion !== undefined;
    740       var dataTable = getGraphDataTable(groups);
    741       // Calculate the average row
    742       var row = ['Average'];
    743       groups.forEach((group) => {
    744         if (isDiffView) {
    745           row.push(group.isTotal ? 0 : group.getAverageTimeImpact());
    746         } else {
    747           row.push(group.isTotal ? 0 : group.getTimeImpact());
    748         }
    749       });
    750       dataTable.addRow(row);
    751       // Sort the pages by the selected group.
    752       var pages = page.version.pages.filter(page => page.enabled);
    753       function sumDiff(page) {
    754         var sum = 0;
    755         groups.forEach(group => {
    756           var value = group.getTimePercentImpact() -
    757             page.getEntry(group).timePercent;
    758           sum += value * value;
    759         });
    760         return sum;
    761       }
    762       if (isDiffView) {
    763         pages.sort((a, b) => {
    764           return b.getEntry(selectedGroup).time-
    765             a.getEntry(selectedGroup).time;
    766         });
    767       } else {
    768         pages.sort((a, b) => {
    769           return b.getEntry(selectedGroup).timePercent -
    770             a.getEntry(selectedGroup).timePercent;
    771         });
    772       }
    773       // Sort by sum of squared distance to the average.
    774       // pages.sort((a, b) => {
    775       //   return a.distanceFromTotalPercent() - b.distanceFromTotalPercent();
    776       // });
    777       // Calculate the entries for the pages
    778       pages.forEach((page) => {
    779         row = [page.name];
    780         groups.forEach((group) => {
    781           row.push(group.isTotal ? 0 : page.getEntry(group).time);
    782         });
    783         var rowIndex = dataTable.addRow(row);
    784         dataTable.setRowProperty(rowIndex, 'page', page);
    785       });
    786       renderGraph('Pages for ' + page.version.name, groups, dataTable,
    787           'pageGraph', isDiffView ? true : 'percent');
    788     }
    789 
    790     function showVersionGraph(groups, page) {
    791       var dataTable = getGraphDataTable(groups);
    792       var row;
    793       var vs = versions.versions.filter(version => version.enabled);
    794       vs.sort((a, b) => {
    795         return b.getEntry(selectedGroup).getTimeImpact() -
    796           a.getEntry(selectedGroup).getTimeImpact();
    797       });
    798       // Calculate the entries for the versions
    799       vs.forEach((version) => {
    800         row = [version.name];
    801         groups.forEach((group) => {
    802           row.push(group.isTotal ? 0 : version.getEntry(group).getTimeImpact());
    803         });
    804         var rowIndex = dataTable.addRow(row);
    805         dataTable.setRowProperty(rowIndex, 'page', page);
    806       });
    807       renderGraph('Versions Total Time over all Pages', groups, dataTable,
    808           'versionGraph', true);
    809     }
    810 
    811     function showPageVersionGraph(groups, page) {
    812       var dataTable = getGraphDataTable(groups);
    813       var row;
    814       var vs = versions.getPageVersions(page);
    815       vs.sort((a, b) => {
    816         return b.getEntry(selectedGroup).time - a.getEntry(selectedGroup).time;
    817       });
    818       // Calculate the entries for the versions
    819       vs.forEach((page) => {
    820         row = [page.version.name];
    821         groups.forEach((group) => {
    822           row.push(group.isTotal ? 0 : page.getEntry(group).time);
    823         });
    824         var rowIndex = dataTable.addRow(row);
    825         dataTable.setRowProperty(rowIndex, 'page', page);
    826       });
    827       renderGraph('Versions for ' + page.name, groups, dataTable,
    828           'pageVersionGraph', true);
    829     }
    830 
    831     function renderGraph(title, groups, dataTable, id, isStacked) {
    832       var isDiffView = baselineVersion !== undefined;
    833       var formatter = new google.visualization.NumberFormat({
    834         suffix: (isDiffView ? 'ms' : 'ms'),
    835         negativeColor: 'red',
    836         groupingSymbol: "'"
    837       });
    838       for (var i = 1; i < dataTable.getNumberOfColumns(); i++) {
    839         formatter.format(dataTable, i);
    840       }
    841       var height = 85 + 28 * dataTable.getNumberOfRows();
    842       var options = {
    843         isStacked: isStacked,
    844         height: height,
    845         hAxis: {
    846           minValue: 0,
    847           textStyle: { fontSize: 14 }
    848         },
    849         animation:{
    850           duration: dataTable.getNumberOfRows() > 50 ? 0 : 500 ,
    851           easing: 'out',
    852         },
    853         vAxis: {
    854           textStyle: { fontSize: 14 }
    855         },
    856         tooltip: { textStyle: { fontSize: 14 }},
    857         explorer: {
    858           actions: ['dragToZoom', 'rightClickToReset'],
    859           maxZoomIn: 0.01
    860         },
    861         legend: {position:'top', maxLines: 1, textStyle: { fontSize: 14 }},
    862         chartArea: {left:200, top:50, width:'98%', height:'80%'},
    863         colors: groups.map(each => each.color)
    864       };
    865       var parentNode = $(id);
    866       parentNode.querySelector('h2>span, h3>span').textContent = title;
    867       var graphNode = parentNode.querySelector('.content');
    868 
    869       var chart = graphNode.chart;
    870       if (chart === undefined) {
    871         chart = graphNode.chart = new google.visualization.BarChart(graphNode);
    872       } else {
    873         google.visualization.events.removeAllListeners(chart);
    874       }
    875       google.visualization.events.addListener(chart, 'select', selectHandler);
    876       function getChartEntry(selection) {
    877         if (!selection) return undefined;
    878         var column = selection.column;
    879         if (column == undefined) return undefined;
    880         var selectedGroup = dataTable.getColumnProperty(column, 'group');
    881         var row = selection.row;
    882         if (row == null) return selectedGroup;
    883         var page = dataTable.getRowProperty(row, 'page');
    884         if (!page) return selectedGroup;
    885         return page.getEntry(selectedGroup);
    886       }
    887       function selectHandler() {
    888         selectedGroup = getChartEntry(chart.getSelection()[0])
    889         if (!selectedGroup) return;
    890         selectEntry(selectedGroup, true);
    891       }
    892 
    893       // Make our global tooltips work
    894       google.visualization.events.addListener(chart, 'onmouseover', mouseOverHandler);
    895       function mouseOverHandler(selection) {
    896         graphNode.entry = getChartEntry(selection);
    897       }
    898       chart.draw(dataTable, options);
    899     }
    900 
    901     function showGroup(entry) {
    902       toggleGroup(entry, true);
    903     }
    904 
    905     function toggleGroup(group, show) {
    906       $('view').querySelectorAll(".child").forEach((tr) => {
    907         var entry = tr.parentEntry;
    908         if (!entry) return;
    909         if (entry.name !== group.name) return;
    910         toggleCssClass(tr, 'visible', show);
    911       });
    912     }
    913 
    914     function showPopover(entry) {
    915       var popover = $('popover');
    916       popover.querySelector('td.name').textContent = entry.name;
    917       popover.querySelector('td.page').textContent = entry.page.name;
    918       setPopoverDetail(popover, entry, '');
    919       popover.querySelector('table').className = "";
    920       if (baselineVersion !== undefined) {
    921         entry = baselineVersion.getEntry(entry);
    922         setPopoverDetail(popover, entry, '.compare');
    923         popover.querySelector('table').className = "compare";
    924       }
    925     }
    926 
    927     function setPopoverDetail(popover, entry, prefix) {
    928       var node = (name) => popover.querySelector(prefix + name);
    929       if (entry == undefined) {
    930         node('.version').textContent = baselineVersion.name;
    931         node('.time').textContent = '-';
    932         node('.timeVariance').textContent = '-';
    933         node('.percent').textContent = '-';
    934         node('.percentPerEntry').textContent = '-';
    935         node('.percentVariance').textContent  = '-';
    936         node('.count').textContent =  '-';
    937         node('.countVariance').textContent = '-';
    938         node('.timeImpact').textContent = '-';
    939         node('.timePercentImpact').textContent = '-';
    940       } else {
    941         node('.version').textContent = entry.page.version.name;
    942         node('.time').textContent = ms(entry._time, false);
    943         node('.timeVariance').textContent
    944             = percent(entry.timeVariancePercent, false);
    945         node('.percent').textContent = percent(entry.timePercent, false);
    946         node('.percentPerEntry').textContent
    947             = percent(entry.timePercentPerEntry, false);
    948         node('.percentVariance').textContent
    949             = percent(entry.timePercentVariancePercent, false);
    950         node('.count').textContent = count(entry._count, false);
    951         node('.countVariance').textContent
    952             = percent(entry.timeVariancePercent, false);
    953         node('.timeImpact').textContent
    954             = ms(entry.getTimeImpact(false), false);
    955         node('.timePercentImpact').textContent
    956             = percent(entry.getTimeImpactVariancePercent(false), false);
    957       }
    958     }
    959   </script>
    960   <script type="text/javascript">
    961   "use strict"
    962     // =========================================================================
    963     // Helpers
    964     function $(id) {
    965       return document.getElementById(id)
    966     }
    967 
    968     function removeAllChildren(node) {
    969       while (node.firstChild) {
    970         node.removeChild(node.firstChild);
    971       }
    972     }
    973 
    974     function selectOption(select, match) {
    975       var options = select.options;
    976       for (var i = 0; i < options.length; i++) {
    977         if (match(i, options[i])) {
    978           select.selectedIndex = i;
    979           return;
    980         }
    981       }
    982     }
    983 
    984     function addCodeSearchButton(entry, node) {
    985       if (entry.isGroup) return;
    986       var button = document.createElement("div");
    987       button.textContent = '?'
    988       button.className = "codeSearch"
    989       button.addEventListener('click', handleCodeSearch);
    990       node.appendChild(button);
    991       return node;
    992     }
    993 
    994     function td(tr, content, className) {
    995       var td = document.createElement("td");
    996       if (content[0] == '<') {
    997         td.innerHTML = content;
    998       } else {
    999         td.textContent = content;
   1000       }
   1001       td.className = className
   1002       tr.appendChild(td);
   1003       return td
   1004     }
   1005 
   1006     function nodeIndex(node) {
   1007       var children = node.parentNode.childNodes,
   1008         i = 0;
   1009       for (; i < children.length; i++) {
   1010         if (children[i] == node) {
   1011           return i;
   1012         }
   1013       }
   1014       return -1;
   1015     }
   1016 
   1017     function toggleCssClass(node, cssClass, toggleState) {
   1018       var index = -1;
   1019       var classes;
   1020       if (node.className != undefined) {
   1021         classes = node.className.split(' ');
   1022         index = classes.indexOf(cssClass);
   1023       }
   1024       if (index == -1) {
   1025         if (toggleState === false) return;
   1026         node.className += ' ' + cssClass;
   1027         return;
   1028       }
   1029       if (toggleState === true) return;
   1030       classes.splice(index, 1);
   1031       node.className = classes.join(' ');
   1032     }
   1033 
   1034     function NameComparator(a, b) {
   1035       if (a.name > b.name) return 1;
   1036       if (a.name < b.name) return -1;
   1037       return 0
   1038     }
   1039 
   1040     function diffSign(value, digits, unit, showDiff) {
   1041       if (showDiff === false || baselineVersion == undefined) {
   1042         if (value === undefined) return '';
   1043         return value.toFixed(digits) + unit;
   1044       }
   1045       return (value >= 0 ? '+' : '') + value.toFixed(digits) + unit + '';
   1046     }
   1047 
   1048     function ms(value, showDiff) {
   1049       return diffSign(value, 1, 'ms', showDiff);
   1050     }
   1051 
   1052     function count(value, showDiff) {
   1053       return diffSign(value, 0, '#', showDiff);
   1054     }
   1055 
   1056     function percent(value, showDiff) {
   1057       return diffSign(value, 1, '%', showDiff);
   1058     }
   1059 
   1060   </script>
   1061   <script type="text/javascript">
   1062   "use strict"
   1063     // =========================================================================
   1064     // EventHandlers
   1065     function handleBodyLoad() {
   1066       $('uploadInput').focus();
   1067       if (defaultData) {
   1068         handleLoadJSON(defaultData);
   1069       } else if (window.location.protocol !== 'file:') {
   1070         tryLoadDefaultResults();
   1071       }
   1072     }
   1073 
   1074     function tryLoadDefaultResults() {
   1075      // Try to load a results.json file adjacent to this day.
   1076      var xhr = new XMLHttpRequest();
   1077      // The markers on the following line can be used to replace the url easily
   1078      // with scripts.
   1079      xhr.open('GET', /*results-url-start*/'results.json'/*results-url-end*/, true);
   1080      xhr.onreadystatechange = function(e) {
   1081        if(this.readyState !== XMLHttpRequest.DONE || this.status !== 200) return;
   1082        handleLoadText(this.responseText);
   1083      };
   1084      xhr.send();
   1085     }
   1086 
   1087     function handleAppendFile() {
   1088       var files = document.getElementById("appendInput").files;
   1089       loadFiles(files, true);
   1090     }
   1091 
   1092     function handleLoadFile() {
   1093       var files = document.getElementById("uploadInput").files;
   1094       loadFiles(files, false)
   1095     }
   1096 
   1097     function loadFiles(files, append) {
   1098       var file = files[0];
   1099       var reader = new FileReader();
   1100 
   1101       reader.onload = function(evt) {
   1102         handleLoadText(this.result, append);
   1103       }
   1104       reader.readAsText(file);
   1105     }
   1106 
   1107     function handleLoadText(text, append) {
   1108       handleLoadJSON(JSON.parse(text), append);
   1109     }
   1110 
   1111     function getStateFromParams() {
   1112       var query = window.location.search.substr(1);
   1113       var result = {};
   1114       query.split("&").forEach((part) => {
   1115         var item = part.split("=");
   1116         var key = decodeURIComponent(item[0])
   1117         result[key] = decodeURIComponent(item[1]);
   1118       });
   1119       return result;
   1120     }
   1121 
   1122     function handleLoadJSON(json, append) {
   1123       let isFirstLoad = pages === undefined;
   1124       json = fixClusterTelemetryResults(json);
   1125       json = fixSinglePageJSON(json);
   1126       if (append && !isFirstLoad) {
   1127         json = createUniqueVersions(json)
   1128       }
   1129       var state = getStateFromParams();
   1130       if (!append || isFirstLoad) {
   1131         pages = new Pages();
   1132         versions = Versions.fromJSON(json);
   1133       } else {
   1134         Versions.fromJSON(json).forEach(e => versions.add(e))
   1135       }
   1136       initialize()
   1137       if (isFirstLoad && !popHistoryState(state)) {
   1138         showEntry(selectedPage.total);
   1139       }
   1140     }
   1141 
   1142     function fixClusterTelemetryResults(json) {
   1143       // Convert CT results to callstats compatible JSON
   1144       // Input:
   1145       // { PATH: { "pairs": { METRIC: { "count": XX, "time": XX }.. }}.. }
   1146       let firstEntry;
   1147       for (let key in json) {
   1148         firstEntry = json[key];
   1149         break;
   1150       }
   1151       // Return the original JSON if it is not a CT result.
   1152       if (firstEntry.pairs === undefined) return json;
   1153       // The results include already the group totals, remove them by filtering.
   1154       let groupNames = new Set(Array.from(Group.groups.values()).map(e => e.name));
   1155       let result = Object.create(null);
   1156       for (let file_name in json) {
   1157         let entries = [];
   1158         let file_data = json[file_name].pairs;
   1159         for (let name in file_data) {
   1160           if(name != "Total" && groupNames.has(name)) continue;
   1161           let entry = file_data[name];
   1162           let count = entry.count;
   1163           let time = entry.time;
   1164           entries.push([name, time, 0, 0, count, 0, 0]);
   1165         }
   1166         let domain = file_name.split("/").slice(-1)[0];
   1167         result[domain] = entries;
   1168       }
   1169       return {__proto__:null, ClusterTelemetry: result};
   1170     }
   1171 
   1172     function fixSinglePageJSON(json) {
   1173       // Try to detect the single-version case, where we're missing the toplevel
   1174       // version object. The incoming JSON is of the form:
   1175       //    {"Page 1": [... data points ... ], "Page 2": [...], ...}
   1176       // Instead of the default multi-page JSON:
   1177       //    {"Version 1": { "Page 1": ..., ...}, "Version 2": {...}, ...}
   1178       // In this case insert a single "Default" version as top-level entry.
   1179       var firstProperty = (object) => {
   1180         for (var key in object) return key;
   1181       };
   1182       var maybePage = json[firstProperty(json)];
   1183       if (!Array.isArray(maybePage)) return json;
   1184       return {"Default": json}
   1185     }
   1186 
   1187     var appendIndex = 0;
   1188     function createUniqueVersions(json) {
   1189       // Make sure all toplevel entries are unique namaes and added properly
   1190       appendIndex++;
   1191       let result = {__proto__:null}
   1192       for (let key in json) {
   1193         result[key+"_"+appendIndex] = json[key];
   1194       }
   1195       return result
   1196     }
   1197 
   1198     function handleToggleGroup(event) {
   1199       var group = event.target.parentNode.parentNode.entry;
   1200       toggleGroup(selectedPage.get(group.name));
   1201     }
   1202 
   1203     function handleSelectPage(select, event) {
   1204       var option = select.options[select.selectedIndex];
   1205       if (select.id == "select_0") {
   1206         showSelectedEntryInPage(option.page);
   1207       } else {
   1208         var columnIndex = select.id.split('_')[1];
   1209         showPageInColumn(option.page, columnIndex);
   1210       }
   1211     }
   1212 
   1213     function handleSelectVersion(select, event) {
   1214       var option = select.options[select.selectedIndex];
   1215       var version = option.version;
   1216       if (select.id == "selectVersion_0") {
   1217         var page = version.get(selectedPage.name);
   1218         showSelectedEntryInPage(page);
   1219       } else {
   1220         var columnIndex = select.id.split('_')[1];
   1221         var pageSelect = $('select_' + columnIndex);
   1222         var page = pageSelect.options[pageSelect.selectedIndex].page;
   1223         page = version.get(page.name);
   1224         showPageInColumn(page, columnIndex);
   1225       }
   1226     }
   1227 
   1228     function handleSelectDetailRow(table, event) {
   1229       if (event.target.tagName != 'TD') return;
   1230       var tr = event.target.parentNode;
   1231       if (tr.tagName != 'TR') return;
   1232       if (tr.entry === undefined) return;
   1233       selectEntry(tr.entry, true);
   1234     }
   1235 
   1236     function handleSelectRow(table, event, fromDetail) {
   1237       if (event.target.tagName != 'TD') return;
   1238       var tr = event.target.parentNode;
   1239       if (tr.tagName != 'TR') return;
   1240       if (tr.entry === undefined) return;
   1241       selectEntry(tr.entry, false);
   1242     }
   1243 
   1244     function handleSelectBaseline(select, event) {
   1245       var option = select.options[select.selectedIndex];
   1246       baselineVersion = option.version;
   1247       var showingDiff = baselineVersion !== undefined;
   1248       var body = $('body');
   1249       toggleCssClass(body, 'diff', showingDiff);
   1250       toggleCssClass(body, 'noDiff', !showingDiff);
   1251       showPage(selectedPage);
   1252       if (selectedEntry === undefined) return;
   1253       selectEntry(selectedEntry, true);
   1254     }
   1255 
   1256     function findEntry(event) {
   1257       var target = event.target;
   1258       while (target.entry === undefined) {
   1259         target = target.parentNode;
   1260         if (!target) return undefined;
   1261       }
   1262       return target.entry;
   1263     }
   1264 
   1265     function handleUpdatePopover(event) {
   1266       var popover = $('popover');
   1267       popover.style.left = event.pageX + 'px';
   1268       popover.style.top = event.pageY + 'px';
   1269       popover.style.display = 'none';
   1270       popover.style.display = event.shiftKey ? 'block' : 'none';
   1271       var entry = findEntry(event);
   1272       if (entry === undefined) return;
   1273       showPopover(entry);
   1274     }
   1275 
   1276     function handleToggleVersionOrPageEnable(event) {
   1277       var item = this.item ;
   1278       if (item  === undefined) return;
   1279       item .enabled = this.checked;
   1280       initialize();
   1281       var page = selectedPage;
   1282       if (page === undefined || !page.version.enabled) {
   1283         page = versions.getEnabledPage(page.name);
   1284       }
   1285       if (!page.enabled) {
   1286         page = page.getNextPage();
   1287       }
   1288       showPage(page);
   1289     }
   1290 
   1291     function handleToggleContentVisibility(event) {
   1292       var content = event.target.contentNode;
   1293       toggleCssClass(content, 'hidden');
   1294     }
   1295 
   1296     function handleCodeSearch(event) {
   1297       var entry = findEntry(event);
   1298       if (entry === undefined) return;
   1299       var url = "https://cs.chromium.org/search/?sq=package:chromium&type=cs&q=";
   1300       name = entry.name;
   1301       if (name.startsWith("API_")) {
   1302         name = name.substring(4);
   1303       }
   1304       url += encodeURIComponent(name) + "+file:src/v8/src";
   1305       window.open(url,'_blank');
   1306     }
   1307   </script>
   1308   <script type="text/javascript">
   1309   "use strict"
   1310     // =========================================================================
   1311     class Versions {
   1312       constructor() {
   1313         this.versions = [];
   1314       }
   1315       add(version) {
   1316         this.versions.push(version)
   1317       }
   1318       getPageVersions(page) {
   1319         var result = [];
   1320         this.versions.forEach((version) => {
   1321           if (!version.enabled) return;
   1322           var versionPage = version.get(page.name);
   1323           if (versionPage  !== undefined) result.push(versionPage);
   1324         });
   1325         return result;
   1326       }
   1327       get length() {
   1328         return this.versions.length
   1329       }
   1330       get(index) {
   1331         return this.versions[index]
   1332       }
   1333       getByName(name) {
   1334         return this.versions.find((each) => each.name == name);
   1335       }
   1336       forEach(f) {
   1337         this.versions.forEach(f);
   1338       }
   1339       sort() {
   1340         this.versions.sort(NameComparator);
   1341       }
   1342       getEnabledPage(name) {
   1343         for (var i = 0; i < this.versions.length; i++) {
   1344           var version = this.versions[i];
   1345           if (!version.enabled) continue;
   1346           var page = version.get(name);
   1347           if (page !== undefined) return page;
   1348         }
   1349       }
   1350     }
   1351     Versions.fromJSON = function(json) {
   1352       var versions = new Versions();
   1353       for (var version in json) {
   1354         versions.add(Version.fromJSON(version, json[version]));
   1355       }
   1356       versions.sort();
   1357       return versions;
   1358     }
   1359 
   1360     class Version {
   1361       constructor(name) {
   1362         this.name = name;
   1363         this.enabled = true;
   1364         this.pages = [];
   1365       }
   1366       add(page) {
   1367         this.pages.push(page);
   1368       }
   1369       indexOf(name) {
   1370         for (var i = 0; i < this.pages.length; i++) {
   1371           if (this.pages[i].name == name) return i;
   1372         }
   1373         return -1;
   1374       }
   1375       getNextPage(page) {
   1376         if (this.length == 0) return undefined;
   1377         return this.pages[(this.indexOf(page.name) + 1) % this.length];
   1378       }
   1379       get(name) {
   1380         var index = this.indexOf(name);
   1381         if (0 <= index) return this.pages[index];
   1382         return undefined
   1383       }
   1384       get length() {
   1385         return this.pages.length
   1386       }
   1387       getEntry(entry) {
   1388         if (entry === undefined) return undefined;
   1389         var page = this.get(entry.page.name);
   1390         if (page === undefined) return undefined;
   1391         return page.get(entry.name);
   1392       }
   1393       forEachEntry(fun) {
   1394         this.forEachPage((page) => {
   1395           page.forEach(fun);
   1396         });
   1397       }
   1398       forEachPage(fun) {
   1399         this.pages.forEach((page) => {
   1400           if (!page.enabled) return;
   1401           fun(page);
   1402         })
   1403       }
   1404       allEntries() {
   1405         var map = new Map();
   1406         this.forEachEntry((group, entry) => {
   1407           if (!map.has(entry.name)) map.set(entry.name, entry);
   1408         });
   1409         return Array.from(map.values());
   1410       }
   1411       getTotalValue(name, property) {
   1412         if (name === undefined) name = this.pages[0].total.name;
   1413         var sum = 0;
   1414         this.forEachPage((page) => {
   1415           var entry = page.get(name);
   1416           if (entry !== undefined) sum += entry[property];
   1417         });
   1418         return sum;
   1419       }
   1420       getTotalTime(name, showDiff) {
   1421         return this.getTotalValue(name, showDiff === false ? '_time' : 'time');
   1422       }
   1423       getTotalTimePercent(name, showDiff) {
   1424         if (baselineVersion === undefined || showDiff === false) {
   1425           // Return the overall average percent of the given entry name.
   1426           return this.getTotalValue(name, 'time') /
   1427             this.getTotalTime('Group-Total') * 100;
   1428         }
   1429         // Otherwise return the difference to the sum of the baseline version.
   1430         var baselineValue = baselineVersion.getTotalTime(name, false);
   1431         var total = this.getTotalValue(name, '_time');
   1432         return (total / baselineValue - 1)  * 100;
   1433       }
   1434       getTotalTimeVariance(name, showDiff) {
   1435         // Calculate the overall error for a given entry name
   1436         var sum = 0;
   1437         this.forEachPage((page) => {
   1438           var entry = page.get(name);
   1439           if (entry === undefined) return;
   1440           sum += entry.timeVariance * entry.timeVariance;
   1441         });
   1442         return Math.sqrt(sum);
   1443       }
   1444       getTotalTimeVariancePercent(name, showDiff) {
   1445         return this.getTotalTimeVariance(name, showDiff) /
   1446           this.getTotalTime(name, showDiff) * 100;
   1447       }
   1448       getTotalCount(name, showDiff) {
   1449         return this.getTotalValue(name, showDiff === false ? '_count' : 'count');
   1450       }
   1451       getAverageTimeImpact(name, showDiff) {
   1452         return this.getTotalTime(name, showDiff) / this.pages.length;
   1453       }
   1454       getPagesByPercentImpact(name) {
   1455         var sortedPages =
   1456           this.pages.filter((each) => {
   1457             return each.get(name) !== undefined
   1458           });
   1459         sortedPages.sort((a, b) => {
   1460           return b.get(name).timePercent - a.get(name).timePercent;
   1461         });
   1462         return sortedPages;
   1463       }
   1464       sort() {
   1465         this.pages.sort(NameComparator)
   1466       }
   1467     }
   1468     Version.fromJSON = function(name, data) {
   1469       var version = new Version(name);
   1470       for (var pageName in data) {
   1471         version.add(PageVersion.fromJSON(version, pageName, data[pageName]));
   1472       }
   1473       version.sort();
   1474       return version;
   1475     }
   1476 
   1477     class Pages extends Map {
   1478       get(name) {
   1479         if (name.indexOf('www.') == 0) {
   1480           name = name.substring(4);
   1481         }
   1482         if (!this.has(name)) {
   1483           this.set(name, new Page(name));
   1484         }
   1485         return super.get(name);
   1486       }
   1487     }
   1488 
   1489     class Page {
   1490       constructor(name) {
   1491         this.name = name;
   1492         this.enabled = true;
   1493         this.versions = [];
   1494       }
   1495       add(page) {
   1496         this.versions.push(page);
   1497       }
   1498     }
   1499 
   1500     class PageVersion {
   1501       constructor(version, page) {
   1502         this.page = page;
   1503         this.page.add(this);
   1504         this.total = Group.groups.get('total').entry();
   1505         this.total.isTotal = true;
   1506         this.unclassified = new UnclassifiedEntry(this)
   1507         this.groups = [
   1508           this.total,
   1509           Group.groups.get('ic').entry(),
   1510           Group.groups.get('optimize').entry(),
   1511           Group.groups.get('compile-background').entry(),
   1512           Group.groups.get('compile').entry(),
   1513           Group.groups.get('parse-background').entry(),
   1514           Group.groups.get('parse').entry(),
   1515           Group.groups.get('callback').entry(),
   1516           Group.groups.get('api').entry(),
   1517           Group.groups.get('gc').entry(),
   1518           Group.groups.get('javascript').entry(),
   1519           Group.groups.get('runtime').entry(),
   1520           this.unclassified
   1521         ];
   1522         this.entryDict = new Map();
   1523         this.groups.forEach((entry) => {
   1524           entry.page = this;
   1525           this.entryDict.set(entry.name, entry);
   1526         });
   1527         this.version = version;
   1528       }
   1529       toString() {
   1530         return this.version.name + ": " + this.name;
   1531       }
   1532       urlParams() {
   1533         return { version: this.version.name, page: this.name};
   1534       }
   1535       add(entry) {
   1536         // Ignore accidentally added Group entries.
   1537         if (entry.name.startsWith(GroupedEntry.prefix)) return;
   1538         entry.page = this;
   1539         this.entryDict.set(entry.name, entry);
   1540         var added = false;
   1541         this.groups.forEach((group) => {
   1542           if (!added) added = group.add(entry);
   1543         });
   1544         if (added) return;
   1545         this.unclassified.push(entry);
   1546       }
   1547       get(name) {
   1548         return this.entryDict.get(name)
   1549       }
   1550       getEntry(entry) {
   1551         if (entry === undefined) return undefined;
   1552         return this.get(entry.name);
   1553       }
   1554       get length() {
   1555         return this.versions.length
   1556       }
   1557       get name() { return this.page.name }
   1558       get enabled() { return this.page.enabled }
   1559       forEachSorted(referencePage, func) {
   1560         // Iterate over all the entries in the order they appear on the
   1561         // reference page.
   1562         referencePage.forEach((parent, referenceEntry) => {
   1563           var entry;
   1564           if (parent) parent = this.entryDict.get(parent.name);
   1565           if (referenceEntry) entry = this.entryDict.get(referenceEntry.name);
   1566           func(parent, entry, referenceEntry);
   1567         });
   1568       }
   1569       forEach(fun) {
   1570         this.forEachGroup((group) => {
   1571           fun(undefined, group);
   1572           group.forEach((entry) => {
   1573             fun(group, entry)
   1574           });
   1575         });
   1576       }
   1577       forEachGroup(fun) {
   1578         this.groups.forEach(fun)
   1579       }
   1580       sort() {
   1581         this.groups.sort((a, b) => {
   1582           return b.time - a.time;
   1583         });
   1584         this.groups.forEach((group) => {
   1585           group.sort()
   1586         });
   1587       }
   1588       distanceFromTotalPercent() {
   1589         var sum = 0;
   1590         this.groups.forEach(group => {
   1591           if (group == this.total) return;
   1592           var value = group.getTimePercentImpact() -
   1593               this.getEntry(group).timePercent;
   1594           sum += value * value;
   1595         });
   1596         return sum;
   1597       }
   1598       getNextPage() {
   1599         return this.version.getNextPage(this);
   1600       }
   1601     }
   1602     PageVersion.fromJSON = function(version, name, data) {
   1603       var page = new PageVersion(version, pages.get(name));
   1604       for (var i = 0; i < data.length; i++) {
   1605         page.add(Entry.fromJSON(i, data[data.length - i - 1]));
   1606       }
   1607       page.sort();
   1608       return page
   1609     }
   1610 
   1611 
   1612     class Entry {
   1613       constructor(position, name, time, timeVariance, timeVariancePercent,
   1614         count,
   1615         countVariance, countVariancePercent) {
   1616         this.position = position;
   1617         this.name = name;
   1618         this._time = time;
   1619         this._timeVariance = timeVariance;
   1620         this._timeVariancePercent = timeVariancePercent;
   1621         this._count = count;
   1622         this.countVariance = countVariance;
   1623         this.countVariancePercent = countVariancePercent;
   1624         this.page = undefined;
   1625         this.parent = undefined;
   1626         this.isTotal = false;
   1627       }
   1628       urlParams() {
   1629         var params = this.page.urlParams();
   1630         params.entry = this.name;
   1631         return params;
   1632       }
   1633       getCompareWithBaseline(value, property) {
   1634         if (baselineVersion == undefined) return value;
   1635         var baselineEntry = baselineVersion.getEntry(this);
   1636         if (!baselineEntry) return value;
   1637         if (baselineVersion === this.page.version) return value;
   1638         return value - baselineEntry[property];
   1639       }
   1640       cssClass() {
   1641         return ''
   1642       }
   1643       get time() {
   1644         return this.getCompareWithBaseline(this._time, '_time');
   1645       }
   1646       get count() {
   1647         return this.getCompareWithBaseline(this._count, '_count');
   1648       }
   1649       get timePercent() {
   1650         var value = this._time / this.page.total._time * 100;
   1651         if (baselineVersion == undefined) return value;
   1652         var baselineEntry = baselineVersion.getEntry(this);
   1653         if (!baselineEntry) return value;
   1654         if (baselineVersion === this.page.version) return value;
   1655         return (this._time - baselineEntry._time) / this.page.total._time *
   1656           100;
   1657       }
   1658       get timePercentPerEntry() {
   1659         var value = this._time / this.page.total._time * 100;
   1660         if (baselineVersion == undefined) return value;
   1661         var baselineEntry = baselineVersion.getEntry(this);
   1662         if (!baselineEntry) return value;
   1663         if (baselineVersion === this.page.version) return value;
   1664         return (this._time / baselineEntry._time - 1) * 100;
   1665       }
   1666       get timePercentVariancePercent() {
   1667         // Get the absolute values for the percentages
   1668         return this.timeVariance / this.page.total._time * 100;
   1669       }
   1670       getTimeImpact(showDiff) {
   1671         return this.page.version.getTotalTime(this.name, showDiff);
   1672       }
   1673       getTimeImpactVariancePercent(showDiff) {
   1674         return this.page.version.getTotalTimeVariancePercent(this.name, showDiff);
   1675       }
   1676       getTimePercentImpact(showDiff) {
   1677         return this.page.version.getTotalTimePercent(this.name, showDiff);
   1678       }
   1679       getCountImpact(showDiff) {
   1680         return this.page.version.getTotalCount(this.name, showDiff);
   1681       }
   1682       getAverageTimeImpact(showDiff) {
   1683         return this.page.version.getAverageTimeImpact(this.name, showDiff);
   1684       }
   1685       getPagesByPercentImpact() {
   1686         return this.page.version.getPagesByPercentImpact(this.name);
   1687       }
   1688       get isGroup() {
   1689         return false
   1690       }
   1691       get timeVariance() {
   1692         return this._timeVariance
   1693       }
   1694       get timeVariancePercent() {
   1695         return this._timeVariancePercent
   1696       }
   1697     }
   1698     Entry.fromJSON = function(position, data) {
   1699       return new Entry(position, ...data);
   1700     }
   1701 
   1702     class Group {
   1703       constructor(name, regexp, color) {
   1704         this.name = name;
   1705         this.regexp = regexp;
   1706         this.color = color;
   1707         this.enabled = true;
   1708       }
   1709       entry() { return new GroupedEntry(this) };
   1710     }
   1711     Group.groups = new Map();
   1712     Group.add = function(name, group) {
   1713       this.groups.set(name, group);
   1714       return group;
   1715     }
   1716     Group.add('total', new Group('Total', /.*Total.*/, '#BBB'));
   1717     Group.add('ic', new Group('IC', /.*IC_.*/, "#3366CC"));
   1718     Group.add('optimize', new Group('Optimize',
   1719         /StackGuard|.*Optimize.*|.*Deoptimize.*|Recompile.*/, "#DC3912"));
   1720     Group.add('compile-background', new Group('Compile-Background',
   1721         /(.*CompileBackground.*)/, "#b9a720"));
   1722     Group.add('compile', new Group('Compile',
   1723         /(^Compile.*)|(.*_Compile.*)/, "#FFAA00"));
   1724     Group.add('parse-background',
   1725         new Group('Parse-Background', /.*ParseBackground.*/, "#af744d"));
   1726     Group.add('parse', new Group('Parse', /.*Parse.*/, "#FF6600"));
   1727     Group.add('callback', new Group('Blink C++', /.*Callback.*/, "#109618"));
   1728     Group.add('api', new Group('API', /.*API.*/, "#990099"));
   1729     Group.add('gc-custom', new Group('GC-Custom', /GC_Custom_.*/, "#0099C6"));
   1730     Group.add('gc-background',
   1731         new Group('GC-Background', /.*GC.*BACKGROUND.*/, "#00597c"));
   1732     Group.add('gc', new Group('GC', /GC_.*|AllocateInTargetSpace/, "#00799c"));
   1733     Group.add('javascript', new Group('JavaScript', /JS_Execution/, "#DD4477"));
   1734     Group.add('runtime', new Group('V8 C++', /.*/, "#88BB00"));
   1735     var group =
   1736       Group.add('unclassified', new Group('Unclassified', /.*/, "#000"));
   1737     group.enabled = false;
   1738 
   1739     class GroupedEntry extends Entry {
   1740       constructor(group) {
   1741         super(0, GroupedEntry.prefix + group.name, 0, 0, 0, 0, 0, 0);
   1742         this.group = group;
   1743         this.entries = [];
   1744         this.missingEntries = null;
   1745       }
   1746       get regexp() { return this.group.regexp }
   1747       get color() { return this.group.color }
   1748       get enabled() { return this.group.enabled }
   1749       add(entry) {
   1750         if (!this.regexp.test(entry.name)) return false;
   1751         this._time += entry.time;
   1752         this._count += entry.count;
   1753         // TODO: sum up variance
   1754         this.entries.push(entry);
   1755         entry.parent = this;
   1756         return true;
   1757       }
   1758       _initializeMissingEntries() {
   1759         var dummyEntryNames = new Set();
   1760         versions.forEach((version) => {
   1761           var groupEntry = version.getEntry(this);
   1762           if (groupEntry != this) {
   1763             for (var entry of groupEntry.entries) {
   1764               if (this.page.get(entry.name) == undefined) {
   1765                 dummyEntryNames.add(entry.name);
   1766               }
   1767             }
   1768           }
   1769         });
   1770         this.missingEntries  = [];
   1771         for (var name of dummyEntryNames) {
   1772           var tmpEntry = new Entry(0, name, 0, 0, 0, 0, 0, 0);
   1773           tmpEntry.page = this.page;
   1774           this.missingEntries.push(tmpEntry);
   1775         };
   1776       }
   1777 
   1778       forEach(fun) {
   1779         // Show also all entries which are in at least one version.
   1780         // Concatenate our real entries.
   1781         if (this.missingEntries == null) {
   1782           this._initializeMissingEntries();
   1783         }
   1784         var tmpEntries = this.missingEntries.concat(this.entries);
   1785 
   1786         // The compared entries are sorted by absolute impact.
   1787         tmpEntries.sort((a, b) => {
   1788           return b.time - a.time
   1789         });
   1790         tmpEntries.forEach(fun);
   1791       }
   1792       sort() {
   1793         this.entries.sort((a, b) => {
   1794           return b.time - a.time;
   1795         });
   1796       }
   1797       cssClass() {
   1798         if (this.page.total == this) return 'total';
   1799         return '';
   1800       }
   1801       get isGroup() {
   1802         return true
   1803       }
   1804       getVarianceForProperty(property) {
   1805         var sum = 0;
   1806         this.entries.forEach((entry) => {
   1807           sum += entry[property + 'Variance'] * entry[property +
   1808             'Variance'];
   1809         });
   1810         return Math.sqrt(sum);
   1811       }
   1812       get timeVariancePercent() {
   1813         if (this._time == 0) return 0;
   1814         return this.getVarianceForProperty('time')  / this._time * 100
   1815       }
   1816       get timeVariance() {
   1817         return this.getVarianceForProperty('time')
   1818       }
   1819     }
   1820     GroupedEntry.prefix = 'Group-';
   1821 
   1822     class UnclassifiedEntry extends GroupedEntry {
   1823       constructor(page) {
   1824         super(Group.groups.get('unclassified'));
   1825         this.page = page;
   1826         this._time = undefined;
   1827         this._count = undefined;
   1828       }
   1829       add(entry) {
   1830         this.entries.push(entry);
   1831         entry.parent = this;
   1832         return true;
   1833       }
   1834       forEachPageGroup(fun) {
   1835         this.page.forEachGroup((group) => {
   1836           if (group == this) return;
   1837           if (group == this.page.total) return;
   1838           fun(group);
   1839         });
   1840       }
   1841       get time() {
   1842         if (this._time === undefined) {
   1843           this._time = this.page.total._time;
   1844           this.forEachPageGroup((group) => {
   1845             this._time -= group._time;
   1846           });
   1847         }
   1848         return this.getCompareWithBaseline(this._time, '_time');
   1849       }
   1850       get count() {
   1851         if (this._count === undefined) {
   1852           this._count = this.page.total._count;
   1853           this.forEachPageGroup((group) => {
   1854             this._count -= group._count;
   1855           });
   1856         }
   1857         return this.getCompareWithBaseline(this._count, '_count');
   1858       }
   1859     }
   1860   </script>
   1861 </head>
   1862 
   1863 <body id="body" onmousemove="handleUpdatePopover(event)" onload="handleBodyLoad()" class="noDiff">
   1864   <h1>Runtime Stats Komparator</h1>
   1865 
   1866   <div id="results">
   1867     <div class="inline">
   1868       <h2>Data</h2>
   1869       <form name="fileForm">
   1870         <p>
   1871           <label for="uploadInput">Load File:</label>
   1872           <input id="uploadInput" type="file" name="files" onchange="handleLoadFile();" accept=".json">
   1873         </p>
   1874         <p>
   1875           <label for="appendInput">Append File:</label>
   1876           <input id="appendInput" type="file" name="files" onchange="handleAppendFile();" accept=".json">
   1877         </p>
   1878       </form>
   1879     </div>
   1880 
   1881     <div class="inline hidden">
   1882       <h2>Result</h2>
   1883       <div class="compareSelector inline">
   1884         Compare against:&nbsp;<select id="baseline" onchange="handleSelectBaseline(this, event)"></select><br/>
   1885         <span style="color: #060">Green</span> the selected version above performs
   1886         better on this measurement.
   1887       </div>
   1888     </div>
   1889 
   1890     <div id="versionSelector" class="inline toggleContentVisibility">
   1891       <h2>Versions</h2>
   1892       <div class="content hidden">
   1893         <ul></ul>
   1894       </div>
   1895     </div>
   1896 
   1897     <div id="pageSelector" class="inline toggleContentVisibility">
   1898       <h2>Pages</h2>
   1899       <div class="content hidden">
   1900         <ul></ul>
   1901       </div>
   1902     </div>
   1903 
   1904     <div id="groupSelector" class="inline toggleContentVisibility">
   1905       <h2>Groups</h2>
   1906       <div class="content hidden">
   1907         <ul></ul>
   1908       </div>
   1909     </div>
   1910 
   1911     <div id="view">
   1912     </div>
   1913 
   1914     <div id="detailView" class="hidden">
   1915       <div class="versionDetail inline toggleContentVisibility">
   1916         <h3><span></span></h3>
   1917         <div class="content">
   1918           <table class="versionDetailTable" onclick="handleSelectDetailRow(this, event);">
   1919             <thead>
   1920               <tr>
   1921                 <th class="version">Version&nbsp;</th>
   1922                 <th class="position">Pos.&nbsp;</th>
   1923                 <th class="value time">Time&nbsp;</th>
   1924                 <th class="value time">Percent&nbsp;</th>
   1925                 <th class="value count">Count&nbsp;</th>
   1926               </tr>
   1927             </thead>
   1928             <tbody></tbody>
   1929           </table>
   1930         </div>
   1931       </div>
   1932       <div class="pageDetail inline toggleContentVisibility">
   1933         <h3>Page Comparison for <span></span></h3>
   1934         <div class="content">
   1935           <table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
   1936             <thead>
   1937               <tr>
   1938                 <th class="page">Page&nbsp;</th>
   1939                 <th class="value time">Time&nbsp;</th>
   1940                 <th class="value time">Percent&nbsp;</th>
   1941                 <th class="value time hideNoDiff">%/Entry&nbsp;</th>
   1942                 <th class="value count">Count&nbsp;</th>
   1943               </tr>
   1944             </thead>
   1945             <tfoot>
   1946               <tr>
   1947                 <td class="page">Total:</td>
   1948                 <td class="value time"></td>
   1949                 <td class="value time"></td>
   1950                 <td class="value time hideNoDiff"></td>
   1951                 <td class="value count"></td>
   1952               </tr>
   1953             </tfoot>
   1954             <tbody></tbody>
   1955           </table>
   1956         </div>
   1957       </div>
   1958       <div class="impactView inline toggleContentVisibility">
   1959         <h3>Impact list for <span></span></h3>
   1960         <div class="content">
   1961           <table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
   1962             <thead>
   1963               <tr>
   1964                 <th class="page">Name&nbsp;</th>
   1965                 <th class="value time">Time&nbsp;</th>
   1966                 <th class="value time">Percent&nbsp;</th>
   1967                 <th class="">Top Pages</th>
   1968               </tr>
   1969             </thead>
   1970             <tbody></tbody>
   1971           </table>
   1972         </div>
   1973       </div>
   1974     </div>
   1975     <div id="pageVersionGraph" class="graph hidden toggleContentVisibility">
   1976       <h3><span></span></h3>
   1977       <div class="content"></div>
   1978     </div>
   1979     <div id="pageGraph" class="graph hidden toggleContentVisibility">
   1980       <h3><span></span></h3>
   1981       <div class="content"></div>
   1982     </div>
   1983     <div id="versionGraph" class="graph hidden toggleContentVisibility">
   1984       <h3><span></span></h3>
   1985       <div class="content"></div>
   1986     </div>
   1987 
   1988     <div id="column" class="column">
   1989       <div class="header">
   1990         <select class="version" onchange="handleSelectVersion(this, event);"></select>
   1991         <select class="pageVersion" onchange="handleSelectPage(this, event);"></select>
   1992       </div>
   1993       <table class="list" onclick="handleSelectRow(this, event);">
   1994         <thead>
   1995           <tr>
   1996             <th class="position">Pos.&nbsp;</th>
   1997             <th class="name">Name&nbsp;</th>
   1998             <th class="value time">Time&nbsp;</th>
   1999             <th class="value time">Percent&nbsp;</th>
   2000             <th class="value count">Count&nbsp;</th>
   2001           </tr>
   2002         </thead>
   2003         <tbody></tbody>
   2004       </table>
   2005     </div>
   2006   </div>
   2007 
   2008   <div class="inline">
   2009     <h2>Usage</h2>
   2010     <ol>
   2011       <li>Install scipy, e.g. <code>sudo aptitude install python-scipy</code>
   2012       <li>Build chrome.</li>
   2013       <li>Check out a known working version of webpagereply:
   2014         <pre>git -C $CHROME_DIR/third_party/webpagereplay checkout 7dbd94752d1cde5536ffc623a9e10a51721eff1d</pre>
   2015       </li>
   2016       <li>Run <code>callstats.py</code> with a web-page-replay archive:
   2017         <pre>$V8_DIR/tools/callstats.py run \
   2018         --replay-bin=$CHROME_SRC/third_party/webpagereplay/replay.py \
   2019         --replay-wpr=$INPUT_DIR/top25.wpr \
   2020         --js-flags="" \
   2021         --with-chrome=$CHROME_SRC/out/Release/chrome \
   2022         --sites-file=$INPUT_DIR/top25.json</pre>
   2023       </li>
   2024       <li>Move results file to a subdirectory: <code>mkdir $VERSION_DIR; mv *.txt $VERSION_DIR</code></li>
   2025       <li>Repeat from step 1 with a different configuration (e.g. <code>--js-flags="--nolazy"</code>).</li>
   2026       <li>Create the final results file: <code>./callstats.py json $VERSION_DIR1 $VERSION_DIR2 > result.json</code></li>
   2027       <li>Use <code>results.json</code> on this site.</code>
   2028     </ol>
   2029   </div>
   2030 
   2031   <div id="popover">
   2032     <div class="popoverArrow"></div>
   2033     <table>
   2034       <tr>
   2035         <td class="name" colspan="6"></td>
   2036       </tr>
   2037       <tr>
   2038         <td>Page:</td>
   2039         <td class="page name" colspan="6"></td>
   2040       </tr>
   2041       <tr>
   2042         <td>Version:</td>
   2043         <td class="version name" colspan="3"></td>
   2044         <td class="compare version name" colspan="3"></td>
   2045       </tr>
   2046       <tr>
   2047         <td>Time:</td>
   2048         <td class="time"></td><td></td><td class="timeVariance"></td>
   2049         <td class="compare time"></td><td class="compare">  </td><td class="compare timeVariance"></td>
   2050       </tr>
   2051       <tr>
   2052         <td>Percent:</td>
   2053         <td class="percent"></td><td></td><td class="percentVariance"></td>
   2054         <td class="compare percent"></td><td class="compare">  </td><td class="compare percentVariance"></td>
   2055       </tr>
   2056       <tr>
   2057         <td>Percent per Entry:</td>
   2058         <td class="percentPerEntry"></td><td colspan=2></td>
   2059         <td class="compare percentPerEntry"></td><td colspan=2></td>
   2060       </tr>
   2061       <tr>
   2062         <td>Count:</td>
   2063         <td class="count"></td><td></td><td class="countVariance"></td>
   2064         <td class="compare count"></td><td class="compare">  </td><td class="compare countVariance"></td>
   2065       </tr>
   2066       <tr>
   2067         <td>Overall Impact:</td>
   2068         <td class="timeImpact"></td><td></td><td class="timePercentImpact"></td>
   2069         <td class="compare timeImpact"></td><td class="compare">  </td><td class="compare timePercentImpact"></td>
   2070       </tr>
   2071     </table>
   2072   </div>
   2073 </body>
   2074 </html>
   2075