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