Index: tools/profview/profview.js |
diff --git a/tools/profview/profview.js b/tools/profview/profview.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b45b4bc1d29015aed211cc0aa6dede88ca82d403 |
--- /dev/null |
+++ b/tools/profview/profview.js |
@@ -0,0 +1,812 @@ |
+// 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()); |
+ |
+ let modeBar = $("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.callTree.mode === id) return; |
+ let old = $("mode-" + main.currentState.callTree.mode); |
+ old.classList = "mode-button"; |
+ div.classList = "mode-button active-mode-button"; |
+ main.setMode(id); |
+ }; |
+ modeBar.appendChild(div); |
+ } |
+ |
+ addMode("bottom-up", "Bottom up", true); |
+ addMode("top-down", "Top down"); |
+ addMode("function-list", "Functions"); |
+ |
+ main.setMode("bottom-up"); |
+} |
+ |
+function emptyState() { |
+ return { |
+ file : null, |
+ start : 0, |
+ end : Infinity, |
+ timeLine : { |
+ width : 100, |
+ height : 100 |
+ }, |
+ callTree : { |
+ mode : "none", |
+ 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) { |
+ let callTreeState = Object.assign({}, main.currentState.callTree); |
+ callTreeState.mode = mode; |
+ switch (mode) { |
+ case "bottom-up": |
+ callTreeState.attribution = "js-exclude-bc"; |
+ callTreeState.categories = "code-type"; |
+ callTreeState.sort = "time"; |
+ break; |
+ case "top-down": |
+ callTreeState.attribution = "js-exclude-bc"; |
+ callTreeState.categories = "none"; |
+ callTreeState.sort = "time"; |
+ break; |
+ case "function-list": |
+ callTreeState.attribution = "js-exclude-bc"; |
+ callTreeState.categories = "none"; |
+ callTreeState.sort = "own-time"; |
+ break; |
+ default: |
+ console.error("Invalid mode"); |
+ } |
+ main.currentState = setCallTreeState(main.currentState, callTreeState); |
+ 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(); |
+ } |
+ }, |
+ |
+ onResize() { |
+ main.setTimeLineDimensions( |
+ window.innerWidth - 20, window.innerHeight / 8); |
+ }, |
+ |
+ 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 : "#ffb000", |
+ backgroundColor : "#ffe0c0", |
+ text : "JS Optimized" }, |
+ { kinds : [ "JSUNOPT", "BC" ], |
+ color : "#00ff00", |
+ backgroundColor : "#c0ffc0", |
+ 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 : [ "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" } |
+ ]; |
+ |
+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; |
+} |
+ |
+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; |
+ } |
+ |
+ 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' || !CallTreeView.IsBytecodeHandler(kind); |
+ } |
+ } |
+ |
+ sortFromId(id) { |
+ switch (id) { |
+ case "time": |
+ return (c1, c2) => c2.ticks - c1.ticks; |
+ case "own-time": |
+ return (c1, c2) => c2.ownTicks - c1.ownTicks; |
+ 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; |
+ }; |
+ } |
+ } |
+ |
+ static IsBytecodeHandler(kind) { |
+ return kind === "BytecodeHandler"; |
+ } |
+ |
+ createExpander(indent) { |
+ let div = document.createElement("div"); |
+ div.style.width = (1 + indent) + "em"; |
+ div.style.display = "inline-block"; |
+ div.style.textAlign = "right"; |
+ return div; |
+ } |
+ |
+ codeTypeToText(type) { |
+ switch (type) { |
+ case "UNKNOWN": |
+ return "Unknown"; |
+ 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); |
+ } |
+ |
+ 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 = this.codeTypeToText(type); |
+ div.appendChild(span); |
+ |
+ span = document.createElement("span"); |
+ span.classList.add("code-type-chip-space"); |
+ div.appendChild(span); |
+ |
+ return div; |
+ } |
+ |
+ expandTree(tree, indent) { |
+ let that = this; |
+ let index = 0; |
+ let id = "R/"; |
+ let row = tree.row; |
+ let expander = tree.expander; |
+ |
+ if (row) { |
+ console.assert("expander"); |
+ 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 = []; |
+ for (let child in tree.children) { |
+ if (tree.children[child].ticks > 0) { |
+ children.push(tree.children[child]); |
+ } |
+ } |
+ 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.callTree.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 = this.createExpander(indent); |
+ nameCell.appendChild(expander); |
+ nameCell.appendChild(this.createTypeDiv(node.type)); |
+ nameCell.appendChild(document.createTextNode(node.name)); |
+ |
+ // Inclusive ticks cell. |
+ c = row.insertCell(); |
+ c.textContent = node.ticks; |
+ c.style.textAlign = "right"; |
+ if (this.currentState.callTree.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(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 (calltree.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" } |
+ ], 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 : "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"); |
+ } |
+ |
+ render(newState) { |
+ let oldState = this.currentState; |
+ if (!newState.file) { |
+ this.element.style.display = "none"; |
+ return; |
+ } |
+ |
+ this.currentState = newState; |
+ if (oldState) { |
+ if (newState.file === oldState.file && |
+ newState.start === oldState.start && |
+ newState.end === oldState.end && |
+ newState.callTree.mode === oldState.callTree.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.callTree.mode; |
+ if (!oldState || mode !== oldState.callTree.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.callTree); |
+ } |
+ |
+ let inclusiveDisplay = (mode === "bottom-up") ? "none" : "inherit"; |
+ let ownTimeTh = $(this.treeElement.id + "-own-time-header"); |
+ ownTimeTh.style.display = inclusiveDisplay; |
+ let ownTicksTh = $(this.treeElement.id + "-own-ticks-header"); |
+ ownTicksTh.style.display = inclusiveDisplay; |
+ |
+ // Build the tree. |
+ let stackProcessor; |
+ let filter = this.filterFromFilterId(this.currentState.callTree.attribution); |
+ if (mode === "top-down") { |
+ stackProcessor = |
+ new PlainCallTreeProcessor(filter, false); |
+ } else if (mode === "function-list") { |
+ stackProcessor = |
+ new FunctionListTree(filter); |
+ |
+ } 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.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.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, 0); |
+ } 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"); |
+ ctx.drawImage(this.buffer, 0, 0); |
+ |
+ if (this.selectionStart !== null && this.selectionEnd !== null) { |
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)"; |
+ let left = Math.min(this.selectionStart, this.selectionEnd); |
+ let right = Math.max(this.selectionStart, this.selectionEnd); |
+ ctx.fillRect(0, 0, left, this.buffer.height); |
+ ctx.fillRect(right, 0, this.buffer.width - right, this.buffer.height); |
+ } |
+ } |
+ |
+ |
+ 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.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; |
+ this.canvas.width = width; |
+ this.canvas.height = this.currentState.timeLine.height; |
+ |
+ let file = this.currentState.file; |
+ if (!file) return; |
+ |
+ 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 buffer = document.createElement("canvas"); |
+ |
+ buffer.width = this.canvas.width; |
+ buffer.height = this.canvas.height; |
+ |
+ // Calculate the bar heights for each bucket. |
+ let graphHeight = buffer.height; |
+ 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(graphHeight * sum / total); |
+ } |
+ bucketsGraph.push(bucketData); |
+ } |
+ |
+ // Draw the graph into the buffer. |
+ let bucketWidth = width / bucketCount; |
+ 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++) { |
+ ctx.beginPath(); |
+ ctx.moveTo(i * bucketWidth, j && bucketData[j - 1]); |
+ ctx.lineTo((i + 1) * bucketWidth, j && nextBucketData[j - 1]); |
+ ctx.lineTo((i + 1) * bucketWidth, nextBucketData[j]); |
+ ctx.lineTo(i * bucketWidth, bucketData[j]); |
+ ctx.closePath(); |
+ ctx.fillStyle = bucketDescriptors[j].color; |
+ 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)); |
+ } |
+ } |
+} |
+ |
+class HelpView { |
+ constructor() { |
+ this.element = $("help"); |
+ } |
+ |
+ render(newState) { |
+ this.element.style.display = newState.file ? "none" : "inherit"; |
+ } |
+} |