1// Copyright 2017 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5"use strict" 6 7function $(id) { 8 return document.getElementById(id); 9} 10 11let components; 12function createViews() { 13 components = [ 14 new CallTreeView(), 15 new TimelineView(), 16 new HelpView(), 17 new SummaryView(), 18 new ModeBarView(), 19 ]; 20} 21 22function emptyState() { 23 return { 24 file : null, 25 mode : null, 26 currentCodeId : null, 27 start : 0, 28 end : Infinity, 29 timelineSize : { 30 width : 0, 31 height : 0 32 }, 33 callTree : { 34 attribution : "js-exclude-bc", 35 categories : "code-type", 36 sort : "time" 37 } 38 }; 39} 40 41function setCallTreeState(state, callTreeState) { 42 state = Object.assign({}, state); 43 state.callTree = callTreeState; 44 return state; 45} 46 47let main = { 48 currentState : emptyState(), 49 50 setMode(mode) { 51 if (mode !== main.currentState.mode) { 52 53 function setCallTreeModifiers(attribution, categories, sort) { 54 let callTreeState = Object.assign({}, main.currentState.callTree); 55 callTreeState.attribution = attribution; 56 callTreeState.categories = categories; 57 callTreeState.sort = sort; 58 return callTreeState; 59 } 60 61 let state = Object.assign({}, main.currentState); 62 63 switch (mode) { 64 case "bottom-up": 65 state.callTree = 66 setCallTreeModifiers("js-exclude-bc", "code-type", "time"); 67 break; 68 case "top-down": 69 state.callTree = 70 setCallTreeModifiers("js-exclude-bc", "none", "time"); 71 break; 72 case "function-list": 73 state.callTree = 74 setCallTreeModifiers("js-exclude-bc", "code-type", "own-time"); 75 break; 76 } 77 78 state.mode = mode; 79 80 main.currentState = state; 81 main.delayRender(); 82 } 83 }, 84 85 setCallTreeAttribution(attribution) { 86 if (attribution !== main.currentState.attribution) { 87 let callTreeState = Object.assign({}, main.currentState.callTree); 88 callTreeState.attribution = attribution; 89 main.currentState = setCallTreeState(main.currentState, callTreeState); 90 main.delayRender(); 91 } 92 }, 93 94 setCallTreeSort(sort) { 95 if (sort !== main.currentState.sort) { 96 let callTreeState = Object.assign({}, main.currentState.callTree); 97 callTreeState.sort = sort; 98 main.currentState = setCallTreeState(main.currentState, callTreeState); 99 main.delayRender(); 100 } 101 }, 102 103 setCallTreeCategories(categories) { 104 if (categories !== main.currentState.categories) { 105 let callTreeState = Object.assign({}, main.currentState.callTree); 106 callTreeState.categories = categories; 107 main.currentState = setCallTreeState(main.currentState, callTreeState); 108 main.delayRender(); 109 } 110 }, 111 112 setViewInterval(start, end) { 113 if (start !== main.currentState.start || 114 end !== main.currentState.end) { 115 main.currentState = Object.assign({}, main.currentState); 116 main.currentState.start = start; 117 main.currentState.end = end; 118 main.delayRender(); 119 } 120 }, 121 122 setFile(file) { 123 if (file !== main.currentState.file) { 124 let lastMode = main.currentState.mode || "summary"; 125 main.currentState = emptyState(); 126 main.currentState.file = file; 127 main.setMode(lastMode); 128 main.delayRender(); 129 } 130 }, 131 132 setCurrentCode(codeId) { 133 if (codeId !== main.currentState.currentCodeId) { 134 main.currentState = Object.assign({}, main.currentState); 135 main.currentState.currentCodeId = codeId; 136 main.delayRender(); 137 } 138 }, 139 140 onResize() { 141 main.delayRender(); 142 }, 143 144 onLoad() { 145 function loadHandler(evt) { 146 let f = evt.target.files[0]; 147 if (f) { 148 let reader = new FileReader(); 149 reader.onload = function(event) { 150 main.setFile(JSON.parse(event.target.result)); 151 }; 152 reader.onerror = function(event) { 153 console.error( 154 "File could not be read! Code " + event.target.error.code); 155 }; 156 reader.readAsText(f); 157 } else { 158 main.setFile(null); 159 } 160 } 161 $("fileinput").addEventListener( 162 "change", loadHandler, false); 163 createViews(); 164 }, 165 166 delayRender() { 167 Promise.resolve().then(() => { 168 for (let c of components) { 169 c.render(main.currentState); 170 } 171 }); 172 } 173}; 174 175const CATEGORY_COLOR = "#f5f5f5"; 176const bucketDescriptors = 177 [ { kinds : [ "JSOPT" ], 178 color : "#64dd17", 179 backgroundColor : "#80e27e", 180 text : "JS Optimized" }, 181 { kinds : [ "JSUNOPT", "BC" ], 182 color : "#dd2c00", 183 backgroundColor : "#ff9e80", 184 text : "JS Unoptimized" }, 185 { kinds : [ "IC" ], 186 color : "#ff6d00", 187 backgroundColor : "#ffab40", 188 text : "IC" }, 189 { kinds : [ "STUB", "BUILTIN", "REGEXP" ], 190 color : "#ffd600", 191 backgroundColor : "#ffea00", 192 text : "Other generated" }, 193 { kinds : [ "CPP", "LIB" ], 194 color : "#304ffe", 195 backgroundColor : "#6ab7ff", 196 text : "C++" }, 197 { kinds : [ "CPPEXT" ], 198 color : "#003c8f", 199 backgroundColor : "#c0cfff", 200 text : "C++/external" }, 201 { kinds : [ "CPPPARSE" ], 202 color : "#aa00ff", 203 backgroundColor : "#ffb2ff", 204 text : "C++/Parser" }, 205 { kinds : [ "CPPCOMPBC" ], 206 color : "#43a047", 207 backgroundColor : "#88c399", 208 text : "C++/Bytecode compiler" }, 209 { kinds : [ "CPPCOMP" ], 210 color : "#00e5ff", 211 backgroundColor : "#6effff", 212 text : "C++/Compiler" }, 213 { kinds : [ "CPPGC" ], 214 color : "#6200ea", 215 backgroundColor : "#e1bee7", 216 text : "C++/GC" }, 217 { kinds : [ "UNKNOWN" ], 218 color : "#bdbdbd", 219 backgroundColor : "#efefef", 220 text : "Unknown" } 221 ]; 222 223let kindToBucketDescriptor = {}; 224for (let i = 0; i < bucketDescriptors.length; i++) { 225 let bucket = bucketDescriptors[i]; 226 for (let j = 0; j < bucket.kinds.length; j++) { 227 kindToBucketDescriptor[bucket.kinds[j]] = bucket; 228 } 229} 230 231function bucketFromKind(kind) { 232 for (let i = 0; i < bucketDescriptors.length; i++) { 233 let bucket = bucketDescriptors[i]; 234 for (let j = 0; j < bucket.kinds.length; j++) { 235 if (bucket.kinds[j] === kind) { 236 return bucket; 237 } 238 } 239 } 240 return null; 241} 242 243function codeTypeToText(type) { 244 switch (type) { 245 case "UNKNOWN": 246 return "Unknown"; 247 case "CPPPARSE": 248 return "C++ Parser"; 249 case "CPPCOMPBC": 250 return "C++ Bytecode Compiler)"; 251 case "CPPCOMP": 252 return "C++ Compiler"; 253 case "CPPGC": 254 return "C++ GC"; 255 case "CPPEXT": 256 return "C++ External"; 257 case "CPP": 258 return "C++"; 259 case "LIB": 260 return "Library"; 261 case "IC": 262 return "IC"; 263 case "BC": 264 return "Bytecode"; 265 case "STUB": 266 return "Stub"; 267 case "BUILTIN": 268 return "Builtin"; 269 case "REGEXP": 270 return "RegExp"; 271 case "JSOPT": 272 return "JS opt"; 273 case "JSUNOPT": 274 return "JS unopt"; 275 } 276 console.error("Unknown type: " + type); 277} 278 279function createTypeNode(type) { 280 if (type === "CAT") { 281 return document.createTextNode(""); 282 } 283 let span = document.createElement("span"); 284 span.classList.add("code-type-chip"); 285 span.textContent = codeTypeToText(type); 286 287 return span; 288} 289 290function filterFromFilterId(id) { 291 switch (id) { 292 case "full-tree": 293 return (type, kind) => true; 294 case "js-funs": 295 return (type, kind) => type !== 'CODE'; 296 case "js-exclude-bc": 297 return (type, kind) => 298 type !== 'CODE' || kind !== "BytecodeHandler"; 299 } 300} 301 302function createIndentNode(indent) { 303 let div = document.createElement("div"); 304 div.style.display = "inline-block"; 305 div.style.width = (indent + 0.5) + "em"; 306 return div; 307} 308 309function createArrowNode() { 310 let span = document.createElement("span"); 311 span.classList.add("tree-row-arrow"); 312 return span; 313} 314 315function createFunctionNode(name, codeId) { 316 let nameElement = document.createElement("span"); 317 nameElement.appendChild(document.createTextNode(name)); 318 nameElement.classList.add("tree-row-name"); 319 if (codeId !== -1) { 320 nameElement.classList.add("codeid-link"); 321 nameElement.onclick = (event) => { 322 main.setCurrentCode(codeId); 323 // Prevent the click from bubbling to the row and causing it to 324 // collapse/expand. 325 event.stopPropagation(); 326 }; 327 } 328 return nameElement; 329} 330 331const COLLAPSED_ARROW = "\u25B6"; 332const EXPANDED_ARROW = "\u25BC"; 333 334class CallTreeView { 335 constructor() { 336 this.element = $("calltree"); 337 this.treeElement = $("calltree-table"); 338 this.selectAttribution = $("calltree-attribution"); 339 this.selectCategories = $("calltree-categories"); 340 this.selectSort = $("calltree-sort"); 341 342 this.selectAttribution.onchange = () => { 343 main.setCallTreeAttribution(this.selectAttribution.value); 344 }; 345 346 this.selectCategories.onchange = () => { 347 main.setCallTreeCategories(this.selectCategories.value); 348 }; 349 350 this.selectSort.onchange = () => { 351 main.setCallTreeSort(this.selectSort.value); 352 }; 353 354 this.currentState = null; 355 } 356 357 sortFromId(id) { 358 switch (id) { 359 case "time": 360 return (c1, c2) => { 361 if (c1.ticks < c2.ticks) return 1; 362 else if (c1.ticks > c2.ticks) return -1; 363 return c2.ownTicks - c1.ownTicks; 364 }; 365 case "own-time": 366 return (c1, c2) => { 367 if (c1.ownTicks < c2.ownTicks) return 1; 368 else if (c1.ownTicks > c2.ownTicks) return -1; 369 return c2.ticks - c1.ticks; 370 }; 371 case "category-time": 372 return (c1, c2) => { 373 if (c1.type === c2.type) return c2.ticks - c1.ticks; 374 if (c1.type < c2.type) return 1; 375 return -1; 376 }; 377 case "category-own-time": 378 return (c1, c2) => { 379 if (c1.type === c2.type) return c2.ownTicks - c1.ownTicks; 380 if (c1.type < c2.type) return 1; 381 return -1; 382 }; 383 } 384 } 385 386 expandTree(tree, indent) { 387 let index = 0; 388 let id = "R/"; 389 let row = tree.row; 390 391 if (row) { 392 index = row.rowIndex; 393 id = row.id; 394 395 tree.arrow.textContent = EXPANDED_ARROW; 396 // Collapse the children when the row is clicked again. 397 let expandHandler = row.onclick; 398 row.onclick = () => { 399 this.collapseRow(tree, expandHandler); 400 } 401 } 402 403 // Collect the children, and sort them by ticks. 404 let children = []; 405 let filter = 406 filterFromFilterId(this.currentState.callTree.attribution); 407 for (let childId in tree.children) { 408 let child = tree.children[childId]; 409 if (child.ticks > 0) { 410 children.push(child); 411 if (child.delayedExpansion) { 412 expandTreeNode(this.currentState.file, child, filter); 413 } 414 } 415 } 416 children.sort(this.sortFromId(this.currentState.callTree.sort)); 417 418 for (let i = 0; i < children.length; i++) { 419 let node = children[i]; 420 let row = this.rows.insertRow(index); 421 row.id = id + i + "/"; 422 423 if (node.type === "CAT") { 424 row.style.backgroundColor = CATEGORY_COLOR; 425 } else { 426 row.style.backgroundColor = bucketFromKind(node.type).backgroundColor; 427 } 428 429 // Inclusive time % cell. 430 let c = row.insertCell(); 431 c.textContent = (node.ticks * 100 / this.tickCount).toFixed(2) + "%"; 432 c.style.textAlign = "right"; 433 // Percent-of-parent cell. 434 c = row.insertCell(); 435 c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%"; 436 c.style.textAlign = "right"; 437 // Exclusive time % cell. 438 if (this.currentState.mode !== "bottom-up") { 439 c = row.insertCell(-1); 440 c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%"; 441 c.style.textAlign = "right"; 442 } 443 444 // Create the name cell. 445 let nameCell = row.insertCell(); 446 nameCell.appendChild(createIndentNode(indent + 1)); 447 let arrow = createArrowNode(); 448 nameCell.appendChild(arrow); 449 nameCell.appendChild(createTypeNode(node.type)); 450 nameCell.appendChild(createFunctionNode(node.name, node.codeId)); 451 452 // Inclusive ticks cell. 453 c = row.insertCell(); 454 c.textContent = node.ticks; 455 c.style.textAlign = "right"; 456 if (this.currentState.mode !== "bottom-up") { 457 // Exclusive ticks cell. 458 c = row.insertCell(-1); 459 c.textContent = node.ownTicks; 460 c.style.textAlign = "right"; 461 } 462 if (node.children.length > 0) { 463 arrow.textContent = COLLAPSED_ARROW; 464 row.onclick = () => { this.expandTree(node, indent + 1); }; 465 } 466 467 node.row = row; 468 node.arrow = arrow; 469 470 index++; 471 } 472 } 473 474 collapseRow(tree, expandHandler) { 475 let row = tree.row; 476 let id = row.id; 477 let index = row.rowIndex; 478 while (row.rowIndex < this.rows.rows.length && 479 this.rows.rows[index].id.startsWith(id)) { 480 this.rows.deleteRow(index); 481 } 482 483 tree.arrow.textContent = COLLAPSED_ARROW; 484 row.onclick = expandHandler; 485 } 486 487 fillSelects(mode, calltree) { 488 function addOptions(e, values, current) { 489 while (e.options.length > 0) { 490 e.remove(0); 491 } 492 for (let i = 0; i < values.length; i++) { 493 let option = document.createElement("option"); 494 option.value = values[i].value; 495 option.textContent = values[i].text; 496 e.appendChild(option); 497 } 498 e.value = current; 499 } 500 501 let attributions = [ 502 { value : "js-exclude-bc", 503 text : "Attribute bytecode handlers to caller" }, 504 { value : "full-tree", 505 text : "Count each code object separately" }, 506 { value : "js-funs", 507 text : "Attribute non-functions to JS functions" } 508 ]; 509 510 switch (mode) { 511 case "bottom-up": 512 addOptions(this.selectAttribution, attributions, calltree.attribution); 513 addOptions(this.selectCategories, [ 514 { value : "code-type", text : "Code type" }, 515 { value : "none", text : "None" } 516 ], calltree.categories); 517 addOptions(this.selectSort, [ 518 { value : "time", text : "Time (including children)" }, 519 { value : "category-time", text : "Code category, time" }, 520 ], calltree.sort); 521 return; 522 case "top-down": 523 addOptions(this.selectAttribution, attributions, calltree.attribution); 524 addOptions(this.selectCategories, [ 525 { value : "none", text : "None" }, 526 { value : "rt-entry", text : "Runtime entries" } 527 ], calltree.categories); 528 addOptions(this.selectSort, [ 529 { value : "time", text : "Time (including children)" }, 530 { value : "own-time", text : "Own time" }, 531 { value : "category-time", text : "Code category, time" }, 532 { value : "category-own-time", text : "Code category, own time"} 533 ], calltree.sort); 534 return; 535 case "function-list": 536 addOptions(this.selectAttribution, attributions, calltree.attribution); 537 addOptions(this.selectCategories, [ 538 { value : "code-type", text : "Code type" }, 539 { value : "none", text : "None" } 540 ], calltree.categories); 541 addOptions(this.selectSort, [ 542 { value : "own-time", text : "Own time" }, 543 { value : "time", text : "Time (including children)" }, 544 { value : "category-own-time", text : "Code category, own time"}, 545 { value : "category-time", text : "Code category, time" }, 546 ], calltree.sort); 547 return; 548 } 549 console.error("Unexpected mode"); 550 } 551 552 static isCallTreeMode(mode) { 553 switch (mode) { 554 case "bottom-up": 555 case "top-down": 556 case "function-list": 557 return true; 558 default: 559 return false; 560 } 561 } 562 563 render(newState) { 564 let oldState = this.currentState; 565 if (!newState.file || !CallTreeView.isCallTreeMode(newState.mode)) { 566 this.element.style.display = "none"; 567 this.currentState = null; 568 return; 569 } 570 571 this.currentState = newState; 572 if (oldState) { 573 if (newState.file === oldState.file && 574 newState.start === oldState.start && 575 newState.end === oldState.end && 576 newState.mode === oldState.mode && 577 newState.callTree.attribution === oldState.callTree.attribution && 578 newState.callTree.categories === oldState.callTree.categories && 579 newState.callTree.sort === oldState.callTree.sort) { 580 // No change => just return. 581 return; 582 } 583 } 584 585 this.element.style.display = "inherit"; 586 587 let mode = this.currentState.mode; 588 if (!oldState || mode !== oldState.mode) { 589 // Technically, we should also call this if attribution, categories or 590 // sort change, but the selection is already highlighted by the combobox 591 // itself, so we do need to do anything here. 592 this.fillSelects(newState.mode, newState.callTree); 593 } 594 595 let ownTimeClass = (mode === "bottom-up") ? "numeric-hidden" : "numeric"; 596 let ownTimeTh = $(this.treeElement.id + "-own-time-header"); 597 ownTimeTh.classList = ownTimeClass; 598 let ownTicksTh = $(this.treeElement.id + "-own-ticks-header"); 599 ownTicksTh.classList = ownTimeClass; 600 601 // Build the tree. 602 let stackProcessor; 603 let filter = filterFromFilterId(this.currentState.callTree.attribution); 604 if (mode === "top-down") { 605 if (this.currentState.callTree.categories === "rt-entry") { 606 stackProcessor = 607 new RuntimeCallTreeProcessor(); 608 } else { 609 stackProcessor = 610 new PlainCallTreeProcessor(filter, false); 611 } 612 } else if (mode === "function-list") { 613 stackProcessor = new FunctionListTree( 614 filter, this.currentState.callTree.categories === "code-type"); 615 616 } else { 617 console.assert(mode === "bottom-up"); 618 if (this.currentState.callTree.categories === "none") { 619 stackProcessor = 620 new PlainCallTreeProcessor(filter, true); 621 } else { 622 console.assert(this.currentState.callTree.categories === "code-type"); 623 stackProcessor = 624 new CategorizedCallTreeProcessor(filter, true); 625 } 626 } 627 this.tickCount = 628 generateTree(this.currentState.file, 629 this.currentState.start, 630 this.currentState.end, 631 stackProcessor); 632 // TODO(jarin) Handle the case when tick count is negative. 633 634 this.tree = stackProcessor.tree; 635 636 // Remove old content of the table, replace with new one. 637 let oldRows = this.treeElement.getElementsByTagName("tbody"); 638 let newRows = document.createElement("tbody"); 639 this.rows = newRows; 640 641 // Populate the table. 642 this.expandTree(this.tree, 0); 643 644 // Swap in the new rows. 645 this.treeElement.replaceChild(newRows, oldRows[0]); 646 } 647} 648 649class TimelineView { 650 constructor() { 651 this.element = $("timeline"); 652 this.canvas = $("timeline-canvas"); 653 this.legend = $("timeline-legend"); 654 this.currentCode = $("timeline-currentCode"); 655 656 this.canvas.onmousedown = this.onMouseDown.bind(this); 657 this.canvas.onmouseup = this.onMouseUp.bind(this); 658 this.canvas.onmousemove = this.onMouseMove.bind(this); 659 660 this.selectionStart = null; 661 this.selectionEnd = null; 662 this.selecting = false; 663 664 this.fontSize = 12; 665 this.imageOffset = Math.round(this.fontSize * 1.2); 666 this.functionTimelineHeight = 24; 667 this.functionTimelineTickHeight = 16; 668 669 this.currentState = null; 670 } 671 672 onMouseDown(e) { 673 this.selectionStart = 674 e.clientX - this.canvas.getBoundingClientRect().left; 675 this.selectionEnd = this.selectionStart + 1; 676 this.selecting = true; 677 } 678 679 onMouseMove(e) { 680 if (this.selecting) { 681 this.selectionEnd = 682 e.clientX - this.canvas.getBoundingClientRect().left; 683 this.drawSelection(); 684 } 685 } 686 687 onMouseUp(e) { 688 if (this.selectionStart !== null) { 689 let x = e.clientX - this.canvas.getBoundingClientRect().left; 690 if (Math.abs(x - this.selectionStart) < 10) { 691 this.selectionStart = null; 692 this.selectionEnd = null; 693 let ctx = this.canvas.getContext("2d"); 694 ctx.drawImage(this.buffer, 0, this.imageOffset); 695 } else { 696 this.selectionEnd = x; 697 this.drawSelection(); 698 } 699 let file = this.currentState.file; 700 if (file) { 701 let start = this.selectionStart === null ? 0 : this.selectionStart; 702 let end = this.selectionEnd === null ? Infinity : this.selectionEnd; 703 let firstTime = file.ticks[0].tm; 704 let lastTime = file.ticks[file.ticks.length - 1].tm; 705 706 let width = this.buffer.width; 707 708 start = (start / width) * (lastTime - firstTime) + firstTime; 709 end = (end / width) * (lastTime - firstTime) + firstTime; 710 711 if (end < start) { 712 let temp = start; 713 start = end; 714 end = temp; 715 } 716 717 main.setViewInterval(start, end); 718 } 719 } 720 this.selecting = false; 721 } 722 723 drawSelection() { 724 let ctx = this.canvas.getContext("2d"); 725 726 // Draw the timeline image. 727 ctx.drawImage(this.buffer, 0, this.imageOffset); 728 729 // Draw the current interval highlight. 730 let left; 731 let right; 732 if (this.selectionStart !== null && this.selectionEnd !== null) { 733 ctx.fillStyle = "rgba(0, 0, 0, 0.3)"; 734 left = Math.min(this.selectionStart, this.selectionEnd); 735 right = Math.max(this.selectionStart, this.selectionEnd); 736 let height = this.buffer.height - this.functionTimelineHeight; 737 ctx.fillRect(0, this.imageOffset, left, height); 738 ctx.fillRect(right, this.imageOffset, this.buffer.width - right, height); 739 } else { 740 left = 0; 741 right = this.buffer.width; 742 } 743 744 // Draw the scale text. 745 let file = this.currentState.file; 746 ctx.fillStyle = "white"; 747 ctx.fillRect(0, 0, this.canvas.width, this.imageOffset); 748 if (file && file.ticks.length > 0) { 749 let firstTime = file.ticks[0].tm; 750 let lastTime = file.ticks[file.ticks.length - 1].tm; 751 752 let leftTime = 753 firstTime + left / this.canvas.width * (lastTime - firstTime); 754 let rightTime = 755 firstTime + right / this.canvas.width * (lastTime - firstTime); 756 757 let leftText = (leftTime / 1000000).toFixed(3) + "s"; 758 let rightText = (rightTime / 1000000).toFixed(3) + "s"; 759 760 ctx.textBaseline = 'top'; 761 ctx.font = this.fontSize + "px Arial"; 762 ctx.fillStyle = "black"; 763 764 let leftWidth = ctx.measureText(leftText).width; 765 let rightWidth = ctx.measureText(rightText).width; 766 767 let leftStart = left - leftWidth / 2; 768 let rightStart = right - rightWidth / 2; 769 770 if (leftStart < 0) leftStart = 0; 771 if (rightStart + rightWidth > this.canvas.width) { 772 rightStart = this.canvas.width - rightWidth; 773 } 774 if (leftStart + leftWidth > rightStart) { 775 if (leftStart > this.canvas.width - (rightStart - rightWidth)) { 776 rightStart = leftStart + leftWidth; 777 778 } else { 779 leftStart = rightStart - leftWidth; 780 } 781 } 782 783 ctx.fillText(leftText, leftStart, 0); 784 ctx.fillText(rightText, rightStart, 0); 785 } 786 } 787 788 render(newState) { 789 let oldState = this.currentState; 790 791 if (!newState.file) { 792 this.element.style.display = "none"; 793 return; 794 } 795 796 let width = Math.round(window.innerWidth - 20); 797 let height = Math.round(window.innerHeight / 5); 798 799 if (oldState) { 800 if (width === oldState.timelineSize.width && 801 height === oldState.timelineSize.height && 802 newState.file === oldState.file && 803 newState.currentCodeId === oldState.currentCodeId && 804 newState.start === oldState.start && 805 newState.end === oldState.end) { 806 // No change, nothing to do. 807 return; 808 } 809 } 810 this.currentState = newState; 811 this.currentState.timelineSize.width = width; 812 this.currentState.timelineSize.height = height; 813 814 this.element.style.display = "inherit"; 815 816 let file = this.currentState.file; 817 818 const minPixelsPerBucket = 10; 819 const minTicksPerBucket = 8; 820 let maxBuckets = Math.round(file.ticks.length / minTicksPerBucket); 821 let bucketCount = Math.min( 822 Math.round(width / minPixelsPerBucket), maxBuckets); 823 824 // Make sure the canvas has the right dimensions. 825 this.canvas.width = width; 826 this.canvas.height = height; 827 828 // Make space for the selection text. 829 height -= this.imageOffset; 830 831 let currentCodeId = this.currentState.currentCodeId; 832 833 let firstTime = file.ticks[0].tm; 834 let lastTime = file.ticks[file.ticks.length - 1].tm; 835 let start = Math.max(this.currentState.start, firstTime); 836 let end = Math.min(this.currentState.end, lastTime); 837 838 this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width; 839 this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width; 840 841 let stackProcessor = new CategorySampler(file, bucketCount); 842 generateTree(file, 0, Infinity, stackProcessor); 843 let codeIdProcessor = new FunctionTimelineProcessor( 844 currentCodeId, 845 filterFromFilterId(this.currentState.callTree.attribution)); 846 generateTree(file, 0, Infinity, codeIdProcessor); 847 848 let buffer = document.createElement("canvas"); 849 850 buffer.width = width; 851 buffer.height = height; 852 853 // Calculate the bar heights for each bucket. 854 let graphHeight = height - this.functionTimelineHeight; 855 let buckets = stackProcessor.buckets; 856 let bucketsGraph = []; 857 for (let i = 0; i < buckets.length; i++) { 858 let sum = 0; 859 let bucketData = []; 860 let total = buckets[i].total; 861 if (total > 0) { 862 for (let j = 0; j < bucketDescriptors.length; j++) { 863 let desc = bucketDescriptors[j]; 864 for (let k = 0; k < desc.kinds.length; k++) { 865 sum += buckets[i][desc.kinds[k]]; 866 } 867 bucketData.push(Math.round(graphHeight * sum / total)); 868 } 869 } else { 870 // No ticks fell into this bucket. Fill with "Unknown." 871 for (let j = 0; j < bucketDescriptors.length; j++) { 872 let desc = bucketDescriptors[j]; 873 bucketData.push(desc.text === "Unknown" ? graphHeight : 0); 874 } 875 } 876 bucketsGraph.push(bucketData); 877 } 878 879 // Draw the category graph into the buffer. 880 let bucketWidth = width / (bucketsGraph.length - 1); 881 let ctx = buffer.getContext('2d'); 882 for (let i = 0; i < bucketsGraph.length - 1; i++) { 883 let bucketData = bucketsGraph[i]; 884 let nextBucketData = bucketsGraph[i + 1]; 885 let x1 = Math.round(i * bucketWidth); 886 let x2 = Math.round((i + 1) * bucketWidth); 887 for (let j = 0; j < bucketData.length; j++) { 888 ctx.beginPath(); 889 ctx.moveTo(x1, j > 0 ? bucketData[j - 1] : 0); 890 ctx.lineTo(x2, j > 0 ? nextBucketData[j - 1] : 0); 891 ctx.lineTo(x2, nextBucketData[j]); 892 ctx.lineTo(x1, bucketData[j]); 893 ctx.closePath(); 894 ctx.fillStyle = bucketDescriptors[j].color; 895 ctx.fill(); 896 } 897 } 898 899 // Draw the function ticks. 900 let functionTimelineYOffset = graphHeight; 901 let functionTimelineTickHeight = this.functionTimelineTickHeight; 902 let functionTimelineHalfHeight = 903 Math.round(functionTimelineTickHeight / 2); 904 let timestampScaler = width / (lastTime - firstTime); 905 let timestampToX = (t) => Math.round((t - firstTime) * timestampScaler); 906 ctx.fillStyle = "white"; 907 ctx.fillRect( 908 0, 909 functionTimelineYOffset, 910 buffer.width, 911 this.functionTimelineHeight); 912 for (let i = 0; i < codeIdProcessor.blocks.length; i++) { 913 let block = codeIdProcessor.blocks[i]; 914 let bucket = kindToBucketDescriptor[block.kind]; 915 ctx.fillStyle = bucket.color; 916 ctx.fillRect( 917 timestampToX(block.start), 918 functionTimelineYOffset, 919 Math.max(1, Math.round((block.end - block.start) * timestampScaler)), 920 block.topOfStack ? 921 functionTimelineTickHeight : functionTimelineHalfHeight); 922 } 923 ctx.strokeStyle = "black"; 924 ctx.lineWidth = "1"; 925 ctx.beginPath(); 926 ctx.moveTo(0, functionTimelineYOffset + 0.5); 927 ctx.lineTo(buffer.width, functionTimelineYOffset + 0.5); 928 ctx.stroke(); 929 ctx.strokeStyle = "rgba(0,0,0,0.2)"; 930 ctx.lineWidth = "1"; 931 ctx.beginPath(); 932 ctx.moveTo(0, functionTimelineYOffset + functionTimelineHalfHeight - 0.5); 933 ctx.lineTo(buffer.width, 934 functionTimelineYOffset + functionTimelineHalfHeight - 0.5); 935 ctx.stroke(); 936 937 // Draw marks for optimizations and deoptimizations in the function 938 // timeline. 939 if (currentCodeId && currentCodeId >= 0 && 940 file.code[currentCodeId].func) { 941 let y = Math.round(functionTimelineYOffset + functionTimelineTickHeight + 942 (this.functionTimelineHeight - functionTimelineTickHeight) / 2); 943 let func = file.functions[file.code[currentCodeId].func]; 944 for (let i = 0; i < func.codes.length; i++) { 945 let code = file.code[func.codes[i]]; 946 if (code.kind === "Opt") { 947 if (code.deopt) { 948 // Draw deoptimization mark. 949 let x = timestampToX(code.deopt.tm); 950 ctx.lineWidth = 0.7; 951 ctx.strokeStyle = "red"; 952 ctx.beginPath(); 953 ctx.moveTo(x - 3, y - 3); 954 ctx.lineTo(x + 3, y + 3); 955 ctx.stroke(); 956 ctx.beginPath(); 957 ctx.moveTo(x - 3, y + 3); 958 ctx.lineTo(x + 3, y - 3); 959 ctx.stroke(); 960 } 961 // Draw optimization mark. 962 let x = timestampToX(code.tm); 963 ctx.lineWidth = 0.7; 964 ctx.strokeStyle = "blue"; 965 ctx.beginPath(); 966 ctx.moveTo(x - 3, y - 3); 967 ctx.lineTo(x, y); 968 ctx.stroke(); 969 ctx.beginPath(); 970 ctx.moveTo(x - 3, y + 3); 971 ctx.lineTo(x, y); 972 ctx.stroke(); 973 } else { 974 // Draw code creation mark. 975 let x = Math.round(timestampToX(code.tm)); 976 ctx.beginPath(); 977 ctx.fillStyle = "black"; 978 ctx.arc(x, y, 3, 0, 2 * Math.PI); 979 ctx.fill(); 980 } 981 } 982 } 983 984 // Remember stuff for later. 985 this.buffer = buffer; 986 987 // Draw the buffer. 988 this.drawSelection(); 989 990 // (Re-)Populate the graph legend. 991 while (this.legend.cells.length > 0) { 992 this.legend.deleteCell(0); 993 } 994 let cell = this.legend.insertCell(-1); 995 cell.textContent = "Legend: "; 996 cell.style.padding = "1ex"; 997 for (let i = 0; i < bucketDescriptors.length; i++) { 998 let cell = this.legend.insertCell(-1); 999 cell.style.padding = "1ex"; 1000 let desc = bucketDescriptors[i]; 1001 let div = document.createElement("div"); 1002 div.style.display = "inline-block"; 1003 div.style.width = "0.6em"; 1004 div.style.height = "1.2ex"; 1005 div.style.backgroundColor = desc.color; 1006 div.style.borderStyle = "solid"; 1007 div.style.borderWidth = "1px"; 1008 div.style.borderColor = "Black"; 1009 cell.appendChild(div); 1010 cell.appendChild(document.createTextNode(" " + desc.text)); 1011 } 1012 1013 while (this.currentCode.firstChild) { 1014 this.currentCode.removeChild(this.currentCode.firstChild); 1015 } 1016 if (currentCodeId) { 1017 let currentCode = file.code[currentCodeId]; 1018 this.currentCode.appendChild(document.createTextNode(currentCode.name)); 1019 } else { 1020 this.currentCode.appendChild(document.createTextNode("<none>")); 1021 } 1022 } 1023} 1024 1025class ModeBarView { 1026 constructor() { 1027 let modeBar = this.element = $("mode-bar"); 1028 1029 function addMode(id, text, active) { 1030 let div = document.createElement("div"); 1031 div.classList = "mode-button" + (active ? " active-mode-button" : ""); 1032 div.id = "mode-" + id; 1033 div.textContent = text; 1034 div.onclick = () => { 1035 if (main.currentState.mode === id) return; 1036 let old = $("mode-" + main.currentState.mode); 1037 old.classList = "mode-button"; 1038 div.classList = "mode-button active-mode-button"; 1039 main.setMode(id); 1040 }; 1041 modeBar.appendChild(div); 1042 } 1043 1044 addMode("summary", "Summary", true); 1045 addMode("bottom-up", "Bottom up"); 1046 addMode("top-down", "Top down"); 1047 addMode("function-list", "Functions"); 1048 } 1049 1050 render(newState) { 1051 if (!newState.file) { 1052 this.element.style.display = "none"; 1053 return; 1054 } 1055 1056 this.element.style.display = "inherit"; 1057 } 1058} 1059 1060class SummaryView { 1061 constructor() { 1062 this.element = $("summary"); 1063 this.currentState = null; 1064 } 1065 1066 render(newState) { 1067 let oldState = this.currentState; 1068 1069 if (!newState.file || newState.mode !== "summary") { 1070 this.element.style.display = "none"; 1071 this.currentState = null; 1072 return; 1073 } 1074 1075 this.currentState = newState; 1076 if (oldState) { 1077 if (newState.file === oldState.file && 1078 newState.start === oldState.start && 1079 newState.end === oldState.end) { 1080 // No change, nothing to do. 1081 return; 1082 } 1083 } 1084 1085 this.element.style.display = "inherit"; 1086 1087 while (this.element.firstChild) { 1088 this.element.removeChild(this.element.firstChild); 1089 } 1090 1091 let stats = computeOptimizationStats( 1092 this.currentState.file, newState.start, newState.end); 1093 1094 let table = document.createElement("table"); 1095 let rows = document.createElement("tbody"); 1096 1097 function addRow(text, number, indent) { 1098 let row = rows.insertRow(-1); 1099 let textCell = row.insertCell(-1); 1100 textCell.textContent = text; 1101 let numberCell = row.insertCell(-1); 1102 numberCell.textContent = number; 1103 if (indent) { 1104 textCell.style.textIndent = indent + "em"; 1105 numberCell.style.textIndent = indent + "em"; 1106 } 1107 return row; 1108 } 1109 1110 function makeCollapsible(row, arrow) { 1111 arrow.textContent = EXPANDED_ARROW; 1112 let expandHandler = row.onclick; 1113 row.onclick = () => { 1114 let id = row.id; 1115 let index = row.rowIndex + 1; 1116 while (index < rows.rows.length && 1117 rows.rows[index].id.startsWith(id)) { 1118 rows.deleteRow(index); 1119 } 1120 arrow.textContent = COLLAPSED_ARROW; 1121 row.onclick = expandHandler; 1122 } 1123 } 1124 1125 function expandDeoptInstances(row, arrow, instances, indent, kind) { 1126 let index = row.rowIndex; 1127 for (let i = 0; i < instances.length; i++) { 1128 let childRow = rows.insertRow(index + 1); 1129 childRow.id = row.id + i + "/"; 1130 1131 let deopt = instances[i].deopt; 1132 1133 let textCell = childRow.insertCell(-1); 1134 textCell.appendChild(document.createTextNode(deopt.posText)); 1135 textCell.style.textIndent = indent + "em"; 1136 let reasonCell = childRow.insertCell(-1); 1137 reasonCell.appendChild( 1138 document.createTextNode("Reason: " + deopt.reason)); 1139 reasonCell.style.textIndent = indent + "em"; 1140 } 1141 makeCollapsible(row, arrow); 1142 } 1143 1144 function expandDeoptFunctionList(row, arrow, list, indent, kind) { 1145 let index = row.rowIndex; 1146 for (let i = 0; i < list.length; i++) { 1147 let childRow = rows.insertRow(index + 1); 1148 childRow.id = row.id + i + "/"; 1149 1150 let textCell = childRow.insertCell(-1); 1151 textCell.appendChild(createIndentNode(indent)); 1152 let childArrow = createArrowNode(); 1153 textCell.appendChild(childArrow); 1154 textCell.appendChild( 1155 createFunctionNode(list[i].f.name, list[i].f.codes[0])); 1156 1157 let numberCell = childRow.insertCell(-1); 1158 numberCell.textContent = list[i].instances.length; 1159 numberCell.style.textIndent = indent + "em"; 1160 1161 childArrow.textContent = COLLAPSED_ARROW; 1162 childRow.onclick = () => { 1163 expandDeoptInstances( 1164 childRow, childArrow, list[i].instances, indent + 1); 1165 }; 1166 } 1167 makeCollapsible(row, arrow); 1168 } 1169 1170 function expandOptimizedFunctionList(row, arrow, list, indent, kind) { 1171 let index = row.rowIndex; 1172 for (let i = 0; i < list.length; i++) { 1173 let childRow = rows.insertRow(index + 1); 1174 childRow.id = row.id + i + "/"; 1175 1176 let textCell = childRow.insertCell(-1); 1177 textCell.appendChild( 1178 createFunctionNode(list[i].f.name, list[i].f.codes[0])); 1179 textCell.style.textIndent = indent + "em"; 1180 1181 let numberCell = childRow.insertCell(-1); 1182 numberCell.textContent = list[i].instances.length; 1183 numberCell.style.textIndent = indent + "em"; 1184 } 1185 makeCollapsible(row, arrow); 1186 } 1187 1188 function addExpandableRow(text, list, indent, kind) { 1189 let row = rows.insertRow(-1); 1190 1191 row.id = "opt-table/" + kind + "/"; 1192 row.style.backgroundColor = CATEGORY_COLOR; 1193 1194 let textCell = row.insertCell(-1); 1195 textCell.appendChild(createIndentNode(indent)); 1196 let arrow = createArrowNode(); 1197 textCell.appendChild(arrow); 1198 textCell.appendChild(document.createTextNode(text)); 1199 1200 let numberCell = row.insertCell(-1); 1201 numberCell.textContent = list.count; 1202 if (indent) { 1203 numberCell.style.textIndent = indent + "em"; 1204 } 1205 1206 if (list.count > 0) { 1207 arrow.textContent = COLLAPSED_ARROW; 1208 if (kind === "opt") { 1209 row.onclick = () => { 1210 expandOptimizedFunctionList( 1211 row, arrow, list.functions, indent + 1, kind); 1212 }; 1213 } else { 1214 row.onclick = () => { 1215 expandDeoptFunctionList( 1216 row, arrow, list.functions, indent + 1, kind); 1217 }; 1218 } 1219 } 1220 return row; 1221 } 1222 1223 addRow("Total function count:", stats.functionCount); 1224 addRow("Optimized function count:", stats.optimizedFunctionCount, 1); 1225 addRow("Deoptimized function count:", stats.deoptimizedFunctionCount, 2); 1226 1227 addExpandableRow("Optimization count:", stats.optimizations, 0, "opt"); 1228 let deoptCount = stats.eagerDeoptimizations.count + 1229 stats.softDeoptimizations.count + stats.lazyDeoptimizations.count; 1230 addRow("Deoptimization count:", deoptCount); 1231 addExpandableRow("Eager:", stats.eagerDeoptimizations, 1, "eager"); 1232 addExpandableRow("Lazy:", stats.lazyDeoptimizations, 1, "lazy"); 1233 addExpandableRow("Soft:", stats.softDeoptimizations, 1, "soft"); 1234 1235 table.appendChild(rows); 1236 this.element.appendChild(table); 1237 } 1238} 1239 1240class HelpView { 1241 constructor() { 1242 this.element = $("help"); 1243 } 1244 1245 render(newState) { 1246 this.element.style.display = newState.file ? "none" : "inherit"; 1247 } 1248} 1249