| // Copyright 2017 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| "use strict" |
| |
| function $(id) { |
| return document.getElementById(id); |
| } |
| |
| let components = []; |
| |
| function createViews() { |
| components.push(new CallTreeView()); |
| components.push(new TimelineView()); |
| components.push(new HelpView()); |
| components.push(new SummaryView()); |
| components.push(new ModeBarView()); |
| |
| main.setMode("summary"); |
| } |
| |
| function emptyState() { |
| return { |
| file : null, |
| mode : "none", |
| currentCodeId : null, |
| start : 0, |
| end : Infinity, |
| timeLine : { |
| width : 100, |
| height : 100 |
| }, |
| callTree : { |
| attribution : "js-exclude-bc", |
| categories : "code-type", |
| sort : "time" |
| } |
| }; |
| } |
| |
| function setCallTreeState(state, callTreeState) { |
| state = Object.assign({}, state); |
| state.callTree = callTreeState; |
| return state; |
| } |
| |
| let main = { |
| currentState : emptyState(), |
| |
| setMode(mode) { |
| if (mode !== main.currentState.mode) { |
| |
| function setCallTreeModifiers(attribution, categories, sort) { |
| let callTreeState = Object.assign({}, main.currentState.callTree); |
| callTreeState.attribution = attribution; |
| callTreeState.categories = categories; |
| callTreeState.sort = sort; |
| return callTreeState; |
| } |
| |
| let state = Object.assign({}, main.currentState); |
| |
| switch (mode) { |
| case "bottom-up": |
| state.callTree = |
| setCallTreeModifiers("js-exclude-bc", "code-type", "time"); |
| break; |
| case "top-down": |
| state.callTree = |
| setCallTreeModifiers("js-exclude-bc", "none", "time"); |
| break; |
| case "function-list": |
| state.callTree = |
| setCallTreeModifiers("js-exclude-bc", "code-type", "own-time"); |
| break; |
| } |
| |
| state.mode = mode; |
| |
| main.currentState = state; |
| main.delayRender(); |
| } |
| }, |
| |
| setCallTreeAttribution(attribution) { |
| if (attribution !== main.currentState.attribution) { |
| let callTreeState = Object.assign({}, main.currentState.callTree); |
| callTreeState.attribution = attribution; |
| main.currentState = setCallTreeState(main.currentState, callTreeState); |
| main.delayRender(); |
| } |
| }, |
| |
| setCallTreeSort(sort) { |
| if (sort !== main.currentState.sort) { |
| let callTreeState = Object.assign({}, main.currentState.callTree); |
| callTreeState.sort = sort; |
| main.currentState = setCallTreeState(main.currentState, callTreeState); |
| main.delayRender(); |
| } |
| }, |
| |
| setCallTreeCategories(categories) { |
| if (categories !== main.currentState.categories) { |
| let callTreeState = Object.assign({}, main.currentState.callTree); |
| callTreeState.categories = categories; |
| main.currentState = setCallTreeState(main.currentState, callTreeState); |
| main.delayRender(); |
| } |
| }, |
| |
| setViewInterval(start, end) { |
| if (start !== main.currentState.start || |
| end !== main.currentState.end) { |
| main.currentState = Object.assign({}, main.currentState); |
| main.currentState.start = start; |
| main.currentState.end = end; |
| main.delayRender(); |
| } |
| }, |
| |
| setTimeLineDimensions(width, height) { |
| if (width !== main.currentState.timeLine.width || |
| height !== main.currentState.timeLine.height) { |
| let timeLine = Object.assign({}, main.currentState.timeLine); |
| timeLine.width = width; |
| timeLine.height = height; |
| main.currentState = Object.assign({}, main.currentState); |
| main.currentState.timeLine = timeLine; |
| main.delayRender(); |
| } |
| }, |
| |
| setFile(file) { |
| if (file !== main.currentState.file) { |
| main.currentState = Object.assign({}, main.currentState); |
| main.currentState.file = file; |
| main.delayRender(); |
| } |
| }, |
| |
| setCurrentCode(codeId) { |
| if (codeId !== main.currentState.currentCodeId) { |
| main.currentState = Object.assign({}, main.currentState); |
| main.currentState.currentCodeId = codeId; |
| main.delayRender(); |
| } |
| }, |
| |
| onResize() { |
| main.setTimeLineDimensions( |
| Math.round(window.innerWidth - 20), |
| Math.round(window.innerHeight / 5)); |
| }, |
| |
| onLoad() { |
| function loadHandler(evt) { |
| let f = evt.target.files[0]; |
| if (f) { |
| let reader = new FileReader(); |
| reader.onload = function(event) { |
| let profData = JSON.parse(event.target.result); |
| main.setViewInterval(0, Infinity); |
| main.setFile(profData); |
| }; |
| reader.onerror = function(event) { |
| console.error( |
| "File could not be read! Code " + event.target.error.code); |
| }; |
| reader.readAsText(f); |
| } else { |
| main.setFile(null); |
| } |
| } |
| $("fileinput").addEventListener( |
| "change", loadHandler, false); |
| createViews(); |
| main.onResize(); |
| }, |
| |
| delayRender() { |
| Promise.resolve().then(() => { |
| for (let c of components) { |
| c.render(main.currentState); |
| } |
| }); |
| } |
| }; |
| |
| let bucketDescriptors = |
| [ { kinds : [ "JSOPT" ], |
| color : "#00ff00", |
| backgroundColor : "#c0ffc0", |
| text : "JS Optimized" }, |
| { kinds : [ "JSUNOPT", "BC" ], |
| color : "#ffb000", |
| backgroundColor : "#ffe0c0", |
| text : "JS Unoptimized" }, |
| { kinds : [ "IC" ], |
| color : "#ffff00", |
| backgroundColor : "#ffffc0", |
| text : "IC" }, |
| { kinds : [ "STUB", "BUILTIN", "REGEXP" ], |
| color : "#ffb0b0", |
| backgroundColor : "#fff0f0", |
| text : "Other generated" }, |
| { kinds : [ "CPP", "LIB" ], |
| color : "#0000ff", |
| backgroundColor : "#c0c0ff", |
| text : "C++" }, |
| { kinds : [ "CPPEXT" ], |
| color : "#8080ff", |
| backgroundColor : "#e0e0ff", |
| text : "C++/external" }, |
| { kinds : [ "CPPPARSE" ], |
| color : "#b890f7", |
| backgroundColor : "#ebdeff", |
| text : "C++/Parser" }, |
| { kinds : [ "CPPCOMPBC" ], |
| color : "#52b0ce", |
| backgroundColor : "#a5c8d4", |
| text : "C++/Bytecode compiler" }, |
| { kinds : [ "CPPCOMP" ], |
| color : "#00ffff", |
| backgroundColor : "#c0ffff", |
| text : "C++/Compiler" }, |
| { kinds : [ "CPPGC" ], |
| color : "#ff00ff", |
| backgroundColor : "#ffc0ff", |
| text : "C++/GC" }, |
| { kinds : [ "UNKNOWN" ], |
| color : "#f0f0f0", |
| backgroundColor : "#e0e0e0", |
| text : "Unknown" } |
| ]; |
| |
| let kindToBucketDescriptor = {}; |
| for (let i = 0; i < bucketDescriptors.length; i++) { |
| let bucket = bucketDescriptors[i]; |
| for (let j = 0; j < bucket.kinds.length; j++) { |
| kindToBucketDescriptor[bucket.kinds[j]] = bucket; |
| } |
| } |
| |
| function bucketFromKind(kind) { |
| for (let i = 0; i < bucketDescriptors.length; i++) { |
| let bucket = bucketDescriptors[i]; |
| for (let j = 0; j < bucket.kinds.length; j++) { |
| if (bucket.kinds[j] === kind) { |
| return bucket; |
| } |
| } |
| } |
| return null; |
| } |
| |
| function codeTypeToText(type) { |
| switch (type) { |
| case "UNKNOWN": |
| return "Unknown"; |
| case "CPPPARSE": |
| return "C++ (parser)"; |
| case "CPPCOMPBC": |
| return "C++ (bytecode compiler)"; |
| case "CPPCOMP": |
| return "C++ (compiler)"; |
| case "CPPGC": |
| return "C++"; |
| case "CPPEXT": |
| return "C++ External"; |
| case "CPP": |
| return "C++"; |
| case "LIB": |
| return "Library"; |
| case "IC": |
| return "IC"; |
| case "BC": |
| return "Bytecode"; |
| case "STUB": |
| return "Stub"; |
| case "BUILTIN": |
| return "Builtin"; |
| case "REGEXP": |
| return "RegExp"; |
| case "JSOPT": |
| return "JS opt"; |
| case "JSUNOPT": |
| return "JS unopt"; |
| } |
| console.error("Unknown type: " + type); |
| } |
| |
| function createTypeDiv(type) { |
| if (type === "CAT") { |
| return document.createTextNode(""); |
| } |
| let div = document.createElement("div"); |
| div.classList.add("code-type-chip"); |
| |
| let span = document.createElement("span"); |
| span.classList.add("code-type-chip"); |
| span.textContent = codeTypeToText(type); |
| div.appendChild(span); |
| |
| span = document.createElement("span"); |
| span.classList.add("code-type-chip-space"); |
| div.appendChild(span); |
| |
| return div; |
| } |
| |
| function isBytecodeHandler(kind) { |
| return kind === "BytecodeHandler"; |
| } |
| |
| function filterFromFilterId(id) { |
| switch (id) { |
| case "full-tree": |
| return (type, kind) => true; |
| case "js-funs": |
| return (type, kind) => type !== 'CODE'; |
| case "js-exclude-bc": |
| return (type, kind) => |
| type !== 'CODE' || !isBytecodeHandler(kind); |
| } |
| } |
| |
| function createTableExpander(indent) { |
| let div = document.createElement("div"); |
| div.style.width = (indent + 0.5) + "em"; |
| div.style.display = "inline-block"; |
| div.style.textAlign = "right"; |
| return div; |
| } |
| |
| function createFunctionNode(name, codeId) { |
| if (codeId === -1) { |
| return document.createTextNode(name); |
| } |
| let nameElement = document.createElement("span"); |
| nameElement.classList.add("codeid-link"); |
| nameElement.onclick = function() { |
| main.setCurrentCode(codeId); |
| }; |
| nameElement.appendChild(document.createTextNode(name)); |
| return nameElement; |
| } |
| |
| class CallTreeView { |
| constructor() { |
| this.element = $("calltree"); |
| this.treeElement = $("calltree-table"); |
| this.selectAttribution = $("calltree-attribution"); |
| this.selectCategories = $("calltree-categories"); |
| this.selectSort = $("calltree-sort"); |
| |
| this.selectAttribution.onchange = () => { |
| main.setCallTreeAttribution(this.selectAttribution.value); |
| }; |
| |
| this.selectCategories.onchange = () => { |
| main.setCallTreeCategories(this.selectCategories.value); |
| }; |
| |
| this.selectSort.onchange = () => { |
| main.setCallTreeSort(this.selectSort.value); |
| }; |
| |
| this.currentState = null; |
| } |
| |
| sortFromId(id) { |
| switch (id) { |
| case "time": |
| return (c1, c2) => { |
| if (c1.ticks < c2.ticks) return 1; |
| else if (c1.ticks > c2.ticks) return -1; |
| return c2.ownTicks - c1.ownTicks; |
| }; |
| case "own-time": |
| return (c1, c2) => { |
| if (c1.ownTicks < c2.ownTicks) return 1; |
| else if (c1.ownTicks > c2.ownTicks) return -1; |
| return c2.ticks - c1.ticks; |
| }; |
| case "category-time": |
| return (c1, c2) => { |
| if (c1.type === c2.type) return c2.ticks - c1.ticks; |
| if (c1.type < c2.type) return 1; |
| return -1; |
| }; |
| case "category-own-time": |
| return (c1, c2) => { |
| if (c1.type === c2.type) return c2.ownTicks - c1.ownTicks; |
| if (c1.type < c2.type) return 1; |
| return -1; |
| }; |
| } |
| } |
| |
| expandTree(tree, indent) { |
| let that = this; |
| let index = 0; |
| let id = "R/"; |
| let row = tree.row; |
| let expander = tree.expander; |
| |
| if (row) { |
| index = row.rowIndex; |
| id = row.id; |
| |
| // Make sure we collapse the children when the row is clicked |
| // again. |
| expander.textContent = "\u25BE"; |
| let expandHandler = expander.onclick; |
| expander.onclick = () => { |
| that.collapseRow(tree, expander, expandHandler); |
| } |
| } |
| |
| // Collect the children, and sort them by ticks. |
| let children = []; |
| let filter = |
| filterFromFilterId(this.currentState.callTree.attribution); |
| for (let childId in tree.children) { |
| let child = tree.children[childId]; |
| if (child.ticks > 0) { |
| children.push(child); |
| if (child.delayedExpansion) { |
| expandTreeNode(this.currentState.file, child, filter); |
| } |
| } |
| } |
| children.sort(this.sortFromId(this.currentState.callTree.sort)); |
| |
| for (let i = 0; i < children.length; i++) { |
| let node = children[i]; |
| let row = this.rows.insertRow(index); |
| row.id = id + i + "/"; |
| |
| if (node.type !== "CAT") { |
| row.style.backgroundColor = bucketFromKind(node.type).backgroundColor; |
| } |
| |
| // Inclusive time % cell. |
| let c = row.insertCell(); |
| c.textContent = (node.ticks * 100 / this.tickCount).toFixed(2) + "%"; |
| c.style.textAlign = "right"; |
| // Percent-of-parent cell. |
| c = row.insertCell(); |
| c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%"; |
| c.style.textAlign = "right"; |
| // Exclusive time % cell. |
| if (this.currentState.mode !== "bottom-up") { |
| c = row.insertCell(-1); |
| c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%"; |
| c.style.textAlign = "right"; |
| } |
| |
| // Create the name cell. |
| let nameCell = row.insertCell(); |
| let expander = createTableExpander(indent + 1); |
| nameCell.appendChild(expander); |
| nameCell.appendChild(createTypeDiv(node.type)); |
| nameCell.appendChild(createFunctionNode(node.name, node.codeId)); |
| |
| // Inclusive ticks cell. |
| c = row.insertCell(); |
| c.textContent = node.ticks; |
| c.style.textAlign = "right"; |
| if (this.currentState.mode !== "bottom-up") { |
| // Exclusive ticks cell. |
| c = row.insertCell(-1); |
| c.textContent = node.ownTicks; |
| c.style.textAlign = "right"; |
| } |
| if (node.children.length > 0) { |
| expander.textContent = "\u25B8"; |
| expander.onclick = () => { that.expandTree(node, indent + 1); }; |
| } |
| |
| node.row = row; |
| node.expander = expander; |
| |
| index++; |
| } |
| } |
| |
| collapseRow(tree, expander, expandHandler) { |
| let row = tree.row; |
| let id = row.id; |
| let index = row.rowIndex; |
| while (row.rowIndex < this.rows.rows.length && |
| this.rows.rows[index].id.startsWith(id)) { |
| this.rows.deleteRow(index); |
| } |
| |
| expander.textContent = "\u25B8"; |
| expander.onclick = expandHandler; |
| } |
| |
| fillSelects(mode, calltree) { |
| function addOptions(e, values, current) { |
| while (e.options.length > 0) { |
| e.remove(0); |
| } |
| for (let i = 0; i < values.length; i++) { |
| let option = document.createElement("option"); |
| option.value = values[i].value; |
| option.textContent = values[i].text; |
| e.appendChild(option); |
| } |
| e.value = current; |
| } |
| |
| let attributions = [ |
| { value : "js-exclude-bc", |
| text : "Attribute bytecode handlers to caller" }, |
| { value : "full-tree", |
| text : "Count each code object separately" }, |
| { value : "js-funs", |
| text : "Attribute non-functions to JS functions" } |
| ]; |
| |
| switch (mode) { |
| case "bottom-up": |
| addOptions(this.selectAttribution, attributions, calltree.attribution); |
| addOptions(this.selectCategories, [ |
| { value : "code-type", text : "Code type" }, |
| { value : "none", text : "None" } |
| ], calltree.categories); |
| addOptions(this.selectSort, [ |
| { value : "time", text : "Time (including children)" }, |
| { value : "category-time", text : "Code category, time" }, |
| ], calltree.sort); |
| return; |
| case "top-down": |
| addOptions(this.selectAttribution, attributions, calltree.attribution); |
| addOptions(this.selectCategories, [ |
| { value : "none", text : "None" }, |
| { value : "rt-entry", text : "Runtime entries" } |
| ], calltree.categories); |
| addOptions(this.selectSort, [ |
| { value : "time", text : "Time (including children)" }, |
| { value : "own-time", text : "Own time" }, |
| { value : "category-time", text : "Code category, time" }, |
| { value : "category-own-time", text : "Code category, own time"} |
| ], calltree.sort); |
| return; |
| case "function-list": |
| addOptions(this.selectAttribution, attributions, calltree.attribution); |
| addOptions(this.selectCategories, [ |
| { value : "code-type", text : "Code type" }, |
| { value : "none", text : "None" } |
| ], calltree.categories); |
| addOptions(this.selectSort, [ |
| { value : "own-time", text : "Own time" }, |
| { value : "time", text : "Time (including children)" }, |
| { value : "category-own-time", text : "Code category, own time"}, |
| { value : "category-time", text : "Code category, time" }, |
| ], calltree.sort); |
| return; |
| } |
| console.error("Unexpected mode"); |
| } |
| |
| static isCallTreeMode(mode) { |
| switch (mode) { |
| case "bottom-up": |
| case "top-down": |
| case "function-list": |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| render(newState) { |
| let oldState = this.currentState; |
| if (!newState.file || !CallTreeView.isCallTreeMode(newState.mode)) { |
| this.element.style.display = "none"; |
| this.currentState = null; |
| return; |
| } |
| |
| this.currentState = newState; |
| if (oldState) { |
| if (newState.file === oldState.file && |
| newState.start === oldState.start && |
| newState.end === oldState.end && |
| newState.mode === oldState.mode && |
| newState.callTree.attribution === oldState.callTree.attribution && |
| newState.callTree.categories === oldState.callTree.categories && |
| newState.callTree.sort === oldState.callTree.sort) { |
| // No change => just return. |
| return; |
| } |
| } |
| |
| this.element.style.display = "inherit"; |
| |
| let mode = this.currentState.mode; |
| if (!oldState || mode !== oldState.mode) { |
| // Technically, we should also call this if attribution, categories or |
| // sort change, but the selection is already highlighted by the combobox |
| // itself, so we do need to do anything here. |
| this.fillSelects(newState.mode, newState.callTree); |
| } |
| |
| let ownTimeClass = (mode === "bottom-up") ? "numeric-hidden" : "numeric"; |
| let ownTimeTh = $(this.treeElement.id + "-own-time-header"); |
| ownTimeTh.classList = ownTimeClass; |
| let ownTicksTh = $(this.treeElement.id + "-own-ticks-header"); |
| ownTicksTh.classList = ownTimeClass; |
| |
| // Build the tree. |
| let stackProcessor; |
| let filter = filterFromFilterId(this.currentState.callTree.attribution); |
| if (mode === "top-down") { |
| if (this.currentState.callTree.categories === "rt-entry") { |
| stackProcessor = |
| new RuntimeCallTreeProcessor(); |
| } else { |
| stackProcessor = |
| new PlainCallTreeProcessor(filter, false); |
| } |
| } else if (mode === "function-list") { |
| stackProcessor = new FunctionListTree( |
| filter, this.currentState.callTree.categories === "code-type"); |
| |
| } else { |
| console.assert(mode === "bottom-up"); |
| if (this.currentState.callTree.categories === "none") { |
| stackProcessor = |
| new PlainCallTreeProcessor(filter, true); |
| } else { |
| console.assert(this.currentState.callTree.categories === "code-type"); |
| stackProcessor = |
| new CategorizedCallTreeProcessor(filter, true); |
| } |
| } |
| this.tickCount = |
| generateTree(this.currentState.file, |
| this.currentState.start, |
| this.currentState.end, |
| stackProcessor); |
| // TODO(jarin) Handle the case when tick count is negative. |
| |
| this.tree = stackProcessor.tree; |
| |
| // Remove old content of the table, replace with new one. |
| let oldRows = this.treeElement.getElementsByTagName("tbody"); |
| let newRows = document.createElement("tbody"); |
| this.rows = newRows; |
| |
| // Populate the table. |
| this.expandTree(this.tree, 0); |
| |
| // Swap in the new rows. |
| this.treeElement.replaceChild(newRows, oldRows[0]); |
| } |
| } |
| |
| class TimelineView { |
| constructor() { |
| this.element = $("timeline"); |
| this.canvas = $("timeline-canvas"); |
| this.legend = $("timeline-legend"); |
| this.currentCode = $("timeline-currentCode"); |
| |
| this.canvas.onmousedown = this.onMouseDown.bind(this); |
| this.canvas.onmouseup = this.onMouseUp.bind(this); |
| this.canvas.onmousemove = this.onMouseMove.bind(this); |
| |
| this.selectionStart = null; |
| this.selectionEnd = null; |
| this.selecting = false; |
| |
| this.fontSize = 12; |
| this.imageOffset = Math.round(this.fontSize * 1.2); |
| this.functionTimelineHeight = 24; |
| this.functionTimelineTickHeight = 16; |
| |
| this.currentState = null; |
| } |
| |
| onMouseDown(e) { |
| this.selectionStart = |
| e.clientX - this.canvas.getBoundingClientRect().left; |
| this.selectionEnd = this.selectionStart + 1; |
| this.selecting = true; |
| } |
| |
| onMouseMove(e) { |
| if (this.selecting) { |
| this.selectionEnd = |
| e.clientX - this.canvas.getBoundingClientRect().left; |
| this.drawSelection(); |
| } |
| } |
| |
| onMouseUp(e) { |
| if (this.selectionStart !== null) { |
| let x = e.clientX - this.canvas.getBoundingClientRect().left; |
| if (Math.abs(x - this.selectionStart) < 10) { |
| this.selectionStart = null; |
| this.selectionEnd = null; |
| let ctx = this.canvas.getContext("2d"); |
| ctx.drawImage(this.buffer, 0, this.imageOffset); |
| } else { |
| this.selectionEnd = x; |
| this.drawSelection(); |
| } |
| let file = this.currentState.file; |
| if (file) { |
| let start = this.selectionStart === null ? 0 : this.selectionStart; |
| let end = this.selectionEnd === null ? Infinity : this.selectionEnd; |
| let firstTime = file.ticks[0].tm; |
| let lastTime = file.ticks[file.ticks.length - 1].tm; |
| |
| let width = this.buffer.width; |
| |
| start = (start / width) * (lastTime - firstTime) + firstTime; |
| end = (end / width) * (lastTime - firstTime) + firstTime; |
| |
| if (end < start) { |
| let temp = start; |
| start = end; |
| end = temp; |
| } |
| |
| main.setViewInterval(start, end); |
| } |
| } |
| this.selecting = false; |
| } |
| |
| drawSelection() { |
| let ctx = this.canvas.getContext("2d"); |
| |
| // Draw the timeline image. |
| ctx.drawImage(this.buffer, 0, this.imageOffset); |
| |
| // Draw the current interval highlight. |
| let left; |
| let right; |
| if (this.selectionStart !== null && this.selectionEnd !== null) { |
| ctx.fillStyle = "rgba(0, 0, 0, 0.3)"; |
| left = Math.min(this.selectionStart, this.selectionEnd); |
| right = Math.max(this.selectionStart, this.selectionEnd); |
| let height = this.buffer.height - this.functionTimelineHeight; |
| ctx.fillRect(0, this.imageOffset, left, height); |
| ctx.fillRect(right, this.imageOffset, this.buffer.width - right, height); |
| } else { |
| left = 0; |
| right = this.buffer.width; |
| } |
| |
| // Draw the scale text. |
| let file = this.currentState.file; |
| ctx.fillStyle = "white"; |
| ctx.fillRect(0, 0, this.canvas.width, this.imageOffset); |
| if (file && file.ticks.length > 0) { |
| let firstTime = file.ticks[0].tm; |
| let lastTime = file.ticks[file.ticks.length - 1].tm; |
| |
| let leftTime = |
| firstTime + left / this.canvas.width * (lastTime - firstTime); |
| let rightTime = |
| firstTime + right / this.canvas.width * (lastTime - firstTime); |
| |
| let leftText = (leftTime / 1000000).toFixed(3) + "s"; |
| let rightText = (rightTime / 1000000).toFixed(3) + "s"; |
| |
| ctx.textBaseline = 'top'; |
| ctx.font = this.fontSize + "px Arial"; |
| ctx.fillStyle = "black"; |
| |
| let leftWidth = ctx.measureText(leftText).width; |
| let rightWidth = ctx.measureText(rightText).width; |
| |
| let leftStart = left - leftWidth / 2; |
| let rightStart = right - rightWidth / 2; |
| |
| if (leftStart < 0) leftStart = 0; |
| if (rightStart + rightWidth > this.canvas.width) { |
| rightStart = this.canvas.width - rightWidth; |
| } |
| if (leftStart + leftWidth > rightStart) { |
| if (leftStart > this.canvas.width - (rightStart - rightWidth)) { |
| rightStart = leftStart + leftWidth; |
| |
| } else { |
| leftStart = rightStart - leftWidth; |
| } |
| } |
| |
| ctx.fillText(leftText, leftStart, 0); |
| ctx.fillText(rightText, rightStart, 0); |
| } |
| } |
| |
| render(newState) { |
| let oldState = this.currentState; |
| |
| if (!newState.file) { |
| this.element.style.display = "none"; |
| return; |
| } |
| |
| this.currentState = newState; |
| if (oldState) { |
| if (newState.timeLine.width === oldState.timeLine.width && |
| newState.timeLine.height === oldState.timeLine.height && |
| newState.file === oldState.file && |
| newState.currentCodeId === oldState.currentCodeId && |
| newState.start === oldState.start && |
| newState.end === oldState.end) { |
| // No change, nothing to do. |
| return; |
| } |
| } |
| |
| this.element.style.display = "inherit"; |
| |
| // Make sure the canvas has the right dimensions. |
| let width = this.currentState.timeLine.width; |
| let height = this.currentState.timeLine.height; |
| this.canvas.width = width; |
| this.canvas.height = height; |
| |
| // Make space for the selection text. |
| height -= this.imageOffset; |
| |
| let file = this.currentState.file; |
| if (!file) return; |
| |
| let currentCodeId = this.currentState.currentCodeId; |
| |
| let firstTime = file.ticks[0].tm; |
| let lastTime = file.ticks[file.ticks.length - 1].tm; |
| let start = Math.max(this.currentState.start, firstTime); |
| let end = Math.min(this.currentState.end, lastTime); |
| |
| this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width; |
| this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width; |
| |
| let tickCount = file.ticks.length; |
| |
| let minBucketPixels = 10; |
| let minBucketSamples = 30; |
| let bucketCount = Math.min(width / minBucketPixels, |
| tickCount / minBucketSamples); |
| |
| let stackProcessor = new CategorySampler(file, bucketCount); |
| generateTree(file, 0, Infinity, stackProcessor); |
| let codeIdProcessor = new FunctionTimelineProcessor( |
| currentCodeId, |
| filterFromFilterId(this.currentState.callTree.attribution)); |
| generateTree(file, 0, Infinity, codeIdProcessor); |
| |
| let buffer = document.createElement("canvas"); |
| |
| buffer.width = width; |
| buffer.height = height; |
| |
| // Calculate the bar heights for each bucket. |
| let graphHeight = height - this.functionTimelineHeight; |
| let buckets = stackProcessor.buckets; |
| let bucketsGraph = []; |
| for (let i = 0; i < buckets.length; i++) { |
| let sum = 0; |
| let bucketData = []; |
| let total = buckets[i].total; |
| for (let j = 0; j < bucketDescriptors.length; j++) { |
| let desc = bucketDescriptors[j]; |
| for (let k = 0; k < desc.kinds.length; k++) { |
| sum += buckets[i][desc.kinds[k]]; |
| } |
| bucketData.push(Math.round(graphHeight * sum / total)); |
| } |
| bucketsGraph.push(bucketData); |
| } |
| |
| // Draw the category graph into the buffer. |
| let bucketWidth = width / bucketsGraph.length; |
| let ctx = buffer.getContext('2d'); |
| for (let i = 0; i < bucketsGraph.length - 1; i++) { |
| let bucketData = bucketsGraph[i]; |
| let nextBucketData = bucketsGraph[i + 1]; |
| for (let j = 0; j < bucketData.length; j++) { |
| let x1 = Math.round(i * bucketWidth); |
| let x2 = Math.round((i + 1) * bucketWidth); |
| ctx.beginPath(); |
| ctx.moveTo(x1, j && bucketData[j - 1]); |
| ctx.lineTo(x2, j && nextBucketData[j - 1]); |
| ctx.lineTo(x2, nextBucketData[j]); |
| ctx.lineTo(x1, bucketData[j]); |
| ctx.closePath(); |
| ctx.fillStyle = bucketDescriptors[j].color; |
| ctx.fill(); |
| } |
| } |
| |
| // Draw the function ticks. |
| let functionTimelineYOffset = graphHeight; |
| let functionTimelineTickHeight = this.functionTimelineTickHeight; |
| let functionTimelineHalfHeight = |
| Math.round(functionTimelineTickHeight / 2); |
| let timestampScaler = width / (lastTime - firstTime); |
| let timestampToX = (t) => Math.round((t - firstTime) * timestampScaler); |
| ctx.fillStyle = "white"; |
| ctx.fillRect( |
| 0, |
| functionTimelineYOffset, |
| buffer.width, |
| this.functionTimelineHeight); |
| for (let i = 0; i < codeIdProcessor.blocks.length; i++) { |
| let block = codeIdProcessor.blocks[i]; |
| let bucket = kindToBucketDescriptor[block.kind]; |
| ctx.fillStyle = bucket.color; |
| ctx.fillRect( |
| timestampToX(block.start), |
| functionTimelineYOffset, |
| Math.max(1, Math.round((block.end - block.start) * timestampScaler)), |
| block.topOfStack ? |
| functionTimelineTickHeight : functionTimelineHalfHeight); |
| } |
| ctx.strokeStyle = "black"; |
| ctx.lineWidth = "1"; |
| ctx.beginPath(); |
| ctx.moveTo(0, functionTimelineYOffset + 0.5); |
| ctx.lineTo(buffer.width, functionTimelineYOffset + 0.5); |
| ctx.stroke(); |
| ctx.strokeStyle = "rgba(0,0,0,0.2)"; |
| ctx.lineWidth = "1"; |
| ctx.beginPath(); |
| ctx.moveTo(0, functionTimelineYOffset + functionTimelineHalfHeight - 0.5); |
| ctx.lineTo(buffer.width, |
| functionTimelineYOffset + functionTimelineHalfHeight - 0.5); |
| ctx.stroke(); |
| |
| // Draw marks for optimizations and deoptimizations in the function |
| // timeline. |
| if (currentCodeId && currentCodeId >= 0 && |
| file.code[currentCodeId].func) { |
| let y = Math.round(functionTimelineYOffset + functionTimelineTickHeight + |
| (this.functionTimelineHeight - functionTimelineTickHeight) / 2); |
| let func = file.functions[file.code[currentCodeId].func]; |
| for (let i = 0; i < func.codes.length; i++) { |
| let code = file.code[func.codes[i]]; |
| if (code.kind === "Opt") { |
| if (code.deopt) { |
| // Draw deoptimization mark. |
| let x = timestampToX(code.deopt.tm); |
| ctx.lineWidth = 0.7; |
| ctx.strokeStyle = "red"; |
| ctx.beginPath(); |
| ctx.moveTo(x - 3, y - 3); |
| ctx.lineTo(x + 3, y + 3); |
| ctx.stroke(); |
| ctx.beginPath(); |
| ctx.moveTo(x - 3, y + 3); |
| ctx.lineTo(x + 3, y - 3); |
| ctx.stroke(); |
| } |
| // Draw optimization mark. |
| let x = timestampToX(code.tm); |
| ctx.lineWidth = 0.7; |
| ctx.strokeStyle = "blue"; |
| ctx.beginPath(); |
| ctx.moveTo(x - 3, y - 3); |
| ctx.lineTo(x, y); |
| ctx.stroke(); |
| ctx.beginPath(); |
| ctx.moveTo(x - 3, y + 3); |
| ctx.lineTo(x, y); |
| ctx.stroke(); |
| } else { |
| // Draw code creation mark. |
| let x = Math.round(timestampToX(code.tm)); |
| ctx.beginPath(); |
| ctx.fillStyle = "black"; |
| ctx.arc(x, y, 3, 0, 2 * Math.PI); |
| ctx.fill(); |
| } |
| } |
| } |
| |
| // Remember stuff for later. |
| this.buffer = buffer; |
| |
| // Draw the buffer. |
| this.drawSelection(); |
| |
| // (Re-)Populate the graph legend. |
| while (this.legend.cells.length > 0) { |
| this.legend.deleteCell(0); |
| } |
| let cell = this.legend.insertCell(-1); |
| cell.textContent = "Legend: "; |
| cell.style.padding = "1ex"; |
| for (let i = 0; i < bucketDescriptors.length; i++) { |
| let cell = this.legend.insertCell(-1); |
| cell.style.padding = "1ex"; |
| let desc = bucketDescriptors[i]; |
| let div = document.createElement("div"); |
| div.style.display = "inline-block"; |
| div.style.width = "0.6em"; |
| div.style.height = "1.2ex"; |
| div.style.backgroundColor = desc.color; |
| div.style.borderStyle = "solid"; |
| div.style.borderWidth = "1px"; |
| div.style.borderColor = "Black"; |
| cell.appendChild(div); |
| cell.appendChild(document.createTextNode(" " + desc.text)); |
| } |
| |
| while (this.currentCode.firstChild) { |
| this.currentCode.removeChild(this.currentCode.firstChild); |
| } |
| if (currentCodeId) { |
| let currentCode = file.code[currentCodeId]; |
| this.currentCode.appendChild(document.createTextNode(currentCode.name)); |
| } else { |
| this.currentCode.appendChild(document.createTextNode("<none>")); |
| } |
| } |
| } |
| |
| class ModeBarView { |
| constructor() { |
| let modeBar = this.element = $("mode-bar"); |
| |
| function addMode(id, text, active) { |
| let div = document.createElement("div"); |
| div.classList = "mode-button" + (active ? " active-mode-button" : ""); |
| div.id = "mode-" + id; |
| div.textContent = text; |
| div.onclick = () => { |
| if (main.currentState.mode === id) return; |
| let old = $("mode-" + main.currentState.mode); |
| old.classList = "mode-button"; |
| div.classList = "mode-button active-mode-button"; |
| main.setMode(id); |
| }; |
| modeBar.appendChild(div); |
| } |
| |
| addMode("summary", "Summary", true); |
| addMode("bottom-up", "Bottom up"); |
| addMode("top-down", "Top down"); |
| addMode("function-list", "Functions"); |
| } |
| |
| render(newState) { |
| if (!newState.file) { |
| this.element.style.display = "none"; |
| return; |
| } |
| |
| this.element.style.display = "inherit"; |
| } |
| } |
| |
| class SummaryView { |
| constructor() { |
| this.element = $("summary"); |
| this.currentState = null; |
| } |
| |
| render(newState) { |
| let oldState = this.currentState; |
| |
| if (!newState.file || newState.mode !== "summary") { |
| this.element.style.display = "none"; |
| this.currentState = null; |
| return; |
| } |
| |
| this.currentState = newState; |
| if (oldState) { |
| if (newState.file === oldState.file && |
| newState.start === oldState.start && |
| newState.end === oldState.end) { |
| // No change, nothing to do. |
| return; |
| } |
| } |
| |
| this.element.style.display = "inherit"; |
| |
| while (this.element.firstChild) { |
| this.element.removeChild(this.element.firstChild); |
| } |
| |
| let stats = computeOptimizationStats( |
| this.currentState.file, newState.start, newState.end); |
| |
| let table = document.createElement("table"); |
| let rows = document.createElement("tbody"); |
| |
| function addRow(text, number, indent) { |
| let row = rows.insertRow(-1); |
| let textCell = row.insertCell(-1); |
| textCell.textContent = text; |
| let numberCell = row.insertCell(-1); |
| numberCell.textContent = number; |
| if (indent) { |
| textCell.style.textIndent = indent + "em"; |
| numberCell.style.textIndent = indent + "em"; |
| } |
| return row; |
| } |
| |
| function makeCollapsible(row, expander) { |
| expander.textContent = "\u25BE"; |
| let expandHandler = expander.onclick; |
| expander.onclick = () => { |
| let id = row.id; |
| let index = row.rowIndex + 1; |
| while (index < rows.rows.length && |
| rows.rows[index].id.startsWith(id)) { |
| rows.deleteRow(index); |
| } |
| expander.textContent = "\u25B8"; |
| expander.onclick = expandHandler; |
| } |
| } |
| |
| function expandDeoptInstances(row, expander, instances, indent, kind) { |
| let index = row.rowIndex; |
| for (let i = 0; i < instances.length; i++) { |
| let childRow = rows.insertRow(index + 1); |
| childRow.id = row.id + i + "/"; |
| |
| let deopt = instances[i].deopt; |
| |
| let textCell = childRow.insertCell(-1); |
| textCell.appendChild(document.createTextNode(deopt.posText)); |
| textCell.style.textIndent = indent + "em"; |
| let reasonCell = childRow.insertCell(-1); |
| reasonCell.appendChild( |
| document.createTextNode("Reason: " + deopt.reason)); |
| reasonCell.style.textIndent = indent + "em"; |
| } |
| makeCollapsible(row, expander); |
| } |
| |
| function expandDeoptFunctionList(row, expander, list, indent, kind) { |
| let index = row.rowIndex; |
| for (let i = 0; i < list.length; i++) { |
| let childRow = rows.insertRow(index + 1); |
| childRow.id = row.id + i + "/"; |
| |
| let textCell = childRow.insertCell(-1); |
| let expander = createTableExpander(indent); |
| textCell.appendChild(expander); |
| textCell.appendChild( |
| createFunctionNode(list[i].f.name, list[i].f.codes[0])); |
| |
| let numberCell = childRow.insertCell(-1); |
| numberCell.textContent = list[i].instances.length; |
| numberCell.style.textIndent = indent + "em"; |
| |
| expander.textContent = "\u25B8"; |
| expander.onclick = () => { |
| expandDeoptInstances( |
| childRow, expander, list[i].instances, indent + 1); |
| }; |
| } |
| makeCollapsible(row, expander); |
| } |
| |
| function expandOptimizedFunctionList(row, expander, list, indent, kind) { |
| let index = row.rowIndex; |
| for (let i = 0; i < list.length; i++) { |
| let childRow = rows.insertRow(index + 1); |
| childRow.id = row.id + i + "/"; |
| |
| let textCell = childRow.insertCell(-1); |
| textCell.appendChild( |
| createFunctionNode(list[i].f.name, list[i].f.codes[0])); |
| textCell.style.textIndent = indent + "em"; |
| |
| let numberCell = childRow.insertCell(-1); |
| numberCell.textContent = list[i].instances.length; |
| numberCell.style.textIndent = indent + "em"; |
| } |
| makeCollapsible(row, expander); |
| } |
| |
| function addExpandableRow(text, list, indent, kind) { |
| let row = rows.insertRow(-1); |
| |
| row.id = "opt-table/" + kind + "/"; |
| |
| let textCell = row.insertCell(-1); |
| let expander = createTableExpander(indent); |
| textCell.appendChild(expander); |
| textCell.appendChild(document.createTextNode(text)); |
| |
| let numberCell = row.insertCell(-1); |
| numberCell.textContent = list.count; |
| if (indent) { |
| numberCell.style.textIndent = indent + "em"; |
| } |
| |
| if (list.count > 0) { |
| expander.textContent = "\u25B8"; |
| if (kind === "opt") { |
| expander.onclick = () => { |
| expandOptimizedFunctionList( |
| row, expander, list.functions, indent + 1, kind); |
| }; |
| } else { |
| expander.onclick = () => { |
| expandDeoptFunctionList( |
| row, expander, list.functions, indent + 1, kind); |
| }; |
| } |
| } |
| return row; |
| } |
| |
| addRow("Total function count:", stats.functionCount); |
| addRow("Optimized function count:", stats.optimizedFunctionCount, 1); |
| addRow("Deoptimized function count:", stats.deoptimizedFunctionCount, 2); |
| |
| addExpandableRow("Optimization count:", stats.optimizations, 0, "opt"); |
| let deoptCount = stats.eagerDeoptimizations.count + |
| stats.softDeoptimizations.count + stats.lazyDeoptimizations.count; |
| addRow("Deoptimization count:", deoptCount); |
| addExpandableRow("Eager:", stats.eagerDeoptimizations, 1, "eager"); |
| addExpandableRow("Lazy:", stats.lazyDeoptimizations, 1, "lazy"); |
| addExpandableRow("Soft:", stats.softDeoptimizations, 1, "soft"); |
| |
| table.appendChild(rows); |
| this.element.appendChild(table); |
| } |
| } |
| |
| class HelpView { |
| constructor() { |
| this.element = $("help"); |
| } |
| |
| render(newState) { |
| this.element.style.display = newState.file ? "none" : "inherit"; |
| } |
| } |