blob: da85494f14c1bcd48e8b4f27bbc16fd76ded1914 [file] [log] [blame]
Ben Murdochc5610432016-08-08 18:44:38 +01001<html>
2<!--
3Copyright 2016 the V8 project authors. All rights reserved. Use of this source
4code is governed by a BSD-style license that can be found in the LICENSE file.
5-->
6
7<head>
8 <meta charset="UTF-8">
9 <style>
10 body {
11 font-family: arial;
12 }
13
14 table {
15 display: table;
16 border-spacing: 0px;
17 }
18
19 tr {
20 border-spacing: 0px;
21 padding: 10px;
22 }
23
24 td,
25 th {
26 padding: 3px 10px 3px 5px;
27 }
28
29 .inline {
30 display: inline-block;
31 vertical-align: top;
32 }
33
34 h2,
35 h3 {
36 margin-bottom: 0px;
37 }
38
39 .hidden {
40 display: none;
41 }
42
43 .view {
44 display: table;
45 }
46
47 .column {
48 display: table-cell;
49 border-right: 1px black dotted;
50 min-width: 200px;
51 }
52
53 .column .header {
54 padding: 0 10px 0 10px
55 }
56
57 #column {
58 display: none;
59 }
60
61 .list {
62 width: 100%;
63 }
64
65 select {
66 width: 100%
67 }
68
69 .list tbody {
70 cursor: pointer;
71 }
72
73 .list tr:nth-child(even) {
74 background-color: #EFEFEF;
75 }
76
77 .list tr:nth-child(even).selected {
78 background-color: #DDD;
79 }
80
81 .list tr.child {
82 display: none;
83 }
84
85 .list tr.child.visible {
86 display: table-row;
87 }
88
89 .list .child .name {
90 padding-left: 20px;
91 }
92
93 .list .parent td {
94 border-top: 1px solid #AAA;
95 }
96
97 .list .total {
98 font-weight: bold
99 }
100
101 .list tr.parent {
102 background-color: #FFF;
103 }
104
105 .list tr.parent.selected {
106 background-color: #DDD;
107 }
108
109 tr.selected {
110 background-color: #DDD;
111 }
112
113 .list .position {
114 text-align: right;
115 display: none;
116 }
117
118 .list div.toggle {
119 cursor: pointer;
120 }
121
122 #column_0 .position {
123 display: table-cell;
124 }
125
126 #column_0 .name {
127 display: table-cell;
128 }
129
130 .list .name {
131 display: none;
132 white-space: nowrap;
133 }
134
135 .value {
136 text-align: right;
137 }
138
139 .selectedVersion {
140 font-weight: bold;
141 }
142
143 #baseline {
144 width: auto;
145 }
146
147 .compareSelector {
148 padding-bottom: 20px;
149 }
150
151 .pageDetailTable tbody {
152 cursor: pointer
153 }
154
155 .pageDetailTable tfoot td {
156 border-top: 1px grey solid;
157 }
158
159 #popover {
160 position: absolute;
161 transform: translateY(-50%) translateX(40px);
162 box-shadow: -2px 10px 44px -10px #000;
163 border-radius: 5px;
164 z-index: 1;
165 background-color: #FFF;
166 display: none;
167 white-space: nowrap;
168 }
169
170 #popover table {
171 position: relative;
172 z-index: 1;
173 text-align: right;
174 margin: 10px;
175 }
176 #popover td {
177 padding: 3px 0px 3px 5px;
178 white-space: nowrap;
179 }
180
181 .popoverArrow {
182 background-color: #FFF;
183 position: absolute;
184 width: 30px;
185 height: 30px;
186 transform: translateY(-50%)rotate(45deg);
187 top: 50%;
188 left: -10px;
189 z-index: 0;
190 }
191
192 #popover .name {
193 padding: 5px;
194 font-weight: bold;
195 text-align: center;
196 }
197
198 #popover table .compare {
199 display: none
200 }
201
202 #popover table.compare .compare {
203 display: table-cell;
204 }
205
206 #popover .compare .time,
207 #popover .compare .version {
208 padding-left: 10px;
209 }
210 </style>
211 <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
212 <script type="text/javascript">
213 "use strict"
214 google.charts.load('current', {packages: ['corechart']});
215
216 // Did anybody say monkeypatching?
217 if (!NodeList.prototype.forEach) {
218 NodeList.prototype.forEach = function(func) {
219 for (var i = 0; i < this.length; i++) {
220 func(this[i]);
221 }
222 }
223 }
224
225 var versions;
226 var selectedPage;
227 var baselineVersion;
228 var selectedEntry;
229
230 function initialize() {
231 var original = $("column");
232 var view = document.createElement('div');
233 view.id = 'view';
234 var i = 0;
235 versions.forEach((version) => {
236 if (!version.enabled) return;
237 // add column
238 var column = original.cloneNode(true);
239 column.id = "column_" + i;
240 // Fill in all versions
241 var select = column.querySelector(".version");
242 select.id = "selectVersion_" + i;
243 // add all select options
244 versions.forEach((version) => {
245 if (!version.enabled) return;
246 var option = document.createElement("option");
247 option.textContent = version.name;
248 option.version = version;
249 select.appendChild(option);
250 });
251 // Fill in all page versions
252 select = column.querySelector(".pageVersion");
253 select.id = "select_" + i;
254 // add all pages
255 versions.forEach((version) => {
256 if (!version.enabled) return;
257 var optgroup = document.createElement("optgroup");
258 optgroup.label = version.name;
259 optgroup.version = version;
260 version.pages.forEach((page) => {
261 var option = document.createElement("option");
262 option.textContent = page.name;
263 option.page = page;
264 optgroup.appendChild(option);
265 });
266 select.appendChild(optgroup);
267 });
268 view.appendChild(column);
269 i++;
270 });
271 var oldView = $('view');
272 oldView.parentNode.replaceChild(view, oldView);
273
274 var select = $('baseline');
275 removeAllChildren(select);
276 select.appendChild(document.createElement('option'));
277 versions.forEach((version) => {
278 var option = document.createElement("option");
279 option.textContent = version.name;
280 option.version = version;
281 select.appendChild(option);
282 });
283
284 var versionSelectorList = $('results').querySelector('.versionSelector ul');
285 removeAllChildren(versionSelectorList);
286 versions.forEach((version) => {
287 var li = document.createElement('li');
288 var checkbox = document.createElement('input');
289 checkbox.type = 'checkbox';
290 checkbox.checked = version.enabled;
291 checkbox.version = version;
292 checkbox.addEventListener('click', handleToggleVersionEnable);
293 li.appendChild(checkbox);
294 li.appendChild(document.createTextNode(version.name));
295 versionSelectorList.appendChild(li);
296 });
297 $('results').querySelectorAll('#results > .hidden').forEach((node) => {
298 toggleCssClass(node, 'hidden', false);
299 });
300 }
301
302 function showPage(firstPage) {
303 var changeSelectedEntry = selectedEntry !== undefined
304 && selectedEntry.page === selectedPage;
305 selectedPage = firstPage;
306 selectedPage.sort();
307 showPageInColumn(firstPage, 0);
308 // Show the other versions of this page in the following columns.
309 var pageVersions = versions.pageVersions(firstPage.name);
310 var index = 1;
311 pageVersions.forEach((page) => {
312 if (page !== firstPage) {
313 showPageInColumn(page, index);
314 index++;
315 }
316 });
317 if (changeSelectedEntry) {
318 showEntryDetail(selectedPage.getEntry(selectedEntry));
319 } else {
320 showImpactList(selectedPage);
321 }
322 }
323
324 function showPageInColumn(page, columnIndex) {
325 page.sort();
326 var showDiff = (baselineVersion === undefined && columnIndex !== 0) ||
327 (baselineVersion !== undefined && page.version !== baselineVersion);
328 var diffStatus = (td, a, b) => {};
329 if (showDiff) {
330 if (baselineVersion !== undefined) {
331 diffStatus = (td, a, b) => {
332 if (a == 0) return;
333 td.style.color = a < 0 ? '#FF0000' : '#00BB00';
334 };
335 } else {
336 diffStatus = (td, a, b) => {
337 if (a == b) return;
338 var color;
339 var ratio = a / b;
340 if (ratio > 1) {
341 ratio = Math.min(Math.round((ratio - 1) * 255 * 10), 200);
342 color = '#' + ratio.toString(16) + "0000";
343 } else {
344 ratio = Math.min(Math.round((1 - ratio) * 255 * 10), 200);
345 color = '#00' + ratio.toString(16) + "00";
346 }
347 td.style.color = color;
348 }
349 }
350 }
351
352 var column = $('column_' + columnIndex);
353 var select = $('select_' + columnIndex);
354 // Find the matching option
355 selectOption(select, (i, option) => {
356 return option.page == page
357 });
358 var table = column.querySelector("table");
359 var oldTbody = table.querySelector('tbody');
360 var tbody = document.createElement('tbody');
361 var referencePage = selectedPage;
362 page.forEachSorted(selectedPage, (parentEntry, entry, referenceEntry) => {
363 // Filter out entries that do not exist in the first column for the default
364 // view.
365 if (baselineVersion === undefined && referenceEntry &&
366 referenceEntry.time == 0) {
367 return;
368 }
369 var tr = document.createElement('tr');
370 tbody.appendChild(tr);
371 tr.entry = entry;
372 tr.parentEntry = parentEntry;
373 tr.className = parentEntry === undefined ? 'parent' : 'child';
374 // Don't show entries that do not exist on the current page or if we
375 // compare against the current page
376 if (entry !== undefined && page.version !== baselineVersion) {
377 // If we show a diff, use the baselineVersion as the referenceEntry
378 if (baselineVersion !== undefined) {
379 var baselineEntry = baselineVersion.getEntry(entry);
380 if (baselineEntry !== undefined) referenceEntry = baselineEntry
381 }
382 if (!parentEntry) {
383 var node = td(tr, '<div class="toggle">►</div>', 'position');
384 node.firstChild.addEventListener('click', handleToggleGroup);
385 } else {
386 td(tr, entry.position == 0 ? '' : entry.position, 'position');
387 }
388 td(tr, entry.name, 'name ' + entry.cssClass());
389 diffStatus(
390 td(tr, ms(entry.time), 'value time'),
391 entry.time, referenceEntry.time);
392 diffStatus(
393 td(tr, percent(entry.timePercent), 'value time'),
394 entry.time, referenceEntry.time);
395 diffStatus(
396 td(tr, count(entry.count), 'value count'),
397 entry.count, referenceEntry.count);
398 } else if (baselineVersion !== undefined && referenceEntry
399 && page.version !== baselineVersion) {
400 // Show comparison of entry that does not exist on the current page.
401 tr.entry = referenceEntry;
402 td(tr, '-', 'position');
403 td(tr, referenceEntry.name, 'name');
404 diffStatus(
405 td(tr, ms(referenceEntry.time), 'value time'),
406 referenceEntry.time, 0);
407 diffStatus(
408 td(tr, percent(referenceEntry.timePercent), 'value time'),
409 referenceEntry.timePercent, 0);
410 diffStatus(
411 td(tr, count(referenceEntry.count), 'value count'),
412 referenceEntry.count, 0);
413 } else {
414 // Display empty entry / baseline entry
415 var showBaselineEntry = entry !== undefined;
416 if (showBaselineEntry) {
417 if (!parentEntry) {
418 var node = td(tr, '<div class="toggle">►</div>', 'position');
419 node.firstChild.addEventListener('click', handleToggleGroup);
420 } else {
421 td(tr, entry.position == 0 ? '' : entry.position, 'position');
422 }
423 td(tr, entry.name, 'name');
424 td(tr, ms(entry.time, false), 'value time');
425 td(tr, percent(entry.timePercent, false), 'value time');
426 td(tr, count(entry.count, false), 'value count');
427 } else {
428 td(tr, '-', 'position');
429 td(tr, '-', 'name');
430 td(tr, '-', 'value time');
431 td(tr, '-', 'value time');
432 td(tr, '-', 'value count');
433 }
434 }
435 });
436 table.replaceChild(tbody, oldTbody);
437 var versionSelect = column.querySelector('select.version');
438 selectOption(versionSelect, (index, option) => {
439 return option.version == page.version
440 });
441 }
442
443 function selectEntry(entry, updateSelectedPage) {
444 if (updateSelectedPage) {
445 entry = selectedPage.version.getEntry(entry);
446 }
447 var rowIndex;
448 var needsPageSwitch = updateSelectedPage && entry.page != selectedPage;
449 // If clicked in the detail row change the first column to that page.
450 if (needsPageSwitch) showPage(entry.page);
451 var childNodes = $('column_0').querySelector('.list tbody').childNodes;
452 for (var i = 0; i < childNodes.length; i++) {
453 if (childNodes[i].entry.name == entry.name) {
454 rowIndex = i;
455 break;
456 }
457 }
458 var firstEntry = childNodes[rowIndex].entry;
459 if (rowIndex) {
460 if (firstEntry.parent) showGroup(firstEntry.parent);
461 }
462 // Deselect all
463 $('view').querySelectorAll('.list tbody tr').forEach((tr) => {
464 toggleCssClass(tr, 'selected', false);
465 });
466 // Select the entry row
467 $('view').querySelectorAll("tbody").forEach((body) => {
468 var row = body.childNodes[rowIndex];
469 if (!row) return;
470 toggleCssClass(row, 'selected', row.entry && row.entry.name ==
471 firstEntry.name);
472 });
473 if (updateSelectedPage) {
474 entry = selectedEntry.page.version.getEntry(entry);
475 }
476 selectedEntry = entry;
477 showEntryDetail(entry);
478 }
479
480 function showEntryDetail(entry) {
481 var table, tbody, entries;
482 table = $('detailView').querySelector('.versionDetailTable');
483 tbody = document.createElement('tbody');
484 if (entry !== undefined) {
485 $('detailView').querySelector('.versionDetail h3 span').innerHTML =
486 entry.name;
487 entries = versions.pageVersions(entry.page.name).map(
488 (page) => {
489 return page.get(entry.name)
490 });
491 entries.sort((a, b) => {
492 return a.time - b.time
493 });
494 entries.forEach((pageEntry) => {
495 if (pageEntry === undefined) return;
496 var tr = document.createElement('tr');
497 if (pageEntry == entry) tr.className += 'selected';
498 tr.entry = pageEntry;
499 td(tr, pageEntry.page.version.name, 'version');
500 td(tr, pageEntry.position, 'value position');
501 td(tr, ms(pageEntry.time), 'value time');
502 td(tr, percent(pageEntry.timePercent), 'value time');
503 td(tr, count(pageEntry.count), 'value count');
504 tbody.appendChild(tr);
505 });
506 }
507 table.replaceChild(tbody, table.querySelector('tbody'));
508
509 table = $('detailView').querySelector('.pageDetailTable');
510 tbody = document.createElement('tbody');
511 if (entry !== undefined) {
512 var version = entry.page.version;
513 $('detailView').querySelector('.pageDetail h3 span').innerHTML =
514 version.name;
515 entries = version.pages.map(
516 (page) => {
517 return page.get(entry.name)
518 });
519 entries.sort((a, b) => {
520 var cmp = b.timePercent - a.timePercent;
521 if (cmp.toFixed(1) == 0) return b.time - a.time;
522 return cmp
523 });
524 entries.forEach((pageEntry) => {
525 if (pageEntry === undefined) return;
526 var tr = document.createElement('tr');
527 if (pageEntry === entry) tr.className += 'selected';
528 tr.entry = pageEntry;
529 td(tr, pageEntry.page.name, 'name');
530 td(tr, pageEntry.position, 'value position');
531 td(tr, ms(pageEntry.time), 'value time');
532 td(tr, percent(pageEntry.timePercent), 'value time');
533 td(tr, count(pageEntry.count), 'value count');
534 tbody.appendChild(tr);
535 });
536 // show the total for all pages
537 var tds = table.querySelectorAll('tfoot td');
538 tds[2].innerHTML = ms(entry.getTimeImpact());
539 // Only show the percentage total if we are in diff mode:
540 tds[3].innerHTML = percent(entry.getTimePercentImpact());
541 tds[4].innerHTML = count(entry.getCountImpact());
542 }
543 table.replaceChild(tbody, table.querySelector('tbody'));
544 showImpactList(entry.page);
545 showPageGraphs(entry.page);
546 }
547
548 function showImpactList(page) {
549 var impactView = $('detailView').querySelector('.impactView');
550 impactView.querySelector('h3 span').innerHTML = page.version.name;
551
552 var table = impactView.querySelector('table');
553 var tbody = document.createElement('tbody');
554 var version = page.version;
555 var entries = version.allEntries();
556 if (selectedEntry !== undefined && selectedEntry.isGroup) {
557 impactView.querySelector('h3 span').innerHTML += " " + selectedEntry.name;
558 entries = entries.filter((entry) => {
559 return entry.name == selectedEntry.name ||
560 (entry.parent && entry.parent.name == selectedEntry.name)
561 });
562 }
563 var isCompareView = baselineVersion !== undefined;
564 entries = entries.filter((entry) => {
565 if (isCompareView) {
566 var impact = entry.getTimeImpact();
567 return impact < -1 || 1 < impact
568 }
569 return entry.getTimePercentImpact() > 0.1;
570 });
571 entries.sort((a, b) => {
572 var cmp = b.getTimePercentImpact() - a.getTimePercentImpact();
573 if (isCompareView || cmp.toFixed(1) == 0) {
574 return b.getTimeImpact() - a.getTimeImpact();
575 }
576 return cmp
577 });
578 entries.forEach((entry) => {
579 var tr = document.createElement('tr');
580 tr.entry = entry;
581 td(tr, entry.name, 'name');
582 td(tr, ms(entry.getTimeImpact()), 'value time');
583 var percentImpact = entry.getTimePercentImpact();
584 td(tr, percentImpact > 1000 ? '-' : percent(percentImpact), 'value time');
585 var topPages = entry.getPagesByPercentImpact().slice(0, 3)
586 .map((each) => {
587 return each.name + ' (' + percent(each.getEntry(entry).timePercent) +
588 ')'
589 });
590 td(tr, topPages.join(', '), 'name');
591 tbody.appendChild(tr);
592 });
593 table.replaceChild(tbody, table.querySelector('tbody'));
594 }
595
596 var selectedGroup;
597 function showPageGraphs(page) {
598 var groups = page.groups.filter(each => each.name != page.total.name);
599 if (selectedGroup == undefined) {
600 selectedGroup = groups[0];
601 } else {
602 groups = groups.filter(each => each.name != selectedGroup.name);
603 groups.unshift(selectedGroup);
604 }
605 var dataTable = new google.visualization.DataTable();
606 dataTable.addColumn('string', 'Name');
607 groups.forEach(group => {
608 var column = dataTable.addColumn('number', group.name);
609 dataTable.setColumnProperty(column, 'group', group);
610 });
611 // Calculate the average row
612 var row = ['Average'];
613 groups.forEach((group) => {
614 row.push(group.getTimeImpact());
615 });
616 dataTable.addRow(row);
617 // Sort the pages by the selected group.
618 var pages = page.version.pages.slice();
619 pages.sort((a, b) => {
620 return b.getEntry(selectedGroup).timePercent - a.getEntry(selectedGroup).timePercent;
621 });
622 // Calculate the entries for the pages
623 pages.forEach((page) => {
624 row = [page.name];
625 groups.forEach((group) => {
626 row.push(page.getEntry(group).time);
627 });
628 dataTable.addRow(row);
629 });
630
631 var height = 1000/27*page.version.pages.length;
632 var options = {
633 title: 'Page Comparison for Version ' + page.version.name,
634 isStacked: 'percent',
635 height: height ,
636 hAxis: {
637 title: '% Time',
638 minValue: 0,
639 },
640 vAxis: {
641 }
642 };
643 var chart = new google.visualization.BarChart($('pageGraphs'));
644 chart.draw(dataTable, options);
645 google.visualization.events.addListener(chart, 'select', selectHandler);
646 function selectHandler() {
647 var column = chart.getSelection()[0].column;
648 if (column === undefined) return;
649 selectedGroup = dataTable.getColumnProperty(column, 'group');
650 showPageGraphs(selectedEntry.page);
651 }
652 }
653
654 function showGroup(entry) {
655 toggleGroup(entry, true);
656 }
657
658 function toggleGroup(group, show) {
659 $('view').querySelectorAll(".child").forEach((tr) => {
660 var entry = tr.parentEntry;
661 if (!entry) return;
662 if (entry.name !== group.name) return;
663 toggleCssClass(tr, 'visible', show);
664 });
665 }
666
667 function showPopover(entry) {
668 var popover = $('popover');
669 popover.querySelector('td.name').innerHTML = entry.name;
670 popover.querySelector('td.page').innerHTML = entry.page.name;
671 setPopoverDetail(popover, entry, '');
672 popover.querySelector('table').className = "";
673 if (baselineVersion !== undefined) {
674 entry = baselineVersion.getEntry(entry);
675 setPopoverDetail(popover, entry, '.compare');
676 popover.querySelector('table').className = "compare";
677 }
678 }
679
680 function setPopoverDetail(popover, entry, prefix) {
681 var node = (name) => popover.querySelector(prefix + name);
682 if (entry == undefined) {
683 node('.version').innerHTML = baselineVersion.name;
684 node('.time').innerHTML = '-';
685 node('.timeVariance').innerHTML = '-';
686 node('.percent').innerHTML = '-';
687 node('.percentVariance').innerHTML = '-';
688 node('.count').innerHTML = '-';
689 node('.countVariance').innerHTML = '-';
690 node('.timeImpact').innerHTML = '-';
691 node('.timePercentImpact').innerHTML = '-';
692 } else {
693 node('.version').innerHTML = entry.page.version.name;
694 node('.time').innerHTML = ms(entry._time, false);
695 node('.timeVariance').innerHTML
696 = percent(entry.timeVariancePercent, false);
697 node('.percent').innerHTML = percent(entry.timePercent, false);
698 node('.percentVariance').innerHTML
699 = percent(entry.timePercentVariancePercent, false);
700 node('.count').innerHTML = count(entry._count, false);
701 node('.countVariance').innerHTML
702 = percent(entry.timeVariancePercent, false);
703 node('.timeImpact').innerHTML
704 = ms(entry.getTimeImpact(false), false);
705 node('.timePercentImpact').innerHTML
706 = percent(entry.getTimeImpactVariancePercent(false), false);
707 }
708 }
709
710 // ===========================================================================
711 // Helpers
712 function $(id) {
713 return document.getElementById(id)
714 }
715
716 function removeAllChildren(node) {
717 while (node.firstChild) {
718 node.removeChild(node.firstChild);
719 }
720 }
721
722 function selectOption(select, match) {
723 var options = select.options;
724 for (var i = 0; i < options.length; i++) {
725 if (match(i, options[i])) {
726 select.selectedIndex = i;
727 return;
728 }
729 }
730 }
731
732 function td(tr, content, className) {
733 var td = document.createElement("td");
734 td.innerHTML = content;
735 td.className = className
736 tr.appendChild(td);
737 return td
738 }
739
740 function nodeIndex(node) {
741 var children = node.parentNode.childNodes,
742 i = 0;
743 for (; i < children.length; i++) {
744 if (children[i] == node) {
745 return i;
746 }
747 }
748 return -1;
749 }
750
751 function toggleCssClass(node, cssClass, toggleState) {
752 var index = -1;
753 var classes;
754 if (node.className != undefined) {
755 classes = node.className.split(' ');
756 index = classes.indexOf(cssClass);
757 }
758 if (index == -1) {
759 if (toggleState === false) return;
760 node.className += ' ' + cssClass;
761 return;
762 }
763 if (toggleState === true) return;
764 classes.splice(index, 1);
765 node.className = classes.join(' ');
766 }
767
768 function diffSign(value, digits, unit, showDiff) {
769 if (showDiff === false || baselineVersion == undefined) {
770 return value.toFixed(digits) + unit;
771 }
772 return (value >= 0 ? '+' : '') + value.toFixed(digits) + unit + 'Δ';
773 }
774
775 function ms(value, showDiff) {
776 return diffSign(value, 1, 'ms', showDiff);
777 }
778
779 function count(value, showDiff) {
780 return diffSign(value, 0, '#', showDiff);
781 }
782
783 function percent(value, showDiff) {
784 return diffSign(value, 1, '%', showDiff);
785 }
786
787 // =========================================================================
788 // EventHandlers
789 function handleBodyLoad() {
790 $('uploadInput').focus();
791 }
792
793 function handleLoadFile() {
794 var files = document.getElementById("uploadInput").files;
795 var file = files[0];
796 var reader = new FileReader();
797
798 reader.onload = function(evt) {
799 versions = Versions.fromJSON(JSON.parse(this.result));
800 initialize()
801 showPage(versions.versions[0].pages[0]);
802 }
803 reader.readAsText(file);
804 }
805
806 function handleToggleGroup(event) {
807 var group = event.target.parentNode.parentNode.entry;
808 toggleGroup(selectedPage.get(group.name));
809 }
810
811 function handleSelectPage(select, event) {
812 var option = select.options[select.selectedIndex];
813 if (select.id == "select_0") {
814 showPage(option.page);
815 } else {
816 var columnIndex = select.id.split('_')[1];
817 showPageInColumn(option.page, columnIndex);
818 }
819 }
820
821 function handleSelectVersion(select, event) {
822 var option = select.options[select.selectedIndex];
823 var version = option.version;
824 if (select.id == "selectVersion_0") {
825 var page = version.get(selectedPage.name);
826 showPage(page);
827 } else {
828 var columnIndex = select.id.split('_')[1];
829 var pageSelect = $('select_' + columnIndex);
830 var page = pageSelect.options[select.selectedIndex].page;
831 page = version.get(page.name);
832 showPageInColumn(page, columnIndex);
833 }
834 }
835
836 function handleSelectDetailRow(table, event) {
837 if (event.target.tagName != 'TD') return;
838 var tr = event.target.parentNode;
839 if (tr.tagName != 'TR') return;
840 if (tr.entry === undefined) return;
841 selectEntry(tr.entry, true);
842 }
843
844 function handleSelectRow(table, event, fromDetail) {
845 if (event.target.tagName != 'TD') return;
846 var tr = event.target.parentNode;
847 if (tr.tagName != 'TR') return;
848 if (tr.entry === undefined) return;
849 selectEntry(tr.entry, false);
850 }
851
852 function handleSelectBaseline(select, event) {
853 var option = select.options[select.selectedIndex];
854 baselineVersion = option.version
855 showPage(selectedPage);
856 if (selectedEntry === undefined) return;
857 selectEntry(selectedEntry, true);
858 }
859
860 function handleUpdatePopover(event) {
861 var popover = $('popover');
862 popover.style.left = event.pageX + 'px';
863 popover.style.top = event.pageY + 'px';
864 popover.style.display = event.shiftKey ? 'block' : 'none';
865 var target = event.target;
866 while (target.entry === undefined) {
867 target = target.parentNode;
868 if (!target) return;
869 }
870 showPopover(target.entry);
871 }
872
873 function handleToggleVersionEnable(event) {
874 var version = this.version;
875 if (version === undefined) return;
876 version.enabled = this.checked;
877 initialize();
878 var page = selectedPage;
879 if (page === undefined || !page.version.enabled) {
880 page = versions.getEnabledPage(page.name);
881 }
882 showPage(page);
883 }
884
885 // ===========================================================================
886
887 class Versions {
888 constructor() {
889 this.versions = [];
890 }
891 add(version) {
892 this.versions.push(version)
893 }
894 pageVersions(name) {
895 var result = [];
896 this.versions.forEach((version) => {
897 if (!version.enabled) return;
898 var page = version.get(name);
899 if (page !== undefined) result.push(page);
900 });
901 return result;
902 }
903 get length() {
904 return this.versions.length
905 }
906 get(index) {
907 return this.versions[index]
908 };
909 forEach(f) {
910 this.versions.forEach(f);
911 }
912 sort() {
913 this.versions.sort((a, b) => {
914 if (a.name > b.name) return 1;
915 if (a.name < b.name) return -1;
916 return 0
917 })
918 }
919 getEnabledPage(name) {
920 for (var i = 0; i < this.versions.length; i++) {
921 var version = this.versions[i];
922 if (!version.enabled) continue;
923 var page = version.get(name);
924 if (page !== undefined) return page;
925 }
926 }
927 }
928 Versions.fromJSON = function(json) {
929 var versions = new Versions();
930 for (var version in json) {
931 versions.add(Version.fromJSON(version, json[version]));
932 }
933 versions.sort();
934 return versions;
935 }
936
937 class Version {
938 constructor(name) {
939 this.name = name;
940 this.enabled = true;
941 this.pages = [];
942 }
943 add(page) {
944 this.pages.push(page);
945 }
946 indexOf(name) {
947 for (var i = 0; i < this.pages.length; i++) {
948 if (this.pages[i].name == name) return i;
949 }
950 return -1;
951 }
952 get(name) {
953 var index = this.indexOf(name);
954 if (0 <= index) return this.pages[index];
955 return undefined
956 }
957 get length() {
958 return this.versions.length
959 }
960 getEntry(entry) {
961 if (entry === undefined) return undefined;
962 var page = this.get(entry.page.name);
963 if (page === undefined) return undefined;
964 return page.get(entry.name);
965 }
966 forEachEntry(fun) {
967 this.pages.forEach((page) => {
968 page.forEach(fun);
969 });
970 }
971 allEntries() {
972 var map = new Map();
973 this.forEachEntry((group, entry) => {
974 if (!map.has(entry.name)) map.set(entry.name, entry);
975 });
976 return Array.from(map.values());
977 }
978 getTotalValue(name, property) {
979 if (name === undefined) name = this.pages[0].total.name;
980 var sum = 0;
981 this.pages.forEach((page) => {
982 var entry = page.get(name);
983 if (entry !== undefined) sum += entry[property];
984 });
985 return sum;
986 }
987 getTotalTime(name, showDiff) {
988 return this.getTotalValue(name, showDiff === false ? '_time' : 'time');
989 }
990 getTotalTimePercent(name, showDiff) {
991 if (baselineVersion === undefined) {
992 // Return the overall average percent of the given entry name.
993 return this.getTotalValue(name, 'time') /
994 this.getTotalTime('Group-Total') * 100;
995 }
996 // Otherwise return the difference to the sum of the baseline version.
997 var baselineValue = baselineVersion.getTotalTime(name, false);
998 return this.getTotalValue(name, '_time') / baselineValue * 100;
999 }
1000 getTotalTimeVariance(name, showDiff) {
1001 // Calculate the overall error for a given entry name
1002 var sum = 0;
1003 this.pages.forEach((page) => {
1004 var entry = page.get(name);
1005 if (entry === undefined) return;
1006 sum += entry.timeVariance * entry.timeVariance;
1007 });
1008 return Math.sqrt(sum);
1009 }
1010 getTotalTimeVariancePercent(name, showDiff) {
1011 return this.getTotalTimeVariance(name, showDiff) /
1012 this.getTotalTime(name, showDiff) * 100;
1013 }
1014 getTotalCount(name, showDiff) {
1015 return this.getTotalValue(name, showDiff === false ? '_count' : 'count');
1016 }
1017 getPagesByPercentImpact(name) {
1018 var sortedPages =
1019 this.pages.filter((each) => {
1020 return each.get(name) !== undefined
1021 });
1022 sortedPages.sort((a, b) => {
1023 return b.get(name).timePercent - a.get(name).timePercent;
1024 });
1025 return sortedPages;
1026 }
1027 sort() {
1028 this.pages.sort((a, b) => {
1029 if (a.name > b.name) return 1;
1030 if (a.name < b.name) return -1;
1031 return 0
1032 })
1033 }
1034 }
1035 Version.fromJSON = function(name, data) {
1036 var version = new Version(name);
1037 for (var page in data) {
1038 version.add(Page.fromJSON(version, page, data[page]));
1039 }
1040 version.sort();
1041 return version;
1042 }
1043
1044
1045 class Page {
1046 constructor(version, name) {
1047 this.name = name;
1048 this.total = new GroupedEntry('Total', /.*Total.*/);
1049 this.unclassified = new UnclassifiedEntry(this)
1050 this.groups = [
1051 this.total,
1052 new GroupedEntry('IC', /.*IC.*/),
1053 new GroupedEntry('Optimize',
1054 /StackGuard|.*Optimize.*|.*Deoptimize.*|Recompile.*/),
1055 new GroupedEntry('Compile', /.*Compile.*|Parse.*/),
1056 new GroupedEntry('Callback', /.*Callback$/),
1057 new GroupedEntry('API', /.*API.*/),
1058 new GroupedEntry('GC', /GC|AllocateInTargetSpace/),
1059 new GroupedEntry('JavaScript', /JS_Execution/),
1060 this.unclassified
1061 ];
1062 this.entryDict = new Map();
1063 this.groups.forEach((entry) => {
1064 entry.page = this;
1065 this.entryDict.set(entry.name, entry);
1066 });
1067 this.version = version;
1068 }
1069 add(entry) {
1070 entry.page = this;
1071 this.entryDict.set(entry.name, entry);
1072 var added = false;
1073 this.groups.forEach((group) => {
1074 if (!added) added = group.add(entry);
1075 });
1076 if (added) return;
1077 this.unclassified.push(entry);
1078 }
1079 get(name) {
1080 return this.entryDict.get(name)
1081 }
1082 getEntry(entry) {
1083 if (entry === undefined) return undefined;
1084 return this.get(entry.name);
1085 }
1086 get length() {
1087 return this.versions.length
1088 }
1089 forEachSorted(referencePage, func) {
1090 // Iterate over all the entries in the order they appear on the reference page.
1091 referencePage.forEach((parent, referenceEntry) => {
1092 var entry;
1093 if (parent) parent = this.entryDict.get(parent.name);
1094 if (referenceEntry) entry = this.entryDict.get(referenceEntry.name);
1095 func(parent, entry, referenceEntry);
1096 });
1097 }
1098 forEach(fun) {
1099 this.forEachGroup((group) => {
1100 fun(undefined, group);
1101 group.forEach((entry) => {
1102 fun(group, entry)
1103 });
1104 });
1105 }
1106 forEachGroup(fun) {
1107 this.groups.forEach(fun)
1108 }
1109 sort() {
1110 this.groups.sort((a, b) => {
1111 return b.time - a.time;
1112 });
1113 this.groups.forEach((group) => {
1114 group.sort()
1115 });
1116 }
1117 }
1118 Page.fromJSON = function(version, name, data) {
1119 if (name.indexOf('www.') == 0) {
1120 name = name.substring(4);
1121 }
1122 var page = new Page(version, name);
1123 for (var i = 0; i < data.length; i++) {
1124 page.add(Entry.fromJSON(i, data[data.length - i - 1]));
1125 }
1126 page.sort();
1127 return page
1128 }
1129
1130
1131 class Entry {
1132 constructor(position, name, time, timeVariance, timeVariancePercent,
1133 count,
1134 countVariance, countVariancePercent) {
1135 this.position = position;
1136 this.name = name;
1137 this._time = time;
1138 this._timeVariance = timeVariance;
1139 this._timeVariancePercent = timeVariancePercent;
1140 this._count = count;
1141 this.countVariance = countVariance;
1142 this.countVariancePercent = countVariancePercent;
1143 this.page = undefined;
1144 this.parent = undefined;
1145 }
1146 getCompareWithBaseline(value, property) {
1147 if (baselineVersion == undefined) return value;
1148 var baselineEntry = baselineVersion.getEntry(this);
1149 if (!baselineEntry) return value;
1150 if (baselineVersion === this.page.version) return value;
1151 return value - baselineEntry[property];
1152 }
1153 cssClass() {
1154 return ''
1155 }
1156 get time() {
1157 return this.getCompareWithBaseline(this._time, '_time');
1158 }
1159 get count() {
1160 return this.getCompareWithBaseline(this._count, '_count');
1161 }
1162 get timePercent() {
1163 var value = this._time / this.page.total._time * 100;
1164 if (baselineVersion == undefined) return value;
1165 var baselineEntry = baselineVersion.getEntry(this);
1166 if (!baselineEntry) return value;
1167 if (baselineVersion === this.page.version) return value;
1168 return (this._time - baselineEntry._time) / this.page.total._time *
1169 100;
1170 }
1171 get timePercentVariancePercent() {
1172 // Get the absolute values for the percentages
1173 return this.timeVariance / this.page.total._time * 100;
1174 }
1175 getTimeImpact(showDiff) {
1176 return this.page.version.getTotalTime(this.name, showDiff);
1177 }
1178 getTimeImpactVariancePercent(showDiff) {
1179 return this.page.version.getTotalTimeVariancePercent(this.name, showDiff);
1180 }
1181 getTimePercentImpact(showDiff) {
1182 return this.page.version.getTotalTimePercent(this.name, showDiff);
1183 }
1184 getCountImpact(showDiff) {
1185 return this.page.version.getTotalCount(this.name, showDiff);
1186 }
1187 getPagesByPercentImpact() {
1188 return this.page.version.getPagesByPercentImpact(this.name);
1189 }
1190 get isGroup() {
1191 return false
1192 }
1193 get timeVariance() {
1194 return this._timeVariance
1195 }
1196 get timeVariancePercent() {
1197 return this._timeVariancePercent
1198 }
1199 }
1200 Entry.fromJSON = function(position, data) {
1201 return new Entry(position, ...data);
1202 }
1203
1204
1205 class GroupedEntry extends Entry {
1206 constructor(name, regexp) {
1207 super(0, 'Group-' + name, 0, 0, 0, 0, 0, 0);
1208 this.regexp = regexp;
1209 this.entries = [];
1210 }
1211 add(entry) {
1212 if (!entry.name.match(this.regexp)) return false;
1213 this._time += entry.time;
1214 this._count += entry.count;
1215 // TODO: sum up variance
1216 this.entries.push(entry);
1217 entry.parent = this;
1218 return true;
1219 }
1220 forEach(fun) {
1221 if (baselineVersion === undefined) {
1222 this.entries.forEach(fun);
1223 return;
1224 }
1225 // If we have a baslineVersion to compare against show also all entries from the
1226 // other group.
1227 var tmpEntries = baselineVersion.getEntry(this)
1228 .entries.filter((entry) => {
1229 return this.page.get(entry.name) == undefined
1230 });
1231
1232 // The compared entries are sorted by absolute impact.
1233 tmpEntries = tmpEntries.map((entry) => {
1234 var tmpEntry = new Entry(0, entry.name, 0, 0, 0, 0, 0, 0);
1235 tmpEntry.page = this.page;
1236 return tmpEntry;
1237 });
1238 tmpEntries = tmpEntries.concat(this.entries);
1239 tmpEntries.sort((a, b) => {
1240 return a.time - b.time
1241 });
1242 tmpEntries.forEach(fun);
1243 }
1244 sort() {
1245 this.entries.sort((a, b) => {
1246 return b.time - a.time;
1247 });
1248 }
1249 cssClass() {
1250 if (this.page.total == this) return 'total';
1251 return '';
1252 }
1253 get isGroup() {
1254 return true
1255 }
1256 getVarianceForProperty(property) {
1257 var sum = 0;
1258 this.entries.forEach((entry) => {
1259 sum += entry[property + 'Variance'] * entry[property +
1260 'Variance'];
1261 });
1262 return Math.sqrt(sum);
1263 }
1264 get timeVariancePercent() {
1265 if (this._time == 0) return 0;
1266 return this.getVarianceForProperty('time') / this._time * 100
1267 }
1268 get timeVariance() {
1269 return this.getVarianceForProperty('time')
1270 }
1271 }
1272
1273 class UnclassifiedEntry extends GroupedEntry {
1274 constructor(page) {
1275 super('Runtime');
1276 this.page = page;
1277 this._time = undefined;
1278 this._count = undefined;
1279 }
1280 add(entry) {
1281 this.entries.push(entry);
1282 entry.parent = this;
1283 return true;
1284 }
1285 forEachPageGroup(fun) {
1286 this.page.forEachGroup((group) => {
1287 if (group == this) return;
1288 if (group == this.page.total) return;
1289 fun(group);
1290 });
1291 }
1292 get time() {
1293 if (this._time === undefined) {
1294 this._time = this.page.total._time;
1295 this.forEachPageGroup((group) => {
1296 this._time -= group._time;
1297 });
1298 }
1299 return this.getCompareWithBaseline(this._time, '_time');
1300 }
1301 get count() {
1302 if (this._count === undefined) {
1303 this._count = this.page.total._count;
1304 this.forEachPageGroup((group) => {
1305 this._count -= group._count;
1306 });
1307 }
1308 return this.getCompareWithBaseline(this._count, '_count');
1309 }
1310 }
1311 </script>
1312</head>
1313
1314<body onmousemove="handleUpdatePopover(event)" onload="handleBodyLoad()">
1315 <h1>Runtime Stats Komparator</h1>
1316
1317 <div id="results">
1318 <div class="inline">
1319 <h2>Data</h2>
1320 <form name="fileForm">
1321 <p>
1322 <input id="uploadInput" type="file" name="files" onchange="handleLoadFile();" accept=".json">
1323 </p>
1324 </form>
1325 </div>
1326
1327 <div class="inline hidden">
1328 <h2>Result</h2>
1329 <div class="compareSelector inline">
1330 Compare against:&nbsp;<select id="baseline" onchange="handleSelectBaseline(this, event)"></select><br/>
1331 <span style="color: #060">Green</span> the selected version above performs
1332 better on this measurement.
1333 </div>
1334 <div class="versionSelector inline">
1335 Select Versions:
1336 <ul></ul>
1337 </div>
1338 </div>
1339 <div id="view">
1340 </div>
1341
1342 <div id="detailView" class="hidden">
1343 <h2></h2>
1344 <div class="versionDetail inline">
1345 <h3>Version Comparison for <span></span></h3>
1346 <table class="versionDetailTable" onclick="handleSelectDetailRow(this, event);">
1347 <thead>
1348 <tr>
1349 <th class="version">Version&nbsp;</th>
1350 <th class="position">Pos.&nbsp;</th>
1351 <th class="value time">Time▴&nbsp;</th>
1352 <th class="value time">Percent&nbsp;</th>
1353 <th class="value count">Count&nbsp;</th>
1354 </tr>
1355 </thead>
1356 <tbody></tbody>
1357 </table>
1358 </div>
1359 <div class="pageDetail inline">
1360 <h3>Page Comparison for <span></span></h3>
1361 <table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
1362 <thead>
1363 <tr>
1364 <th class="page">Page&nbsp;</th>
1365 <th class="position">Pos.&nbsp;</th>
1366 <th class="value time">Time&nbsp;</th>
1367 <th class="value time">Percent▾&nbsp;</th>
1368 <th class="value count">Count&nbsp;</th>
1369 </tr>
1370 </thead>
1371 <tfoot>
1372 <tr>
1373 <td class="page">Total:</td>
1374 <td class="position"></td>
1375 <td class="value time"></td>
1376 <td class="value time"></td>
1377 <td class="value count"></td>
1378 </tr>
1379 </tfoot>
1380 <tbody></tbody>
1381 </table>
1382 </div>
1383 <div class="impactView inline">
1384 <h3>Impact list for <span></span></h3>
1385 <table class="pageDetailTable" onclick="handleSelectDetailRow(this, event);">
1386 <thead>
1387 <tr>
1388 <th class="page">Name&nbsp;</th>
1389 <th class="value time">Time&nbsp;</th>
1390 <th class="value time">Percent▾&nbsp;</th>
1391 <th class="">Top Pages</th>
1392 </tr>
1393 </thead>
1394 <tbody></tbody>
1395 </table>
1396 </div>
1397 </div>
1398 <div id="pageGraphs" class="hidden">
1399 </div>
1400
1401 <div id="column" class="column">
1402 <div class="header">
1403 <select class="version" onchange="handleSelectVersion(this, event);"></select>
1404 <select class="pageVersion" onchange="handleSelectPage(this, event);"></select>
1405 </div>
1406 <table class="list" onclick="handleSelectRow(this, event);">
1407 <thead>
1408 <tr>
1409 <th class="position">Pos.&nbsp;</th>
1410 <th class="name">Name&nbsp;</th>
1411 <th class="value time">Time&nbsp;</th>
1412 <th class="value time">Percent&nbsp;</th>
1413 <th class="value count">Count&nbsp;</th>
1414 </tr>
1415 </thead>
1416 <tbody></tbody>
1417 </table>
1418 </div>
1419 </div>
1420
1421 <div class="inline">
1422 <h2>Usage</h2>
1423 <ol>
1424 <li>Install scipy, e.g. <code>sudo aptitude install python-scipy</code>
1425 <li>Build chrome with the <a href="https://codereview.chromium.org/1923893002">extended runtime callstats</a>.</li>
1426 <li>Run <code>callstats.py</code> with a web-page-replay archive:
1427 <pre>$V8_DIR/tools/callstats.py run \
1428 --replay-bin=$CHROME_SRC/third_party/webpagereplay/replay.py \
1429 --replay-wpr=$INPUT_DIR/top25.wpr \
1430 --js-flags="" \
1431 --with-chrome=$CHROME_SRC/out/Release/chrome \
1432 --sites-file=$INPUT_DIR/top25.json</pre>
1433 </li>
1434 <li>Move results file to a subdirectory: <code>mkdir $VERSION_DIR; mv *.txt $VERSION_DIR</code></li>
1435 <li>Repeat from step 1 with a different configuration (e.g. <code>--js-flags="--nolazy"</code>).</li>
1436 <li>Create the final results file: <code>./callstats.py json $VERSION_DIR1 $VERSION_DIR2 > result.json</code></li>
1437 <li>Use <code>results.json</code> on this site.</code>
1438 </ol>
1439 </div>
1440
1441 <div id="popover">
1442 <div class="popoverArrow"></div>
1443 <table>
1444 <tr>
1445 <td class="name" colspan="6"></td>
1446 </tr>
1447 <tr>
1448 <td>Page:</td>
1449 <td class="page name" colspan="6"></td>
1450 </tr>
1451 <tr>
1452 <td>Version:</td>
1453 <td class="version name" colspan="3"></td>
1454 <td class="compare version name" colspan="3"></td>
1455 </tr>
1456 <tr>
1457 <td>Time:</td>
1458 <td class="time"></td><td>±</td><td class="timeVariance"></td>
1459 <td class="compare time"></td><td class="compare"> ± </td><td class="compare timeVariance"></td>
1460 </tr>
1461 <tr>
1462 <td>Percent:</td>
1463 <td class="percent"></td><td>±</td><td class="percentVariance"></td>
1464 <td class="compare percent"></td><td class="compare"> ± </td><td class="compare percentVariance"></td>
1465 </tr>
1466 <tr>
1467 <td>Count:</td>
1468 <td class="count"></td><td>±</td><td class="countVariance"></td>
1469 <td class="compare count"></td><td class="compare"> ± </td><td class="compare countVariance"></td>
1470 </tr>
1471 <tr>
1472 <td>Overall Impact:</td>
1473 <td class="timeImpact"></td><td>±</td><td class="timePercentImpact"></td>
1474 <td class="compare timeImpact"></td><td class="compare"> ± </td><td class="compare timePercentImpact"></td>
1475 </tr>
1476 </table>
1477 </div>
1478
1479</body>
1480
1481</html>