v8/tools/profile.mjs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1490 lines
40 KiB
JavaScript
Raw Normal View History

// Copyright 2009 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import { CodeMap, CodeEntry } from "./codemap.mjs";
import { ConsArray } from "./consarray.mjs";
import { WebInspector } from "./sourcemap.mjs";
// Used to associate log entries with source positions in scripts.
// TODO: move to separate modules
export class SourcePosition {
script = null;
line = -1;
column = -1;
entries = [];
isFunction = false;
originalPosition = undefined;
constructor(script, line, column) {
this.script = script;
this.line = line;
this.column = column;
}
addEntry(entry) {
this.entries.push(entry);
}
toString() {
return `${this.script.name}:${this.line}:${this.column}`;
}
get functionPosition() {
// TODO(cbruni)
return undefined;
}
get toolTipDict() {
return {
title: this.toString(),
__this__: this,
script: this.script,
entries: this.entries,
}
}
}
export class Script {
url;
source = "";
name;
sourcePosition = undefined;
// Map<line, Map<column, SourcePosition>>
lineToColumn = new Map();
_entries = [];
_sourceMapState = "unknown";
constructor(id) {
this.id = id;
this.sourcePositions = [];
}
update(url, source) {
this.url = url;
this.name = Script.getShortestUniqueName(url, this);
this.source = source;
}
get length() {
return this.source.length;
}
get entries() {
return this._entries;
}
get startLine() {
return this.sourcePosition?.line ?? 1;
}
get sourceMapState() {
return this._sourceMapState;
}
findFunctionSourcePosition(sourcePosition) {
// TODO(cbruni): implement
return undefined;
}
addSourcePosition(line, column, entry) {
let sourcePosition = this.lineToColumn.get(line)?.get(column);
if (sourcePosition === undefined) {
sourcePosition = new SourcePosition(this, line, column,)
this._addSourcePosition(line, column, sourcePosition);
}
if (this.sourcePosition === undefined && entry.entry?.type === "Script") {
// Mark the source position of scripts, for inline scripts which don't
// start at line 1.
this.sourcePosition = sourcePosition;
}
sourcePosition.addEntry(entry);
this._entries.push(entry);
return sourcePosition;
}
_addSourcePosition(line, column, sourcePosition) {
let columnToSourcePosition;
if (this.lineToColumn.has(line)) {
columnToSourcePosition = this.lineToColumn.get(line);
} else {
columnToSourcePosition = new Map();
this.lineToColumn.set(line, columnToSourcePosition);
}
this.sourcePositions.push(sourcePosition);
columnToSourcePosition.set(column, sourcePosition);
}
toString() {
return `Script(${this.id}): ${this.name}`;
}
get toolTipDict() {
return {
title: this.toString(),
__this__: this,
id: this.id,
url: this.url,
source: this.source,
sourcePositions: this.sourcePositions
}
}
static getShortestUniqueName(url, script) {
const parts = url.split('/');
const filename = parts[parts.length -1];
const dict = this._dict ?? (this._dict = new Map());
const matchingScripts = dict.get(filename);
if (matchingScripts == undefined) {
dict.set(filename, [script]);
return filename;
}
// TODO: find shortest unique substring
// Update all matching scripts to have a unique filename again.
for (let matchingScript of matchingScripts) {
matchingScript.name = script.url
}
matchingScripts.push(script);
return url;
}
ensureSourceMapCalculated(sourceMapFetchPrefix=undefined) {
if (this._sourceMapState !== "unknown") return;
const sourceMapURLMatch =
this.source.match(/\/\/# sourceMappingURL=(.*)\n/);
if (!sourceMapURLMatch) {
this._sourceMapState = "none";
return;
}
this._sourceMapState = "loading";
let sourceMapURL = sourceMapURLMatch[1];
(async () => {
try {
let sourceMapPayload;
const options = { timeout: 15 };
try {
sourceMapPayload = await fetch(sourceMapURL, options);
} catch (e) {
if (e instanceof TypeError && sourceMapFetchPrefix) {
// Try again with fetch prefix.
// TODO(leszeks): Remove the retry once the prefix is
// configurable.
sourceMapPayload =
await fetch(sourceMapFetchPrefix + sourceMapURL, options);
} else {
throw e;
}
}
sourceMapPayload = await sourceMapPayload.text();
if (sourceMapPayload.startsWith(')]}')) {
sourceMapPayload =
sourceMapPayload.substring(sourceMapPayload.indexOf('\n'));
}
sourceMapPayload = JSON.parse(sourceMapPayload);
const sourceMap =
new WebInspector.SourceMap(sourceMapURL, sourceMapPayload);
const startLine = this.startLine;
for (const sourcePosition of this.sourcePositions) {
const line = sourcePosition.line - startLine;
const column = sourcePosition.column - 1;
const mapping = sourceMap.findEntry(line, column);
if (mapping) {
sourcePosition.originalPosition = {
source: new URL(mapping[2], sourceMapURL).href,
line: mapping[3] + 1,
column: mapping[4] + 1
};
} else {
sourcePosition.originalPosition = {source: null, line:0, column:0};
}
}
this._sourceMapState = "loaded";
} catch (e) {
console.error(e);
this._sourceMapState = "failed";
}
})();
}
}
const kOffsetPairRegex = /C([0-9]+)O([0-9]+)/g;
class SourcePositionTable {
constructor(encodedTable) {
this._offsets = [];
while (true) {
const regexResult = kOffsetPairRegex.exec(encodedTable);
if (!regexResult) break;
const codeOffset = parseInt(regexResult[1]);
const scriptOffset = parseInt(regexResult[2]);
if (isNaN(codeOffset) || isNaN(scriptOffset)) continue;
this._offsets.push({code: codeOffset, script: scriptOffset});
}
}
getScriptOffset(codeOffset) {
if (codeOffset < 0) {
throw new Exception(`Invalid codeOffset=${codeOffset}, should be >= 0`);
}
for (let i = this.offsetTable.length - 1; i >= 0; i--) {
const offset = this._offsets[i];
if (offset.code <= codeOffset) {
return offset.script;
}
}
return this._offsets[0].script;
}
}
class SourceInfo {
script;
start;
end;
positions;
inlined;
fns;
disassemble;
setSourcePositionInfo(
script, startPos, endPos, sourcePositionTableData, inliningPositions,
inlinedFunctions) {
this.script = script;
this.start = startPos;
this.end = endPos;
this.positions = sourcePositionTableData;
this.inlined = inliningPositions;
this.fns = inlinedFunctions;
this.sourcePositionTable = new SourcePositionTable(sourcePositionTableData);
}
setDisassemble(code) {
this.disassemble = code;
}
getSourceCode() {
return this.script.source?.substring(this.start, this.end);
}
}
const kProfileOperationMove = 0;
const kProfileOperationDelete = 1;
const kProfileOperationTick = 2;
/**
* Creates a profile object for processing profiling-related events
* and calculating function execution times.
*
* @constructor
*/
export class Profile {
codeMap_ = new CodeMap();
topDownTree_ = new CallTree();
bottomUpTree_ = new CallTree();
c_entries_ = {__proto__:null};
scripts_ = [];
urlToScript_ = new Map();
warnings = new Set();
serializeVMSymbols() {
let result = this.codeMap_.getAllStaticEntriesWithAddresses();
result.concat(this.codeMap_.getAllLibraryEntriesWithAddresses())
return result.map(([startAddress, codeEntry]) => {
return [codeEntry.getName(), startAddress, startAddress + codeEntry.size]
});
}
/**
* Returns whether a function with the specified name must be skipped.
* Should be overridden by subclasses.
*
* @param {string} name Function name.
*/
skipThisFunction(name) {
return false;
}
/**
* Enum for profiler operations that involve looking up existing
* code entries.
*
* @enum {number}
*/
static Operation = {
MOVE: kProfileOperationMove,
DELETE: kProfileOperationDelete,
TICK: kProfileOperationTick
}
/**
* Enum for code state regarding its dynamic optimization.
*
* @enum {number}
*/
static CodeState = {
COMPILED: 0,
IGNITION: 1,
SPARKPLUG: 2,
MAGLEV: 4,
TURBOFAN: 5,
}
static VMState = {
JS: 0,
GC: 1,
PARSER: 2,
BYTECODE_COMPILER: 3,
// TODO(cbruni): add SPARKPLUG_COMPILER
COMPILER: 4,
OTHER: 5,
EXTERNAL: 6,
IDLE: 7,
}
static CodeType = {
CPP: 0,
SHARED_LIB: 1
}
/**
* Parser for dynamic code optimization state.
*/
static parseState(s) {
switch (s) {
case '':
return this.CodeState.COMPILED;
case '~':
return this.CodeState.IGNITION;
case '^':
return this.CodeState.SPARKPLUG;
case '+':
return this.CodeState.MAGLEV;
case '*':
return this.CodeState.TURBOFAN;
}
throw new Error(`unknown code state: ${s}`);
}
static getKindFromState(state) {
if (state === this.CodeState.COMPILED) {
return "Builtin";
} else if (state === this.CodeState.IGNITION) {
return "Unopt";
} else if (state === this.CodeState.SPARKPLUG) {
return "Sparkplug";
} else if (state === this.CodeState.MAGLEV) {
return "Maglev";
} else if (state === this.CodeState.TURBOFAN) {
return "Opt";
}
throw new Error(`unknown code state: ${state}`);
}
static vmStateString(state) {
switch (state) {
case this.VMState.JS:
return 'JS';
case this.VMState.GC:
return 'GC';
case this.VMState.PARSER:
return 'Parse';
case this.VMState.BYTECODE_COMPILER:
return 'Compile Bytecode';
case this.VMState.COMPILER:
return 'Compile';
case this.VMState.OTHER:
return 'Other';
case this.VMState.EXTERNAL:
return 'External';
case this.VMState.IDLE:
return 'Idle';
}
return 'unknown';
}
/**
* Called whenever the specified operation has failed finding a function
* containing the specified address. Should be overridden by subclasses.
* See the Profile.Operation enum for the list of
* possible operations.
*
* @param {number} operation Operation.
* @param {number} addr Address of the unknown code.
* @param {number} opt_stackPos If an unknown address is encountered
* during stack strace processing, specifies a position of the frame
* containing the address.
*/
handleUnknownCode(operation, addr, opt_stackPos) { }
/**
* Registers a library.
*
* @param {string} name Code entry name.
* @param {number} startAddr Starting address.
* @param {number} endAddr Ending address.
*/
addLibrary(name, startAddr, endAddr) {
const entry = new CodeEntry(endAddr - startAddr, name, 'SHARED_LIB');
this.codeMap_.addLibrary(startAddr, entry);
return entry;
}
/**
* Registers statically compiled code entry.
*
* @param {string} name Code entry name.
* @param {number} startAddr Starting address.
* @param {number} endAddr Ending address.
*/
addStaticCode(name, startAddr, endAddr) {
const entry = new CodeEntry(endAddr - startAddr, name, 'CPP');
this.codeMap_.addStaticCode(startAddr, entry);
return entry;
}
/**
* Registers dynamic (JIT-compiled) code entry.
*
* @param {string} type Code entry type.
* @param {string} name Code entry name.
* @param {number} start Starting address.
* @param {number} size Code entry size.
*/
addCode(type, name, timestamp, start, size) {
const entry = new DynamicCodeEntry(size, type, name);
this.codeMap_.addCode(start, entry);
return entry;
}
/**
* Registers dynamic (JIT-compiled) code entry or entries that overlap with
* static entries (like builtins).
*
* @param {string} type Code entry type.
* @param {string} name Code entry name.
* @param {number} start Starting address.
* @param {number} size Code entry size.
*/
addAnyCode(type, name, timestamp, start, size) {
const entry = new DynamicCodeEntry(size, type, name);
this.codeMap_.addAnyCode(start, entry);
return entry;
}
/**
* Registers dynamic (JIT-compiled) code entry.
*
* @param {string} type Code entry type.
* @param {string} name Code entry name.
* @param {number} start Starting address.
* @param {number} size Code entry size.
* @param {number} funcAddr Shared function object address.
* @param {Profile.CodeState} state Optimization state.
*/
addFuncCode(type, name, timestamp, start, size, funcAddr, state) {
// As code and functions are in the same address space,
// it is safe to put them in a single code map.
let func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr);
if (func === null) {
func = new FunctionEntry(name);
this.codeMap_.addCode(funcAddr, func);
} else if (func.name !== name) {
// Function object has been overwritten with a new one.
func.name = name;
}
let entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (entry !== null) {
if (entry.size === size && entry.func === func) {
// Entry state has changed.
entry.state = state;
} else {
this.codeMap_.deleteCode(start);
entry = null;
}
}
if (entry === null) {
entry = new DynamicFuncCodeEntry(size, type, func, state);
this.codeMap_.addCode(start, entry);
}
return entry;
}
/**
* Reports about moving of a dynamic code entry.
*
* @param {number} from Current code entry address.
* @param {number} to New code entry address.
*/
moveCode(from, to) {
try {
this.codeMap_.moveCode(from, to);
} catch (e) {
this.handleUnknownCode(kProfileOperationMove, from);
}
}
deoptCode(timestamp, code, inliningId, scriptOffset, bailoutType,
sourcePositionText, deoptReasonText) {
}
/**
* Reports about deletion of a dynamic code entry.
*
* @param {number} start Starting address.
*/
deleteCode(start) {
try {
this.codeMap_.deleteCode(start);
} catch (e) {
this.handleUnknownCode(kProfileOperationDelete, start);
}
}
/**
* Adds source positions for given code.
*/
addSourcePositions(start, scriptId, startPos, endPos, sourcePositionTable,
inliningPositions, inlinedFunctions) {
const script = this.getOrCreateScript(scriptId);
const entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (entry === null) return;
// Resolve the inlined functions list.
if (inlinedFunctions.length > 0) {
inlinedFunctions = inlinedFunctions.substring(1).split("S");
for (let i = 0; i < inlinedFunctions.length; i++) {
const funcAddr = parseInt(inlinedFunctions[i]);
const func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr);
if (func === null || func.funcId === undefined) {
// TODO: fix
this.warnings.add(`Could not find function ${inlinedFunctions[i]}`);
inlinedFunctions[i] = null;
} else {
inlinedFunctions[i] = func.funcId;
}
}
} else {
inlinedFunctions = [];
}
this.getOrCreateSourceInfo(entry).setSourcePositionInfo(
script, startPos, endPos, sourcePositionTable, inliningPositions,
inlinedFunctions);
}
addDisassemble(start, kind, disassemble) {
const entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (entry !== null) {
this.getOrCreateSourceInfo(entry).setDisassemble(disassemble);
}
return entry;
}
getOrCreateSourceInfo(entry) {
return entry.source ?? (entry.source = new SourceInfo());
}
addScriptSource(id, url, source) {
const script = this.getOrCreateScript(id);
script.update(url, source);
this.urlToScript_.set(url, script);
}
getOrCreateScript(id) {
let script = this.scripts_[id];
if (script === undefined) {
script = new Script(id);
this.scripts_[id] = script;
}
return script;
}
getScript(url) {
return this.urlToScript_.get(url);
}
/**
* Reports about moving of a dynamic code entry.
*
* @param {number} from Current code entry address.
* @param {number} to New code entry address.
*/
moveFunc(from, to) {
if (this.codeMap_.findDynamicEntryByStartAddress(from)) {
this.codeMap_.moveCode(from, to);
}
}
/**
* Retrieves a code entry by an address.
*
* @param {number} addr Entry address.
*/
findEntry(addr) {
return this.codeMap_.findEntry(addr);
}
/**
* Records a tick event. Stack must contain a sequence of
* addresses starting with the program counter value.
*
* @param {number[]} stack Stack sample.
*/
recordTick(time_ns, vmState, stack) {
const {nameStack, entryStack} = this.resolveAndFilterFuncs_(stack);
this.bottomUpTree_.addPath(nameStack);
nameStack.reverse();
this.topDownTree_.addPath(nameStack);
return entryStack;
}
/**
* Translates addresses into function names and filters unneeded
* functions.
*
* @param {number[]} stack Stack sample.
*/
resolveAndFilterFuncs_(stack) {
const nameStack = [];
const entryStack = [];
let last_seen_c_function = '';
let look_for_first_c_function = false;
for (let i = 0; i < stack.length; ++i) {
const pc = stack[i];
const entry = this.codeMap_.findEntry(pc);
if (entry !== null) {
entryStack.push(entry);
const name = entry.getName();
if (i === 0 && (entry.type === 'CPP' || entry.type === 'SHARED_LIB')) {
look_for_first_c_function = true;
}
if (look_for_first_c_function && entry.type === 'CPP') {
last_seen_c_function = name;
}
if (!this.skipThisFunction(name)) {
nameStack.push(name);
}
} else {
this.handleUnknownCode(kProfileOperationTick, pc, i);
if (i === 0) nameStack.push("UNKNOWN");
entryStack.push(pc);
}
if (look_for_first_c_function && i > 0 &&
(entry === null || entry.type !== 'CPP')
&& last_seen_c_function !== '') {
if (this.c_entries_[last_seen_c_function] === undefined) {
this.c_entries_[last_seen_c_function] = 0;
}
this.c_entries_[last_seen_c_function]++;
look_for_first_c_function = false; // Found it, we're done.
}
}
return {nameStack, entryStack};
}
/**
* Performs a BF traversal of the top down call graph.
*
* @param {function(CallTreeNode)} f Visitor function.
*/
traverseTopDownTree(f) {
this.topDownTree_.traverse(f);
}
/**
* Performs a BF traversal of the bottom up call graph.
*
* @param {function(CallTreeNode)} f Visitor function.
*/
traverseBottomUpTree(f) {
this.bottomUpTree_.traverse(f);
}
/**
* Calculates a top down profile for a node with the specified label.
* If no name specified, returns the whole top down calls tree.
*
* @param {string} opt_label Node label.
*/
getTopDownProfile(opt_label) {
return this.getTreeProfile_(this.topDownTree_, opt_label);
}
/**
* Calculates a bottom up profile for a node with the specified label.
* If no name specified, returns the whole bottom up calls tree.
*
* @param {string} opt_label Node label.
*/
getBottomUpProfile(opt_label) {
return this.getTreeProfile_(this.bottomUpTree_, opt_label);
}
/**
* Helper function for calculating a tree profile.
*
* @param {Profile.CallTree} tree Call tree.
* @param {string} opt_label Node label.
*/
getTreeProfile_(tree, opt_label) {
if (!opt_label) {
tree.computeTotalWeights();
return tree;
} else {
const subTree = tree.cloneSubtree(opt_label);
subTree.computeTotalWeights();
return subTree;
}
}
/**
* Calculates a flat profile of callees starting from a node with
* the specified label. If no name specified, starts from the root.
*
* @param {string} opt_label Starting node label.
*/
getFlatProfile(opt_label) {
const counters = new CallTree();
const rootLabel = opt_label || CallTree.ROOT_NODE_LABEL;
const precs = {__proto__:null};
precs[rootLabel] = 0;
const root = counters.findOrAddChild(rootLabel);
this.topDownTree_.computeTotalWeights();
this.topDownTree_.traverseInDepth(
function onEnter(node) {
if (!(node.label in precs)) {
precs[node.label] = 0;
}
const nodeLabelIsRootLabel = node.label == rootLabel;
if (nodeLabelIsRootLabel || precs[rootLabel] > 0) {
if (precs[rootLabel] == 0) {
root.selfWeight += node.selfWeight;
root.totalWeight += node.totalWeight;
} else {
const rec = root.findOrAddChild(node.label);
rec.selfWeight += node.selfWeight;
if (nodeLabelIsRootLabel || precs[node.label] == 0) {
rec.totalWeight += node.totalWeight;
}
}
precs[node.label]++;
}
},
function onExit(node) {
if (node.label == rootLabel || precs[rootLabel] > 0) {
precs[node.label]--;
}
},
null);
if (!opt_label) {
// If we have created a flat profile for the whole program, we don't
// need an explicit root in it. Thus, replace the counters tree
// root with the node corresponding to the whole program.
counters.root_ = root;
} else {
// Propagate weights so percents can be calculated correctly.
counters.getRoot().selfWeight = root.selfWeight;
counters.getRoot().totalWeight = root.totalWeight;
}
return counters;
}
getCEntryProfile() {
const result = [new CEntryNode("TOTAL", 0)];
let total_ticks = 0;
for (let f in this.c_entries_) {
const ticks = this.c_entries_[f];
total_ticks += ticks;
result.push(new CEntryNode(f, ticks));
}
result[0].ticks = total_ticks; // Sorting will keep this at index 0.
result.sort((n1, n2) => n2.ticks - n1.ticks || (n2.name < n1.name ? -1 : 1));
return result;
}
/**
* Cleans up function entries that are not referenced by code entries.
*/
cleanUpFuncEntries() {
const referencedFuncEntries = [];
const entries = this.codeMap_.getAllDynamicEntriesWithAddresses();
for (let i = 0, l = entries.length; i < l; ++i) {
if (entries[i][1].constructor === FunctionEntry) {
entries[i][1].used = false;
}
}
for (let i = 0, l = entries.length; i < l; ++i) {
if ("func" in entries[i][1]) {
entries[i][1].func.used = true;
}
}
for (let i = 0, l = entries.length; i < l; ++i) {
if (entries[i][1].constructor === FunctionEntry &&
!entries[i][1].used) {
this.codeMap_.deleteCode(entries[i][0]);
}
}
}
}
class CEntryNode {
constructor(name, ticks) {
this.name = name;
this.ticks = ticks;
}
}
/**
* Creates a dynamic code entry.
*
* @param {number} size Code size.
* @param {string} type Code type.
* @param {string} name Function name.
* @constructor
*/
class DynamicCodeEntry extends CodeEntry {
constructor(size, type, name) {
super(size, name, type);
}
getName() {
return this.type + ': ' + this.name;
}
/**
* Returns raw node name (without type decoration).
*/
getRawName() {
return this.name;
}
isJSFunction() {
return false;
}
toString() {
return this.getName() + ': ' + this.size.toString(16);
}
}
/**
* Creates a dynamic code entry.
*
* @param {number} size Code size.
* @param {string} type Code type.
* @param {FunctionEntry} func Shared function entry.
* @param {Profile.CodeState} state Code optimization state.
* @constructor
*/
class DynamicFuncCodeEntry extends CodeEntry {
constructor(size, type, func, state) {
super(size, '', type);
this.func = func;
func.addDynamicCode(this);
this.state = state;
}
get functionName() {
return this.func.functionName;
}
getSourceCode() {
return this.source?.getSourceCode();
}
static STATE_PREFIX = ["", "~", "^", "-", "+", "*"];
getState() {
return DynamicFuncCodeEntry.STATE_PREFIX[this.state];
}
getName() {
const name = this.func.getName();
return this.type + ': ' + this.getState() + name;
}
/**
* Returns raw node name (without type decoration).
*/
getRawName() {
return this.func.getName();
}
isJSFunction() {
return true;
}
toString() {
return this.getName() + ': ' + this.size.toString(16);
}
}
/**
* Creates a shared function object entry.
*
* @param {string} name Function name.
* @constructor
*/
class FunctionEntry extends CodeEntry {
// Contains the list of generated code for this function.
/** @type {Set<DynamicCodeEntry>} */
_codeEntries = new Set();
constructor(name) {
super(0, name);
const index = name.lastIndexOf(' ');
this.functionName = 1 <= index ? name.substring(0, index) : '<anonymous>';
}
addDynamicCode(code) {
if (code.func != this) {
throw new Error("Adding dynamic code to wrong function");
}
this._codeEntries.add(code);
}
getSourceCode() {
// All code entries should map to the same source positions.
return this._codeEntries.values().next().value.getSourceCode();
}
get codeEntries() {
return this._codeEntries;
}
/**
* Returns node name.
*/
getName() {
let name = this.name;
if (name.length == 0) {
return '<anonymous>';
} else if (name.charAt(0) == ' ') {
// An anonymous function with location: " aaa.js:10".
return `<anonymous>${name}`;
}
return name;
}
}
/**
* Constructs a call graph.
*
* @constructor
*/
class CallTree {
root_ = new CallTreeNode(CallTree.ROOT_NODE_LABEL);
totalsComputed_ = false;
/**
* The label of the root node.
*/
static ROOT_NODE_LABEL = '';
/**
* Returns the tree root.
*/
getRoot() {
return this.root_;
}
/**
* Adds the specified call path, constructing nodes as necessary.
*
* @param {string[]} path Call path.
*/
addPath(path) {
if (path.length == 0) return;
let curr = this.root_;
for (let i = 0; i < path.length; ++i) {
curr = curr.findOrAddChild(path[i]);
}
curr.selfWeight++;
this.totalsComputed_ = false;
}
/**
* Finds an immediate child of the specified parent with the specified
* label, creates a child node if necessary. If a parent node isn't
* specified, uses tree root.
*
* @param {string} label Child node label.
*/
findOrAddChild(label) {
return this.root_.findOrAddChild(label);
}
/**
* Creates a subtree by cloning and merging all subtrees rooted at nodes
* with a given label. E.g. cloning the following call tree on label 'A'
* will give the following result:
*
* <A>--<B> <B>
* / /
* <root> == clone on 'A' ==> <root>--<A>
* \ \
* <C>--<A>--<D> <D>
*
* And <A>'s selfWeight will be the sum of selfWeights of <A>'s from the
* source call tree.
*
* @param {string} label The label of the new root node.
*/
cloneSubtree(label) {
const subTree = new CallTree();
this.traverse((node, parent) => {
if (!parent && node.label != label) {
return null;
}
const child = (parent ? parent : subTree).findOrAddChild(node.label);
child.selfWeight += node.selfWeight;
return child;
});
return subTree;
}
/**
* Computes total weights in the call graph.
*/
computeTotalWeights() {
if (this.totalsComputed_) return;
this.root_.computeTotalWeight();
this.totalsComputed_ = true;
}
/**
* Traverses the call graph in preorder. This function can be used for
* building optionally modified tree clones. This is the boilerplate code
* for this scenario:
*
* callTree.traverse(function(node, parentClone) {
* var nodeClone = cloneNode(node);
* if (parentClone)
* parentClone.addChild(nodeClone);
* return nodeClone;
* });
*
* @param {function(CallTreeNode, *)} f Visitor function.
* The second parameter is the result of calling 'f' on the parent node.
*/
traverse(f) {
const pairsToProcess = new ConsArray();
pairsToProcess.concat([{ node: this.root_, param: null }]);
while (!pairsToProcess.atEnd()) {
const pair = pairsToProcess.next();
const node = pair.node;
const newParam = f(node, pair.param);
const morePairsToProcess = [];
node.forEachChild((child) => {
morePairsToProcess.push({ node: child, param: newParam });
});
pairsToProcess.concat(morePairsToProcess);
}
}
/**
* Performs an indepth call graph traversal.
*
* @param {function(CallTreeNode)} enter A function called
* prior to visiting node's children.
* @param {function(CallTreeNode)} exit A function called
* after visiting node's children.
*/
traverseInDepth(enter, exit) {
function traverse(node) {
enter(node);
node.forEachChild(traverse);
exit(node);
}
traverse(this.root_);
}
}
/**
* Constructs a call graph node.
*
* @param {string} label Node label.
* @param {CallTreeNode} opt_parent Node parent.
*/
class CallTreeNode {
constructor(label, opt_parent) {
// Node self weight (how many times this node was the last node in
// a call path).
this.selfWeight = 0;
// Node total weight (includes weights of all children).
this.totalWeight = 0;
this. children = { __proto__:null };
this.label = label;
this.parent = opt_parent;
}
/**
* Adds a child node.
*
* @param {string} label Child node label.
*/
addChild(label) {
const child = new CallTreeNode(label, this);
this.children[label] = child;
return child;
}
/**
* Computes node's total weight.
*/
computeTotalWeight() {
let totalWeight = this.selfWeight;
this.forEachChild(function (child) {
totalWeight += child.computeTotalWeight();
});
return this.totalWeight = totalWeight;
}
/**
* Returns all node's children as an array.
*/
exportChildren() {
const result = [];
this.forEachChild(function (node) { result.push(node); });
return result;
}
/**
* Finds an immediate child with the specified label.
*
* @param {string} label Child node label.
*/
findChild(label) {
const found = this.children[label];
return found === undefined ? null : found;
}
/**
* Finds an immediate child with the specified label, creates a child
* node if necessary.
*
* @param {string} label Child node label.
*/
findOrAddChild(label) {
const found = this.findChild(label)
if (found === null) return this.addChild(label);
return found;
}
/**
* Calls the specified function for every child.
*
* @param {function(CallTreeNode)} f Visitor function.
*/
forEachChild(f) {
for (let c in this.children) {
f(this.children[c]);
}
}
/**
* Walks up from the current node up to the call tree root.
*
* @param {function(CallTreeNode)} f Visitor function.
*/
walkUpToRoot(f) {
for (let curr = this; curr !== null; curr = curr.parent) {
f(curr);
}
}
/**
* Tries to find a node with the specified path.
*
* @param {string[]} labels The path.
* @param {function(CallTreeNode)} opt_f Visitor function.
*/
descendToChild(labels, opt_f) {
let curr = this;
for (let pos = 0; pos < labels.length && curr != null; pos++) {
const child = curr.findChild(labels[pos]);
if (opt_f) {
opt_f(child, pos);
}
curr = child;
}
return curr;
}
}
export function JsonProfile() {
this.codeMap_ = new CodeMap();
this.codeEntries_ = [];
this.functionEntries_ = [];
this.ticks_ = [];
this.scripts_ = [];
}
JsonProfile.prototype.addLibrary = function (
name, startAddr, endAddr) {
const entry = new CodeEntry(
endAddr - startAddr, name, 'SHARED_LIB');
this.codeMap_.addLibrary(startAddr, entry);
entry.codeId = this.codeEntries_.length;
this.codeEntries_.push({ name: entry.name, type: entry.type });
return entry;
};
JsonProfile.prototype.addStaticCode = function (
name, startAddr, endAddr) {
const entry = new CodeEntry(
endAddr - startAddr, name, 'CPP');
this.codeMap_.addStaticCode(startAddr, entry);
entry.codeId = this.codeEntries_.length;
this.codeEntries_.push({ name: entry.name, type: entry.type });
return entry;
};
JsonProfile.prototype.addCode = function (
kind, name, timestamp, start, size) {
let codeId = this.codeEntries_.length;
// Find out if we have a static code entry for the code. If yes, we will
// make sure it is written to the JSON file just once.
let staticEntry = this.codeMap_.findAddress(start);
if (staticEntry && staticEntry.entry.type === 'CPP') {
codeId = staticEntry.entry.codeId;
}
const entry = new CodeEntry(size, name, 'CODE');
this.codeMap_.addCode(start, entry);
entry.codeId = codeId;
this.codeEntries_[codeId] = {
name: entry.name,
timestamp: timestamp,
type: entry.type,
kind: kind,
};
return entry;
};
JsonProfile.prototype.addFuncCode = function (
kind, name, timestamp, start, size, funcAddr, state) {
// As code and functions are in the same address space,
// it is safe to put them in a single code map.
let func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr);
if (!func) {
func = new CodeEntry(0, name, 'SFI');
this.codeMap_.addCode(funcAddr, func);
func.funcId = this.functionEntries_.length;
this.functionEntries_.push({ name, codes: [] });
} else if (func.name !== name) {
// Function object has been overwritten with a new one.
func.name = name;
func.funcId = this.functionEntries_.length;
this.functionEntries_.push({ name, codes: [] });
}
// TODO(jarin): Insert the code object into the SFI's code list.
let entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (entry) {
if (entry.size === size && entry.func === func) {
// Entry state has changed.
entry.state = state;
} else {
this.codeMap_.deleteCode(start);
entry = null;
}
}
if (!entry) {
entry = new CodeEntry(size, name, 'JS');
this.codeMap_.addCode(start, entry);
entry.codeId = this.codeEntries_.length;
this.functionEntries_[func.funcId].codes.push(entry.codeId);
kind = Profile.getKindFromState(state);
this.codeEntries_.push({
name: entry.name,
type: entry.type,
kind: kind,
func: func.funcId,
tm: timestamp,
});
}
return entry;
};
JsonProfile.prototype.moveCode = function (from, to) {
try {
this.codeMap_.moveCode(from, to);
} catch (e) {
printErr(`Move: unknown source ${from}`);
}
};
JsonProfile.prototype.addSourcePositions = function (
start, script, startPos, endPos, sourcePositions, inliningPositions,
inlinedFunctions) {
const entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (!entry) return;
const codeId = entry.codeId;
// Resolve the inlined functions list.
if (inlinedFunctions.length > 0) {
inlinedFunctions = inlinedFunctions.substring(1).split("S");
for (let i = 0; i < inlinedFunctions.length; i++) {
const funcAddr = parseInt(inlinedFunctions[i]);
const func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr);
if (!func || func.funcId === undefined) {
printErr(`Could not find function ${inlinedFunctions[i]}`);
inlinedFunctions[i] = null;
} else {
inlinedFunctions[i] = func.funcId;
}
}
} else {
inlinedFunctions = [];
}
this.codeEntries_[entry.codeId].source = {
script: script,
start: startPos,
end: endPos,
positions: sourcePositions,
inlined: inliningPositions,
fns: inlinedFunctions
};
};
JsonProfile.prototype.addScriptSource = function (id, url, source) {
const script = new Script(id);
script.update(url, source);
this.scripts_[id] = script;
};
JsonProfile.prototype.deoptCode = function (
timestamp, code, inliningId, scriptOffset, bailoutType,
sourcePositionText, deoptReasonText) {
let entry = this.codeMap_.findDynamicEntryByStartAddress(code);
if (entry) {
let codeId = entry.codeId;
if (!this.codeEntries_[codeId].deopt) {
// Only add the deopt if there was no deopt before.
// The subsequent deoptimizations should be lazy deopts for
// other on-stack activations.
this.codeEntries_[codeId].deopt = {
tm: timestamp,
inliningId: inliningId,
scriptOffset: scriptOffset,
posText: sourcePositionText,
reason: deoptReasonText,
bailoutType: bailoutType,
};
}
}
};
JsonProfile.prototype.deleteCode = function (start) {
try {
this.codeMap_.deleteCode(start);
} catch (e) {
printErr(`Delete: unknown address ${start}`);
}
};
JsonProfile.prototype.moveFunc = function (from, to) {
if (this.codeMap_.findDynamicEntryByStartAddress(from)) {
this.codeMap_.moveCode(from, to);
}
};
JsonProfile.prototype.findEntry = function (addr) {
return this.codeMap_.findEntry(addr);
};
JsonProfile.prototype.recordTick = function (time_ns, vmState, stack) {
// TODO(jarin) Resolve the frame-less case (when top of stack is
// known code).
const processedStack = [];
for (let i = 0; i < stack.length; i++) {
const resolved = this.codeMap_.findAddress(stack[i]);
if (resolved) {
processedStack.push(resolved.entry.codeId, resolved.offset);
} else {
processedStack.push(-1, stack[i]);
}
}
this.ticks_.push({ tm: time_ns, vm: vmState, s: processedStack });
};
function writeJson(s) {
write(JSON.stringify(s, null, 2));
}
JsonProfile.prototype.writeJson = function () {
// Write out the JSON in a partially manual way to avoid creating too-large
// strings in one JSON.stringify call when there are a lot of ticks.
write('{\n')
write(' "code": ');
writeJson(this.codeEntries_);
write(',\n');
write(' "functions": ');
writeJson(this.functionEntries_);
write(',\n');
write(' "ticks": [\n');
for (let i = 0; i < this.ticks_.length; i++) {
write(' ');
writeJson(this.ticks_[i]);
if (i < this.ticks_.length - 1) {
write(',\n');
} else {
write('\n');
}
}
write(' ],\n');
write(' "scripts": ');
writeJson(this.scripts_);
write('}\n');
};