Skip to content

Instantly share code, notes, and snippets.

@flaviut
Last active July 5, 2024 16:08
Show Gist options
  • Save flaviut/c14bfc9b9360969097066a983d911daf to your computer and use it in GitHub Desktop.
Save flaviut/c14bfc9b9360969097066a983d911daf to your computer and use it in GitHub Desktop.
optimized node tickprocessor

An optimized version of tickprocessor from the node source code: https://github.com/nodejs/node/blob/main/deps/v8/tools/

This is also directly compatible with nodejs & doesn't require d8.

Usage:

$ node --max-old-space-size=16000 tickprocessor-driver.mjs --preprocess -j v8-etc.log > output.txt

This output can then be loaded in to speedscope.app

// 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.
export class BaseArgumentsProcessor {
constructor(args) {
this.args_ = args;
this.result_ = this.getDefaultResults();
console.assert(this.result_ !== undefined)
console.assert(this.result_.logFileName !== undefined);
this.argsDispatch_ = this.getArgsDispatch();
console.assert(this.argsDispatch_ !== undefined);
}
getDefaultResults() {
throw "Implement in getDefaultResults in subclass";
}
getArgsDispatch() {
throw "Implement getArgsDispatch in subclass";
}
result() { return this.result_ }
static process(args) {
const processor = new this(args);
if (processor.parse()) {
return processor.result();
} else {
processor.printUsageAndExit();
return false;
}
}
printUsageAndExit() {
console.log('Cmdline args: [options] [log-file-name]\n' +
'Default log file name is "' +
this.result_.logFileName + '".\n');
console.log('Options:');
for (const arg in this.argsDispatch_) {
const synonyms = [arg];
const dispatch = this.argsDispatch_[arg];
for (const synArg in this.argsDispatch_) {
if (arg !== synArg && dispatch === this.argsDispatch_[synArg]) {
synonyms.push(synArg);
delete this.argsDispatch_[synArg];
}
}
console.log(` ${synonyms.join(', ').padEnd(20)} ${dispatch[2]}`);
}
process.exit(2);
}
parse() {
while (this.args_.length) {
let arg = this.args_.shift();
if (arg.charAt(0) != '-') {
this.result_.logFileName = arg;
continue;
}
let userValue = null;
const eqPos = arg.indexOf('=');
if (eqPos != -1) {
userValue = arg.substr(eqPos + 1);
arg = arg.substr(0, eqPos);
}
if (arg in this.argsDispatch_) {
const dispatch = this.argsDispatch_[arg];
const property = dispatch[0];
const defaultValue = dispatch[1];
if (typeof defaultValue == "function") {
userValue = defaultValue(userValue);
} else if (userValue == null) {
userValue = defaultValue;
}
this.result_[property] = userValue;
} else {
return false;
}
}
return true;
}
}
export function parseBool(str) {
if (str == "true" || str == "1") return true;
return false;
}
// 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 { SplayTree } from "./splaytree.mjs";
/**
* The number of alignment bits in a page address.
*/
const kPageAlignment = 12;
/**
* Page size in bytes.
*/
const kPageSize = 1 << kPageAlignment;
/**
* Constructs a mapper that maps addresses into code entries.
*/
export class CodeMap {
/**
* Dynamic code entries. Used for JIT compiled code.
*/
dynamics_ = new SplayTree();
/**
* Name generator for entries having duplicate names.
*/
dynamicsNameGen_ = new NameGenerator();
/**
* Static code entries. Used for statically compiled code.
*/
statics_ = new SplayTree();
/**
* Libraries entries. Used for the whole static code libraries.
*/
libraries_ = new SplayTree();
/**
* Map of memory pages occupied with static code.
*/
pages_ = new Set();
constructor(useBigInt=false) {
this.useBigInt = useBigInt;
this.kPageSize = useBigInt ? BigInt(kPageSize) : kPageSize;
this.kOne = useBigInt ? 1n : 1;
this.kZero = useBigInt ? 0n : 0;
}
/**
* Adds a code entry that might overlap with static code (e.g. for builtins).
*
* @param {number} start The starting address.
* @param {CodeEntry} codeEntry Code entry object.
*/
addAnyCode(start, codeEntry) {
const pageAddr = (start / this.kPageSize) | this.kZero;
if (!this.pages_.has(pageAddr)) return this.addCode(start, codeEntry);
// We might have loaded static code (builtins, bytecode handlers)
// and we get more information later in v8.log with code-creation events.
// Overwrite the existing entries in this case.
let result = this.findInTree_(this.statics_, start);
if (result === null) return this.addCode(start, codeEntry);
const removedNode = this.statics_.remove(start);
this.deleteAllCoveredNodes_(
this.statics_, start, start + removedNode.value.size);
this.statics_.insert(start, codeEntry);
}
/**
* Adds a dynamic (i.e. moveable and discardable) code entry.
*
* @param {number} start The starting address.
* @param {CodeEntry} codeEntry Code entry object.
*/
addCode(start, codeEntry) {
this.deleteAllCoveredNodes_(this.dynamics_, start, start + codeEntry.size);
this.dynamics_.insert(start, codeEntry);
}
/**
* Moves a dynamic code entry. Throws an exception if there is no dynamic
* code entry with the specified starting address.
*
* @param {number} from The starting address of the entry being moved.
* @param {number} to The destination address.
*/
moveCode(from, to) {
const removedNode = this.dynamics_.remove(from);
this.deleteAllCoveredNodes_(this.dynamics_, to, to + removedNode.value.size);
this.dynamics_.insert(to, removedNode.value);
}
/**
* Discards a dynamic code entry. Throws an exception if there is no dynamic
* code entry with the specified starting address.
*
* @param {number} start The starting address of the entry being deleted.
*/
deleteCode(start) {
const removedNode = this.dynamics_.remove(start);
}
/**
* Adds a library entry.
*
* @param {number} start The starting address.
* @param {CodeEntry} codeEntry Code entry object.
*/
addLibrary(start, codeEntry) {
this.markPages_(start, start + codeEntry.size);
this.libraries_.insert(start, codeEntry);
}
/**
* Adds a static code entry.
*
* @param {number} start The starting address.
* @param {CodeEntry} codeEntry Code entry object.
*/
addStaticCode(start, codeEntry) {
this.statics_.insert(start, codeEntry);
}
/**
* @private
*/
markPages_(start, end) {
for (let addr = start; addr <= end; addr += this.kPageSize) {
this.pages_.add((addr / this.kPageSize) | this.kZero);
}
}
/**
* @private
*/
deleteAllCoveredNodes_(tree, start, end) {
const to_delete = [];
let addr = end - this.kOne;
while (addr >= start) {
const node = tree.findGreatestLessThan(addr);
if (node === null) break;
const start2 = node.key, end2 = start2 + node.value.size;
if (start2 < end && start < end2) to_delete.push(start2);
addr = start2 - this.kOne;
}
for (let i = 0, l = to_delete.length; i < l; ++i) tree.remove(to_delete[i]);
}
/**
* @private
*/
isAddressBelongsTo_(addr, node) {
return addr >= node.key && addr < (node.key + node.value.size);
}
/**
* @private
*/
findInTree_(tree, addr) {
const node = tree.findGreatestLessThan(addr);
return node !== null && this.isAddressBelongsTo_(addr, node) ? node : null;
}
/**
* Finds a code entry that contains the specified address. Both static and
* dynamic code entries are considered. Returns the code entry and the offset
* within the entry.
*
* @param {number} addr Address.
*/
findAddress(addr) {
const pageAddr = (addr / this.kPageSize) | this.kZero;
if (this.pages_.has(pageAddr)) {
// Static code entries can contain "holes" of unnamed code.
// In this case, the whole library is assigned to this address.
let result = this.findInTree_(this.statics_, addr);
if (result === null) {
result = this.findInTree_(this.libraries_, addr);
if (result === null) return null;
}
return {entry: result.value, offset: addr - result.key};
}
const max = this.dynamics_.findMax();
if (max === null) return null;
const min = this.dynamics_.findMin();
if (addr >= min.key && addr < (max.key + max.value.size)) {
const dynaEntry = this.findInTree_(this.dynamics_, addr);
if (dynaEntry === null) return null;
// Dedupe entry name.
const entry = dynaEntry.value;
if (!entry.nameUpdated_) {
entry.name = this.dynamicsNameGen_.getName(entry.name);
entry.nameUpdated_ = true;
}
return {entry, offset: addr - dynaEntry.key};
}
return null;
}
/**
* Finds a code entry that contains the specified address. Both static and
* dynamic code entries are considered.
*
* @param {number} addr Address.
*/
findEntry(addr) {
const result = this.findAddress(addr);
return result !== null ? result.entry : null;
}
/**
* Returns a dynamic code entry using its starting address.
*
* @param {number} addr Address.
*/
findDynamicEntryByStartAddress(addr) {
const node = this.dynamics_.find(addr);
return node !== null ? node.value : null;
}
/**
* Returns an array of all dynamic code entries.
*/
getAllDynamicEntries() {
return this.dynamics_.exportValues();
}
/**
* Returns an array of pairs of all dynamic code entries and their addresses.
*/
getAllDynamicEntriesWithAddresses() {
return this.dynamics_.exportKeysAndValues();
}
/**
* Returns an array of all static code entries.
*/
getAllStaticEntries() {
return this.statics_.exportValues();
}
/**
* Returns an array of pairs of all static code entries and their addresses.
*/
getAllStaticEntriesWithAddresses() {
return this.statics_.exportKeysAndValues();
}
/**
* Returns an array of all library entries.
*/
getAllLibraryEntries() {
return this.libraries_.exportValues();
}
/**
* Returns an array of pairs of all library entries and their addresses.
*/
getAllLibraryEntriesWithAddresses() {
return this.libraries_.exportKeysAndValues();
}
}
export class CodeEntry {
constructor(size, opt_name, opt_type) {
/** @type {number} */
this.size = size;
/** @type {string} */
this.name = opt_name || '';
/** @type {string} */
this.type = opt_type || '';
this.nameUpdated_ = false;
/** @type {?string} */
this.source = undefined;
}
getName() {
return this.name;
}
toString() {
return this.name + ': ' + this.size.toString(16);
}
getSourceCode() {
return '';
}
get sourcePosition() {
return this.logEntry.sourcePosition;
}
}
class NameGenerator {
knownNames_ = { __proto__:null }
getName(name) {
if (!(name in this.knownNames_)) {
this.knownNames_[name] = 0;
return name;
}
const count = ++this.knownNames_[name];
return name + ' {' + count + '}';
};
}
// 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.
/**
* Constructs a ConsArray object. It is used mainly for tree traversal.
* In this use case we have lots of arrays that we need to iterate
* sequentally. The internal Array implementation is horribly slow
* when concatenating on large (10K items) arrays due to memory copying.
* That's why we avoid copying memory and insead build a linked list
* of arrays to iterate through.
*
* @constructor
*/
export class ConsArray {
constructor() {
this.tail_ = new ConsArrayCell(null, null);
this.currCell_ = this.tail_;
this.currCellPos_ = 0;
}
/**
* Concatenates another array for iterating. Empty arrays are ignored.
* This operation can be safely performed during ongoing ConsArray
* iteration.
*
* @param {Array} arr Array to concatenate.
*/
concat(arr) {
if (arr.length > 0) {
this.tail_.data = arr;
this.tail_ = this.tail_.next = new ConsArrayCell(null, null);
}
}
/**
* Whether the end of iteration is reached.
*/
atEnd() {
return this.currCell_ === null ||
this.currCell_.data === null ||
this.currCellPos_ >= this.currCell_.data.length;
}
/**
* Returns the current item, moves to the next one.
*/
next() {
const result = this.currCell_.data[this.currCellPos_++];
if (this.currCellPos_ >= this.currCell_.data.length) {
this.currCell_ = this.currCell_.next;
this.currCellPos_ = 0;
}
return result;
}
}
/**
* A cell object used for constructing a list in ConsArray.
*
* @constructor
*/
class ConsArrayCell {
constructor(data, next) {
this.data = data;
this.next = next;
}
}
// 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.
/**
* Creates a CSV lines parser.
*/
export class CsvParser {
/**
* Converts \x00 and \u0000 escape sequences in the given string.
*
* @param {string} input field.
**/
escapeField(string) {
let nextPos = string.indexOf("\\");
if (nextPos === -1) return string;
let result = [string.substring(0, nextPos)];
// Escape sequences of the form \x00 and \u0000;
let pos = 0;
while (nextPos !== -1) {
const escapeIdentifier = string[nextPos + 1];
pos = nextPos + 2;
if (escapeIdentifier === 'n') {
result.push('\n');
nextPos = pos;
} else if (escapeIdentifier === '\\') {
result.push('\\');
nextPos = pos;
} else {
if (escapeIdentifier === 'x') {
// \x00 ascii range escapes consume 2 chars.
nextPos = pos + 2;
} else {
// \u0000 unicode range escapes consume 4 chars.
nextPos = pos + 4;
}
// Convert the selected escape sequence to a single character.
const escapeChars = string.substring(pos, nextPos);
if (escapeChars === '2C') {
result.push(',');
} else {
result.push(String.fromCharCode(parseInt(escapeChars, 16)));
}
}
// Continue looking for the next escape sequence.
pos = nextPos;
nextPos = string.indexOf("\\", pos);
// If there are no more escape sequences consume the rest of the string.
if (nextPos === -1) {
result.push(string.substr(pos));
break;
} else if (pos !== nextPos) {
result.push(string.substring(pos, nextPos));
}
}
return result.join('');
}
/**
* Parses a line of CSV-encoded values. Returns an array of fields.
*
* @param {Buffer} line Input line.
*/
parseLine(buffer) {
let pos = 0;
const endPos = buffer.length;
const fields = [];
if (endPos == 0) return fields;
let nextPos = 0;
while(nextPos !== -1) {
nextPos = buffer.indexOf(',', pos);
let field;
if (nextPos === -1) {
field = buffer.slice(pos);
} else {
field = buffer.slice(pos, nextPos);
}
fields.push(field.toString('latin1'));
pos = nextPos + 1;
};
return fields;
}
}
// Copyright 2011 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.
/**
* @fileoverview Log Reader is used to process log file produced by V8.
*/
import { CsvParser } from "./csvparser.mjs";
// Parses dummy variable for readability;
export function parseString(field) { return field };
export const parseVarArgs = 'parse-var-args';
// Checks fields for numbers that are not safe integers. Returns true if any are
// found.
function containsUnsafeInts(fields) {
for (let i = 0; i < fields.length; i++) {
let field = fields[i];
if ('number' == typeof(field) && !Number.isSafeInteger(field)) return true;
}
return false;
}
/**
* Base class for processing log files.
*
* @param {boolean} timedRange Ignore ticks outside timed range.
* @param {boolean} pairwiseTimedRange Ignore ticks outside pairs of timer
* markers.
* @constructor
*/
export class LogReader {
constructor(timedRange=false, pairwiseTimedRange=false, useBigInt=false) {
this.dispatchTable_ = new Map();
this.timedRange_ = timedRange;
this.pairwiseTimedRange_ = pairwiseTimedRange;
if (pairwiseTimedRange) this.timedRange_ = true;
this.lineNum_ = 0;
this.csvParser_ = new CsvParser();
// Variables for tracking of 'current-time' log entries:
this.hasSeenTimerMarker_ = false;
this.logLinesSinceLastTimerMarker_ = [];
// Flag to parse all numeric fields as BigInt to avoid arithmetic errors
// caused by memory addresses being greater than MAX_SAFE_INTEGER
this.useBigInt = useBigInt;
this.parseFrame = useBigInt ? BigInt : parseInt;
this.hasSeenUnsafeIntegers = false;
this.suppressedWarnings = new Set()
}
/**
* @param {Object} table A table used for parsing and processing
* log records.
* exampleDispatchTable = {
* "log-entry-XXX": {
* parser: [parseString, parseInt, ..., parseVarArgs],
* processor: this.processXXX.bind(this)
* },
* ...
* }
*/
setDispatchTable(table) {
if (Object.getPrototypeOf(table) !== null) {
throw new Error("Dispatch expected table.__proto__=null for speedup");
}
for (let name in table) {
const parser = table[name];
if (parser === undefined) continue;
if (!parser.isAsync) parser.isAsync = false;
if (!Array.isArray(parser.parsers)) {
throw new Error(`Invalid parsers: dispatchTable['${
name}'].parsers should be an Array.`);
}
let type = typeof parser.processor;
if (type !== 'function') {
throw new Error(`Invalid processor: typeof dispatchTable['${
name}'].processor is '${type}' instead of 'function'`);
}
if (!parser.processor.name.startsWith('bound ')) {
parser.processor = parser.processor.bind(this);
}
this.dispatchTable_.set(name, parser);
}
}
/**
* A thin wrapper around shell's 'read' function showing a file name on error.
*/
readFile(fileName) {
try {
return read(fileName);
} catch (e) {
printErr(`file="${fileName}": ${e.message || e}`);
throw e;
}
}
/**
* Used for printing error messages.
*
* @param {string} str Error message.
*/
printError(str) {
// Do nothing.
}
/**
* Processes a portion of V8 profiler event log.
*
* @param {string} chunk A portion of log.
*/
async processLogChunk(chunk) {
let end = chunk.length;
let current = 0;
// Kept for debugging in case of parsing errors.
let lineNumber = 0;
while (current < end) {
const next = chunk.indexOf("\n", current);
if (next === -1) break;
lineNumber++;
const line = chunk.substring(current, next);
current = next + 1;
await this.processLogLine(line);
}
}
/**
* Processes a line of V8 profiler event log.
*
* @param {string} line A line of log.
*/
async processLogLine(line) {
if (!this.timedRange_) {
await this.processLogLine_(line);
return;
}
if (line.startsWith("current-time")) {
if (this.hasSeenTimerMarker_) {
await this.processLog_(this.logLinesSinceLastTimerMarker_);
this.logLinesSinceLastTimerMarker_ = [];
// In pairwise mode, a "current-time" line ends the timed range.
if (this.pairwiseTimedRange_) {
this.hasSeenTimerMarker_ = false;
}
} else {
this.hasSeenTimerMarker_ = true;
}
} else {
if (this.hasSeenTimerMarker_) {
this.logLinesSinceLastTimerMarker_.push(line);
} else if (!line.startsWith("tick")) {
await this.processLogLine_(line);
}
}
}
/**
* Processes stack record.
*
* @param {number} pc Program counter.
* @param {number} func JS Function.
* @param {string[]} stack String representation of a stack.
* @return {number[]} Processed stack.
*/
processStack(pc, func, stack) {
const fullStack = func ? [pc, func] : [pc];
let prevFrame = pc;
const length = stack.length;
for (let i = 0, n = length; i < n; ++i) {
const frame = stack[i];
const firstChar = frame[0];
if (firstChar === '+' || firstChar === '-') {
// An offset from the previous frame.
prevFrame += this.parseFrame(frame);
fullStack.push(prevFrame);
// Filter out possible 'overflow' string.
} else if (firstChar !== 'o') {
fullStack.push(this.parseFrame(frame));
} else {
if(!this.suppressedWarnings.has(frame)) {
console.error(`Dropping unknown tick frame: ${frame}`);
this.suppressedWarnings.add(frame)
}
}
}
return fullStack;
}
/**
* Does a dispatch of a log record.
*
* @param {string[]} fields Log record.
* @private
*/
async dispatchLogRow_(fields) {
// Obtain the dispatch.
const command = fields[0];
const dispatch = this.dispatchTable_.get(command);
if (dispatch === undefined) return;
const parsers = dispatch.parsers;
const length = parsers.length;
// Parse fields.
const parsedFields = new Array(length);
for (let i = 0; i < length; ++i) {
const parser = parsers[i];
if (parser === parseVarArgs) {
parsedFields[i] = fields.slice(1 + i);
break;
} else {
parsedFields[i] = parser(fields[1 + i]);
}
}
if (!this.useBigInt) {
if (!this.hasSeenUnsafeIntegers && containsUnsafeInts(parsedFields)) {
console.warn(`Log line containts unsafe integers: ${fields}`);
this.hasSeenUnsafeIntegers = true;
}
}
// Run the processor.
await dispatch.processor(...parsedFields);
}
/**
* Processes log lines.
*
* @param {string[]} lines Log lines.
* @private
*/
async processLog_(lines) {
for (let i = 0, n = lines.length; i < n; ++i) {
await this.processLogLine_(lines[i]);
}
}
/**
* Processes a single log line.
*
* @param {Buffer} a log line
* @private
*/
async processLogLine_(line) {
if (line.length > 0) {
try {
const fields = this.csvParser_.parseLine(line);
await this.dispatchLogRow_(fields);
} catch (e) {
this.printError(`line ${this.lineNum_ + 1}: ${e.message || e}\n${e.stack}`);
}
}
this.lineNum_++;
}
}
// 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) {
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 {
topDownTree_ = new CallTree();
bottomUpTree_ = new CallTree();
c_entries_ = {__proto__:null};
scripts_ = [];
urlToScript_ = new Map();
warnings = new Set();
constructor(useBigInt=false) {
this.useBigInt = useBigInt;
this.codeMap_ = new CodeMap(useBigInt);
}
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.useBigInt);
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, useBigInt=false) {
super(useBigInt ? 0n : 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) {
process.stdout.write(JSON.stringify(s));
}
function writeJsonArray(name, arr) {
process.stdout.write(` "${name}": [\n`);
for (const [i, entry] of arr.entries()) {
if (entry == null) { continue; }
process.stdout.write(' ');
writeJson(entry);
if (i < arr.length - 1) { process.stdout.write(','); }
process.stdout.write('\n');
}
process.stdout.write(']')
}
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.
process.stdout.write('{\n')
process.stderr.write("\nwriting code");
writeJsonArray('code', this.codeEntries_);
process.stdout.write(',\n');
process.stderr.write("\nwriting functions");
writeJsonArray('functions', this.functionEntries_);
process.stdout.write(',\n');
process.stderr.write("\nwriting ticks")
writeJsonArray('ticks', this.ticks_);
process.stdout.write(',\n');
process.stderr.write("\nwriting scripts")
writeJsonArray('scripts', this.scripts_);
process.stdout.write('\n');
process.stdout.write('}\n');
process.stderr.write("\nwrote output\n")
};
// 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 { ConsArray } from "./consarray.mjs";
/**
* Creates a Profile View builder object.
*
* @param {number} samplingRate Number of ms between profiler ticks.
* @constructor
*/
export function ViewBuilder(samplingRate) {
this.samplingRate = samplingRate;
};
/**
* Builds a profile view for the specified call tree.
*
* @param {CallTree} callTree A call tree.
* @param {boolean} opt_bottomUpViewWeights Whether remapping
* of self weights for a bottom up view is needed.
*/
ViewBuilder.prototype.buildView = function(
callTree, opt_bottomUpViewWeights) {
let head;
const samplingRate = this.samplingRate;
const createViewNode = this.createViewNode;
callTree.traverse(function(node, viewParent) {
const totalWeight = node.totalWeight * samplingRate;
let selfWeight = node.selfWeight * samplingRate;
if (opt_bottomUpViewWeights === true) {
if (viewParent === head) {
selfWeight = totalWeight;
} else {
selfWeight = 0;
}
}
const viewNode = createViewNode(node.label, totalWeight, selfWeight, head);
if (viewParent) {
viewParent.addChild(viewNode);
} else {
head = viewNode;
}
return viewNode;
});
const view = this.createView(head);
return view;
};
/**
* Factory method for a profile view.
*
* @param {ProfileView.Node} head View head node.
* @return {ProfileView} Profile view.
*/
ViewBuilder.prototype.createView = head => new ProfileView(head);
/**
* Factory method for a profile view node.
*
* @param {string} internalFuncName A fully qualified function name.
* @param {number} totalTime Amount of time that application spent in the
* corresponding function and its descendants (not that depending on
* profile they can be either callees or callers.)
* @param {number} selfTime Amount of time that application spent in the
* corresponding function only.
* @param {ProfileView.Node} head Profile view head.
* @return {ProfileView.Node} Profile view node.
*/
ViewBuilder.prototype.createViewNode = (
funcName, totalTime, selfTime, head) =>
new ProfileView.Node(
funcName, totalTime, selfTime, head)
;
/**
* Creates a Profile View object. It allows to perform sorting
* and filtering actions on the profile.
*
* @param {ProfileView.Node} head Head (root) node.
* @constructor
*/
export function ProfileView(head) {
this.head = head;
};
/**
* Sorts the profile view using the specified sort function.
*
* @param {function(ProfileView.Node,
* ProfileView.Node):number} sortFunc A sorting
* functions. Must comply with Array.sort sorting function requirements.
*/
ProfileView.prototype.sort = function(sortFunc) {
this.traverse(function (node) {
node.sortChildren(sortFunc);
});
};
/**
* Traverses profile view nodes in preorder.
*
* @param {function(ProfileView.Node)} f Visitor function.
*/
ProfileView.prototype.traverse = function(f) {
const nodesToTraverse = new ConsArray();
nodesToTraverse.concat([this.head]);
while (!nodesToTraverse.atEnd()) {
const node = nodesToTraverse.next();
f(node);
nodesToTraverse.concat(node.children);
}
};
/**
* Constructs a Profile View node object. Each node object corresponds to
* a function call.
*
* @param {string} internalFuncName A fully qualified function name.
* @param {number} totalTime Amount of time that application spent in the
* corresponding function and its descendants (not that depending on
* profile they can be either callees or callers.)
* @param {number} selfTime Amount of time that application spent in the
* corresponding function only.
* @param {ProfileView.Node} head Profile view head.
* @constructor
*/
ProfileView.Node = function(
internalFuncName, totalTime, selfTime, head) {
this.internalFuncName = internalFuncName;
this.totalTime = totalTime;
this.selfTime = selfTime;
this.head = head;
this.parent = null;
this.children = [];
};
/**
* Returns a share of the function's total time in its parent's total time.
*/
ProfileView.Node.prototype.__defineGetter__(
'parentTotalPercent',
function() { return this.totalTime /
(this.parent ? this.parent.totalTime : this.totalTime) * 100.0; });
/**
* Adds a child to the node.
*
* @param {ProfileView.Node} node Child node.
*/
ProfileView.Node.prototype.addChild = function(node) {
node.parent = this;
this.children.push(node);
};
/**
* Sorts all the node's children recursively.
*
* @param {function(ProfileView.Node,
* ProfileView.Node):number} sortFunc A sorting
* functions. Must comply with Array.sort sorting function requirements.
*/
ProfileView.Node.prototype.sortChildren = function(
sortFunc) {
this.children.sort(sortFunc);
};
// Copyright 2013 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.
// This is a copy from blink dev tools, see:
// http://src.chromium.org/viewvc/blink/trunk/Source/devtools/front_end/SourceMap.js
// revision: 153407
// Added to make the file work without dev tools
export const WebInspector = {};
WebInspector.ParsedURL = {};
WebInspector.ParsedURL.completeURL = function(){};
// start of original file content
/*
* Copyright (C) 2012 Google Inc. 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.
*/
/**
* Implements Source Map V3 model. See http://code.google.com/p/closure-compiler/wiki/SourceMaps
* for format description.
* @constructor
* @param {string} sourceMappingURL
* @param {SourceMapV3} payload
*/
WebInspector.SourceMap = function(sourceMappingURL, payload)
{
if (!WebInspector.SourceMap.prototype._base64Map) {
const base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
WebInspector.SourceMap.prototype._base64Map = {};
for (let i = 0; i < base64Digits.length; ++i)
WebInspector.SourceMap.prototype._base64Map[base64Digits.charAt(i)] = i;
}
this._sourceMappingURL = sourceMappingURL;
this._reverseMappingsBySourceURL = {};
this._mappings = [];
this._sources = {};
this._sourceContentByURL = {};
this._parseMappingPayload(payload);
}
/**
* @param {string} sourceMapURL
* @param {string} compiledURL
* @param {function(WebInspector.SourceMap)} callback
*/
WebInspector.SourceMap.load = function(sourceMapURL, compiledURL, callback)
{
NetworkAgent.loadResourceForFrontend(WebInspector.resourceTreeModel.mainFrame.id, sourceMapURL, undefined, contentLoaded.bind(this));
/**
* @param {?Protocol.Error} error
* @param {number} statusCode
* @param {NetworkAgent.Headers} headers
* @param {string} content
*/
function contentLoaded(error, statusCode, headers, content)
{
if (error || !content || statusCode >= 400) {
console.error(`Could not load content for ${sourceMapURL} : ${error || (`HTTP status code: ${statusCode}`)}`);
callback(null);
return;
}
if (content.slice(0, 3) === ")]}")
content = content.substring(content.indexOf('\n'));
try {
const payload = /** @type {SourceMapV3} */ (JSON.parse(content));
const baseURL = sourceMapURL.startsWith("data:") ? compiledURL : sourceMapURL;
callback(new WebInspector.SourceMap(baseURL, payload));
} catch(e) {
console.error(e.message);
callback(null);
}
}
}
WebInspector.SourceMap.prototype = {
/**
* @return {string[]}
*/
sources()
{
return Object.keys(this._sources);
},
/**
* @param {string} sourceURL
* @return {string|undefined}
*/
sourceContent(sourceURL)
{
return this._sourceContentByURL[sourceURL];
},
/**
* @param {string} sourceURL
* @param {WebInspector.ResourceType} contentType
* @return {WebInspector.ContentProvider}
*/
sourceContentProvider(sourceURL, contentType)
{
const lastIndexOfDot = sourceURL.lastIndexOf(".");
const extension = lastIndexOfDot !== -1 ? sourceURL.substr(lastIndexOfDot + 1) : "";
const mimeType = WebInspector.ResourceType.mimeTypesForExtensions[extension.toLowerCase()];
const sourceContent = this.sourceContent(sourceURL);
if (sourceContent)
return new WebInspector.StaticContentProvider(contentType, sourceContent, mimeType);
return new WebInspector.CompilerSourceMappingContentProvider(sourceURL, contentType, mimeType);
},
/**
* @param {SourceMapV3} mappingPayload
*/
_parseMappingPayload(mappingPayload)
{
if (mappingPayload.sections)
this._parseSections(mappingPayload.sections);
else
this._parseMap(mappingPayload, 0, 0);
},
/**
* @param {Array.<SourceMapV3.Section>} sections
*/
_parseSections(sections)
{
for (let i = 0; i < sections.length; ++i) {
const section = sections[i];
this._parseMap(section.map, section.offset.line, section.offset.column);
}
},
/**
* @param {number} lineNumber in compiled resource
* @param {number} columnNumber in compiled resource
* @return {?Array}
*/
findEntry(lineNumber, columnNumber)
{
let first = 0;
let count = this._mappings.length;
while (count > 1) {
const step = count >> 1;
const middle = first + step;
const mapping = this._mappings[middle];
if (lineNumber < mapping[0] || (lineNumber === mapping[0] && columnNumber < mapping[1]))
count = step;
else {
first = middle;
count -= step;
}
}
const entry = this._mappings[first];
if (!first && entry && (lineNumber < entry[0] || (lineNumber === entry[0] && columnNumber < entry[1])))
return null;
return entry;
},
/**
* @param {string} sourceURL of the originating resource
* @param {number} lineNumber in the originating resource
* @return {Array}
*/
findEntryReversed(sourceURL, lineNumber)
{
const mappings = this._reverseMappingsBySourceURL[sourceURL];
for ( ; lineNumber < mappings.length; ++lineNumber) {
const mapping = mappings[lineNumber];
if (mapping)
return mapping;
}
return this._mappings[0];
},
/**
* @override
*/
_parseMap(map, lineNumber, columnNumber)
{
let sourceIndex = 0;
let sourceLineNumber = 0;
let sourceColumnNumber = 0;
let nameIndex = 0;
const sources = [];
const originalToCanonicalURLMap = {};
for (let i = 0; i < map.sources.length; ++i) {
const originalSourceURL = map.sources[i];
let sourceRoot = map.sourceRoot || "";
if (sourceRoot && !sourceRoot.endsWith("/")) sourceRoot += "/";
const href = sourceRoot + originalSourceURL;
const url = WebInspector.ParsedURL.completeURL(this._sourceMappingURL, href) || href;
originalToCanonicalURLMap[originalSourceURL] = url;
sources.push(url);
this._sources[url] = true;
if (map.sourcesContent && map.sourcesContent[i]) {
this._sourceContentByURL[url] = map.sourcesContent[i];
}
}
const stringCharIterator = new WebInspector.SourceMap.StringCharIterator(map.mappings);
let sourceURL = sources[sourceIndex];
while (true) {
if (stringCharIterator.peek() === ",")
stringCharIterator.next();
else {
while (stringCharIterator.peek() === ";") {
lineNumber += 1;
columnNumber = 0;
stringCharIterator.next();
}
if (!stringCharIterator.hasNext())
break;
}
columnNumber += this._decodeVLQ(stringCharIterator);
if (this._isSeparator(stringCharIterator.peek())) {
this._mappings.push([lineNumber, columnNumber]);
continue;
}
const sourceIndexDelta = this._decodeVLQ(stringCharIterator);
if (sourceIndexDelta) {
sourceIndex += sourceIndexDelta;
sourceURL = sources[sourceIndex];
}
sourceLineNumber += this._decodeVLQ(stringCharIterator);
sourceColumnNumber += this._decodeVLQ(stringCharIterator);
if (!this._isSeparator(stringCharIterator.peek()))
nameIndex += this._decodeVLQ(stringCharIterator);
this._mappings.push([lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber]);
}
for (let i = 0; i < this._mappings.length; ++i) {
const mapping = this._mappings[i];
const url = mapping[2];
if (!url) continue;
if (!this._reverseMappingsBySourceURL[url]) {
this._reverseMappingsBySourceURL[url] = [];
}
const reverseMappings = this._reverseMappingsBySourceURL[url];
const sourceLine = mapping[3];
if (!reverseMappings[sourceLine]) {
reverseMappings[sourceLine] = [mapping[0], mapping[1]];
}
}
},
/**
* @param {string} char
* @return {boolean}
*/
_isSeparator(char)
{
return char === "," || char === ";";
},
/**
* @param {WebInspector.SourceMap.StringCharIterator} stringCharIterator
* @return {number}
*/
_decodeVLQ(stringCharIterator)
{
// Read unsigned value.
let result = 0;
let shift = 0;
let digit;
do {
digit = this._base64Map[stringCharIterator.next()];
result += (digit & this._VLQ_BASE_MASK) << shift;
shift += this._VLQ_BASE_SHIFT;
} while (digit & this._VLQ_CONTINUATION_MASK);
// Fix the sign.
const negate = result & 1;
// Use unsigned right shift, so that the 32nd bit is properly shifted
// to the 31st, and the 32nd becomes unset.
result >>>= 1;
if (negate) {
// We need to OR 0x80000000 here to ensure the 32nd bit (the sign bit
// in a 32bit int) is always set for negative numbers. If `result`
// were 1, (meaning `negate` is true and all other bits were zeros),
// `result` would now be 0. But -0 doesn't flip the 32nd bit as
// intended. All other numbers will successfully set the 32nd bit
// without issue, so doing this is a noop for them.
return -result | 0x80000000;
}
return result;
},
_VLQ_BASE_SHIFT: 5,
_VLQ_BASE_MASK: (1 << 5) - 1,
_VLQ_CONTINUATION_MASK: 1 << 5
}
/**
* @constructor
* @param {string} string
*/
WebInspector.SourceMap.StringCharIterator = function(string)
{
this._string = string;
this._position = 0;
}
WebInspector.SourceMap.StringCharIterator.prototype = {
/**
* @return {string}
*/
next()
{
return this._string.charAt(this._position++);
},
/**
* @return {string}
*/
peek()
{
return this._string.charAt(this._position);
},
/**
* @return {boolean}
*/
hasNext()
{
return this._position < this._string.length;
}
}
// 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.
/**
* Constructs a Splay tree. A splay tree is a self-balancing binary
* search tree with the additional property that recently accessed
* elements are quick to access again. It performs basic operations
* such as insertion, look-up and removal in O(log(n)) amortized time.
*
* @constructor
*/
export class SplayTree {
/**
* Pointer to the root node of the tree.
*
* @type {SplayTreeNode}
* @private
*/
root_ = null;
/**
* @return {boolean} Whether the tree is empty.
*/
isEmpty() {
return this.root_ === null;
}
/**
* Inserts a node into the tree with the specified key and value if
* the tree does not already contain a node with the specified key. If
* the value is inserted, it becomes the root of the tree.
*
* @param {number} key Key to insert into the tree.
* @param {*} value Value to insert into the tree.
*/
insert(key, value) {
if (this.isEmpty()) {
this.root_ = new SplayTreeNode(key, value);
return;
}
// Splay on the key to move the last node on the search path for
// the key to the root of the tree.
this.splay_(key);
if (this.root_.key == key) return;
const node = new SplayTreeNode(key, value);
if (key > this.root_.key) {
node.left = this.root_;
node.right = this.root_.right;
this.root_.right = null;
} else {
node.right = this.root_;
node.left = this.root_.left;
this.root_.left = null;
}
this.root_ = node;
}
/**
* Removes a node with the specified key from the tree if the tree
* contains a node with this key. The removed node is returned. If the
* key is not found, an exception is thrown.
*
* @param {number} key Key to find and remove from the tree.
* @return {SplayTreeNode} The removed node.
*/
remove(key) {
if (this.isEmpty()) {
throw Error(`Key not found: ${key}`);
}
this.splay_(key);
if (this.root_.key != key) {
throw Error(`Key not found: ${key}`);
}
const removed = this.root_;
if (this.root_.left === null) {
this.root_ = this.root_.right;
} else {
const { right } = this.root_;
this.root_ = this.root_.left;
// Splay to make sure that the new root has an empty right child.
this.splay_(key);
// Insert the original right child as the right child of the new
// root.
this.root_.right = right;
}
return removed;
}
/**
* Returns the node having the specified key or null if the tree doesn't contain
* a node with the specified key.
*
* @param {number} key Key to find in the tree.
* @return {SplayTreeNode} Node having the specified key.
*/
find(key) {
if (this.isEmpty()) return null;
this.splay_(key);
return this.root_.key == key ? this.root_ : null;
}
/**
* @return {SplayTreeNode} Node having the minimum key value.
*/
findMin() {
if (this.isEmpty()) return null;
let current = this.root_;
while (current.left !== null) {
current = current.left;
}
return current;
}
/**
* @return {SplayTreeNode} Node having the maximum key value.
*/
findMax(opt_startNode) {
if (this.isEmpty()) return null;
let current = opt_startNode || this.root_;
while (current.right !== null) {
current = current.right;
}
return current;
}
/**
* @return {SplayTreeNode} Node having the maximum key value that
* is less or equal to the specified key value.
*/
findGreatestLessThan(key) {
if (this.isEmpty()) return null;
// Splay on the key to move the node with the given key or the last
// node on the search path to the top of the tree.
this.splay_(key);
// Now the result is either the root node or the greatest node in
// the left subtree.
if (this.root_.key <= key) {
return this.root_;
} else if (this.root_.left !== null) {
return this.findMax(this.root_.left);
} else {
return null;
}
}
/**
* @return {Array<*>} An array containing all the values of tree's nodes paired
* with keys.
*/
exportKeysAndValues() {
const result = [];
this.traverse_(function(node) { result.push([node.key, node.value]); });
return result;
}
/**
* @return {Array<*>} An array containing all the values of tree's nodes.
*/
exportValues() {
const result = [];
this.traverse_(function(node) { result.push(node.value) });
return result;
}
/**
* Perform the splay operation for the given key. Moves the node with
* the given key to the top of the tree. If no node has the given
* key, the last node on the search path is moved to the top of the
* tree. This is the simplified top-down splaying algorithm from:
* "Self-adjusting Binary Search Trees" by Sleator and Tarjan
*
* @param {number} key Key to splay the tree on.
* @private
*/
splay_(key) {
if (this.isEmpty()) return;
// Create a dummy node. The use of the dummy node is a bit
// counter-intuitive: The right child of the dummy node will hold
// the L tree of the algorithm. The left child of the dummy node
// will hold the R tree of the algorithm. Using a dummy node, left
// and right will always be nodes and we avoid special cases.
let dummy, left, right;
dummy = left = right = new SplayTreeNode(null, null);
let current = this.root_;
while (true) {
if (key < current.key) {
if (current.left === null) break;
if (key < current.left.key) {
// Rotate right.
const tmp = current.left;
current.left = tmp.right;
tmp.right = current;
current = tmp;
if (current.left === null) break;
}
// Link right.
right.left = current;
right = current;
current = current.left;
} else if (key > current.key) {
if (current.right === null) break;
if (key > current.right.key) {
// Rotate left.
const tmp = current.right;
current.right = tmp.left;
tmp.left = current;
current = tmp;
if (current.right === null) break;
}
// Link left.
left.right = current;
left = current;
current = current.right;
} else {
break;
}
}
// Assemble.
left.right = current.left;
right.left = current.right;
current.left = dummy.right;
current.right = dummy.left;
this.root_ = current;
}
/**
* Performs a preorder traversal of the tree.
*
* @param {function(SplayTreeNode)} f Visitor function.
* @private
*/
traverse_(f) {
const nodesToVisit = [this.root_];
while (nodesToVisit.length > 0) {
const node = nodesToVisit.shift();
if (node === null) continue;
f(node);
nodesToVisit.push(node.left);
nodesToVisit.push(node.right);
}
}
}
/**
* Constructs a Splay tree node.
*
* @param {number} key Key.
* @param {*} value Value.
*/
class SplayTreeNode {
constructor(key, value) {
this.key = key;
this.value = value;
/**
* @type {SplayTreeNode}
*/
this.left = null;
/**
* @type {SplayTreeNode}
*/
this.right = null;
}
};
#!/bin/bash
node --max-old-space-size=16000 tickprocessor-driver.mjs --preprocess -j testlog.log > /dev/null
// Copyright 2012 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 { ArgumentsProcessor, TickProcessor } from "./tickprocessor.mjs";
const params = ArgumentsProcessor.process(process.argv);
const tickProcessor = TickProcessor.fromParams(params);
await tickProcessor.processLogFile(params.logFileName);
if (params.serializeVMSymbols) {
tickProcessor.printVMSymbols();
} else {
tickProcessor.printStatistics();
}
// Copyright 2012 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 { LogReader, parseString, parseVarArgs } from "./logreader.mjs";
import { BaseArgumentsProcessor, parseBool } from "./arguments.mjs";
import { Profile, JsonProfile } from "./profile.mjs";
import { ViewBuilder } from "./profile_view.mjs";
import { WebInspector} from "./sourcemap.mjs";
import * as stream from 'node:stream';
import * as fs from 'node:fs/promises'
class V8Profile extends Profile {
static IC_RE =
/^(LoadGlobalIC: )|(Handler: )|(?:CallIC|LoadIC|StoreIC)|(?:Builtin: (?:Keyed)?(?:Load|Store)IC_)/;
static BYTECODES_RE = /^(BytecodeHandler: )/;
static SPARKPLUG_HANDLERS_RE = /^(Builtin: .*Baseline.*)/;
static BUILTINS_RE = /^(Builtin: )/;
static STUBS_RE = /^(Stub: )/;
constructor(separateIc, separateBytecodes, separateBuiltins, separateStubs,
separateSparkplugHandlers) {
super();
const regexps = [];
if (!separateIc) regexps.push(V8Profile.IC_RE);
if (!separateBytecodes) regexps.push(V8Profile.BYTECODES_RE);
if (!separateBuiltins) regexps.push(V8Profile.BUILTINS_RE);
if (!separateStubs) regexps.push(V8Profile.STUBS_RE);
if (regexps.length > 0) {
this.skipThisFunction = function(name) {
for (let i = 0; i < regexps.length; i++) {
if (regexps[i].test(name)) return true;
}
return false;
};
}
}
}
class CppEntriesProvider {
constructor() {
this._isEnabled = true;
}
inRange(funcInfo, start, end) {
return funcInfo.start >= start && funcInfo.end <= end;
}
async parseVmSymbols(libName, libStart, libEnd, libASLRSlide, processorFunc) {
if (!this._isEnabled) return;
await this.loadSymbols(libName);
let lastUnknownSize;
let lastAdded;
let addEntry = (funcInfo) => {
// Several functions can be mapped onto the same address. To avoid
// creating zero-sized entries, skip such duplicates.
// Also double-check that function belongs to the library address space.
if (lastUnknownSize &&
lastUnknownSize.start < funcInfo.start) {
// Try to update lastUnknownSize based on new entries start position.
lastUnknownSize.end = funcInfo.start;
if ((!lastAdded ||
!this.inRange(lastUnknownSize, lastAdded.start, lastAdded.end)) &&
this.inRange(lastUnknownSize, libStart, libEnd)) {
processorFunc(
lastUnknownSize.name, lastUnknownSize.start, lastUnknownSize.end);
lastAdded = lastUnknownSize;
}
}
lastUnknownSize = undefined;
if (funcInfo.end) {
// Skip duplicates that have the same start address as the last added.
if ((!lastAdded || lastAdded.start != funcInfo.start) &&
this.inRange(funcInfo, libStart, libEnd)) {
processorFunc(funcInfo.name, funcInfo.start, funcInfo.end);
lastAdded = funcInfo;
}
} else {
// If a funcInfo doesn't have an end, try to match it up with the next
// entry.
lastUnknownSize = funcInfo;
}
}
while (true) {
const funcInfo = this.parseNextLine();
if (funcInfo === null) continue;
if (funcInfo === false) break;
if (funcInfo.start < libStart - libASLRSlide &&
funcInfo.start < libEnd - libStart) {
funcInfo.start += libStart;
} else {
funcInfo.start += libASLRSlide;
}
if (funcInfo.size) {
funcInfo.end = funcInfo.start + funcInfo.size;
}
addEntry(funcInfo);
}
addEntry({ name: '', start: libEnd });
}
async loadSymbols(libName) {}
async loadSymbolsRemote(platform, libName) {
this.parsePos = 0;
const url = new URL("http://localhost:8000/v8/loadVMSymbols");
url.searchParams.set('libName', libName);
url.searchParams.set('platform', platform);
this._setRemoteQueryParams(url.searchParams);
let response;
let json;
try {
response = await fetch(url, { timeout: 20 });
if (response.status == 404) {
throw new Error(
`Local symbol server returned 404: ${await response.text()}`);
}
json = await response.json();
if (json.error) console.warn(json.error);
} catch (e) {
if (!response || response.status == 404) {
// Assume that the local symbol server is not reachable.
console.warn("Disabling remote symbol loading:", e);
this._isEnabled = false;
return;
}
}
this._handleRemoteSymbolsResult(json);
}
_setRemoteQueryParams(searchParams) {
// Subclass responsibility.
}
_handleRemoteSymbolsResult(json) {
this.symbols = json.symbols;
}
parseNextLine() { return false }
}
export class LinuxCppEntriesProvider extends CppEntriesProvider {
constructor(nmExec, objdumpExec, targetRootFS, apkEmbeddedLibrary) {
super();
this.symbols = [];
// File offset of a symbol minus the virtual address of a symbol found in
// the symbol table.
this.fileOffsetMinusVma = 0;
this.parsePos = 0;
this.nmExec = nmExec;
this.objdumpExec = objdumpExec;
this.targetRootFS = targetRootFS;
this.apkEmbeddedLibrary = apkEmbeddedLibrary;
this.FUNC_RE = /^([0-9a-fA-F]{8,16}) ([0-9a-fA-F]{8,16} )?[tTwW] (.*)$/;
}
_setRemoteQueryParams(searchParams) {
super._setRemoteQueryParams(searchParams);
searchParams.set('targetRootFS', this.targetRootFS ?? "");
searchParams.set('apkEmbeddedLibrary', this.apkEmbeddedLibrary);
}
_handleRemoteSymbolsResult(json) {
super._handleRemoteSymbolsResult(json);
this.fileOffsetMinusVma = json.fileOffsetMinusVma;
}
async loadSymbols(libName) {
this.parsePos = 0;
if (this.apkEmbeddedLibrary && libName.endsWith('.apk')) {
libName = this.apkEmbeddedLibrary;
}
if (this.targetRootFS) {
libName = libName.substring(libName.lastIndexOf('/') + 1);
libName = this.targetRootFS + libName;
}
try {
this.symbols = [
os.system(this.nmExec, ['-C', '-n', '-S', libName], -1, -1),
os.system(this.nmExec, ['-C', '-n', '-S', '-D', libName], -1, -1)
];
const objdumpOutput = os.system(this.objdumpExec, ['-h', libName], -1, -1);
for (const line of objdumpOutput.split('\n')) {
const [, sectionName, , vma, , fileOffset] = line.trim().split(/\s+/);
if (sectionName === ".text") {
this.fileOffsetMinusVma = parseInt(fileOffset, 16) - parseInt(vma, 16);
}
}
} catch (e) {
// If the library cannot be found on this system let's not panic.
this.symbols = ['', ''];
}
}
parseNextLine() {
if (this.symbols.length == 0) return false;
const lineEndPos = this.symbols[0].indexOf('\n', this.parsePos);
if (lineEndPos == -1) {
this.symbols.shift();
this.parsePos = 0;
return this.parseNextLine();
}
const line = this.symbols[0].substring(this.parsePos, lineEndPos);
this.parsePos = lineEndPos + 1;
const fields = line.match(this.FUNC_RE);
let funcInfo = null;
if (fields) {
funcInfo = { name: fields[3], start: parseInt(fields[1], 16) + this.fileOffsetMinusVma };
if (fields[2]) {
funcInfo.size = parseInt(fields[2], 16);
}
}
return funcInfo;
}
}
export class RemoteLinuxCppEntriesProvider extends LinuxCppEntriesProvider {
async loadSymbols(libName) {
return this.loadSymbolsRemote('linux', libName);
}
}
export class MacOSCppEntriesProvider extends LinuxCppEntriesProvider {
constructor(nmExec, objdumpExec, targetRootFS, apkEmbeddedLibrary) {
super(nmExec, objdumpExec, targetRootFS, apkEmbeddedLibrary);
// Note an empty group. It is required, as LinuxCppEntriesProvider expects 3 groups.
this.FUNC_RE = /^([0-9a-fA-F]{8,16})() (.*)$/;
}
async loadSymbols(libName) {
this.parsePos = 0;
libName = this.targetRootFS + libName;
// It seems that in OS X `nm` thinks that `-f` is a format option, not a
// "flat" display option flag.
try {
this.symbols = [
os.system(this.nmExec, ['--demangle', '-n', libName], -1, -1),
''];
} catch (e) {
// If the library cannot be found on this system let's not panic.
this.symbols = '';
}
}
}
export class RemoteMacOSCppEntriesProvider extends LinuxCppEntriesProvider {
async loadSymbols(libName) {
return this.loadSymbolsRemote('macos', libName);
}
}
export class WindowsCppEntriesProvider extends CppEntriesProvider {
constructor(_ignored_nmExec, _ignored_objdumpExec, targetRootFS,
_ignored_apkEmbeddedLibrary) {
super();
this.targetRootFS = targetRootFS;
this.symbols = '';
this.parsePos = 0;
}
static FILENAME_RE = /^(.*)\.([^.]+)$/;
static FUNC_RE =
/^\s+0001:[0-9a-fA-F]{8}\s+([_\?@$0-9a-zA-Z]+)\s+([0-9a-fA-F]{8}).*$/;
static IMAGE_BASE_RE =
/^\s+0000:00000000\s+___ImageBase\s+([0-9a-fA-F]{8}).*$/;
// This is almost a constant on Windows.
static EXE_IMAGE_BASE = 0x00400000;
loadSymbols(libName) {
libName = this.targetRootFS + libName;
const fileNameFields = libName.match(WindowsCppEntriesProvider.FILENAME_RE);
if (!fileNameFields) return;
const mapFileName = `${fileNameFields[1]}.map`;
this.moduleType_ = fileNameFields[2].toLowerCase();
try {
this.symbols = read(mapFileName);
} catch (e) {
// If .map file cannot be found let's not panic.
this.symbols = '';
}
}
parseNextLine() {
const lineEndPos = this.symbols.indexOf('\r\n', this.parsePos);
if (lineEndPos == -1) {
return false;
}
const line = this.symbols.substring(this.parsePos, lineEndPos);
this.parsePos = lineEndPos + 2;
// Image base entry is above all other symbols, so we can just
// terminate parsing.
const imageBaseFields = line.match(WindowsCppEntriesProvider.IMAGE_BASE_RE);
if (imageBaseFields) {
const imageBase = parseInt(imageBaseFields[1], 16);
if ((this.moduleType_ == 'exe') !=
(imageBase == WindowsCppEntriesProvider.EXE_IMAGE_BASE)) {
return false;
}
}
const fields = line.match(WindowsCppEntriesProvider.FUNC_RE);
return fields ?
{ name: this.unmangleName(fields[1]), start: parseInt(fields[2], 16) } :
null;
}
/**
* Performs very simple unmangling of C++ names.
*
* Does not handle arguments and template arguments. The mangled names have
* the form:
*
* ?LookupInDescriptor@JSObject@internal@v8@@...arguments info...
*/
unmangleName(name) {
// Empty or non-mangled name.
if (name.length < 1 || name.charAt(0) != '?') return name;
const nameEndPos = name.indexOf('@@');
const components = name.substring(1, nameEndPos).split('@');
components.reverse();
return components.join('::');
}
}
export class ArgumentsProcessor extends BaseArgumentsProcessor {
getArgsDispatch() {
let dispatch = {
__proto__:null,
'-j': ['stateFilter', TickProcessor.VmStates.JS,
'Show only ticks from JS VM state'],
'-g': ['stateFilter', TickProcessor.VmStates.GC,
'Show only ticks from GC VM state'],
'-p': ['stateFilter', TickProcessor.VmStates.PARSER,
'Show only ticks from PARSER VM state'],
'-b': ['stateFilter', TickProcessor.VmStates.BYTECODE_COMPILER,
'Show only ticks from BYTECODE_COMPILER VM state'],
'-c': ['stateFilter', TickProcessor.VmStates.COMPILER,
'Show only ticks from COMPILER VM state'],
'-o': ['stateFilter', TickProcessor.VmStates.OTHER,
'Show only ticks from OTHER VM state'],
'-e': ['stateFilter', TickProcessor.VmStates.EXTERNAL,
'Show only ticks from EXTERNAL VM state'],
'--filter-runtime-timer': ['runtimeTimerFilter', null,
'Show only ticks matching the given runtime timer scope'],
'--call-graph-size': ['callGraphSize', TickProcessor.CALL_GRAPH_SIZE,
'Set the call graph size'],
'--ignore-unknown': ['ignoreUnknown', true,
'Exclude ticks of unknown code entries from processing'],
'--separate-ic': ['separateIc', parseBool,
'Separate IC entries'],
'--separate-bytecodes': ['separateBytecodes', parseBool,
'Separate Bytecode entries'],
'--separate-builtins': ['separateBuiltins', parseBool,
'Separate Builtin entries'],
'--separate-stubs': ['separateStubs', parseBool,
'Separate Stub entries'],
'--separate-sparkplug-handlers': ['separateSparkplugHandlers', parseBool,
'Separate Sparkplug Handler entries'],
'--linux': ['platform', 'linux',
'Specify that we are running on *nix platform'],
'--windows': ['platform', 'windows',
'Specify that we are running on Windows platform'],
'--mac': ['platform', 'macos',
'Specify that we are running on Mac OS X platform'],
'--nm': ['nm', 'nm',
'Specify the \'nm\' executable to use (e.g. --nm=/my_dir/nm)'],
'--objdump': ['objdump', 'objdump',
'Specify the \'objdump\' executable to use (e.g. --objdump=/my_dir/objdump)'],
'--target': ['targetRootFS', '',
'Specify the target root directory for cross environment'],
'--apk-embedded-library': ['apkEmbeddedLibrary', '',
'Specify the path of the embedded library for Android traces'],
'--range': ['range', 'auto,auto',
'Specify the range limit as [start],[end]'],
'--distortion': ['distortion', 0,
'Specify the logging overhead in picoseconds'],
'--source-map': ['sourceMap', null,
'Specify the source map that should be used for output'],
'--timed-range': ['timedRange', true,
'Ignore ticks before first and after last Date.now() call'],
'--pairwise-timed-range': ['pairwiseTimedRange', true,
'Ignore ticks outside pairs of Date.now() calls'],
'--only-summary': ['onlySummary', true,
'Print only tick summary, exclude other information'],
'--serialize-vm-symbols': ['serializeVMSymbols', true,
'Print all C++ symbols and library addresses as JSON data'],
'--preprocess': ['preprocessJson', true,
'Preprocess for consumption with web interface']
};
dispatch['--js'] = dispatch['-j'];
dispatch['--gc'] = dispatch['-g'];
dispatch['--compiler'] = dispatch['-c'];
dispatch['--other'] = dispatch['-o'];
dispatch['--external'] = dispatch['-e'];
dispatch['--ptr'] = dispatch['--pairwise-timed-range'];
return dispatch;
}
getDefaultResults() {
return {
logFileName: 'v8.log',
platform: 'linux',
stateFilter: null,
callGraphSize: 5,
ignoreUnknown: false,
separateIc: true,
separateBytecodes: false,
separateBuiltins: true,
separateStubs: true,
separateSparkplugHandlers: false,
preprocessJson: null,
sourceMap: null,
targetRootFS: '',
nm: 'nm',
objdump: 'objdump',
range: 'auto,auto',
distortion: 0,
timedRange: false,
pairwiseTimedRange: false,
onlySummary: false,
runtimeTimerFilter: null,
serializeVMSymbols: false,
};
}
}
class LineReader extends stream.Transform {
constructor() {
super({ objectMode: true });
this._lastLineData = Buffer.alloc(0);
this.byteCount = 0;
}
_transform(chunk, _encoding, done) {
this._lastLineData = Buffer.concat([this._lastLineData, chunk]);
let lineStart = 0;
let lineEnd = this._lastLineData.indexOf('\n');
while (lineEnd !== -1) {
this.push(this._lastLineData.slice(lineStart, lineEnd));
this.byteCount += (lineEnd - lineStart);
lineStart = lineEnd + 1;
lineEnd = this._lastLineData.indexOf('\n', lineStart);
}
this._lastLineData = this._lastLineData.slice(lineStart);
done();
}
_flush(done) {
if (this._lastLineData.length) {
this.push(this._lastLineData);
this.byteCount += this._lastLineData.length;
this._lastLineData = Buffer.alloc(0);
}
done();
}
}
export class TickProcessor extends LogReader {
static EntriesProvider = {
'linux': LinuxCppEntriesProvider,
'windows': WindowsCppEntriesProvider,
'macos': MacOSCppEntriesProvider
};
static fromParams(params, entriesProvider) {
if (entriesProvider == undefined) {
entriesProvider = new this.EntriesProvider[params.platform](
params.nm, params.objdump, params.targetRootFS,
params.apkEmbeddedLibrary);
}
return new TickProcessor(
entriesProvider,
params.separateIc,
params.separateBytecodes,
params.separateBuiltins,
params.separateStubs,
params.separateSparkplugHandlers,
params.callGraphSize,
params.ignoreUnknown,
params.stateFilter,
params.distortion,
params.range,
params.sourceMap,
params.timedRange,
params.pairwiseTimedRange,
params.onlySummary,
params.runtimeTimerFilter,
params.preprocessJson);
}
constructor(
cppEntriesProvider,
separateIc,
separateBytecodes,
separateBuiltins,
separateStubs,
separateSparkplugHandlers,
callGraphSize,
ignoreUnknown,
stateFilter,
distortion,
range,
sourceMap,
timedRange,
pairwiseTimedRange,
onlySummary,
runtimeTimerFilter,
preprocessJson) {
super(timedRange, pairwiseTimedRange);
this.setDispatchTable({
__proto__: null,
'shared-library': {
parsers: [parseString, parseInt, parseInt, parseInt],
processor: this.processSharedLibrary
},
'code-creation': {
parsers: [parseString, parseInt, parseInt, parseInt, parseInt,
parseString, parseVarArgs],
processor: this.processCodeCreation
},
'code-deopt': {
parsers: [parseInt, parseInt, parseInt, parseInt, parseInt,
parseString, parseString, parseString],
processor: this.processCodeDeopt
},
'code-move': {
parsers: [parseInt, parseInt,],
processor: this.processCodeMove
},
'code-delete': {
parsers: [parseInt],
processor: this.processCodeDelete
},
'code-source-info': {
parsers: [parseInt, parseInt, parseInt, parseInt, parseString,
parseString, parseString],
processor: this.processCodeSourceInfo
},
'script-source': {
parsers: [parseInt, parseString, parseString],
processor: this.processScriptSource
},
'sfi-move': {
parsers: [parseInt, parseInt],
processor: this.processFunctionMove
},
'active-runtime-timer': {
parsers: [parseString],
processor: this.processRuntimeTimerEvent
},
'tick': {
parsers: [parseInt, parseInt, parseInt,
parseInt, parseInt, parseVarArgs],
processor: this.processTick
},
'heap-sample-begin': {
parsers: [parseString, parseString, parseInt],
processor: this.processHeapSampleBegin
},
'heap-sample-end': {
parsers: [parseString, parseString],
processor: this.processHeapSampleEnd
},
'timer-event-start': {
parsers: [parseString, parseString, parseString],
processor: this.advanceDistortion
},
'timer-event-end': {
parsers: [parseString, parseString, parseString],
processor: this.advanceDistortion
},
// Ignored events.
'profiler': undefined,
'function-creation': undefined,
'function-move': undefined,
'function-delete': undefined,
'heap-sample-item': undefined,
'current-time': undefined, // Handled specially, not parsed.
// Obsolete row types.
'code-allocate': undefined,
'begin-code-region': undefined,
'end-code-region': undefined
});
this.preprocessJson = preprocessJson;
this.cppEntriesProvider_ = cppEntriesProvider;
this.callGraphSize_ = callGraphSize;
this.ignoreUnknown_ = ignoreUnknown;
this.stateFilter_ = stateFilter;
this.runtimeTimerFilter_ = runtimeTimerFilter;
this.sourceMap = this.loadSourceMap(sourceMap);
const ticks = this.ticks_ =
{ total: 0, unaccounted: 0, excluded: 0, gc: 0 };
distortion = parseInt(distortion);
// Convert picoseconds to nanoseconds.
this.distortion_per_entry = isNaN(distortion) ? 0 : (distortion / 1000);
this.distortion = 0;
const rangelimits = range ? range.split(",") : [];
const range_start = parseInt(rangelimits[0]);
const range_end = parseInt(rangelimits[1]);
// Convert milliseconds to nanoseconds.
this.range_start = isNaN(range_start) ? -Infinity : (range_start * 1000);
this.range_end = isNaN(range_end) ? Infinity : (range_end * 1000)
V8Profile.prototype.handleUnknownCode = function (
operation, addr, opt_stackPos) {
const op = Profile.Operation;
switch (operation) {
case op.MOVE:
printErr(`Code move event for unknown code: 0x${addr.toString(16)}`);
break;
case op.DELETE:
printErr(`Code delete event for unknown code: 0x${addr.toString(16)}`);
break;
case op.TICK:
// Only unknown PCs (the first frame) are reported as unaccounted,
// otherwise tick balance will be corrupted (this behavior is compatible
// with the original tickprocessor.py script.)
if (opt_stackPos == 0) {
ticks.unaccounted++;
}
break;
}
};
if (preprocessJson) {
this.profile_ = new JsonProfile();
} else {
this.profile_ = new V8Profile(separateIc, separateBytecodes,
separateBuiltins, separateStubs, separateSparkplugHandlers);
}
this.codeTypes_ = {};
// Count each tick as a time unit.
this.viewBuilder_ = new ViewBuilder(1);
this.lastLogFileName_ = null;
this.generation_ = 1;
this.currentProducerProfile_ = null;
this.onlySummary_ = onlySummary;
}
loadSourceMap(sourceMap) {
if (!sourceMap) return null;
// Overwrite the load function to load scripts synchronously.
WebInspector.SourceMap.load = (sourceMapURL) => {
const content = d8.file.read(sourceMapURL);
const sourceMapObject = JSON.parse(content);
return new SourceMap(sourceMapURL, sourceMapObject);
};
return WebInspector.SourceMap.load(sourceMap);
}
static VmStates = {
JS: 0,
GC: 1,
PARSER: 2,
BYTECODE_COMPILER: 3,
// TODO(cbruni): add SPARKPLUG_COMPILER
COMPILER: 4,
OTHER: 5,
EXTERNAL: 6,
IDLE: 7,
};
static CodeTypes = {
CPP: 0,
SHARED_LIB: 1
};
// Otherwise, this is JS-related code. We are not adding it to
// codeTypes_ map because there can be zillions of them.
static CALL_PROFILE_CUTOFF_PCT = 1.0;
static CALL_GRAPH_SIZE = 5;
/**
* @override
*/
printError(str) {
printErr(str);
}
setCodeType(name, type) {
this.codeTypes_[name] = TickProcessor.CodeTypes[type];
}
isSharedLibrary(name) {
return this.codeTypes_[name] == TickProcessor.CodeTypes.SHARED_LIB;
}
isCppCode(name) {
return this.codeTypes_[name] == TickProcessor.CodeTypes.CPP;
}
isJsCode(name) {
return name !== "UNKNOWN" && !(name in this.codeTypes_);
}
async processLogFile(fileName) {
this.lastLogFileName_ = fileName;
const file = await fs.open(fileName);
const { size } = await file.stat();
const linereader = new LineReader()
file.createReadStream().pipe(linereader)
const progressInterval = setInterval(() => {
process.stderr.write(`${((linereader.byteCount / size) * 100).toFixed(2)}%\r`);
}, 5000);
for await (const line of linereader) {
await this.processLogLine(line);
}
clearInterval(progressInterval)
}
async processLogFileInTest(fileName) {
// Hack file name to avoid dealing with platform specifics.
this.lastLogFileName_ = 'v8.log';
const contents = d8.file.read(fileName);
await this.processLogChunk(contents);
}
processSharedLibrary(name, startAddr, endAddr, aslrSlide) {
const entry = this.profile_.addLibrary(name, startAddr, endAddr, aslrSlide);
this.setCodeType(entry.getName(), 'SHARED_LIB');
this.cppEntriesProvider_.parseVmSymbols(
name, startAddr, endAddr, aslrSlide, (fName, fStart, fEnd) => {
this.profile_.addStaticCode(fName, fStart, fEnd);
this.setCodeType(fName, 'CPP');
});
}
processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
if (type != 'RegExp' && maybe_func.length) {
const funcAddr = parseInt(maybe_func[0]);
const state = Profile.parseState(maybe_func[1]);
this.profile_.addFuncCode(type, name, timestamp, start, size, funcAddr, state);
} else {
this.profile_.addCode(type, name, timestamp, start, size);
}
}
processCodeDeopt(
timestamp, size, code, inliningId, scriptOffset, bailoutType,
sourcePositionText, deoptReasonText) {
this.profile_.deoptCode(timestamp, code, inliningId, scriptOffset,
bailoutType, sourcePositionText, deoptReasonText);
}
processCodeMove(from, to) {
this.profile_.moveCode(from, to);
}
processCodeDelete(start) {
this.profile_.deleteCode(start);
}
processCodeSourceInfo(
start, script, startPos, endPos, sourcePositions, inliningPositions,
inlinedFunctions) {
this.profile_.addSourcePositions(start, script, startPos,
endPos, sourcePositions, inliningPositions, inlinedFunctions);
}
processScriptSource(script, url, source) {
this.profile_.addScriptSource(script, url, source);
}
processFunctionMove(from, to) {
this.profile_.moveFunc(from, to);
}
includeTick(vmState) {
if (this.stateFilter_ !== null) {
return this.stateFilter_ == vmState;
} else if (this.runtimeTimerFilter_ !== null) {
return this.currentRuntimeTimer == this.runtimeTimerFilter_;
}
return true;
}
processRuntimeTimerEvent(name) {
this.currentRuntimeTimer = name;
}
processTick(pc,
ns_since_start,
is_external_callback,
tos_or_external_callback,
vmState,
stack) {
this.distortion += this.distortion_per_entry;
ns_since_start -= this.distortion;
if (ns_since_start < this.range_start || ns_since_start > this.range_end) {
return;
}
this.ticks_.total++;
if (vmState == TickProcessor.VmStates.GC) this.ticks_.gc++;
if (!this.includeTick(vmState)) {
this.ticks_.excluded++;
return;
}
if (is_external_callback) {
// Don't use PC when in external callback code, as it can point
// inside callback's code, and we will erroneously report
// that a callback calls itself. Instead we use tos_or_external_callback,
// as simply resetting PC will produce unaccounted ticks.
pc = tos_or_external_callback;
tos_or_external_callback = 0;
} else if (tos_or_external_callback) {
// Find out, if top of stack was pointing inside a JS function
// meaning that we have encountered a frameless invocation.
const funcEntry = this.profile_.findEntry(tos_or_external_callback);
if (!funcEntry || !funcEntry.isJSFunction || !funcEntry.isJSFunction()) {
tos_or_external_callback = 0;
}
}
this.profile_.recordTick(
ns_since_start, vmState,
this.processStack(pc, tos_or_external_callback, stack));
}
advanceDistortion() {
this.distortion += this.distortion_per_entry;
}
processHeapSampleBegin(space, state, ticks) {
if (space != 'Heap') return;
this.currentProducerProfile_ = new CallTree();
}
processHeapSampleEnd(space, state) {
if (space != 'Heap' || !this.currentProducerProfile_) return;
print(`Generation ${this.generation_}:`);
const tree = this.currentProducerProfile_;
tree.computeTotalWeights();
const producersView = this.viewBuilder_.buildView(tree);
// Sort by total time, desc, then by name, desc.
producersView.sort((rec1, rec2) =>
rec2.totalTime - rec1.totalTime ||
(rec2.internalFuncName < rec1.internalFuncName ? -1 : 1));
this.printHeavyProfile(producersView.head.children);
this.currentProducerProfile_ = null;
this.generation_++;
}
printVMSymbols() {
console.log(
JSON.stringify(this.profile_.serializeVMSymbols()));
}
printStatistics() {
if (this.preprocessJson) {
this.profile_.writeJson();
return;
}
print(`Statistical profiling result from ${this.lastLogFileName_}` +
`, (${this.ticks_.total} ticks, ${this.ticks_.unaccounted} unaccounted, ` +
`${this.ticks_.excluded} excluded).`);
if (this.ticks_.total == 0) return;
const flatProfile = this.profile_.getFlatProfile();
const flatView = this.viewBuilder_.buildView(flatProfile);
// Sort by self time, desc, then by name, desc.
flatView.sort((rec1, rec2) =>
rec2.selfTime - rec1.selfTime ||
(rec2.internalFuncName < rec1.internalFuncName ? -1 : 1));
let totalTicks = this.ticks_.total;
if (this.ignoreUnknown_) {
totalTicks -= this.ticks_.unaccounted;
}
const printAllTicks = !this.onlySummary_;
// Count library ticks
const flatViewNodes = flatView.head.children;
let libraryTicks = 0;
if (printAllTicks) this.printHeader('Shared libraries');
this.printEntries(flatViewNodes, totalTicks, null,
name => this.isSharedLibrary(name),
(rec) => { libraryTicks += rec.selfTime; }, printAllTicks);
const nonLibraryTicks = totalTicks - libraryTicks;
let jsTicks = 0;
if (printAllTicks) this.printHeader('JavaScript');
this.printEntries(flatViewNodes, totalTicks, nonLibraryTicks,
name => this.isJsCode(name),
(rec) => { jsTicks += rec.selfTime; }, printAllTicks);
let cppTicks = 0;
if (printAllTicks) this.printHeader('C++');
this.printEntries(flatViewNodes, totalTicks, nonLibraryTicks,
name => this.isCppCode(name),
(rec) => { cppTicks += rec.selfTime; }, printAllTicks);
this.printHeader('Summary');
this.printLine('JavaScript', jsTicks, totalTicks, nonLibraryTicks);
this.printLine('C++', cppTicks, totalTicks, nonLibraryTicks);
this.printLine('GC', this.ticks_.gc, totalTicks, nonLibraryTicks);
this.printLine('Shared libraries', libraryTicks, totalTicks, null);
if (!this.ignoreUnknown_ && this.ticks_.unaccounted > 0) {
this.printLine('Unaccounted', this.ticks_.unaccounted,
this.ticks_.total, null);
}
if (printAllTicks) {
print('\n [C++ entry points]:');
print(' ticks cpp total name');
const c_entry_functions = this.profile_.getCEntryProfile();
const total_c_entry = c_entry_functions[0].ticks;
for (let i = 1; i < c_entry_functions.length; i++) {
const c = c_entry_functions[i];
this.printLine(c.name, c.ticks, total_c_entry, totalTicks);
}
this.printHeavyProfHeader();
const heavyProfile = this.profile_.getBottomUpProfile();
const heavyView = this.viewBuilder_.buildView(heavyProfile);
// To show the same percentages as in the flat profile.
heavyView.head.totalTime = totalTicks;
// Sort by total time, desc, then by name, desc.
heavyView.sort((rec1, rec2) =>
rec2.totalTime - rec1.totalTime ||
(rec2.internalFuncName < rec1.internalFuncName ? -1 : 1));
this.printHeavyProfile(heavyView.head.children);
}
}
printHeader(headerTitle) {
print(`\n [${headerTitle}]:`);
print(' ticks total nonlib name');
}
printLine(
entry, ticks, totalTicks, nonLibTicks) {
const pct = ticks * 100 / totalTicks;
const nonLibPct = nonLibTicks != null
? `${(ticks * 100 / nonLibTicks).toFixed(1).toString().padStart(5)}% `
: ' ';
print(`${` ${ticks.toString().padStart(5)} ` +
pct.toFixed(1).toString().padStart(5)}% ${nonLibPct}${entry}`);
}
printHeavyProfHeader() {
print('\n [Bottom up (heavy) profile]:');
print(' Note: percentage shows a share of a particular caller in the ' +
'total\n' +
' amount of its parent calls.');
print(` Callers occupying less than ${TickProcessor.CALL_PROFILE_CUTOFF_PCT.toFixed(1)}% are not shown.\n`);
print(' ticks parent name');
}
processProfile(profile, filterP, func) {
for (let i = 0, n = profile.length; i < n; ++i) {
const rec = profile[i];
if (!filterP(rec.internalFuncName)) {
continue;
}
func(rec);
}
}
getLineAndColumn(name) {
const re = /:([0-9]+):([0-9]+)$/;
const array = re.exec(name);
if (!array) {
return null;
}
return { line: array[1], column: array[2] };
}
hasSourceMap() {
return this.sourceMap != null;
}
formatFunctionName(funcName) {
if (!this.hasSourceMap()) {
return funcName;
}
const lc = this.getLineAndColumn(funcName);
if (lc == null) {
return funcName;
}
// in source maps lines and columns are zero based
const lineNumber = lc.line - 1;
const column = lc.column - 1;
const entry = this.sourceMap.findEntry(lineNumber, column);
const sourceFile = entry[2];
const sourceLine = entry[3] + 1;
const sourceColumn = entry[4] + 1;
return `${sourceFile}:${sourceLine}:${sourceColumn} -> ${funcName}`;
}
printEntries(
profile, totalTicks, nonLibTicks, filterP, callback, printAllTicks) {
this.processProfile(profile, filterP, (rec) => {
if (rec.selfTime == 0) return;
callback(rec);
const funcName = this.formatFunctionName(rec.internalFuncName);
if (printAllTicks) {
this.printLine(funcName, rec.selfTime, totalTicks, nonLibTicks);
}
});
}
printHeavyProfile(profile, opt_indent) {
const indent = opt_indent || 0;
const indentStr = ''.padStart(indent);
this.processProfile(profile, () => true, (rec) => {
// Cut off too infrequent callers.
if (rec.parentTotalPercent < TickProcessor.CALL_PROFILE_CUTOFF_PCT) return;
const funcName = this.formatFunctionName(rec.internalFuncName);
print(`${` ${rec.totalTime.toString().padStart(5)} ` +
rec.parentTotalPercent.toFixed(1).toString().padStart(5)}% ${indentStr}${funcName}`);
// Limit backtrace depth.
if (indent < 2 * this.callGraphSize_) {
this.printHeavyProfile(rec.children, indent + 2);
}
// Delimit top-level functions.
if (indent == 0) print('');
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment