diff options
Diffstat (limited to 'toolkit/devtools/shared/profiler/tree-model.js')
-rw-r--r-- | toolkit/devtools/shared/profiler/tree-model.js | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/toolkit/devtools/shared/profiler/tree-model.js b/toolkit/devtools/shared/profiler/tree-model.js new file mode 100644 index 000000000..b513904cb --- /dev/null +++ b/toolkit/devtools/shared/profiler/tree-model.js @@ -0,0 +1,281 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {Cc, Ci, Cu, Cr} = require("chrome"); + +loader.lazyRequireGetter(this, "Services"); +loader.lazyRequireGetter(this, "L10N", + "devtools/shared/profiler/global", true); +loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS", + "devtools/shared/profiler/global", true); +loader.lazyRequireGetter(this, "CATEGORY_JIT", + "devtools/shared/profiler/global", true); + +const CHROME_SCHEMES = ["chrome://", "resource://"]; +const CONTENT_SCHEMES = ["http://", "https://", "file://"]; + +exports.ThreadNode = ThreadNode; +exports.FrameNode = FrameNode; +exports.FrameNode.isContent = isContent; + +/** + * A call tree for a thread. This is essentially a linkage between all frames + * of all samples into a single tree structure, with additional information + * on each node, like the time spent (in milliseconds) and samples count. + * + * 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 options + * Additional supported options, @see ThreadNode.prototype.insert + * - number startTime [optional] + * - number endTime [optional] + * - boolean contentOnly [optional] + * - boolean invertTree [optional] + */ +function ThreadNode(threadSamples, options = {}) { + this.samples = 0; + this.duration = 0; + this.calls = {}; + this._previousSampleTime = 0; + + for (let sample of threadSamples) { + this.insert(sample, options); + } +} + +ThreadNode.prototype = { + /** + * Adds function calls in the tree from a sample's frames. + * + * @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 + */ + insert: function(sample, options = {}) { + let startTime = options.startTime || 0; + let endTime = options.endTime || Infinity; + let sampleTime = sample.time; + if (!sampleTime || sampleTime < startTime || sampleTime > endTime) { + return; + } + + let sampleFrames = sample.frames; + + // 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 = sampleFrames.filter(isContent); + } 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(); + } + + let sampleDuration = sampleTime - this._previousSampleTime; + this._previousSampleTime = sampleTime; + this.samples++; + this.duration += sampleDuration; + + FrameNode.prototype.insert( + sampleFrames, 0, sampleTime, sampleDuration, this.calls); + }, + + /** + * Gets additional details about this node. + * @return object + */ + getInfo: function() { + return { + nodeType: "Thread", + functionName: L10N.getStr("table.root"), + categoryData: {} + }; + } +}; + +/** + * A function call node in a tree. + * + * @param string location + * The location of this function call. Note that this isn't sanitized, + * so it may very well (not?) include the function name, url, etc. + * @param number line + * The line number inside the source containing this function call. + * @param number 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. + */ +function FrameNode({ location, line, column, category, allocations }) { + this.location = location; + this.line = line; + this.column = column; + this.category = category; + this.allocations = allocations || 0; + this.sampleTimes = []; + this.samples = 0; + this.duration = 0; + this.calls = {}; +} + +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: + * + * A + * A -> B -> C / \ + * A -> B -> D ~> B E + * A -> E -> F / \ \ + * C D F + * @param frames + * The sample call stack. + * @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. + */ + insert: function(frames, index, time, duration, _store = this.calls) { + let frame = frames[index]; + if (!frame) { + return; + } + let location = frame.location; + let child = _store[location] || (_store[location] = new FrameNode(frame)); + child.sampleTimes.push({ start: time, end: time + duration }); + child.samples++; + child.duration += duration; + child.insert(frames, ++index, time, duration); + }, + + /** + * Parses the raw location of this function call to retrieve the actual + * function name and source url. + * + * @return object + * The computed { name, file, url, line } properties for this + * function call. + */ + getInfo: function() { + // "EnterJIT" pseudoframes are special, not actually on the stack. + if (this.location == "EnterJIT") { + this.category = CATEGORY_JIT; + } + + // Since only C++ stack frames have associated category information, + // default to an "unknown" category otherwise. + let categoryData = CATEGORY_MAPPINGS[this.category] || {}; + + // Parse the `location` for the function name, source url, line, column etc. + let lineAndColumn = this.location.match(/((:\d+)*)\)?$/)[1]; + let [, line, column] = lineAndColumn.split(":"); + line = line || this.line; + column = column || this.column; + + let firstParenIndex = this.location.indexOf("("); + let lineAndColumnIndex = this.location.indexOf(lineAndColumn); + let resource = this.location.substring(firstParenIndex + 1, lineAndColumnIndex); + + let url = resource.split(" -> ").pop(); + let uri = nsIURL(url); + let functionName, fileName, hostName; + + // If the URI digged out from the `location` is valid, this is a JS frame. + if (uri) { + functionName = this.location.substring(0, firstParenIndex - 1); + fileName = (uri.fileName + (uri.ref ? "#" + uri.ref : "")) || "/"; + hostName = uri.host; + } else { + functionName = this.location; + url = null; + } + + return { + nodeType: "Frame", + functionName: functionName, + fileName: fileName, + hostName: hostName, + url: url, + line: line, + column: column, + categoryData: categoryData, + isContent: !!isContent(this) + }; + } +}; + +/** + * 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. + */ +function isContent({ category, location }) { + // Only C++ stack frames have associated category information. + return !category && + !CHROME_SCHEMES.find(e => location.contains(e)) && + CONTENT_SCHEMES.find(e => location.contains(e)); +} + +/** + * Helper for getting an nsIURL instance out of a string. + */ +function nsIURL(url) { + let cached = gNSURLStore.get(url); + if (cached) { + return cached; + } + let uri = null; + try { + uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL); + } catch(e) { + // The passed url string is invalid. + } + gNSURLStore.set(url, uri); + return uri; +} + +// The cache used in the `nsIURL` function. +let gNSURLStore = new Map(); |