summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/modules/widgets/tree-view.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/modules/widgets/tree-view.js')
-rw-r--r--devtools/client/performance/modules/widgets/tree-view.js406
1 files changed, 406 insertions, 0 deletions
diff --git a/devtools/client/performance/modules/widgets/tree-view.js b/devtools/client/performance/modules/widgets/tree-view.js
new file mode 100644
index 000000000..d3d81fe3b
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/tree-view.js
@@ -0,0 +1,406 @@
+/* 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";
+
+/**
+ * This file contains the tree view, displaying all the samples and frames
+ * received from the proviler in a tree-like structure.
+ */
+
+const { L10N } = require("devtools/client/performance/modules/global");
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
+
+const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
+const VIEW_OPTIMIZATIONS_TOOLTIP = L10N.getStr("table.view-optimizations.tooltiptext2");
+
+// px
+const CALL_TREE_INDENTATION = 16;
+
+// Used for rendering values in cells
+const FORMATTERS = {
+ TIME: (value) => L10N.getFormatStr("table.ms2", L10N.numberWithDecimals(value, 2)),
+ PERCENT: (value) => L10N.getFormatStr("table.percentage3",
+ L10N.numberWithDecimals(value, 2)),
+ NUMBER: (value) => value || 0,
+ BYTESIZE: (value) => L10N.getFormatStr("table.bytes", (value || 0))
+};
+
+/**
+ * Definitions for rendering cells. Triads of class name, property name from
+ * `frame.getInfo()`, and a formatter function.
+ */
+const CELLS = {
+ duration: ["duration", "totalDuration", FORMATTERS.TIME],
+ percentage: ["percentage", "totalPercentage", FORMATTERS.PERCENT],
+ selfDuration: ["self-duration", "selfDuration", FORMATTERS.TIME],
+ selfPercentage: ["self-percentage", "selfPercentage", FORMATTERS.PERCENT],
+ samples: ["samples", "samples", FORMATTERS.NUMBER],
+
+ selfSize: ["self-size", "selfSize", FORMATTERS.BYTESIZE],
+ selfSizePercentage: ["self-size-percentage", "selfSizePercentage", FORMATTERS.PERCENT],
+ selfCount: ["self-count", "selfCount", FORMATTERS.NUMBER],
+ selfCountPercentage: ["self-count-percentage", "selfCountPercentage",
+ FORMATTERS.PERCENT],
+ size: ["size", "totalSize", FORMATTERS.BYTESIZE],
+ sizePercentage: ["size-percentage", "totalSizePercentage", FORMATTERS.PERCENT],
+ count: ["count", "totalCount", FORMATTERS.NUMBER],
+ countPercentage: ["count-percentage", "totalCountPercentage", FORMATTERS.PERCENT],
+};
+const CELL_TYPES = Object.keys(CELLS);
+
+const DEFAULT_SORTING_PREDICATE = (frameA, frameB) => {
+ let dataA = frameA.getDisplayedData();
+ let dataB = frameB.getDisplayedData();
+ let isAllocations = "totalSize" in dataA;
+
+ if (isAllocations) {
+ if (this.inverted && dataA.selfSize !== dataB.selfSize) {
+ return dataA.selfSize < dataB.selfSize ? 1 : -1;
+ }
+ return dataA.totalSize < dataB.totalSize ? 1 : -1;
+ }
+
+ if (this.inverted && dataA.selfPercentage !== dataB.selfPercentage) {
+ return dataA.selfPercentage < dataB.selfPercentage ? 1 : -1;
+ }
+ return dataA.totalPercentage < dataB.totalPercentage ? 1 : -1;
+};
+
+// depth
+const DEFAULT_AUTO_EXPAND_DEPTH = 3;
+const DEFAULT_VISIBLE_CELLS = {
+ duration: true,
+ percentage: true,
+ selfDuration: true,
+ selfPercentage: true,
+ samples: true,
+ function: true,
+
+ // allocation columns
+ count: false,
+ selfCount: false,
+ size: false,
+ selfSize: false,
+ countPercentage: false,
+ selfCountPercentage: false,
+ sizePercentage: false,
+ selfSizePercentage: false,
+};
+
+/**
+ * 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 newly created
+ * 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 [optional]
+ * 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 DEFAULT_AUTO_EXPAND_DEPTH.
+ * @param object visibleCells
+ * An object specifying which cells are visible in the tree. Defaults to
+ * the caller's `visibleCells` if a caller exists, otherwise defaults
+ * to DEFAULT_VISIBLE_CELLS.
+ * @param boolean showOptimizationHint [optional]
+ * Whether or not to show an icon indicating if the frame has optimization
+ * data.
+ */
+function CallView({
+ caller, frame, level, hidden, inverted,
+ sortingPredicate, autoExpandDepth, visibleCells,
+ showOptimizationHint
+}) {
+ AbstractTreeItem.call(this, {
+ parent: caller,
+ level: level | 0 - (hidden ? 1 : 0)
+ });
+
+ if (sortingPredicate != null) {
+ this.sortingPredicate = sortingPredicate;
+ } else if (caller) {
+ this.sortingPredicate = caller.sortingPredicate;
+ } else {
+ this.sortingPredicate = DEFAULT_SORTING_PREDICATE;
+ }
+
+ if (autoExpandDepth != null) {
+ this.autoExpandDepth = autoExpandDepth;
+ } else if (caller) {
+ this.autoExpandDepth = caller.autoExpandDepth;
+ } else {
+ this.autoExpandDepth = DEFAULT_AUTO_EXPAND_DEPTH;
+ }
+
+ if (visibleCells != null) {
+ this.visibleCells = visibleCells;
+ } else if (caller) {
+ this.visibleCells = caller.visibleCells;
+ } else {
+ this.visibleCells = Object.create(DEFAULT_VISIBLE_CELLS);
+ }
+
+ this.caller = caller;
+ this.frame = frame;
+ this.hidden = hidden;
+ this.inverted = inverted;
+ this.showOptimizationHint = showOptimizationHint;
+
+ this._onUrlClick = this._onUrlClick.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) {
+ let frameInfo = this.getDisplayedData();
+ let cells = [];
+
+ for (let type of CELL_TYPES) {
+ if (this.visibleCells[type]) {
+ // Inline for speed, but pass in the formatted value via
+ // cell definition, as well as the element type.
+ cells.push(this._createCell(document, CELLS[type][2](frameInfo[CELLS[type][1]]),
+ CELLS[type][0]));
+ }
+ }
+
+ if (this.visibleCells.function) {
+ cells.push(this._createFunctionCell(document, arrowNode, frameInfo.name, 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", frameInfo.tooltiptext);
+
+ if (this.hidden) {
+ targetNode.style.display = "none";
+ }
+
+ for (let i = 0; i < cells.length; i++) {
+ targetNode.appendChild(cells[i]);
+ }
+
+ return targetNode;
+ },
+
+ /**
+ * 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.frame.calls) {
+ 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.bind(this));
+ },
+
+ /**
+ * Functions creating each cell in this call view.
+ * Invoked by `_displaySelf`.
+ */
+ _createCell: function (doc, value, type) {
+ let cell = doc.createElement("description");
+ cell.className = "plain call-tree-cell";
+ cell.setAttribute("type", type);
+ cell.setAttribute("crop", "end");
+ // Add a tabulation to the cell text in case it's is selected and copied.
+ cell.textContent = value + "\t";
+ return cell;
+ },
+
+ _createFunctionCell: function (doc, arrowNode, frameName, frameInfo, frameLevel) {
+ let cell = doc.createElement("hbox");
+ cell.className = "call-tree-cell";
+ cell.style.marginInlineStart = (frameLevel * CALL_TREE_INDENTATION) + "px";
+ cell.setAttribute("type", "function");
+ cell.appendChild(arrowNode);
+
+ // Render optimization hint if this frame has opt data.
+ if (this.root.showOptimizationHint && frameInfo.hasOptimizations &&
+ !frameInfo.isMetaCategory) {
+ let icon = doc.createElement("description");
+ icon.setAttribute("tooltiptext", VIEW_OPTIMIZATIONS_TOOLTIP);
+ icon.className = "opt-icon";
+ cell.appendChild(icon);
+ }
+
+ // Don't render a name label node if there's no function name. A different
+ // location label node will be rendered instead.
+ if (frameName) {
+ let nameNode = doc.createElement("description");
+ nameNode.className = "plain call-tree-name";
+ nameNode.textContent = frameName;
+ cell.appendChild(nameNode);
+ }
+
+ // Don't render detailed labels for meta category frames
+ if (!frameInfo.isMetaCategory) {
+ this._appendFunctionDetailsCells(doc, cell, frameInfo);
+ }
+
+ // Don't render an expando-arrow for leaf nodes.
+ let hasDescendants = Object.keys(this.frame.calls).length > 0;
+ if (!hasDescendants) {
+ arrowNode.setAttribute("invisible", "");
+ }
+
+ // Add a line break to the last description of the row in case it's selected
+ // and copied.
+ let lastDescription = cell.querySelector("description:last-of-type");
+ lastDescription.textContent = lastDescription.textContent + "\n";
+
+ // Add spaces as frameLevel indicators in case the row is selected and
+ // copied. These spaces won't be displayed in the cell content.
+ let firstDescription = cell.querySelector("description:first-of-type");
+ let levelIndicator = frameLevel > 0 ? " ".repeat(frameLevel) : "";
+ firstDescription.textContent = levelIndicator + firstDescription.textContent;
+
+ return cell;
+ },
+
+ _appendFunctionDetailsCells: function (doc, cell, frameInfo) {
+ if (frameInfo.fileName) {
+ let urlNode = doc.createElement("description");
+ urlNode.className = "plain call-tree-url";
+ urlNode.textContent = frameInfo.fileName;
+ urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
+ urlNode.addEventListener("mousedown", this._onUrlClick);
+ cell.appendChild(urlNode);
+ }
+
+ if (frameInfo.line) {
+ let lineNode = doc.createElement("description");
+ lineNode.className = "plain call-tree-line";
+ lineNode.textContent = ":" + frameInfo.line;
+ cell.appendChild(lineNode);
+ }
+
+ if (frameInfo.column) {
+ let columnNode = doc.createElement("description");
+ columnNode.className = "plain call-tree-column";
+ columnNode.textContent = ":" + frameInfo.column;
+ cell.appendChild(columnNode);
+ }
+
+ if (frameInfo.host) {
+ let hostNode = doc.createElement("description");
+ hostNode.className = "plain call-tree-host";
+ hostNode.textContent = frameInfo.host;
+ cell.appendChild(hostNode);
+ }
+
+ if (frameInfo.categoryData.label) {
+ let categoryNode = doc.createElement("description");
+ categoryNode.className = "plain call-tree-category";
+ categoryNode.style.color = frameInfo.categoryData.color;
+ categoryNode.textContent = frameInfo.categoryData.label;
+ cell.appendChild(categoryNode);
+ }
+ },
+
+ /**
+ * Gets the data displayed about this tree item, based on the FrameNode
+ * model associated with this view.
+ *
+ * @return object
+ */
+ getDisplayedData: function () {
+ if (this._cachedDisplayedData) {
+ return this._cachedDisplayedData;
+ }
+
+ this._cachedDisplayedData = this.frame.getInfo({
+ root: this.root.frame,
+ allocations: (this.visibleCells.count || this.visibleCells.selfCount)
+ });
+
+ return this._cachedDisplayedData;
+
+ /**
+ * When inverting call tree, the costs and times are dependent on position
+ * in the tree. We must only count leaf nodes with self cost, and total costs
+ * dependent on how many times the leaf node was found with a full stack path.
+ *
+ * Total | Self | Calls | Function
+ * ============================================================================
+ * 100% | 100% | 100 | ▼ C
+ * 50% | 0% | 50 | ▼ B
+ * 50% | 0% | 50 | ▼ A
+ * 50% | 0% | 50 | ▼ B
+ *
+ * Every instance of a `CallView` represents a row in the call tree. The same
+ * container node is used for all rows.
+ */
+ },
+
+ /**
+ * 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();
+ // Only emit for left click events
+ if (e.button === 0) {
+ this.root.emit("link", this);
+ }
+ },
+});
+
+exports.CallView = CallView;