| 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");
|
|
|