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 11function removeAllChildren(element) { 12 while (element.firstChild) { 13 element.removeChild(element.firstChild); 14 } 15} 16 17let components; 18function createViews() { 19 components = [ 20 new CallTreeView(), 21 new TimelineView(), 22 new HelpView(), 23 new SummaryView(), 24 new ModeBarView(), 25 new ScriptSourceView(), 26 ]; 27} 28 29function emptyState() { 30 return { 31 file : null, 32 mode : null, 33 currentCodeId : null, 34 viewingSource: false, 35 start : 0, 36 end : Infinity, 37 timelineSize : { 38 width : 0, 39 height : 0 40 }, 41 callTree : { 42 attribution : "js-exclude-bc", 43 categories : "code-type", 44 sort : "time" 45 }, 46 sourceData: null 47 }; 48} 49 50function setCallTreeState(state, callTreeState) { 51 state = Object.assign({}, state); 52 state.callTree = callTreeState; 53 return state; 54} 55 56let main = { 57 currentState : emptyState(), 58 renderPending : false, 59 60 setMode(mode) { 61 if (mode !== main.currentState.mode) { 62 63 function setCallTreeModifiers(attribution, categories, sort) { 64 let callTreeState = Object.assign({}, main.currentState.callTree); 65 callTreeState.attribution = attribution; 66 callTreeState.categories = categories; 67 callTreeState.sort = sort; 68 return callTreeState; 69 } 70 71 let state = Object.assign({}, main.currentState); 72 73 switch (mode) { 74 case "bottom-up": 75 state.callTree = 76 setCallTreeModifiers("js-exclude-bc", "code-type", "time"); 77 break; 78 case "top-down": 79 state.callTree = 80 setCallTreeModifiers("js-exclude-bc", "none", "time"); 81 break; 82 case "function-list": 83 state.callTree = 84 setCallTreeModifiers("js-exclude-bc", "code-type", "own-time"); 85 break; 86 } 87 88 state.mode = mode; 89 90 main.currentState = state; 91 main.delayRender(); 92 } 93 }, 94 95 setCallTreeAttribution(attribution) { 96 if (attribution !== main.currentState.attribution) { 97 let callTreeState = Object.assign({}, main.currentState.callTree); 98 callTreeState.attribution = attribution; 99 main.currentState = setCallTreeState(main.currentState, callTreeState); 100 main.delayRender(); 101 } 102 }, 103 104 setCallTreeSort(sort) { 105 if (sort !== main.currentState.sort) { 106 let callTreeState = Object.assign({}, main.currentState.callTree); 107 callTreeState.sort = sort; 108 main.currentState = setCallTreeState(main.currentState, callTreeState); 109 main.delayRender(); 110 } 111 }, 112 113 setCallTreeCategories(categories) { 114 if (categories !== main.currentState.categories) { 115 let callTreeState = Object.assign({}, main.currentState.callTree); 116 callTreeState.categories = categories; 117 main.currentState = setCallTreeState(main.currentState, callTreeState); 118 main.delayRender(); 119 } 120 }, 121 122 setViewInterval(start, end) { 123 if (start !== main.currentState.start || 124 end !== main.currentState.end) { 125 main.currentState = Object.assign({}, main.currentState); 126 main.currentState.start = start; 127 main.currentState.end = end; 128 main.delayRender(); 129 } 130 }, 131 132 updateSources(file) { 133 let statusDiv = $("source-status"); 134 if (!file) { 135 statusDiv.textContent = ""; 136 return; 137 } 138 if (!file.scripts || file.scripts.length === 0) { 139 statusDiv.textContent = 140 "Script source not available. Run profiler with --log-source-code."; 141 return; 142 } 143 statusDiv.textContent = "Script source is available."; 144 main.currentState.sourceData = new SourceData(file); 145 }, 146 147 setFile(file) { 148 if (file !== main.currentState.file) { 149 let lastMode = main.currentState.mode || "summary"; 150 main.currentState = emptyState(); 151 main.currentState.file = file; 152 main.updateSources(file); 153 main.setMode(lastMode); 154 main.delayRender(); 155 } 156 }, 157 158 setCurrentCode(codeId) { 159 if (codeId !== main.currentState.currentCodeId) { 160 main.currentState = Object.assign({}, main.currentState); 161 main.currentState.currentCodeId = codeId; 162 main.delayRender(); 163 } 164 }, 165 166 setViewingSource(value) { 167 if (main.currentState.viewingSource !== value) { 168 main.currentState = Object.assign({}, main.currentState); 169 main.currentState.viewingSource = value; 170 main.delayRender(); 171 } 172 }, 173 174 onResize() { 175 main.delayRender(); 176 }, 177 178 onLoad() { 179 function loadHandler(evt) { 180 let f = evt.target.files[0]; 181 if (f) { 182 let reader = new FileReader(); 183 reader.onload = function(event) { 184 main.setFile(JSON.parse(event.target.result)); 185 }; 186 reader.onerror = function(event) { 187 console.error( 188 "File could not be read! Code " + event.target.error.code); 189 }; 190 reader.readAsText(f); 191 } else { 192 main.setFile(null); 193 } 194 } 195 $("fileinput").addEventListener( 196 "change", loadHandler, false); 197 createViews(); 198 }, 199 200 delayRender() { 201 if (main.renderPending) return; 202 main.renderPending = true; 203 204 window.requestAnimationFrame(() => { 205 main.renderPending = false; 206 for (let c of components) { 207 c.render(main.currentState); 208 } 209 }); 210 } 211}; 212 213const CATEGORY_COLOR = "#f5f5f5"; 214const bucketDescriptors = 215 [{ 216 kinds: ["JS_OPT"], 217 color: "#64dd17", 218 backgroundColor: "#80e27e", 219 text: "JS Optimized" 220 }, 221 { 222 kinds: ["JS_TURBOPROP"], 223 color: "#693eb8", 224 backgroundColor: "#a6c452", 225 text: "JS Turboprop" 226 }, 227 { 228 kinds: ["JS_BASELINE"], 229 color: "#b3005b", 230 backgroundColor: "#ff9e80", 231 text: "JS Baseline" 232 }, 233 { 234 kinds: ["JS_UNOPT", "BC"], 235 color: "#dd2c00", 236 backgroundColor: "#ff9e80", 237 text: "JS Unoptimized" 238 }, 239 { 240 kinds: ["IC"], 241 color: "#ff6d00", 242 backgroundColor: "#ffab40", 243 text: "IC" 244 }, 245 { 246 kinds: ["STUB", "BUILTIN", "REGEXP"], 247 color: "#ffd600", 248 backgroundColor: "#ffea00", 249 text: "Other generated" 250 }, 251 { 252 kinds: ["CPP", "LIB"], 253 color: "#304ffe", 254 backgroundColor: "#6ab7ff", 255 text: "C++" 256 }, 257 { 258 kinds: ["CPP_EXT"], 259 color: "#003c8f", 260 backgroundColor: "#c0cfff", 261 text: "C++/external" 262 }, 263 { 264 kinds: ["CPP_PARSE"], 265 color: "#aa00ff", 266 backgroundColor: "#ffb2ff", 267 text: "C++/Parser" 268 }, 269 { 270 kinds: ["CPP_COMP_BC"], 271 color: "#43a047", 272 backgroundColor: "#88c399", 273 text: "C++/Bytecode compiler" 274 }, 275 { 276 kinds: ["CPP_COMP_BASELINE"], 277 color: "#43a047", 278 backgroundColor: "#5a8000", 279 text: "C++/Baseline compiler" 280 }, 281 { 282 kinds: ["CPP_COMP"], 283 color: "#00e5ff", 284 backgroundColor: "#6effff", 285 text: "C++/Compiler" 286 }, 287 { 288 kinds: ["CPP_GC"], 289 color: "#6200ea", 290 backgroundColor: "#e1bee7", 291 text: "C++/GC" 292 }, 293 { 294 kinds: ["UNKNOWN"], 295 color: "#bdbdbd", 296 backgroundColor: "#efefef", 297 text: "Unknown" 298 } 299 ]; 300 301let kindToBucketDescriptor = {}; 302for (let i = 0; i < bucketDescriptors.length; i++) { 303 let bucket = bucketDescriptors[i]; 304 for (let j = 0; j < bucket.kinds.length; j++) { 305 kindToBucketDescriptor[bucket.kinds[j]] = bucket; 306 } 307} 308 309function bucketFromKind(kind) { 310 for (let i = 0; i < bucketDescriptors.length; i++) { 311 let bucket = bucketDescriptors[i]; 312 for (let j = 0; j < bucket.kinds.length; j++) { 313 if (bucket.kinds[j] === kind) { 314 return bucket; 315 } 316 } 317 } 318 return null; 319} 320 321function codeTypeToText(type) { 322 switch (type) { 323 case "UNKNOWN": 324 return "Unknown"; 325 case "CPP_PARSE": 326 return "C++ Parser"; 327 case "CPP_COMP_BASELINE": 328 return "C++ Baseline Compiler"; 329 case "CPP_COMP_BC": 330 return "C++ Bytecode Compiler"; 331 case "CPP_COMP": 332 return "C++ Compiler"; 333 case "CPP_GC": 334 return "C++ GC"; 335 case "CPP_EXT": 336 return "C++ External"; 337 case "CPP": 338 return "C++"; 339 case "LIB": 340 return "Library"; 341 case "IC": 342 return "IC"; 343 case "BC": 344 return "Bytecode"; 345 case "STUB": 346 return "Stub"; 347 case "BUILTIN": 348 return "Builtin"; 349 case "REGEXP": 350 return "RegExp"; 351 case "JS_OPT": 352 return "JS opt"; 353 case "JS_TURBOPROP": 354 return "JS Turboprop"; 355 case "JS_BASELINE": 356 return "JS Baseline"; 357 case "JS_UNOPT": 358 return "JS unopt"; 359 } 360 console.error("Unknown type: " + type); 361} 362 363function createTypeNode(type) { 364 if (type === "CAT") { 365 return document.createTextNode(""); 366 } 367 let span = document.createElement("span"); 368 span.classList.add("code-type-chip"); 369 span.textContent = codeTypeToText(type); 370 371 return span; 372} 373 374function filterFromFilterId(id) { 375 switch (id) { 376 case "full-tree": 377 return (type, kind) => true; 378 case "js-funs": 379 return (type, kind) => type !== 'CODE'; 380 case "js-exclude-bc": 381 return (type, kind) => 382 type !== 'CODE' || kind !== "BytecodeHandler"; 383 } 384} 385 386function createIndentNode(indent) { 387 let div = document.createElement("div"); 388 div.style.display = "inline-block"; 389 div.style.width = (indent + 0.5) + "em"; 390 return div; 391} 392 393function createArrowNode() { 394 let span = document.createElement("span"); 395 span.classList.add("tree-row-arrow"); 396 return span; 397} 398 399function createFunctionNode(name, codeId) { 400 let nameElement = document.createElement("span"); 401 nameElement.appendChild(document.createTextNode(name)); 402 nameElement.classList.add("tree-row-name"); 403 if (codeId !== -1) { 404 nameElement.classList.add("codeid-link"); 405 nameElement.onclick = (event) => { 406 main.setCurrentCode(codeId); 407 // Prevent the click from bubbling to the row and causing it to 408 // collapse/expand. 409 event.stopPropagation(); 410 }; 411 } 412 return nameElement; 413} 414 415function createViewSourceNode(codeId) { 416 let linkElement = document.createElement("span"); 417 linkElement.appendChild(document.createTextNode("View source")); 418 linkElement.classList.add("view-source-link"); 419 linkElement.onclick = (event) => { 420 main.setCurrentCode(codeId); 421 main.setViewingSource(true); 422 // Prevent the click from bubbling to the row and causing it to 423 // collapse/expand. 424 event.stopPropagation(); 425 }; 426 return linkElement; 427} 428 429const COLLAPSED_ARROW = "\u25B6"; 430const EXPANDED_ARROW = "\u25BC"; 431 432class CallTreeView { 433 constructor() { 434 this.element = $("calltree"); 435 this.treeElement = $("calltree-table"); 436 this.selectAttribution = $("calltree-attribution"); 437 this.selectCategories = $("calltree-categories"); 438 this.selectSort = $("calltree-sort"); 439 440 this.selectAttribution.onchange = () => { 441 main.setCallTreeAttribution(this.selectAttribution.value); 442 }; 443 444 this.selectCategories.onchange = () => { 445 main.setCallTreeCategories(this.selectCategories.value); 446 }; 447 448 this.selectSort.onchange = () => { 449 main.setCallTreeSort(this.selectSort.value); 450 }; 451 452 this.currentState = null; 453 } 454 455 sortFromId(id) { 456 switch (id) { 457 case "time": 458 return (c1, c2) => { 459 if (c1.ticks < c2.ticks) return 1; 460 else if (c1.ticks > c2.ticks) return -1; 461 return c2.ownTicks - c1.ownTicks; 462 }; 463 case "own-time": 464 return (c1, c2) => { 465 if (c1.ownTicks < c2.ownTicks) return 1; 466 else if (c1.ownTicks > c2.ownTicks) return -1; 467 return c2.ticks - c1.ticks; 468 }; 469 case "category-time": 470 return (c1, c2) => { 471 if (c1.type === c2.type) return c2.ticks - c1.ticks; 472 if (c1.type < c2.type) return 1; 473 return -1; 474 }; 475 case "category-own-time": 476 return (c1, c2) => { 477 if (c1.type === c2.type) return c2.ownTicks - c1.ownTicks; 478 if (c1.type < c2.type) return 1; 479 return -1; 480 }; 481 } 482 } 483 484 expandTree(tree, indent) { 485 let index = 0; 486 let id = "R/"; 487 let row = tree.row; 488 489 if (row) { 490 index = row.rowIndex; 491 id = row.id; 492 493 tree.arrow.textContent = EXPANDED_ARROW; 494 // Collapse the children when the row is clicked again. 495 let expandHandler = row.onclick; 496 row.onclick = () => { 497 this.collapseRow(tree, expandHandler); 498 } 499 } 500 501 // Collect the children, and sort them by ticks. 502 let children = []; 503 let filter = 504 filterFromFilterId(this.currentState.callTree.attribution); 505 for (let childId in tree.children) { 506 let child = tree.children[childId]; 507 if (child.ticks > 0) { 508 children.push(child); 509 if (child.delayedExpansion) { 510 expandTreeNode(this.currentState.file, child, filter); 511 } 512 } 513 } 514 children.sort(this.sortFromId(this.currentState.callTree.sort)); 515 516 for (let i = 0; i < children.length; i++) { 517 let node = children[i]; 518 let row = this.rows.insertRow(index); 519 row.id = id + i + "/"; 520 521 if (node.type === "CAT") { 522 row.style.backgroundColor = CATEGORY_COLOR; 523 } else { 524 row.style.backgroundColor = bucketFromKind(node.type).backgroundColor; 525 } 526 527 // Inclusive time % cell. 528 let c = row.insertCell(); 529 c.textContent = (node.ticks * 100 / this.tickCount).toFixed(2) + "%"; 530 c.style.textAlign = "right"; 531 // Percent-of-parent cell. 532 c = row.insertCell(); 533 c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%"; 534 c.style.textAlign = "right"; 535 // Exclusive time % cell. 536 if (this.currentState.mode !== "bottom-up") { 537 c = row.insertCell(-1); 538 c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%"; 539 c.style.textAlign = "right"; 540 } 541 542 // Create the name cell. 543 let nameCell = row.insertCell(); 544 nameCell.appendChild(createIndentNode(indent + 1)); 545 let arrow = createArrowNode(); 546 nameCell.appendChild(arrow); 547 nameCell.appendChild(createTypeNode(node.type)); 548 nameCell.appendChild(createFunctionNode(node.name, node.codeId)); 549 if (main.currentState.sourceData && 550 node.codeId >= 0 && 551 main.currentState.sourceData.hasSource( 552 this.currentState.file.code[node.codeId].func)) { 553 nameCell.appendChild(createViewSourceNode(node.codeId)); 554 } 555 556 // Inclusive ticks cell. 557 c = row.insertCell(); 558 c.textContent = node.ticks; 559 c.style.textAlign = "right"; 560 if (this.currentState.mode !== "bottom-up") { 561 // Exclusive ticks cell. 562 c = row.insertCell(-1); 563 c.textContent = node.ownTicks; 564 c.style.textAlign = "right"; 565 } 566 if (node.children.length > 0) { 567 arrow.textContent = COLLAPSED_ARROW; 568 row.onclick = () => { this.expandTree(node, indent + 1); }; 569 } 570 571 node.row = row; 572 node.arrow = arrow; 573 574 index++; 575 } 576 } 577 578 collapseRow(tree, expandHandler) { 579 let row = tree.row; 580 let id = row.id; 581 let index = row.rowIndex; 582 while (row.rowIndex < this.rows.rows.length && 583 this.rows.rows[index].id.startsWith(id)) { 584 this.rows.deleteRow(index); 585 } 586 587 tree.arrow.textContent = COLLAPSED_ARROW; 588 row.onclick = expandHandler; 589 } 590 591 fillSelects(mode, calltree) { 592 function addOptions(e, values, current) { 593 while (e.options.length > 0) { 594 e.remove(0); 595 } 596 for (let i = 0; i < values.length; i++) { 597 let option = document.createElement("option"); 598 option.value = values[i].value; 599 option.textContent = values[i].text; 600 e.appendChild(option); 601 } 602 e.value = current; 603 } 604 605 let attributions = [ 606 { value : "js-exclude-bc", 607 text : "Attribute bytecode handlers to caller" }, 608 { value : "full-tree", 609 text : "Count each code object separately" }, 610 { value : "js-funs", 611 text : "Attribute non-functions to JS functions" } 612 ]; 613 614 switch (mode) { 615 case "bottom-up": 616 addOptions(this.selectAttribution, attributions, calltree.attribution); 617 addOptions(this.selectCategories, [ 618 { value : "code-type", text : "Code type" }, 619 { value : "none", text : "None" } 620 ], calltree.categories); 621 addOptions(this.selectSort, [ 622 { value : "time", text : "Time (including children)" }, 623 { value : "category-time", text : "Code category, time" }, 624 ], calltree.sort); 625 return; 626 case "top-down": 627 addOptions(this.selectAttribution, attributions, calltree.attribution); 628 addOptions(this.selectCategories, [ 629 { value : "none", text : "None" }, 630 { value : "rt-entry", text : "Runtime entries" } 631 ], calltree.categories); 632 addOptions(this.selectSort, [ 633 { value : "time", text : "Time (including children)" }, 634 { value : "own-time", text : "Own time" }, 635 { value : "category-time", text : "Code category, time" }, 636 { value : "category-own-time", text : "Code category, own time"} 637 ], calltree.sort); 638 return; 639 case "function-list": 640 addOptions(this.selectAttribution, attributions, calltree.attribution); 641 addOptions(this.selectCategories, [ 642 { value : "code-type", text : "Code type" }, 643 { value : "none", text : "None" } 644 ], calltree.categories); 645 addOptions(this.selectSort, [ 646 { value : "own-time", text : "Own time" }, 647 { value : "time", text : "Time (including children)" }, 648 { value : "category-own-time", text : "Code category, own time"}, 649 { value : "category-time", text : "Code category, time" }, 650 ], calltree.sort); 651 return; 652 } 653 console.error("Unexpected mode"); 654 } 655 656 static isCallTreeMode(mode) { 657 switch (mode) { 658 case "bottom-up": 659 case "top-down": 660 case "function-list": 661 return true; 662 default: 663 return false; 664 } 665 } 666 667 render(newState) { 668 let oldState = this.currentState; 669 if (!newState.file || !CallTreeView.isCallTreeMode(newState.mode)) { 670 this.element.style.display = "none"; 671 this.currentState = null; 672 return; 673 } 674 675 this.currentState = newState; 676 if (oldState) { 677 if (newState.file === oldState.file && 678 newState.start === oldState.start && 679 newState.end === oldState.end && 680 newState.mode === oldState.mode && 681 newState.callTree.attribution === oldState.callTree.attribution && 682 newState.callTree.categories === oldState.callTree.categories && 683 newState.callTree.sort === oldState.callTree.sort) { 684 // No change => just return. 685 return; 686 } 687 } 688 689 this.element.style.display = "inherit"; 690 691 let mode = this.currentState.mode; 692 if (!oldState || mode !== oldState.mode) { 693 // Technically, we should also call this if attribution, categories or 694 // sort change, but the selection is already highlighted by the combobox 695 // itself, so we do need to do anything here. 696 this.fillSelects(newState.mode, newState.callTree); 697 } 698 699 let ownTimeClass = (mode === "bottom-up") ? "numeric-hidden" : "numeric"; 700 let ownTimeTh = $(this.treeElement.id + "-own-time-header"); 701 ownTimeTh.classList = ownTimeClass; 702 let ownTicksTh = $(this.treeElement.id + "-own-ticks-header"); 703 ownTicksTh.classList = ownTimeClass; 704 705 // Build the tree. 706 let stackProcessor; 707 let filter = filterFromFilterId(this.currentState.callTree.attribution); 708 if (mode === "top-down") { 709 if (this.currentState.callTree.categories === "rt-entry") { 710 stackProcessor = 711 new RuntimeCallTreeProcessor(); 712 } else { 713 stackProcessor = 714 new PlainCallTreeProcessor(filter, false); 715 } 716 } else if (mode === "function-list") { 717 stackProcessor = new FunctionListTree( 718 filter, this.currentState.callTree.categories === "code-type"); 719 720 } else { 721 console.assert(mode === "bottom-up"); 722 if (this.currentState.callTree.categories === "none") { 723 stackProcessor = 724 new PlainCallTreeProcessor(filter, true); 725 } else { 726 console.assert(this.currentState.callTree.categories === "code-type"); 727 stackProcessor = 728 new CategorizedCallTreeProcessor(filter, true); 729 } 730 } 731 this.tickCount = 732 generateTree(this.currentState.file, 733 this.currentState.start, 734 this.currentState.end, 735 stackProcessor); 736 // TODO(jarin) Handle the case when tick count is negative. 737 738 this.tree = stackProcessor.tree; 739 740 // Remove old content of the table, replace with new one. 741 let oldRows = this.treeElement.getElementsByTagName("tbody"); 742 let newRows = document.createElement("tbody"); 743 this.rows = newRows; 744 745 // Populate the table. 746 this.expandTree(this.tree, 0); 747 748 // Swap in the new rows. 749 this.treeElement.replaceChild(newRows, oldRows[0]); 750 } 751} 752 753class TimelineView { 754 constructor() { 755 this.element = $("timeline"); 756 this.canvas = $("timeline-canvas"); 757 this.legend = $("timeline-legend"); 758 this.currentCode = $("timeline-currentCode"); 759 760 this.canvas.onmousedown = this.onMouseDown.bind(this); 761 this.canvas.onmouseup = this.onMouseUp.bind(this); 762 this.canvas.onmousemove = this.onMouseMove.bind(this); 763 764 this.selectionStart = null; 765 this.selectionEnd = null; 766 this.selecting = false; 767 768 this.fontSize = 12; 769 this.imageOffset = Math.round(this.fontSize * 1.2); 770 this.functionTimelineHeight = 24; 771 this.functionTimelineTickHeight = 16; 772 773 this.currentState = null; 774 } 775 776 onMouseDown(e) { 777 this.selectionStart = 778 e.clientX - this.canvas.getBoundingClientRect().left; 779 this.selectionEnd = this.selectionStart + 1; 780 this.selecting = true; 781 } 782 783 onMouseMove(e) { 784 if (this.selecting) { 785 this.selectionEnd = 786 e.clientX - this.canvas.getBoundingClientRect().left; 787 this.drawSelection(); 788 } 789 } 790 791 onMouseUp(e) { 792 if (this.selectionStart !== null) { 793 let x = e.clientX - this.canvas.getBoundingClientRect().left; 794 if (Math.abs(x - this.selectionStart) < 10) { 795 this.selectionStart = null; 796 this.selectionEnd = null; 797 let ctx = this.canvas.getContext("2d"); 798 ctx.drawImage(this.buffer, 0, this.imageOffset); 799 } else { 800 this.selectionEnd = x; 801 this.drawSelection(); 802 } 803 let file = this.currentState.file; 804 if (file) { 805 let start = this.selectionStart === null ? 0 : this.selectionStart; 806 let end = this.selectionEnd === null ? Infinity : this.selectionEnd; 807 let firstTime = file.ticks[0].tm; 808 let lastTime = file.ticks[file.ticks.length - 1].tm; 809 810 let width = this.buffer.width; 811 812 start = (start / width) * (lastTime - firstTime) + firstTime; 813 end = (end / width) * (lastTime - firstTime) + firstTime; 814 815 if (end < start) { 816 let temp = start; 817 start = end; 818 end = temp; 819 } 820 821 main.setViewInterval(start, end); 822 } 823 } 824 this.selecting = false; 825 } 826 827 drawSelection() { 828 let ctx = this.canvas.getContext("2d"); 829 830 // Draw the timeline image. 831 ctx.drawImage(this.buffer, 0, this.imageOffset); 832 833 // Draw the current interval highlight. 834 let left; 835 let right; 836 if (this.selectionStart !== null && this.selectionEnd !== null) { 837 ctx.fillStyle = "rgba(0, 0, 0, 0.3)"; 838 left = Math.min(this.selectionStart, this.selectionEnd); 839 right = Math.max(this.selectionStart, this.selectionEnd); 840 let height = this.buffer.height - this.functionTimelineHeight; 841 ctx.fillRect(0, this.imageOffset, left, height); 842 ctx.fillRect(right, this.imageOffset, this.buffer.width - right, height); 843 } else { 844 left = 0; 845 right = this.buffer.width; 846 } 847 848 // Draw the scale text. 849 let file = this.currentState.file; 850 ctx.fillStyle = "white"; 851 ctx.fillRect(0, 0, this.canvas.width, this.imageOffset); 852 if (file && file.ticks.length > 0) { 853 let firstTime = file.ticks[0].tm; 854 let lastTime = file.ticks[file.ticks.length - 1].tm; 855 856 let leftTime = 857 firstTime + left / this.canvas.width * (lastTime - firstTime); 858 let rightTime = 859 firstTime + right / this.canvas.width * (lastTime - firstTime); 860 861 let leftText = (leftTime / 1000000).toFixed(3) + "s"; 862 let rightText = (rightTime / 1000000).toFixed(3) + "s"; 863 864 ctx.textBaseline = 'top'; 865 ctx.font = this.fontSize + "px Arial"; 866 ctx.fillStyle = "black"; 867 868 let leftWidth = ctx.measureText(leftText).width; 869 let rightWidth = ctx.measureText(rightText).width; 870 871 let leftStart = left - leftWidth / 2; 872 let rightStart = right - rightWidth / 2; 873 874 if (leftStart < 0) leftStart = 0; 875 if (rightStart + rightWidth > this.canvas.width) { 876 rightStart = this.canvas.width - rightWidth; 877 } 878 if (leftStart + leftWidth > rightStart) { 879 if (leftStart > this.canvas.width - (rightStart - rightWidth)) { 880 rightStart = leftStart + leftWidth; 881 882 } else { 883 leftStart = rightStart - leftWidth; 884 } 885 } 886 887 ctx.fillText(leftText, leftStart, 0); 888 ctx.fillText(rightText, rightStart, 0); 889 } 890 } 891 892 render(newState) { 893 let oldState = this.currentState; 894 895 if (!newState.file) { 896 this.element.style.display = "none"; 897 return; 898 } 899 900 let width = Math.round(document.documentElement.clientWidth - 20); 901 let height = Math.round(document.documentElement.clientHeight / 5); 902 903 if (oldState) { 904 if (width === oldState.timelineSize.width && 905 height === oldState.timelineSize.height && 906 newState.file === oldState.file && 907 newState.currentCodeId === oldState.currentCodeId && 908 newState.start === oldState.start && 909 newState.end === oldState.end) { 910 // No change, nothing to do. 911 return; 912 } 913 } 914 this.currentState = newState; 915 this.currentState.timelineSize.width = width; 916 this.currentState.timelineSize.height = height; 917 918 this.element.style.display = "inherit"; 919 920 let file = this.currentState.file; 921 922 const minPixelsPerBucket = 10; 923 const minTicksPerBucket = 8; 924 let maxBuckets = Math.round(file.ticks.length / minTicksPerBucket); 925 let bucketCount = Math.min( 926 Math.round(width / minPixelsPerBucket), maxBuckets); 927 928 // Make sure the canvas has the right dimensions. 929 this.canvas.width = width; 930 this.canvas.height = height; 931 932 // Make space for the selection text. 933 height -= this.imageOffset; 934 935 let currentCodeId = this.currentState.currentCodeId; 936 937 let firstTime = file.ticks[0].tm; 938 let lastTime = file.ticks[file.ticks.length - 1].tm; 939 let start = Math.max(this.currentState.start, firstTime); 940 let end = Math.min(this.currentState.end, lastTime); 941 942 this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width; 943 this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width; 944 945 let stackProcessor = new CategorySampler(file, bucketCount); 946 generateTree(file, 0, Infinity, stackProcessor); 947 let codeIdProcessor = new FunctionTimelineProcessor( 948 currentCodeId, 949 filterFromFilterId(this.currentState.callTree.attribution)); 950 generateTree(file, 0, Infinity, codeIdProcessor); 951 952 let buffer = document.createElement("canvas"); 953 954 buffer.width = width; 955 buffer.height = height; 956 957 // Calculate the bar heights for each bucket. 958 let graphHeight = height - this.functionTimelineHeight; 959 let buckets = stackProcessor.buckets; 960 let bucketsGraph = []; 961 for (let i = 0; i < buckets.length; i++) { 962 let sum = 0; 963 let bucketData = []; 964 let total = buckets[i].total; 965 if (total > 0) { 966 for (let j = 0; j < bucketDescriptors.length; j++) { 967 let desc = bucketDescriptors[j]; 968 for (let k = 0; k < desc.kinds.length; k++) { 969 sum += buckets[i][desc.kinds[k]]; 970 } 971 bucketData.push(Math.round(graphHeight * sum / total)); 972 } 973 } else { 974 // No ticks fell into this bucket. Fill with "Unknown." 975 for (let j = 0; j < bucketDescriptors.length; j++) { 976 let desc = bucketDescriptors[j]; 977 bucketData.push(desc.text === "Unknown" ? graphHeight : 0); 978 } 979 } 980 bucketsGraph.push(bucketData); 981 } 982 983 // Draw the category graph into the buffer. 984 let bucketWidth = width / (bucketsGraph.length - 1); 985 let ctx = buffer.getContext('2d'); 986 for (let i = 0; i < bucketsGraph.length - 1; i++) { 987 let bucketData = bucketsGraph[i]; 988 let nextBucketData = bucketsGraph[i + 1]; 989 let x1 = Math.round(i * bucketWidth); 990 let x2 = Math.round((i + 1) * bucketWidth); 991 for (let j = 0; j < bucketData.length; j++) { 992 ctx.beginPath(); 993 ctx.moveTo(x1, j > 0 ? bucketData[j - 1] : 0); 994 ctx.lineTo(x2, j > 0 ? nextBucketData[j - 1] : 0); 995 ctx.lineTo(x2, nextBucketData[j]); 996 ctx.lineTo(x1, bucketData[j]); 997 ctx.closePath(); 998 ctx.fillStyle = bucketDescriptors[j].color; 999 ctx.fill(); 1000 } 1001 } 1002 1003 // Draw the function ticks. 1004 let functionTimelineYOffset = graphHeight; 1005 let functionTimelineTickHeight = this.functionTimelineTickHeight; 1006 let functionTimelineHalfHeight = 1007 Math.round(functionTimelineTickHeight / 2); 1008 let timestampScaler = width / (lastTime - firstTime); 1009 let timestampToX = (t) => Math.round((t - firstTime) * timestampScaler); 1010 ctx.fillStyle = "white"; 1011 ctx.fillRect( 1012 0, 1013 functionTimelineYOffset, 1014 buffer.width, 1015 this.functionTimelineHeight); 1016 for (let i = 0; i < codeIdProcessor.blocks.length; i++) { 1017 let block = codeIdProcessor.blocks[i]; 1018 let bucket = kindToBucketDescriptor[block.kind]; 1019 ctx.fillStyle = bucket.color; 1020 ctx.fillRect( 1021 timestampToX(block.start), 1022 functionTimelineYOffset, 1023 Math.max(1, Math.round((block.end - block.start) * timestampScaler)), 1024 block.topOfStack ? 1025 functionTimelineTickHeight : functionTimelineHalfHeight); 1026 } 1027 ctx.strokeStyle = "black"; 1028 ctx.lineWidth = "1"; 1029 ctx.beginPath(); 1030 ctx.moveTo(0, functionTimelineYOffset + 0.5); 1031 ctx.lineTo(buffer.width, functionTimelineYOffset + 0.5); 1032 ctx.stroke(); 1033 ctx.strokeStyle = "rgba(0,0,0,0.2)"; 1034 ctx.lineWidth = "1"; 1035 ctx.beginPath(); 1036 ctx.moveTo(0, functionTimelineYOffset + functionTimelineHalfHeight - 0.5); 1037 ctx.lineTo(buffer.width, 1038 functionTimelineYOffset + functionTimelineHalfHeight - 0.5); 1039 ctx.stroke(); 1040 1041 // Draw marks for optimizations and deoptimizations in the function 1042 // timeline. 1043 if (currentCodeId && currentCodeId >= 0 && 1044 file.code[currentCodeId].func) { 1045 let y = Math.round(functionTimelineYOffset + functionTimelineTickHeight + 1046 (this.functionTimelineHeight - functionTimelineTickHeight) / 2); 1047 let func = file.functions[file.code[currentCodeId].func]; 1048 for (let i = 0; i < func.codes.length; i++) { 1049 let code = file.code[func.codes[i]]; 1050 if (code.kind === "Opt") { 1051 if (code.deopt) { 1052 // Draw deoptimization mark. 1053 let x = timestampToX(code.deopt.tm); 1054 ctx.lineWidth = 0.7; 1055 ctx.strokeStyle = "red"; 1056 ctx.beginPath(); 1057 ctx.moveTo(x - 3, y - 3); 1058 ctx.lineTo(x + 3, y + 3); 1059 ctx.stroke(); 1060 ctx.beginPath(); 1061 ctx.moveTo(x - 3, y + 3); 1062 ctx.lineTo(x + 3, y - 3); 1063 ctx.stroke(); 1064 } 1065 // Draw optimization mark. 1066 let x = timestampToX(code.tm); 1067 ctx.lineWidth = 0.7; 1068 ctx.strokeStyle = "blue"; 1069 ctx.beginPath(); 1070 ctx.moveTo(x - 3, y - 3); 1071 ctx.lineTo(x, y); 1072 ctx.stroke(); 1073 ctx.beginPath(); 1074 ctx.moveTo(x - 3, y + 3); 1075 ctx.lineTo(x, y); 1076 ctx.stroke(); 1077 } else { 1078 // Draw code creation mark. 1079 let x = Math.round(timestampToX(code.tm)); 1080 ctx.beginPath(); 1081 ctx.fillStyle = "black"; 1082 ctx.arc(x, y, 3, 0, 2 * Math.PI); 1083 ctx.fill(); 1084 } 1085 } 1086 } 1087 1088 // Remember stuff for later. 1089 this.buffer = buffer; 1090 1091 // Draw the buffer. 1092 this.drawSelection(); 1093 1094 // (Re-)Populate the graph legend. 1095 while (this.legend.cells.length > 0) { 1096 this.legend.deleteCell(0); 1097 } 1098 let cell = this.legend.insertCell(-1); 1099 cell.textContent = "Legend: "; 1100 cell.style.padding = "1ex"; 1101 for (let i = 0; i < bucketDescriptors.length; i++) { 1102 let cell = this.legend.insertCell(-1); 1103 cell.style.padding = "1ex"; 1104 let desc = bucketDescriptors[i]; 1105 let div = document.createElement("div"); 1106 div.style.display = "inline-block"; 1107 div.style.width = "0.6em"; 1108 div.style.height = "1.2ex"; 1109 div.style.backgroundColor = desc.color; 1110 div.style.borderStyle = "solid"; 1111 div.style.borderWidth = "1px"; 1112 div.style.borderColor = "Black"; 1113 cell.appendChild(div); 1114 cell.appendChild(document.createTextNode(" " + desc.text)); 1115 } 1116 1117 removeAllChildren(this.currentCode); 1118 if (currentCodeId) { 1119 let currentCode = file.code[currentCodeId]; 1120 this.currentCode.appendChild(document.createTextNode(currentCode.name)); 1121 } else { 1122 this.currentCode.appendChild(document.createTextNode("<none>")); 1123 } 1124 } 1125} 1126 1127class ModeBarView { 1128 constructor() { 1129 let modeBar = this.element = $("mode-bar"); 1130 1131 function addMode(id, text, active) { 1132 let div = document.createElement("div"); 1133 div.classList = "mode-button" + (active ? " active-mode-button" : ""); 1134 div.id = "mode-" + id; 1135 div.textContent = text; 1136 div.onclick = () => { 1137 if (main.currentState.mode === id) return; 1138 let old = $("mode-" + main.currentState.mode); 1139 old.classList = "mode-button"; 1140 div.classList = "mode-button active-mode-button"; 1141 main.setMode(id); 1142 }; 1143 modeBar.appendChild(div); 1144 } 1145 1146 addMode("summary", "Summary", true); 1147 addMode("bottom-up", "Bottom up"); 1148 addMode("top-down", "Top down"); 1149 addMode("function-list", "Functions"); 1150 } 1151 1152 render(newState) { 1153 if (!newState.file) { 1154 this.element.style.display = "none"; 1155 return; 1156 } 1157 1158 this.element.style.display = "inherit"; 1159 } 1160} 1161 1162class SummaryView { 1163 constructor() { 1164 this.element = $("summary"); 1165 this.currentState = null; 1166 } 1167 1168 render(newState) { 1169 let oldState = this.currentState; 1170 1171 if (!newState.file || newState.mode !== "summary") { 1172 this.element.style.display = "none"; 1173 this.currentState = null; 1174 return; 1175 } 1176 1177 this.currentState = newState; 1178 if (oldState) { 1179 if (newState.file === oldState.file && 1180 newState.start === oldState.start && 1181 newState.end === oldState.end) { 1182 // No change, nothing to do. 1183 return; 1184 } 1185 } 1186 1187 this.element.style.display = "inherit"; 1188 removeAllChildren(this.element); 1189 1190 let stats = computeOptimizationStats( 1191 this.currentState.file, newState.start, newState.end); 1192 1193 let table = document.createElement("table"); 1194 let rows = document.createElement("tbody"); 1195 1196 function addRow(text, number, indent) { 1197 let row = rows.insertRow(-1); 1198 let textCell = row.insertCell(-1); 1199 textCell.textContent = text; 1200 let numberCell = row.insertCell(-1); 1201 numberCell.textContent = number; 1202 if (indent) { 1203 textCell.style.textIndent = indent + "em"; 1204 numberCell.style.textIndent = indent + "em"; 1205 } 1206 return row; 1207 } 1208 1209 function makeCollapsible(row, arrow) { 1210 arrow.textContent = EXPANDED_ARROW; 1211 let expandHandler = row.onclick; 1212 row.onclick = () => { 1213 let id = row.id; 1214 let index = row.rowIndex + 1; 1215 while (index < rows.rows.length && 1216 rows.rows[index].id.startsWith(id)) { 1217 rows.deleteRow(index); 1218 } 1219 arrow.textContent = COLLAPSED_ARROW; 1220 row.onclick = expandHandler; 1221 } 1222 } 1223 1224 function expandDeoptInstances(row, arrow, instances, indent, kind) { 1225 let index = row.rowIndex; 1226 for (let i = 0; i < instances.length; i++) { 1227 let childRow = rows.insertRow(index + 1); 1228 childRow.id = row.id + i + "/"; 1229 1230 let deopt = instances[i].deopt; 1231 1232 let textCell = childRow.insertCell(-1); 1233 textCell.appendChild(document.createTextNode(deopt.posText)); 1234 textCell.style.textIndent = indent + "em"; 1235 let reasonCell = childRow.insertCell(-1); 1236 reasonCell.appendChild( 1237 document.createTextNode("Reason: " + deopt.reason)); 1238 reasonCell.style.textIndent = indent + "em"; 1239 } 1240 makeCollapsible(row, arrow); 1241 } 1242 1243 function expandDeoptFunctionList(row, arrow, list, indent, kind) { 1244 let index = row.rowIndex; 1245 for (let i = 0; i < list.length; i++) { 1246 let childRow = rows.insertRow(index + 1); 1247 childRow.id = row.id + i + "/"; 1248 1249 let textCell = childRow.insertCell(-1); 1250 textCell.appendChild(createIndentNode(indent)); 1251 let childArrow = createArrowNode(); 1252 textCell.appendChild(childArrow); 1253 textCell.appendChild( 1254 createFunctionNode(list[i].f.name, list[i].f.codes[0])); 1255 1256 let numberCell = childRow.insertCell(-1); 1257 numberCell.textContent = list[i].instances.length; 1258 numberCell.style.textIndent = indent + "em"; 1259 1260 childArrow.textContent = COLLAPSED_ARROW; 1261 childRow.onclick = () => { 1262 expandDeoptInstances( 1263 childRow, childArrow, list[i].instances, indent + 1); 1264 }; 1265 } 1266 makeCollapsible(row, arrow); 1267 } 1268 1269 function expandOptimizedFunctionList(row, arrow, list, indent, kind) { 1270 let index = row.rowIndex; 1271 for (let i = 0; i < list.length; i++) { 1272 let childRow = rows.insertRow(index + 1); 1273 childRow.id = row.id + i + "/"; 1274 1275 let textCell = childRow.insertCell(-1); 1276 textCell.appendChild( 1277 createFunctionNode(list[i].f.name, list[i].f.codes[0])); 1278 textCell.style.textIndent = indent + "em"; 1279 1280 let numberCell = childRow.insertCell(-1); 1281 numberCell.textContent = list[i].instances.length; 1282 numberCell.style.textIndent = indent + "em"; 1283 } 1284 makeCollapsible(row, arrow); 1285 } 1286 1287 function addExpandableRow(text, list, indent, kind) { 1288 let row = rows.insertRow(-1); 1289 1290 row.id = "opt-table/" + kind + "/"; 1291 row.style.backgroundColor = CATEGORY_COLOR; 1292 1293 let textCell = row.insertCell(-1); 1294 textCell.appendChild(createIndentNode(indent)); 1295 let arrow = createArrowNode(); 1296 textCell.appendChild(arrow); 1297 textCell.appendChild(document.createTextNode(text)); 1298 1299 let numberCell = row.insertCell(-1); 1300 numberCell.textContent = list.count; 1301 if (indent) { 1302 numberCell.style.textIndent = indent + "em"; 1303 } 1304 1305 if (list.count > 0) { 1306 arrow.textContent = COLLAPSED_ARROW; 1307 if (kind === "opt") { 1308 row.onclick = () => { 1309 expandOptimizedFunctionList( 1310 row, arrow, list.functions, indent + 1, kind); 1311 }; 1312 } else { 1313 row.onclick = () => { 1314 expandDeoptFunctionList( 1315 row, arrow, list.functions, indent + 1, kind); 1316 }; 1317 } 1318 } 1319 return row; 1320 } 1321 1322 addRow("Total function count:", stats.functionCount); 1323 addRow("Optimized function count:", stats.optimizedFunctionCount, 1); 1324 if (stats.turbopropOptimizedFunctionCount != 0) { 1325 addRow("Turboprop optimized function count:", stats.turbopropOptimizedFunctionCount, 1); 1326 } 1327 addRow("Deoptimized function count:", stats.deoptimizedFunctionCount, 2); 1328 1329 addExpandableRow("Optimization count:", stats.optimizations, 0, "opt"); 1330 if (stats.turbopropOptimizedFunctionCount != 0) { 1331 addExpandableRow("Turboprop Optimization count:", stats.turbopropOptimizations, 0, "tp"); 1332 } 1333 let deoptCount = stats.eagerDeoptimizations.count + 1334 stats.softDeoptimizations.count + stats.lazyDeoptimizations.count; 1335 addRow("Deoptimization count:", deoptCount); 1336 addExpandableRow("Eager:", stats.eagerDeoptimizations, 1, "eager"); 1337 addExpandableRow("Lazy:", stats.lazyDeoptimizations, 1, "lazy"); 1338 addExpandableRow("Soft:", stats.softDeoptimizations, 1, "soft"); 1339 if (stats.softBailouts.count != 0) { 1340 addExpandableRow("SoftBailout:", stats.softBailouts, 1, "softbailout"); 1341 } 1342 if (stats.eagerBailouts.count != 0) { 1343 addExpandableRow("EagerBailout:", stats.eagerBailouts, 1, "eagerbailout"); 1344 } 1345 1346 table.appendChild(rows); 1347 this.element.appendChild(table); 1348 } 1349} 1350 1351class ScriptSourceView { 1352 constructor() { 1353 this.table = $("source-viewer"); 1354 this.hideButton = $("source-viewer-hide-button"); 1355 this.hideButton.onclick = () => { 1356 main.setViewingSource(false); 1357 }; 1358 } 1359 1360 render(newState) { 1361 let oldState = this.currentState; 1362 if (!newState.file || !newState.viewingSource) { 1363 this.table.style.display = "none"; 1364 this.hideButton.style.display = "none"; 1365 this.currentState = null; 1366 return; 1367 } 1368 if (oldState) { 1369 if (newState.file === oldState.file && 1370 newState.currentCodeId === oldState.currentCodeId && 1371 newState.viewingSource === oldState.viewingSource) { 1372 // No change, nothing to do. 1373 return; 1374 } 1375 } 1376 this.currentState = newState; 1377 1378 this.table.style.display = "inline-block"; 1379 this.hideButton.style.display = "inline"; 1380 removeAllChildren(this.table); 1381 1382 let functionId = 1383 this.currentState.file.code[this.currentState.currentCodeId].func; 1384 let sourceView = 1385 this.currentState.sourceData.generateSourceView(functionId); 1386 for (let i = 0; i < sourceView.source.length; i++) { 1387 let sampleCount = sourceView.lineSampleCounts[i] || 0; 1388 let sampleProportion = sourceView.samplesTotal > 0 ? 1389 sampleCount / sourceView.samplesTotal : 0; 1390 let heatBucket; 1391 if (sampleProportion === 0) { 1392 heatBucket = "line-none"; 1393 } else if (sampleProportion < 0.2) { 1394 heatBucket = "line-cold"; 1395 } else if (sampleProportion < 0.4) { 1396 heatBucket = "line-mediumcold"; 1397 } else if (sampleProportion < 0.6) { 1398 heatBucket = "line-mediumhot"; 1399 } else if (sampleProportion < 0.8) { 1400 heatBucket = "line-hot"; 1401 } else { 1402 heatBucket = "line-superhot"; 1403 } 1404 1405 let row = this.table.insertRow(-1); 1406 1407 let lineNumberCell = row.insertCell(-1); 1408 lineNumberCell.classList.add("source-line-number"); 1409 lineNumberCell.textContent = i + sourceView.firstLineNumber; 1410 1411 let sampleCountCell = row.insertCell(-1); 1412 sampleCountCell.classList.add(heatBucket); 1413 sampleCountCell.textContent = sampleCount; 1414 1415 let sourceLineCell = row.insertCell(-1); 1416 sourceLineCell.classList.add(heatBucket); 1417 sourceLineCell.textContent = sourceView.source[i]; 1418 } 1419 1420 $("timeline-currentCode").scrollIntoView(); 1421 } 1422} 1423 1424class SourceData { 1425 constructor(file) { 1426 this.scripts = new Map(); 1427 for (let i = 0; i < file.scripts.length; i++) { 1428 const scriptBlock = file.scripts[i]; 1429 if (scriptBlock === null) continue; // Array may be sparse. 1430 if (scriptBlock.source === undefined) continue; 1431 let source = scriptBlock.source.split("\n"); 1432 this.scripts.set(i, source); 1433 } 1434 1435 this.functions = new Map(); 1436 for (let codeId = 0; codeId < file.code.length; ++codeId) { 1437 let codeBlock = file.code[codeId]; 1438 if (codeBlock.source && codeBlock.func !== undefined) { 1439 let data = this.functions.get(codeBlock.func); 1440 if (!data) { 1441 data = new FunctionSourceData(codeBlock.source.script, 1442 codeBlock.source.start, 1443 codeBlock.source.end); 1444 this.functions.set(codeBlock.func, data); 1445 } 1446 data.addSourceBlock(codeId, codeBlock.source); 1447 } 1448 } 1449 1450 for (let tick of file.ticks) { 1451 let stack = tick.s; 1452 for (let i = 0; i < stack.length; i += 2) { 1453 let codeId = stack[i]; 1454 if (codeId < 0) continue; 1455 let functionId = file.code[codeId].func; 1456 if (this.functions.has(functionId)) { 1457 let codeOffset = stack[i + 1]; 1458 this.functions.get(functionId).addOffsetSample(codeId, codeOffset); 1459 } 1460 } 1461 } 1462 } 1463 1464 getScript(scriptId) { 1465 return this.scripts.get(scriptId); 1466 } 1467 1468 getLineForScriptOffset(script, scriptOffset) { 1469 let line = 0; 1470 let charsConsumed = 0; 1471 for (; line < script.length; ++line) { 1472 charsConsumed += script[line].length + 1; // Add 1 for newline. 1473 if (charsConsumed > scriptOffset) break; 1474 } 1475 return line; 1476 } 1477 1478 hasSource(functionId) { 1479 return this.functions.has(functionId); 1480 } 1481 1482 generateSourceView(functionId) { 1483 console.assert(this.hasSource(functionId)); 1484 let data = this.functions.get(functionId); 1485 let scriptId = data.scriptId; 1486 let script = this.getScript(scriptId); 1487 let firstLineNumber = 1488 this.getLineForScriptOffset(script, data.startScriptOffset); 1489 let lastLineNumber = 1490 this.getLineForScriptOffset(script, data.endScriptOffset); 1491 let lines = script.slice(firstLineNumber, lastLineNumber + 1); 1492 normalizeLeadingWhitespace(lines); 1493 1494 let samplesTotal = 0; 1495 let lineSampleCounts = []; 1496 for (let [codeId, block] of data.codes) { 1497 block.offsets.forEach((sampleCount, codeOffset) => { 1498 let sourceOffset = block.positionTable.getScriptOffset(codeOffset); 1499 let lineNumber = 1500 this.getLineForScriptOffset(script, sourceOffset) - firstLineNumber; 1501 samplesTotal += sampleCount; 1502 lineSampleCounts[lineNumber] = 1503 (lineSampleCounts[lineNumber] || 0) + sampleCount; 1504 }); 1505 } 1506 1507 return { 1508 source: lines, 1509 lineSampleCounts: lineSampleCounts, 1510 samplesTotal: samplesTotal, 1511 firstLineNumber: firstLineNumber + 1 // Source code is 1-indexed. 1512 }; 1513 } 1514} 1515 1516class FunctionSourceData { 1517 constructor(scriptId, startScriptOffset, endScriptOffset) { 1518 this.scriptId = scriptId; 1519 this.startScriptOffset = startScriptOffset; 1520 this.endScriptOffset = endScriptOffset; 1521 1522 this.codes = new Map(); 1523 } 1524 1525 addSourceBlock(codeId, source) { 1526 this.codes.set(codeId, { 1527 positionTable: new SourcePositionTable(source.positions), 1528 offsets: [] 1529 }); 1530 } 1531 1532 addOffsetSample(codeId, codeOffset) { 1533 let codeIdOffsets = this.codes.get(codeId).offsets; 1534 codeIdOffsets[codeOffset] = (codeIdOffsets[codeOffset] || 0) + 1; 1535 } 1536} 1537 1538class SourcePositionTable { 1539 constructor(encodedTable) { 1540 this.offsetTable = []; 1541 let offsetPairRegex = /C([0-9]+)O([0-9]+)/g; 1542 while (true) { 1543 let regexResult = offsetPairRegex.exec(encodedTable); 1544 if (!regexResult) break; 1545 let codeOffset = parseInt(regexResult[1]); 1546 let scriptOffset = parseInt(regexResult[2]); 1547 if (isNaN(codeOffset) || isNaN(scriptOffset)) continue; 1548 this.offsetTable.push(codeOffset, scriptOffset); 1549 } 1550 } 1551 1552 getScriptOffset(codeOffset) { 1553 console.assert(codeOffset >= 0); 1554 for (let i = this.offsetTable.length - 2; i >= 0; i -= 2) { 1555 if (this.offsetTable[i] <= codeOffset) { 1556 return this.offsetTable[i + 1]; 1557 } 1558 } 1559 return this.offsetTable[1]; 1560 } 1561} 1562 1563class HelpView { 1564 constructor() { 1565 this.element = $("help"); 1566 } 1567 1568 render(newState) { 1569 this.element.style.display = newState.file ? "none" : "inherit"; 1570 } 1571} 1572