// 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"; 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, }; } } 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; let line; while (line = readline()) { await this.processLogLine(line); } } 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(''); }); } }