dadbde038d
Rather than lumping in parsing, bytecode compilation and optimized compilation all into the same VM "compile" state, seperate them out into individual states. Additionally, add support for these states to tickprocessor and profview. Cq-Include-Trybots: master.tryserver.chromium.linux:linux_chromium_rel_ng Change-Id: I5be943e23cae042e32e9ccb24415c67c18658b4b Reviewed-on: https://chromium-review.googlesource.com/608973 Commit-Queue: Leszek Swirski <leszeks@chromium.org> Reviewed-by: Jaroslav Sevcik <jarin@chromium.org> Reviewed-by: Marja Hölttä <marja@chromium.org> Cr-Commit-Position: refs/heads/master@{#47283}
1253 lines
38 KiB
JavaScript
1253 lines
38 KiB
JavaScript
// 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());
|
|
components.push(new SummaryView());
|
|
components.push(new ModeBarView());
|
|
|
|
main.setMode("summary");
|
|
}
|
|
|
|
function emptyState() {
|
|
return {
|
|
file : null,
|
|
mode : "none",
|
|
currentCodeId : null,
|
|
start : 0,
|
|
end : Infinity,
|
|
timeLine : {
|
|
width : 100,
|
|
height : 100
|
|
},
|
|
callTree : {
|
|
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) {
|
|
|
|
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();
|
|
}
|
|
},
|
|
|
|
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();
|
|
}
|
|
},
|
|
|
|
setCurrentCode(codeId) {
|
|
if (codeId != main.currentState.currentCodeId) {
|
|
main.currentState = Object.assign({}, main.currentState);
|
|
main.currentState.currentCodeId = codeId;
|
|
main.delayRender();
|
|
}
|
|
},
|
|
|
|
onResize() {
|
|
main.setTimeLineDimensions(
|
|
Math.round(window.innerWidth - 20),
|
|
Math.round(window.innerHeight / 5));
|
|
},
|
|
|
|
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 : "#00ff00",
|
|
backgroundColor : "#c0ffc0",
|
|
text : "JS Optimized" },
|
|
{ kinds : [ "JSUNOPT", "BC" ],
|
|
color : "#ffb000",
|
|
backgroundColor : "#ffe0c0",
|
|
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 : [ "CPPPARSE" ],
|
|
color : "#b890f7",
|
|
backgroundColor : "#ebdeff",
|
|
text : "C++/Parser" },
|
|
{ kinds : [ "CPPCOMPBC" ],
|
|
color : "#52b0ce",
|
|
backgroundColor : "#a5c8d4",
|
|
text : "C++/Bytecode compiler" },
|
|
{ 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" }
|
|
];
|
|
|
|
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 "CPPPARSE":
|
|
return "C++ (parser)";
|
|
case "CPPCOMPBC":
|
|
return "C++ (bytecode compiler)";
|
|
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);
|
|
}
|
|
|
|
function 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 = codeTypeToText(type);
|
|
div.appendChild(span);
|
|
|
|
span = document.createElement("span");
|
|
span.classList.add("code-type-chip-space");
|
|
div.appendChild(span);
|
|
|
|
return div;
|
|
}
|
|
|
|
function isBytecodeHandler(kind) {
|
|
return kind === "BytecodeHandler";
|
|
}
|
|
|
|
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' || !isBytecodeHandler(kind);
|
|
}
|
|
}
|
|
|
|
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");
|
|
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 that = this;
|
|
let index = 0;
|
|
let id = "R/";
|
|
let row = tree.row;
|
|
let expander = tree.expander;
|
|
|
|
if (row) {
|
|
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 = [];
|
|
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 = 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();
|
|
let expander = createTableExpander(indent + 1);
|
|
nameCell.appendChild(expander);
|
|
nameCell.appendChild(createTypeDiv(node.type));
|
|
nameCell.appendChild(createFunctionNode(node.name, 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) {
|
|
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(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;
|
|
}
|
|
|
|
this.currentState = newState;
|
|
if (oldState) {
|
|
if (newState.timeLine.width === oldState.timeLine.width &&
|
|
newState.timeLine.height === oldState.timeLine.height &&
|
|
newState.file === oldState.file &&
|
|
newState.currentCodeId === oldState.currentCodeId &&
|
|
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;
|
|
let height = this.currentState.timeLine.height;
|
|
this.canvas.width = width;
|
|
this.canvas.height = height;
|
|
|
|
// Make space for the selection text.
|
|
height -= this.imageOffset;
|
|
|
|
let file = this.currentState.file;
|
|
if (!file) return;
|
|
|
|
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 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 codeIdProcessor = new FunctionTimelineProcessor(
|
|
currentCodeId,
|
|
filterFromFilterId(this.currentState.callTree.attribution));
|
|
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;
|
|
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));
|
|
}
|
|
bucketsGraph.push(bucketData);
|
|
}
|
|
|
|
// 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];
|
|
let nextBucketData = bucketsGraph[i + 1];
|
|
for (let j = 0; j < bucketData.length; j++) {
|
|
let x1 = Math.round(i * bucketWidth);
|
|
let x2 = Math.round((i + 1) * bucketWidth);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, j && bucketData[j - 1]);
|
|
ctx.lineTo(x2, j && nextBucketData[j - 1]);
|
|
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));
|
|
}
|
|
|
|
while (this.currentCode.firstChild) {
|
|
this.currentCode.removeChild(this.currentCode.firstChild);
|
|
}
|
|
if (currentCodeId) {
|
|
let currentCode = file.code[currentCodeId];
|
|
this.currentCode.appendChild(document.createTextNode(currentCode.name));
|
|
} else {
|
|
this.currentCode.appendChild(document.createTextNode("<none>"));
|
|
}
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
render(newState) {
|
|
this.element.style.display = newState.file ? "none" : "inherit";
|
|
}
|
|
}
|