v8/tools/profview/profview.js
Leszek Swirski 9bfc84f18f [tools] Use code attribution in timeline view
Reflect the code attribution (in particular, "attribute non-functions to
JS functions") in the timeline view, which visualises the distribution
of each code kind. This allows easier visualisation of which tiers are
active, ignoring stubs that are shared between tiers (like ICs).

Change-Id: I1f2818ffd4e466ce18c01627865186e6a94e2bed
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4105021
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Camillo Bruni <cbruni@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#84829}
2022-12-14 10:04:50 +00:00

1575 lines
47 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: ["JS_IGNITION", "BC"],
color: "#dd2c00",
backgroundColor: "#ff9e80",
text: "JS Ignition"
},
{
kinds: ["JS_SPARKPLUG"],
color: "#b3005b",
backgroundColor: "#ff9e80",
text: "JS Sparkplug"
},
{
kinds: ["JS_MAGLEV"],
color: "#693eb8",
backgroundColor: "#d80093",
text: "JS Maglev"
},
{
kinds: ["JS_TURBOFAN"],
color: "#64dd17",
backgroundColor: "#80e27e",
text: "JS Turbofan"
},
{
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: ["CPP_EXT"],
color: "#003c8f",
backgroundColor: "#c0cfff",
text: "C++/external"
},
{
kinds: ["CPP_PARSE"],
color: "#aa00ff",
backgroundColor: "#ffb2ff",
text: "C++/Parser"
},
{
kinds: ["CPP_COMP_BC"],
color: "#43a047",
backgroundColor: "#88c399",
text: "C++/Bytecode compiler"
},
{
kinds: ["CPP_COMP_BASELINE"],
color: "#43a047",
backgroundColor: "#5a8000",
text: "C++/Baseline compiler"
},
{
kinds: ["CPP_COMP"],
color: "#00e5ff",
backgroundColor: "#6effff",
text: "C++/Compiler"
},
{
kinds: ["CPP_GC"],
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 "CPP_PARSE":
return "C++ Parser";
case "CPP_COMP_BASELINE":
return "C++ Baseline Compiler";
case "CPP_COMP_BC":
return "C++ Bytecode Compiler";
case "CPP_COMP":
return "C++ Compiler";
case "CPP_GC":
return "C++ GC";
case "CPP_EXT":
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 "JS_IGNITION":
return "JS Ignition";
case "JS_SPARKPLUG":
return "JS Sparkplug";
case "JS_TURBOPROP":
return "JS Turboprop";
case "JS_MAGLEV":
return "JS Maglev";
case "JS_TURBOFAN":
return "JS Turbofan";
}
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.callTree.attribution === oldState.callTree.attribution &&
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 filter = filterFromFilterId(this.currentState.callTree.attribution);
let stackProcessor = new CategorySampler(file, bucketCount, filter);
generateTree(file, 0, Infinity, stackProcessor);
let codeIdProcessor = new FunctionTimelineProcessor(currentCodeId, filter);
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);
if (stats.turbopropOptimizedFunctionCount != 0) {
addRow("Turboprop optimized function count:", stats.turbopropOptimizedFunctionCount, 1);
}
addRow("Deoptimized function count:", stats.deoptimizedFunctionCount, 2);
addExpandableRow("Optimization count:", stats.optimizations, 0, "opt");
if (stats.turbopropOptimizedFunctionCount != 0) {
addExpandableRow("Turboprop Optimization count:", stats.turbopropOptimizations, 0, "tp");
}
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");
if (stats.softBailouts.count != 0) {
addExpandableRow("SoftBailout:", stats.softBailouts, 1, "softbailout");
}
if (stats.eagerBailouts.count != 0) {
addExpandableRow("EagerBailout:", stats.eagerBailouts, 1, "eagerbailout");
}
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.
if (scriptBlock.source === undefined) continue;
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";
}
}