mirror of
https://github.com/classilla/tenfourfox.git
synced 2026-03-12 13:42:03 +00:00
555 lines
18 KiB
JavaScript
555 lines
18 KiB
JavaScript
/* 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 { Cc, Ci, Cu, Cr } = require("chrome");
|
|
|
|
loader.lazyRequireGetter(this, "JITOptimizations",
|
|
"devtools/client/performance/modules/logic/jit", true);
|
|
loader.lazyRequireGetter(this, "FrameUtils",
|
|
"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;
|
|
|
|
let { 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
|
|
* "ThreadProfile 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 InflatedFrame = FrameUtils.InflatedFrame;
|
|
const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
|
|
|
|
let samplesData = samples.data;
|
|
let stacksData = stackTable.data;
|
|
|
|
// Caches.
|
|
let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
|
|
let leafTable = Object.create(null);
|
|
|
|
let startTime = options.startTime;
|
|
let endTime = options.endTime;
|
|
let flattenRecursion = options.flattenRecursion;
|
|
|
|
// Reused options object passed to InflatedFrame.prototype.getFrameKey.
|
|
let mutableFrameKeyOptions = {
|
|
contentOnly: options.contentOnly,
|
|
isRoot: false,
|
|
isLeaf: false,
|
|
isMetaCategoryOut: false
|
|
};
|
|
|
|
let byteSize = 0;
|
|
for (let i = 0; i < samplesData.length; i++) {
|
|
let sample = samplesData[i];
|
|
let 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;
|
|
let 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) {
|
|
let stackEntry = stacksData[stackIndex];
|
|
let 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.
|
|
let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache, frameIndex, frameTable,
|
|
stringTable);
|
|
|
|
// Compute the frame key.
|
|
mutableFrameKeyOptions.isRoot = stackIndex === null;
|
|
let 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.
|
|
let shouldFlatten = flattenRecursion && frameKey === prevFrameKey;
|
|
if (!shouldFlatten) {
|
|
calls = prevCalls;
|
|
}
|
|
|
|
let 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) {
|
|
let foundNode = calls[i];
|
|
foundNode._merge(node, samples, size);
|
|
return foundNode.calls;
|
|
}
|
|
}
|
|
let copy = node._clone(samples, size);
|
|
calls.push(copy);
|
|
return copy.calls;
|
|
}
|
|
|
|
let workstack = [{ node: this, level: 0 }];
|
|
let spine = [];
|
|
let entry;
|
|
|
|
// The new root.
|
|
let rootCalls = [];
|
|
|
|
// Walk depth-first and keep the current spine (e.g., callstack).
|
|
while (entry = workstack.pop()) {
|
|
spine[entry.level] = entry;
|
|
|
|
let node = entry.node;
|
|
let 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.
|
|
|
|
let samplesDelta = node.samples - callSamples;
|
|
let 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--) {
|
|
let callee = spine[level];
|
|
uninvertedCalls = mergeOrAddFrameNode(uninvertedCalls, callee.node, samplesDelta, byteSizeDelta);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
let 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 = [];
|
|
}
|
|
let opts = this._optimizations;
|
|
let otherOpts = otherNode._optimizations;
|
|
for (let i = 0; i < otherOpts.length; i++) {
|
|
opts.push(otherOpts[i]);
|
|
}
|
|
}
|
|
|
|
if (otherNode._tierData.length) {
|
|
let tierData = this._tierData;
|
|
let 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<object>}
|
|
*/
|
|
getTierData: function () {
|
|
return this._tierData;
|
|
}
|
|
};
|
|
|
|
exports.ThreadNode = ThreadNode;
|
|
exports.FrameNode = FrameNode;
|