v8/tools/profview/profview.js
Leszek Swirski d92705e53b [profview] Use requestAnimationFrame to schedule a render
This is more idiomatic than using a Promise.

Change-Id: I61443e90ef1c0c589284851ea9be410153919eb5
Reviewed-on: https://chromium-review.googlesource.com/c/456278
Reviewed-by: Jaroslav Sevcik <jarin@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/master@{#58828}
2019-01-15 13:08:46 +00:00

1513 lines
46 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);
}
function removeAllChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
let components;
function createViews() {
components = [
new CallTreeView(),
new TimelineView(),
new HelpView(),
new SummaryView(),
new ModeBarView(),
new ScriptSourceView(),
];
}
function emptyState() {
return {
file : null,
mode : null,
currentCodeId : null,
viewingSource: false,
start : 0,
end : Infinity,
timelineSize : {
width : 0,
height : 0
},
callTree : {
attribution : "js-exclude-bc",
categories : "code-type",
sort : "time"
},
sourceData: null
};
}
function setCallTreeState(state, callTreeState) {
state = Object.assign({}, state);
state.callTree = callTreeState;
return state;
}
let main = {
currentState : emptyState(),
renderPending : false,
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();
}
},
updateSources(file) {
let statusDiv = $("source-status");
if (!file) {
statusDiv.textContent = "";
return;
}
if (!file.scripts || file.scripts.length === 0) {
statusDiv.textContent =
"Script source not available. Run profiler with --log-source-code.";
return;
}
statusDiv.textContent = "Script source is available.";
main.currentState.sourceData = new SourceData(file);
},
setFile(file) {
if (file !== main.currentState.file) {
let lastMode = main.currentState.mode || "summary";
main.currentState = emptyState();
main.currentState.file = file;
main.updateSources(file);
main.setMode(lastMode);
main.delayRender();
}
},
setCurrentCode(codeId) {
if (codeId !== main.currentState.currentCodeId) {
main.currentState = Object.assign({}, main.currentState);
main.currentState.currentCodeId = codeId;
main.delayRender();
}
},
setViewingSource(value) {
if (main.currentState.viewingSource !== value) {
main.currentState = Object.assign({}, main.currentState);
main.currentState.viewingSource = value;
main.delayRender();
}
},
onResize() {
main.delayRender();
},
onLoad() {
function loadHandler(evt) {
let f = evt.target.files[0];
if (f) {
let reader = new FileReader();
reader.onload = function(event) {
main.setFile(JSON.parse(event.target.result));
};
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();
},
delayRender() {
if (main.renderPending) return;
main.renderPending = true;
window.requestAnimationFrame(() => {
main.renderPending = false;
for (let c of components) {
c.render(main.currentState);
}
});
}
};
const CATEGORY_COLOR = "#f5f5f5";
const bucketDescriptors =
[ { kinds : [ "JSOPT" ],
color : "#64dd17",
backgroundColor : "#80e27e",
text : "JS Optimized" },
{ kinds : [ "JSUNOPT", "BC" ],
color : "#dd2c00",
backgroundColor : "#ff9e80",
text : "JS Unoptimized" },
{ kinds : [ "IC" ],
color : "#ff6d00",
backgroundColor : "#ffab40",
text : "IC" },
{ kinds : [ "STUB", "BUILTIN", "REGEXP" ],
color : "#ffd600",
backgroundColor : "#ffea00",
text : "Other generated" },
{ kinds : [ "CPP", "LIB" ],
color : "#304ffe",
backgroundColor : "#6ab7ff",
text : "C++" },
{ kinds : [ "CPPEXT" ],
color : "#003c8f",
backgroundColor : "#c0cfff",
text : "C++/external" },
{ kinds : [ "CPPPARSE" ],
color : "#aa00ff",
backgroundColor : "#ffb2ff",
text : "C++/Parser" },
{ kinds : [ "CPPCOMPBC" ],
color : "#43a047",
backgroundColor : "#88c399",
text : "C++/Bytecode compiler" },
{ kinds : [ "CPPCOMP" ],
color : "#00e5ff",
backgroundColor : "#6effff",
text : "C++/Compiler" },
{ kinds : [ "CPPGC" ],
color : "#6200ea",
backgroundColor : "#e1bee7",
text : "C++/GC" },
{ kinds : [ "UNKNOWN" ],
color : "#bdbdbd",
backgroundColor : "#efefef",
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++ GC";
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 createTypeNode(type) {
if (type === "CAT") {
return document.createTextNode("");
}
let span = document.createElement("span");
span.classList.add("code-type-chip");
span.textContent = codeTypeToText(type);
return span;
}
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' || kind !== "BytecodeHandler";
}
}
function createIndentNode(indent) {
let div = document.createElement("div");
div.style.display = "inline-block";
div.style.width = (indent + 0.5) + "em";
return div;
}
function createArrowNode() {
let span = document.createElement("span");
span.classList.add("tree-row-arrow");
return span;
}
function createFunctionNode(name, codeId) {
let nameElement = document.createElement("span");
nameElement.appendChild(document.createTextNode(name));
nameElement.classList.add("tree-row-name");
if (codeId !== -1) {
nameElement.classList.add("codeid-link");
nameElement.onclick = (event) => {
main.setCurrentCode(codeId);
// Prevent the click from bubbling to the row and causing it to
// collapse/expand.
event.stopPropagation();
};
}
return nameElement;
}
function createViewSourceNode(codeId) {
let linkElement = document.createElement("span");
linkElement.appendChild(document.createTextNode("View source"));
linkElement.classList.add("view-source-link");
linkElement.onclick = (event) => {
main.setCurrentCode(codeId);
main.setViewingSource(true);
// Prevent the click from bubbling to the row and causing it to
// collapse/expand.
event.stopPropagation();
};
return linkElement;
}
const COLLAPSED_ARROW = "\u25B6";
const EXPANDED_ARROW = "\u25BC";
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 index = 0;
let id = "R/";
let row = tree.row;
if (row) {
index = row.rowIndex;
id = row.id;
tree.arrow.textContent = EXPANDED_ARROW;
// Collapse the children when the row is clicked again.
let expandHandler = row.onclick;
row.onclick = () => {
this.collapseRow(tree, 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 = CATEGORY_COLOR;
} else {
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();
nameCell.appendChild(createIndentNode(indent + 1));
let arrow = createArrowNode();
nameCell.appendChild(arrow);
nameCell.appendChild(createTypeNode(node.type));
nameCell.appendChild(createFunctionNode(node.name, node.codeId));
if (main.currentState.sourceData &&
node.codeId >= 0 &&
main.currentState.sourceData.hasSource(
this.currentState.file.code[node.codeId].func)) {
nameCell.appendChild(createViewSourceNode(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) {
arrow.textContent = COLLAPSED_ARROW;
row.onclick = () => { this.expandTree(node, indent + 1); };
}
node.row = row;
node.arrow = arrow;
index++;
}
}
collapseRow(tree, 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);
}
tree.arrow.textContent = COLLAPSED_ARROW;
row.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;
}
let width = Math.round(document.documentElement.clientWidth - 20);
let height = Math.round(document.documentElement.clientHeight / 5);
if (oldState) {
if (width === oldState.timelineSize.width &&
height === oldState.timelineSize.height &&
newState.file === oldState.file &&
newState.currentCodeId === oldState.currentCodeId &&
newState.start === oldState.start &&
newState.end === oldState.end) {
// No change, nothing to do.
return;
}
}
this.currentState = newState;
this.currentState.timelineSize.width = width;
this.currentState.timelineSize.height = height;
this.element.style.display = "inherit";
let file = this.currentState.file;
const minPixelsPerBucket = 10;
const minTicksPerBucket = 8;
let maxBuckets = Math.round(file.ticks.length / minTicksPerBucket);
let bucketCount = Math.min(
Math.round(width / minPixelsPerBucket), maxBuckets);
// Make sure the canvas has the right dimensions.
this.canvas.width = width;
this.canvas.height = height;
// Make space for the selection text.
height -= this.imageOffset;
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 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;
if (total > 0) {
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));
}
} else {
// No ticks fell into this bucket. Fill with "Unknown."
for (let j = 0; j < bucketDescriptors.length; j++) {
let desc = bucketDescriptors[j];
bucketData.push(desc.text === "Unknown" ? graphHeight : 0);
}
}
bucketsGraph.push(bucketData);
}
// Draw the category graph into the buffer.
let bucketWidth = width / (bucketsGraph.length - 1);
let ctx = buffer.getContext('2d');
for (let i = 0; i < bucketsGraph.length - 1; i++) {
let bucketData = bucketsGraph[i];
let nextBucketData = bucketsGraph[i + 1];
let x1 = Math.round(i * bucketWidth);
let x2 = Math.round((i + 1) * bucketWidth);
for (let j = 0; j < bucketData.length; j++) {
ctx.beginPath();
ctx.moveTo(x1, j > 0 ? bucketData[j - 1] : 0);
ctx.lineTo(x2, j > 0 ? nextBucketData[j - 1] : 0);
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));
}
removeAllChildren(this.currentCode);
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";
removeAllChildren(this.element);
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, arrow) {
arrow.textContent = EXPANDED_ARROW;
let expandHandler = row.onclick;
row.onclick = () => {
let id = row.id;
let index = row.rowIndex + 1;
while (index < rows.rows.length &&
rows.rows[index].id.startsWith(id)) {
rows.deleteRow(index);
}
arrow.textContent = COLLAPSED_ARROW;
row.onclick = expandHandler;
}
}
function expandDeoptInstances(row, arrow, 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, arrow);
}
function expandDeoptFunctionList(row, arrow, 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(createIndentNode(indent));
let childArrow = createArrowNode();
textCell.appendChild(childArrow);
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";
childArrow.textContent = COLLAPSED_ARROW;
childRow.onclick = () => {
expandDeoptInstances(
childRow, childArrow, list[i].instances, indent + 1);
};
}
makeCollapsible(row, arrow);
}
function expandOptimizedFunctionList(row, arrow, 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, arrow);
}
function addExpandableRow(text, list, indent, kind) {
let row = rows.insertRow(-1);
row.id = "opt-table/" + kind + "/";
row.style.backgroundColor = CATEGORY_COLOR;
let textCell = row.insertCell(-1);
textCell.appendChild(createIndentNode(indent));
let arrow = createArrowNode();
textCell.appendChild(arrow);
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) {
arrow.textContent = COLLAPSED_ARROW;
if (kind === "opt") {
row.onclick = () => {
expandOptimizedFunctionList(
row, arrow, list.functions, indent + 1, kind);
};
} else {
row.onclick = () => {
expandDeoptFunctionList(
row, arrow, 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 ScriptSourceView {
constructor() {
this.table = $("source-viewer");
this.hideButton = $("source-viewer-hide-button");
this.hideButton.onclick = () => {
main.setViewingSource(false);
};
}
render(newState) {
let oldState = this.currentState;
if (!newState.file || !newState.viewingSource) {
this.table.style.display = "none";
this.hideButton.style.display = "none";
this.currentState = null;
return;
}
if (oldState) {
if (newState.file === oldState.file &&
newState.currentCodeId === oldState.currentCodeId &&
newState.viewingSource === oldState.viewingSource) {
// No change, nothing to do.
return;
}
}
this.currentState = newState;
this.table.style.display = "inline-block";
this.hideButton.style.display = "inline";
removeAllChildren(this.table);
let functionId =
this.currentState.file.code[this.currentState.currentCodeId].func;
let sourceView =
this.currentState.sourceData.generateSourceView(functionId);
for (let i = 0; i < sourceView.source.length; i++) {
let sampleCount = sourceView.lineSampleCounts[i] || 0;
let sampleProportion = sourceView.samplesTotal > 0 ?
sampleCount / sourceView.samplesTotal : 0;
let heatBucket;
if (sampleProportion === 0) {
heatBucket = "line-none";
} else if (sampleProportion < 0.2) {
heatBucket = "line-cold";
} else if (sampleProportion < 0.4) {
heatBucket = "line-mediumcold";
} else if (sampleProportion < 0.6) {
heatBucket = "line-mediumhot";
} else if (sampleProportion < 0.8) {
heatBucket = "line-hot";
} else {
heatBucket = "line-superhot";
}
let row = this.table.insertRow(-1);
let lineNumberCell = row.insertCell(-1);
lineNumberCell.classList.add("source-line-number");
lineNumberCell.textContent = i + sourceView.firstLineNumber;
let sampleCountCell = row.insertCell(-1);
sampleCountCell.classList.add(heatBucket);
sampleCountCell.textContent = sampleCount;
let sourceLineCell = row.insertCell(-1);
sourceLineCell.classList.add(heatBucket);
sourceLineCell.textContent = sourceView.source[i];
}
$("timeline-currentCode").scrollIntoView();
}
}
class SourceData {
constructor(file) {
this.scripts = new Map();
for (let i = 0; i < file.scripts.length; i++) {
const scriptBlock = file.scripts[i];
if (scriptBlock === null) continue; // Array may be sparse.
let source = scriptBlock.source.split("\n");
this.scripts.set(i, source);
}
this.functions = new Map();
for (let codeId = 0; codeId < file.code.length; ++codeId) {
let codeBlock = file.code[codeId];
if (codeBlock.source && codeBlock.func !== undefined) {
let data = this.functions.get(codeBlock.func);
if (!data) {
data = new FunctionSourceData(codeBlock.source.script,
codeBlock.source.start,
codeBlock.source.end);
this.functions.set(codeBlock.func, data);
}
data.addSourceBlock(codeId, codeBlock.source);
}
}
for (let tick of file.ticks) {
let stack = tick.s;
for (let i = 0; i < stack.length; i += 2) {
let codeId = stack[i];
if (codeId < 0) continue;
let functionId = file.code[codeId].func;
if (this.functions.has(functionId)) {
let codeOffset = stack[i + 1];
this.functions.get(functionId).addOffsetSample(codeId, codeOffset);
}
}
}
}
getScript(scriptId) {
return this.scripts.get(scriptId);
}
getLineForScriptOffset(script, scriptOffset) {
let line = 0;
let charsConsumed = 0;
for (; line < script.length; ++line) {
charsConsumed += script[line].length + 1; // Add 1 for newline.
if (charsConsumed > scriptOffset) break;
}
return line;
}
hasSource(functionId) {
return this.functions.has(functionId);
}
generateSourceView(functionId) {
console.assert(this.hasSource(functionId));
let data = this.functions.get(functionId);
let scriptId = data.scriptId;
let script = this.getScript(scriptId);
let firstLineNumber =
this.getLineForScriptOffset(script, data.startScriptOffset);
let lastLineNumber =
this.getLineForScriptOffset(script, data.endScriptOffset);
let lines = script.slice(firstLineNumber, lastLineNumber + 1);
normalizeLeadingWhitespace(lines);
let samplesTotal = 0;
let lineSampleCounts = [];
for (let [codeId, block] of data.codes) {
block.offsets.forEach((sampleCount, codeOffset) => {
let sourceOffset = block.positionTable.getScriptOffset(codeOffset);
let lineNumber =
this.getLineForScriptOffset(script, sourceOffset) - firstLineNumber;
samplesTotal += sampleCount;
lineSampleCounts[lineNumber] =
(lineSampleCounts[lineNumber] || 0) + sampleCount;
});
}
return {
source: lines,
lineSampleCounts: lineSampleCounts,
samplesTotal: samplesTotal,
firstLineNumber: firstLineNumber + 1 // Source code is 1-indexed.
};
}
}
class FunctionSourceData {
constructor(scriptId, startScriptOffset, endScriptOffset) {
this.scriptId = scriptId;
this.startScriptOffset = startScriptOffset;
this.endScriptOffset = endScriptOffset;
this.codes = new Map();
}
addSourceBlock(codeId, source) {
this.codes.set(codeId, {
positionTable: new SourcePositionTable(source.positions),
offsets: []
});
}
addOffsetSample(codeId, codeOffset) {
let codeIdOffsets = this.codes.get(codeId).offsets;
codeIdOffsets[codeOffset] = (codeIdOffsets[codeOffset] || 0) + 1;
}
}
class SourcePositionTable {
constructor(encodedTable) {
this.offsetTable = [];
let offsetPairRegex = /C([0-9]+)O([0-9]+)/g;
while (true) {
let regexResult = offsetPairRegex.exec(encodedTable);
if (!regexResult) break;
let codeOffset = parseInt(regexResult[1]);
let scriptOffset = parseInt(regexResult[2]);
if (isNaN(codeOffset) || isNaN(scriptOffset)) continue;
this.offsetTable.push(codeOffset, scriptOffset);
}
}
getScriptOffset(codeOffset) {
console.assert(codeOffset >= 0);
for (let i = this.offsetTable.length - 2; i >= 0; i -= 2) {
if (this.offsetTable[i] <= codeOffset) {
return this.offsetTable[i + 1];
}
}
return this.offsetTable[1];
}
}
class HelpView {
constructor() {
this.element = $("help");
}
render(newState) {
this.element.style.display = newState.file ? "none" : "inherit";
}
}