[tools/profview] Add individual function timeline view

Adds a bar below the current timeline view which can show the time
when an individual function was on the stack. Functions in the call
stack are now clickable to show them in this view.

Sections where the function was on the stack, but not at the top, are
displayed at half height.

Review-Url: https://codereview.chromium.org/2737083003
Cr-Commit-Position: refs/heads/master@{#43673}
This commit is contained in:
leszeks 2017-03-08 07:03:22 -08:00 committed by Commit bot
parent fb887b8192
commit 65a07b7a10
4 changed files with 200 additions and 77 deletions

View File

@ -57,6 +57,9 @@ found in the LICENSE file. -->
<tr id="timeline-legend">
</tr>
</table>
<div>
Current code object: <span id="timeline-currentCode"></span>
</div>
</div>
<br>

View File

@ -72,10 +72,10 @@ function resolveCodeKindAndVmState(code, vmState) {
return kind;
}
function createNodeFromStackEntry(code) {
function createNodeFromStackEntry(code, codeId) {
let name = code ? code.name : "UNKNOWN";
return { name, type : resolveCodeKind(code),
return { name, codeId, type : resolveCodeKind(code),
children : [], ownTicks : 0, ticks : 0 };
}
@ -143,7 +143,7 @@ function addOrUpdateChildNode(parent, file, stackIndex, stackPos, ascending) {
let childId = childIdFromCode(codeId, code);
let child = parent.children[childId];
if (!child) {
child = createNodeFromStackEntry(code);
child = createNodeFromStackEntry(code, codeId);
child.delayedExpansion = { frameList : [], ascending };
parent.children[childId] = child;
}
@ -177,6 +177,7 @@ function expandTreeNode(file, node, filter) {
function createEmptyNode(name) {
return {
name : name,
codeId: -1,
type : "CAT",
children : [],
ownTicks : 0,
@ -265,7 +266,13 @@ class FunctionListTree {
this.tree = root;
this.categories = categories;
} else {
this.tree = { name : "root", children : [], ownTicks : 0, ticks : 0 };
this.tree = {
name : "root",
codeId: -1,
children : [],
ownTicks : 0,
ticks : 0
};
this.categories = null;
}
@ -299,7 +306,7 @@ class FunctionListTree {
}
child = tree.children[childId];
if (!child) {
child = createNodeFromStackEntry(code);
child = createNodeFromStackEntry(code, codeId);
child.children[0] = createEmptyNode("Top-down tree");
child.children[0].delayedExpansion =
{ frameList : [], ascending : false };
@ -367,6 +374,54 @@ class CategorySampler {
}
}
class FunctionTimelineProcessor {
constructor(functionCodeId, filter) {
this.functionCodeId = functionCodeId;
this.filter = filter;
this.blocks = [];
this.currentBlock = null;
}
addStack(file, tickIndex) {
let { tm : timestamp, vm : vmState, s : stack } = file.ticks[tickIndex];
let codeInStack = stack.includes(this.functionCodeId);
if (codeInStack) {
let topOfStack = -1;
for (let i = 0; i < stack.length - 1; i += 2) {
let codeId = stack[i];
let code = codeId >= 0 ? file.code[codeId] : undefined;
let type = code ? code.type : undefined;
let kind = code ? code.kind : undefined;
if (!this.filter(type, kind)) continue;
topOfStack = i;
break;
}
let codeIsTopOfStack =
(topOfStack !== -1 && stack[topOfStack] === this.functionCodeId);
if (this.currentBlock !== null) {
this.currentBlock.end = timestamp;
if (codeIsTopOfStack === this.currentBlock.topOfStack) {
return;
}
}
this.currentBlock = {
start: timestamp,
end: timestamp,
topOfStack: codeIsTopOfStack
};
this.blocks.push(this.currentBlock);
} else {
this.currentBlock = null;
}
}
}
// Generates a tree out of a ticks sequence.
// {file} is the JSON files with the ticks and code objects.
// {startTime}, {endTime} is the interval.

View File

@ -34,6 +34,11 @@ span.code-type-chip-space {
display : inline-block;
}
span.codeid-link {
text-decoration: underline;
cursor: pointer;
}
div.mode-button {
padding: 1em 3em;
display: inline-block;

View File

@ -151,6 +151,14 @@ let main = {
}
},
setCurrentCode(codeId) {
if (codeId != main.currentState.currentCodeId) {
main.currentState = Object.assign({}, main.currentState);
main.currentState.currentCodeId = codeId;
main.delayRender();
}
},
onResize() {
main.setTimeLineDimensions(
window.innerWidth - 20, window.innerHeight / 5);
@ -241,6 +249,73 @@ function bucketFromKind(kind) {
return null;
}
function codeTypeToText(type) {
switch (type) {
case "UNKNOWN":
return "Unknown";
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);
}
}
class CallTreeView {
constructor() {
this.element = $("calltree");
@ -264,18 +339,6 @@ class CallTreeView {
this.currentState = null;
}
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' || !CallTreeView.IsBytecodeHandler(kind);
}
}
sortFromId(id) {
switch (id) {
case "time":
@ -305,10 +368,6 @@ class CallTreeView {
}
}
static IsBytecodeHandler(kind) {
return kind === "BytecodeHandler";
}
createExpander(indent) {
let div = document.createElement("div");
div.style.width = (1 + indent) + "em";
@ -317,55 +376,17 @@ class CallTreeView {
return div;
}
codeTypeToText(type) {
switch (type) {
case "UNKNOWN":
return "Unknown";
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";
createFunctionNode(name, codeId) {
if (codeId == -1) {
return document.createTextNode(name);
}
console.error("Unknown type: " + type);
}
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 = this.codeTypeToText(type);
div.appendChild(span);
span = document.createElement("span");
span.classList.add("code-type-chip-space");
div.appendChild(span);
return div;
let nameElement = document.createElement("span");
nameElement.classList.add("codeid-link")
nameElement.onclick = function() {
main.setCurrentCode(codeId);
};
nameElement.appendChild(document.createTextNode(name));
return nameElement;
}
expandTree(tree, indent) {
@ -392,7 +413,7 @@ class CallTreeView {
// Collect the children, and sort them by ticks.
let children = [];
let filter =
this.filterFromFilterId(this.currentState.callTree.attribution);
filterFromFilterId(this.currentState.callTree.attribution);
for (let childId in tree.children) {
let child = tree.children[childId];
if (child.ticks > 0) {
@ -432,8 +453,8 @@ class CallTreeView {
let nameCell = row.insertCell();
let expander = this.createExpander(indent);
nameCell.appendChild(expander);
nameCell.appendChild(this.createTypeDiv(node.type));
nameCell.appendChild(document.createTextNode(node.name));
nameCell.appendChild(createTypeDiv(node.type));
nameCell.appendChild(this.createFunctionNode(node.name, node.codeId));
// Inclusive ticks cell.
c = row.insertCell();
@ -573,7 +594,7 @@ class CallTreeView {
// Build the tree.
let stackProcessor;
let filter = this.filterFromFilterId(this.currentState.callTree.attribution);
let filter = filterFromFilterId(this.currentState.callTree.attribution);
if (mode === "top-down") {
stackProcessor =
new PlainCallTreeProcessor(filter, false);
@ -619,6 +640,7 @@ class TimelineView {
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);
@ -630,6 +652,7 @@ class TimelineView {
this.fontSize = 12;
this.imageOffset = this.fontSize * 1.2;
this.functionTimelineHeight = 12;
this.currentState = null;
}
@ -698,9 +721,9 @@ class TimelineView {
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
left = Math.min(this.selectionStart, this.selectionEnd);
right = Math.max(this.selectionStart, this.selectionEnd);
ctx.fillRect(0, this.imageOffset, left, this.buffer.height);
ctx.fillRect(right, this.imageOffset, this.buffer.width - right,
this.buffer.height);
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;
@ -763,6 +786,7 @@ class TimelineView {
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.
@ -784,6 +808,8 @@ class TimelineView {
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);
@ -801,6 +827,10 @@ class TimelineView {
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");
@ -808,7 +838,7 @@ class TimelineView {
buffer.height = height;
// Calculate the bar heights for each bucket.
let graphHeight = height;
let graphHeight = height - this.functionTimelineHeight;
let buckets = stackProcessor.buckets;
let bucketsGraph = [];
for (let i = 0; i < buckets.length; i++) {
@ -842,6 +872,24 @@ class TimelineView {
ctx.fill();
}
}
let functionTimelineYOffset = graphHeight;
let functionTimelineHeight = this.functionTimelineHeight;
let timestampScaler = width / (lastTime - firstTime);
ctx.fillStyle = "white";
ctx.fillRect(
0,
functionTimelineYOffset,
buffer.width,
functionTimelineHeight);
for (let i = 0; i < codeIdProcessor.blocks.length; i++) {
let block = codeIdProcessor.blocks[i];
ctx.fillStyle = "#000000";
ctx.fillRect(
Math.round((block.start - firstTime) * timestampScaler),
functionTimelineYOffset,
Math.max(1, Math.round((block.end - block.start) * timestampScaler)),
block.topOfStack ? functionTimelineHeight : functionTimelineHeight / 2);
}
// Remember stuff for later.
this.buffer = buffer;
@ -871,6 +919,18 @@ class TimelineView {
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(createTypeDiv(resolveCodeKind(currentCode)));
this.currentCode.appendChild(document.createTextNode(currentCode.name));
} else {
this.currentCode.appendChild(document.createTextNode("<none>"));
}
}
}