diff options
Diffstat (limited to 'toolkit/devtools/shared/profiler')
-rw-r--r-- | toolkit/devtools/shared/profiler/global.js | 108 | ||||
-rw-r--r-- | toolkit/devtools/shared/profiler/tree-model.js | 281 | ||||
-rw-r--r-- | toolkit/devtools/shared/profiler/tree-view.js | 345 |
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); + } +}); |