summaryrefslogtreecommitdiff
path: root/toolkit/devtools/shared/profiler
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/shared/profiler')
-rw-r--r--toolkit/devtools/shared/profiler/global.js108
-rw-r--r--toolkit/devtools/shared/profiler/tree-model.js281
-rw-r--r--toolkit/devtools/shared/profiler/tree-view.js345
3 files changed, 734 insertions, 0 deletions
diff --git a/toolkit/devtools/shared/profiler/global.js b/toolkit/devtools/shared/profiler/global.js
new file mode 100644
index 000000000..0a89be089
--- /dev/null
+++ b/toolkit/devtools/shared/profiler/global.js
@@ -0,0 +1,108 @@
+/* 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");
+
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+/**
+ * Localization convenience methods.
+ */
+const STRINGS_URI = "chrome://browser/locale/devtools/profiler.properties";
+const L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+/**
+ * Details about each profile entry cateogry.
+ * @see CATEGORY_MAPPINGS.
+ */
+const CATEGORIES = [
+ { ordinal: 7, color: "#5e88b0", abbrev: "other", label: L10N.getStr("category.other") },
+ { ordinal: 4, color: "#46afe3", abbrev: "css", label: L10N.getStr("category.css") },
+ { ordinal: 1, color: "#d96629", abbrev: "js", label: L10N.getStr("category.js") },
+ { ordinal: 2, color: "#eb5368", abbrev: "gc", label: L10N.getStr("category.gc") },
+ { ordinal: 0, color: "#df80ff", abbrev: "network", label: L10N.getStr("category.network") },
+ { ordinal: 5, color: "#70bf53", abbrev: "graphics", label: L10N.getStr("category.graphics") },
+ { ordinal: 6, color: "#8fa1b2", abbrev: "storage", label: L10N.getStr("category.storage") },
+ { ordinal: 3, color: "#d99b28", abbrev: "events", label: L10N.getStr("category.events") }
+];
+
+/**
+ * Mapping from category bitmasks in the profiler data to additional details.
+ * To be kept in sync with the js::ProfileEntry::Category in ProfilingStack.h
+ */
+const CATEGORY_MAPPINGS = {
+ "16": CATEGORIES[0], // js::ProfileEntry::Category::OTHER
+ "32": CATEGORIES[1], // js::ProfileEntry::Category::CSS
+ "64": CATEGORIES[2], // js::ProfileEntry::Category::JS
+ "128": CATEGORIES[3], // js::ProfileEntry::Category::GC
+ "256": CATEGORIES[3], // js::ProfileEntry::Category::CC
+ "512": CATEGORIES[4], // js::ProfileEntry::Category::NETWORK
+ "1024": CATEGORIES[5], // js::ProfileEntry::Category::GRAPHICS
+ "2048": CATEGORIES[6], // js::ProfileEntry::Category::STORAGE
+ "4096": CATEGORIES[7], // js::ProfileEntry::Category::EVENTS
+};
+
+/**
+ * Get the numeric bitmask (or set of masks) for the given category
+ * abbreviation. See CATEGORIES and CATEGORY_MAPPINGS above.
+ *
+ * CATEGORY_MASK can be called with just a name if it is expected that the
+ * category is mapped to by exactly one bitmask. If the category is mapped
+ * to by multiple masks, CATEGORY_MASK for that name must be called with
+ * an additional argument specifying the desired id (in ascending order).
+ */
+const [CATEGORY_MASK, CATEGORY_MASK_LIST] = (function () {
+ let mappings = {};
+ for (let category of CATEGORIES) {
+ let numList = Object.keys(CATEGORY_MAPPINGS)
+ .filter(k => CATEGORY_MAPPINGS[k] == category)
+ .map(k => +k);
+ numList.sort();
+ mappings[category.abbrev] = numList;
+ }
+
+ return [
+ function (name, num) {
+ if (!(name in mappings)) {
+ throw new Error(`Category abbreviation '${name}' does not exist.`);
+ }
+ if (arguments.length == 1) {
+ if (mappings[name].length != 1) {
+ throw new Error(`Expected exactly one category number for '${name}'.`);
+ }
+ return mappings[name][0];
+ }
+ if (num > mappings[name].length) {
+ throw new Error(`Num '${num}' too high for category '${name}'.`);
+ }
+ return mappings[name][num - 1];
+ },
+
+ function (name) {
+ if (!(name in mappings)) {
+ throw new Error(`Category abbreviation '${name}' does not exist.`);
+ }
+ return mappings[name];
+ }
+ ];
+})();
+
+// Human-readable "other" category bitmask. Older Geckos don't have all the
+// necessary instrumentation in the sampling profiler backend for creating
+// a categories graph, in which case we default to the "other" category.
+const CATEGORY_OTHER = CATEGORY_MASK('other');
+
+// Human-readable JIT category bitmask. Certain pseudo-frames in a sample,
+// like "EnterJIT", don't have any associated `cateogry` information.
+const CATEGORY_JIT = CATEGORY_MASK('js');
+
+// Exported symbols.
+exports.L10N = L10N;
+exports.CATEGORIES = CATEGORIES;
+exports.CATEGORY_MAPPINGS = CATEGORY_MAPPINGS;
+exports.CATEGORY_OTHER = CATEGORY_OTHER;
+exports.CATEGORY_JIT = CATEGORY_JIT;
+exports.CATEGORY_MASK = CATEGORY_MASK;
+exports.CATEGORY_MASK_LIST = CATEGORY_MASK_LIST;
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();
diff --git a/toolkit/devtools/shared/profiler/tree-view.js b/toolkit/devtools/shared/profiler/tree-view.js
new file mode 100644
index 000000000..9a05e5dee
--- /dev/null
+++ b/toolkit/devtools/shared/profiler/tree-view.js
@@ -0,0 +1,345 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+
+loader.lazyRequireGetter(this, "L10N",
+ "devtools/shared/profiler/global", true);
+
+loader.lazyImporter(this, "Heritage",
+ "resource:///modules/devtools/ViewHelpers.jsm");
+loader.lazyImporter(this, "AbstractTreeItem",
+ "resource:///modules/devtools/AbstractTreeItem.jsm");
+
+const MILLISECOND_UNITS = L10N.getStr("table.ms");
+const PERCENTAGE_UNITS = L10N.getStr("table.percentage");
+const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
+const ZOOM_BUTTON_TOOLTIP = L10N.getStr("table.zoom.tooltiptext");
+const CALL_TREE_AUTO_EXPAND = 3; // depth
+const CALL_TREE_INDENTATION = 16; // px
+const DEFAULT_SORTING_PREDICATE = (a, b) => a.frame.samples < b.frame.samples ? 1 : -1;
+
+const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
+const sum = vals => vals.reduce((a, b) => a + b, 0);
+
+exports.CallView = CallView;
+
+/**
+ * An item in a call tree view, which looks like this:
+ *
+ * Time (ms) | Cost | Calls | Function
+ * ============================================================================
+ * 1,000.00 | 100.00% | | ▼ (root)
+ * 500.12 | 50.01% | 300 | ▼ foo Categ. 1
+ * 300.34 | 30.03% | 1500 | ▼ bar Categ. 2
+ * 10.56 | 0.01% | 42 | ▶ call_with_children Categ. 3
+ * 90.78 | 0.09% | 25 | call_without_children Categ. 4
+ *
+ * Every instance of a `CallView` represents a row in the call tree. The same
+ * parent node is used for all rows.
+ *
+ * @param CallView caller
+ * The CallView considered the "caller" frame. This instance will be
+ * represent the "callee". Should be null for root nodes.
+ * @param ThreadNode | FrameNode frame
+ * Details about this function, like { samples, duration, calls } etc.
+ * @param number level
+ * The indentation level in the call tree. The root node is at level 0.
+ * @param boolean hidden [optional]
+ * Whether this node should be hidden and not contribute to depth/level
+ * calculations. Defaults to false.
+ * @param boolean inverted [optional]
+ * Whether the call tree has been inverted (bottom up, rather than
+ * top-down). Defaults to false.
+ * @param function sortingPredicate [optional]
+ * The predicate used to sort the tree items when created. Defaults to
+ * the caller's sortingPredicate if a caller exists, otherwise defaults
+ * to DEFAULT_SORTING_PREDICATE. The two passed arguments are FrameNodes.
+ * @param number autoExpandDepth [optional]
+ * The depth to which the tree should automatically expand. Defualts to
+ * the caller's `autoExpandDepth` if a caller exists, otherwise defaults
+ * to CALL_TREE_AUTO_EXPAND.
+ */
+function CallView({ caller, frame, level, hidden, inverted, sortingPredicate, autoExpandDepth }) {
+ // Assume no indentation if this tree item's level is not specified.
+ level = level || 0;
+
+ // Don't increase indentation if this tree item is hidden.
+ if (hidden) {
+ level--;
+ }
+
+ AbstractTreeItem.call(this, { parent: caller, level });
+
+ this.sortingPredicate = sortingPredicate != null
+ ? sortingPredicate
+ : caller ? caller.sortingPredicate
+ : DEFAULT_SORTING_PREDICATE
+
+ this.autoExpandDepth = autoExpandDepth != null
+ ? autoExpandDepth
+ : caller ? caller.autoExpandDepth
+ : CALL_TREE_AUTO_EXPAND;
+
+ this.caller = caller;
+ this.frame = frame;
+ this.hidden = hidden;
+ this.inverted = inverted;
+
+ this._onUrlClick = this._onUrlClick.bind(this);
+ this._onZoomClick = this._onZoomClick.bind(this);
+};
+
+CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+ /**
+ * Creates the view for this tree node.
+ * @param nsIDOMNode document
+ * @param nsIDOMNode arrowNode
+ * @return nsIDOMNode
+ */
+ _displaySelf: function(document, arrowNode) {
+ this.document = document;
+
+ let frameInfo = this.frame.getInfo();
+ let framePercentage = this._getPercentage(this.frame.samples);
+
+ let selfPercentage;
+ let selfDuration;
+ let totalAllocations;
+
+ if (!this._getChildCalls().length) {
+ selfPercentage = framePercentage;
+ selfDuration = this.frame.duration;
+ totalAllocations = this.frame.allocations;
+ } else {
+ let childrenPercentage = sum(
+ [this._getPercentage(c.samples) for (c of this._getChildCalls())]);
+ let childrenDuration = sum(
+ [c.duration for (c of this._getChildCalls())]);
+ let childrenAllocations = sum(
+ [c.allocations for (c of this._getChildCalls())]);
+
+ selfPercentage = clamp(framePercentage - childrenPercentage, 0, 100);
+ selfDuration = this.frame.duration - childrenDuration;
+ totalAllocations = this.frame.allocations + childrenAllocations;
+
+ if (this.inverted) {
+ selfPercentage = framePercentage - selfPercentage;
+ selfDuration = this.frame.duration - selfDuration;
+ }
+ }
+
+ let durationCell = this._createTimeCell(this.frame.duration);
+ let selfDurationCell = this._createTimeCell(selfDuration, true);
+ let percentageCell = this._createExecutionCell(framePercentage);
+ let selfPercentageCell = this._createExecutionCell(selfPercentage, true);
+ let allocationsCell = this._createAllocationsCell(totalAllocations);
+ let selfAllocationsCell = this._createAllocationsCell(this.frame.allocations, true);
+ let samplesCell = this._createSamplesCell(this.frame.samples);
+ let functionCell = this._createFunctionCell(arrowNode, frameInfo, this.level);
+
+ let targetNode = document.createElement("hbox");
+ targetNode.className = "call-tree-item";
+ targetNode.setAttribute("origin", frameInfo.isContent ? "content" : "chrome");
+ targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
+ targetNode.setAttribute("tooltiptext", this.frame.location || "");
+ if (this.hidden) {
+ targetNode.style.display = "none";
+ }
+
+ let isRoot = frameInfo.nodeType == "Thread";
+ if (isRoot) {
+ functionCell.querySelector(".call-tree-zoom").hidden = true;
+ functionCell.querySelector(".call-tree-category").hidden = true;
+ }
+
+ targetNode.appendChild(durationCell);
+ targetNode.appendChild(percentageCell);
+ targetNode.appendChild(allocationsCell);
+ targetNode.appendChild(selfDurationCell);
+ targetNode.appendChild(selfPercentageCell);
+ targetNode.appendChild(selfAllocationsCell);
+ targetNode.appendChild(samplesCell);
+ targetNode.appendChild(functionCell);
+
+ return targetNode;
+ },
+
+ /**
+ * Calculate what percentage of all samples the given number of samples is.
+ */
+ _getPercentage: function(samples) {
+ return samples / this.root.frame.samples * 100;
+ },
+
+ /**
+ * 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
+ */
+ _populateSelf: function(children) {
+ let newLevel = this.level + 1;
+
+ for (let newFrame of this._getChildCalls()) {
+ children.push(new CallView({
+ caller: this,
+ frame: newFrame,
+ level: newLevel,
+ inverted: this.inverted
+ }));
+ }
+
+ // Sort the "callees" asc. by samples, before inserting them in the tree,
+ // if no other sorting predicate was specified on this on the root item.
+ children.sort(this.sortingPredicate);
+ },
+
+ /**
+ * Functions creating each cell in this call view.
+ * Invoked by `_displaySelf`.
+ */
+ _createTimeCell: function(duration, isSelf = false) {
+ let cell = this.document.createElement("label");
+ cell.className = "plain call-tree-cell";
+ cell.setAttribute("type", isSelf ? "self-duration" : "duration");
+ cell.setAttribute("crop", "end");
+ cell.setAttribute("value", L10N.numberWithDecimals(duration, 2) + " " + MILLISECOND_UNITS);
+ return cell;
+ },
+ _createExecutionCell: function(percentage, isSelf = false) {
+ let cell = this.document.createElement("label");
+ cell.className = "plain call-tree-cell";
+ cell.setAttribute("type", isSelf ? "self-percentage" : "percentage");
+ cell.setAttribute("crop", "end");
+ cell.setAttribute("value", L10N.numberWithDecimals(percentage, 2) + PERCENTAGE_UNITS);
+ return cell;
+ },
+ _createAllocationsCell: function(count, isSelf = false) {
+ let cell = this.document.createElement("label");
+ cell.className = "plain call-tree-cell";
+ cell.setAttribute("type", isSelf ? "self-allocations" : "allocations");
+ cell.setAttribute("crop", "end");
+ cell.setAttribute("value", count || 0);
+ return cell;
+ },
+ _createSamplesCell: function(count) {
+ let cell = this.document.createElement("label");
+ cell.className = "plain call-tree-cell";
+ cell.setAttribute("type", "samples");
+ cell.setAttribute("crop", "end");
+ cell.setAttribute("value", count || "");
+ return cell;
+ },
+ _createFunctionCell: function(arrowNode, frameInfo, frameLevel) {
+ let cell = this.document.createElement("hbox");
+ cell.className = "call-tree-cell";
+ cell.style.MozMarginStart = (frameLevel * CALL_TREE_INDENTATION) + "px";
+ cell.setAttribute("type", "function");
+ cell.appendChild(arrowNode);
+
+ let nameNode = this.document.createElement("label");
+ nameNode.className = "plain call-tree-name";
+ nameNode.setAttribute("flex", "1");
+ nameNode.setAttribute("crop", "end");
+ nameNode.setAttribute("value", frameInfo.functionName || "");
+ cell.appendChild(nameNode);
+
+ let urlNode = this.document.createElement("label");
+ urlNode.className = "plain call-tree-url";
+ urlNode.setAttribute("flex", "1");
+ urlNode.setAttribute("crop", "end");
+ urlNode.setAttribute("value", frameInfo.fileName || "");
+ urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
+ urlNode.addEventListener("mousedown", this._onUrlClick);
+ cell.appendChild(urlNode);
+
+ let lineNode = this.document.createElement("label");
+ lineNode.className = "plain call-tree-line";
+ lineNode.setAttribute("value", frameInfo.line ? ":" + frameInfo.line : "");
+ cell.appendChild(lineNode);
+
+ let columnNode = this.document.createElement("label");
+ columnNode.className = "plain call-tree-column";
+ columnNode.setAttribute("value", frameInfo.column ? ":" + frameInfo.column : "");
+ cell.appendChild(columnNode);
+
+ let hostNode = this.document.createElement("label");
+ hostNode.className = "plain call-tree-host";
+ hostNode.setAttribute("value", frameInfo.hostName || "");
+ cell.appendChild(hostNode);
+
+ let zoomNode = this.document.createElement("button");
+ zoomNode.className = "plain call-tree-zoom";
+ zoomNode.setAttribute("tooltiptext", ZOOM_BUTTON_TOOLTIP);
+ zoomNode.addEventListener("mousedown", this._onZoomClick);
+ cell.appendChild(zoomNode);
+
+ let spacerNode = this.document.createElement("spacer");
+ spacerNode.setAttribute("flex", "10000");
+ cell.appendChild(spacerNode);
+
+ let categoryNode = this.document.createElement("label");
+ categoryNode.className = "plain call-tree-category";
+ categoryNode.style.color = frameInfo.categoryData.color;
+ categoryNode.setAttribute("value", frameInfo.categoryData.label || "");
+ cell.appendChild(categoryNode);
+
+ let hasDescendants = Object.keys(this.frame.calls).length > 0;
+ if (hasDescendants == false) {
+ arrowNode.setAttribute("invisible", "");
+ }
+
+ return cell;
+ },
+
+ /**
+ * Toggles the allocations information hidden or visible.
+ * @param boolean visible
+ */
+ toggleAllocations: function(visible) {
+ if (!visible) {
+ this.container.setAttribute("allocations-hidden", "");
+ } else {
+ this.container.removeAttribute("allocations-hidden");
+ }
+ },
+
+ /**
+ * Toggles the category information hidden or visible.
+ * @param boolean visible
+ */
+ toggleCategories: function(visible) {
+ if (!visible) {
+ this.container.setAttribute("categories-hidden", "");
+ } else {
+ this.container.removeAttribute("categories-hidden");
+ }
+ },
+
+ /**
+ * Handler for the "click" event on the url node of this call view.
+ */
+ _onUrlClick: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.root.emit("link", this);
+ },
+
+ /**
+ * Handler for the "click" event on the zoom node of this call view.
+ */
+ _onZoomClick: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.root.emit("zoom", this);
+ }
+});