Merge V8 5.2.361.47  DO NOT MERGE

https://chromium.googlesource.com/v8/v8/+/5.2.361.47

FPIIM-449

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