mirror of
https://github.com/roytam1/palemoon27.git
synced 2026-07-10 17:19:43 +00:00
98894236c9
- Bug 1171200 - Add means of checking if a document links to a manifest. r=billm (066ddad20) - Bug 1167300 - Consolidate the performance tool directory, r=jsantell (c7dd7dc34) - Bug 1167300 - Create a way to get strings from multiple localization files, r=jsantell (0973b8d3e) - modules not in gre (914e4080e) - Bug 1153011 - Remove zoom button from call tree. r=vporof (797b8f91d) - Bug 1151973 - Inverted call tree should be ordered by 'self cost', not 'total cost', r=jsantell (f2800b272) - more gre removal (27aed87a0) - Bug 1144034 - Flamegraph text is barely readable on non-retina display, r=jsantell (cb19fd9f2) - Bug 1151973 - Inverted call tree should be ordered by 'self cost', not 'total cost', r=jsantell (9c579599e) - Bug 1167300 - Fix all performance tool imports to work with the new file locations, r=jsantell (70b2995c4) - Bug 1167298 - Remove the ordinal property on categories, r=jsantell (00b3f5830) - Bug 1167733 - Consolidate prefs access and usage in the new performance tool, r=jsantell (4dab15e7f) - Bug 1167006 - part 3 fully revert merge from 780e1f999f54. (8aaa33c9c) - Bug 1167961 - Task is incorrectly used in compatibility.js, r=jsantell (7291f68d1) - Bug 1138641 - Updated remaining callsites to use newChannel2 in browser/devtools (r=vporof) (60ac4b2c8) - Bug 1164130 - Correctly include RecordingUtils when importing older version 2 profiler data. r=vp (8169d0398) - Bug 1167962 - Keep exports at bottom of modules, r=jsantell (7426919db) - Bug 1167962 - Fix import in synthesizeProfileForTest, r=orange (cc7fab771) - fix merge of later patch Bug 1167006 (c0b57b0e2) - Bug 1157523 - Fix intermittent where markers are selected in the waterfall views when there is no recording selected. r=vp (35cec0bd1) - Bug 1196253 - update in-tree psutil to 3.1.1. r=gps (80f243738)
505 lines
17 KiB
JavaScript
505 lines
17 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, "L10N",
|
|
"devtools/performance/global", true);
|
|
loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
|
|
"devtools/performance/global", true);
|
|
loader.lazyRequireGetter(this, "CATEGORIES",
|
|
"devtools/performance/global", true);
|
|
loader.lazyRequireGetter(this, "CATEGORY_JIT",
|
|
"devtools/performance/global", true);
|
|
loader.lazyRequireGetter(this, "CATEGORY_OTHER",
|
|
"devtools/performance/global", true);
|
|
loader.lazyRequireGetter(this, "JITOptimizations",
|
|
"devtools/performance/jit", true);
|
|
loader.lazyRequireGetter(this, "FrameUtils",
|
|
"devtools/performance/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, @see ThreadNode.prototype.insert
|
|
* - number startTime [optional]
|
|
* - number endTime [optional]
|
|
* - boolean contentOnly [optional]
|
|
* - boolean invertTree [optional]
|
|
* - boolean flattenRecursion [optional]
|
|
*/
|
|
function ThreadNode(thread, options = {}) {
|
|
this.samples = 0;
|
|
this.duration = 0;
|
|
this.calls = [];
|
|
|
|
// Maps of frame to their self counts and duration.
|
|
this.selfCount = Object.create(null);
|
|
this.selfDuration = Object.create(null);
|
|
|
|
let { samples, stackTable, frameTable, stringTable, allocationsTable } = thread;
|
|
|
|
// Nothing to do if there are no samples.
|
|
if (samples.data.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this._buildInverted(samples, stackTable, frameTable, stringTable, allocationsTable, 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 allocationsTable
|
|
* The table of allocation counts from the backend. Indexed by frame
|
|
* index.
|
|
* @param object options
|
|
* Additional supported options
|
|
* - number startTime [optional]
|
|
* - number endTime [optional]
|
|
* - boolean contentOnly [optional]
|
|
* - boolean invertTree [optional]
|
|
*/
|
|
_buildInverted: function buildInverted(samples, stackTable, frameTable, stringTable, allocationsTable, 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 STACK_PREFIX_SLOT = stackTable.schema.prefix;
|
|
const STACK_FRAME_SLOT = stackTable.schema.frame;
|
|
|
|
const InflatedFrame = FrameUtils.InflatedFrame;
|
|
const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
|
|
|
|
let selfCount = this.selfCount;
|
|
let selfDuration = this.selfDuration;
|
|
|
|
let samplesData = samples.data;
|
|
let stacksData = stackTable.data;
|
|
|
|
// Caches.
|
|
let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
|
|
let leafTable = Object.create(null);
|
|
|
|
let startTime = options.startTime || 0
|
|
let endTime = options.endTime || Infinity;
|
|
let flattenRecursion = options.flattenRecursion;
|
|
|
|
// Take the timestamp of the first sample as prevSampleTime. 0 is
|
|
// incorrect due to circular buffer wraparound. If wraparound happens,
|
|
// then the first sample will have an incorrect, large duration.
|
|
let prevSampleTime = samplesData[0][SAMPLE_TIME_SLOT];
|
|
|
|
// Reused options object passed to InflatedFrame.prototype.getFrameKey.
|
|
let mutableFrameKeyOptions = {
|
|
contentOnly: options.contentOnly,
|
|
isRoot: false,
|
|
isLeaf: false,
|
|
isMetaCategoryOut: false
|
|
};
|
|
|
|
// Start iteration at the second sample, as we use the first sample to
|
|
// compute prevSampleTime.
|
|
for (let i = 1; i < samplesData.length; i++) {
|
|
let sample = samplesData[i];
|
|
let sampleTime = sample[SAMPLE_TIME_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) {
|
|
prevSampleTime = sampleTime;
|
|
continue;
|
|
}
|
|
|
|
let sampleDuration = sampleTime - prevSampleTime;
|
|
let stackIndex = sample[SAMPLE_STACK_SLOT];
|
|
let calls = this.calls;
|
|
let prevCalls = this.calls;
|
|
let prevFrameKey;
|
|
let isLeaf = mutableFrameKeyOptions.isLeaf = true;
|
|
|
|
// 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];
|
|
|
|
// Inflate the frame.
|
|
let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache, frameIndex, frameTable,
|
|
stringTable, allocationsTable);
|
|
|
|
// Compute the frame key.
|
|
mutableFrameKeyOptions.isRoot = stackIndex === null;
|
|
let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
|
|
|
|
// Leaf frames are never skipped and require self count and duration
|
|
// bookkeeping.
|
|
if (isLeaf) {
|
|
// Tabulate self count and duration for the leaf frame. The frameKey
|
|
// is never empty for a leaf frame.
|
|
if (selfCount[frameKey] === undefined) {
|
|
selfCount[frameKey] = 0;
|
|
selfDuration[frameKey] = 0;
|
|
}
|
|
selfCount[frameKey]++;
|
|
selfDuration[frameKey] += sampleDuration;
|
|
} else {
|
|
// 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.
|
|
if (!flattenRecursion || frameKey !== prevFrameKey) {
|
|
calls = prevCalls;
|
|
}
|
|
|
|
let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
|
|
mutableFrameKeyOptions.isMetaCategoryOut,
|
|
leafTable);
|
|
|
|
frameNode._countSample(prevSampleTime, sampleTime, inflatedFrame.optimizations,
|
|
stringTable);
|
|
|
|
prevFrameKey = frameKey;
|
|
prevCalls = frameNode.calls;
|
|
isLeaf = mutableFrameKeyOptions.isLeaf = false;
|
|
}
|
|
|
|
this.duration += sampleDuration;
|
|
this.samples++;
|
|
prevSampleTime = sampleTime;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Uninverts the call tree after its having been built.
|
|
*/
|
|
_uninvert: function uninvert() {
|
|
function mergeOrAddFrameNode(calls, node) {
|
|
// 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);
|
|
return foundNode.calls;
|
|
}
|
|
}
|
|
let copy = node._clone();
|
|
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;
|
|
|
|
if (calls.length === 0) {
|
|
// We've bottomed out. 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);
|
|
}
|
|
} else {
|
|
// We still have children. Continue the depth-first walk.
|
|
for (let i = 0; i < calls.length; i++) {
|
|
workstack.push({ node: calls[i], level: entry.level + 1 });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replace the toplevel calls with rootCalls, which now contains the
|
|
// uninverted roots.
|
|
this.calls = rootCalls;
|
|
},
|
|
|
|
/**
|
|
* Gets additional details about this node.
|
|
* @return object
|
|
*/
|
|
getInfo: function() {
|
|
return {
|
|
nodeType: "Thread",
|
|
functionName: L10N.getStr("table.root"),
|
|
categoryData: {}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @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, allocations, isContent }, isMetaCategory) {
|
|
this.key = frameKey;
|
|
this.location = location;
|
|
this.line = line;
|
|
this.category = category;
|
|
this.allocations = allocations;
|
|
this.samples = 0;
|
|
this.duration = 0;
|
|
this.calls = [];
|
|
this.isContent = isContent;
|
|
this._optimizations = null;
|
|
this._stringTable = null;
|
|
this.isMetaCategory = isMetaCategory;
|
|
}
|
|
|
|
FrameNode.prototype = {
|
|
/**
|
|
* Count a sample as associated with this node.
|
|
*
|
|
* @param number prevSampleTime
|
|
* The time when the immediate previous sample was sampled.
|
|
* @param number sampleTime
|
|
* The time when the current sample was sampled.
|
|
* @param object optimizationSite
|
|
* Any JIT optimization information attached to the current
|
|
* sample. Lazily inflated via stringTable.
|
|
* @param object stringTable
|
|
* The string table used to inflate the optimizationSite.
|
|
*/
|
|
_countSample: function (prevSampleTime, sampleTime, optimizationSite, stringTable) {
|
|
this.samples++;
|
|
this.duration += sampleTime - prevSampleTime;
|
|
|
|
// Simply accumulate optimization sites for now. Processing is done lazily
|
|
// by JITOptimizations, if optimization information is actually displayed.
|
|
if (optimizationSite) {
|
|
let opts = this._optimizations;
|
|
if (opts === null) {
|
|
opts = this._optimizations = [];
|
|
this._stringTable = stringTable;
|
|
}
|
|
opts.push(optimizationSite);
|
|
}
|
|
},
|
|
|
|
_clone: function () {
|
|
let newNode = new FrameNode(this.key, this, this.isMetaCategory);
|
|
newNode._merge(this);
|
|
return newNode;
|
|
},
|
|
|
|
_merge: function (otherNode) {
|
|
if (this === otherNode) {
|
|
return;
|
|
}
|
|
|
|
this.samples += otherNode.samples;
|
|
this.duration += otherNode.duration;
|
|
|
|
if (otherNode._optimizations) {
|
|
let opts = this._optimizations;
|
|
if (opts === null) {
|
|
opts = this._optimizations = [];
|
|
this._stringTable = otherNode._stringTable;
|
|
}
|
|
let otherOpts = otherNode._optimizations;
|
|
for (let i = 0; i < otherOpts.length; i++) {
|
|
opts.push(otherOpts[i]);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the parsed location and additional data describing
|
|
* this frame. Uses cached data if possible.
|
|
*
|
|
* @return object
|
|
* The computed { name, file, url, line } properties for this
|
|
* function call.
|
|
*/
|
|
getInfo: function() {
|
|
return this._data || this._computeInfo();
|
|
},
|
|
|
|
/**
|
|
* Parses the raw location of this function call to retrieve the actual
|
|
* function name and source url.
|
|
*/
|
|
_computeInfo: function() {
|
|
// "EnterJIT" pseudoframes are special, not actually on the stack.
|
|
if (this.location == "EnterJIT") {
|
|
this.category = CATEGORY_JIT;
|
|
}
|
|
|
|
if (this.isMetaCategory && !this.category) {
|
|
this.category = CATEGORY_OTHER;
|
|
}
|
|
|
|
// Since only C++ stack frames have associated category information,
|
|
// default to an "unknown" category otherwise.
|
|
let categoryData = CATEGORY_MAPPINGS[this.category] || {};
|
|
|
|
let parsedData = FrameUtils.parseLocation(this.location, this.line, this.column);
|
|
parsedData.nodeType = "Frame";
|
|
parsedData.categoryData = categoryData;
|
|
parsedData.isContent = this.isContent;
|
|
parsedData.isMetaCategory = this.isMetaCategory;
|
|
|
|
return this._data = parsedData;
|
|
},
|
|
|
|
/**
|
|
* Returns whether or not the frame node has an JITOptimizations model.
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
hasOptimizations: function () {
|
|
return !!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);
|
|
}
|
|
};
|
|
|
|
exports.ThreadNode = ThreadNode;
|
|
exports.FrameNode = FrameNode;
|
|
exports.FrameNode.isContent = FrameUtils.isContent;
|