callstats.html 87.3 KB
Newer Older
1
<!DOCTYPE html>
2 3 4 5 6 7 8
<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>
9
  <meta charset="utf-8">
10 11
  <title>V8 Runtime Call Stats Komparator</title>
  <link rel="stylesheet" type="text/css" href="system-analyzer/index.css">
12 13 14 15
  <style>
    body {
      font-family: arial;
    }
16

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
    .panel {
      display: none;
    }

    .loaded .panel {
      display: block;
    }

    .panel.alwaysVisible {
      display: inherit !important;
    }

    .error #inputs {
      background-color: var(--error-color);
    }

33 34 35 36
    table {
      display: table;
      border-spacing: 0px;
    }
37

38 39 40 41
    tr {
      border-spacing: 0px;
      padding: 10px;
    }
42

43 44 45 46
    td,
    th {
      padding: 3px 10px 3px 5px;
    }
47

48 49
    .inline {
      display: inline-block;
50 51
      vertical-align: middle;
      margin-right: 10px;
52
    }
53

54 55 56
    .hidden {
      display: none;
    }
57

58 59 60
    .view {
      display: table;
    }
61

62 63 64 65 66 67 68 69 70
    .panel-group {
      display: grid;
      align-content: center;
      grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
      grid-auto-flow: row dense;
      grid-gap: 10px;
      margin-top: 10px;
    }

71 72 73 74 75
    .column {
      display: table-cell;
      border-right: 1px black dotted;
      min-width: 200px;
    }
76

77 78 79
    .column .header {
      padding: 0 10px 0 10px
    }
80

81 82 83
    #column {
      display: none;
    }
84

85 86 87
    .list {
      width: 100%;
    }
88

89 90 91
    select {
      width: 100%
    }
92

93 94 95
    .list tbody {
      cursor: pointer;
    }
96

97
    .list tr:nth-child(even) {
98
      background-color: rgba(0.5, 0.5, 0.5, 0.1);
99
    }
100

101 102 103
    .list tr.child {
      display: none;
    }
104

105 106 107
    .list tr.child.visible {
      display: table-row;
    }
108

109 110 111
    .list .child .name {
      padding-left: 20px;
    }
112

113 114 115
    .list .parent td {
      border-top: 1px solid #AAA;
    }
116

117 118 119
    .list .total {
      font-weight: bold
    }
120

121 122
    .list tr.parent.selected,
    .list tr:nth-child(even).selected,
123
    tr.selected {
124
      background-color: rgba(0.5, 0.5, 0.5, 0.1);
125
    }
126

127 128 129 130
    .codeSearch {
      display: block-inline;
      float: right;
      border-radius: 5px;
131
      background-color: #333;
132 133 134
      width: 1em;
      text-align: center;
    }
135

136 137 138 139
    .list .position {
      text-align: right;
      display: none;
    }
140

141 142 143
    .list div.toggle {
      cursor: pointer;
    }
144

145 146 147
    #column_0 .position {
      display: table-cell;
    }
148

149 150 151
    #column_0 .name {
      display: table-cell;
    }
152

153 154 155 156
    .list .name {
      display: none;
      white-space: nowrap;
    }
157

158 159 160
    .value {
      text-align: right;
    }
161

162 163 164
    .selectedVersion {
      font-weight: bold;
    }
165

166 167 168
    #baseline {
      width: auto;
    }
169

170 171 172
    .pageDetailTable tbody {
      cursor: pointer
    }
173

174 175 176
    .pageDetailTable tfoot td {
      border-top: 1px grey solid;
    }
177

178 179 180 181 182 183
    #popover {
      position: absolute;
      transform: translateY(-50%) translateX(40px);
      box-shadow: -2px 10px 44px -10px #000;
      border-radius: 5px;
      z-index: 1;
184
      background-color: var(--surface-color);
185 186 187
      display: none;
      white-space: nowrap;
    }
188

189 190 191 192 193 194
    #popover table {
      position: relative;
      z-index: 1;
      text-align: right;
      margin: 10px;
    }
195

196 197 198 199
    #popover td {
      padding: 3px 0px 3px 5px;
      white-space: nowrap;
    }
200

201
    .popoverArrow {
202
      background-color: var(--surface-color);
203 204 205 206 207 208 209 210
      position: absolute;
      width: 30px;
      height: 30px;
      transform: translateY(-50%)rotate(45deg);
      top: 50%;
      left: -10px;
      z-index: 0;
    }
211

212 213 214 215 216
    #popover .name {
      padding: 5px;
      font-weight: bold;
      text-align: center;
    }
217

218 219 220
    #popover table .compare {
      display: none
    }
221

222 223 224
    #popover table.compare .compare {
      display: table-cell;
    }
225 226 227 228 229

    #popover .compare .time,
    #popover .compare .version {
      padding-left: 10px;
    }
230

231 232 233
    .diff .hideDiff {
      display: none;
    }
234

235 236 237
    .noDiff .hideNoDiff {
      display: none;
    }
238
  </style>
239 240
  <script src="https://www.gstatic.com/charts/loader.js"></script>
  <script>
241
    "use strict"
242 243 244
    google.charts.load('current', {
      packages: ['corechart']
    });
245 246 247

    // Did anybody say monkeypatching?
    if (!NodeList.prototype.forEach) {
248
      NodeList.prototype.forEach = function (func) {
249
        for (let i = 0; i < this.length; i++) {
250 251 252 253 254
          func(this[i]);
        }
      }
    }

255 256 257 258 259
    let versions;
    let pages;
    let selectedPage;
    let baselineVersion;
    let selectedEntry;
260
    let sortByLabel = false;
261

262
    // Marker to programatically replace the defaultData.
263
    let defaultData = /*default-data-start*/ undefined /*default-data-end*/;
264 265

    function initialize() {
266
      // Initialize the stats table and toggle lists.
267
      let original = $("column");
268 269
      let viewBody = $("view").querySelector('.panelBody');
      removeAllChildren(viewBody);
270
      let i = 0;
271
      versions.forEach((version) => {
272
        if (!version.enabled) return;
273
        // add column
274
        let column = original.cloneNode(true);
275 276
        column.id = "column_" + i;
        // Fill in all versions
277
        let select = column.querySelector(".version");
278 279 280
        select.id = "selectVersion_" + i;
        // add all select options
        versions.forEach((version) => {
281
          if (!version.enabled) return;
282
          let option = document.createElement("option");
283 284 285 286 287 288 289
          option.textContent = version.name;
          option.version = version;
          select.appendChild(option);
        });
        // Fill in all page versions
        select = column.querySelector(".pageVersion");
        select.id = "select_" + i;
290
        // add all pages
291
        versions.forEach((version) => {
292
          if (!version.enabled) return;
293
          let optgroup = document.createElement("optgroup");
294 295
          optgroup.label = version.name;
          optgroup.version = version;
296
          version.forEachPage((page) => {
297
            let option = document.createElement("option");
298 299 300 301 302 303
            option.textContent = page.name;
            option.page = page;
            optgroup.appendChild(option);
          });
          select.appendChild(optgroup);
        });
304
        viewBody.appendChild(column);
305 306
        i++;
      });
307

308
      let select = $('baseline');
309 310 311
      removeAllChildren(select);
      select.appendChild(document.createElement('option'));
      versions.forEach((version) => {
312
        let option = document.createElement("option");
313 314 315 316
        option.textContent = version.name;
        option.version = version;
        select.appendChild(option);
      });
317 318
      initializeToggleList(versions.versions, $('versionSelector'));
      initializeToggleList(pages.values(), $('pageSelector'));
319
      initializeToggleList(Group.groups.values(), $('groupSelector'));
320
    }
321

322
    function initializeToggleList(items, node) {
323
      let list = node.querySelector('ul');
324 325 326 327
      removeAllChildren(list);
      items = Array.from(items);
      items.sort(NameComparator);
      items.forEach((item) => {
328 329
        let li = document.createElement('li');
        let checkbox = document.createElement('input');
330
        checkbox.type = 'checkbox';
331 332
        checkbox.checked = item.enabled;
        checkbox.item = item;
333
        checkbox.addEventListener('click', handleToggleVersionOrPageEnable);
334
        li.appendChild(checkbox);
335 336
        li.appendChild(document.createTextNode(item.name));
        list.appendChild(li);
337
      });
338 339
    }

340 341 342 343 344 345 346
    window.addEventListener('popstate', (event) => {
      popHistoryState(event.state);
    });

    function popHistoryState(state) {
      if (!state.version) return false;
      if (!versions) return false;
347
      let version = versions.getByName(state.version);
348
      if (!version) return false;
349
      let page = version.get(state.page);
350 351
      if (!page) return false;
      if (!state.entry) {
352
        showEntry(page.total);
353
      } else {
354
        let entry = page.get(state.entry);
355
        if (!entry) {
356
          showEntry(page.total);
357 358 359 360 361 362 363 364
        } else {
          showEntry(entry);
        }
      }
      return true;
    }

    function pushHistoryState() {
365
      let selection = selectedEntry ? selectedEntry : selectedPage;
366
      if (!selection) return;
367
      let state = selection.urlParams();
368 369
      // Don't push a history state if it didn't change.
      if (JSON.stringify(window.history.state) === JSON.stringify(state)) return;
370 371
      let params = "?";
      for (let pairs of Object.entries(state)) {
372 373
        params += encodeURIComponent(pairs[0]) + "=" +
          encodeURIComponent(pairs[1]) + "&";
374 375 376 377
      }
      window.history.pushState(state, selection.toString(), params);
    }

378 379
    function showSelectedEntryInPage(page) {
      if (!selectedEntry) return showPage(page);
380
      let entry = page.get(selectedEntry.name);
381 382 383 384
      if (!entry) return showPage(page);
      selectEntry(entry);
    }

385
    function showPage(firstPage) {
386 387
      let changeSelectedEntry = selectedEntry !== undefined &&
        selectedEntry.page === selectedPage;
388 389 390 391
      selectedPage = firstPage;
      selectedPage.sort();
      showPageInColumn(firstPage, 0);
      // Show the other versions of this page in the following columns.
392 393
      let pageVersions = versions.getPageVersions(firstPage);
      let index = 1;
394 395 396 397 398 399
      pageVersions.forEach((page) => {
        if (page !== firstPage) {
          showPageInColumn(page, index);
          index++;
        }
      });
400 401 402
      if (changeSelectedEntry) {
        showEntryDetail(selectedPage.getEntry(selectedEntry));
      }
403
      showImpactList(selectedPage);
404
      pushHistoryState();
405 406
    }

407 408 409 410 411 412
    function clamp(value, min, max) {
      if (value < min) return min;
      if (value > max) return max;
      return value;
    }

413
    function diffColorFromRatio(ratio) {
414 415 416 417 418 419
      if (ratio == Infinity) {
        return '#ff0000';
      }
      if (ratio == -Infinity) {
        return '#00ff00';
      }
420 421
      if (ratio > 1) {
        // ratio > 1: #FFFFFF => #00FF00
422 423 424
        const red = clamp(((ratio - 1) * 255 * 10) | 0, 0, 255);
        const other = (255 - red).toString(16).padStart(2, '0');
        return `#ff${other}${other}`;
425 426
      }
      // ratio < 1: #FF0000 => #FFFFFF
427 428 429
      const green = clamp(((1 - ratio) * 255 * 10) | 0, 0, 255);
      const other = (255 - green).toString(16).padStart(2, '0');
      return `#${other}ff${other}`;
430 431
    }

432 433
    function showPageInColumn(page, columnIndex) {
      page.sort();
434 435
      let showDiff = columnIndex !== 0;
      if (baselineVersion) showDiff = page.version !== baselineVersion;
436
      let diffColor = (td, a, b) => { };
437
      if (showDiff) {
438 439 440
        if (baselineVersion) {
          diffColor = (td, diff, baseline) => {
            if (diff == 0) return;
441
            const ratio = (baseline + diff) / baseline;
442
            td.style.color = diffColorFromRatio(ratio);
443 444
          };
        } else {
445 446 447 448
          diffColor = (td, value, reference) => {
            if (value == reference) return;
            const ratio = value / reference;
            td.style.color = diffColorFromRatio(ratio);
449 450 451 452
          }
        }
      }

453 454
      let column = $('column_' + columnIndex);
      let select = $('select_' + columnIndex);
455 456 457 458
      // Find the matching option
      selectOption(select, (i, option) => {
        return option.page == page
      });
459 460 461 462
      let table = column.querySelector("table");
      let oldTbody = table.querySelector('tbody');
      let tbody = document.createElement('tbody');
      let referencePage = selectedPage;
463
      page.forEachSorted(selectedPage, (parentEntry, entry, referenceEntry) => {
464
        let tr = document.createElement('tr');
465 466 467
        tbody.appendChild(tr);
        tr.entry = entry;
        tr.parentEntry = parentEntry;
468
        tr.className = parentEntry === undefined ? 'parent' : 'child';
469 470 471 472 473
        // 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) {
474
            let baselineEntry = baselineVersion.getEntry(entry);
475 476 477
            if (baselineEntry !== undefined) referenceEntry = baselineEntry
          }
          if (!parentEntry) {
478
            let node = td(tr, '<div class="toggle">►</div>', 'position');
479 480 481 482
            node.firstChild.addEventListener('click', handleToggleGroup);
          } else {
            td(tr, entry.position == 0 ? '' : entry.position, 'position');
          }
483
          addCodeSearchButton(entry,
484
            td(tr, entry.name, 'name ' + entry.cssClass()));
485

486
          diffColor(
487 488
            td(tr, ms(entry.time), 'value time'),
            entry.time, referenceEntry.time);
489
          diffColor(
490 491
            td(tr, percent(entry.timePercent), 'value time'),
            entry.time, referenceEntry.time);
492
          diffColor(
493 494
            td(tr, count(entry.count), 'value count'),
            entry.count, referenceEntry.count);
495 496
        } else if (baselineVersion !== undefined && referenceEntry &&
          page.version !== baselineVersion) {
497
          // Show comparison of entry that does not exist on the current page.
498 499
          tr.entry = new Entry(0, referenceEntry.name);
          tr.entry.page = page;
500 501
          td(tr, '-', 'position');
          td(tr, referenceEntry.name, 'name');
502
          diffColor(
503 504
            td(tr, ms(referenceEntry.time), 'value time'),
            referenceEntry.time, 0);
505
          diffColor(
506 507
            td(tr, percent(referenceEntry.timePercent), 'value time'),
            referenceEntry.timePercent, 0);
508
          diffColor(
509 510
            td(tr, count(referenceEntry.count), 'value count'),
            referenceEntry.count, 0);
511 512
        } else {
          // Display empty entry / baseline entry
513
          let showBaselineEntry = entry !== undefined;
514
          if (showBaselineEntry) {
515
            if (!parentEntry) {
516
              let node = td(tr, '<div class="toggle">►</div>', 'position');
517 518 519 520 521
              node.firstChild.addEventListener('click', handleToggleGroup);
            } else {
              td(tr, entry.position == 0 ? '' : entry.position, 'position');
            }
            td(tr, entry.name, 'name');
522 523 524
            td(tr, ms(entry.time, false), 'value time');
            td(tr, percent(entry.timePercent, false), 'value time');
            td(tr, count(entry.count, false), 'value count');
525 526
          } else {
            td(tr, '-', 'position');
527
            td(tr, referenceEntry.name, 'name');
528 529 530
            td(tr, '-', 'value time');
            td(tr, '-', 'value time');
            td(tr, '-', 'value count');
531 532 533 534
          }
        }
      });
      table.replaceChild(tbody, oldTbody);
535
      let versionSelect = column.querySelector('select.version');
536 537 538 539 540
      selectOption(versionSelect, (index, option) => {
        return option.version == page.version
      });
    }

541 542 543 544
    function showEntry(entry) {
      selectEntry(entry, true);
    }

545
    function selectEntry(entry, updateSelectedPage) {
546
      let needsPageSwitch = true;
547
      if (updateSelectedPage && selectedPage) {
548
        entry = selectedPage.version.getEntry(entry);
549
        needsPageSwitch = updateSelectedPage && entry.page != selectedPage;
550
      }
551
      let rowIndex = 0;
552 553
      // If clicked in the detail row change the first column to that page.
      if (needsPageSwitch) showPage(entry.page);
554 555
      let childNodes = $('column_0').querySelector('.list tbody').childNodes;
      for (let i = 0; i < childNodes.length; i++) {
556
        if (childNodes[i].entry !== undefined &&
557
          childNodes[i].entry.name == entry.name) {
558 559 560 561
          rowIndex = i;
          break;
        }
      }
562
      let firstEntry = childNodes[rowIndex].entry;
563 564 565 566 567 568 569 570 571
      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) => {
572
        let row = body.childNodes[rowIndex];
573
        if (!row) return;
574 575
        toggleCssClass(row, 'selected', row.entry && row.entry.name ==
          firstEntry.name);
576
      });
577
      if (updateSelectedPage && selectedEntry) {
578 579
        entry = selectedEntry.page.version.getEntry(entry);
      }
580 581 582 583
      if (entry !== selectedEntry) {
        selectedEntry = entry;
        showEntryDetail(entry);
      }
584 585 586
    }

    function showEntryDetail(entry) {
587 588 589 590
      showVersionDetails(entry);
      showPageDetails(entry);
      showImpactList(entry.page);
      showGraphs(entry.page);
591
      pushHistoryState();
592
    }
593

594
    function showVersionDetails(entry) {
595
      let table, tbody, entries;
596
      table = $('versionDetails').querySelector('.versionDetailTable');
597 598
      tbody = document.createElement('tbody');
      if (entry !== undefined) {
599
        $('versionDetails').querySelector('h2 span').textContent =
600 601
          entry.name + ' in ' + entry.page.name;
        entries = versions.getPageVersions(entry.page).map(
602 603 604 605 606 607 608 609
          (page) => {
            return page.get(entry.name)
          });
        entries.sort((a, b) => {
          return a.time - b.time
        });
        entries.forEach((pageEntry) => {
          if (pageEntry === undefined) return;
610
          let tr = document.createElement('tr');
611 612
          if (pageEntry == entry) tr.className += 'selected';
          tr.entry = pageEntry;
613
          let isBaselineEntry = pageEntry.page.version == baselineVersion;
614
          td(tr, pageEntry.page.version.name, 'version');
615 616 617
          td(tr, ms(pageEntry.time, !isBaselineEntry), 'value time');
          td(tr, percent(pageEntry.timePercent, !isBaselineEntry), 'value time');
          td(tr, count(pageEntry.count, !isBaselineEntry), 'value count');
618 619 620 621
          tbody.appendChild(tr);
        });
      }
      table.replaceChild(tbody, table.querySelector('tbody'));
622
    }
623

624
    function showPageDetails(entry) {
625
      let table, tbody, entries;
626
      table = $('pageDetail').querySelector('.pageDetailTable');
627
      tbody = document.createElement('tbody');
628 629 630
      if (entry === undefined) {
        table.replaceChild(tbody, table.querySelector('tbody'));
        return;
631
      }
632 633
      let version = entry.page.version;
      let showDiff = version !== baselineVersion;
634
      $('pageDetail').querySelector('h2 span').textContent =
635 636
        version.name;
      entries = version.pages.map((page) => {
637 638 639
        if (!page.enabled) return;
        return page.get(entry.name)
      });
640
      entries.sort((a, b) => {
641
        let cmp = b.timePercent - a.timePercent;
642 643 644 645 646
        if (cmp.toFixed(1) == 0) return b.time - a.time;
        return cmp
      });
      entries.forEach((pageEntry) => {
        if (pageEntry === undefined) return;
647
        let tr = document.createElement('tr');
648 649 650 651 652 653
        if (pageEntry === entry) tr.className += 'selected';
        tr.entry = pageEntry;
        td(tr, pageEntry.page.name, 'name');
        td(tr, ms(pageEntry.time, showDiff), 'value time');
        td(tr, percent(pageEntry.timePercent, showDiff), 'value time');
        td(tr, percent(pageEntry.timePercentPerEntry, showDiff),
654
          'value time hideNoDiff');
655 656 657 658
        td(tr, count(pageEntry.count, showDiff), 'value count');
        tbody.appendChild(tr);
      });
      // show the total for all pages
659
      let tds = table.querySelectorAll('tfoot td');
660
      tds[1].textContent = ms(entry.getTimeImpact(), showDiff);
661
      // Only show the percentage total if we are in diff mode:
662 663 664
      tds[2].textContent = percent(entry.getTimePercentImpact(), showDiff);
      tds[3].textContent = '';
      tds[4].textContent = count(entry.getCountImpact(), showDiff);
665 666 667 668
      table.replaceChild(tbody, table.querySelector('tbody'));
    }

    function showImpactList(page) {
669 670
      let impactView = $('impactView');
      impactView.querySelector('h2 span').textContent = page.version.name;
671

672 673 674 675
      let table = impactView.querySelector('table');
      let tbody = document.createElement('tbody');
      let version = page.version;
      let entries = version.allEntries();
676
      if (selectedEntry !== undefined && selectedEntry.isGroup) {
677
        impactView.querySelector('h2 span').textContent += " " + selectedEntry.name;
678 679 680 681 682
        entries = entries.filter((entry) => {
          return entry.name == selectedEntry.name ||
            (entry.parent && entry.parent.name == selectedEntry.name)
        });
      }
683
      let isCompareView = baselineVersion !== undefined;
684 685
      entries = entries.filter((entry) => {
        if (isCompareView) {
686
          let impact = entry.getTimeImpact();
687 688
          return impact < -1 || 1 < impact
        }
689
        return entry.getTimePercentImpact() > 0.01;
690
      });
691
      entries = entries.slice(0, 50);
692
      entries.sort((a, b) => {
693
        let cmp = b.getTimePercentImpact() - a.getTimePercentImpact();
694 695 696 697
        if (isCompareView || cmp.toFixed(1) == 0) {
          return b.getTimeImpact() - a.getTimeImpact();
        }
        return cmp
698 699
      });
      entries.forEach((entry) => {
700
        let tr = document.createElement('tr');
701 702 703
        tr.entry = entry;
        td(tr, entry.name, 'name');
        td(tr, ms(entry.getTimeImpact()), 'value time');
704
        let percentImpact = entry.getTimePercentImpact();
705
        td(tr, percentImpact > 1000 ? '-' : percent(percentImpact), 'value time');
706
        let topPages = entry.getPagesByPercentImpact().slice(0, 3)
707
          .map((each) => {
708 709
            return each.name + ' (' + percent(each.getEntry(entry).timePercent) +
              ')'
710 711 712 713 714 715
          });
        td(tr, topPages.join(', '), 'name');
        tbody.appendChild(tr);
      });
      table.replaceChild(tbody, table.querySelector('tbody'));
    }
716

717
    function showGraphs(page) {
718
      let groups = page.groups.filter(each => each.enabled && !each.isTotal);
719
      // Sort groups by the biggest impact
720
      groups.sort((a, b) => b.getTimeImpact() - a.getTimeImpact());
721 722 723
      if (selectedGroup == undefined) {
        selectedGroup = groups[0];
      } else {
724
        groups = groups.filter(each => each.name != selectedGroup.name);
725 726 727
        if (!selectedGroup.isTotal && selectedGroup.enabled) {
          groups.unshift(selectedGroup);
        }
728
      }
729 730
      // Display graphs delayed for a snappier UI.
      setTimeout(() => {
731 732 733 734
        showPageVersionGraph(groups, page);
        showPageGraph(groups, page);
        showVersionGraph(groups, page);
      }, 10);
735
    }
736

737
    function getGraphDataTable(groups, page) {
738
      let dataTable = new google.visualization.DataTable();
739 740
      dataTable.addColumn('string', 'Name');
      groups.forEach(group => {
741
        let column = dataTable.addColumn('number', group.name.substring(6));
742
        dataTable.setColumnProperty(column, 'group', group);
743 744 745
        column = dataTable.addColumn({
          role: "annotation"
        });
746
        dataTable.setColumnProperty(column, 'group', group);
747
      });
748 749
      let column = dataTable.addColumn('number', 'Chart Total');
      dataTable.setColumnProperty(column, 'group', page.total);
750 751 752
      column = dataTable.addColumn({
        role: "annotation"
      });
753
      dataTable.setColumnProperty(column, 'group', page.total);
754 755 756
      return dataTable;
    }

757
    let selectedGroup;
758 759 760

    class ChartRow {
      static kSortFirstValueRelative(chartRow) {
761
        if (selectedGroup?.isTotal) return chartRow.total;
762 763 764 765
        return chartRow.data[0] / chartRow.total;
      }

      static kSortByFirstValue(chartRow) {
766
        if (selectedGroup?.isTotal) return chartRow.total;
767 768 769 770
        return chartRow.data[0];
      }

      constructor(linkedPage, label, sortValue_fn, data,
771
        excludeFromAverage = false) {
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
        this.linkedPage = linkedPage;
        this.label = label;
        if (!Array.isArray(data)) {
          throw new Error("Provide an Array for data");
        }
        this.data = data;
        this.total = 0;
        for (let i = 0; i < data.length; i++) this.total += data[i];
        this.sortValue = sortValue_fn(this);
        this.excludeFromAverage = excludeFromAverage;
      }

      forDataTable(maxRowsTotal) {
        // row = [label, entry1, annotation1, entry2, annotation2, ...]
        const rowData = [this.label];
        const kShowLabelLimit = 0.1;
        const kMinLabelWidth = 80;
        const chartWidth = window.innerWidth - 400;
        // Add value,label pairs
        for (let i = 0; i < this.data.length; i++) {
          const value = this.data[i];
          let label = '';
          // Only show labels for entries that are large enough..
795
          if (Math.abs(value / maxRowsTotal) * chartWidth > kMinLabelWidth) {
796 797 798 799 800 801 802 803 804 805
            label = ms(value);
          }
          rowData.push(value, label);
        }
        // Add the total row, with very small negative dummy entry for correct
        // placement of labels in diff view.
        rowData.push(this.total >= 0 ? 0 : -0.000000001, ms(this.total));
        return rowData;
      }
    }
806
    const collator = new Intl.Collator('en-UK');
807 808 809 810

    function setDataTableRows(dataTable, rows) {
      let skippedRows = 0;
      // Always sort by the selected entry (first column after the label)
811 812 813 814 815
      if (sortByLabel) {
        rows.sort((a, b) => collator.compare(a.label, b.label));
      } else {
        rows.sort((a, b) => b.sortValue - a.sortValue);
      }
816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836
      // Aggregate row data for Average/SUM chart entry:
      const aggregateData = rows[0].data.slice().fill(0);
      let maxTotal = 0;
      for (let i = 0; i < rows.length; i++) {
        const row = rows[i];
        let total = Math.abs(row.total);
        if (total > maxTotal) maxTotal = total;
        if (row.excludeFromAverage) {
          skippedRows++;
          continue
        }
        const chartRowData = row.data;
        for (let j = 0; j < chartRowData.length; j++) {
          aggregateData[j] += chartRowData[j];
        }
      }
      const length = rows.length - skippedRows;
      for (let i = 0; i < aggregateData.length; i++) {
        aggregateData[i] /= rows.length;
      }
      const averageRow = new ChartRow(undefined, 'Average',
837
        ChartRow.kSortByFirstValue, aggregateData);
838 839 840 841 842 843 844 845 846 847 848 849 850
      dataTable.addRow(averageRow.forDataTable());

      rows.forEach(chartRow => {
        let rowIndex = dataTable.addRow(chartRow.forDataTable(maxTotal));
        dataTable.setRowProperty(rowIndex, 'page', chartRow.linkedPage);
      });
    }

    function showPageVersionGraph(groups, page) {
      let dataTable = getGraphDataTable(groups, page);
      let vs = versions.getPageVersions(page);
      // Calculate the entries for the versions
      const rows = vs.map(page => new ChartRow(
851 852 853
        page, page.version.name, ChartRow.kSortByFirstValue,
        groups.map(group => page.getEntry(group).time),
        page.version === baselineVersion));
854
      renderGraph(`Versions for ${page.name}`, groups, dataTable, rows,
855
        'pageVersionGraph', true);
856 857
    }

858
    function showPageGraph(groups, page) {
859
      let isDiffView = baselineVersion !== undefined;
860
      let dataTable = getGraphDataTable(groups, page);
861 862
      // Calculate the average row
      // Sort the pages by the selected group.
863
      let pages = page.version.pages.filter(page => page.enabled);
864
      // Calculate the entries for the pages
865
      const rows = pages.map(page => new ChartRow(
866 867 868 869
        page, page.name,
        isDiffView ?
          ChartRow.kSortByFirstValue : ChartRow.kSortFirstValueRelative,
        groups.map(group => page.getEntry(group).time)));
870
      renderGraph(`Pages for ${page.version.name}`, groups, dataTable, rows,
871
        'pageGraph', isDiffView ? true : 'percent');
872
    }
873

874
    function showVersionGraph(groups, page) {
875
      let dataTable = getGraphDataTable(groups, page);
876
      let vs = versions.versions.filter(version => version.enabled);
877
      // Calculate the entries for the versions
878
      const rows = vs.map((version) => new ChartRow(
879 880 881
        version.get(page), version.name, ChartRow.kSortByFirstValue,
        groups.map(group => version.getEntry(group).getTimeImpact()),
        version === baselineVersion));
882
      renderGraph('Versions Total Time over all Pages', groups, dataTable, rows,
883
        'versionGraph', true);
884 885
    }

886
    function renderGraph(title, groups, dataTable, rows, id, isStacked) {
887
      let isDiffView = baselineVersion !== undefined;
888
      setDataTableRows(dataTable, rows);
889
      let formatter = new google.visualization.NumberFormat({
890 891
        suffix: (isDiffView ? 'msΔ' : 'ms'),
        negativeColor: 'red',
892 893
        groupingSymbol: "'"
      });
894
      for (let i = 1; i < dataTable.getNumberOfColumns(); i++) {
895 896
        formatter.format(dataTable, i);
      }
897 898
      let height = 85 + 28 * dataTable.getNumberOfRows();
      let options = {
899 900
        isStacked: isStacked,
        height: height,
901 902
        hAxis: {
          minValue: 0,
903 904 905
          textStyle: {
            fontSize: 14
          }
906 907
        },
        vAxis: {
908 909 910 911 912 913 914 915 916 917 918 919 920
          textStyle: {
            fontSize: 14
          }
        },
        tooltip: {
          textStyle: {
            fontSize: 14
          }
        },
        annotations: {
          textStyle: {
            fontSize: 8
          }
921 922 923 924 925
        },
        explorer: {
          actions: ['dragToZoom', 'rightClickToReset'],
          maxZoomIn: 0.01
        },
926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941
        legend: {
          position: 'top',
          maxLines: 3,
          textStyle: {
            fontSize: 12
          }
        },
        chartArea: {
          left: 200,
          top: 50
        },
        colors: [
          ...groups.map(each => each.color),
          /* Chart Total */
          "#000000",
        ]
942
      };
943
      let parentNode = $(id);
944
      parentNode.querySelector('h2>span, h3>span').textContent = title;
945
      let graphNode = parentNode.querySelector('.panelBody');
946

947
      let chart = graphNode.chart;
948 949 950 951 952
      if (chart === undefined) {
        chart = graphNode.chart = new google.visualization.BarChart(graphNode);
      } else {
        google.visualization.events.removeAllListeners(chart);
      }
953
      google.visualization.events.addListener(chart, 'select', selectHandler);
954

955 956
      function getChartEntry(selection) {
        if (!selection) return undefined;
957
        let column = selection.column;
958
        if (column == undefined) return undefined;
959 960
        let selectedGroup = dataTable.getColumnProperty(column, 'group');
        let row = selection.row;
961
        if (row == null) return selectedGroup;
962
        let page = dataTable.getRowProperty(row, 'page');
963 964 965
        if (!page) return selectedGroup;
        return page.getEntry(selectedGroup);
      }
966

967
      function selectHandler(e) {
968 969 970 971 972 973 974 975 976 977
        const newSelectedGroup = getChartEntry(chart.getSelection()[0]);
        if (newSelectedGroup == selectedGroup) {
          sortByLabel = !sortByLabel;
        } else if (newSelectedGroup === undefined && selectedPage) {
          sortByLabel = true;
          return showGraphs(selectedPage);
        } else {
          sortByLabel = false;
        }
        selectedGroup = newSelectedGroup;
978
        selectEntry(selectedGroup, true);
979
      }
980 981 982

      // Make our global tooltips work
      google.visualization.events.addListener(chart, 'onmouseover', mouseOverHandler);
983

984
      function mouseOverHandler(selection) {
985 986
        const selectedGroup = getChartEntry(selection);
        graphNode.entry = selectedGroup;
987 988
      }
      chart.draw(dataTable, options);
989
    }
990 991 992 993 994 995 996

    function showGroup(entry) {
      toggleGroup(entry, true);
    }

    function toggleGroup(group, show) {
      $('view').querySelectorAll(".child").forEach((tr) => {
997
        let entry = tr.parentEntry;
998 999 1000 1001 1002 1003 1004
        if (!entry) return;
        if (entry.name !== group.name) return;
        toggleCssClass(tr, 'visible', show);
      });
    }

    function showPopover(entry) {
1005
      let popover = $('popover');
1006 1007
      popover.querySelector('td.name').textContent = entry.name;
      popover.querySelector('td.page').textContent = entry.page.name;
1008 1009 1010 1011 1012 1013 1014 1015 1016 1017
      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) {
1018
      let node = (name) => popover.querySelector(prefix + name);
1019
      if (entry == undefined) {
1020 1021 1022 1023 1024
        node('.version').textContent = baselineVersion.name;
        node('.time').textContent = '-';
        node('.timeVariance').textContent = '-';
        node('.percent').textContent = '-';
        node('.percentPerEntry').textContent = '-';
1025 1026
        node('.percentVariance').textContent = '-';
        node('.count').textContent = '-';
1027 1028 1029
        node('.countVariance').textContent = '-';
        node('.timeImpact').textContent = '-';
        node('.timePercentImpact').textContent = '-';
1030
      } else {
1031 1032
        node('.version').textContent = entry.page.version.name;
        node('.time').textContent = ms(entry._time, false);
1033
        node('.timeVariance').textContent = percent(entry.timeVariancePercent, false);
1034
        node('.percent').textContent = percent(entry.timePercent, false);
1035 1036
        node('.percentPerEntry').textContent = percent(entry.timePercentPerEntry, false);
        node('.percentVariance').textContent = percent(entry.timePercentVariancePercent, false);
1037
        node('.count').textContent = count(entry._count, false);
1038 1039 1040
        node('.countVariance').textContent = percent(entry.timeVariancePercent, false);
        node('.timeImpact').textContent = ms(entry.getTimeImpact(false), false);
        node('.timePercentImpact').textContent = percent(entry.getTimeImpactVariancePercent(false), false);
1041
      }
1042
    }
1043
  </script>
1044
  <script>
1045
    "use strict"
1046
    // =========================================================================
1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058
    // Helpers
    function $(id) {
      return document.getElementById(id)
    }

    function removeAllChildren(node) {
      while (node.firstChild) {
        node.removeChild(node.firstChild);
      }
    }

    function selectOption(select, match) {
1059 1060
      let options = select.options;
      for (let i = 0; i < options.length; i++) {
1061 1062 1063 1064 1065 1066 1067
        if (match(i, options[i])) {
          select.selectedIndex = i;
          return;
        }
      }
    }

1068 1069
    function addCodeSearchButton(entry, node) {
      if (entry.isGroup) return;
1070
      let button = document.createElement("div");
1071
      button.textContent = '?'
1072 1073 1074 1075 1076 1077
      button.className = "codeSearch"
      button.addEventListener('click', handleCodeSearch);
      node.appendChild(button);
      return node;
    }

1078
    function td(tr, content, className) {
1079
      let td = document.createElement("td");
1080 1081 1082 1083 1084
      if (content[0] == '<') {
        td.innerHTML = content;
      } else {
        td.textContent = content;
      }
1085 1086 1087 1088 1089 1090
      td.className = className
      tr.appendChild(td);
      return td
    }

    function nodeIndex(node) {
1091
      let children = node.parentNode.childNodes,
1092 1093 1094 1095 1096 1097 1098 1099 1100
        i = 0;
      for (; i < children.length; i++) {
        if (children[i] == node) {
          return i;
        }
      }
      return -1;
    }

1101
    function toggleCssClass(node, cssClass, toggleState = true) {
1102 1103
      let index = -1;
      let classes;
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117
      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(' ');
    }

1118 1119 1120 1121 1122 1123
    function NameComparator(a, b) {
      if (a.name > b.name) return 1;
      if (a.name < b.name) return -1;
      return 0
    }

1124 1125
    function diffSign(value, digits, unit, showDiff) {
      if (showDiff === false || baselineVersion == undefined) {
1126
        if (value === undefined) return '';
1127 1128 1129
        return value.toFixed(digits) + unit;
      }
      return (value >= 0 ? '+' : '') + value.toFixed(digits) + unit + 'Δ';
1130 1131
    }

1132 1133
    function ms(value, showDiff) {
      return diffSign(value, 1, 'ms', showDiff);
1134 1135
    }

1136 1137
    function count(value, showDiff) {
      return diffSign(value, 0, '#', showDiff);
1138 1139
    }

1140 1141
    function percent(value, showDiff) {
      return diffSign(value, 1, '%', showDiff);
1142
    }
1143
  </script>
1144
  <script>
1145
    "use strict"
1146
    // =========================================================================
1147
    // EventHandlers
1148
    async function handleBodyLoad() {
1149
      $('uploadInput').focus();
1150
      if (tryLoadDefaultData() || await tryLoadFromURLParams() ||
1151
        await tryLoadDefaultResults()) {
1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169
        displayResultsAfterLoading();
      }
    }

    function tryLoadDefaultData() {
      if (!defaultData) return false;
      handleLoadJSON(defaultData);
      return true;
    }

    async function tryLoadFromURLParams() {
      let params = new URLSearchParams(document.location.search);
      let hasFile = false;
      params.forEach(async (value, key) => {
        if (key !== 'file') return;
        hasFile ||= await tryLoadFile(value, true);
      });
      return hasFile;
1170 1171
    }

1172
    async function tryLoadDefaultResults() {
1173 1174 1175 1176 1177 1178
      if (window.location.protocol === 'file:') return false;
      // Try to load a results.json file adjacent to this day.
      // The markers on the following line can be used to replace the url easily
      // with scripts.
      const url = /*results-url-start*/ 'results.json' /*results-url-end*/;
      return tryLoadFile(url);
1179 1180
    }

1181
    async function tryLoadFile(url, append = false) {
1182 1183 1184 1185 1186 1187 1188 1189 1190
      if (!url.startsWith('http')) {
        // hack to get relative urls
        let location = window.location;
        let parts = location.pathname.split("/").slice(0, -1);
        url = location.origin + parts.join('/') + '/' + url;
      }
      let response = await fetch(url);
      if (!response.ok) return false;
      let filename = url.split('/');
1191
      filename = filename[filename.length - 1];
1192
      handleLoadText(await response.text(), append, filename);
1193
      return true;
1194 1195
    }

1196
    function handleAppendFiles() {
1197
      let files = document.getElementById("appendInput").files;
1198 1199 1200
      loadFiles(files, true);
    }

1201
    function handleLoadFiles() {
1202
      let files = document.getElementById("uploadInput").files;
1203 1204
      loadFiles(files, false)
    }
1205

1206 1207 1208 1209 1210 1211 1212 1213 1214 1215
    async function loadFiles(files, append) {
      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        console.log(file.name);
        let text = await new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.onload = () => resolve(reader.result)
          reader.readAsText(file);
        });
        handleLoadText(text, append, file.name);
1216 1217 1218
        // Only the first file might clear existing data, all sequent files
        // are always append.
        append = true;
1219
      }
1220 1221
      displayResultsAfterLoading();
      toggleCssClass(document.body, "loaded");
1222 1223
    }

1224
    function handleLoadText(text, append, fileName) {
1225 1226 1227
      if (fileName.endsWith('.json')) {
        handleLoadJSON(JSON.parse(text), append, fileName);
      } else if (fileName.endsWith('.csv') ||
1228
        fileName.endsWith('.output') || fileName.endsWith('.output.txt')) {
1229 1230 1231 1232
        handleLoadCSV(text, append, fileName);
      } else if (fileName.endsWith('.txt')) {
        handleLoadTXT(text, append, fileName);
      } else {
1233
        alert(`Unsupported file extension: "${fileName}"`);
1234
      }
1235 1236
    }

1237
    function getStateFromParams() {
1238 1239
      let query = window.location.search.substr(1);
      let result = {};
1240
      query.split("&").forEach((part) => {
1241 1242
        let item = part.split("=");
        let key = decodeURIComponent(item[0])
1243 1244 1245 1246 1247
        result[key] = decodeURIComponent(item[1]);
      });
      return result;
    }

1248
    function handleLoadJSON(json, append, fileName) {
1249
      json = fixClusterTelemetryResults(json);
1250 1251
      json = fixTraceImportJSON(json);
      json = fixSingleVersionJSON(json, fileName);
1252
      let isFirstLoad = pages === undefined;
1253
      if (append && !isFirstLoad) {
1254
        json = createUniqueVersions(json);
1255 1256 1257 1258 1259 1260 1261
      }
      if (!append || isFirstLoad) {
        pages = new Pages();
        versions = Versions.fromJSON(json);
      } else {
        Versions.fromJSON(json).forEach(e => versions.add(e))
      }
1262 1263
    }

1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276
    function handleLoadCSV(csv, append, fileName) {
      let isFirstLoad = pages === undefined;
      if (!append || isFirstLoad) {
        pages = new Pages();
        versions = new Versions();
      }
      const lines = csv.split(/\r?\n/);
      // The first line contains only the field names.
      const fields = new Map();
      csvSplit(lines[0]).forEach((name, index) => {
        fields.set(name, index);
      });
      if (fields.has('displayLabel') && fields.has('stories')) {
1277
        handleLoadResultCSV(fields, lines);
1278
      } else if (fields.has('page_name')) {
1279
        handleLoadClusterTelemetryCSV(fields, lines, fileName);
1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290
      } else {
        return alert("Unknown CSV format");
      }
    }

    function csvSplit(line) {
      let fields = [];
      let index = 0;
      while (index < line.length) {
        let lastIndex = index;
        if (line[lastIndex] == '"') {
1291
          index = line.indexOf('"', lastIndex + 1);
1292
          if (index < 0) index = line.length;
1293
          fields.push(line.substring(lastIndex + 1, index));
1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306
          // Consume ','
          index++;
        } else {
          index = line.indexOf(',', lastIndex);
          if (index === -1) index = line.length;
          fields.push(line.substring(lastIndex, index))
        }
        // Consume ','
        index++;
      }
      return fields;
    }

1307 1308 1309
    // Ignore the following categories as they are aggregated values and are
    // created by callstats.html on the fly.
    const import_skip_categories = new Set([
1310 1311
      'V8-Only', 'V8-Only-Main-Thread', 'Total-Main-Thread', 'Blink_Total'
    ])
1312

1313 1314
    function handleLoadClusterTelemetryCSV(fields, lines, fileName) {
      const rscFields = Array.from(fields.keys())
1315 1316 1317 1318
        .filter(field => {
          return field.endsWith(':duration (ms)') &&
            !import_skip_categories.has(field.split(':')[0])
        })
1319 1320 1321 1322 1323 1324
        .map(field => {
          let name = field.split(':')[0];
          return [name, fields.get(field), fields.get(`${name}:count`)];
        })
      const page_name_i = fields.get('page_name');
      const version = versions.getOrCreate(fileName);
1325
      for (let i = 1; i < lines.length; i++) {
1326
        const line = csvSplit(lines[i]);
1327
        if (line.length == 0) continue;
1328 1329 1330 1331 1332 1333 1334 1335
        let page_name = line[page_name_i];
        if (page_name === undefined) continue;
        page_name = page_name.split(' ')[0];
        const pageVersion = version.getOrCreate(page_name);
        for (let [fieldName, duration_i, count_i] of rscFields) {
          const duration = Number.parseFloat(line[duration_i]);
          const count = Number.parseFloat(line[count_i]);
          // Skip over entries without metrics (most likely crashes)
1336
          if (Number.isNaN(count) || Number.isNaN(duration)) {
1337 1338 1339
            console.warn(`BROKEN ${page_name}`, lines[i])
            break;
          }
1340
          pageVersion.add(new Entry(0, fieldName, duration, 0, 0, count, 0, 0))
1341 1342 1343 1344
        }
      }
    }

1345
    function handleLoadResultCSV(fields, lines) {
1346 1347 1348 1349 1350
      const version_i = fields.get('displayLabel');
      const page_i = fields.get('stories');
      const category_i = fields.get('name');
      const value_i = fields.get('avg');
      const tempEntriesCache = new Map();
1351
      for (let i = 1; i < lines.length; i++) {
1352
        const line = csvSplit(lines[i]);
1353
        if (line.length == 0) continue;
1354 1355
        const raw_category = line[category_i];
        if (!raw_category.endsWith(':duration') &&
1356
          !raw_category.endsWith(':count')) {
1357 1358 1359
          continue;
        }
        let [category, type] = raw_category.split(':');
1360
        if (import_skip_categories.has(category)) continue;
1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391
        const version = versions.getOrCreate(line[version_i]);
        const pageVersion = version.getOrCreate(line[page_i]);
        const value = Number.parseFloat(line[value_i]);
        const entry = TempEntry.get(tempEntriesCache, pageVersion, category);
        if (type == 'duration') {
          entry.durations.push(value)
        } else {
          entry.counts.push(value)
        }
      }

      tempEntriesCache.forEach((tempEntries, pageVersion) => {
        tempEntries.forEach(tmpEntry => {
          pageVersion.add(tmpEntry.toEntry())
        })
      });
    }

    class TempEntry {
      constructor(category) {
        this.category = category;
        this.durations = [];
        this.counts = [];
      }

      static get(cache, pageVersion, category) {
        let tempEntries = cache.get(pageVersion);
        if (tempEntries === undefined) {
          tempEntries = new Map();
          cache.set(pageVersion, tempEntries);
        }
1392
        let tempEntry = tempEntries.get(category);
1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403
        if (tempEntry === undefined) {
          tempEntry = new TempEntry(category);
          tempEntries.set(category, tempEntry);
        }
        return tempEntry;
      }

      toEntry() {
        const [duration, durationStddev] = this.stats(this.durations);
        const [count, countStddev] = this.stats(this.durations);
        return new Entry(0, this.category,
1404
          duration, durationStddev, 0, count, countStddev, 0)
1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422
      }

      stats(values) {
        let sum = 0;
        for (let i = 0; i < values.length; i++) {
          sum += values[i];
        }
        const avg = sum / values.length;
        let stddevSquared = 0;
        for (let i = 0; i < values.length; i++) {
          const delta = values[i] - avg;
          stddevSquared += delta * delta;
        }
        const stddev = Math.sqrt(stddevSquared / values.length);
        return [avg, stddev];
      }
    }

1423
    function handleLoadTXT(txt, append, fileName) {
1424
      fileName = window.prompt('Version name:', fileName);
1425 1426 1427 1428 1429 1430
      let isFirstLoad = pages === undefined;
      // Load raw RCS output which contains a single page
      if (!append || isFirstLoad) {
        pages = new Pages();
        versions = new Versions()
      }
1431
      versions.add(Version.fromTXT(fileName, txt));
1432 1433 1434

    }

1435 1436
    function displayResultsAfterLoading() {
      const isFirstLoad = pages === undefined;
1437
      let state = getStateFromParams();
1438
      initialize()
1439
      if (isFirstLoad && !popHistoryState(state) && selectedPage) {
1440
        showEntry(selectedPage.total);
1441
        return;
1442
      }
1443
      const page = versions.versions[0].pages[0]
1444
      if (page == undefined) return;
1445 1446
      showPage(page);
      showEntry(page.total);
1447 1448 1449 1450 1451
    }

    function fixClusterTelemetryResults(json) {
      // Convert CT results to callstats compatible JSON
      // Input:
1452
      // { VERSION_NAME: { PAGE: { METRIC: { "count": {XX}, "duration": {XX} }.. }}.. }
1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466
      let firstEntry;
      for (let key in json) {
        firstEntry = json[key];
        break;
      }
      // Return the original JSON if it is not a CT result.
      if (firstEntry.pairs === undefined) return json;
      // The results include already the group totals, remove them by filtering.
      let groupNames = new Set(Array.from(Group.groups.values()).map(e => e.name));
      let result = Object.create(null);
      for (let file_name in json) {
        let entries = [];
        let file_data = json[file_name].pairs;
        for (let name in file_data) {
1467
          if (name != "Total" && groupNames.has(name)) continue;
1468 1469 1470 1471
          let entry = file_data[name];
          let count = entry.count;
          let time = entry.time;
          entries.push([name, time, 0, 0, count, 0, 0]);
1472
        }
1473 1474 1475
        let domain = file_name.split("/").slice(-1)[0];
        result[domain] = entries;
      }
1476 1477 1478 1479
      return {
        __proto__: null,
        ClusterTelemetry: result
      };
1480 1481
    }

1482 1483 1484 1485
    function fixTraceImportJSON(json) {
      // Fix json file that was created by converting a trace json output
      if (!('telemetry-results' in json)) return json;
      // { telemetry-results: { PAGE:[ { METRIC: [ COUNT TIME ], ... }, ... ]}}
1486 1487 1488
      let version_data = {
        __proto__: null
      };
1489 1490 1491 1492
      json = json["telemetry-results"];
      for (let page_name in json) {
        if (page_name == "placeholder") continue;
        let page_data = {
1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504
          __proto__: null,
          Total: {
            duration: {
              average: 0,
              stddev: 0
            },
            count: {
              average: 0,
              stddev: 0
            }
          }
        };
1505
        let page = json[page_name];
1506
        for (let slice of page) {
1507 1508 1509 1510 1511
          for (let metric_name in slice) {
            if (metric_name == "Blink_V8") continue;
            // sum up entries
            if (!(metric_name in page_data)) {
              page_data[metric_name] = {
1512 1513 1514 1515 1516 1517 1518 1519
                duration: {
                  average: 0,
                  stddev: 0
                },
                count: {
                  average: 0,
                  stddev: 0
                }
1520 1521 1522 1523
              }
            }
            let [metric_count, metric_duration] = slice[metric_name]
            let metric = page_data[metric_name];
1524
            const kMicroToMilli = 1 / 1000;
1525 1526 1527 1528 1529 1530 1531
            metric.duration.average += metric_duration * kMicroToMilli;
            metric.count.average += metric_count;

            if (metric_name.startsWith('Blink_')) continue;
            let total = page_data['Total'];
            total.duration.average += metric_duration * kMicroToMilli;
            total.count.average += metric_count;
1532
          }
1533 1534 1535 1536 1537 1538 1539
        }
        version_data[page_name] = page_data;
      }
      return version_data;
    }

    function fixSingleVersionJSON(json, name) {
1540 1541
      // Try to detect the single-version case, where we're missing the toplevel
      // version object. The incoming JSON is of the form:
1542
      //   { PAGE: ... , PAGE_2:  }
1543 1544 1545
      // Instead of the default multi-page JSON:
      //    {"Version 1": { "Page 1": ..., ...}, "Version 2": {...}, ...}
      // In this case insert a single "Default" version as top-level entry.
1546 1547
      let firstProperty = (object) => {
        for (let key in object) return object[key];
1548
      };
1549 1550 1551
      let maybePage = firstProperty(json);
      let maybeMetrics = firstProperty(maybePage);
      let tempName = name ? name : new Date().toISOString();
1552
      tempName = window.prompt('Enter a name for the loaded file:', tempName);
1553
      if ('count' in maybeMetrics && 'duration' in maybeMetrics) {
1554 1555 1556
        return {
          [tempName]: json
        }
1557 1558 1559 1560
      }
      // Legacy fallback where the metrics are encoded as arrays:
      //  { PAGE: [[metric_name, ...], [...], ]}
      if (Array.isArray(maybeMetrics)) {
1561 1562 1563
        return {
          [tempName]: json
        }
1564 1565
      }
      return json
1566 1567
    }

1568
    let appendIndex = 0;
1569

1570
    function createUniqueVersions(json) {
1571
      // Make sure all toplevel entries are unique names and added properly
1572
      appendIndex++;
1573 1574 1575
      let result = {
        __proto__: null
      }
1576
      for (let key in json) {
1577
        result[key + "_" + appendIndex] = json[key];
1578
      }
1579
      return result
1580 1581
    }

1582
    function handleCopyToClipboard(event) {
1583 1584
      const names = ["Group", ...versions.versions.map(e => e.name)];
      let result = [names.join("\t")];
1585 1586 1587 1588 1589 1590
      let groups = Array.from(Group.groups.values());
      // Move the total group to the end.
      groups.push(groups.shift())
      groups.forEach(group => {
        let row = [group.name];
        versions.forEach(v => {
1591 1592
          const time = v.pages[0].get("Group-" + group.name)?._time ?? 0;
          row.push(time);
1593 1594 1595 1596
        })
        result.push(row.join("\t"));
      });
      result = result.join("\n");
1597
      navigator.clipboard.writeText(result);
1598 1599
    }

1600
    function handleToggleGroup(event) {
1601
      let group = event.target.parentNode.parentNode.entry;
1602
      toggleGroup(selectedPage.get(group.name), 'toggle');
1603 1604 1605
    }

    function handleSelectPage(select, event) {
1606
      let option = select.options[select.selectedIndex];
1607
      if (select.id == "select_0") {
1608
        showSelectedEntryInPage(option.page);
1609
      } else {
1610
        let columnIndex = select.id.split('_')[1];
1611 1612 1613 1614 1615
        showPageInColumn(option.page, columnIndex);
      }
    }

    function handleSelectVersion(select, event) {
1616 1617
      let option = select.options[select.selectedIndex];
      let version = option.version;
1618
      if (select.id == "selectVersion_0") {
1619
        let page = version.get(selectedPage.name);
1620
        showSelectedEntryInPage(page);
1621
      } else {
1622 1623 1624
        let columnIndex = select.id.split('_')[1];
        let pageSelect = $('select_' + columnIndex);
        let page = pageSelect.options[pageSelect.selectedIndex].page;
1625 1626 1627 1628 1629 1630 1631
        page = version.get(page.name);
        showPageInColumn(page, columnIndex);
      }
    }

    function handleSelectDetailRow(table, event) {
      if (event.target.tagName != 'TD') return;
1632
      let tr = event.target.parentNode;
1633 1634 1635 1636 1637 1638 1639
      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;
1640
      let tr = event.target.parentNode;
1641 1642 1643 1644 1645 1646
      if (tr.tagName != 'TR') return;
      if (tr.entry === undefined) return;
      selectEntry(tr.entry, false);
    }

    function handleSelectBaseline(select, event) {
1647
      let option = select.options[select.selectedIndex];
1648
      baselineVersion = option.version;
1649 1650
      let showingDiff = baselineVersion !== undefined;
      let body = $('body');
1651 1652
      toggleCssClass(body, 'diff', showingDiff);
      toggleCssClass(body, 'noDiff', !showingDiff);
1653 1654 1655 1656 1657
      showPage(selectedPage);
      if (selectedEntry === undefined) return;
      selectEntry(selectedEntry, true);
    }

1658
    function findEntry(event) {
1659
      let target = event.target;
1660 1661 1662 1663 1664 1665 1666
      while (target.entry === undefined) {
        target = target.parentNode;
        if (!target) return undefined;
      }
      return target.entry;
    }

1667
    function handleUpdatePopover(event) {
1668
      let popover = $('popover');
1669 1670
      popover.style.left = event.pageX + 'px';
      popover.style.top = event.pageY + 'px';
1671
      popover.style.display = 'none';
1672
      popover.style.display = event.shiftKey ? 'block' : 'none';
1673
      let entry = findEntry(event);
1674 1675
      if (entry === undefined) return;
      showPopover(entry);
1676 1677
    }

1678
    function handleToggleVersionOrPageEnable(event) {
1679 1680 1681
      let item = this.item;
      if (item === undefined) return;
      item.enabled = this.checked;
1682
      initialize();
1683
      let page = selectedPage;
1684 1685 1686
      if (page === undefined || !page.version.enabled) {
        page = versions.getEnabledPage(page.name);
      }
1687 1688 1689
      if (!page.enabled) {
        page = page.getNextPage();
      }
1690 1691 1692
      showPage(page);
    }

1693
    function handleCodeSearch(event) {
1694
      let entry = findEntry(event);
1695
      if (entry === undefined) return;
1696
      let url = "https://cs.chromium.org/search/?sq=package:chromium&type=cs&q=";
1697 1698 1699 1700 1701
      name = entry.name;
      if (name.startsWith("API_")) {
        name = name.substring(4);
      }
      url += encodeURIComponent(name) + "+file:src/v8/src";
1702
      window.open(url, '_blank');
1703 1704
    }
  </script>
1705
  <script>
1706
    "use strict"
1707
    // =========================================================================
1708 1709 1710 1711 1712
    class Versions {
      constructor() {
        this.versions = [];
      }
      add(version) {
1713 1714
        this.versions.push(version);
        return version;
1715
      }
1716
      getPageVersions(page) {
1717
        let result = [];
1718
        this.versions.forEach((version) => {
1719
          if (!version.enabled) return;
1720
          let versionPage = version.get(page.name);
1721
          if (versionPage !== undefined) result.push(versionPage);
1722 1723 1724 1725 1726 1727 1728 1729
        });
        return result;
      }
      get length() {
        return this.versions.length
      }
      get(index) {
        return this.versions[index]
1730 1731 1732 1733
      }
      getByName(name) {
        return this.versions.find((each) => each.name == name);
      }
1734
      getOrCreate(name) {
1735
        return this.getByName(name) ?? this.add(new Version(name));
1736
      }
1737 1738 1739 1740
      forEach(f) {
        this.versions.forEach(f);
      }
      sort() {
1741
        this.versions.sort(NameComparator);
1742
      }
1743
      getEnabledPage(name) {
1744 1745
        for (let i = 0; i < this.versions.length; i++) {
          let version = this.versions[i];
1746
          if (!version.enabled) continue;
1747
          let page = version.get(name);
1748
          if (page !== undefined) return page;
1749
        }
1750
      }
1751 1752

      static fromJSON(json) {
1753 1754
        let versions = new Versions();
        for (let version in json) {
1755 1756 1757 1758
          versions.add(Version.fromJSON(version, json[version]));
        }
        versions.sort();
        return versions;
1759 1760 1761 1762 1763 1764
      }
    }

    class Version {
      constructor(name) {
        this.name = name;
1765 1766
        this.enabled = true;
        this.pages = [];
1767 1768 1769
      }
      add(page) {
        this.pages.push(page);
1770
        return page;
1771 1772
      }
      indexOf(name) {
1773
        for (let i = 0; i < this.pages.length; i++) {
1774 1775 1776 1777
          if (this.pages[i].name == name) return i;
        }
        return -1;
      }
1778 1779 1780 1781
      getNextPage(page) {
        if (this.length == 0) return undefined;
        return this.pages[(this.indexOf(page.name) + 1) % this.length];
      }
1782
      get(name) {
1783
        let index = this.indexOf(name);
1784
        if (0 <= index) return this.pages[index];
1785
        return undefined;
1786
      }
1787 1788 1789 1790
      getOrCreate(name) {
        return this.get(name) ??
            this.add(new PageVersion(this, pages.getOrCreate(name)));
      }
1791
      get length() {
1792
        return this.pages.length;
1793 1794 1795
      }
      getEntry(entry) {
        if (entry === undefined) return undefined;
1796
        let page = this.get(entry.page.name);
1797 1798 1799 1800
        if (page === undefined) return undefined;
        return page.get(entry.name);
      }
      forEachEntry(fun) {
1801
        this.forEachPage((page) => {
1802 1803 1804
          page.forEach(fun);
        });
      }
1805 1806 1807 1808 1809 1810
      forEachPage(fun) {
        this.pages.forEach((page) => {
          if (!page.enabled) return;
          fun(page);
        })
      }
1811
      allEntries() {
1812
        let map = new Map();
1813 1814 1815 1816 1817 1818 1819
        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;
1820
        let sum = 0;
1821
        this.forEachPage((page) => {
1822
          let entry = page.get(name);
1823 1824 1825 1826 1827 1828 1829 1830
          if (entry !== undefined) sum += entry[property];
        });
        return sum;
      }
      getTotalTime(name, showDiff) {
        return this.getTotalValue(name, showDiff === false ? '_time' : 'time');
      }
      getTotalTimePercent(name, showDiff) {
1831
        if (baselineVersion === undefined || showDiff === false) {
1832 1833 1834
          // Return the overall average percent of the given entry name.
          return this.getTotalValue(name, 'time') /
            this.getTotalTime('Group-Total') * 100;
1835
        }
1836
        // Otherwise return the difference to the sum of the baseline version.
1837 1838
        let baselineValue = baselineVersion.getTotalTime(name, false);
        let total = this.getTotalValue(name, '_time');
1839
        return (total / baselineValue - 1) * 100;
1840 1841 1842
      }
      getTotalTimeVariance(name, showDiff) {
        // Calculate the overall error for a given entry name
1843
        let sum = 0;
1844
        this.forEachPage((page) => {
1845
          let entry = page.get(name);
1846 1847 1848 1849 1850 1851
          if (entry === undefined) return;
          sum += entry.timeVariance * entry.timeVariance;
        });
        return Math.sqrt(sum);
      }
      getTotalTimeVariancePercent(name, showDiff) {
1852
        return this.getTotalTimeVariance(name, showDiff) /
1853 1854 1855
          this.getTotalTime(name, showDiff) * 100;
      }
      getTotalCount(name, showDiff) {
1856
        return this.getTotalValue(name, showDiff === false ? '_count' : 'count');
1857
      }
1858 1859 1860
      getAverageTimeImpact(name, showDiff) {
        return this.getTotalTime(name, showDiff) / this.pages.length;
      }
1861
      getPagesByPercentImpact(name) {
1862
        let sortedPages =
1863 1864 1865 1866 1867 1868 1869 1870
          this.pages.filter((each) => {
            return each.get(name) !== undefined
          });
        sortedPages.sort((a, b) => {
          return b.get(name).timePercent - a.get(name).timePercent;
        });
        return sortedPages;
      }
1871
      sort() {
1872
        this.pages.sort(NameComparator)
1873
      }
1874 1875

      static fromJSON(name, data) {
1876 1877
        let version = new Version(name);
        for (let pageName in data) {
1878 1879 1880 1881 1882 1883 1884 1885
          version.add(PageVersion.fromJSON(version, pageName, data[pageName]));
        }
        version.sort();
        return version;
      }

      static fromTXT(name, txt) {
        let version = new Version(name);
1886 1887 1888
        let defaultName = "RAW DATA";
        PageVersion.fromTXT(version, defaultName, txt)
          .forEach(each => version.add(each));
1889
        return version;
1890 1891
      }
    }
1892

1893 1894 1895 1896 1897 1898 1899 1900 1901 1902
    class Pages extends Map {
      get(name) {
        if (name.indexOf('www.') == 0) {
          name = name.substring(4);
        }
        if (!this.has(name)) {
          this.set(name, new Page(name));
        }
        return super.get(name);
      }
1903 1904 1905
      getOrCreate(name) {
        return this.get(name);
      }
1906
    }
1907 1908

    class Page {
1909
      constructor(name) {
1910
        this.name = name;
1911 1912 1913
        this.enabled = true;
        this.versions = [];
      }
1914 1915 1916
      add(pageVersion) {
        this.versions.push(pageVersion);
        return pageVersion;
1917 1918 1919 1920 1921 1922 1923
      }
    }

    class PageVersion {
      constructor(version, page) {
        this.page = page;
        this.page.add(this);
1924
        this.total = Group.groups.get('total').entry();
1925
        this.total.isTotal = true;
1926
        this.unclassified = new UnclassifiedEntry(this)
1927 1928
        this.groups = [
          this.total,
1929
          Group.groups.get('ic').entry(),
1930
          Group.groups.get('optimize-background').entry(),
1931
          Group.groups.get('optimize').entry(),
1932
          Group.groups.get('compile-background').entry(),
1933
          Group.groups.get('compile').entry(),
1934
          Group.groups.get('parse-background').entry(),
1935
          Group.groups.get('parse').entry(),
1936
          Group.groups.get('blink').entry(),
1937 1938
          Group.groups.get('callback').entry(),
          Group.groups.get('api').entry(),
1939 1940
          Group.groups.get('gc-custom').entry(),
          Group.groups.get('gc-background').entry(),
1941 1942
          Group.groups.get('gc').entry(),
          Group.groups.get('javascript').entry(),
1943
          Group.groups.get('websnapshot').entry(),
1944
          Group.groups.get('runtime').entry(),
1945 1946 1947 1948 1949 1950 1951 1952 1953
          this.unclassified
        ];
        this.entryDict = new Map();
        this.groups.forEach((entry) => {
          entry.page = this;
          this.entryDict.set(entry.name, entry);
        });
        this.version = version;
      }
1954 1955 1956 1957
      toString() {
        return this.version.name + ": " + this.name;
      }
      urlParams() {
1958 1959 1960 1961
        return {
          version: this.version.name,
          page: this.name
        };
1962
      }
1963
      add(entry) {
1964 1965
        let existingEntry = this.entryDict.get(entry.name);
        if (existingEntry !== undefined) {
1966
          // Duplicate entries happen when multiple runs are combined into a
1967 1968
          // single file.
          existingEntry.add(entry);
1969 1970
          for (let i = 0; i < this.groups.length; i++) {
            const group = this.groups[i];
1971 1972 1973
            if (group.addTimeAndCount(entry)) return;
          }
        } else {
1974 1975 1976 1977 1978
          // Ignore accidentally added Group entries.
          if (entry.name.startsWith(GroupedEntry.prefix)) {
            console.warn("Skipping accidentally added Group entry:", entry, this);
            return;
          }
1979 1980
          entry.page = this;
          this.entryDict.set(entry.name, entry);
1981
          for (let group of this.groups) {
1982 1983
            if (group.add(entry)) return;
          }
1984
        }
1985
        console.error("Should not get here", entry);
1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996
      }
      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
      }
1997 1998 1999 2000 2001 2002
      get name() {
        return this.page.name
      }
      get enabled() {
        return this.page.enabled
      }
2003
      forEachSorted(referencePage, func) {
2004 2005
        // Iterate over all the entries in the order they appear on the
        // reference page.
2006
        referencePage.forEach((parent, referenceEntry) => {
2007
          let entry;
2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031
          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()
        });
      }
2032
      distanceFromTotalPercent() {
2033
        let sum = 0;
2034 2035
        this.groups.forEach(group => {
          if (group == this.total) return;
2036
          let value = group.getTimePercentImpact() -
2037
            this.getEntry(group).timePercent;
2038 2039 2040
          sum += value * value;
        });
        return sum;
2041
      }
2042 2043 2044
      getNextPage() {
        return this.version.getNextPage(this);
      }
2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059

      static fromJSON(version, name, data) {
        let page = new PageVersion(version, pages.get(name));
        // Distinguish between the legacy format which just uses Arrays,
        // or the new object style.
        if (Array.isArray(data)) {
          for (let i = 0; i < data.length; i++) {
            page.add(Entry.fromLegacyJSON(i, data[data.length - i - 1]));
          }
        } else {
          let position = 0;
          for (let metric_name in data) {
            page.add(Entry.fromJSON(position, metric_name, data[metric_name]));
            position++;
          }
2060
        }
2061 2062 2063 2064
        page.sort();
        return page
      }

2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092
      static fromTXT(version, defaultName, txt) {
        const kPageNameIdentifier = "== Page:";
        const kCommentStart = "=="
        const lines = txt.split('\n');
        const split = / +/g
        const result = [];
        let pageVersion = undefined;
        for (let i = 0; i < lines.length; i++) {
          const line = lines[i];
          // Skip header separators
          if (line.startsWith(kCommentStart)) {
            // Check for page names
            if (line.startsWith(kPageNameIdentifier)) {
              const name = line.split(kPageNameIdentifier)[1];
              pageVersion = new PageVersion(version, pages.get(name));
              result.push(pageVersion);
            }
          }
          // Skip header lines.
          if (lines[i + 1]?.startsWith(kCommentStart)) continue;
          const split_line = line.trim().split(split)
          if (split_line.length != 5) continue;
          if (pageVersion === undefined) {
            pageVersion = new PageVersion(version, pages.get(defaultName));
            result.push(pageVersion);
          }
          const position = i - 2;
          pageVersion.add(Entry.fromTXT(position, split_line));
2093
        }
2094
        return result;
2095 2096 2097 2098 2099 2100
      }
    }


    class Entry {
      constructor(position, name, time, timeVariance, timeVariancePercent,
2101
        count, countVariance, countVariancePercent) {
2102 2103 2104 2105
        this.position = position;
        this.name = name;
        this._time = time;
        this._timeVariance = timeVariance;
2106 2107
        this._timeVariancePercent =
            this._variancePercent(time, timeVariance, timeVariancePercent);
2108 2109
        this._count = count;
        this.countVariance = countVariance;
2110 2111
        this.countVariancePercent =
            this._variancePercent(count, countVariance, countVariancePercent);
2112 2113
        this.page = undefined;
        this.parent = undefined;
2114
        this.isTotal = false;
2115
      }
2116 2117 2118 2119 2120 2121
      _variancePercent(value, valueVariance, valueVariancePercent) {
        if (valueVariancePercent) return valueVariancePercent;
        if (!valueVariance) return 0;
        return valueVariance / value * 100;
      }

2122
      add(entry) {
2123
        if (this.name !== entry.name) {
2124 2125 2126 2127 2128 2129
          console.error("Should not combine entries with different names");
          return;
        }
        this._time += entry._time;
        this._count += entry._count;
      }
2130
      urlParams() {
2131
        let params = this.page.urlParams();
2132 2133 2134
        params.entry = this.name;
        return params;
      }
2135 2136
      getCompareWithBaseline(value, property) {
        if (baselineVersion == undefined) return value;
2137
        let baselineEntry = baselineVersion.getEntry(this);
2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151
        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() {
2152
        let value = this._time / this.page.total._time * 100;
2153
        if (baselineVersion == undefined) return value;
2154
        let baselineEntry = baselineVersion.getEntry(this);
2155
        if (!baselineEntry) return value;
2156 2157 2158
        if (baselineVersion === this.page.version) return value;
        return (this._time - baselineEntry._time) / this.page.total._time *
          100;
2159
      }
2160
      get timePercentPerEntry() {
2161
        let value = this._time / this.page.total._time * 100;
2162
        if (baselineVersion == undefined) return value;
2163
        let baselineEntry = baselineVersion.getEntry(this);
2164 2165 2166 2167
        if (!baselineEntry) return value;
        if (baselineVersion === this.page.version) return value;
        return (this._time / baselineEntry._time - 1) * 100;
      }
2168 2169 2170 2171 2172 2173 2174 2175
      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) {
2176
        return this.page.version.getTotalTimeVariancePercent(this.name, showDiff);
2177 2178 2179 2180 2181 2182 2183
      }
      getTimePercentImpact(showDiff) {
        return this.page.version.getTotalTimePercent(this.name, showDiff);
      }
      getCountImpact(showDiff) {
        return this.page.version.getTotalCount(this.name, showDiff);
      }
2184 2185 2186
      getAverageTimeImpact(showDiff) {
        return this.page.version.getAverageTimeImpact(this.name, showDiff);
      }
2187 2188 2189 2190
      getPagesByPercentImpact() {
        return this.page.version.getPagesByPercentImpact(this.name);
      }
      get isGroup() {
2191
        return false;
2192 2193
      }
      get timeVariance() {
2194
        return this._timeVariance;
2195 2196
      }
      get timeVariancePercent() {
2197
        return this._timeVariancePercent;
2198
      }
2199 2200 2201 2202 2203 2204 2205 2206 2207

      static fromLegacyJSON(position, data) {
        return new Entry(position, ...data);
      }

      static fromJSON(position, name, data) {
        let time = data.duration;
        let count = data.count;
        return new Entry(position, name, time.average, time.stddev, 0,
2208
            count.average, count.stddev, 0);
2209 2210 2211
      }

      static fromTXT(position, splitLine) {
2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222
        const name = splitLine[0];
        let time = splitLine[1];
        const msIndex = time.indexOf('m');
        if (msIndex > 0) time = time.substring(0, msIndex);
        const timePercent = splitLine[2];
        const count = splitLine[3];
        const countPercent = splitLine[4];
        const timeDeviation = 0;
        const countDeviation = 0;
        const timeDeviationPercent = 0;
        const countDeviationPercent = 0
2223 2224 2225 2226
        return new Entry(position, name,
          Number.parseFloat(time), timeDeviation, timeDeviationPercent,
          Number.parseInt(count), countDeviation, countDeviationPercent)
      }
2227
    }
2228

2229
    class Group {
2230
      constructor(name, regexp, color, enabled = true, addsToTotal = true) {
2231
        this.name = name;
2232
        this.regexp = regexp;
2233
        this.color = color;
2234
        this.enabled = enabled;
2235
        this.addsToTotal = addsToTotal;
2236
      }
2237 2238 2239
      entry() {
        return new GroupedEntry(this);
      }
2240 2241
    }
    Group.groups = new Map();
2242
    Group.add = function (name, group) {
2243
      this.groups.set(name, group);
2244
      return group;
2245
    }
2246
    Group.add('total', new Group('Total', /.*Total.*/, '#BBB', true, false));
2247
    Group.add('ic', new Group('IC', /(.*IC_.*)|IC/, "#3366CC"));
2248
    Group.add('optimize-background', new Group('Optimize-Background',
2249
      /.*Optimize(d?-?)(Background|Concurrent).*/, "#702000"));
2250
    Group.add('optimize', new Group('Optimize',
2251
      /(StackGuard|Optimize|Deoptimize|Recompile).*/, "#DC3912"));
2252
    Group.add('compile-background', new Group('Compile-Background',
2253
      /(.*Compile-?Background.*)/, "#b08000"));
2254
    Group.add('compile', new Group('Compile',
2255
      /(^Compile.*)|(.*_Compile.*)/, "#FFAA00"));
2256
    Group.add('parse-background',
2257
      new Group('Parse-Background', /.*Parse-?Background.*/, "#c05000"));
2258
    Group.add('parse', new Group('Parse', /.*Parse.*/, "#FF6600"));
2259
    Group.add('callback',
2260
      new Group('Blink C++', /.*(Callback)|(Blink C\+\+).*/, "#109618"));
2261
    Group.add('api', new Group('API', /.*API.*/, "#990099"));
2262
    Group.add('gc-custom', new Group('GC-Custom', /GC_Custom_.*/, "#0099C6"));
2263
    Group.add('gc-background',
2264 2265
      new Group(
        'GC-Background', /.*GC.*(BACKGROUND|Background).*/, "#00597c"));
2266
    Group.add('gc',
2267
      new Group('GC', /GC_.*|AllocateInTargetSpace|GC/, "#00799c"));
2268
    Group.add('javascript',
2269
      new Group('JavaScript', /JS_Execution|JavaScript/, "#DD4477"));
2270
    Group.add('websnapshot', new Group('WebSnapshot', /.*Web.*/, "#E8E11C"));
2271
    Group.add('runtime', new Group('V8 C++', /.*/, "#88BB00"));
2272
    Group.add('blink',
2273
      new Group('Blink RCS', /.*Blink_.*/, "#006600", false, false));
2274
    Group.add('unclassified', new Group('Unclassified', /.*/, "#000", false));
2275 2276 2277

    class GroupedEntry extends Entry {
      constructor(group) {
2278
        super(0, GroupedEntry.prefix + group.name, 0, 0, 0, 0, 0, 0);
2279
        this.group = group;
2280
        this.entries = [];
2281
        this.missingEntries = null;
2282
        this.addsToTotal = group.addsToTotal;
2283
      }
2284 2285 2286 2287 2288 2289 2290 2291 2292
      get regexp() {
        return this.group.regexp;
      }
      get color() {
        return this.group.color;
      }
      get enabled() {
        return this.group.enabled;
      }
2293
      add(entry) {
2294
        if (!this.addTimeAndCount(entry)) return;
2295 2296 2297 2298 2299
        // TODO: sum up variance
        this.entries.push(entry);
        entry.parent = this;
        return true;
      }
2300 2301 2302 2303 2304 2305
      addTimeAndCount(entry) {
        if (!this.regexp.test(entry.name)) return false;
        this._time += entry.time;
        this._count += entry.count;
        return true;
      }
2306
      _initializeMissingEntries() {
2307
        let dummyEntryNames = new Set();
2308
        versions.forEach((version) => {
2309 2310
          let page = version.getOrCreate(this.page.name);
          let groupEntry = page.get(this.name);
2311
          if (groupEntry != this) {
2312
            for (let entry of groupEntry.entries) {
2313 2314 2315 2316 2317
              if (this.page.get(entry.name) == undefined) {
                dummyEntryNames.add(entry.name);
              }
            }
          }
2318
        });
2319
        this.missingEntries = [];
2320 2321
        for (let name of dummyEntryNames) {
          let tmpEntry = new Entry(0, name, 0, 0, 0, 0, 0, 0);
2322
          tmpEntry.page = this.page;
2323
          this.missingEntries.push(tmpEntry);
2324
        };
2325 2326 2327
      }
      forEach(fun) {
        // Show also all entries which are in at least one version.
2328
        // Concatenate our real entries.
2329 2330 2331
        if (this.missingEntries == null) {
          this._initializeMissingEntries();
        }
2332
        let tmpEntries = this.missingEntries.concat(this.entries);
2333 2334

        // The compared entries are sorted by absolute impact.
2335
        tmpEntries.sort((a, b) => {
2336
          return b.time - a.time
2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352
        });
        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) {
2353
        let sum = 0;
2354
        const key = property + 'Variance';
2355
        this.entries.forEach((entry) => {
2356 2357
          const value = entry[key];
          sum += value * value;
2358 2359 2360 2361 2362
        });
        return Math.sqrt(sum);
      }
      get timeVariancePercent() {
        if (this._time == 0) return 0;
2363
        return this.getVarianceForProperty('time') / this._time * 100
2364 2365 2366 2367 2368
      }
      get timeVariance() {
        return this.getVarianceForProperty('time')
      }
    }
2369
    GroupedEntry.prefix = 'Group-';
2370 2371

    class UnclassifiedEntry extends GroupedEntry {
2372 2373
      constructor(page) {
        super(Group.groups.get('unclassified'));
2374 2375 2376 2377 2378
        this.page = page;
        this._time = undefined;
        this._count = undefined;
      }
      add(entry) {
2379
        console.log("Adding unclassified:", entry);
2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394
        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) => {
2395
            if (group.addsToTotal) this._time -= group._time;
2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412
          });
        }
        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>

2413
<body id="body" onmousemove="handleUpdatePopover(event)" onload="handleBodyLoad()" class="noDiff">
2414 2415
  <h1>Runtime Stats Komparator</h1>

2416 2417 2418 2419
  <section id="inputs" class="panel alwaysVisible">
    <input type="checkbox" id="inputsCheckbox" class="panelCloserInput">
    <label class="panelCloserLabel" for="inputsCheckbox"></label>
    <h2>Input/Output</h2>
2420
    <div class="panelBody">
2421 2422 2423
      <form name="fileForm" class="inline">
        <p class="inline">
          <label for="uploadInput">Load Files:</label>
2424 2425
          <input id="uploadInput" type="file" name="files" onchange="handleLoadFiles();" multiple
            accept=".json,.txt,.csv,.output">
2426
        </p>
2427
        <p class="inline">
2428
          <label for="appendInput">Append Files:</label>
2429 2430
          <input id="appendInput" type="file" name="files" onchange="handleAppendFiles();" multiple
            accept=".json,.txt,.csv,.output">
2431
        </p>
2432
      </form>
2433
      <p class="inline">
2434 2435
        <button onclick="handleCopyToClipboard()">Copy Table to Clipboard</button>
      </p>
2436
    </div>
2437 2438 2439 2440 2441
  </section>

  <section class="panel">
    <h2>Baseline Selector</h2>
    <div class="panel-body">
2442
      Compare against baseline:&nbsp;<select id="baseline" onchange="handleSelectBaseline(this, event)"></select><br />
2443 2444
      <span style="color: #060">Green</span> a selected version performs
      better than the baseline.
2445
    </div>
2446 2447 2448 2449 2450 2451 2452 2453
  </section>

  <section class="panel-group">
    <div id="versionSelector" class="panel">
      <input type="checkbox" checked id="versionSelectorCheckbox" class="panelCloserInput">
      <label class="panelCloserLabel" for="versionSelectorCheckbox"></label>
      <h2>Selected Versions</h2>
      <div class="panelBody">
2454 2455
        <ul></ul>
      </div>
2456
    </div>
2457

2458 2459 2460 2461 2462
    <div id="pageSelector" class="panel">
      <input type="checkbox" checked id="pageSelectorCheckbox" class="panelCloserInput">
      <label class="panelCloserLabel" for="pageSelectorCheckbox"></label>
      <h2>Selected Pages</h2>
      <div class="panelBody">
2463 2464 2465 2466
        <ul></ul>
      </div>
    </div>

2467 2468 2469 2470 2471
    <div id="groupSelector" class="panel">
      <input type="checkbox" checked id="groupSelectorCheckbox" class="panelCloserInput">
      <label class="panelCloserLabel" for="groupSelectorCheckbox"></label>
      <h2>Selected RCS Groups</h2>
      <div class="panelBody">
2472 2473 2474
        <ul></ul>
      </div>
    </div>
2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502
  </section>

  <section id="view" class="panel">
    <input type="checkbox" id="tableViewCheckbox" class="panelCloserInput">
    <label class="panelCloserLabel" for="tableViewCheckbox"></label>
    <h2>RCS Table</h2>
    <div class="panelBody"></div>
  </section>

  <section class="panel-group">
    <div id="versionDetails" class="panel">
      <input type="checkbox" checked id="versionDetailCheckbox" class="panelCloserInput">
      <label class="panelCloserLabel" for="versionDetailCheckbox"></label>
      <h2><span>Compare Page Versions</span></h2>
      <div class="conten panelBody">
        <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>
2503 2504
    </div>

2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530
    <div id="pageDetail" class="panel">
      <input type="checkbox" checked id="pageDetailCheckbox" class="panelCloserInput">
      <label class="panelCloserLabel" for="pageDetailCheckbox"></label>
      <h2>Page Comparison for <span></span></h2>
      <div class="panelBody">
        <table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
          <thead>
            <tr>
              <th class="page">Page&nbsp;</th>
              <th class="value time">Time&nbsp;</th>
              <th class="value time">Percent▾&nbsp;</th>
              <th class="value time hideNoDiff">%/Entry&nbsp;</th>
              <th class="value count">Count&nbsp;</th>
            </tr>
          </thead>
          <tfoot>
            <tr>
              <td class="page">Total:</td>
              <td class="value time"></td>
              <td class="value time"></td>
              <td class="value time hideNoDiff"></td>
              <td class="value count"></td>
            </tr>
          </tfoot>
          <tbody></tbody>
        </table>
2531 2532
      </div>
    </div>
2533 2534

    <div id="impactView" class="panel">
2535
      <input type="checkbox" checked id="impactViewCheckbox" class="panelCloserInput">
2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550
      <label class="panelCloserLabel" for="impactViewCheckbox"></label>
      <h2>Impact list for <span></span></h2>
      <div class="panelBody">
        <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>
2551
    </div>
2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578
  </section>

  <section id="pageVersionGraph" class="panel">
    <input type="checkbox" id="pageVersionGraphCheckbox" class="panelCloserInput">
    <label class="panelCloserLabel" for="pageVersionGraphCheckbox"></label>
    <h2><span></span></h2>
    <div class="panelBody"></div>
  </section>

  <section id="pageGraph" class="panel">
    <input type="checkbox" id="pageGraphCheckbox" class="panelCloserInput">
    <label class="panelCloserLabel" for="pageGraphCheckbox"></label>
    <h2><span></span></h2>
    <div class="panelBody"></div>
  </section>

  <section id="versionGraph" class="panel">
    <input type="checkbox" id="versionGraphCheckbox" class="panelCloserInput">
    <label class="panelCloserLabel" for="versionGraphCheckbox"></label>
    <h2><span></span></h2>
    <div class="panelBody"></div>
  </section>

  <div id="column" class="column">
    <div class="header">
      <select class="version" onchange="handleSelectVersion(this, event);"></select>
      <select class="pageVersion" onchange="handleSelectPage(this, event);"></select>
2579
    </div>
2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591
    <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>
2592
  </div>
2593

2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638
  <section class="panel alwaysVisible">
    <h2>Instructions</h2>
    <div class="panelBody">
      <ol>
        <li>Build chrome.</li>
      </ol>
      <h3>Telemetry benchmark</h3>
      <ol>
        <li>Run <code>v8.browsing</code> benchmarks:
          <pre>$CHROMIUM_DIR/tools/perf/run_benchmark run v8.browsing_desktop \
            --browser=exact --browser-executable=$CHROMIUM_DIR/out/release/chrome \
            --story-filter='.*2020 ' \
            --also-run-disabled-tests
          </pre>
        </li>
        <li>Install <a href="https://stedolan.github.io/jq/">jq</a>.</li>
        <li>Convert the telemetry JSON files to callstats JSON file:
          <pre>
            $V8_DIR/tools/callstats-from-telemetry.sh $CHROMIUM_DIR/tools/perf/artifacts/run_XXXX
          </pre>
        </li>
        <li>Load the generated <code>out.json</code></li>
      </ol>
      <h3>Merged CSV from results.html</h3>
      <ol>
        <li>Open a results.html page for RCS-enabled benchmarks</li>
        <li>Select "Export merged CSV" in the toolbar</li>
        <li>Load the downloading .csv file normally in callstats.html</li>
      </ol>
      <h3>Aggregated raw txt output</h3>
      <ol>
        <li>Install scipy, e.g. <code>sudo aptitude install python-scipy</code>
        <li>Check out a known working version of webpagereply:
          <pre>git -C $CHROME_DIR/third_party/webpagereplay checkout 7dbd94752d1cde5536ffc623a9e10a51721eff1d</pre>
        </li>
        <li>Run <code>callstats.py</code> with a web-page-replay archive:
          <pre>$V8_DIR/tools/callstats.py run \
          --replay-bin=$CHROME_SRC/third_party/webpagereplay/replay.py \
          --replay-wpr=$INPUT_DIR/top25.wpr \
          --js-flags="" \
          --with-chrome=$CHROME_SRC/out/Release/chrome \
          --sites-file=$INPUT_DIR/top25.json</pre>
        </li>
        <li>Move results file to a subdirectory: <code>mkdir $VERSION_DIR; mv *.txt $VERSION_DIR</code></li>
        <li>Repeat from step 1 with a different configuration (e.g. <code>--js-flags="--nolazy"</code>).</li>
2639 2640
        <li>Create the final results file: <code>./callstats.py json $VERSION_DIR1 $VERSION_DIR2 > result.json</code>
        </li>
2641 2642
        <li>Use <code>results.json</code> on this site.</code>
      </ol>
2643
    </div>
2644
  </section>
2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662

  <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>
2663 2664 2665 2666 2667 2668
        <td class="time"></td>
        <td>±</td>
        <td class="timeVariance"></td>
        <td class="compare time"></td>
        <td class="compare"> ± </td>
        <td class="compare timeVariance"></td>
2669 2670 2671
      </tr>
      <tr>
        <td>Percent:</td>
2672 2673 2674 2675 2676 2677
        <td class="percent"></td>
        <td>±</td>
        <td class="percentVariance"></td>
        <td class="compare percent"></td>
        <td class="compare"> ± </td>
        <td class="compare percentVariance"></td>
2678
      </tr>
2679 2680
      <tr>
        <td>Percent per Entry:</td>
2681 2682 2683 2684
        <td class="percentPerEntry"></td>
        <td colspan=2></td>
        <td class="compare percentPerEntry"></td>
        <td colspan=2></td>
2685
      </tr>
2686 2687
      <tr>
        <td>Count:</td>
2688 2689 2690 2691 2692 2693
        <td class="count"></td>
        <td>±</td>
        <td class="countVariance"></td>
        <td class="compare count"></td>
        <td class="compare"> ± </td>
        <td class="compare countVariance"></td>
2694 2695 2696
      </tr>
      <tr>
        <td>Overall Impact:</td>
2697 2698 2699 2700 2701 2702
        <td class="timeImpact"></td>
        <td>±</td>
        <td class="timePercentImpact"></td>
        <td class="compare timeImpact"></td>
        <td class="compare"> ± </td>
        <td class="compare timePercentImpact"></td>
2703 2704 2705 2706
      </tr>
    </table>
  </div>
</body>
2707 2708

</html>