Skip to content

Instantly share code, notes, and snippets.

@syg
Created April 30, 2015 23:16
Show Gist options
  • Save syg/8acd3b036924a44735ac to your computer and use it in GitHub Desktop.
Save syg/8acd3b036924a44735ac to your computer and use it in GitHub Desktop.
commit 8fa2597ffad99a337cc7aa1c258b240b8e961add
Author: Shu-yu Guo <[email protected]>
Date: Fri Apr 24 16:20:04 2015 -0700
WIP tree-model inverted
diff --git a/browser/devtools/performance/modules/recording-utils.js b/browser/devtools/performance/modules/recording-utils.js
index 1b570f3..3d0710c 100644
--- a/browser/devtools/performance/modules/recording-utils.js
+++ b/browser/devtools/performance/modules/recording-utils.js
@@ -13,23 +13,6 @@ const { Cc, Ci, Cu, Cr } = require("chrome");
exports.RecordingUtils = {};
/**
- * Filters all the samples in the provided profiler data to be more recent
- * than the specified start time.
- *
- * @param object profile
- * The profiler data received from the backend.
- * @param number profilerStartTime
- * The earliest acceptable sample time (in milliseconds).
- */
-exports.RecordingUtils.filterSamples = function(profile, profilerStartTime) {
- let firstThread = profile.threads[0];
-
- firstThread.samples = firstThread.samples.filter(e => {
- return e.time >= profilerStartTime;
- });
-}
-
-/**
* Offsets all the samples in the provided profiler data by the specified time.
*
* @param object profile
@@ -39,9 +22,9 @@ exports.RecordingUtils.filterSamples = function(profile, profilerStartTime) {
*/
exports.RecordingUtils.offsetSampleTimes = function(profile, timeOffset) {
let firstThread = profile.threads[0];
-
- for (let sample of firstThread.samples) {
- sample.time -= timeOffset;
+ const TIME_SLOT = firstThread.samples.schema.time;
+ for (let sample of firstThread.samples.data) {
+ sample[TIME_SLOT] -= timeOffset;
}
}
diff --git a/browser/devtools/performance/views/details-js-call-tree.js b/browser/devtools/performance/views/details-js-call-tree.js
index fdef5e6..16b468d 100644
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -10,7 +10,8 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
rerenderPrefs: [
"invert-call-tree",
- "show-platform-data"
+ "show-platform-data",
+ "flatten-tree-recursion"
],
rangeChangeDebounceTime: 50, // ms
@@ -47,7 +48,8 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
render: function (interval={}) {
let options = {
contentOnly: !PerformanceController.getOption("show-platform-data"),
- invertTree: PerformanceController.getOption("invert-call-tree")
+ invertTree: PerformanceController.getOption("invert-call-tree"),
+ flattenRecursion: PerformanceController.getOption("flatten-tree-recursion")
};
let recording = PerformanceController.getCurrentRecording();
let profile = recording.getProfile();
@@ -75,12 +77,17 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
* populate the call tree.
*/
_prepareCallTree: function (profile, { startTime, endTime }, options) {
- let threadSamples = profile.threads[0].samples;
- let optimizations = profile.threads[0].optimizations;
- let { contentOnly, invertTree } = options;
-
- let threadNode = new ThreadNode(threadSamples,
- { startTime, endTime, contentOnly, invertTree, optimizations });
+ let thread = profile.threads[0];
+ let { contentOnly, invertTree, flattenRecursion } = options;
+
+ var start = Date.now();
+ let threadNode = new ThreadNode(thread.samples,
+ thread.stackTable,
+ thread.frameTable,
+ thread.stringTable,
+ { startTime, endTime,
+ contentOnly, invertTree, flattenRecursion });
+ console.log(">> SHU prepareCallTree took " + (Date.now() - start) + " ms\n");
return threadNode;
},
diff --git a/browser/devtools/performance/views/details-js-flamegraph.js b/browser/devtools/performance/views/details-js-flamegraph.js
index 8c1a7dc..9f31d3e 100644
--- a/browser/devtools/performance/views/details-js-flamegraph.js
+++ b/browser/devtools/performance/views/details-js-flamegraph.js
@@ -56,12 +56,15 @@ let JsFlameGraphView = Heritage.extend(DetailsSubview, {
let recording = PerformanceController.getCurrentRecording();
let duration = recording.getDuration();
let profile = recording.getProfile();
- let samples = profile.threads[0].samples;
+ let thread = profile.threads[0];
- let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
- invertStack: PerformanceController.getOption("invert-flame-graph"),
+ let data = FlameGraphUtils.createFlameGraphDataFromSamples(thread.samples,
+ thread.stackTable,
+ thread.frameTable,
+ thread.stringTable, {
+ invertTree: PerformanceController.getOption("invert-flame-graph"),
flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
- filterFrames: !PerformanceController.getOption("show-platform-data") && FrameNode.isContent,
+ contentOnly: !PerformanceController.getOption("show-platform-data"),
showIdleBlocks: PerformanceController.getOption("show-idle-blocks") && L10N.getStr("table.idle")
});
diff --git a/browser/devtools/performance/views/jit-optimizations.js b/browser/devtools/performance/views/jit-optimizations.js
index 6141b8d..5b6c1d8 100644
--- a/browser/devtools/performance/views/jit-optimizations.js
+++ b/browser/devtools/performance/views/jit-optimizations.js
@@ -140,9 +140,10 @@ let JITOptimizationsView = {
// An array of sorted OptimizationSites.
let sites = frameNode.getOptimizations().getOptimizationSites();
+ let stringTable = frameNode.getOptimizations().stringTable;
for (let site of sites) {
- this._renderSite(view, site, frameData);
+ this._renderSite(view, site, frameData, stringTable);
}
this.emit(EVENTS.OPTIMIZATIONS_RENDERED, this.getCurrentFrame());
@@ -151,10 +152,10 @@ let JITOptimizationsView = {
/**
* Creates an entry in the tree widget for an optimization site.
*/
- _renderSite: function (view, site, frameData) {
+ _renderSite: function (view, site, frameData, stringTable) {
let { id, samples, data } = site;
let { types, attempts } = data;
- let siteNode = this._createSiteNode(frameData, site);
+ let siteNode = this._createSiteNode(frameData, site, stringTable);
// Cast `id` to a string so TreeWidget doesn't think it does not exist
id = id + "";
@@ -164,12 +165,16 @@ let JITOptimizationsView = {
// Add types -- Ion types are the parent, with
// the observed types as children.
view.add([id, { id: `${id}-types`, label: `Types (${types.length})` }]);
- this._renderIonType(view, site);
+ this._renderIonType(view, site, stringTable);
// Add attempts
- view.add([id, { id: `${id}-attempts`, label: `Attempts (${attempts.length})` }]);
- for (let i = attempts.length - 1; i >= 0; i--) {
- let node = this._createAttemptNode(attempts[i]);
+ const STRATEGY_SLOT = attempts.schema.strategy;
+ const OUTCOME_SLOT = attempts.schema.outcome;
+ let attemptsData = attempts.data;
+ view.add([id, { id: `${id}-attempts`, label: `Attempts (${attemptsData.length})` }]);
+ for (let i = attemptsData.length - 1; i >= 0; i--) {
+ let node = this._createAttemptNode(stringTable[attemptsData[i][STRATEGY_SLOT]],
+ stringTable[attemptsData[i][OUTCOME_SLOT]]);
view.add([id, `${id}-attempts`, { node }]);
}
},
@@ -178,17 +183,18 @@ let JITOptimizationsView = {
* Renders all Ion types from an optimization site, with its children
* ObservedTypes.
*/
- _renderIonType: function (view, site) {
+ _renderIonType: function (view, site, stringTable) {
let { id, data: { types }} = site;
// Cast `id` to a string so TreeWidget doesn't think it does not exist
id = id + "";
for (let i = 0; i < types.length; i++) {
let ionType = types[i];
- let ionNode = this._createIonNode(ionType);
+ let ionNode = this._createIonNode(stringTable[ionType.site],
+ stringTable[ionType.mirType]);
view.add([id, `${id}-types`, { id: `${id}-types-${i}`, node: ionNode }]);
- for (let observedType of (ionType.types || [])) {
- let node = this._createObservedTypeNode(observedType);
+ for (let observedType of (ionType.typeset || [])) {
+ let node = this._createObservedTypeNode(observedType, stringTable);
view.add([id, `${id}-types`, `${id}-types-${i}`, { node }]);
}
}
@@ -198,7 +204,7 @@ let JITOptimizationsView = {
* Creates an element for insertion in the raw view for an OptimizationSite.
*/
- _createSiteNode: function (frameData, site) {
+ _createSiteNode: function (frameData, site, stringTable) {
let node = document.createElement("span");
let desc = document.createElement("span");
let line = document.createElement("span");
@@ -206,19 +212,22 @@ let JITOptimizationsView = {
let urlNode = this._createDebuggerLinkNode(frameData.url, site.data.line);
let attempts = site.getAttempts();
- let lastStrategy = attempts[attempts.length - 1].strategy;
+ let attemptsData = attempts.data;
+ const STRATEGY_SLOT = attempts.schema.strategy;
+ let lastStrategy = attemptsData[attemptsData.length - 1][STRATEGY_SLOT];
let propString = "";
if (site.data.propertyName) {
- if (site.data.propertyName.length > PROPNAME_MAX_LENGTH) {
- propString = ` (.${site.data.propertyName.substr(0, PROPNAME_MAX_LENGTH)}…)`;
- desc.setAttribute("tooltiptext", site.data.propertyName);
+ let propName = stringTable[site.data.propertyName];
+ if (propName.length > PROPNAME_MAX_LENGTH) {
+ propString = ` (.${propName.substr(0, PROPNAME_MAX_LENGTH)}…)`;
+ desc.setAttribute("tooltiptext", propName);
} else {
- propString = ` (.${site.data.propertyName})`;
+ propString = ` (.${propName})`;
}
}
- if (!site.hasSuccessfulOutcome()) {
+ if (!site.hasSuccessfulOutcome(stringTable)) {
let icon = document.createElement("span");
icon.setAttribute("tooltiptext", OPTIMIZATION_FAILURE);
icon.setAttribute("severity", "warning");
@@ -227,7 +236,7 @@ let JITOptimizationsView = {
}
let sampleString = PluralForm.get(site.samples, JIT_SAMPLES).replace("#1", site.samples);
- desc.textContent = `${lastStrategy}${propString} – (${sampleString})`;
+ desc.textContent = `${stringTable[lastStrategy]}${propString} – (${sampleString})`;
line.textContent = site.data.line;
line.className = "opt-line";
column.textContent = site.data.column;
@@ -244,22 +253,16 @@ let JITOptimizationsView = {
* Creates an element for insertion in the raw view for an IonType.
*
* @see browser/devtools/shared/profiler/jit.js
- * @param {IonType} ionType
+ * @param string typeSite
+ * @param string mirType
* @return {Element}
*/
- _createIonNode: function (ionType) {
+ _createIonNode: function (typeSite, mirType) {
let node = document.createElement("span");
- let icon = document.createElement("span");
- let typeNode = document.createElement("span");
- let siteNode = document.createElement("span");
- typeNode.textContent = ionType.mirType;
- typeNode.className = "opt-ion-type";
- siteNode.textContent = `(${ionType.site})`;
- siteNode.className = "opt-ion-type-site";
- node.appendChild(typeNode);
- node.appendChild(siteNode);
+ node.textContent = `${typeSite} : ${mirType}`;
+ node.className = "opt-ion-type";
return node;
},
@@ -269,28 +272,31 @@ let JITOptimizationsView = {
*
* @see browser/devtools/shared/profiler/jit.js
* @param {ObservedType} type
+ * @param {Array<string>} stringTable
* @return {Element}
*/
- _createObservedTypeNode: function (type) {
+ _createObservedTypeNode: function (type, stringTable) {
let node = document.createElement("span");
let typeNode = document.createElement("span");
- typeNode.textContent = `${type.keyedBy}` + (type.name ? ` → ${type.name}` : "");
+ typeNode.textContent = `${stringTable[type.keyedBy]}` +
+ (type.name ? ` → ${stringTable[type.name]}` : "");
typeNode.className = "opt-type";
node.appendChild(typeNode);
// If we have a type and a location, try to make a
// link to the debugger
- if (type.location && type.line) {
- let urlNode = this._createDebuggerLinkNode(type.location, type.line);
+ let location = type.location ? stringTable[type.location] : undefined;
+ if (location && type.line) {
+ let urlNode = this._createDebuggerLinkNode(location, type.line);
node.appendChild(urlNode);
}
// Otherwise if we just have a location, it could just
// be a memory location
- else if (type.location) {
+ else if (location) {
let locNode = document.createElement("span");
- locNode.textContent = `@${type.location}`;
+ locNode.textContent = `@${location}`;
locNode.className = "opt-url";
node.appendChild(locNode);
}
@@ -308,21 +314,22 @@ let JITOptimizationsView = {
* Creates an element for insertion in the raw view for an OptimizationAttempt.
*
* @see browser/devtools/shared/profiler/jit.js
- * @param {OptimizationAttempt} attempt
+ * @param {string} strategy
+ * @param {string} outcome
* @return {Element}
*/
- _createAttemptNode: function (attempt) {
+ _createAttemptNode: function (strategy, outcome) {
let node = document.createElement("span");
let strategyNode = document.createElement("span");
let outcomeNode = document.createElement("span");
- strategyNode.textContent = attempt.strategy;
+ strategyNode.textContent = strategy;
strategyNode.className = "opt-strategy";
- outcomeNode.textContent = attempt.outcome;
+ outcomeNode.textContent = outcome;
outcomeNode.className = "opt-outcome";
outcomeNode.setAttribute("outcome",
- JITOptimizations.isSuccessfulOutcome(attempt.outcome) ? "success" : "failure");
+ JITOptimizations.isSuccessfulOutcome(outcome) ? "success" : "failure");
node.appendChild(strategyNode);
node.appendChild(outcomeNode);
diff --git a/browser/devtools/shared/profiler/frame-utils.js b/browser/devtools/shared/profiler/frame-utils.js
index e3d3d3b..3eb7644 100644
--- a/browser/devtools/shared/profiler/frame-utils.js
+++ b/browser/devtools/shared/profiler/frame-utils.js
@@ -11,8 +11,6 @@ loader.lazyRequireGetter(this, "CATEGORY_OTHER",
// The cache used in the `nsIURL` function.
const gNSURLStore = new Map();
-const CHROME_SCHEMES = ["chrome://", "resource://", "jar:file://"];
-const CONTENT_SCHEMES = ["http://", "https://", "file://", "app://"];
/**
* Parses the raw location of this function call to retrieve the actual
@@ -51,67 +49,95 @@ exports.parseLocation = function parseLocation (frame) {
line: line,
column: column
};
-},
+}
+
+const CHAR_CODE_LPAREN = "(".charCodeAt(0);
/**
-* Checks if the specified function represents a chrome or content frame.
-*
-* @param object frame
-* The { category, location } properties of the frame.
-* @return boolean
-* True if a content frame, false if a chrome frame.
-*/
-exports.isContent = function isContent ({ category, location }) {
+ * Checks if the specified function represents a chrome or content frame.
+ *
+ * @param string location
+ * The location of the frame.
+ * @param number category [optional]
+ * If a chrome frame, the category.
+ * @return boolean
+ * True if a content frame, false if a chrome frame.
+ */
+function isContent(location, category) {
// Only C++ stack frames have associated category information.
- return !!(!category &&
- !CHROME_SCHEMES.find(e => location.contains(e)) &&
- CONTENT_SCHEMES.find(e => location.contains(e)));
+ if (category) {
+ return false;
+ }
+
+ // Locations in frames look like "functionName (foo://bar)". Look for the
+ // starting left parenthesis, then try to match a scheme name.
+ for (let i = 0; i < location.length; i++) {
+ if (location.charCodeAt(i) === CHAR_CODE_LPAREN) {
+ return isContentScheme(location, i + 1);
+ }
+ }
+ return false;
}
+exports.isContent = isContent;
+
/**
- * This filters out platform data frames in a sample. With latest performance
- * tool in Fx40, when displaying only content, we still filter out all platform data,
- * except we generalize platform data that are leaves. We do this because of two
- * observations:
- *
- * 1. The leaf is where time is _actually_ being spent, so we _need_ to show it
- * to developers in some way to give them accurate profiling data. We decide to
- * split the platform into various category buckets and just show time spent in
- * each bucket.
- *
- * 2. The calls leading to the leaf _aren't_ where we are spending time, but
- * _do_ give the developer context for how they got to the leaf where they _are_
- * spending time. For non-platform hackers, the non-leaf platform frames don't
- * give any meaningful context, and so we can safely filter them out.
+ * An intermediate data structured used to hold inflated frames.
*
- * Example transformations:
- * Before: PlatformA -> PlatformB -> ContentA -> ContentB
- * After: ContentA -> ContentB
+ * @param string location
+ * @param object optimizations
+ * @param number line
+ * @param number category
+ */
+function InflatedFrame(location, optimizations, line, category) {
+ this._set(location, optimizations, line, category);
+};
+
+InflatedFrame.prototype._set = function _set(location, optimizations, line, category) {
+ this.location = location;
+ this.optimizations = optimizations;
+ this.line = line;
+ this.category = category;
+ this.isContent = isContent(location, category);
+}
+
+/**
+ * Gets the frame key (i.e., equivalence group) according to options. Content
+ * frames are always identified by location. Chrome frames are identified by
+ * location if content-only filtering is off. If content-filtering is on, they
+ * are identified by their category
*
- * Before: PlatformA -> ContentA -> PlatformB -> PlatformC
- * After: ContentA -> Category(PlatformC)
+ * @param object options
*/
-exports.filterPlatformData = function filterPlatformData (frames) {
- let result = [];
- let last = frames.length - 1;
- let frame;
-
- for (let i = 0; i < frames.length; i++) {
- frame = frames[i];
- if (exports.isContent(frame)) {
- result.push(frame);
- } else if (last === i) {
- // Extend here so we're not destructively editing
- // the original profiler data. Set isMetaCategory `true`,
- // and ensure we have a category set by default, because that's how
- // the generalized frame nodes are organized.
- result.push(extend({ isMetaCategory: true, category: CATEGORY_OTHER }, frame));
- }
+InflatedFrame.prototype.getFrameKey = function getFrameKey(options) {
+ if (this.isContent || !options.contentOnly) {
+ return this.location;
}
- return result;
+ if (options.isLeaf) {
+ // We only care about leaf platform frames if we are displaying content
+ // only. If no category is present, give the default category of
+ // CATEGORY_OTHER.
+ //
+ // 1. The leaf is where time is _actually_ being spent, so we _need_ to
+ // show it to developers in some way to give them accurate profiling
+ // data. We decide to split the platform into various category buckets
+ // and just show time spent in each bucket.
+ //
+ // 2. The calls leading to the leaf _aren't_ where we are spending time,
+ // but _do_ give the developer context for how they got to the leaf
+ // where they _are_ spending time. For non-platform hackers, the
+ // non-leaf platform frames don't give any meaningful context, and so we
+ // can safely filter them out.
+ options.isMetaCategoryOut = true;
+ return this.category || CATEGORY_OTHER;
+ }
+
+ return undefined;
}
+exports.InflatedFrame = InflatedFrame;
+
/**
* Helper for getting an nsIURL instance out of a string.
*/
@@ -135,8 +161,114 @@ function nsIURL(url) {
* returns the same string, or null, if it's an invalid host.
*/
function getHost (url, hostName) {
- if (CHROME_SCHEMES.find(e => url.indexOf(e) === 0)) {
- return null;
+ return isChromeScheme(url) ? null : hostName;
+}
+
+const CHAR_CODE_A = "a".charCodeAt(0);
+const CHAR_CODE_C = "c".charCodeAt(0);
+const CHAR_CODE_E = "e".charCodeAt(0);
+const CHAR_CODE_F = "f".charCodeAt(0);
+const CHAR_CODE_H = "h".charCodeAt(0);
+const CHAR_CODE_I = "i".charCodeAt(0);
+const CHAR_CODE_J = "j".charCodeAt(0);
+const CHAR_CODE_L = "l".charCodeAt(0);
+const CHAR_CODE_M = "m".charCodeAt(0);
+const CHAR_CODE_O = "o".charCodeAt(0);
+const CHAR_CODE_P = "p".charCodeAt(0);
+const CHAR_CODE_R = "r".charCodeAt(0);
+const CHAR_CODE_S = "s".charCodeAt(0);
+const CHAR_CODE_T = "t".charCodeAt(0);
+const CHAR_CODE_U = "u".charCodeAt(0);
+
+const CHAR_CODE_COLON = ":".charCodeAt(0);
+const CHAR_CODE_SLASH = "/".charCodeAt(0);
+
+// For the functions below, we assume that we will never access the location
+// argument out of bounds, which is indeed the vast majority of cases.
+//
+// They are written the way they are very hot. Each frame is checked for being
+// content or chrome when processing the profile.
+
+function isColonSlashSlash(location, i) {
+ return location.charCodeAt(++i) === CHAR_CODE_COLON &&
+ location.charCodeAt(++i) === CHAR_CODE_SLASH &&
+ location.charCodeAt(++i) === CHAR_CODE_SLASH;
+}
+
+function isContentScheme(location, i) {
+ let firstChar = location.charCodeAt(i);
+
+ switch (firstChar) {
+ case CHAR_CODE_H: // "http://" or "https://"
+ if (location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_P) {
+ if (location.charCodeAt(i) === CHAR_CODE_S) {
+ ++i;
+ }
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ case CHAR_CODE_F: // "file://"
+ if (location.charCodeAt(++i) === CHAR_CODE_I &&
+ location.charCodeAt(++i) === CHAR_CODE_L &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ case CHAR_CODE_A: // "app://"
+ if (location.charCodeAt(++i) == CHAR_CODE_P &&
+ location.charCodeAt(++i) == CHAR_CODE_P) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ default:
+ return false;
+ }
+}
+
+function isChromeScheme(location, i) {
+ let firstChar = location.charCodeAt(i);
+
+ switch (firstChar) {
+ case CHAR_CODE_C: // "chrome://"
+ if (location.charCodeAt(++i) === CHAR_CODE_H &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_O &&
+ location.charCodeAt(++i) === CHAR_CODE_M &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ case CHAR_CODE_R: // "resource://"
+ if (location.charCodeAt(++i) === CHAR_CODE_E &&
+ location.charCodeAt(++i) === CHAR_CODE_S &&
+ location.charCodeAt(++i) === CHAR_CODE_O &&
+ location.charCodeAt(++i) === CHAR_CODE_U &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_C &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ case CHAR_CODE_J: // "jar:file://"
+ if (location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_COLON &&
+ location.charCodeAt(++i) === CHAR_CODE_F &&
+ location.charCodeAt(++i) === CHAR_CODE_I &&
+ location.charCodeAt(++i) === CHAR_CODE_L &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ default:
+ return false;
}
- return hostName;
}
diff --git a/browser/devtools/shared/profiler/jit.js b/browser/devtools/shared/profiler/jit.js
index 426152a..aeb9a11 100644
--- a/browser/devtools/shared/profiler/jit.js
+++ b/browser/devtools/shared/profiler/jit.js
@@ -111,9 +111,9 @@ const SUCCESSFUL_OUTCOMES = [
* @type {number} id
*/
-const OptimizationSite = exports.OptimizationSite = function (optimizations, optsIndex) {
- this.id = optsIndex;
- this.data = optimizations[optsIndex];
+const OptimizationSite = exports.OptimizationSite = function (id, opts) {
+ this.id = id;
+ this.data = opts;
this.samples = 0;
};
@@ -121,12 +121,15 @@ const OptimizationSite = exports.OptimizationSite = function (optimizations, opt
* Returns a boolean indicating if the passed in OptimizationSite
* has a "good" outcome at the end of its attempted strategies.
*
+ * @param {Array<string>} stringTable
* @return {boolean}
*/
-OptimizationSite.prototype.hasSuccessfulOutcome = function () {
+OptimizationSite.prototype.hasSuccessfulOutcome = function (stringTable) {
let attempts = this.getAttempts();
- let lastOutcome = attempts[attempts.length - 1].outcome;
+ const OUTCOME_SLOT = attempts.schema.outcome;
+ let attemptsData = attempts.data;
+ let lastOutcome = stringTable[attemptsData[attemptsData.length - 1][OUTCOME_SLOT]];
return OptimizationSite.isSuccessfulOutcome(lastOutcome);
};
@@ -155,28 +158,14 @@ OptimizationSite.prototype.getIonTypes = function () {
* Constructor for JITOptimizations. A collection of OptimizationSites for a frame.
*
* @constructor
- * @param {Array<RawOptimizationSite>} optimizations
- * Array of RawOptimizationSites from the profiler. Do not modify this!
- */
-
-const JITOptimizations = exports.JITOptimizations = function (optimizations) {
- this._opts = optimizations;
- // Hash of OptimizationSites observed for this frame.
- this._optSites = {};
-};
-
-/**
- * Called when a sample detects an optimization on this frame. Takes an `optsIndex`,
- * referring to an optimization in the stored `this._opts` array. Creates a histogram
- * of optimization site data by creating or incrementing an OptimizationSite
- * for each observed optimization.
- *
- * @param {Number} optsIndex
+ * @param {Array<string>} stringTable
+ * Array of strings from the profiler used to inflate
+ * JIT optimizations. Do not modify this!
*/
-JITOptimizations.prototype.addOptimizationSite = function (optsIndex) {
- let op = this._optSites[optsIndex] || (this._optSites[optsIndex] = new OptimizationSite(this._opts, optsIndex));
- op.samples++;
+const JITOptimizations = exports.JITOptimizations = function (optSites, stringTable) {
+ this._optSites = optSites;
+ this.stringTable = stringTable;
};
/**
@@ -186,11 +175,7 @@ JITOptimizations.prototype.addOptimizationSite = function (optsIndex) {
*/
JITOptimizations.prototype.getOptimizationSites = function () {
- let opts = [];
- for (let opt of Object.keys(this._optSites)) {
- opts.push(this._optSites[opt]);
- }
- return opts.sort((a, b) => b.samples - a.samples);
+ return this._optSites.sort((a, b) => b.samples - a.samples);
};
/**
diff --git a/browser/devtools/shared/profiler/tree-model.js b/browser/devtools/shared/profiler/tree-model.js
index f194424..5b79d007 100644
--- a/browser/devtools/shared/profiler/tree-model.js
+++ b/browser/devtools/shared/profiler/tree-model.js
@@ -13,6 +13,8 @@ loader.lazyRequireGetter(this, "CATEGORIES",
"devtools/shared/profiler/global", true);
loader.lazyRequireGetter(this, "CATEGORY_JIT",
"devtools/shared/profiler/global", true);
+loader.lazyRequireGetter(this, "CATEGORY_OTHER",
+ "devtools/shared/profiler/global", true);
loader.lazyRequireGetter(this, "JITOptimizations",
"devtools/shared/profiler/jit", true);
loader.lazyRequireGetter(this, "FrameUtils",
@@ -27,98 +29,261 @@ exports.FrameNode.isContent = FrameUtils.isContent;
* of all samples into a single tree structure, with additional information
* on each node, like the time spent (in milliseconds) and samples count.
*
- * Example:
- * {
- * duration: number,
- * calls: {
- * "FunctionName (url:line)": {
- * line: number,
- * category: number,
- * samples: number,
- * duration: number,
- * calls: {
- * ...
- * }
- * }, // FrameNode
- * ...
- * }
- * } // ThreadNode
- *
* @param object threadSamples
* 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, @see ThreadNode.prototype.insert
* - number startTime [optional]
* - number endTime [optional]
* - boolean contentOnly [optional]
* - boolean invertTree [optional]
- * - object optimizations [optional]
- * The raw tracked optimizations array received from the backend.
*/
-function ThreadNode(threadSamples, options = {}) {
+function ThreadNode(threadSamples, stackTable, frameTable, stringTable, options = {}) {
this.samples = 0;
this.duration = 0;
- this.calls = {};
- this._previousSampleTime = 0;
+ this.calls = [];
+
+ // Maps of frame to their self counts and duration.
+ this.selfCount = Object.create(null);
+ this.selfDuration = Object.create(null);
- for (let sample of threadSamples) {
- this.insert(sample, options);
+ this._buildInverted(threadSamples, stackTable, frameTable, stringTable, options);
+ if (!options.invertTree) {
+ this._uninvertInPlace();
}
}
ThreadNode.prototype = {
/**
- * Adds function calls in the tree from a sample's frames.
+ * 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".
*
- * @param object sample
- * The { frames, time } sample, containing an array of frames and
- * the time the sample was taken. This sample is assumed to be older
- * than the most recently inserted one.
- * @param object options [optional]
- * Additional supported options:
- * - number startTime: the earliest sample to start at (in milliseconds)
- * - number endTime: the latest sample to end at (in milliseconds)
- * - boolean contentOnly: if platform frames shouldn't be used
- * - boolean invertTree: if the call tree should be inverted
- * - object optimizations: The array of all indexable optimizations from the backend.
+ * 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 [optional]
+ * - number endTime [optional]
+ * - boolean contentOnly [optional]
+ * - boolean invertTree [optional]
*/
- insert: function(sample, options = {}) {
+ _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.
+ 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 FRAME_LOCATION_SLOT = frameTable.schema.location;
+ const FRAME_OPTIMIZATIONS_SLOT = frameTable.schema.optimizations;
+ const FRAME_LINE_SLOT = frameTable.schema.line;
+ const FRAME_CATEGORY_SLOT = frameTable.schema.category;
+
+ const InflatedFrame = FrameUtils.InflatedFrame;
+
+ let selfCount = this.selfCount;
+ let selfDuration = this.selfDuration;
+
+ let samplesData = samples.data;
+ let stacksData = stackTable.data;
+ let framesData = frameTable.data;
+ let inflatedFramesCache = new Array(framesData.length);
+ let leafTable = Object.create(null);
+
let startTime = options.startTime || 0;
let endTime = options.endTime || Infinity;
- let optimizations = options.optimizations;
- let sampleTime = sample.time;
- if (!sampleTime || sampleTime < startTime || sampleTime > endTime) {
- return;
- }
+ let prevSampleTime = 0;
- let sampleFrames = sample.frames;
+ // Options object passed to InflatedFrame.prototype.getFrameKey. Reused.
+ let mutableFrameKeyOptions = {
+ contentOnly: options.contentOnly,
+ isLeaf: false,
+ isMetaCategoryOut: false
+ };
- // Filter out platform frames if only content-related function calls
- // should be taken into consideration.
- if (options.contentOnly) {
- // The (root) node is not considered a content function, it'll be removed.
- sampleFrames = FrameUtils.filterPlatformData(sampleFrames);
- } else {
- // Remove the (root) node manually.
- sampleFrames = sampleFrames.slice(1);
- }
- // If no frames remain after filtering, then this is a leaf node, no need
- // to continue.
- if (!sampleFrames.length) {
- return;
- }
- // Invert the tree after filtering, if preferred.
- if (options.invertTree) {
- sampleFrames.reverse();
+ for (let i = 0; i < samplesData.length; i++) {
+ let sample = samplesData[i];
+ let sampleTime = sample[SAMPLE_TIME_SLOT];
+
+ if (!sampleTime || sampleTime < startTime || sampleTime > endTime) {
+ prevSampleTime = sampleTime;
+ continue;
+ }
+
+ let sampleDuration = sampleTime - prevSampleTime;
+ let calls = this.calls;
+ let stackEntry = stacksData[sample[SAMPLE_STACK_SLOT]];
+ let prevFrameKey;
+
+ // Walk the deduplicated stack from youngest to oldest, which is
+ // formatted as a trie in the JSON.
+ while (true) {
+ let prefix = stackEntry[STACK_PREFIX_SLOT];
+ let frameIndex = stackEntry[STACK_FRAME_SLOT];
+
+ // Skip the root frame.
+ if (prefix === null) {
+ break;
+ }
+
+ // Inflate the frame if necessary.
+ let inflatedFrame = inflatedFramesCache[frameIndex];
+ if (inflatedFrame === undefined) {
+ let frame = framesData[frameIndex];
+ inflatedFrame = new InflatedFrame(stringTable[frame[FRAME_LOCATION_SLOT]],
+ frame[FRAME_OPTIMIZATIONS_SLOT],
+ frame[FRAME_LINE_SLOT],
+ frame[FRAME_CATEGORY_SLOT]);
+ inflatedFramesCache[frameIndex] = inflatedFrame;
+ }
+
+ // Fetch the stack prefix (i.e. older frames) now so that below, if we
+ // don't need to insert the frame into the call tree, we can skip it.
+ stackEntry = stacksData[prefix];
+
+ // Compute the frame key.
+ //
+ // Leaf frames are never skipped andrequire self count and duration
+ // bookkeeping.
+ let isLeaf = calls === this.calls;
+ let frameKey;
+ if (isLeaf) {
+ mutableFrameKeyOptions.isLeaf = true;
+ mutableFrameKeyOptions.isMetaCategoryOut = false;
+ frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
+
+ // Tabulate self count and duration for the leaf frame.
+ if (selfCount[frameKey] === undefined) {
+ selfCount[frameKey] = 0;
+ selfDuration[frameKey] = 0;
+ }
+ selfCount[frameKey]++;
+ selfDuration[frameKey] += sampleDuration;
+ } else {
+ mutableFrameKeyOptions.isLeaf = false;
+ mutableFrameKeyOptions.isMetaCategoryOut = false;
+ frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
+
+ // No frame key means this frame should be skipped.
+ if (frameKey === undefined) {
+ continue;
+ }
+ }
+
+ let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
+ mutableFrameKeyOptions.isMetaCategoryOut,
+ leafTable);
+
+ frameNode._countSample(prevSampleTime, sampleTime, inflatedFrame.optimizations,
+ stringTable);
+
+ // When flattening recursion, only add a new level to the call tree if
+ // the current frame key differs from the previous frame key.
+ //
+ // That is, this flattens
+ // A > B > B > C
+ // into
+ // A > B > C
+ if (options.flattenRecursion) {
+ if (frameKey !== prevFrameKey) {
+ calls = frameNode.calls;
+ }
+ } else {
+ calls = frameNode.calls;
+ }
+
+ prevFrameKey = frameKey;
+ }
+
+ this.duration += sampleDuration;
+ this.samples++;
+ prevSampleTime = sampleTime;
}
+ },
- let sampleDuration = sampleTime - this._previousSampleTime;
- this._previousSampleTime = sampleTime;
- this.samples++;
- this.duration += sampleDuration;
+ /**
+ * Uninverts the call tree after its having been built.
+ */
+ _uninvertInPlace: function uninvertInPlace() {
+ let workstack = [{ node: this, level: 0 }];
+ let spine = [];
+ let entry;
+
+ // Walk depth-first and keep the current spine (e.g., callstack).
+ while (entry = workstack.pop()) {
+ spine[entry.level] = entry;
- FrameNode.prototype.insert(
- sampleFrames, optimizations, 0, sampleTime, sampleDuration, this.calls);
+ let node = entry.node;
+ let calls = node.calls;
+ let callees = Object.keys(calls);
+
+ if (callees.length === 0) {
+ // We've bottomed out. Reverse the spine and add them to the
+ // uninverted call tree.
+ let callerNode = this;
+ for (let level = entry.level; level > 0; level--) {
+ let callee = spine[level];
+ callerNode.calls[callee.key] = callee.node;
+ callerNode = callee.node;
+ }
+ } else {
+ // We still have children. Continue the depth-first walk.
+ for (let i = 0; i < callees.length; i++) {
+ let key = callees[i];
+ workstack.push({ node: calls[key],
+ level: entry.level + 1,
+ key: key });
+ }
+
+ // Clear out the current callees, as we are uninverting in-place.
+ node.calls = Object.create(null);
+ }
+ }
},
/**
@@ -155,71 +320,66 @@ ThreadNode.prototype = {
* 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 column
- * The column 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({ location, line, column, category, allocations, isMetaCategory }) {
+function FrameNode(frameKey, { location, line, category, isContent }, isMetaCategory) {
+ if (isMetaCategory && !category) {
+ category = CATEGORY_OTHER;
+ }
+
+ this.key = frameKey;
this.location = location;
this.line = line;
- this.column = column;
this.category = category;
- this.allocations = allocations || 0;
- this.sampleTimes = [];
+ this.allocations = 0;
this.samples = 0;
this.duration = 0;
- this.calls = {};
+ this.calls = [];
+ this.isContent = isContent;
this._optimizations = null;
+ this._stringTable = null;
this.isMetaCategory = isMetaCategory;
}
FrameNode.prototype = {
/**
- * Adds function calls in the tree from a sample's frames. For example, given
- * the the frames below (which would account for three calls to `insert` on
- * the root frame), the following tree structure is created:
+ * Count a sample as associated with this node.
*
- * A
- * A -> B -> C / \
- * A -> B -> D ~> B E
- * A -> E -> F / \ \
- * C D F
- * @param frames
- * The sample call stack.
- * @param optimizations
- * The array of indexable optimizations.
- * @param index
- * The index of the call in the stack representing this node.
- * @param number time
- * The delta time (in milliseconds) when the frame was sampled.
- * @param number duration
- * The amount of time spent executing all functions on the stack.
+ * @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.
*/
- insert: function(frames, optimizations, index, time, duration, _store = this.calls) {
- let frame = frames[index];
- if (!frame) {
- return;
- }
- // If we are only displaying content, then platform data will have
- // a `isMetaCategory` property. Group by category (GC, Graphics, etc.)
- // to group together frames so they're displayed only once, since we don't
- // need the location anyway.
- let key = frame.isMetaCategory ? frame.category : frame.location;
- let child = _store[key] || (_store[key] = new FrameNode(frame));
- child.sampleTimes.push({ start: time, end: time + duration });
- child.samples++;
- child.duration += duration;
- if (optimizations && frame.optsIndex != null) {
- let opts = child._optimizations || (child._optimizations = new JITOptimizations(optimizations));
- opts.addOptimizationSite(frame.optsIndex);
+ _countSample: function (prevSampleTime, sampleTime, optimizationSite, stringTable) {
+ this.samples++;
+ this.duration += sampleTime - prevSampleTime;
+
+ if (optimizationSite) {
+ let opts = this._optimizations;
+ if (opts === null) {
+ opts = this._optimizations = [];
+ this._stringTable = stringTable;
+ }
+ for (let i = 0; i < opts.length; i++) {
+ if (opts[i] === optimizationSite) {
+ break;
+ }
+ }
+ opts.push(optimizationSite);
}
- child.insert(frames, optimizations, index + 1, time, duration);
},
/**
@@ -251,7 +411,7 @@ FrameNode.prototype = {
let parsedData = FrameUtils.parseLocation(this);
parsedData.nodeType = "Frame";
parsedData.categoryData = categoryData;
- parsedData.isContent = FrameUtils.isContent(this);
+ parsedData.isContent = this.isContent;
parsedData.isMetaCategory = this.isMetaCategory;
return this._data = parsedData;
@@ -273,6 +433,6 @@ FrameNode.prototype = {
* @return {JITOptimizations|null}
*/
getOptimizations: function () {
- return this._optimizations;
+ return new JITOptimization(this._optimizations, this._stringTable);
}
};
diff --git a/browser/devtools/shared/profiler/tree-view.js b/browser/devtools/shared/profiler/tree-view.js
index df52a29..4506a23 100644
--- a/browser/devtools/shared/profiler/tree-view.js
+++ b/browser/devtools/shared/profiler/tree-view.js
@@ -125,7 +125,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
let selfDuration;
let totalAllocations;
- if (!this._getChildCalls().length) {
+ if (!this.frame.calls.length) {
if (this.visibleCells.selfPercentage) {
selfPercentage = framePercentage;
}
@@ -136,24 +136,18 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
totalAllocations = this.frame.allocations;
}
} else {
- // Avoid performing costly computations if the respective columns
- // won't be shown anyway.
+ let key = this.frame.isMetaCategory ? this.frame.category : this.frame.location;
if (this.visibleCells.selfPercentage) {
- let childrenPercentage = sum([this._getPercentage(c.samples) for (c of this._getChildCalls())]);
- selfPercentage = clamp(framePercentage - childrenPercentage, 0, 100);
+ let selfSamples = this.root.frame.selfCount[key];
+ selfPercentage = selfSamples / this.root.frame.totalSamples;
}
if (this.visibleCells.selfDuration) {
- let childrenDuration = sum([c.duration for (c of this._getChildCalls())]);
- selfDuration = this.frame.duration - childrenDuration;
+ selfDuration = this.root.frame.selfDuration[key];
}
if (this.visibleCells.allocations) {
- let childrenAllocations = sum([c.allocations for (c of this._getChildCalls())]);
+ let childrenAllocations = sum([c.allocations for (c of this.frame.calls)]);
totalAllocations = this.frame.allocations + childrenAllocations;
}
- if (this.inverted) {
- selfPercentage = framePercentage - selfPercentage;
- selfDuration = this.frame.duration - selfDuration;
- }
}
if (this.visibleCells.duration) {
@@ -232,13 +226,6 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
},
/**
- * Return an array of this frame's child calls.
- */
- _getChildCalls: function() {
- return Object.keys(this.frame.calls).map(k => this.frame.calls[k]);
- },
-
- /**
* Populates this node in the call tree with the corresponding "callees".
* These are defined in the `frame` data source for this call view.
* @param array:AbstractTreeItem children
@@ -246,7 +233,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
_populateSelf: function(children) {
let newLevel = this.level + 1;
- for (let newFrame of this._getChildCalls()) {
+ for (let newFrame of this.frame.calls) {
children.push(new CallView({
caller: this,
frame: newFrame,
diff --git a/browser/devtools/shared/widgets/FlameGraph.js b/browser/devtools/shared/widgets/FlameGraph.js
index ede7852..65d15ac 100644
--- a/browser/devtools/shared/widgets/FlameGraph.js
+++ b/browser/devtools/shared/widgets/FlameGraph.js
@@ -11,6 +11,9 @@ const { getColor } = require("devtools/shared/theme");
const EventEmitter = require("devtools/toolkit/event-emitter");
const FrameUtils = require("devtools/shared/profiler/frame-utils");
+loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
+ "devtools/shared/profiler/global", true);
+
const HTML_NS = "http://www.w3.org/1999/xhtml";
const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
const L10N = new ViewHelpers.L10N();
@@ -1092,6 +1095,179 @@ let FlameGraphUtils = {
return out;
},
+ createFlameGraphDataFromSamples: function(samples, stackTable, frameTable, stringTable,
+ options = {}, out = []) {
+ let cached = this._cache.get(samples);
+ if (cached) {
+ return cached;
+ }
+
+ // 1. Create a map of colors to arrays, representing buckets of
+ // blocks inside the flame graph pyramid sharing the same style.
+
+ let buckets = new Map();
+
+ for (let color of COLOR_PALLETTE) {
+ buckets.set(color, []);
+ }
+
+ // 2. Populate the buckets by iterating over every frame in every sample.
+
+ 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 FRAME_LOCATION_SLOT = frameTable.schema.location;
+ const FRAME_CATEGORY_SLOT = frameTable.schema.category;
+
+ let samplesData = samples.data;
+ let stacksData = stackTable.data;
+ let framesData = frameTable.data;
+ let frameKeyCache = new Array(framesData.length);
+
+ // Reused objects.
+ let scratchFrame = new FrameUtils.InflatedFrame("");
+ let mutableFrameKeyOptions = {
+ contentOnly: options.contentOnly,
+ isRoot: false,
+ isMetaCategoryOut: false
+ };
+
+ let prevTime = 0;
+ let prevFrames = [];
+ let sampleFrames = [];
+
+ for (let i = 0; i < samplesData.length; i++) {
+ let sample = samplesData[i];
+ let time = sample[SAMPLE_TIME_SLOT];
+
+ let stackEntry = stacksData[sample[SAMPLE_STACK_SLOT]];
+ let prevFrameKey;
+
+ // Build the sample's frames.
+ let stackDepth = 0;
+ while (true) {
+ let prefix = stackEntry[STACK_PREFIX_SLOT];
+ let frameIndex = stackEntry[STACK_FRAME_SLOT];
+
+ let frameKey = frameKeyCache[frameIndex];
+ if (frameKey === undefined) {
+ let frame = framesData[frameIndex];
+
+ // Reuse the scratch frame to compute the frame key.
+ scratchFrame._set(stringTable[frame[FRAME_LOCATION_SLOT]],
+ /* optimizations = */ null,
+ /* line = */ undefined,
+ frame[FRAME_CATEGORY_SLOT]);
+
+ mutableFrameKeyOptions.isRoot = stackDepth === 0;
+ mutableFrameKeyOptions.isMetaCategoryOut = false;
+ frameKey = scratchFrame.getFrameKey(mutableFrameKeyOptions);
+
+ // No frame key means this frame should be skipped. Use null to
+ // distinguish "should be skipped" from "not found in cache".
+ if (frameKey === undefined) {
+ frameKey = null;
+ }
+
+ // If the frame is a meta category, use the category label.
+ if (mutableFrameKeyOptions.isMetaCategoryOut) {
+ frameKey = CATEGORY_MAPPINGS[frameKey].label;
+ }
+
+ frameKeyCache[frameIndex] = frameKey;
+ }
+
+ if (frameKey !== null) {
+ sampleFrames[stackDepth] = frameKey;
+
+ // When flattening recursion, only add the frame to the stack if it
+ // differs from the previous frame key.
+ if (options.flattenRecursion) {
+ if (frameKey !== prevFrameKey) {
+ stackDepth++;
+ }
+ } else {
+ stackDepth++;
+ }
+ }
+
+ // We are at the root frame.
+ if (prefix === null) {
+ break;
+ }
+
+ stackEntry = stacksData[prefix];
+
+ if (frameKey !== null) {
+ prevFrameKey = frameKey;
+ }
+ }
+
+ // Uninvert frames in place if needed.
+ if (!options.invertTree) {
+ sampleFrames.length = stackDepth;
+ sampleFrames.reverse();
+ }
+
+ // If no frames are available, add a pseudo "idle" block in between.
+ let isIdleFrame = false;
+ if (options.showIdleBlocks && stackDepth === 0) {
+ sampleFrames[0] = options.showIdleBlocks;
+ stackDepth = 1;
+ isIdleFrame = true;
+ }
+
+ // Put each frame in a bucket.
+ for (let frameIndex = 0; frameIndex < stackDepth; frameIndex++) {
+ let key = sampleFrames[frameIndex];
+ let prevFrame = prevFrames[frameIndex];
+
+ // Frames at the same location and the same depth will be reused.
+ // If there is a block already created, change its width.
+ if (prevFrame && prevFrame.srcData.key === key) {
+ prevFrame.width = (time - prevFrame.srcData.startTime);
+ }
+ // Otherwise, create a new block for this frame at this depth,
+ // using a simple location based salt for picking a color.
+ else {
+ let hash = this._getStringHash(key);
+ let color = COLOR_PALLETTE[hash % PALLETTE_SIZE];
+ let bucket = buckets.get(color);
+
+ // Reuse the scratch frame for formatLabel.
+ scratchFrame._set(key, null);
+
+ bucket.push(prevFrames[frameIndex] = {
+ srcData: { startTime: prevTime, key: key },
+ x: prevTime,
+ y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
+ width: time - prevTime,
+ height: FLAME_GRAPH_BLOCK_HEIGHT,
+ text: this._formatLabel(scratchFrame, isIdleFrame)
+ });
+ }
+ }
+
+ // Previous frames at stack depths greater than the current sample's
+ // maximum need to be nullified. It's nonsensical to reuse them.
+ prevFrames.length = stackDepth;
+ prevTime = time;
+ }
+
+ // 3. Convert the buckets into a data source usable by the FlameGraph.
+ // This is a simple conversion from a Map to an Array.
+
+ for (let [color, blocks] of buckets) {
+ out.push({ color, blocks });
+ }
+
+ this._cache.set(samples, out);
+ return out;
+ },
+
/**
* Clears the cached flame graph data created for the given source.
* @param any source
@@ -1147,10 +1323,10 @@ let FlameGraphUtils = {
* @param FrameNode frame
* @return string
*/
- _formatLabel: function (frame) {
+ _formatLabel: function (frame, idle) {
// If an idle block, just return the location which will just be "(idle)" text
// anyway.
- if (frame.idle) {
+ if (idle) {
return frame.location;
}
diff --git a/browser/themes/shared/devtools/performance.inc.css b/browser/themes/shared/devtools/performance.inc.css
index 7f0a4bc..c1d0877 100644
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -542,11 +542,6 @@
font-weight: 600;
}
-#jit-optimizations-view .opt-ion-type-site {
- -moz-margin-start: 4px !important;
- opacity: 0.6;
-}
-
#jit-optimizations-view .opt-outcome::before {
content: "→";
margin: 4px 0px;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment