v8/tools/profview/profile-utils.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

652 lines
19 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";
let codeKinds = [
"UNKNOWN",
"CPP_PARSE",
"CPP_COMP_BC",
"CPP_COMP_BASELINE",
"CPP_COMP",
"CPP_GC",
"CPP_EXT",
"CPP",
"LIB",
"IC",
"BC",
"STUB",
"BUILTIN",
"REGEXP",
"JS_IGNITION",
"JS_SPARKPLUG",
"JS_MAGLEV",
"JS_TURBOFAN",
];
function resolveCodeKind(code) {
if (!code || !code.type) {
return "UNKNOWN";
}
const type = code.type;
if (type === "CPP") {
return "CPP";
} else if (type === "SHARED_LIB") {
return "LIB";
}
const kind = code.kind;
if (type === "CODE") {
if (kind === "LoadIC" ||
kind === "StoreIC" ||
kind === "KeyedStoreIC" ||
kind === "KeyedLoadIC" ||
kind === "LoadGlobalIC" ||
kind === "Handler") {
return "IC";
} else if (kind === "BytecodeHandler") {
return "BC";
} else if (kind === "Stub") {
return "STUB";
} else if (kind === "Builtin") {
return "BUILTIN";
} else if (kind === "RegExp") {
return "REGEXP";
}
console.warn("Unknown CODE: '" + kind + "'.");
return "CODE";
} else if (type === "JS") {
if (kind === "Builtin" || kind == "Ignition" || kind === "Unopt") {
return "JS_IGNITION";
} else if (kind === "Baseline" || kind === "Sparkplug") {
return "JS_SPARKPLUG";
} else if (kind === "Maglev") {
return "JS_MAGLEV";
} else if (kind === "Turboprop") {
return "JS_TURBOPROP";
} else if (kind === "Opt" || kind === "Turbofan") {
return "JS_TURBOFAN";
}
}
console.warn("Unknown code type '" + kind + "'.");
}
function resolveCodeKindAndVmState(code, vmState) {
let kind = resolveCodeKind(code);
if (kind === "CPP") {
if (vmState === 1) {
kind = "CPP_GC";
} else if (vmState === 2) {
kind = "CPP_PARSE";
} else if (vmState === 3) {
kind = "CPP_COMP_BC";
} else if (vmState === 4) {
kind = "CPP_COMP";
} else if (vmState === 6) {
kind = "CPP_EXT";
}
// TODO(cbruni): add CPP_COMP_BASELINE
}
return kind;
}
function codeEquals(code1, code2, allowDifferentKinds = false) {
if (!code1 || !code2) return false;
if (code1.name !== code2.name || code1.type !== code2.type) return false;
if (code1.type === 'CODE') {
if (!allowDifferentKinds && code1.kind !== code2.kind) return false;
} else if (code1.type === 'JS') {
if (!allowDifferentKinds && code1.kind !== code2.kind) return false;
if (code1.func !== code2.func) return false;
}
return true;
}
function createNodeFromStackEntry(code, codeId, vmState) {
let name = code ? code.name : "UNKNOWN";
let node = createEmptyNode(name);
node.codeId = codeId;
node.type = resolveCodeKindAndVmState(code, vmState);
return node;
}
function childIdFromCode(codeId, code) {
// For JavaScript function, pretend there is one instance of optimized
// function and one instance of unoptimized function per SFI.
// Otherwise, just compute the id from code id.
let type = resolveCodeKind(code);
if (type === "JSOPT") {
return code.func * 4 + 1;
} else if (type === "JSUNOPT") {
return code.func * 4 + 2;
} else {
return codeId * 4;
}
}
// We store list of ticks and positions within the ticks stack by
// storing flattened triplets of { tickIndex, depth, count }.
// Triplet { 123, 2, 3 } encodes positions in ticks 123, 124, 125,
// all of them at depth 2. The flattened array is used to encode
// position within the call-tree.
// The following function helps to encode such triplets.
function addFrameToFrameList(paths, pathIndex, depth) {
// Try to combine with the previous code run.
if (paths.length > 0 &&
paths[paths.length - 3] + 1 === pathIndex &&
paths[paths.length - 2] === depth) {
paths[paths.length - 1]++;
} else {
paths.push(pathIndex, depth, 1);
}
}
function findNextFrame(file, stack, stackPos, step, filter) {
let codeId = -1;
let code = null;
while (stackPos >= 0 && stackPos < stack.length) {
codeId = stack[stackPos];
code = codeId >= 0 ? file.code[codeId] : undefined;
if (!filter || filter(code?.type, code?.kind)) return stackPos;
stackPos += step;
}
return -1;
}
function addOrUpdateChildNode(parent, file, stackIndex, stackPos, ascending) {
if (stackPos === -1) {
// We reached the end without finding the next step.
// If we are doing top-down call tree, update own ticks.
if (!ascending) {
parent.ownTicks++;
}
return;
}
let stack = file.ticks[stackIndex].s;
console.assert(stackPos >= 0 && stackPos < stack.length);
let codeId = stack[stackPos];
let code = codeId >= 0 ? file.code[codeId] : undefined;
// We found a child node.
let childId = childIdFromCode(codeId, code);
let child = parent.children[childId];
if (!child) {
let vmState = file.ticks[stackIndex].vm;
child = createNodeFromStackEntry(code, codeId, vmState);
child.delayedExpansion = { frameList : [], ascending };
parent.children[childId] = child;
}
child.ticks++;
addFrameToFrameList(child.delayedExpansion.frameList, stackIndex, stackPos);
}
// This expands a tree node (direct children only).
function expandTreeNode(file, node, filter) {
let { frameList, ascending } = node.delayedExpansion;
let step = ascending ? 2 : -2;
for (let i = 0; i < frameList.length; i+= 3) {
let firstStackIndex = frameList[i];
let depth = frameList[i + 1];
let count = frameList[i + 2];
for (let j = 0; j < count; j++) {
let stackIndex = firstStackIndex + j;
let stack = file.ticks[stackIndex].s;
// Get to the next frame that has not been filtered out.
let stackPos = findNextFrame(file, stack, depth + step, step, filter);
addOrUpdateChildNode(node, file, stackIndex, stackPos, ascending);
}
}
node.delayedExpansion = null;
}
function createEmptyNode(name) {
return {
name : name,
codeId: -1,
type : "CAT",
children : [],
ownTicks : 0,
ticks : 0
};
}
class RuntimeCallTreeProcessor {
constructor() {
this.tree = createEmptyNode("root");
this.tree.delayedExpansion = { frameList : [], ascending : false };
}
addStack(file, tickIndex) {
this.tree.ticks++;
let stack = file.ticks[tickIndex].s;
let i;
for (i = 0; i < stack.length; i += 2) {
let codeId = stack[i];
if (codeId < 0) return;
let code = file.code[codeId];
if (code.type !== "CPP" && code.type !== "SHARED_LIB") {
i -= 2;
break;
}
}
if (i < 0 || i >= stack.length) return;
addOrUpdateChildNode(this.tree, file, tickIndex, i, false);
}
}
class PlainCallTreeProcessor {
constructor(filter, isBottomUp) {
this.filter = filter;
this.tree = createEmptyNode("root");
this.tree.delayedExpansion = { frameList : [], ascending : isBottomUp };
this.isBottomUp = isBottomUp;
}
addStack(file, tickIndex) {
let stack = file.ticks[tickIndex].s;
let step = this.isBottomUp ? 2 : -2;
let start = this.isBottomUp ? 0 : stack.length - 2;
let stackPos = findNextFrame(file, stack, start, step, this.filter);
addOrUpdateChildNode(this.tree, file, tickIndex, stackPos, this.isBottomUp);
this.tree.ticks++;
}
}
function buildCategoryTreeAndLookup() {
let root = createEmptyNode("root");
let categories = {};
function addCategory(name, types) {
let n = createEmptyNode(name);
for (let i = 0; i < types.length; i++) {
categories[types[i]] = n;
}
root.children.push(n);
}
addCategory("JS Ignition", [ "JS_IGNITION", "BC" ]);
addCategory("JS Sparkplug", [ "JS_SPARKPLUG" ]);
addCategory("JS Maglev", [ "JS_MAGLEV" ]);
addCategory("JS Turbofan", [ "JS_TURBOFAN" ]);
addCategory("IC", [ "IC" ]);
addCategory("RegExp", [ "REGEXP" ]);
addCategory("Other generated", [ "STUB", "BUILTIN" ]);
addCategory("C++", [ "CPP", "LIB" ]);
addCategory("C++/GC", [ "CPP_GC" ]);
addCategory("C++/Parser", [ "CPP_PARSE" ]);
addCategory("C++/Bytecode Compiler", [ "CPP_COMP_BC" ]);
addCategory("C++/Baseline Compiler", [ "CPP_COMP_BASELINE" ]);
addCategory("C++/Compiler", [ "CPP_COMP" ]);
addCategory("C++/External", [ "CPP_EXT" ]);
addCategory("Unknown", [ "UNKNOWN" ]);
return { categories, root };
}
class CategorizedCallTreeProcessor {
constructor(filter, isBottomUp) {
this.filter = filter;
let { categories, root } = buildCategoryTreeAndLookup();
this.tree = root;
this.categories = categories;
this.isBottomUp = isBottomUp;
}
addStack(file, tickIndex) {
let stack = file.ticks[tickIndex].s;
let vmState = file.ticks[tickIndex].vm;
if (stack.length === 0) return;
let codeId = stack[0];
let code = codeId >= 0 ? file.code[codeId] : undefined;
let kind = resolveCodeKindAndVmState(code, vmState);
let node = this.categories[kind];
this.tree.ticks++;
node.ticks++;
let step = this.isBottomUp ? 2 : -2;
let start = this.isBottomUp ? 0 : stack.length - 2;
let stackPos = findNextFrame(file, stack, start, step, this.filter);
addOrUpdateChildNode(node, file, tickIndex, stackPos, this.isBottomUp);
}
}
class FunctionListTree {
constructor(filter, withCategories) {
if (withCategories) {
let { categories, root } = buildCategoryTreeAndLookup();
this.tree = root;
this.categories = categories;
} else {
this.tree = createEmptyNode("root");
this.categories = null;
}
this.codeVisited = [];
this.filter = filter;
}
addStack(file, tickIndex) {
let stack = file.ticks[tickIndex].s;
let vmState = file.ticks[tickIndex].vm;
this.tree.ticks++;
let child = null;
let tree = null;
for (let i = stack.length - 2; i >= 0; i -= 2) {
let codeId = stack[i];
if (codeId < 0 || this.codeVisited[codeId]) continue;
let code = file.code[codeId];
if (this.filter) {
let type = code ? code.type : undefined;
let kind = code ? code.kind : undefined;
if (!this.filter(type, kind)) continue;
}
let childId = childIdFromCode(codeId, code);
if (this.categories) {
let kind = resolveCodeKindAndVmState(code, vmState);
tree = this.categories[kind];
} else {
tree = this.tree;
}
child = tree.children[childId];
if (!child) {
child = createNodeFromStackEntry(code, codeId, vmState);
child.children[0] = createEmptyNode("Top-down tree");
child.children[0].delayedExpansion =
{ frameList : [], ascending : false };
child.children[1] = createEmptyNode("Bottom-up tree");
child.children[1].delayedExpansion =
{ frameList : [], ascending : true };
tree.children[childId] = child;
}
child.ticks++;
child.children[0].ticks++;
addFrameToFrameList(
child.children[0].delayedExpansion.frameList, tickIndex, i);
child.children[1].ticks++;
addFrameToFrameList(
child.children[1].delayedExpansion.frameList, tickIndex, i);
this.codeVisited[codeId] = true;
}
if (child) {
child.ownTicks++;
console.assert(tree !== null);
tree.ticks++;
console.assert(tree.type === "CAT");
}
for (let i = 0; i < stack.length; i += 2) {
let codeId = stack[i];
if (codeId >= 0) this.codeVisited[codeId] = false;
}
}
}
class CategorySampler {
constructor(file, bucketCount, filter) {
this.bucketCount = bucketCount;
this.filter = filter;
this.firstTime = file.ticks[0].tm;
let lastTime = file.ticks[file.ticks.length - 1].tm;
this.step = (lastTime - this.firstTime) / bucketCount;
this.buckets = [];
let bucket = {};
for (let i = 0; i < codeKinds.length; i++) {
bucket[codeKinds[i]] = 0;
}
for (let i = 0; i < bucketCount; i++) {
this.buckets.push(Object.assign({ total : 0 }, bucket));
}
}
addStack(file, tickIndex) {
let { tm : timestamp, vm : vmState, s : stack } = file.ticks[tickIndex];
let i = Math.floor((timestamp - this.firstTime) / this.step);
if (i === this.buckets.length) i--;
console.assert(i >= 0 && i < this.buckets.length);
let bucket = this.buckets[i];
bucket.total++;
let stackPos = findNextFrame(file, stack, 0, 2, this.filter);
let codeId = stackPos >= 0 ? stack[stackPos] : -1;
let code = codeId >= 0 ? file.code[codeId] : undefined;
let kind = resolveCodeKindAndVmState(code, vmState);
bucket[kind]++;
}
}
class FunctionTimelineProcessor {
constructor(functionCodeId, filter) {
this.functionCodeId = functionCodeId;
this.filter = filter;
this.blocks = [];
this.currentBlock = null;
}
addStack(file, tickIndex) {
if (!this.functionCodeId) return;
let { tm : timestamp, vm : vmState, s : stack } = file.ticks[tickIndex];
let functionCode = file.code[this.functionCodeId];
// Find if the function is on the stack, and its position on the stack,
// ignoring any filtered entries.
let stackCode = undefined;
let functionPosInStack = -1;
let filteredI = 0;
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;
// Match other instances of the same function (e.g. unoptimised, various
// different optimised versions).
if (codeEquals(code, functionCode, true)) {
functionPosInStack = filteredI;
stackCode = code;
break;
}
filteredI++;
}
if (functionPosInStack >= 0) {
let stackKind = resolveCodeKindAndVmState(stackCode, vmState);
let codeIsTopOfStack = (functionPosInStack === 0);
if (this.currentBlock !== null) {
this.currentBlock.end = timestamp;
if (codeIsTopOfStack === this.currentBlock.topOfStack
&& stackKind === this.currentBlock.kind) {
// If we haven't changed the stack top or the function kind, then
// we're happy just extending the current block and not starting
// a new one.
return;
}
}
// Start a new block at the current timestamp.
this.currentBlock = {
start: timestamp,
end: timestamp,
code: stackCode,
kind: stackKind,
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.
// {tree} is the processor of stacks.
function generateTree(
file, startTime, endTime, tree) {
let ticks = file.ticks;
let i = 0;
while (i < ticks.length && ticks[i].tm < startTime) {
i++;
}
let tickCount = 0;
while (i < ticks.length && ticks[i].tm < endTime) {
tree.addStack(file, i);
i++;
tickCount++;
}
return tickCount;
}
function computeOptimizationStats(file,
timeStart = -Infinity, timeEnd = Infinity) {
function newCollection() {
return { count : 0, functions : [], functionTable : [] };
}
function addToCollection(collection, code) {
collection.count++;
let funcData = collection.functionTable[code.func];
if (!funcData) {
funcData = { f : file.functions[code.func], instances : [] };
collection.functionTable[code.func] = funcData;
collection.functions.push(funcData);
}
funcData.instances.push(code);
}
let functionCount = 0;
let optimizedFunctionCount = 0;
let turbopropOptimizedFunctionCount = 0;
let deoptimizedFunctionCount = 0;
let optimizations = newCollection();
let turbopropOptimizations = newCollection();
let eagerDeoptimizations = newCollection();
let softDeoptimizations = newCollection();
let lazyDeoptimizations = newCollection();
let softBailouts = newCollection();
let eagerBailouts = newCollection();
for (let i = 0; i < file.functions.length; i++) {
let f = file.functions[i];
// Skip special SFIs that do not correspond to JS functions.
if (f.codes.length === 0) continue;
if (file.code[f.codes[0]].type !== "JS") continue;
functionCount++;
let optimized = false;
let turboprop_optimized = false;
let deoptimized = false;
for (let j = 0; j < f.codes.length; j++) {
let code = file.code[f.codes[j]];
console.assert(code.type === "JS");
if (code.kind === "Opt") {
optimized = true;
if (code.tm >= timeStart && code.tm <= timeEnd) {
addToCollection(optimizations, code);
}
}
if (code.kind === "Turboprop") {
turboprop_optimized = true;
if (code.tm >= timeStart && code.tm <= timeEnd) {
addToCollection(turbopropOptimizations, code);
}
}
if (code.deopt) {
if (code.deopt.bailoutType === "deopt-lazy" || code.deopt.bailoutType === "deopt-eager" || code.deopt.bailoutType === "deopt-soft") {
deoptimized = true;
}
if (code.deopt.tm >= timeStart && code.deopt.tm <= timeEnd) {
switch (code.deopt.bailoutType) {
case "deopt-lazy":
addToCollection(lazyDeoptimizations, code);
break;
case "deopt-eager":
addToCollection(eagerDeoptimizations, code);
break;
case "deopt-soft":
addToCollection(softDeoptimizations, code);
break;
case "bailout-soft":
addToCollection(softBailouts, code);
break;
case "bailout":
addToCollection(eagerBailouts, code);
break;
}
}
}
}
if (optimized) {
optimizedFunctionCount++;
}
if (turboprop_optimized) {
turbopropOptimizedFunctionCount++;
}
if (deoptimized) {
deoptimizedFunctionCount++;
}
}
function sortCollection(collection) {
collection.functions.sort(
(a, b) => a.instances.length - b.instances.length);
}
sortCollection(eagerDeoptimizations);
sortCollection(lazyDeoptimizations);
sortCollection(softDeoptimizations);
sortCollection(optimizations);
sortCollection(turbopropOptimizations);
return {
functionCount,
optimizedFunctionCount,
turbopropOptimizedFunctionCount,
deoptimizedFunctionCount,
optimizations,
turbopropOptimizations,
eagerDeoptimizations,
lazyDeoptimizations,
softDeoptimizations,
softBailouts,
eagerBailouts,
};
}
function normalizeLeadingWhitespace(lines) {
let regex = /^\s*/;
let minimumLeadingWhitespaceChars = Infinity;
for (let line of lines) {
minimumLeadingWhitespaceChars =
Math.min(minimumLeadingWhitespaceChars, regex.exec(line)[0].length);
}
for (let i = 0; i < lines.length; i++) {
lines[i] = lines[i].substring(minimumLeadingWhitespaceChars);
}
}