/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { JITOptimizations, } = require("devtools/client/performance/modules/logic/jit"); const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils"); /** * A call tree for a thread. This is essentially a linkage between all frames * of all samples into a single tree structure, with additional information * on each node, like the time spent (in milliseconds) and samples count. * * @param object thread * The raw thread object received from the backend. Contains samples, * stackTable, frameTable, and stringTable. * @param object options * Additional supported options * - number startTime * - number endTime * - boolean contentOnly [optional] * - boolean invertTree [optional] * - boolean flattenRecursion [optional] */ function ThreadNode(thread, options = {}) { if (options.endTime == void 0 || options.startTime == void 0) { throw new Error("ThreadNode requires both `startTime` and `endTime`."); } this.samples = 0; this.sampleTimes = []; this.youngestFrameSamples = 0; this.calls = []; this.duration = options.endTime - options.startTime; this.nodeType = "Thread"; this.inverted = options.invertTree; // Total bytesize of all allocations if enabled this.byteSize = 0; this.youngestFrameByteSize = 0; const { samples, stackTable, frameTable, stringTable } = thread; // Nothing to do if there are no samples. if (samples.data.length === 0) { return; } this._buildInverted(samples, stackTable, frameTable, stringTable, options); if (!options.invertTree) { this._uninvert(); } } ThreadNode.prototype = { /** * Build an inverted call tree from profile samples. The format of the * samples is described in tools/profiler/ProfileEntry.h, under the heading * "Thread profile JSON Format". * * The profile data is naturally presented inverted. Inverting the call tree * is also the default in the Performance tool. * * @param object samples * The raw samples array received from the backend. * @param object stackTable * The table of deduplicated stacks from the backend. * @param object frameTable * The table of deduplicated frames from the backend. * @param object stringTable * The table of deduplicated strings from the backend. * @param object options * Additional supported options * - number startTime * - number endTime * - boolean contentOnly [optional] * - boolean invertTree [optional] */ _buildInverted: function buildInverted( samples, stackTable, frameTable, stringTable, options ) { function getOrAddFrameNode( calls, isLeaf, frameKey, inflatedFrame, isMetaCategory, leafTable ) { // Insert the inflated frame into the call tree at the current level. let frameNode; // Leaf nodes have fan out much greater than non-leaf nodes, thus the // use of a hash table. Otherwise, do linear search. // // Note that this method is very hot, thus the manual looping over // Array.prototype.find. if (isLeaf) { frameNode = leafTable[frameKey]; } else { for (let i = 0; i < calls.length; i++) { if (calls[i].key === frameKey) { frameNode = calls[i]; break; } } } if (!frameNode) { frameNode = new FrameNode(frameKey, inflatedFrame, isMetaCategory); if (isLeaf) { leafTable[frameKey] = frameNode; } calls.push(frameNode); } return frameNode; } const SAMPLE_STACK_SLOT = samples.schema.stack; const SAMPLE_TIME_SLOT = samples.schema.time; const SAMPLE_BYTESIZE_SLOT = samples.schema.size; const STACK_PREFIX_SLOT = stackTable.schema.prefix; const STACK_FRAME_SLOT = stackTable.schema.frame; const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame; const samplesData = samples.data; const stacksData = stackTable.data; // Caches. const inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable); const leafTable = Object.create(null); const startTime = options.startTime; const endTime = options.endTime; const flattenRecursion = options.flattenRecursion; // Reused options object passed to InflatedFrame.prototype.getFrameKey. const mutableFrameKeyOptions = { contentOnly: options.contentOnly, isRoot: false, isLeaf: false, isMetaCategoryOut: false, }; let byteSize = 0; for (let i = 0; i < samplesData.length; i++) { const sample = samplesData[i]; const sampleTime = sample[SAMPLE_TIME_SLOT]; if (SAMPLE_BYTESIZE_SLOT !== void 0) { byteSize = sample[SAMPLE_BYTESIZE_SLOT]; } // A sample's end time is considered to be its time of sampling. Its // start time is the sampling time of the previous sample. // // Thus, we compare sampleTime <= start instead of < to filter out // samples that end exactly at the start time. if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) { continue; } let stackIndex = sample[SAMPLE_STACK_SLOT]; let calls = this.calls; let prevCalls = this.calls; let prevFrameKey; let isLeaf = (mutableFrameKeyOptions.isLeaf = true); const skipRoot = options.invertTree; // Inflate the stack and build the FrameNode call tree directly. // // In the profiler data, each frame's stack is referenced by an index // into stackTable. // // Each entry in stackTable is a pair [ prefixIndex, frameIndex ]. The // prefixIndex is itself an index into stackTable, referencing the // prefix of the current stack (that is, the younger frames). In other // words, the stackTable is encoded as a trie of the inverted // callstack. The frameIndex is an index into frameTable, describing the // frame at the current depth. // // This algorithm inflates each frame in the frame table while walking // the stack trie as described above. // // The frame key is then computed from the inflated frame /and/ the // current depth in the FrameNode call tree. That is, the frame key is // not wholly determinable from just the inflated frame. // // For content frames, the frame key is just its location. For chrome // frames, the key may be a metacategory or its location, depending on // rendering options and its position in the FrameNode call tree. // // The frame key is then used to build up the inverted FrameNode call // tree. // // Note that various filtering functions, such as filtering for content // frames or flattening recursion, are inlined into the stack inflation // loop. This is important for performance as it avoids intermediate // structures and multiple passes. while (stackIndex !== null) { const stackEntry = stacksData[stackIndex]; const frameIndex = stackEntry[STACK_FRAME_SLOT]; // Fetch the stack prefix (i.e. older frames) index. stackIndex = stackEntry[STACK_PREFIX_SLOT]; // Do not include the (root) node in this sample, as the costs of each frame // will make it clear to differentiate (root)->B vs (root)->A->B // when a tree is inverted, a revert of bug 1147604 if (stackIndex === null && skipRoot) { break; } // Inflate the frame. const inflatedFrame = getOrAddInflatedFrame( inflatedFrameCache, frameIndex, frameTable, stringTable ); // Compute the frame key. mutableFrameKeyOptions.isRoot = stackIndex === null; const frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions); // An empty frame key means this frame should be skipped. if (frameKey === "") { continue; } // If we shouldn't flatten the current frame into the previous one, advance a // level in the call tree. const shouldFlatten = flattenRecursion && frameKey === prevFrameKey; if (!shouldFlatten) { calls = prevCalls; } const frameNode = getOrAddFrameNode( calls, isLeaf, frameKey, inflatedFrame, mutableFrameKeyOptions.isMetaCategoryOut, leafTable ); if (isLeaf) { frameNode.youngestFrameSamples++; frameNode._addOptimizations( inflatedFrame.optimizations, inflatedFrame.implementation, sampleTime, stringTable ); if (byteSize) { frameNode.youngestFrameByteSize += byteSize; } } // Don't overcount flattened recursive frames. if (!shouldFlatten) { frameNode.samples++; if (byteSize) { frameNode.byteSize += byteSize; } } prevFrameKey = frameKey; prevCalls = frameNode.calls; isLeaf = mutableFrameKeyOptions.isLeaf = false; } this.samples++; this.sampleTimes.push(sampleTime); if (byteSize) { this.byteSize += byteSize; } } }, /** * Uninverts the call tree after its having been built. */ _uninvert: function uninvert() { function mergeOrAddFrameNode(calls, node, samples, size) { // Unlike the inverted call tree, we don't use a root table for the top // level, as in general, there are many fewer entry points than // leaves. Instead, linear search is used regardless of level. for (let i = 0; i < calls.length; i++) { if (calls[i].key === node.key) { const foundNode = calls[i]; foundNode._merge(node, samples, size); return foundNode.calls; } } const copy = node._clone(samples, size); calls.push(copy); return copy.calls; } const workstack = [{ node: this, level: 0 }]; const spine = []; let entry; // The new root. const rootCalls = []; // Walk depth-first and keep the current spine (e.g., callstack). do { entry = workstack.pop(); if (entry) { spine[entry.level] = entry; const node = entry.node; const calls = node.calls; let callSamples = 0; let callByteSize = 0; // Continue the depth-first walk. for (let i = 0; i < calls.length; i++) { workstack.push({ node: calls[i], level: entry.level + 1 }); callSamples += calls[i].samples; callByteSize += calls[i].byteSize; } // The sample delta is used to distinguish stacks. // // Suppose we have the following stack samples: // // A -> B // A -> C // A // // The inverted tree is: // // A // / \ // B C // // with A.samples = 3, B.samples = 1, C.samples = 1. // // A is distinguished as being its own stack because // A.samples - (B.samples + C.samples) > 0. // // Note that bottoming out is a degenerate where callSamples = 0. const samplesDelta = node.samples - callSamples; const byteSizeDelta = node.byteSize - callByteSize; if (samplesDelta > 0) { // Reverse the spine and add them to the uninverted call tree. let uninvertedCalls = rootCalls; for (let level = entry.level; level > 0; level--) { const callee = spine[level]; uninvertedCalls = mergeOrAddFrameNode( uninvertedCalls, callee.node, samplesDelta, byteSizeDelta ); } } } } while (entry); // Replace the toplevel calls with rootCalls, which now contains the // uninverted roots. this.calls = rootCalls; }, /** * Gets additional details about this node. * @see FrameNode.prototype.getInfo for more information. * * @return object */ getInfo: function(options) { return FrameUtils.getFrameInfo(this, options); }, /** * Mimicks the interface of FrameNode, and a ThreadNode can never have * optimization data (at the moment, anyway), so provide a function * to return null so we don't need to check if a frame node is a thread * or not everytime we fetch optimization data. * * @return {null} */ hasOptimizations: function() { return null; }, }; /** * A function call node in a tree. Represents a function call with a unique context, * resulting in each FrameNode having its own row in the corresponding tree view. * Take samples: * A()->B()->C() * A()->B() * Q()->B() * * In inverted tree, A()->B()->C() would have one frame node, and A()->B() and * Q()->B() would share a frame node. * In an uninverted tree, A()->B()->C() and A()->B() would share a frame node, * with Q()->B() having its own. * * In all cases, all the frame nodes originated from the same InflatedFrame. * * @param string frameKey * The key associated with this frame. The key determines identity of * the node. * @param string location * The location of this function call. Note that this isn't sanitized, * so it may very well (not?) include the function name, url, etc. * @param number line * The line number inside the source containing this function call. * @param number category * The category type of this function call ("js", "graphics" etc.). * @param number allocations * The number of memory allocations performed in this frame. * @param number isContent * Whether this frame is content. * @param boolean isMetaCategory * Whether or not this is a platform node that should appear as a * generalized meta category or not. */ function FrameNode( frameKey, { location, line, category, isContent }, isMetaCategory ) { this.key = frameKey; this.location = location; this.line = line; this.youngestFrameSamples = 0; this.samples = 0; this.calls = []; this.isContent = !!isContent; this._optimizations = null; this._tierData = []; this._stringTable = null; this.isMetaCategory = !!isMetaCategory; this.category = category; this.nodeType = "Frame"; this.byteSize = 0; this.youngestFrameByteSize = 0; } FrameNode.prototype = { /** * Take optimization data observed for this frame. * * @param object optimizationSite * Any JIT optimization information attached to the current * sample. Lazily inflated via stringTable. * @param number implementation * JIT implementation used for this observed frame (baseline, ion); * can be null indicating "interpreter" * @param number time * The time this optimization occurred. * @param object stringTable * The string table used to inflate the optimizationSite. */ _addOptimizations: function(site, implementation, time, stringTable) { // Simply accumulate optimization sites for now. Processing is done lazily // by JITOptimizations, if optimization information is actually displayed. if (site) { let opts = this._optimizations; if (opts === null) { opts = this._optimizations = []; } opts.push(site); } if (!this._stringTable) { this._stringTable = stringTable; } // Record type of implementation used and the sample time this._tierData.push({ implementation, time }); }, _clone: function(samples, size) { const newNode = new FrameNode(this.key, this, this.isMetaCategory); newNode._merge(this, samples, size); return newNode; }, _merge: function(otherNode, samples, size) { if (this === otherNode) { return; } this.samples += samples; this.byteSize += size; if (otherNode.youngestFrameSamples > 0) { this.youngestFrameSamples += samples; } if (otherNode.youngestFrameByteSize > 0) { this.youngestFrameByteSize += otherNode.youngestFrameByteSize; } if (this._stringTable === null) { this._stringTable = otherNode._stringTable; } if (otherNode._optimizations) { if (!this._optimizations) { this._optimizations = []; } const opts = this._optimizations; const otherOpts = otherNode._optimizations; for (let i = 0; i < otherOpts.length; i++) { opts.push(otherOpts[i]); } } if (otherNode._tierData.length) { const tierData = this._tierData; const otherTierData = otherNode._tierData; for (let i = 0; i < otherTierData.length; i++) { tierData.push(otherTierData[i]); } tierData.sort((a, b) => a.time - b.time); } }, /** * Returns the parsed location and additional data describing * this frame. Uses cached data if possible. Takes the following * options: * * @param {ThreadNode} options.root * The root thread node to calculate relative costs. * Generates [self|total] [duration|percentage] values. * @param {boolean} options.allocations * Generates `totalAllocations` and `selfAllocations`. * * @return object * The computed { name, file, url, line } properties for this * function call, as well as additional params if options specified. */ getInfo: function(options) { return FrameUtils.getFrameInfo(this, options); }, /** * Returns whether or not the frame node has an JITOptimizations model. * * @return {Boolean} */ hasOptimizations: function() { return !this.isMetaCategory && !!this._optimizations; }, /** * Returns the underlying JITOptimizations model representing * the optimization attempts occuring in this frame. * * @return {JITOptimizations|null} */ getOptimizations: function() { if (!this._optimizations) { return null; } return new JITOptimizations(this._optimizations, this._stringTable); }, /** * Returns the tiers used overtime. * * @return {Array} */ getTierData: function() { return this._tierData; }, }; exports.ThreadNode = ThreadNode; exports.FrameNode = FrameNode;