Index: tools/profview/profview.js |
diff --git a/tools/profview/profview.js b/tools/profview/profview.js |
index da80385de6889289b9ed3742f94c3fc6ce5b5b95..aefdcd6e0cd446b949ed35bf79fbf545f39b1938 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") { |
+ 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; |
@@ -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"); |