1<!DOCTYPE html>
2<html>
3<!--
4Copyright 2016 the V8 project authors. All rights reserved.  Use of this source
5code is governed by a BSD-style license that can be found in the LICENSE file.
6-->
7
8<head>
9  <meta charset="utf-8">
10  <title>V8 Runtime Call Stats Komparator</title>
11  <link rel="stylesheet" type="text/css" href="system-analyzer/index.css">
12  <style>
13    body {
14      font-family: arial;
15    }
16
17    .panel {
18      display: none;
19    }
20
21    .loaded .panel {
22      display: block;
23    }
24
25    .panel.alwaysVisible {
26      display: inherit !important;
27    }
28
29    .error #inputs {
30      background-color: var(--error-color);
31    }
32
33    table {
34      display: table;
35      border-spacing: 0px;
36    }
37
38    tr {
39      border-spacing: 0px;
40      padding: 10px;
41    }
42
43    td,
44    th {
45      padding: 3px 10px 3px 5px;
46    }
47
48    .inline {
49      display: inline-block;
50      vertical-align: middle;
51      margin-right: 10px;
52    }
53
54    .hidden {
55      display: none;
56    }
57
58    .view {
59      display: table;
60    }
61
62    .panel-group {
63      display: grid;
64      align-content: center;
65      grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
66      grid-auto-flow: row dense;
67      grid-gap: 10px;
68      margin-top: 10px;
69    }
70
71    .column {
72      display: table-cell;
73      border-right: 1px black dotted;
74      min-width: 200px;
75    }
76
77    .column .header {
78      padding: 0 10px 0 10px
79    }
80
81    #column {
82      display: none;
83    }
84
85    .list {
86      width: 100%;
87    }
88
89    select {
90      width: 100%
91    }
92
93    .list tbody {
94      cursor: pointer;
95    }
96
97    .list tr:nth-child(even) {
98      background-color: rgba(0.5, 0.5, 0.5, 0.1);
99    }
100
101    .list tr.child {
102      display: none;
103    }
104
105    .list tr.child.visible {
106      display: table-row;
107    }
108
109    .list .child .name {
110      padding-left: 20px;
111    }
112
113    .list .parent td {
114      border-top: 1px solid #AAA;
115    }
116
117    .list .total {
118      font-weight: bold
119    }
120
121    .list tr.parent.selected,
122    .list tr:nth-child(even).selected,
123    tr.selected {
124      background-color: rgba(0.5, 0.5, 0.5, 0.1);
125    }
126
127    .codeSearch {
128      display: block-inline;
129      float: right;
130      border-radius: 5px;
131      background-color: #333;
132      width: 1em;
133      text-align: center;
134    }
135
136    .list .position {
137      text-align: right;
138      display: none;
139    }
140
141    .list div.toggle {
142      cursor: pointer;
143    }
144
145    #column_0 .position {
146      display: table-cell;
147    }
148
149    #column_0 .name {
150      display: table-cell;
151    }
152
153    .list .name {
154      display: none;
155      white-space: nowrap;
156    }
157
158    .value {
159      text-align: right;
160    }
161
162    .selectedVersion {
163      font-weight: bold;
164    }
165
166    #baseline {
167      width: auto;
168    }
169
170    .pageDetailTable tbody {
171      cursor: pointer
172    }
173
174    .pageDetailTable tfoot td {
175      border-top: 1px grey solid;
176    }
177
178    #popover {
179      position: absolute;
180      transform: translateY(-50%) translateX(40px);
181      box-shadow: -2px 10px 44px -10px #000;
182      border-radius: 5px;
183      z-index: 1;
184      background-color: var(--surface-color);
185      display: none;
186      white-space: nowrap;
187    }
188
189    #popover table {
190      position: relative;
191      z-index: 1;
192      text-align: right;
193      margin: 10px;
194    }
195
196    #popover td {
197      padding: 3px 0px 3px 5px;
198      white-space: nowrap;
199    }
200
201    .popoverArrow {
202      background-color: var(--surface-color);
203      position: absolute;
204      width: 30px;
205      height: 30px;
206      transform: translateY(-50%)rotate(45deg);
207      top: 50%;
208      left: -10px;
209      z-index: 0;
210    }
211
212    #popover .name {
213      padding: 5px;
214      font-weight: bold;
215      text-align: center;
216    }
217
218    #popover table .compare {
219      display: none
220    }
221
222    #popover table.compare .compare {
223      display: table-cell;
224    }
225
226    #popover .compare .time,
227    #popover .compare .version {
228      padding-left: 10px;
229    }
230
231    .diff .hideDiff {
232      display: none;
233    }
234
235    .noDiff .hideNoDiff {
236      display: none;
237    }
238  </style>
239  <script src="https://www.gstatic.com/charts/loader.js"></script>
240  <script>
241    "use strict"
242    google.charts.load('current', {
243      packages: ['corechart']
244    });
245
246    // Did anybody say monkeypatching?
247    if (!NodeList.prototype.forEach) {
248      NodeList.prototype.forEach = function (func) {
249        for (let i = 0; i < this.length; i++) {
250          func(this[i]);
251        }
252      }
253    }
254
255    let versions;
256    let pages;
257    let selectedPage;
258    let baselineVersion;
259    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      let viewBody = $("view").querySelector('.panelBody');
269      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        column.id = "column_" + i;
276        // Fill in all versions
277        let select = column.querySelector(".version");
278        select.id = "selectVersion_" + i;
279        // add all select options
280        versions.forEach((version) => {
281          if (!version.enabled) return;
282          let option = document.createElement("option");
283          option.textContent = version.name;
284          option.version = version;
285          select.appendChild(option);
286        });
287        // Fill in all page versions
288        select = column.querySelector(".pageVersion");
289        select.id = "select_" + i;
290        // add all pages
291        versions.forEach((version) => {
292          if (!version.enabled) return;
293          let optgroup = document.createElement("optgroup");
294          optgroup.label = version.name;
295          optgroup.version = version;
296          version.forEachPage((page) => {
297            let option = document.createElement("option");
298            option.textContent = page.name;
299            option.page = page;
300            optgroup.appendChild(option);
301          });
302          select.appendChild(optgroup);
303        });
304        viewBody.appendChild(column);
305        i++;
306      });
307
308      let select = $('baseline');
309      removeAllChildren(select);
310      select.appendChild(document.createElement('option'));
311      versions.forEach((version) => {
312        let option = document.createElement("option");
313        option.textContent = version.name;
314        option.version = version;
315        select.appendChild(option);
316      });
317      initializeToggleList(versions.versions, $('versionSelector'));
318      initializeToggleList(pages.values(), $('pageSelector'));
319      initializeToggleList(Group.groups.values(), $('groupSelector'));
320    }
321
322    function initializeToggleList(items, node) {
323      let list = node.querySelector('ul');
324      removeAllChildren(list);
325      items = Array.from(items);
326      items.sort(NameComparator);
327      items.forEach((item) => {
328        let li = document.createElement('li');
329        let checkbox = document.createElement('input');
330        checkbox.type = 'checkbox';
331        checkbox.checked = item.enabled;
332        checkbox.item = item;
333        checkbox.addEventListener('click', handleToggleVersionOrPageEnable);
334        li.appendChild(checkbox);
335        li.appendChild(document.createTextNode(item.name));
336        list.appendChild(li);
337      });
338    }
339
340    window.addEventListener('popstate', (event) => {
341      popHistoryState(event.state);
342    });
343
344    function popHistoryState(state) {
345      if (!state.version) return false;
346      if (!versions) return false;
347      let version = versions.getByName(state.version);
348      if (!version) return false;
349      let page = version.get(state.page);
350      if (!page) return false;
351      if (!state.entry) {
352        showEntry(page.total);
353      } else {
354        let entry = page.get(state.entry);
355        if (!entry) {
356          showEntry(page.total);
357        } else {
358          showEntry(entry);
359        }
360      }
361      return true;
362    }
363
364    function pushHistoryState() {
365      let selection = selectedEntry ? selectedEntry : selectedPage;
366      if (!selection) return;
367      let state = selection.urlParams();
368      // Don't push a history state if it didn't change.
369      if (JSON.stringify(window.history.state) === JSON.stringify(state)) return;
370      let params = "?";
371      for (let pairs of Object.entries(state)) {
372        params += encodeURIComponent(pairs[0]) + "=" +
373          encodeURIComponent(pairs[1]) + "&";
374      }
375      window.history.pushState(state, selection.toString(), params);
376    }
377
378    function showSelectedEntryInPage(page) {
379      if (!selectedEntry) return showPage(page);
380      let entry = page.get(selectedEntry.name);
381      if (!entry) return showPage(page);
382      selectEntry(entry);
383    }
384
385    function showPage(firstPage) {
386      let changeSelectedEntry = selectedEntry !== undefined &&
387        selectedEntry.page === selectedPage;
388      selectedPage = firstPage;
389      selectedPage.sort();
390      showPageInColumn(firstPage, 0);
391      // Show the other versions of this page in the following columns.
392      let pageVersions = versions.getPageVersions(firstPage);
393      let index = 1;
394      pageVersions.forEach((page) => {
395        if (page !== firstPage) {
396          showPageInColumn(page, index);
397          index++;
398        }
399      });
400      if (changeSelectedEntry) {
401        showEntryDetail(selectedPage.getEntry(selectedEntry));
402      }
403      showImpactList(selectedPage);
404      pushHistoryState();
405    }
406
407    function clamp(value, min, max) {
408      if (value < min) return min;
409      if (value > max) return max;
410      return value;
411    }
412
413    function diffColorFromRatio(ratio) {
414      if (ratio == Infinity) {
415        return '#ff0000';
416      }
417      if (ratio == -Infinity) {
418        return '#00ff00';
419      }
420      if (ratio > 1) {
421        // ratio > 1: #FFFFFF => #00FF00
422        const red = clamp(((ratio - 1) * 255 * 10) | 0, 0, 255);
423        const other = (255 - red).toString(16).padStart(2, '0');
424        return `#ff${other}${other}`;
425      }
426      // ratio < 1: #FF0000 => #FFFFFF
427      const green = clamp(((1 - ratio) * 255 * 10) | 0, 0, 255);
428      const other = (255 - green).toString(16).padStart(2, '0');
429      return `#${other}ff${other}`;
430    }
431
432    function showPageInColumn(page, columnIndex) {
433      page.sort();
434      let showDiff = columnIndex !== 0;
435      if (baselineVersion) showDiff = page.version !== baselineVersion;
436      let diffColor = (td, a, b) => { };
437      if (showDiff) {
438        if (baselineVersion) {
439          diffColor = (td, diff, baseline) => {
440            if (diff == 0) return;
441            const ratio = (baseline + diff) / baseline;
442            td.style.color = diffColorFromRatio(ratio);
443          };
444        } else {
445          diffColor = (td, value, reference) => {
446            if (value == reference) return;
447            const ratio = value / reference;
448            td.style.color = diffColorFromRatio(ratio);
449          }
450        }
451      }
452
453      let column = $('column_' + columnIndex);
454      let select = $('select_' + columnIndex);
455      // Find the matching option
456      selectOption(select, (i, option) => {
457        return option.page == page
458      });
459      let table = column.querySelector("table");
460      let oldTbody = table.querySelector('tbody');
461      let tbody = document.createElement('tbody');
462      let referencePage = selectedPage;
463      page.forEachSorted(selectedPage, (parentEntry, entry, referenceEntry) => {
464        let tr = document.createElement('tr');
465        tbody.appendChild(tr);
466        tr.entry = entry;
467        tr.parentEntry = parentEntry;
468        tr.className = parentEntry === undefined ? 'parent' : 'child';
469        // Don't show entries that do not exist on the current page or if we
470        // compare against the current page
471        if (entry !== undefined && page.version !== baselineVersion) {
472          // If we show a diff, use the baselineVersion as the referenceEntry
473          if (baselineVersion !== undefined) {
474            let baselineEntry = baselineVersion.getEntry(entry);
475            if (baselineEntry !== undefined) referenceEntry = baselineEntry
476          }
477          if (!parentEntry) {
478            let node = td(tr, '<div class="toggle">►</div>', 'position');
479            node.firstChild.addEventListener('click', handleToggleGroup);
480          } else {
481            td(tr, entry.position == 0 ? '' : entry.position, 'position');
482          }
483          addCodeSearchButton(entry,
484            td(tr, entry.name, 'name ' + entry.cssClass()));
485
486          diffColor(
487            td(tr, ms(entry.time), 'value time'),
488            entry.time, referenceEntry.time);
489          diffColor(
490            td(tr, percent(entry.timePercent), 'value time'),
491            entry.time, referenceEntry.time);
492          diffColor(
493            td(tr, count(entry.count), 'value count'),
494            entry.count, referenceEntry.count);
495        } else if (baselineVersion !== undefined && referenceEntry &&
496          page.version !== baselineVersion) {
497          // Show comparison of entry that does not exist on the current page.
498          tr.entry = new Entry(0, referenceEntry.name);
499          tr.entry.page = page;
500          td(tr, '-', 'position');
501          td(tr, referenceEntry.name, 'name');
502          diffColor(
503            td(tr, ms(referenceEntry.time), 'value time'),
504            referenceEntry.time, 0);
505          diffColor(
506            td(tr, percent(referenceEntry.timePercent), 'value time'),
507            referenceEntry.timePercent, 0);
508          diffColor(
509            td(tr, count(referenceEntry.count), 'value count'),
510            referenceEntry.count, 0);
511        } else {
512          // 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              node.firstChild.addEventListener('click', handleToggleGroup);
518            } else {
519              td(tr, entry.position == 0 ? '' : entry.position, 'position');
520            }
521            td(tr, entry.name, 'name');
522            td(tr, ms(entry.time, false), 'value time');
523            td(tr, percent(entry.timePercent, false), 'value time');
524            td(tr, count(entry.count, false), 'value count');
525          } else {
526            td(tr, '-', 'position');
527            td(tr, referenceEntry.name, 'name');
528            td(tr, '-', 'value time');
529            td(tr, '-', 'value time');
530            td(tr, '-', 'value count');
531          }
532        }
533      });
534      table.replaceChild(tbody, oldTbody);
535      let versionSelect = column.querySelector('select.version');
536      selectOption(versionSelect, (index, option) => {
537        return option.version == page.version
538      });
539    }
540
541    function showEntry(entry) {
542      selectEntry(entry, true);
543    }
544
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      // If clicked in the detail row change the first column to that page.
553      if (needsPageSwitch) showPage(entry.page);
554      let childNodes = $('column_0').querySelector('.list tbody').childNodes;
555      for (let i = 0; i < childNodes.length; i++) {
556        if (childNodes[i].entry !== undefined &&
557          childNodes[i].entry.name == entry.name) {
558          rowIndex = i;
559          break;
560        }
561      }
562      let firstEntry = childNodes[rowIndex].entry;
563      if (rowIndex) {
564        if (firstEntry.parent) showGroup(firstEntry.parent);
565      }
566      // Deselect all
567      $('view').querySelectorAll('.list tbody tr').forEach((tr) => {
568        toggleCssClass(tr, 'selected', false);
569      });
570      // Select the entry row
571      $('view').querySelectorAll("tbody").forEach((body) => {
572        let row = body.childNodes[rowIndex];
573        if (!row) return;
574        toggleCssClass(row, 'selected', row.entry && row.entry.name ==
575          firstEntry.name);
576      });
577      if (updateSelectedPage && selectedEntry) {
578        entry = selectedEntry.page.version.getEntry(entry);
579      }
580      if (entry !== selectedEntry) {
581        selectedEntry = entry;
582        showEntryDetail(entry);
583      }
584    }
585
586    function showEntryDetail(entry) {
587      showVersionDetails(entry);
588      showPageDetails(entry);
589      showImpactList(entry.page);
590      showGraphs(entry.page);
591      pushHistoryState();
592    }
593
594    function showVersionDetails(entry) {
595      let table, tbody, entries;
596      table = $('versionDetails').querySelector('.versionDetailTable');
597      tbody = document.createElement('tbody');
598      if (entry !== undefined) {
599        $('versionDetails').querySelector('h2 span').textContent =
600          entry.name + ' in ' + entry.page.name;
601        entries = versions.getPageVersions(entry.page).map(
602          (page) => {
603            return page.get(entry.name)
604          });
605        entries.sort((a, b) => {
606          return a.time - b.time
607        });
608        entries.forEach((pageEntry) => {
609          if (pageEntry === undefined) return;
610          let tr = document.createElement('tr');
611          if (pageEntry == entry) tr.className += 'selected';
612          tr.entry = pageEntry;
613          let isBaselineEntry = pageEntry.page.version == baselineVersion;
614          td(tr, pageEntry.page.version.name, 'version');
615          td(tr, ms(pageEntry.time, !isBaselineEntry), 'value time');
616          td(tr, percent(pageEntry.timePercent, !isBaselineEntry), 'value time');
617          td(tr, count(pageEntry.count, !isBaselineEntry), 'value count');
618          tbody.appendChild(tr);
619        });
620      }
621      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      if (entry === undefined) {
629        table.replaceChild(tbody, table.querySelector('tbody'));
630        return;
631      }
632      let version = entry.page.version;
633      let showDiff = version !== baselineVersion;
634      $('pageDetail').querySelector('h2 span').textContent =
635        version.name;
636      entries = version.pages.map((page) => {
637        if (!page.enabled) return;
638        return page.get(entry.name)
639      });
640      entries.sort((a, b) => {
641        let cmp = b.timePercent - a.timePercent;
642        if (cmp.toFixed(1) == 0) return b.time - a.time;
643        return cmp
644      });
645      entries.forEach((pageEntry) => {
646        if (pageEntry === undefined) return;
647        let tr = document.createElement('tr');
648        if (pageEntry === entry) tr.className += 'selected';
649        tr.entry = pageEntry;
650        td(tr, pageEntry.page.name, 'name');
651        td(tr, ms(pageEntry.time, showDiff), 'value time');
652        td(tr, percent(pageEntry.timePercent, showDiff), 'value time');
653        td(tr, percent(pageEntry.timePercentPerEntry, showDiff),
654          'value time hideNoDiff');
655        td(tr, count(pageEntry.count, showDiff), 'value count');
656        tbody.appendChild(tr);
657      });
658      // 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      tds[2].textContent = percent(entry.getTimePercentImpact(), showDiff);
663      tds[3].textContent = '';
664      tds[4].textContent = count(entry.getCountImpact(), showDiff);
665      table.replaceChild(tbody, table.querySelector('tbody'));
666    }
667
668    function showImpactList(page) {
669      let impactView = $('impactView');
670      impactView.querySelector('h2 span').textContent = page.version.name;
671
672      let table = impactView.querySelector('table');
673      let tbody = document.createElement('tbody');
674      let version = page.version;
675      let entries = version.allEntries();
676      if (selectedEntry !== undefined && selectedEntry.isGroup) {
677        impactView.querySelector('h2 span').textContent += " " + selectedEntry.name;
678        entries = entries.filter((entry) => {
679          return entry.name == selectedEntry.name ||
680            (entry.parent && entry.parent.name == selectedEntry.name)
681        });
682      }
683      let isCompareView = baselineVersion !== undefined;
684      entries = entries.filter((entry) => {
685        if (isCompareView) {
686          let impact = entry.getTimeImpact();
687          return impact < -1 || 1 < impact
688        }
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        if (isCompareView || cmp.toFixed(1) == 0) {
695          return b.getTimeImpact() - a.getTimeImpact();
696        }
697        return cmp
698      });
699      entries.forEach((entry) => {
700        let tr = document.createElement('tr');
701        tr.entry = entry;
702        td(tr, entry.name, 'name');
703        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            return each.name + ' (' + percent(each.getEntry(entry).timePercent) +
709              ')'
710          });
711        td(tr, topPages.join(', '), 'name');
712        tbody.appendChild(tr);
713      });
714      table.replaceChild(tbody, table.querySelector('tbody'));
715    }
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      if (selectedGroup == undefined) {
722        selectedGroup = groups[0];
723      } else {
724        groups = groups.filter(each => each.name != selectedGroup.name);
725        if (!selectedGroup.isTotal && selectedGroup.enabled) {
726          groups.unshift(selectedGroup);
727        }
728      }
729      // Display graphs delayed for a snappier UI.
730      setTimeout(() => {
731        showPageVersionGraph(groups, page);
732        showPageGraph(groups, page);
733        showVersionGraph(groups, page);
734      }, 10);
735    }
736
737    function getGraphDataTable(groups, page) {
738      let dataTable = new google.visualization.DataTable();
739      dataTable.addColumn('string', 'Name');
740      groups.forEach(group => {
741        let column = dataTable.addColumn('number', group.name.substring(6));
742        dataTable.setColumnProperty(column, 'group', group);
743        column = dataTable.addColumn({
744          role: "annotation"
745        });
746        dataTable.setColumnProperty(column, 'group', group);
747      });
748      let column = dataTable.addColumn('number', 'Chart Total');
749      dataTable.setColumnProperty(column, 'group', page.total);
750      column = dataTable.addColumn({
751        role: "annotation"
752      });
753      dataTable.setColumnProperty(column, 'group', page.total);
754      return dataTable;
755    }
756
757    let selectedGroup;
758
759    class ChartRow {
760      static kSortFirstValueRelative(chartRow) {
761        if (selectedGroup?.isTotal) return chartRow.total;
762        return chartRow.data[0] / chartRow.total;
763      }
764
765      static kSortByFirstValue(chartRow) {
766        if (selectedGroup?.isTotal) return chartRow.total;
767        return chartRow.data[0];
768      }
769
770      constructor(linkedPage, label, sortValue_fn, data,
771        excludeFromAverage = false) {
772        this.linkedPage = linkedPage;
773        this.label = label;
774        if (!Array.isArray(data)) {
775          throw new Error("Provide an Array for data");
776        }
777        this.data = data;
778        this.total = 0;
779        for (let i = 0; i < data.length; i++) this.total += data[i];
780        this.sortValue = sortValue_fn(this);
781        this.excludeFromAverage = excludeFromAverage;
782      }
783
784      forDataTable(maxRowsTotal) {
785        // row = [label, entry1, annotation1, entry2, annotation2, ...]
786        const rowData = [this.label];
787        const kShowLabelLimit = 0.1;
788        const kMinLabelWidth = 80;
789        const chartWidth = window.innerWidth - 400;
790        // Add value,label pairs
791        for (let i = 0; i < this.data.length; i++) {
792          const value = this.data[i];
793          let label = '';
794          // Only show labels for entries that are large enough..
795          if (Math.abs(value / maxRowsTotal) * chartWidth > kMinLabelWidth) {
796            label = ms(value);
797          }
798          rowData.push(value, label);
799        }
800        // Add the total row, with very small negative dummy entry for correct
801        // placement of labels in diff view.
802        rowData.push(this.total >= 0 ? 0 : -0.000000001, ms(this.total));
803        return rowData;
804      }
805    }
806    const collator = new Intl.Collator('en-UK');
807
808    function setDataTableRows(dataTable, rows) {
809      let skippedRows = 0;
810      // Always sort by the selected entry (first column after the label)
811      if (sortByLabel) {
812        rows.sort((a, b) => collator.compare(a.label, b.label));
813      } else {
814        rows.sort((a, b) => b.sortValue - a.sortValue);
815      }
816      // Aggregate row data for Average/SUM chart entry:
817      const aggregateData = rows[0].data.slice().fill(0);
818      let maxTotal = 0;
819      for (let i = 0; i < rows.length; i++) {
820        const row = rows[i];
821        let total = Math.abs(row.total);
822        if (total > maxTotal) maxTotal = total;
823        if (row.excludeFromAverage) {
824          skippedRows++;
825          continue
826        }
827        const chartRowData = row.data;
828        for (let j = 0; j < chartRowData.length; j++) {
829          aggregateData[j] += chartRowData[j];
830        }
831      }
832      const length = rows.length - skippedRows;
833      for (let i = 0; i < aggregateData.length; i++) {
834        aggregateData[i] /= rows.length;
835      }
836      const averageRow = new ChartRow(undefined, 'Average',
837        ChartRow.kSortByFirstValue, aggregateData);
838      dataTable.addRow(averageRow.forDataTable());
839
840      rows.forEach(chartRow => {
841        let rowIndex = dataTable.addRow(chartRow.forDataTable(maxTotal));
842        dataTable.setRowProperty(rowIndex, 'page', chartRow.linkedPage);
843      });
844    }
845
846    function showPageVersionGraph(groups, page) {
847      let dataTable = getGraphDataTable(groups, page);
848      let vs = versions.getPageVersions(page);
849      // Calculate the entries for the versions
850      const rows = vs.map(page => new ChartRow(
851        page, page.version.name, ChartRow.kSortByFirstValue,
852        groups.map(group => page.getEntry(group).time),
853        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      // Calculate the average row
862      // 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        page, page.name,
867        isDiffView ?
868          ChartRow.kSortByFirstValue : ChartRow.kSortFirstValueRelative,
869        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        version.get(page), version.name, ChartRow.kSortByFirstValue,
880        groups.map(group => version.getEntry(group).getTimeImpact()),
881        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        suffix: (isDiffView ? 'msΔ' : 'ms'),
891        negativeColor: 'red',
892        groupingSymbol: "'"
893      });
894      for (let i = 1; i < dataTable.getNumberOfColumns(); i++) {
895        formatter.format(dataTable, i);
896      }
897      let height = 85 + 28 * dataTable.getNumberOfRows();
898      let options = {
899        isStacked: isStacked,
900        height: height,
901        hAxis: {
902          minValue: 0,
903          textStyle: {
904            fontSize: 14
905          }
906        },
907        vAxis: {
908          textStyle: {
909            fontSize: 14
910          }
911        },
912        tooltip: {
913          textStyle: {
914            fontSize: 14
915          }
916        },
917        annotations: {
918          textStyle: {
919            fontSize: 8
920          }
921        },
922        explorer: {
923          actions: ['dragToZoom', 'rightClickToReset'],
924          maxZoomIn: 0.01
925        },
926        legend: {
927          position: 'top',
928          maxLines: 3,
929          textStyle: {
930            fontSize: 12
931          }
932        },
933        chartArea: {
934          left: 200,
935          top: 50
936        },
937        colors: [
938          ...groups.map(each => each.color),
939          /* Chart Total */
940          "#000000",
941        ]
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      if (chart === undefined) {
949        chart = graphNode.chart = new google.visualization.BarChart(graphNode);
950      } else {
951        google.visualization.events.removeAllListeners(chart);
952      }
953      google.visualization.events.addListener(chart, 'select', selectHandler);
954
955      function getChartEntry(selection) {
956        if (!selection) return undefined;
957        let column = selection.column;
958        if (column == undefined) return undefined;
959        let selectedGroup = dataTable.getColumnProperty(column, 'group');
960        let row = selection.row;
961        if (row == null) return selectedGroup;
962        let page = dataTable.getRowProperty(row, 'page');
963        if (!page) return selectedGroup;
964        return page.getEntry(selectedGroup);
965      }
966
967      function selectHandler(e) {
968        const newSelectedGroup = getChartEntry(chart.getSelection()[0]);
969        if (newSelectedGroup == selectedGroup) {
970          sortByLabel = !sortByLabel;
971        } else if (newSelectedGroup === undefined && selectedPage) {
972          sortByLabel = true;
973          return showGraphs(selectedPage);
974        } else {
975          sortByLabel = false;
976        }
977        selectedGroup = newSelectedGroup;
978        selectEntry(selectedGroup, true);
979      }
980
981      // Make our global tooltips work
982      google.visualization.events.addListener(chart, 'onmouseover', mouseOverHandler);
983
984      function mouseOverHandler(selection) {
985        const selectedGroup = getChartEntry(selection);
986        graphNode.entry = selectedGroup;
987      }
988      chart.draw(dataTable, options);
989    }
990
991    function showGroup(entry) {
992      toggleGroup(entry, true);
993    }
994
995    function toggleGroup(group, show) {
996      $('view').querySelectorAll(".child").forEach((tr) => {
997        let entry = tr.parentEntry;
998        if (!entry) return;
999        if (entry.name !== group.name) return;
1000        toggleCssClass(tr, 'visible', show);
1001      });
1002    }
1003
1004    function showPopover(entry) {
1005      let popover = $('popover');
1006      popover.querySelector('td.name').textContent = entry.name;
1007      popover.querySelector('td.page').textContent = entry.page.name;
1008      setPopoverDetail(popover, entry, '');
1009      popover.querySelector('table').className = "";
1010      if (baselineVersion !== undefined) {
1011        entry = baselineVersion.getEntry(entry);
1012        setPopoverDetail(popover, entry, '.compare');
1013        popover.querySelector('table').className = "compare";
1014      }
1015    }
1016
1017    function setPopoverDetail(popover, entry, prefix) {
1018      let node = (name) => popover.querySelector(prefix + name);
1019      if (entry == undefined) {
1020        node('.version').textContent = baselineVersion.name;
1021        node('.time').textContent = '-';
1022        node('.timeVariance').textContent = '-';
1023        node('.percent').textContent = '-';
1024        node('.percentPerEntry').textContent = '-';
1025        node('.percentVariance').textContent = '-';
1026        node('.count').textContent = '-';
1027        node('.countVariance').textContent = '-';
1028        node('.timeImpact').textContent = '-';
1029        node('.timePercentImpact').textContent = '-';
1030      } else {
1031        node('.version').textContent = entry.page.version.name;
1032        node('.time').textContent = ms(entry._time, false);
1033        node('.timeVariance').textContent = percent(entry.timeVariancePercent, false);
1034        node('.percent').textContent = percent(entry.timePercent, false);
1035        node('.percentPerEntry').textContent = percent(entry.timePercentPerEntry, false);
1036        node('.percentVariance').textContent = percent(entry.timePercentVariancePercent, false);
1037        node('.count').textContent = count(entry._count, false);
1038        node('.countVariance').textContent = percent(entry.timeVariancePercent, false);
1039        node('.timeImpact').textContent = ms(entry.getTimeImpact(false), false);
1040        node('.timePercentImpact').textContent = percent(entry.getTimeImpactVariancePercent(false), false);
1041      }
1042    }
1043  </script>
1044  <script>
1045    "use strict"
1046    // =========================================================================
1047    // Helpers
1048    function $(id) {
1049      return document.getElementById(id)
1050    }
1051
1052    function removeAllChildren(node) {
1053      while (node.firstChild) {
1054        node.removeChild(node.firstChild);
1055      }
1056    }
1057
1058    function selectOption(select, match) {
1059      let options = select.options;
1060      for (let i = 0; i < options.length; i++) {
1061        if (match(i, options[i])) {
1062          select.selectedIndex = i;
1063          return;
1064        }
1065      }
1066    }
1067
1068    function addCodeSearchButton(entry, node) {
1069      if (entry.isGroup) return;
1070      let button = document.createElement("div");
1071      button.textContent = '?'
1072      button.className = "codeSearch"
1073      button.addEventListener('click', handleCodeSearch);
1074      node.appendChild(button);
1075      return node;
1076    }
1077
1078    function td(tr, content, className) {
1079      let td = document.createElement("td");
1080      if (content[0] == '<') {
1081        td.innerHTML = content;
1082      } else {
1083        td.textContent = content;
1084      }
1085      td.className = className
1086      tr.appendChild(td);
1087      return td
1088    }
1089
1090    function nodeIndex(node) {
1091      let children = node.parentNode.childNodes,
1092        i = 0;
1093      for (; i < children.length; i++) {
1094        if (children[i] == node) {
1095          return i;
1096        }
1097      }
1098      return -1;
1099    }
1100
1101    function toggleCssClass(node, cssClass, toggleState = true) {
1102      let index = -1;
1103      let classes;
1104      if (node.className != undefined) {
1105        classes = node.className.split(' ');
1106        index = classes.indexOf(cssClass);
1107      }
1108      if (index == -1) {
1109        if (toggleState === false) return;
1110        node.className += ' ' + cssClass;
1111        return;
1112      }
1113      if (toggleState === true) return;
1114      classes.splice(index, 1);
1115      node.className = classes.join(' ');
1116    }
1117
1118    function NameComparator(a, b) {
1119      if (a.name > b.name) return 1;
1120      if (a.name < b.name) return -1;
1121      return 0
1122    }
1123
1124    function diffSign(value, digits, unit, showDiff) {
1125      if (showDiff === false || baselineVersion == undefined) {
1126        if (value === undefined) return '';
1127        return value.toFixed(digits) + unit;
1128      }
1129      return (value >= 0 ? '+' : '') + value.toFixed(digits) + unit + 'Δ';
1130    }
1131
1132    function ms(value, showDiff) {
1133      return diffSign(value, 1, 'ms', showDiff);
1134    }
1135
1136    function count(value, showDiff) {
1137      return diffSign(value, 0, '#', showDiff);
1138    }
1139
1140    function percent(value, showDiff) {
1141      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        displayResultsAfterLoading();
1153      }
1154    }
1155
1156    function tryLoadDefaultData() {
1157      if (!defaultData) return false;
1158      handleLoadJSON(defaultData);
1159      return true;
1160    }
1161
1162    async function tryLoadFromURLParams() {
1163      let params = new URLSearchParams(document.location.search);
1164      let hasFile = false;
1165      params.forEach(async (value, key) => {
1166        if (key !== 'file') return;
1167        hasFile ||= await tryLoadFile(value, true);
1168      });
1169      return hasFile;
1170    }
1171
1172    async function tryLoadDefaultResults() {
1173      if (window.location.protocol === 'file:') return false;
1174      // Try to load a results.json file adjacent to this day.
1175      // The markers on the following line can be used to replace the url easily
1176      // with scripts.
1177      const url = /*results-url-start*/ 'results.json' /*results-url-end*/;
1178      return tryLoadFile(url);
1179    }
1180
1181    async function tryLoadFile(url, append = false) {
1182      if (!url.startsWith('http')) {
1183        // hack to get relative urls
1184        let location = window.location;
1185        let parts = location.pathname.split("/").slice(0, -1);
1186        url = location.origin + parts.join('/') + '/' + url;
1187      }
1188      let response = await fetch(url);
1189      if (!response.ok) return false;
1190      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      loadFiles(files, true);
1199    }
1200
1201    function handleLoadFiles() {
1202      let files = document.getElementById("uploadInput").files;
1203      loadFiles(files, false)
1204    }
1205
1206    async function loadFiles(files, append) {
1207      for (let i = 0; i < files.length; i++) {
1208        const file = files[i];
1209        console.log(file.name);
1210        let text = await new Promise((resolve, reject) => {
1211          const reader = new FileReader();
1212          reader.onload = () => resolve(reader.result)
1213          reader.readAsText(file);
1214        });
1215        handleLoadText(text, append, file.name);
1216        // Only the first file might clear existing data, all sequent files
1217        // are always append.
1218        append = true;
1219      }
1220      displayResultsAfterLoading();
1221      toggleCssClass(document.body, "loaded");
1222    }
1223
1224    function handleLoadText(text, append, fileName) {
1225      if (fileName.endsWith('.json')) {
1226        handleLoadJSON(JSON.parse(text), append, fileName);
1227      } else if (fileName.endsWith('.csv') ||
1228        fileName.endsWith('.output') || fileName.endsWith('.output.txt')) {
1229        handleLoadCSV(text, append, fileName);
1230      } else if (fileName.endsWith('.txt')) {
1231        handleLoadTXT(text, append, fileName);
1232      } else {
1233        alert(`Unsupported file extension: "${fileName}"`);
1234      }
1235    }
1236
1237    function getStateFromParams() {
1238      let query = window.location.search.substr(1);
1239      let result = {};
1240      query.split("&").forEach((part) => {
1241        let item = part.split("=");
1242        let key = decodeURIComponent(item[0])
1243        result[key] = decodeURIComponent(item[1]);
1244      });
1245      return result;
1246    }
1247
1248    function handleLoadJSON(json, append, fileName) {
1249      json = fixClusterTelemetryResults(json);
1250      json = fixTraceImportJSON(json);
1251      json = fixSingleVersionJSON(json, fileName);
1252      let isFirstLoad = pages === undefined;
1253      if (append && !isFirstLoad) {
1254        json = createUniqueVersions(json);
1255      }
1256      if (!append || isFirstLoad) {
1257        pages = new Pages();
1258        versions = Versions.fromJSON(json);
1259      } else {
1260        Versions.fromJSON(json).forEach(e => versions.add(e))
1261      }
1262    }
1263
1264    function handleLoadCSV(csv, append, fileName) {
1265      let isFirstLoad = pages === undefined;
1266      if (!append || isFirstLoad) {
1267        pages = new Pages();
1268        versions = new Versions();
1269      }
1270      const lines = csv.split(/\r?\n/);
1271      // The first line contains only the field names.
1272      const fields = new Map();
1273      csvSplit(lines[0]).forEach((name, index) => {
1274        fields.set(name, index);
1275      });
1276      if (fields.has('displayLabel') && fields.has('stories')) {
1277        handleLoadResultCSV(fields, lines);
1278      } else if (fields.has('page_name')) {
1279        handleLoadClusterTelemetryCSV(fields, lines, fileName);
1280      } else {
1281        return alert("Unknown CSV format");
1282      }
1283    }
1284
1285    function csvSplit(line) {
1286      let fields = [];
1287      let index = 0;
1288      while (index < line.length) {
1289        let lastIndex = index;
1290        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          // Consume ','
1295          index++;
1296        } else {
1297          index = line.indexOf(',', lastIndex);
1298          if (index === -1) index = line.length;
1299          fields.push(line.substring(lastIndex, index))
1300        }
1301        // Consume ','
1302        index++;
1303      }
1304      return fields;
1305    }
1306
1307    // Ignore the following categories as they are aggregated values and are
1308    // created by callstats.html on the fly.
1309    const import_skip_categories = new Set([
1310      'V8-Only', 'V8-Only-Main-Thread', 'Total-Main-Thread', 'Blink_Total'
1311    ])
1312
1313    function handleLoadClusterTelemetryCSV(fields, lines, fileName) {
1314      const rscFields = Array.from(fields.keys())
1315        .filter(field => {
1316          return field.endsWith(':duration (ms)') &&
1317            !import_skip_categories.has(field.split(':')[0])
1318        })
1319        .map(field => {
1320          let name = field.split(':')[0];
1321          return [name, fields.get(field), fields.get(`${name}:count`)];
1322        })
1323      const page_name_i = fields.get('page_name');
1324      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        let page_name = line[page_name_i];
1329        if (page_name === undefined) continue;
1330        page_name = page_name.split(' ')[0];
1331        const pageVersion = version.getOrCreate(page_name);
1332        for (let [fieldName, duration_i, count_i] of rscFields) {
1333          const duration = Number.parseFloat(line[duration_i]);
1334          const count = Number.parseFloat(line[count_i]);
1335          // Skip over entries without metrics (most likely crashes)
1336          if (Number.isNaN(count) || Number.isNaN(duration)) {
1337            console.warn(`BROKEN ${page_name}`, lines[i])
1338            break;
1339          }
1340          pageVersion.add(new Entry(0, fieldName, duration, 0, 0, count, 0, 0))
1341        }
1342      }
1343    }
1344
1345    function handleLoadResultCSV(fields, lines) {
1346      const version_i = fields.get('displayLabel');
1347      const page_i = fields.get('stories');
1348      const category_i = fields.get('name');
1349      const value_i = fields.get('avg');
1350      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        const raw_category = line[category_i];
1355        if (!raw_category.endsWith(':duration') &&
1356          !raw_category.endsWith(':count')) {
1357          continue;
1358        }
1359        let [category, type] = raw_category.split(':');
1360        if (import_skip_categories.has(category)) continue;
1361        const version = versions.getOrCreate(line[version_i]);
1362        const pageVersion = version.getOrCreate(line[page_i]);
1363        const value = Number.parseFloat(line[value_i]);
1364        const entry = TempEntry.get(tempEntriesCache, pageVersion, category);
1365        if (type == 'duration') {
1366          entry.durations.push(value)
1367        } else {
1368          entry.counts.push(value)
1369        }
1370      }
1371
1372      tempEntriesCache.forEach((tempEntries, pageVersion) => {
1373        tempEntries.forEach(tmpEntry => {
1374          pageVersion.add(tmpEntry.toEntry())
1375        })
1376      });
1377    }
1378
1379    class TempEntry {
1380      constructor(category) {
1381        this.category = category;
1382        this.durations = [];
1383        this.counts = [];
1384      }
1385
1386      static get(cache, pageVersion, category) {
1387        let tempEntries = cache.get(pageVersion);
1388        if (tempEntries === undefined) {
1389          tempEntries = new Map();
1390          cache.set(pageVersion, tempEntries);
1391        }
1392        let tempEntry = tempEntries.get(category);
1393        if (tempEntry === undefined) {
1394          tempEntry = new TempEntry(category);
1395          tempEntries.set(category, tempEntry);
1396        }
1397        return tempEntry;
1398      }
1399
1400      toEntry() {
1401        const [duration, durationStddev] = this.stats(this.durations);
1402        const [count, countStddev] = this.stats(this.durations);
1403        return new Entry(0, this.category,
1404          duration, durationStddev, 0, count, countStddev, 0)
1405      }
1406
1407      stats(values) {
1408        let sum = 0;
1409        for (let i = 0; i < values.length; i++) {
1410          sum += values[i];
1411        }
1412        const avg = sum / values.length;
1413        let stddevSquared = 0;
1414        for (let i = 0; i < values.length; i++) {
1415          const delta = values[i] - avg;
1416          stddevSquared += delta * delta;
1417        }
1418        const stddev = Math.sqrt(stddevSquared / values.length);
1419        return [avg, stddev];
1420      }
1421    }
1422
1423    function handleLoadTXT(txt, append, fileName) {
1424      fileName = window.prompt('Version name:', fileName);
1425      let isFirstLoad = pages === undefined;
1426      // Load raw RCS output which contains a single page
1427      if (!append || isFirstLoad) {
1428        pages = new Pages();
1429        versions = new Versions()
1430      }
1431      versions.add(Version.fromTXT(fileName, txt));
1432
1433    }
1434
1435    function displayResultsAfterLoading() {
1436      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      showPage(page);
1446      showEntry(page.total);
1447    }
1448
1449    function fixClusterTelemetryResults(json) {
1450      // Convert CT results to callstats compatible JSON
1451      // Input:
1452      // { VERSION_NAME: { PAGE: { METRIC: { "count": {XX}, "duration": {XX} }.. }}.. }
1453      let firstEntry;
1454      for (let key in json) {
1455        firstEntry = json[key];
1456        break;
1457      }
1458      // Return the original JSON if it is not a CT result.
1459      if (firstEntry.pairs === undefined) return json;
1460      // The results include already the group totals, remove them by filtering.
1461      let groupNames = new Set(Array.from(Group.groups.values()).map(e => e.name));
1462      let result = Object.create(null);
1463      for (let file_name in json) {
1464        let entries = [];
1465        let file_data = json[file_name].pairs;
1466        for (let name in file_data) {
1467          if (name != "Total" && groupNames.has(name)) continue;
1468          let entry = file_data[name];
1469          let count = entry.count;
1470          let time = entry.time;
1471          entries.push([name, time, 0, 0, count, 0, 0]);
1472        }
1473        let domain = file_name.split("/").slice(-1)[0];
1474        result[domain] = entries;
1475      }
1476      return {
1477        __proto__: null,
1478        ClusterTelemetry: result
1479      };
1480    }
1481
1482    function fixTraceImportJSON(json) {
1483      // Fix json file that was created by converting a trace json output
1484      if (!('telemetry-results' in json)) return json;
1485      // { telemetry-results: { PAGE:[ { METRIC: [ COUNT TIME ], ... }, ... ]}}
1486      let version_data = {
1487        __proto__: null
1488      };
1489      json = json["telemetry-results"];
1490      for (let page_name in json) {
1491        if (page_name == "placeholder") continue;
1492        let page_data = {
1493          __proto__: null,
1494          Total: {
1495            duration: {
1496              average: 0,
1497              stddev: 0
1498            },
1499            count: {
1500              average: 0,
1501              stddev: 0
1502            }
1503          }
1504        };
1505        let page = json[page_name];
1506        for (let slice of page) {
1507          for (let metric_name in slice) {
1508            if (metric_name == "Blink_V8") continue;
1509            // sum up entries
1510            if (!(metric_name in page_data)) {
1511              page_data[metric_name] = {
1512                duration: {
1513                  average: 0,
1514                  stddev: 0
1515                },
1516                count: {
1517                  average: 0,
1518                  stddev: 0
1519                }
1520              }
1521            }
1522            let [metric_count, metric_duration] = slice[metric_name]
1523            let metric = page_data[metric_name];
1524            const kMicroToMilli = 1 / 1000;
1525            metric.duration.average += metric_duration * kMicroToMilli;
1526            metric.count.average += metric_count;
1527
1528            if (metric_name.startsWith('Blink_')) continue;
1529            let total = page_data['Total'];
1530            total.duration.average += metric_duration * kMicroToMilli;
1531            total.count.average += metric_count;
1532          }
1533        }
1534        version_data[page_name] = page_data;
1535      }
1536      return version_data;
1537    }
1538
1539    function fixSingleVersionJSON(json, name) {
1540      // Try to detect the single-version case, where we're missing the toplevel
1541      // version object. The incoming JSON is of the form:
1542      //   { PAGE: ... , PAGE_2:  }
1543      // Instead of the default multi-page JSON:
1544      //    {"Version 1": { "Page 1": ..., ...}, "Version 2": {...}, ...}
1545      // In this case insert a single "Default" version as top-level entry.
1546      const  firstProperty = (object) => {
1547        for (let key in object) return object[key];
1548      };
1549      const maybeMetrics = firstProperty(json);
1550      const maybeMetric = firstProperty(maybeMetrics);
1551      const tempName = name ? name : new Date().toISOString();
1552      const getFileName =
1553          () => window.prompt('Enter a name for the loaded file:', tempName);
1554      if ('count' in maybeMetric && 'duration' in maybeMetric) {
1555        return {
1556          [getFileName()]: json
1557        }
1558      }
1559      // Legacy fallback where the metrics are encoded as arrays:
1560      //  { PAGE: [[metric_name, ...], [...], ]}
1561      // Also, make sure we don't have the versioned array-style:
1562      // { VERSION: { PAGE: [[metric_name, ...], [...], ]}, ...}
1563      const innerArray = firstProperty(maybeMetrics);
1564      if (Array.isArray(maybeMetric) && !Array.isArray(innerArray)) {
1565        return {
1566          [getFileName()]: json
1567        }
1568      }
1569      return json
1570    }
1571
1572    let appendIndex = 0;
1573
1574    function createUniqueVersions(json) {
1575      // Make sure all toplevel entries are unique names and added properly
1576      appendIndex++;
1577      let result = {
1578        __proto__: null
1579      }
1580      for (let key in json) {
1581        result[key + "_" + appendIndex] = json[key];
1582      }
1583      return result
1584    }
1585
1586    function handleCopyToClipboard(event) {
1587      const names = ["Group", ...versions.versions.map(e => e.name)];
1588      let result = [names.join("\t")];
1589      let groups = Array.from(Group.groups.values());
1590      // Move the total group to the end.
1591      groups.push(groups.shift())
1592      groups.forEach(group => {
1593        let row = [group.name];
1594        versions.forEach(v => {
1595          const time = v.pages[0].get("Group-" + group.name)?._time ?? 0;
1596          row.push(time);
1597        })
1598        result.push(row.join("\t"));
1599      });
1600      result = result.join("\n");
1601      navigator.clipboard.writeText(result);
1602    }
1603
1604    function handleToggleGroup(event) {
1605      let group = event.target.parentNode.parentNode.entry;
1606      toggleGroup(selectedPage.get(group.name), 'toggle');
1607    }
1608
1609    function handleSelectPage(select, event) {
1610      let option = select.options[select.selectedIndex];
1611      if (select.id == "select_0") {
1612        showSelectedEntryInPage(option.page);
1613      } else {
1614        let columnIndex = select.id.split('_')[1];
1615        showPageInColumn(option.page, columnIndex);
1616      }
1617    }
1618
1619    function handleSelectVersion(select, event) {
1620      let option = select.options[select.selectedIndex];
1621      let version = option.version;
1622      if (select.id == "selectVersion_0") {
1623        let page = version.get(selectedPage.name);
1624        showSelectedEntryInPage(page);
1625      } else {
1626        let columnIndex = select.id.split('_')[1];
1627        let pageSelect = $('select_' + columnIndex);
1628        let page = pageSelect.options[pageSelect.selectedIndex].page;
1629        page = version.get(page.name);
1630        showPageInColumn(page, columnIndex);
1631      }
1632    }
1633
1634    function handleSelectDetailRow(table, event) {
1635      if (event.target.tagName != 'TD') return;
1636      let tr = event.target.parentNode;
1637      if (tr.tagName != 'TR') return;
1638      if (tr.entry === undefined) return;
1639      selectEntry(tr.entry, true);
1640    }
1641
1642    function handleSelectRow(table, event, fromDetail) {
1643      if (event.target.tagName != 'TD') return;
1644      let tr = event.target.parentNode;
1645      if (tr.tagName != 'TR') return;
1646      if (tr.entry === undefined) return;
1647      selectEntry(tr.entry, false);
1648    }
1649
1650    function handleSelectBaseline(select, event) {
1651      let option = select.options[select.selectedIndex];
1652      baselineVersion = option.version;
1653      let showingDiff = baselineVersion !== undefined;
1654      let body = $('body');
1655      toggleCssClass(body, 'diff', showingDiff);
1656      toggleCssClass(body, 'noDiff', !showingDiff);
1657      showPage(selectedPage);
1658      if (selectedEntry === undefined) return;
1659      selectEntry(selectedEntry, true);
1660    }
1661
1662    function findEntry(event) {
1663      let target = event.target;
1664      while (target.entry === undefined) {
1665        target = target.parentNode;
1666        if (!target) return undefined;
1667      }
1668      return target.entry;
1669    }
1670
1671    function handleUpdatePopover(event) {
1672      let popover = $('popover');
1673      popover.style.left = event.pageX + 'px';
1674      popover.style.top = event.pageY + 'px';
1675      popover.style.display = 'none';
1676      popover.style.display = event.shiftKey ? 'block' : 'none';
1677      let entry = findEntry(event);
1678      if (entry === undefined) return;
1679      showPopover(entry);
1680    }
1681
1682    function handleToggleVersionOrPageEnable(event) {
1683      let item = this.item;
1684      if (item === undefined) return;
1685      item.enabled = this.checked;
1686      initialize();
1687      let page = selectedPage;
1688      if (page === undefined || !page.version.enabled) {
1689        page = versions.getEnabledPage(page.name);
1690      }
1691      if (!page.enabled) {
1692        page = page.getNextPage();
1693      }
1694      showPage(page);
1695    }
1696
1697    function handleCodeSearch(event) {
1698      let entry = findEntry(event);
1699      if (entry === undefined) return;
1700      let url = "https://cs.chromium.org/search/?sq=package:chromium&type=cs&q=";
1701      name = entry.name;
1702      if (name.startsWith("API_")) {
1703        name = name.substring(4);
1704      }
1705      url += encodeURIComponent(name) + "+file:src/v8/src";
1706      window.open(url, '_blank');
1707    }
1708  </script>
1709  <script>
1710    "use strict"
1711    // =========================================================================
1712    class Versions {
1713      constructor() {
1714        this.versions = [];
1715      }
1716      add(version) {
1717        this.versions.push(version);
1718        return version;
1719      }
1720      getPageVersions(page) {
1721        let result = [];
1722        this.versions.forEach((version) => {
1723          if (!version.enabled) return;
1724          let versionPage = version.get(page.name);
1725          if (versionPage !== undefined) result.push(versionPage);
1726        });
1727        return result;
1728      }
1729      get length() {
1730        return this.versions.length
1731      }
1732      get(index) {
1733        return this.versions[index]
1734      }
1735      getByName(name) {
1736        return this.versions.find((each) => each.name == name);
1737      }
1738      getOrCreate(name) {
1739        return this.getByName(name) ?? this.add(new Version(name));
1740      }
1741      forEach(f) {
1742        this.versions.forEach(f);
1743      }
1744      sort() {
1745        this.versions.sort(NameComparator);
1746      }
1747      getEnabledPage(name) {
1748        for (let i = 0; i < this.versions.length; i++) {
1749          let version = this.versions[i];
1750          if (!version.enabled) continue;
1751          let page = version.get(name);
1752          if (page !== undefined) return page;
1753        }
1754      }
1755
1756      static fromJSON(json) {
1757        let versions = new Versions();
1758        for (let version in json) {
1759          versions.add(Version.fromJSON(version, json[version]));
1760        }
1761        versions.sort();
1762        return versions;
1763      }
1764    }
1765
1766    class Version {
1767      constructor(name) {
1768        this.name = name;
1769        this.enabled = true;
1770        this.pages = [];
1771      }
1772      add(page) {
1773        this.pages.push(page);
1774        return page;
1775      }
1776      indexOf(name) {
1777        for (let i = 0; i < this.pages.length; i++) {
1778          if (this.pages[i].name == name) return i;
1779        }
1780        return -1;
1781      }
1782      getNextPage(page) {
1783        if (this.length == 0) return undefined;
1784        return this.pages[(this.indexOf(page.name) + 1) % this.length];
1785      }
1786      get(name) {
1787        let index = this.indexOf(name);
1788        if (0 <= index) return this.pages[index];
1789        return undefined;
1790      }
1791      getOrCreate(name) {
1792        return this.get(name) ??
1793            this.add(new PageVersion(this, pages.getOrCreate(name)));
1794      }
1795      get length() {
1796        return this.pages.length;
1797      }
1798      getEntry(entry) {
1799        if (entry === undefined) return undefined;
1800        let page = this.get(entry.page.name);
1801        if (page === undefined) return undefined;
1802        return page.get(entry.name);
1803      }
1804      forEachEntry(fun) {
1805        this.forEachPage((page) => {
1806          page.forEach(fun);
1807        });
1808      }
1809      forEachPage(fun) {
1810        this.pages.forEach((page) => {
1811          if (!page.enabled) return;
1812          fun(page);
1813        })
1814      }
1815      allEntries() {
1816        let map = new Map();
1817        this.forEachEntry((group, entry) => {
1818          if (!map.has(entry.name)) map.set(entry.name, entry);
1819        });
1820        return Array.from(map.values());
1821      }
1822      getTotalValue(name, property) {
1823        if (name === undefined) name = this.pages[0].total.name;
1824        let sum = 0;
1825        this.forEachPage((page) => {
1826          let entry = page.get(name);
1827          if (entry !== undefined) sum += entry[property];
1828        });
1829        return sum;
1830      }
1831      getTotalTime(name, showDiff) {
1832        return this.getTotalValue(name, showDiff === false ? '_time' : 'time');
1833      }
1834      getTotalTimePercent(name, showDiff) {
1835        if (baselineVersion === undefined || showDiff === false) {
1836          // Return the overall average percent of the given entry name.
1837          return this.getTotalValue(name, 'time') /
1838            this.getTotalTime('Group-Total') * 100;
1839        }
1840        // Otherwise return the difference to the sum of the baseline version.
1841        let baselineValue = baselineVersion.getTotalTime(name, false);
1842        let total = this.getTotalValue(name, '_time');
1843        return (total / baselineValue - 1) * 100;
1844      }
1845      getTotalTimeVariance(name, showDiff) {
1846        // Calculate the overall error for a given entry name
1847        let sum = 0;
1848        this.forEachPage((page) => {
1849          let entry = page.get(name);
1850          if (entry === undefined) return;
1851          sum += entry.timeVariance * entry.timeVariance;
1852        });
1853        return Math.sqrt(sum);
1854      }
1855      getTotalTimeVariancePercent(name, showDiff) {
1856        return this.getTotalTimeVariance(name, showDiff) /
1857          this.getTotalTime(name, showDiff) * 100;
1858      }
1859      getTotalCount(name, showDiff) {
1860        return this.getTotalValue(name, showDiff === false ? '_count' : 'count');
1861      }
1862      getAverageTimeImpact(name, showDiff) {
1863        return this.getTotalTime(name, showDiff) / this.pages.length;
1864      }
1865      getPagesByPercentImpact(name) {
1866        let sortedPages =
1867          this.pages.filter((each) => {
1868            return each.get(name) !== undefined
1869          });
1870        sortedPages.sort((a, b) => {
1871          return b.get(name).timePercent - a.get(name).timePercent;
1872        });
1873        return sortedPages;
1874      }
1875      sort() {
1876        this.pages.sort(NameComparator)
1877      }
1878
1879      static fromJSON(name, data) {
1880        let version = new Version(name);
1881        for (let pageName in data) {
1882          version.add(PageVersion.fromJSON(version, pageName, data[pageName]));
1883        }
1884        version.sort();
1885        return version;
1886      }
1887
1888      static fromTXT(name, txt) {
1889        let version = new Version(name);
1890        let defaultName = "RAW DATA";
1891        PageVersion.fromTXT(version, defaultName, txt)
1892          .forEach(each => version.add(each));
1893        return version;
1894      }
1895    }
1896
1897    class Pages extends Map {
1898      get(name) {
1899        if (name.indexOf('www.') == 0) {
1900          name = name.substring(4);
1901        }
1902        if (!this.has(name)) {
1903          this.set(name, new Page(name));
1904        }
1905        return super.get(name);
1906      }
1907      getOrCreate(name) {
1908        return this.get(name);
1909      }
1910    }
1911
1912    class Page {
1913      constructor(name) {
1914        this.name = name;
1915        this.enabled = true;
1916        this.versions = [];
1917      }
1918      add(pageVersion) {
1919        this.versions.push(pageVersion);
1920        return pageVersion;
1921      }
1922    }
1923
1924    class PageVersion {
1925      constructor(version, page) {
1926        this.page = page;
1927        this.page.add(this);
1928        this.total = Group.groups.get('total').entry();
1929        this.total.isTotal = true;
1930        this.unclassified = new UnclassifiedEntry(this)
1931        this.groups = [
1932          this.total,
1933          Group.groups.get('ic').entry(),
1934          Group.groups.get('optimize-background').entry(),
1935          Group.groups.get('optimize').entry(),
1936          Group.groups.get('compile-background').entry(),
1937          Group.groups.get('compile').entry(),
1938          Group.groups.get('parse-background').entry(),
1939          Group.groups.get('parse').entry(),
1940          Group.groups.get('blink').entry(),
1941          Group.groups.get('callback').entry(),
1942          Group.groups.get('api').entry(),
1943          Group.groups.get('gc-custom').entry(),
1944          Group.groups.get('gc-background').entry(),
1945          Group.groups.get('gc').entry(),
1946          Group.groups.get('javascript').entry(),
1947          Group.groups.get('websnapshot').entry(),
1948          Group.groups.get('runtime').entry(),
1949          this.unclassified
1950        ];
1951        this.entryDict = new Map();
1952        this.groups.forEach((entry) => {
1953          entry.page = this;
1954          this.entryDict.set(entry.name, entry);
1955        });
1956        this.version = version;
1957      }
1958      toString() {
1959        return this.version.name + ": " + this.name;
1960      }
1961      urlParams() {
1962        return {
1963          version: this.version.name,
1964          page: this.name
1965        };
1966      }
1967      add(entry) {
1968        let existingEntry = this.entryDict.get(entry.name);
1969        if (existingEntry !== undefined) {
1970          // Duplicate entries happen when multiple runs are combined into a
1971          // single file.
1972          existingEntry.add(entry);
1973          for (let i = 0; i < this.groups.length; i++) {
1974            const group = this.groups[i];
1975            if (group.addTimeAndCount(entry)) return;
1976          }
1977        } else {
1978          // Ignore accidentally added Group entries.
1979          if (entry.name.startsWith(GroupedEntry.prefix)) {
1980            console.warn("Skipping accidentally added Group entry:", entry, this);
1981            return;
1982          }
1983          entry.page = this;
1984          this.entryDict.set(entry.name, entry);
1985          for (let group of this.groups) {
1986            if (group.add(entry)) return;
1987          }
1988        }
1989        console.error("Should not get here", entry);
1990      }
1991      get(name) {
1992        return this.entryDict.get(name)
1993      }
1994      getEntry(entry) {
1995        if (entry === undefined) return undefined;
1996        return this.get(entry.name);
1997      }
1998      get length() {
1999        return this.versions.length
2000      }
2001      get name() {
2002        return this.page.name
2003      }
2004      get enabled() {
2005        return this.page.enabled
2006      }
2007      forEachSorted(referencePage, func) {
2008        // Iterate over all the entries in the order they appear on the
2009        // reference page.
2010        referencePage.forEach((parent, referenceEntry) => {
2011          let entry;
2012          if (parent) parent = this.entryDict.get(parent.name);
2013          if (referenceEntry) entry = this.entryDict.get(referenceEntry.name);
2014          func(parent, entry, referenceEntry);
2015        });
2016      }
2017      forEach(fun) {
2018        this.forEachGroup((group) => {
2019          fun(undefined, group);
2020          group.forEach((entry) => {
2021            fun(group, entry)
2022          });
2023        });
2024      }
2025      forEachGroup(fun) {
2026        this.groups.forEach(fun)
2027      }
2028      sort() {
2029        this.groups.sort((a, b) => {
2030          return b.time - a.time;
2031        });
2032        this.groups.forEach((group) => {
2033          group.sort()
2034        });
2035      }
2036      distanceFromTotalPercent() {
2037        let sum = 0;
2038        this.groups.forEach(group => {
2039          if (group == this.total) return;
2040          let value = group.getTimePercentImpact() -
2041            this.getEntry(group).timePercent;
2042          sum += value * value;
2043        });
2044        return sum;
2045      }
2046      getNextPage() {
2047        return this.version.getNextPage(this);
2048      }
2049
2050      static fromJSON(version, name, data) {
2051        let page = new PageVersion(version, pages.get(name));
2052        // Distinguish between the legacy format which just uses Arrays,
2053        // or the new object style.
2054        if (Array.isArray(data)) {
2055          for (let i = 0; i < data.length; i++) {
2056            page.add(Entry.fromLegacyJSON(i, data[data.length - i - 1]));
2057          }
2058        } else {
2059          let position = 0;
2060          for (let metric_name in data) {
2061            page.add(Entry.fromJSON(position, metric_name, data[metric_name]));
2062            position++;
2063          }
2064        }
2065        page.sort();
2066        return page
2067      }
2068
2069      static fromTXT(version, defaultName, txt) {
2070        const kPageNameIdentifier = "== Page:";
2071        const kCommentStart = "=="
2072        const lines = txt.split('\n');
2073        const split = / +/g
2074        const result = [];
2075        let pageVersion = undefined;
2076        for (let i = 0; i < lines.length; i++) {
2077          const line = lines[i];
2078          // Skip header separators
2079          if (line.startsWith(kCommentStart)) {
2080            // Check for page names
2081            if (line.startsWith(kPageNameIdentifier)) {
2082              const name = line.split(kPageNameIdentifier)[1];
2083              pageVersion = new PageVersion(version, pages.get(name));
2084              result.push(pageVersion);
2085            }
2086          }
2087          // Skip header lines.
2088          if (lines[i + 1]?.startsWith(kCommentStart)) continue;
2089          const split_line = line.trim().split(split)
2090          if (split_line.length != 5) continue;
2091          if (pageVersion === undefined) {
2092            pageVersion = new PageVersion(version, pages.get(defaultName));
2093            result.push(pageVersion);
2094          }
2095          const position = i - 2;
2096          pageVersion.add(Entry.fromTXT(position, split_line));
2097        }
2098        return result;
2099      }
2100    }
2101
2102
2103    class Entry {
2104      constructor(position, name, time, timeVariance, timeVariancePercent,
2105        count, countVariance, countVariancePercent) {
2106        this.position = position;
2107        this.name = name;
2108        this._time = time;
2109        this._timeVariance = timeVariance;
2110        this._timeVariancePercent =
2111            this._variancePercent(time, timeVariance, timeVariancePercent);
2112        this._count = count;
2113        this.countVariance = countVariance;
2114        this.countVariancePercent =
2115            this._variancePercent(count, countVariance, countVariancePercent);
2116        this.page = undefined;
2117        this.parent = undefined;
2118        this.isTotal = false;
2119      }
2120      _variancePercent(value, valueVariance, valueVariancePercent) {
2121        if (valueVariancePercent) return valueVariancePercent;
2122        if (!valueVariance) return 0;
2123        return valueVariance / value * 100;
2124      }
2125
2126      add(entry) {
2127        if (this.name !== entry.name) {
2128          console.error("Should not combine entries with different names");
2129          return;
2130        }
2131        this._time += entry._time;
2132        this._count += entry._count;
2133      }
2134      urlParams() {
2135        let params = this.page.urlParams();
2136        params.entry = this.name;
2137        return params;
2138      }
2139      getCompareWithBaseline(value, property) {
2140        if (baselineVersion == undefined) return value;
2141        let baselineEntry = baselineVersion.getEntry(this);
2142        if (!baselineEntry) return value;
2143        if (baselineVersion === this.page.version) return value;
2144        return value - baselineEntry[property];
2145      }
2146      cssClass() {
2147        return ''
2148      }
2149      get time() {
2150        return this.getCompareWithBaseline(this._time, '_time');
2151      }
2152      get count() {
2153        return this.getCompareWithBaseline(this._count, '_count');
2154      }
2155      get timePercent() {
2156        let value = this._time / this.page.total._time * 100;
2157        if (baselineVersion == undefined) return value;
2158        let baselineEntry = baselineVersion.getEntry(this);
2159        if (!baselineEntry) return value;
2160        if (baselineVersion === this.page.version) return value;
2161        return (this._time - baselineEntry._time) / this.page.total._time *
2162          100;
2163      }
2164      get timePercentPerEntry() {
2165        let value = this._time / this.page.total._time * 100;
2166        if (baselineVersion == undefined) return value;
2167        let baselineEntry = baselineVersion.getEntry(this);
2168        if (!baselineEntry) return value;
2169        if (baselineVersion === this.page.version) return value;
2170        return (this._time / baselineEntry._time - 1) * 100;
2171      }
2172      get timePercentVariancePercent() {
2173        // Get the absolute values for the percentages
2174        return this.timeVariance / this.page.total._time * 100;
2175      }
2176      getTimeImpact(showDiff) {
2177        return this.page.version.getTotalTime(this.name, showDiff);
2178      }
2179      getTimeImpactVariancePercent(showDiff) {
2180        return this.page.version.getTotalTimeVariancePercent(this.name, showDiff);
2181      }
2182      getTimePercentImpact(showDiff) {
2183        return this.page.version.getTotalTimePercent(this.name, showDiff);
2184      }
2185      getCountImpact(showDiff) {
2186        return this.page.version.getTotalCount(this.name, showDiff);
2187      }
2188      getAverageTimeImpact(showDiff) {
2189        return this.page.version.getAverageTimeImpact(this.name, showDiff);
2190      }
2191      getPagesByPercentImpact() {
2192        return this.page.version.getPagesByPercentImpact(this.name);
2193      }
2194      get isGroup() {
2195        return false;
2196      }
2197      get timeVariance() {
2198        return this._timeVariance;
2199      }
2200      get timeVariancePercent() {
2201        return this._timeVariancePercent;
2202      }
2203
2204      static fromLegacyJSON(position, data) {
2205        return new Entry(position, ...data);
2206      }
2207
2208      static fromJSON(position, name, data) {
2209        let time = data.duration;
2210        let count = data.count;
2211        return new Entry(position, name, time.average, time.stddev, 0,
2212            count.average, count.stddev, 0);
2213      }
2214
2215      static fromTXT(position, splitLine) {
2216        const name = splitLine[0];
2217        let time = splitLine[1];
2218        const msIndex = time.indexOf('m');
2219        if (msIndex > 0) time = time.substring(0, msIndex);
2220        const timePercent = splitLine[2];
2221        const count = splitLine[3];
2222        const countPercent = splitLine[4];
2223        const timeDeviation = 0;
2224        const countDeviation = 0;
2225        const timeDeviationPercent = 0;
2226        const countDeviationPercent = 0
2227        return new Entry(position, name,
2228          Number.parseFloat(time), timeDeviation, timeDeviationPercent,
2229          Number.parseInt(count), countDeviation, countDeviationPercent)
2230      }
2231    }
2232
2233    class Group {
2234      constructor(name, regexp, color, enabled = true, addsToTotal = true) {
2235        this.name = name;
2236        this.regexp = regexp;
2237        this.color = color;
2238        this.enabled = enabled;
2239        this.addsToTotal = addsToTotal;
2240      }
2241      entry() {
2242        return new GroupedEntry(this);
2243      }
2244    }
2245    Group.groups = new Map();
2246    Group.add = function (name, group) {
2247      this.groups.set(name, group);
2248      return group;
2249    }
2250    Group.add('total', new Group('Total', /.*Total.*/, '#BBB', true, false));
2251    Group.add('ic', new Group('IC', /(.*IC_.*)|IC/, "#3366CC"));
2252    Group.add('optimize-background', new Group('Optimize-Background',
2253      /.*Optimize(d?-?)(Background|Concurrent).*/, "#702000"));
2254    Group.add('optimize', new Group('Optimize',
2255      /(StackGuard|Optimize|Deoptimize|Recompile).*/, "#DC3912"));
2256    Group.add('compile-background', new Group('Compile-Background',
2257      /(.*Compile-?Background.*)/, "#b08000"));
2258    Group.add('compile', new Group('Compile',
2259      /(^Compile.*)|(.*_Compile.*)/, "#FFAA00"));
2260    Group.add('parse-background',
2261      new Group('Parse-Background', /.*Parse-?Background.*/, "#c05000"));
2262    Group.add('parse', new Group('Parse', /.*Parse.*/, "#FF6600"));
2263    Group.add('callback',
2264      new Group('Blink C++', /.*(Callback)|(Blink C\+\+).*/, "#109618"));
2265    Group.add('api', new Group('API', /.*API.*/, "#990099"));
2266    Group.add('gc-custom', new Group('GC-Custom', /GC_Custom_.*/, "#0099C6"));
2267    Group.add('gc-background',
2268      new Group(
2269        'GC-Background', /.*GC.*(BACKGROUND|Background).*/, "#00597c"));
2270    Group.add('gc',
2271      new Group('GC', /GC_.*|AllocateInTargetSpace|GC/, "#00799c"));
2272    Group.add('javascript',
2273      new Group('JavaScript', /JS_Execution|JavaScript/, "#DD4477"));
2274    Group.add('websnapshot', new Group('WebSnapshot', /.*Web.*/, "#E8E11C"));
2275    Group.add('runtime', new Group('V8 C++', /.*/, "#88BB00"));
2276    Group.add('blink',
2277      new Group('Blink RCS', /.*Blink_.*/, "#006600", false, false));
2278    Group.add('unclassified', new Group('Unclassified', /.*/, "#000", false));
2279
2280    class GroupedEntry extends Entry {
2281      constructor(group) {
2282        super(0, GroupedEntry.prefix + group.name, 0, 0, 0, 0, 0, 0);
2283        this.group = group;
2284        this.entries = [];
2285        this.missingEntries = null;
2286        this.addsToTotal = group.addsToTotal;
2287      }
2288      get regexp() {
2289        return this.group.regexp;
2290      }
2291      get color() {
2292        return this.group.color;
2293      }
2294      get enabled() {
2295        return this.group.enabled;
2296      }
2297      add(entry) {
2298        if (!this.addTimeAndCount(entry)) return;
2299        // TODO: sum up variance
2300        this.entries.push(entry);
2301        entry.parent = this;
2302        return true;
2303      }
2304      addTimeAndCount(entry) {
2305        if (!this.regexp.test(entry.name)) return false;
2306        this._time += entry.time;
2307        this._count += entry.count;
2308        return true;
2309      }
2310      _initializeMissingEntries() {
2311        let dummyEntryNames = new Set();
2312        versions.forEach((version) => {
2313          let page = version.getOrCreate(this.page.name);
2314          let groupEntry = page.get(this.name);
2315          if (groupEntry != this) {
2316            for (let entry of groupEntry.entries) {
2317              if (this.page.get(entry.name) == undefined) {
2318                dummyEntryNames.add(entry.name);
2319              }
2320            }
2321          }
2322        });
2323        this.missingEntries = [];
2324        for (let name of dummyEntryNames) {
2325          let tmpEntry = new Entry(0, name, 0, 0, 0, 0, 0, 0);
2326          tmpEntry.page = this.page;
2327          this.missingEntries.push(tmpEntry);
2328        };
2329      }
2330      forEach(fun) {
2331        // Show also all entries which are in at least one version.
2332        // Concatenate our real entries.
2333        if (this.missingEntries == null) {
2334          this._initializeMissingEntries();
2335        }
2336        let tmpEntries = this.missingEntries.concat(this.entries);
2337
2338        // The compared entries are sorted by absolute impact.
2339        tmpEntries.sort((a, b) => {
2340          return b.time - a.time
2341        });
2342        tmpEntries.forEach(fun);
2343      }
2344      sort() {
2345        this.entries.sort((a, b) => {
2346          return b.time - a.time;
2347        });
2348      }
2349      cssClass() {
2350        if (this.page.total == this) return 'total';
2351        return '';
2352      }
2353      get isGroup() {
2354        return true
2355      }
2356      getVarianceForProperty(property) {
2357        let sum = 0;
2358        const key = property + 'Variance';
2359        this.entries.forEach((entry) => {
2360          const value = entry[key];
2361          sum += value * value;
2362        });
2363        return Math.sqrt(sum);
2364      }
2365      get timeVariancePercent() {
2366        if (this._time == 0) return 0;
2367        return this.getVarianceForProperty('time') / this._time * 100
2368      }
2369      get timeVariance() {
2370        return this.getVarianceForProperty('time')
2371      }
2372    }
2373    GroupedEntry.prefix = 'Group-';
2374
2375    class UnclassifiedEntry extends GroupedEntry {
2376      constructor(page) {
2377        super(Group.groups.get('unclassified'));
2378        this.page = page;
2379        this._time = undefined;
2380        this._count = undefined;
2381      }
2382      add(entry) {
2383        console.log("Adding unclassified:", entry);
2384        this.entries.push(entry);
2385        entry.parent = this;
2386        return true;
2387      }
2388      forEachPageGroup(fun) {
2389        this.page.forEachGroup((group) => {
2390          if (group == this) return;
2391          if (group == this.page.total) return;
2392          fun(group);
2393        });
2394      }
2395      get time() {
2396        if (this._time === undefined) {
2397          this._time = this.page.total._time;
2398          this.forEachPageGroup((group) => {
2399            if (group.addsToTotal) this._time -= group._time;
2400          });
2401        }
2402        return this.getCompareWithBaseline(this._time, '_time');
2403      }
2404      get count() {
2405        if (this._count === undefined) {
2406          this._count = this.page.total._count;
2407          this.forEachPageGroup((group) => {
2408            this._count -= group._count;
2409          });
2410        }
2411        return this.getCompareWithBaseline(this._count, '_count');
2412      }
2413    }
2414  </script>
2415</head>
2416
2417<body id="body" onmousemove="handleUpdatePopover(event)" onload="handleBodyLoad()" class="noDiff">
2418  <h1>Runtime Stats Komparator</h1>
2419
2420  <section id="inputs" class="panel alwaysVisible">
2421    <input type="checkbox" id="inputsCheckbox" class="panelCloserInput">
2422    <label class="panelCloserLabel" for="inputsCheckbox">▼</label>
2423    <h2>Input/Output</h2>
2424    <div class="panelBody">
2425      <form name="fileForm" class="inline">
2426        <p class="inline">
2427          <label for="uploadInput">Load Files:</label>
2428          <input id="uploadInput" type="file" name="files" onchange="handleLoadFiles();" multiple
2429            accept=".json,.txt,.csv,.output">
2430        </p>
2431        <p class="inline">
2432          <label for="appendInput">Append Files:</label>
2433          <input id="appendInput" type="file" name="files" onchange="handleAppendFiles();" multiple
2434            accept=".json,.txt,.csv,.output">
2435        </p>
2436      </form>
2437      <p class="inline">
2438        <button onclick="handleCopyToClipboard()">Copy Table to Clipboard</button>
2439      </p>
2440    </div>
2441  </section>
2442
2443  <section class="panel">
2444    <h2>Baseline Selector</h2>
2445    <div class="panel-body">
2446      Compare against baseline:&nbsp;<select id="baseline" onchange="handleSelectBaseline(this, event)"></select><br />
2447      <span style="color: #060">Green</span> a selected version performs
2448      better than the baseline.
2449    </div>
2450  </section>
2451
2452  <section class="panel-group">
2453    <div id="versionSelector" class="panel">
2454      <input type="checkbox" checked id="versionSelectorCheckbox" class="panelCloserInput">
2455      <label class="panelCloserLabel" for="versionSelectorCheckbox">▼</label>
2456      <h2>Selected Versions</h2>
2457      <div class="panelBody">
2458        <ul></ul>
2459      </div>
2460    </div>
2461
2462    <div id="pageSelector" class="panel">
2463      <input type="checkbox" checked id="pageSelectorCheckbox" class="panelCloserInput">
2464      <label class="panelCloserLabel" for="pageSelectorCheckbox">▼</label>
2465      <h2>Selected Pages</h2>
2466      <div class="panelBody">
2467        <ul></ul>
2468      </div>
2469    </div>
2470
2471    <div id="groupSelector" class="panel">
2472      <input type="checkbox" checked id="groupSelectorCheckbox" class="panelCloserInput">
2473      <label class="panelCloserLabel" for="groupSelectorCheckbox">▼</label>
2474      <h2>Selected RCS Groups</h2>
2475      <div class="panelBody">
2476        <ul></ul>
2477      </div>
2478    </div>
2479  </section>
2480
2481  <section id="view" class="panel">
2482    <input type="checkbox" id="tableViewCheckbox" class="panelCloserInput">
2483    <label class="panelCloserLabel" for="tableViewCheckbox">▼</label>
2484    <h2>RCS Table</h2>
2485    <div class="panelBody"></div>
2486  </section>
2487
2488  <section class="panel-group">
2489    <div id="versionDetails" class="panel">
2490      <input type="checkbox" checked id="versionDetailCheckbox" class="panelCloserInput">
2491      <label class="panelCloserLabel" for="versionDetailCheckbox">▼</label>
2492      <h2><span>Compare Page Versions</span></h2>
2493      <div class="conten panelBody">
2494        <table class="versionDetailTable" onclick="handleSelectDetailRow(this, event);">
2495          <thead>
2496            <tr>
2497              <th class="version">Version&nbsp;</th>
2498              <th class="position">Pos.&nbsp;</th>
2499              <th class="value time">Time▴&nbsp;</th>
2500              <th class="value time">Percent&nbsp;</th>
2501              <th class="value count">Count&nbsp;</th>
2502            </tr>
2503          </thead>
2504          <tbody></tbody>
2505        </table>
2506      </div>
2507    </div>
2508
2509    <div id="pageDetail" class="panel">
2510      <input type="checkbox" checked id="pageDetailCheckbox" class="panelCloserInput">
2511      <label class="panelCloserLabel" for="pageDetailCheckbox">▼</label>
2512      <h2>Page Comparison for <span></span></h2>
2513      <div class="panelBody">
2514        <table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
2515          <thead>
2516            <tr>
2517              <th class="page">Page&nbsp;</th>
2518              <th class="value time">Time&nbsp;</th>
2519              <th class="value time">Percent▾&nbsp;</th>
2520              <th class="value time hideNoDiff">%/Entry&nbsp;</th>
2521              <th class="value count">Count&nbsp;</th>
2522            </tr>
2523          </thead>
2524          <tfoot>
2525            <tr>
2526              <td class="page">Total:</td>
2527              <td class="value time"></td>
2528              <td class="value time"></td>
2529              <td class="value time hideNoDiff"></td>
2530              <td class="value count"></td>
2531            </tr>
2532          </tfoot>
2533          <tbody></tbody>
2534        </table>
2535      </div>
2536    </div>
2537
2538    <div id="impactView" class="panel">
2539      <input type="checkbox" checked id="impactViewCheckbox" class="panelCloserInput">
2540      <label class="panelCloserLabel" for="impactViewCheckbox">▼</label>
2541      <h2>Impact list for <span></span></h2>
2542      <div class="panelBody">
2543        <table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
2544          <thead>
2545            <tr>
2546              <th class="page">Name&nbsp;</th>
2547              <th class="value time">Time&nbsp;</th>
2548              <th class="value time">Percent▾&nbsp;</th>
2549              <th class="">Top Pages</th>
2550            </tr>
2551          </thead>
2552          <tbody></tbody>
2553        </table>
2554      </div>
2555    </div>
2556  </section>
2557
2558  <section id="pageVersionGraph" class="panel">
2559    <input type="checkbox" id="pageVersionGraphCheckbox" class="panelCloserInput">
2560    <label class="panelCloserLabel" for="pageVersionGraphCheckbox">▼</label>
2561    <h2><span></span></h2>
2562    <div class="panelBody"></div>
2563  </section>
2564
2565  <section id="pageGraph" class="panel">
2566    <input type="checkbox" id="pageGraphCheckbox" class="panelCloserInput">
2567    <label class="panelCloserLabel" for="pageGraphCheckbox">▼</label>
2568    <h2><span></span></h2>
2569    <div class="panelBody"></div>
2570  </section>
2571
2572  <section id="versionGraph" class="panel">
2573    <input type="checkbox" id="versionGraphCheckbox" class="panelCloserInput">
2574    <label class="panelCloserLabel" for="versionGraphCheckbox">▼</label>
2575    <h2><span></span></h2>
2576    <div class="panelBody"></div>
2577  </section>
2578
2579  <div id="column" class="column">
2580    <div class="header">
2581      <select class="version" onchange="handleSelectVersion(this, event);"></select>
2582      <select class="pageVersion" onchange="handleSelectPage(this, event);"></select>
2583    </div>
2584    <table class="list" onclick="handleSelectRow(this, event);">
2585      <thead>
2586        <tr>
2587          <th class="position">Pos.&nbsp;</th>
2588          <th class="name">Name&nbsp;</th>
2589          <th class="value time">Time&nbsp;</th>
2590          <th class="value time">Percent&nbsp;</th>
2591          <th class="value count">Count&nbsp;</th>
2592        </tr>
2593      </thead>
2594      <tbody></tbody>
2595    </table>
2596  </div>
2597
2598  <section class="panel alwaysVisible">
2599    <h2>Instructions</h2>
2600    <div class="panelBody">
2601      <ol>
2602        <li>Build chrome.</li>
2603      </ol>
2604      <h3>Telemetry benchmark</h3>
2605      <ol>
2606        <li>Run <code>v8.browsing</code> benchmarks:
2607          <pre>$CHROMIUM_DIR/tools/perf/run_benchmark run v8.browsing_desktop \
2608            --browser=exact --browser-executable=$CHROMIUM_DIR/out/release/chrome \
2609            --story-filter='.*2020 ' \
2610            --also-run-disabled-tests
2611          </pre>
2612        </li>
2613        <li>Install <a href="https://stedolan.github.io/jq/">jq</a>.</li>
2614        <li>Convert the telemetry JSON files to callstats JSON file:
2615          <pre>
2616            $V8_DIR/tools/callstats-from-telemetry.sh $CHROMIUM_DIR/tools/perf/artifacts/run_XXXX
2617          </pre>
2618        </li>
2619        <li>Load the generated <code>out.json</code></li>
2620      </ol>
2621      <h3>Merged CSV from results.html</h3>
2622      <ol>
2623        <li>Open a results.html page for RCS-enabled benchmarks</li>
2624        <li>Select "Export merged CSV" in the toolbar</li>
2625        <li>Load the downloading .csv file normally in callstats.html</li>
2626      </ol>
2627      <h3>Aggregated raw txt output</h3>
2628      <ol>
2629        <li>Install scipy, e.g. <code>sudo aptitude install python-scipy</code>
2630        <li>Check out a known working version of webpagereply:
2631          <pre>git -C $CHROME_DIR/third_party/webpagereplay checkout 7dbd94752d1cde5536ffc623a9e10a51721eff1d</pre>
2632        </li>
2633        <li>Run <code>callstats.py</code> with a web-page-replay archive:
2634          <pre>$V8_DIR/tools/callstats.py run \
2635          --replay-bin=$CHROME_SRC/third_party/webpagereplay/replay.py \
2636          --replay-wpr=$INPUT_DIR/top25.wpr \
2637          --js-flags="" \
2638          --with-chrome=$CHROME_SRC/out/Release/chrome \
2639          --sites-file=$INPUT_DIR/top25.json</pre>
2640        </li>
2641        <li>Move results file to a subdirectory: <code>mkdir $VERSION_DIR; mv *.txt $VERSION_DIR</code></li>
2642        <li>Repeat from step 1 with a different configuration (e.g. <code>--js-flags="--nolazy"</code>).</li>
2643        <li>Create the final results file: <code>./callstats.py json $VERSION_DIR1 $VERSION_DIR2 > result.json</code>
2644        </li>
2645        <li>Use <code>results.json</code> on this site.</code>
2646      </ol>
2647    </div>
2648  </section>
2649
2650  <div id="popover">
2651    <div class="popoverArrow"></div>
2652    <table>
2653      <tr>
2654        <td class="name" colspan="6"></td>
2655      </tr>
2656      <tr>
2657        <td>Page:</td>
2658        <td class="page name" colspan="6"></td>
2659      </tr>
2660      <tr>
2661        <td>Version:</td>
2662        <td class="version name" colspan="3"></td>
2663        <td class="compare version name" colspan="3"></td>
2664      </tr>
2665      <tr>
2666        <td>Time:</td>
2667        <td class="time"></td>
2668        <td>±</td>
2669        <td class="timeVariance"></td>
2670        <td class="compare time"></td>
2671        <td class="compare"> ± </td>
2672        <td class="compare timeVariance"></td>
2673      </tr>
2674      <tr>
2675        <td>Percent:</td>
2676        <td class="percent"></td>
2677        <td>±</td>
2678        <td class="percentVariance"></td>
2679        <td class="compare percent"></td>
2680        <td class="compare"> ± </td>
2681        <td class="compare percentVariance"></td>
2682      </tr>
2683      <tr>
2684        <td>Percent per Entry:</td>
2685        <td class="percentPerEntry"></td>
2686        <td colspan=2></td>
2687        <td class="compare percentPerEntry"></td>
2688        <td colspan=2></td>
2689      </tr>
2690      <tr>
2691        <td>Count:</td>
2692        <td class="count"></td>
2693        <td>±</td>
2694        <td class="countVariance"></td>
2695        <td class="compare count"></td>
2696        <td class="compare"> ± </td>
2697        <td class="compare countVariance"></td>
2698      </tr>
2699      <tr>
2700        <td>Overall Impact:</td>
2701        <td class="timeImpact"></td>
2702        <td>±</td>
2703        <td class="timePercentImpact"></td>
2704        <td class="compare timeImpact"></td>
2705        <td class="compare"> ± </td>
2706        <td class="compare timePercentImpact"></td>
2707      </tr>
2708    </table>
2709  </div>
2710</body>
2711
2712</html>
2713