diff options
Diffstat (limited to 'toolkit/devtools/shared/profiler/tree-view.js')
-rw-r--r-- | toolkit/devtools/shared/profiler/tree-view.js | 345 |
1 files changed, 345 insertions, 0 deletions
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); + } +}); |