diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/performance/modules/widgets/tree-view.js | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/performance/modules/widgets/tree-view.js')
-rw-r--r-- | devtools/client/performance/modules/widgets/tree-view.js | 406 |
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; |