/* * Copyright (C) 2016 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ WI.ProfileDataGridNode = class ProfileDataGridNode extends WI.DataGridNode { constructor(callingContextTreeNode, tree) { super(callingContextTreeNode, false); this._node = callingContextTreeNode; this._tree = tree; this._childrenToChargeToSelf = new Set; this._extraSelfTimeFromChargedChildren = 0; // FIXME: Make profile data grid nodes copyable. this.copyable = false; this.addEventListener("populate", this._populate, this); this._updateChildrenForModifiers(); this._recalculateData(); } // Public get callingContextTreeNode() { return this._node; } displayName() { let title = this._node.name; if (!title) return WI.UIString("(anonymous function)"); if (title === "(program)") return WI.UIString("(program)"); return title; } iconClassName() { let script = WI.debuggerManager.scriptForIdentifier(this._node.sourceID, WI.assumingMainTarget()); if (!script || !script.url) return "native-icon"; if (this._node.name === "(program)") return "program-icon"; return "function-icon"; } // Protected get data() { return this._data; } createCellContent(columnIdentifier, cell) { switch (columnIdentifier) { case "totalTime": return this._totalTimeContent(); case "selfTime": return Number.secondsToMillisecondsString(this._data.selfTime); case "function": return this._displayContent(); } return super.createCellContent(columnIdentifier, cell); } sort() { let children = this.children; children.sort(this._tree._sortComparator); for (let i = 0; i < children.length; ++i) { children[i]._recalculateSiblings(i); children[i].sort(); } } refresh() { this._updateChildrenForModifiers(); this._recalculateData(); super.refresh(); } appendContextMenuItems(contextMenu) { let disableFocus = this === this._tree.currentFocusNode; contextMenu.appendItem(WI.UIString("Focus on Subtree"), () => { this._tree.addFocusNode(this); }, disableFocus); // FIXME: Web Inspector: Charge to Caller should work with Bottom Up Profile View let disableChargeToCaller = this._tree.callingContextTree.type === WI.CallingContextTree.Type.BottomUp; contextMenu.appendItem(WI.UIString("Charge ā€˜%s’ to Callers").format(this.displayName()), () => { this._tree.addModifier({type: WI.ProfileDataGridTree.ModifierType.ChargeToCaller, source: this._node}); }, disableChargeToCaller); contextMenu.appendSeparator(); } // Protected filterableDataForColumn(columnIdentifier) { if (columnIdentifier === "function") { let filterableData = [this.displayName()]; let script = WI.debuggerManager.scriptForIdentifier(this._node.sourceID, WI.assumingMainTarget()); if (script && script.url && this._node.line >= 0 && this._node.column >= 0) filterableData.push(script.url); return filterableData; } return super.filterableDataForColumn(columnIdentifier); } // Private _updateChildrenForModifiers() { // NOTE: This currently assumes we either add modifiers or remove them all. // This doesn't handle removing a single modifier and re-inserting a single child. // FIXME: Web Inspector: Charge to Caller should work with Bottom Up Profile View let isBottomUp = this._tree.callingContextTree.type === WI.CallingContextTree.Type.BottomUp; if (!this._tree.hasModifiers() || isBottomUp) { // Add back child data grid nodes that were previously charged to us. if (!this.shouldRefreshChildren && this._childrenToChargeToSelf.size) { for (let child of this._childrenToChargeToSelf) { console.assert(child.hasStackTraceInTimeRange(this._tree.startTime, this._tree.endTime)); this.appendChild(new WI.ProfileDataGridNode(child, this._tree)); } this.sort(); } this._extraSelfTimeFromChargedChildren = 0; this._childrenToChargeToSelf.clear(); this.hasChildren = this._node.hasChildrenInTimeRange(this._tree.startTime, this._tree.endTime); return; } this._extraSelfTimeFromChargedChildren = 0; this._childrenToChargeToSelf.clear(); let hasNonChargedChild = false; this._node.forEachChild((child) => { if (child.hasStackTraceInTimeRange(this._tree.startTime, this._tree.endTime)) { for (let {type, source} of this._tree.modifiers) { if (type === WI.ProfileDataGridTree.ModifierType.ChargeToCaller) { if (child.equals(source)) { this._childrenToChargeToSelf.add(child); this._extraSelfTimeFromChargedChildren += child.filteredTimestampsAndDuration(this._tree.startTime, this._tree.endTime).duration; continue; } } hasNonChargedChild = true; } } }); this.hasChildren = hasNonChargedChild; // Remove child data grid nodes that have been charged to us. if (!this.shouldRefreshChildren && this._childrenToChargeToSelf.size) { for (let childDataGridNode of this.children) { if (this._childrenToChargeToSelf.has(childDataGridNode.callingContextTreeNode)) this.removeChild(childDataGridNode); } } } _recalculateData() { let {timestamps, duration} = this._node.filteredTimestampsAndDuration(this._tree.startTime, this._tree.endTime); let {leafTimestamps, leafDuration} = this._node.filteredLeafTimestampsAndDuration(this._tree.startTime, this._tree.endTime); let totalTime = duration; let selfTime = leafDuration + this._extraSelfTimeFromChargedChildren; let fraction = totalTime / this._tree.totalSampleTime; this._data = {totalTime, selfTime, fraction}; } _totalTimeContent() { let {totalTime, fraction} = this._data; let fragment = document.createDocumentFragment(); let timeElement = fragment.appendChild(document.createElement("span")); timeElement.classList.add("time"); timeElement.textContent = Number.secondsToMillisecondsString(totalTime); let percentElement = fragment.appendChild(document.createElement("span")); percentElement.classList.add("percentage"); percentElement.textContent = Number.percentageString(fraction); return fragment; } _displayContent() { let title = this.displayName(); let iconClassName = this.iconClassName(); let fragment = document.createDocumentFragment(); let iconElement = fragment.appendChild(document.createElement("img")); iconElement.classList.add("icon", iconClassName); let titleElement = fragment.appendChild(document.createElement("span")); titleElement.textContent = title; let script = WI.debuggerManager.scriptForIdentifier(this._node.sourceID, WI.assumingMainTarget()); if (script && script.url && this._node.line >= 0 && this._node.column >= 0) { // Convert from 1-based line and column to 0-based. let sourceCodeLocation = script.createSourceCodeLocation(this._node.line - 1, this._node.column - 1); let locationElement = fragment.appendChild(document.createElement("span")); locationElement.classList.add("location"); sourceCodeLocation.populateLiveDisplayLocationString(locationElement, "textContent", WI.SourceCodeLocation.ColumnStyle.Hidden, WI.SourceCodeLocation.NameStyle.Short); const options = { dontFloat: true, useGoToArrowButton: true, ignoreNetworkTab: true, ignoreSearchTab: true, }; fragment.appendChild(WI.createSourceCodeLocationLink(sourceCodeLocation, options)); } return fragment; } _populate() { if (!this.shouldRefreshChildren) return; this.removeEventListener("populate", this._populate, this); this._node.forEachChild((child) => { if (!this._childrenToChargeToSelf.has(child)) { if (child.hasStackTraceInTimeRange(this._tree.startTime, this._tree.endTime)) this.appendChild(new WI.ProfileDataGridNode(child, this._tree)); } }); this.sort(); } };