Chromium Code Reviews| Index: tools/profview/profview.js |
| diff --git a/tools/profview/profview.js b/tools/profview/profview.js |
| index da80385de6889289b9ed3742f94c3fc6ce5b5b95..aaf45644de42a1d02473e026110ed45a5446b615 100644 |
| --- a/tools/profview/profview.js |
| +++ b/tools/profview/profview.js |
| @@ -14,34 +14,17 @@ function createViews() { |
| components.push(new CallTreeView()); |
| components.push(new TimelineView()); |
| components.push(new HelpView()); |
| + components.push(new SummaryView()); |
| + components.push(new ModeBarView()); |
| - 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"); |
| + main.setMode("summary"); |
| } |
| function emptyState() { |
| return { |
| file : null, |
| + mode : "none", |
| + currentCodeId : null, |
| start : 0, |
| end : Infinity, |
| timeLine : { |
| @@ -49,7 +32,6 @@ function emptyState() { |
| height : 100 |
| }, |
| callTree : { |
| - mode : "none", |
| attribution : "js-exclude-bc", |
| categories : "code-type", |
| sort : "time" |
| @@ -68,28 +50,35 @@ let main = { |
| setMode(mode) { |
| if (mode != main.currentState.mode) { |
| - let callTreeState = Object.assign({}, main.currentState.callTree); |
| - callTreeState.mode = 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": |
| - callTreeState.attribution = "js-exclude-bc"; |
| - callTreeState.categories = "code-type"; |
| - callTreeState.sort = "time"; |
| + state.callTree = |
| + setCallTreeModifiers("js-exclude-bc", "code-type", "time"); |
| break; |
| case "top-down": |
| - callTreeState.attribution = "js-exclude-bc"; |
| - callTreeState.categories = "none"; |
| - callTreeState.sort = "time"; |
| + state.callTree = |
| + setCallTreeModifiers("js-exclude-bc", "none", "time"); |
| break; |
| case "function-list": |
| - callTreeState.attribution = "js-exclude-bc"; |
| - callTreeState.categories = "code-type"; |
| - callTreeState.sort = "own-time"; |
| + state.callTree = |
| + setCallTreeModifiers("js-exclude-bc", "code-type", "own-time"); |
| break; |
| - default: |
| - console.error("Invalid mode"); |
| } |
| - main.currentState = setCallTreeState(main.currentState, callTreeState); |
| + |
| + state.mode = mode; |
| + |
| + main.currentState = state; |
| main.delayRender(); |
| } |
| }, |
| @@ -201,12 +190,12 @@ let main = { |
| let bucketDescriptors = |
| [ { kinds : [ "JSOPT" ], |
| - color : "#ffb000", |
| - backgroundColor : "#ffe0c0", |
| - text : "JS Optimized" }, |
| - { kinds : [ "JSUNOPT", "BC" ], |
| color : "#00ff00", |
| backgroundColor : "#c0ffc0", |
| + text : "JS Optimized" }, |
| + { kinds : [ "JSUNOPT", "BC" ], |
| + color : "#ffb000", |
| + backgroundColor : "#ffe0c0", |
| text : "JS Unoptimized" }, |
| { kinds : [ "IC" ], |
| color : "#ffff00", |
| @@ -325,6 +314,27 @@ function filterFromFilterId(id) { |
| } |
| } |
| +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"); |
| @@ -377,27 +387,6 @@ class CallTreeView { |
| } |
| } |
| - createExpander(indent) { |
| - let div = document.createElement("div"); |
| - div.style.width = (1 + indent) + "em"; |
| - div.style.display = "inline-block"; |
| - div.style.textAlign = "right"; |
| - return div; |
| - } |
| - |
| - 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; |
| - } |
| - |
| expandTree(tree, indent) { |
| let that = this; |
| let index = 0; |
| @@ -406,7 +395,6 @@ class CallTreeView { |
| let expander = tree.expander; |
| if (row) { |
| - console.assert("expander"); |
| index = row.rowIndex; |
| id = row.id; |
| @@ -452,7 +440,7 @@ class CallTreeView { |
| c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%"; |
| c.style.textAlign = "right"; |
| // Exclusive time % cell. |
| - if (this.currentState.callTree.mode !== "bottom-up") { |
| + if (this.currentState.mode !== "bottom-up") { |
| c = row.insertCell(-1); |
| c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%"; |
| c.style.textAlign = "right"; |
| @@ -460,16 +448,16 @@ class CallTreeView { |
| // Create the name cell. |
| let nameCell = row.insertCell(); |
| - let expander = this.createExpander(indent); |
| + let expander = createTableExpander(indent + 1); |
| nameCell.appendChild(expander); |
| nameCell.appendChild(createTypeDiv(node.type)); |
| - nameCell.appendChild(this.createFunctionNode(node.name, node.codeId)); |
| + nameCell.appendChild(createFunctionNode(node.name, node.codeId)); |
| // Inclusive ticks cell. |
| c = row.insertCell(); |
| c.textContent = node.ticks; |
| c.style.textAlign = "right"; |
| - if (this.currentState.callTree.mode !== "bottom-up") { |
| + if (this.currentState.mode !== "bottom-up") { |
| // Exclusive ticks cell. |
| c = row.insertCell(-1); |
| c.textContent = node.ownTicks; |
| @@ -500,7 +488,7 @@ class CallTreeView { |
| expander.onclick = expandHandler; |
| } |
| - fillSelects(calltree) { |
| + fillSelects(mode, calltree) { |
| function addOptions(e, values, current) { |
| while (e.options.length > 0) { |
| e.remove(0); |
| @@ -523,7 +511,7 @@ class CallTreeView { |
| text : "Attribute non-functions to JS functions" } |
| ]; |
| - switch (calltree.mode) { |
| + switch (mode) { |
| case "bottom-up": |
| addOptions(this.selectAttribution, attributions, calltree.attribution); |
| addOptions(this.selectCategories, [ |
| @@ -564,10 +552,22 @@ class CallTreeView { |
| 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) { |
| + if (!newState.file || !CallTreeView.isCallTreeMode(newState.mode)) { |
| this.element.style.display = "none"; |
| + this.currentState = null; |
| return; |
| } |
| @@ -576,7 +576,7 @@ class CallTreeView { |
| if (newState.file === oldState.file && |
| newState.start === oldState.start && |
| newState.end === oldState.end && |
| - newState.callTree.mode === oldState.callTree.mode && |
| + newState.mode === oldState.mode && |
| newState.callTree.attribution === oldState.callTree.attribution && |
| newState.callTree.categories === oldState.callTree.categories && |
| newState.callTree.sort === oldState.callTree.sort) { |
| @@ -587,12 +587,12 @@ class CallTreeView { |
| this.element.style.display = "inherit"; |
| - let mode = this.currentState.callTree.mode; |
| - if (!oldState || mode !== oldState.callTree.mode) { |
| + 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.callTree); |
| + this.fillSelects(newState.mode, newState.callTree); |
| } |
| let ownTimeClass = (mode === "bottom-up") ? "numeric-hidden" : "numeric"; |
| @@ -661,7 +661,8 @@ class TimelineView { |
| this.fontSize = 12; |
| this.imageOffset = Math.round(this.fontSize * 1.2); |
| - this.functionTimelineHeight = 16; |
| + this.functionTimelineHeight = 24; |
| + this.functionTimelineTickHeight = 16; |
| this.currentState = null; |
| } |
| @@ -864,8 +865,8 @@ class TimelineView { |
| bucketsGraph.push(bucketData); |
| } |
| - // Draw the graph into the buffer. |
| - let bucketWidth = width / (bucketsGraph.length - 1); |
| + // 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]; |
| @@ -883,25 +884,30 @@ class TimelineView { |
| ctx.fill(); |
| } |
| } |
| + |
| + // Draw the function ticks. |
| let functionTimelineYOffset = graphHeight; |
| - let functionTimelineHeight = this.functionTimelineHeight; |
| - let functionTimelineHalfHeight = Math.round(functionTimelineHeight / 2); |
| + 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, |
| - functionTimelineHeight); |
| + 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( |
| - Math.round((block.start - firstTime) * timestampScaler), |
| + timestampToX(block.start), |
| functionTimelineYOffset, |
| Math.max(1, Math.round((block.end - block.start) * timestampScaler)), |
| - block.topOfStack ? functionTimelineHeight : functionTimelineHalfHeight); |
| + block.topOfStack ? |
| + functionTimelineTickHeight : functionTimelineHalfHeight); |
| } |
| ctx.strokeStyle = "black"; |
| ctx.lineWidth = "1"; |
| @@ -917,6 +923,53 @@ class TimelineView { |
| 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") { |
| + // Draw optimization mark. |
| + let x = timestampToX(code.tm); |
| + ctx.lineWidth = 0.7; |
| + ctx.strokeStyle = "blue"; |
| + ctx.beginPath(); |
| + ctx.moveTo(x - 3, y - 3); |
|
Leszek Swirski
2017/03/21 14:28:39
maybe we should extract out a "draw line" method,
|
| + ctx.lineTo(x, y); |
| + ctx.stroke(); |
| + ctx.beginPath(); |
| + ctx.moveTo(x - 3, y + 3); |
| + ctx.lineTo(x, y); |
| + ctx.stroke(); |
| + 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(); |
| + } |
| + } 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; |
| @@ -958,6 +1011,218 @@ class TimelineView { |
| } |
| } |
| +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"); |