Created
April 30, 2015 23:16
-
-
Save syg/8acd3b036924a44735ac to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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