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