| /* |
| * 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.ProfileView = class ProfileView extends WI.ContentView |
| { |
| constructor(callingContextTree, extraArguments) |
| { |
| super(callingContextTree); |
| |
| console.assert(callingContextTree instanceof WI.CallingContextTree); |
| |
| this._startTime = 0; |
| this._endTime = Infinity; |
| this._callingContextTree = callingContextTree; |
| |
| this._hoveredDataGridNode = null; |
| |
| this.element.classList.add("profile"); |
| |
| let columns = { |
| totalTime: { |
| title: WI.UIString("Total Time"), |
| width: "120px", |
| sortable: true, |
| aligned: "right", |
| }, |
| selfTime: { |
| title: WI.UIString("Self Time"), |
| width: "75px", |
| sortable: true, |
| aligned: "right", |
| }, |
| function: { |
| title: WI.UIString("Function"), |
| disclosure: true, |
| }, |
| }; |
| |
| this._dataGrid = new WI.DataGrid(columns); |
| this._dataGrid.addEventListener(WI.DataGrid.Event.SortChanged, this._dataGridSortChanged, this); |
| this._dataGrid.addEventListener(WI.DataGrid.Event.SelectedNodeChanged, this._dataGridNodeSelected, this); |
| this._dataGrid.addEventListener(WI.DataGrid.Event.ExpandedNode, this._dataGridNodeExpanded, this); |
| this._dataGrid.element.addEventListener("mouseover", this._mouseOverDataGrid.bind(this)); |
| this._dataGrid.element.addEventListener("mouseleave", this._mouseLeaveDataGrid.bind(this)); |
| this._dataGrid.indentWidth = 20; |
| this._dataGrid.sortColumnIdentifier = "totalTime"; |
| this._dataGrid.sortOrder = WI.DataGrid.SortOrder.Descending; |
| this._dataGrid.createSettings("profile-view"); |
| |
| // Currently we create a new ProfileView for each CallingContextTree, so |
| // to share state between them, use a common shared data object. |
| this._sharedData = extraArguments; |
| |
| this.addSubview(this._dataGrid); |
| } |
| |
| // Public |
| |
| get callingContextTree() { return this._callingContextTree; } |
| get startTime() { return this._startTime; } |
| get endTime() { return this._endTime; } |
| get dataGrid() { return this._dataGrid; } |
| |
| setStartAndEndTime(startTime, endTime) |
| { |
| console.assert(startTime >= 0); |
| console.assert(endTime >= 0); |
| console.assert(startTime <= endTime); |
| |
| this._startTime = startTime; |
| this._endTime = endTime; |
| |
| // FIXME: It would be ideal to update the existing tree, maintaining nodes that were expanded. |
| // For now just recreate the tree for the new time range. |
| |
| this._recreate(); |
| } |
| |
| hasFocusNodes() |
| { |
| if (!this._profileDataGridTree) |
| return false; |
| return this._profileDataGridTree.focusNodes.length > 0; |
| } |
| |
| clearFocusNodes() |
| { |
| if (!this._profileDataGridTree) |
| return; |
| this._profileDataGridTree.clearFocusNodes(); |
| } |
| |
| get scrollableElements() |
| { |
| return [this._dataGrid.scrollContainer]; |
| } |
| |
| // Protected |
| |
| get selectionPathComponents() |
| { |
| let pathComponents = []; |
| |
| if (this._profileDataGridTree) { |
| for (let profileDataGridNode of this._profileDataGridTree.focusNodes) { |
| let displayName = profileDataGridNode.displayName(); |
| let className = profileDataGridNode.iconClassName(); |
| let pathComponent = new WI.HierarchicalPathComponent(displayName, className, profileDataGridNode); |
| pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this); |
| pathComponents.push(pathComponent); |
| } |
| } |
| |
| return pathComponents; |
| } |
| |
| // Private |
| |
| _recreate() |
| { |
| let hadFocusNodes = this.hasFocusNodes(); |
| |
| let sortComparator = WI.ProfileDataGridTree.buildSortComparator(this._dataGrid.sortColumnIdentifier, this._dataGrid.sortOrder); |
| this._profileDataGridTree = new WI.ProfileDataGridTree(this._callingContextTree, this._startTime, this._endTime, sortComparator); |
| this._profileDataGridTree.addEventListener(WI.ProfileDataGridTree.Event.FocusChanged, this._dataGridTreeFocusChanged, this); |
| this._profileDataGridTree.addEventListener(WI.ProfileDataGridTree.Event.ModifiersChanged, this._dataGridTreeModifiersChanged, this); |
| this._repopulateDataGridFromTree(); |
| |
| if (hadFocusNodes) |
| this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange); |
| } |
| |
| _repopulateDataGridFromTree() |
| { |
| this._dataGrid.removeChildren(); |
| |
| for (let child of this._profileDataGridTree.children) |
| this._dataGrid.appendChild(child); |
| |
| this._restoreSharedState(); |
| } |
| |
| _restoreSharedState() |
| { |
| const skipHidden = false; |
| const stayWithin = this._dataGrid; |
| const dontPopulate = true; |
| |
| if (this._sharedData.selectedNodeHash) { |
| let nodeToSelect = this._dataGrid.findNode((node) => node.callingContextTreeNode.hash === this._sharedData.selectedNodeHash, skipHidden, stayWithin, dontPopulate); |
| if (nodeToSelect) |
| nodeToSelect.revealAndSelect(); |
| } |
| } |
| |
| _pathComponentClicked(event) |
| { |
| if (!event.data.pathComponent) |
| return; |
| |
| let profileDataGridNode = event.data.pathComponent.representedObject; |
| if (profileDataGridNode === this._profileDataGridTree.currentFocusNode) |
| return; |
| |
| this._profileDataGridTree.rollbackFocusNode(event.data.pathComponent.representedObject); |
| } |
| |
| _dataGridTreeFocusChanged(event) |
| { |
| this._repopulateDataGridFromTree(); |
| this._profileDataGridTree.refresh(); |
| |
| this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange); |
| } |
| |
| _dataGridTreeModifiersChanged(event) |
| { |
| this._profileDataGridTree.refresh(); |
| } |
| |
| _dataGridSortChanged() |
| { |
| if (!this._profileDataGridTree) |
| return; |
| |
| this._profileDataGridTree.sortComparator = WI.ProfileDataGridTree.buildSortComparator(this._dataGrid.sortColumnIdentifier, this._dataGrid.sortOrder); |
| this._repopulateDataGridFromTree(); |
| } |
| |
| _dataGridNodeSelected(event) |
| { |
| let oldSelectedNode = event.data.oldSelectedNode; |
| if (oldSelectedNode) { |
| this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, oldSelectedNode); |
| oldSelectedNode.forEachChildInSubtree((node) => this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, node)); |
| } |
| |
| let newSelectedNode = this._dataGrid.selectedNode; |
| if (newSelectedNode) { |
| this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, newSelectedNode); |
| newSelectedNode.forEachChildInSubtree((node) => this._appendGuidanceElement(WI.ProfileView.GuidanceType.Selected, node, newSelectedNode)); |
| |
| this._sharedData.selectedNodeHash = newSelectedNode.callingContextTreeNode.hash; |
| } |
| } |
| |
| _dataGridNodeExpanded(event) |
| { |
| let expandedNode = event.data.dataGridNode; |
| |
| if (this._dataGrid.selectedNode) { |
| if (expandedNode.isInSubtreeOfNode(this._dataGrid.selectedNode)) |
| expandedNode.forEachImmediateChild((node) => this._appendGuidanceElement(WI.ProfileView.GuidanceType.Selected, node, this._dataGrid.selectedNode)); |
| } |
| |
| if (this._hoveredDataGridNode) { |
| if (expandedNode.isInSubtreeOfNode(this._hoveredDataGridNode)) |
| expandedNode.forEachImmediateChild((node) => this._appendGuidanceElement(WI.ProfileView.GuidanceType.Hover, node, this._hoveredDataGridNode)); |
| } |
| } |
| |
| _mouseOverDataGrid(event) |
| { |
| let hoveredDataGridNode = this._dataGrid.dataGridNodeFromNode(event.target); |
| if (hoveredDataGridNode === this._hoveredDataGridNode) |
| return; |
| |
| if (this._hoveredDataGridNode) { |
| this._removeGuidanceElement(WI.ProfileView.GuidanceType.Hover, this._hoveredDataGridNode); |
| this._hoveredDataGridNode.forEachChildInSubtree((node) => this._removeGuidanceElement(WI.ProfileView.GuidanceType.Hover, node)); |
| } |
| |
| this._hoveredDataGridNode = hoveredDataGridNode; |
| |
| if (this._hoveredDataGridNode) { |
| this._appendGuidanceElement(WI.ProfileView.GuidanceType.Hover, this._hoveredDataGridNode, this._hoveredDataGridNode); |
| this._hoveredDataGridNode.forEachChildInSubtree((node) => this._appendGuidanceElement(WI.ProfileView.GuidanceType.Hover, node, this._hoveredDataGridNode)); |
| } |
| } |
| |
| _mouseLeaveDataGrid(event) |
| { |
| if (!this._hoveredDataGridNode) |
| return; |
| |
| this._removeGuidanceElement(WI.ProfileView.GuidanceType.Hover, this._hoveredDataGridNode); |
| this._hoveredDataGridNode.forEachChildInSubtree((node) => this._removeGuidanceElement(WI.ProfileView.GuidanceType.Hover, node)); |
| |
| this._hoveredDataGridNode = null; |
| } |
| |
| _guidanceElementKey(type) |
| { |
| return "guidance-element-" + type; |
| } |
| |
| _removeGuidanceElement(type, node) |
| { |
| let key = this._guidanceElementKey(type); |
| let element = node.elementWithColumnIdentifier("function"); |
| if (!element || !element[key]) |
| return; |
| |
| element[key].remove(); |
| element[key] = null; |
| } |
| |
| _appendGuidanceElement(type, node, baseElement) |
| { |
| let depth = baseElement.depth; |
| let guidanceMarkerLeft = depth ? (depth * this._dataGrid.indentWidth) + 1.5 : 7.5; |
| |
| let key = this._guidanceElementKey(type); |
| let element = node.elementWithColumnIdentifier("function"); |
| let guidanceElement = element[key] || element.appendChild(document.createElement("div")); |
| element[key] = guidanceElement; |
| guidanceElement.classList.add("guidance", type); |
| guidanceElement.classList.toggle("base", node === baseElement); |
| guidanceElement.style.left = guidanceMarkerLeft + "px"; |
| } |
| }; |
| |
| WI.ProfileView.GuidanceType = { |
| Selected: "selected", |
| Hover: "hover", |
| }; |