// 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); } function removeAllChildren(element) { while (element.firstChild) { element.removeChild(element.firstChild); } } let components; function createViews() { components = [ new CallTreeView(), new TimelineView(), new HelpView(), new SummaryView(), new ModeBarView(), new ScriptSourceView(), ]; } function emptyState() { return { file : null, mode : null, currentCodeId : null, viewingSource: false, start : 0, end : Infinity, timelineSize : { width : 0, height : 0 }, callTree : { attribution : "js-exclude-bc", categories : "code-type", sort : "time" }, sourceData: null }; } function setCallTreeState(state, callTreeState) { state = Object.assign({}, state); state.callTree = callTreeState; return state; } let main = { currentState : emptyState(), renderPending : false, 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(); } }, updateSources(file) { let statusDiv = $("source-status"); if (!file) { statusDiv.textContent = ""; return; } if (!file.scripts || file.scripts.length === 0) { statusDiv.textContent = "Script source not available. Run profiler with --log-source-code."; return; } statusDiv.textContent = "Script source is available."; main.currentState.sourceData = new SourceData(file); }, setFile(file) { if (file !== main.currentState.file) { let lastMode = main.currentState.mode || "summary"; main.currentState = emptyState(); main.currentState.file = file; main.updateSources(file); main.setMode(lastMode); main.delayRender(); } }, setCurrentCode(codeId) { if (codeId !== main.currentState.currentCodeId) { main.currentState = Object.assign({}, main.currentState); main.currentState.currentCodeId = codeId; main.delayRender(); } }, setViewingSource(value) { if (main.currentState.viewingSource !== value) { main.currentState = Object.assign({}, main.currentState); main.currentState.viewingSource = value; main.delayRender(); } }, onResize() { main.delayRender(); }, onLoad() { function loadHandler(evt) { let f = evt.target.files[0]; if (f) { let reader = new FileReader(); reader.onload = function(event) { main.setFile(JSON.parse(event.target.result)); }; 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(); }, delayRender() { if (main.renderPending) return; main.renderPending = true; window.requestAnimationFrame(() => { main.renderPending = false; for (let c of components) { c.render(main.currentState); } }); } }; const CATEGORY_COLOR = "#f5f5f5"; const bucketDescriptors = [ { kinds: ["JS_IGNITION", "BC"], color: "#dd2c00", backgroundColor: "#ff9e80", text: "JS Ignition" }, { kinds: ["JS_SPARKPLUG"], color: "#b3005b", backgroundColor: "#ff9e80", text: "JS Sparkplug" }, { kinds: ["JS_MAGLEV"], color: "#693eb8", backgroundColor: "#d80093", text: "JS Maglev" }, { kinds: ["JS_TURBOFAN"], color: "#64dd17", backgroundColor: "#80e27e", text: "JS Turbofan" }, { kinds: ["IC"], color: "#ff6d00", backgroundColor: "#ffab40", text: "IC" }, { kinds: ["STUB", "BUILTIN", "REGEXP"], color: "#ffd600", backgroundColor: "#ffea00", text: "Other generated" }, { kinds: ["CPP", "LIB"], color: "#304ffe", backgroundColor: "#6ab7ff", text: "C++" }, { kinds: ["CPP_EXT"], color: "#003c8f", backgroundColor: "#c0cfff", text: "C++/external" }, { kinds: ["CPP_PARSE"], color: "#aa00ff", backgroundColor: "#ffb2ff", text: "C++/Parser" }, { kinds: ["CPP_COMP_BC"], color: "#43a047", backgroundColor: "#88c399", text: "C++/Bytecode compiler" }, { kinds: ["CPP_COMP_BASELINE"], color: "#43a047", backgroundColor: "#5a8000", text: "C++/Baseline compiler" }, { kinds: ["CPP_COMP"], color: "#00e5ff", backgroundColor: "#6effff", text: "C++/Compiler" }, { kinds: ["CPP_GC"], color: "#6200ea", backgroundColor: "#e1bee7", text: "C++/GC" }, { kinds: ["UNKNOWN"], color: "#bdbdbd", backgroundColor: "#efefef", 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 "CPP_PARSE": return "C++ Parser"; case "CPP_COMP_BASELINE": return "C++ Baseline Compiler"; case "CPP_COMP_BC": return "C++ Bytecode Compiler"; case "CPP_COMP": return "C++ Compiler"; case "CPP_GC": return "C++ GC"; case "CPP_EXT": 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 "JS_IGNITION": return "JS Ignition"; case "JS_SPARKPLUG": return "JS Sparkplug"; case "JS_TURBOPROP": return "JS Turboprop"; case "JS_MAGLEV": return "JS Maglev"; case "JS_TURBOFAN": return "JS Turbofan"; } console.error("Unknown type: " + type); } function createTypeNode(type) { if (type === "CAT") { return document.createTextNode(""); } let span = document.createElement("span"); span.classList.add("code-type-chip"); span.textContent = codeTypeToText(type); return span; } 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' || kind !== "BytecodeHandler"; } } function createIndentNode(indent) { let div = document.createElement("div"); div.style.display = "inline-block"; div.style.width = (indent + 0.5) + "em"; return div; } function createArrowNode() { let span = document.createElement("span"); span.classList.add("tree-row-arrow"); return span; } function createFunctionNode(name, codeId) { let nameElement = document.createElement("span"); nameElement.appendChild(document.createTextNode(name)); nameElement.classList.add("tree-row-name"); if (codeId !== -1) { nameElement.classList.add("codeid-link"); nameElement.onclick = (event) => { main.setCurrentCode(codeId); // Prevent the click from bubbling to the row and causing it to // collapse/expand. event.stopPropagation(); }; } return nameElement; } function createViewSourceNode(codeId) { let linkElement = document.createElement("span"); linkElement.appendChild(document.createTextNode("View source")); linkElement.classList.add("view-source-link"); linkElement.onclick = (event) => { main.setCurrentCode(codeId); main.setViewingSource(true); // Prevent the click from bubbling to the row and causing it to // collapse/expand. event.stopPropagation(); }; return linkElement; } const COLLAPSED_ARROW = "\u25B6"; const EXPANDED_ARROW = "\u25BC"; 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 index = 0; let id = "R/"; let row = tree.row; if (row) { index = row.rowIndex; id = row.id; tree.arrow.textContent = EXPANDED_ARROW; // Collapse the children when the row is clicked again. let expandHandler = row.onclick; row.onclick = () => { this.collapseRow(tree, 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 = CATEGORY_COLOR; } else { 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(); nameCell.appendChild(createIndentNode(indent + 1)); let arrow = createArrowNode(); nameCell.appendChild(arrow); nameCell.appendChild(createTypeNode(node.type)); nameCell.appendChild(createFunctionNode(node.name, node.codeId)); if (main.currentState.sourceData && node.codeId >= 0 && main.currentState.sourceData.hasSource( this.currentState.file.code[node.codeId].func)) { nameCell.appendChild(createViewSourceNode(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) { arrow.textContent = COLLAPSED_ARROW; row.onclick = () => { this.expandTree(node, indent + 1); }; } node.row = row; node.arrow = arrow; index++; } } collapseRow(tree, 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); } tree.arrow.textContent = COLLAPSED_ARROW; row.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; } let width = Math.round(document.documentElement.clientWidth - 20); let height = Math.round(document.documentElement.clientHeight / 5); if (oldState) { if (width === oldState.timelineSize.width && height === oldState.timelineSize.height && newState.file === oldState.file && newState.currentCodeId === oldState.currentCodeId && newState.callTree.attribution === oldState.callTree.attribution && newState.start === oldState.start && newState.end === oldState.end) { // No change, nothing to do. return; } } this.currentState = newState; this.currentState.timelineSize.width = width; this.currentState.timelineSize.height = height; this.element.style.display = "inherit"; let file = this.currentState.file; const minPixelsPerBucket = 10; const minTicksPerBucket = 8; let maxBuckets = Math.round(file.ticks.length / minTicksPerBucket); let bucketCount = Math.min( Math.round(width / minPixelsPerBucket), maxBuckets); // Make sure the canvas has the right dimensions. this.canvas.width = width; this.canvas.height = height; // Make space for the selection text. height -= this.imageOffset; 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 filter = filterFromFilterId(this.currentState.callTree.attribution); let stackProcessor = new CategorySampler(file, bucketCount, filter); generateTree(file, 0, Infinity, stackProcessor); let codeIdProcessor = new FunctionTimelineProcessor(currentCodeId, filter); 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; if (total > 0) { 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)); } } else { // No ticks fell into this bucket. Fill with "Unknown." for (let j = 0; j < bucketDescriptors.length; j++) { let desc = bucketDescriptors[j]; bucketData.push(desc.text === "Unknown" ? graphHeight : 0); } } bucketsGraph.push(bucketData); } // Draw the category graph into the buffer. let bucketWidth = width / (bucketsGraph.length - 1); let ctx = buffer.getContext('2d'); for (let i = 0; i < bucketsGraph.length - 1; i++) { let bucketData = bucketsGraph[i]; let nextBucketData = bucketsGraph[i + 1]; let x1 = Math.round(i * bucketWidth); let x2 = Math.round((i + 1) * bucketWidth); for (let j = 0; j < bucketData.length; j++) { ctx.beginPath(); ctx.moveTo(x1, j > 0 ? bucketData[j - 1] : 0); ctx.lineTo(x2, j > 0 ? nextBucketData[j - 1] : 0); 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)); } removeAllChildren(this.currentCode); if (currentCodeId) { let currentCode = file.code[currentCodeId]; this.currentCode.appendChild(document.createTextNode(currentCode.name)); } else { this.currentCode.appendChild(document.createTextNode("")); } } } 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"; removeAllChildren(this.element); 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, arrow) { arrow.textContent = EXPANDED_ARROW; let expandHandler = row.onclick; row.onclick = () => { let id = row.id; let index = row.rowIndex + 1; while (index < rows.rows.length && rows.rows[index].id.startsWith(id)) { rows.deleteRow(index); } arrow.textContent = COLLAPSED_ARROW; row.onclick = expandHandler; } } function expandDeoptInstances(row, arrow, 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, arrow); } function expandDeoptFunctionList(row, arrow, 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(createIndentNode(indent)); let childArrow = createArrowNode(); textCell.appendChild(childArrow); 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"; childArrow.textContent = COLLAPSED_ARROW; childRow.onclick = () => { expandDeoptInstances( childRow, childArrow, list[i].instances, indent + 1); }; } makeCollapsible(row, arrow); } function expandOptimizedFunctionList(row, arrow, 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, arrow); } function addExpandableRow(text, list, indent, kind) { let row = rows.insertRow(-1); row.id = "opt-table/" + kind + "/"; row.style.backgroundColor = CATEGORY_COLOR; let textCell = row.insertCell(-1); textCell.appendChild(createIndentNode(indent)); let arrow = createArrowNode(); textCell.appendChild(arrow); 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) { arrow.textContent = COLLAPSED_ARROW; if (kind === "opt") { row.onclick = () => { expandOptimizedFunctionList( row, arrow, list.functions, indent + 1, kind); }; } else { row.onclick = () => { expandDeoptFunctionList( row, arrow, list.functions, indent + 1, kind); }; } } return row; } addRow("Total function count:", stats.functionCount); addRow("Optimized function count:", stats.optimizedFunctionCount, 1); if (stats.turbopropOptimizedFunctionCount != 0) { addRow("Turboprop optimized function count:", stats.turbopropOptimizedFunctionCount, 1); } addRow("Deoptimized function count:", stats.deoptimizedFunctionCount, 2); addExpandableRow("Optimization count:", stats.optimizations, 0, "opt"); if (stats.turbopropOptimizedFunctionCount != 0) { addExpandableRow("Turboprop Optimization count:", stats.turbopropOptimizations, 0, "tp"); } 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"); if (stats.softBailouts.count != 0) { addExpandableRow("SoftBailout:", stats.softBailouts, 1, "softbailout"); } if (stats.eagerBailouts.count != 0) { addExpandableRow("EagerBailout:", stats.eagerBailouts, 1, "eagerbailout"); } table.appendChild(rows); this.element.appendChild(table); } } class ScriptSourceView { constructor() { this.table = $("source-viewer"); this.hideButton = $("source-viewer-hide-button"); this.hideButton.onclick = () => { main.setViewingSource(false); }; } render(newState) { let oldState = this.currentState; if (!newState.file || !newState.viewingSource) { this.table.style.display = "none"; this.hideButton.style.display = "none"; this.currentState = null; return; } if (oldState) { if (newState.file === oldState.file && newState.currentCodeId === oldState.currentCodeId && newState.viewingSource === oldState.viewingSource) { // No change, nothing to do. return; } } this.currentState = newState; this.table.style.display = "inline-block"; this.hideButton.style.display = "inline"; removeAllChildren(this.table); let functionId = this.currentState.file.code[this.currentState.currentCodeId].func; let sourceView = this.currentState.sourceData.generateSourceView(functionId); for (let i = 0; i < sourceView.source.length; i++) { let sampleCount = sourceView.lineSampleCounts[i] || 0; let sampleProportion = sourceView.samplesTotal > 0 ? sampleCount / sourceView.samplesTotal : 0; let heatBucket; if (sampleProportion === 0) { heatBucket = "line-none"; } else if (sampleProportion < 0.2) { heatBucket = "line-cold"; } else if (sampleProportion < 0.4) { heatBucket = "line-mediumcold"; } else if (sampleProportion < 0.6) { heatBucket = "line-mediumhot"; } else if (sampleProportion < 0.8) { heatBucket = "line-hot"; } else { heatBucket = "line-superhot"; } let row = this.table.insertRow(-1); let lineNumberCell = row.insertCell(-1); lineNumberCell.classList.add("source-line-number"); lineNumberCell.textContent = i + sourceView.firstLineNumber; let sampleCountCell = row.insertCell(-1); sampleCountCell.classList.add(heatBucket); sampleCountCell.textContent = sampleCount; let sourceLineCell = row.insertCell(-1); sourceLineCell.classList.add(heatBucket); sourceLineCell.textContent = sourceView.source[i]; } $("timeline-currentCode").scrollIntoView(); } } class SourceData { constructor(file) { this.scripts = new Map(); for (let i = 0; i < file.scripts.length; i++) { const scriptBlock = file.scripts[i]; if (scriptBlock === null) continue; // Array may be sparse. if (scriptBlock.source === undefined) continue; let source = scriptBlock.source.split("\n"); this.scripts.set(i, source); } this.functions = new Map(); for (let codeId = 0; codeId < file.code.length; ++codeId) { let codeBlock = file.code[codeId]; if (codeBlock.source && codeBlock.func !== undefined) { let data = this.functions.get(codeBlock.func); if (!data) { data = new FunctionSourceData(codeBlock.source.script, codeBlock.source.start, codeBlock.source.end); this.functions.set(codeBlock.func, data); } data.addSourceBlock(codeId, codeBlock.source); } } for (let tick of file.ticks) { let stack = tick.s; for (let i = 0; i < stack.length; i += 2) { let codeId = stack[i]; if (codeId < 0) continue; let functionId = file.code[codeId].func; if (this.functions.has(functionId)) { let codeOffset = stack[i + 1]; this.functions.get(functionId).addOffsetSample(codeId, codeOffset); } } } } getScript(scriptId) { return this.scripts.get(scriptId); } getLineForScriptOffset(script, scriptOffset) { let line = 0; let charsConsumed = 0; for (; line < script.length; ++line) { charsConsumed += script[line].length + 1; // Add 1 for newline. if (charsConsumed > scriptOffset) break; } return line; } hasSource(functionId) { return this.functions.has(functionId); } generateSourceView(functionId) { console.assert(this.hasSource(functionId)); let data = this.functions.get(functionId); let scriptId = data.scriptId; let script = this.getScript(scriptId); let firstLineNumber = this.getLineForScriptOffset(script, data.startScriptOffset); let lastLineNumber = this.getLineForScriptOffset(script, data.endScriptOffset); let lines = script.slice(firstLineNumber, lastLineNumber + 1); normalizeLeadingWhitespace(lines); let samplesTotal = 0; let lineSampleCounts = []; for (let [codeId, block] of data.codes) { block.offsets.forEach((sampleCount, codeOffset) => { let sourceOffset = block.positionTable.getScriptOffset(codeOffset); let lineNumber = this.getLineForScriptOffset(script, sourceOffset) - firstLineNumber; samplesTotal += sampleCount; lineSampleCounts[lineNumber] = (lineSampleCounts[lineNumber] || 0) + sampleCount; }); } return { source: lines, lineSampleCounts: lineSampleCounts, samplesTotal: samplesTotal, firstLineNumber: firstLineNumber + 1 // Source code is 1-indexed. }; } } class FunctionSourceData { constructor(scriptId, startScriptOffset, endScriptOffset) { this.scriptId = scriptId; this.startScriptOffset = startScriptOffset; this.endScriptOffset = endScriptOffset; this.codes = new Map(); } addSourceBlock(codeId, source) { this.codes.set(codeId, { positionTable: new SourcePositionTable(source.positions), offsets: [] }); } addOffsetSample(codeId, codeOffset) { let codeIdOffsets = this.codes.get(codeId).offsets; codeIdOffsets[codeOffset] = (codeIdOffsets[codeOffset] || 0) + 1; } } class SourcePositionTable { constructor(encodedTable) { this.offsetTable = []; let offsetPairRegex = /C([0-9]+)O([0-9]+)/g; while (true) { let regexResult = offsetPairRegex.exec(encodedTable); if (!regexResult) break; let codeOffset = parseInt(regexResult[1]); let scriptOffset = parseInt(regexResult[2]); if (isNaN(codeOffset) || isNaN(scriptOffset)) continue; this.offsetTable.push(codeOffset, scriptOffset); } } getScriptOffset(codeOffset) { console.assert(codeOffset >= 0); for (let i = this.offsetTable.length - 2; i >= 0; i -= 2) { if (this.offsetTable[i] <= codeOffset) { return this.offsetTable[i + 1]; } } return this.offsetTable[1]; } } class HelpView { constructor() { this.element = $("help"); } render(newState) { this.element.style.display = newState.file ? "none" : "inherit"; } }