| /* |
| * Copyright (C) 2009 280 North 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. ``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 |
| * 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. |
| */ |
| |
| /** |
| * @unrestricted |
| */ |
| Profiler.ProfileDataGridNode = class extends DataGrid.DataGridNode { |
| /** |
| * @param {!SDK.ProfileNode} profileNode |
| * @param {!Profiler.ProfileDataGridTree} owningTree |
| * @param {boolean} hasChildren |
| */ |
| constructor(profileNode, owningTree, hasChildren) { |
| super(null, hasChildren); |
| |
| this._searchMatchedSelfColumn = false; |
| this._searchMatchedTotalColumn = false; |
| this._searchMatchedFunctionColumn = false; |
| |
| this.profileNode = profileNode; |
| this.tree = owningTree; |
| /** @type {!Map<string, !Profiler.ProfileDataGridNode>} */ |
| this.childrenByCallUID = new Map(); |
| this.lastComparator = null; |
| |
| this.callUID = profileNode.callUID; |
| this.self = profileNode.self; |
| this.total = profileNode.total; |
| this.functionName = UI.beautifyFunctionName(profileNode.functionName); |
| this._deoptReason = profileNode.deoptReason || ''; |
| this.url = profileNode.url; |
| /** @type {?Element} */ |
| this.linkElement = null; |
| } |
| |
| /** |
| * @param {!Array<!Array<!Profiler.ProfileDataGridNode>>} gridNodeGroups |
| * @param {function(!T, !T)} comparator |
| * @param {boolean} force |
| * @template T |
| */ |
| static sort(gridNodeGroups, comparator, force) { |
| for (let gridNodeGroupIndex = 0; gridNodeGroupIndex < gridNodeGroups.length; ++gridNodeGroupIndex) { |
| const gridNodes = gridNodeGroups[gridNodeGroupIndex]; |
| const count = gridNodes.length; |
| |
| for (let index = 0; index < count; ++index) { |
| const gridNode = gridNodes[index]; |
| |
| // If the grid node is collapsed, then don't sort children (save operation for later). |
| // If the grid node has the same sorting as previously, then there is no point in sorting it again. |
| if (!force && (!gridNode.expanded || gridNode.lastComparator === comparator)) { |
| if (gridNode.children.length) { |
| gridNode.shouldRefreshChildren = true; |
| } |
| continue; |
| } |
| |
| gridNode.lastComparator = comparator; |
| |
| const children = gridNode.children; |
| const childCount = children.length; |
| |
| if (childCount) { |
| children.sort(comparator); |
| |
| for (let childIndex = 0; childIndex < childCount; ++childIndex) { |
| children[childIndex].recalculateSiblings(childIndex); |
| } |
| |
| gridNodeGroups.push(children); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @param {!Profiler.ProfileDataGridNode|!Profiler.ProfileDataGridTree} container |
| * @param {!Profiler.ProfileDataGridNode} child |
| * @param {boolean} shouldAbsorb |
| */ |
| static merge(container, child, shouldAbsorb) { |
| container.self += child.self; |
| |
| if (!shouldAbsorb) { |
| container.total += child.total; |
| } |
| |
| let children = container.children.slice(); |
| |
| container.removeChildren(); |
| |
| let count = children.length; |
| |
| for (let index = 0; index < count; ++index) { |
| if (!shouldAbsorb || children[index] !== child) { |
| container.appendChild(children[index]); |
| } |
| } |
| |
| children = child.children.slice(); |
| count = children.length; |
| |
| for (let index = 0; index < count; ++index) { |
| const orphanedChild = children[index]; |
| const existingChild = container.childrenByCallUID.get(orphanedChild.callUID); |
| |
| if (existingChild) { |
| existingChild.merge(/** @type{!Profiler.ProfileDataGridNode} */ (orphanedChild), false); |
| } else { |
| container.appendChild(orphanedChild); |
| } |
| } |
| } |
| |
| /** |
| * @param {!Profiler.ProfileDataGridNode|!Profiler.ProfileDataGridTree} container |
| */ |
| static populate(container) { |
| if (container._populated) { |
| return; |
| } |
| container._populated = true; |
| |
| container.populateChildren(); |
| |
| const currentComparator = container.tree.lastComparator; |
| |
| if (currentComparator) { |
| container.sort(currentComparator, true); |
| } |
| } |
| |
| /** |
| * @override |
| * @param {string} columnId |
| * @return {!Element} |
| */ |
| createCell(columnId) { |
| let cell; |
| switch (columnId) { |
| case 'self': |
| cell = this._createValueCell(this.self, this.selfPercent); |
| cell.classList.toggle('highlight', this._searchMatchedSelfColumn); |
| break; |
| |
| case 'total': |
| cell = this._createValueCell(this.total, this.totalPercent); |
| cell.classList.toggle('highlight', this._searchMatchedTotalColumn); |
| break; |
| |
| case 'function': |
| cell = this.createTD(columnId); |
| cell.classList.toggle('highlight', this._searchMatchedFunctionColumn); |
| if (this._deoptReason) { |
| cell.classList.add('not-optimized'); |
| const warningIcon = UI.Icon.create('smallicon-warning', 'profile-warn-marker'); |
| warningIcon.title = Common.UIString('Not optimized: %s', this._deoptReason); |
| cell.appendChild(warningIcon); |
| } |
| cell.createTextChild(this.functionName); |
| if (this.profileNode.scriptId === '0') { |
| break; |
| } |
| const urlElement = this.tree._formatter.linkifyNode(this); |
| if (!urlElement) { |
| break; |
| } |
| urlElement.style.maxWidth = '75%'; |
| cell.appendChild(urlElement); |
| this.linkElement = urlElement; |
| break; |
| |
| default: |
| cell = super.createCell(columnId); |
| break; |
| } |
| return cell; |
| } |
| |
| /** |
| * @param {number} value |
| * @param {number} percent |
| * @return {!Element} |
| */ |
| _createValueCell(value, percent) { |
| const cell = createElementWithClass('td', 'numeric-column'); |
| const div = cell.createChild('div', 'profile-multiple-values'); |
| const valueSpan = div.createChild('span'); |
| const valueText = this.tree._formatter.formatValue(value, this); |
| valueSpan.textContent = valueText; |
| const percentSpan = div.createChild('span', 'percent-column'); |
| const percentText = this.tree._formatter.formatPercent(percent, this); |
| percentSpan.textContent = percentText; |
| UI.ARIAUtils.markAsHidden(valueSpan); |
| UI.ARIAUtils.markAsHidden(percentSpan); |
| const valueAccessibleText = this.tree._formatter.formatValueAccessibleText(value, this); |
| UI.ARIAUtils.setAccessibleName(div, ls`${valueAccessibleText}, ${percentText}`); |
| return cell; |
| } |
| |
| /** |
| * @param {function(!T, !T)} comparator |
| * @param {boolean} force |
| * @template T |
| */ |
| sort(comparator, force) { |
| return Profiler.ProfileDataGridNode.sort([[this]], comparator, force); |
| } |
| |
| /** |
| * @override |
| * @param {!DataGrid.DataGridNode} profileDataGridNode |
| * @param {number} index |
| */ |
| insertChild(profileDataGridNode, index) { |
| super.insertChild(profileDataGridNode, index); |
| |
| this.childrenByCallUID.set( |
| profileDataGridNode.callUID, /** @type {!Profiler.ProfileDataGridNode} */ (profileDataGridNode)); |
| } |
| |
| /** |
| * @override |
| * @param {!DataGrid.DataGridNode} profileDataGridNode |
| */ |
| removeChild(profileDataGridNode) { |
| super.removeChild(profileDataGridNode); |
| |
| this.childrenByCallUID.delete((/** @type {!Profiler.ProfileDataGridNode} */ (profileDataGridNode)).callUID); |
| } |
| |
| /** |
| * @override |
| */ |
| removeChildren() { |
| super.removeChildren(); |
| |
| this.childrenByCallUID.clear(); |
| } |
| |
| /** |
| * @param {!Profiler.ProfileDataGridNode} node |
| * @return {?Profiler.ProfileDataGridNode} |
| */ |
| findChild(node) { |
| if (!node) { |
| return null; |
| } |
| return this.childrenByCallUID.get(node.callUID); |
| } |
| |
| get selfPercent() { |
| return this.self / this.tree.total * 100.0; |
| } |
| |
| get totalPercent() { |
| return this.total / this.tree.total * 100.0; |
| } |
| |
| /** |
| * @override |
| */ |
| populate() { |
| Profiler.ProfileDataGridNode.populate(this); |
| } |
| |
| /** |
| * @protected |
| */ |
| populateChildren() { |
| } |
| |
| // When focusing and collapsing we modify lots of nodes in the tree. |
| // This allows us to restore them all to their original state when we revert. |
| |
| save() { |
| if (this._savedChildren) { |
| return; |
| } |
| |
| this._savedSelf = this.self; |
| this._savedTotal = this.total; |
| |
| this._savedChildren = this.children.slice(); |
| } |
| |
| /** |
| * When focusing and collapsing we modify lots of nodes in the tree. |
| * This allows us to restore them all to their original state when we revert. |
| * @protected |
| */ |
| restore() { |
| if (!this._savedChildren) { |
| return; |
| } |
| |
| this.self = this._savedSelf; |
| this.total = this._savedTotal; |
| |
| this.removeChildren(); |
| |
| const children = this._savedChildren; |
| const count = children.length; |
| |
| for (let index = 0; index < count; ++index) { |
| children[index].restore(); |
| this.appendChild(children[index]); |
| } |
| } |
| |
| /** |
| * @param {!Profiler.ProfileDataGridNode} child |
| * @param {boolean} shouldAbsorb |
| */ |
| merge(child, shouldAbsorb) { |
| Profiler.ProfileDataGridNode.merge(this, child, shouldAbsorb); |
| } |
| }; |
| |
| |
| /** |
| * @implements {UI.Searchable} |
| * @unrestricted |
| */ |
| Profiler.ProfileDataGridTree = class { |
| /** |
| * @param {!Profiler.ProfileDataGridNode.Formatter} formatter |
| * @param {!UI.SearchableView} searchableView |
| * @param {number} total |
| */ |
| constructor(formatter, searchableView, total) { |
| this.tree = this; |
| this.children = []; |
| this._formatter = formatter; |
| this._searchableView = searchableView; |
| this.total = total; |
| this.lastComparator = null; |
| this.childrenByCallUID = new Map(); |
| this.deepSearch = true; |
| } |
| |
| /** |
| * @param {string} property |
| * @param {boolean} isAscending |
| * @return {function(!Object.<string, *>, !Object.<string, *>)} |
| */ |
| static propertyComparator(property, isAscending) { |
| let comparator = Profiler.ProfileDataGridTree.propertyComparators[(isAscending ? 1 : 0)][property]; |
| |
| if (!comparator) { |
| if (isAscending) { |
| comparator = function(lhs, rhs) { |
| if (lhs[property] < rhs[property]) { |
| return -1; |
| } |
| |
| if (lhs[property] > rhs[property]) { |
| return 1; |
| } |
| |
| return 0; |
| }; |
| } else { |
| comparator = function(lhs, rhs) { |
| if (lhs[property] > rhs[property]) { |
| return -1; |
| } |
| |
| if (lhs[property] < rhs[property]) { |
| return 1; |
| } |
| |
| return 0; |
| }; |
| } |
| |
| Profiler.ProfileDataGridTree.propertyComparators[(isAscending ? 1 : 0)][property] = comparator; |
| } |
| |
| return comparator; |
| } |
| |
| get expanded() { |
| return true; |
| } |
| |
| appendChild(child) { |
| this.insertChild(child, this.children.length); |
| } |
| |
| insertChild(child, index) { |
| this.children.splice(index, 0, child); |
| this.childrenByCallUID.set(child.callUID, child); |
| } |
| |
| removeChildren() { |
| this.children = []; |
| this.childrenByCallUID.clear(); |
| } |
| |
| populateChildren() { |
| } |
| |
| /** |
| * @param {!Profiler.ProfileDataGridNode} node |
| * @return {?Profiler.ProfileDataGridNode} |
| */ |
| findChild(node) { |
| if (!node) { |
| return null; |
| } |
| return this.childrenByCallUID.get(node.callUID); |
| } |
| |
| /** |
| * @param {function(!T, !T)} comparator |
| * @param {boolean} force |
| * @template T |
| */ |
| sort(comparator, force) { |
| return Profiler.ProfileDataGridNode.sort([[this]], comparator, force); |
| } |
| |
| /** |
| * @protected |
| */ |
| save() { |
| if (this._savedChildren) { |
| return; |
| } |
| |
| this._savedTotal = this.total; |
| this._savedChildren = this.children.slice(); |
| } |
| |
| restore() { |
| if (!this._savedChildren) { |
| return; |
| } |
| |
| this.children = this._savedChildren; |
| this.total = this._savedTotal; |
| |
| const children = this.children; |
| const count = children.length; |
| |
| for (let index = 0; index < count; ++index) { |
| children[index].restore(); |
| } |
| |
| this._savedChildren = null; |
| } |
| |
| /** |
| * @param {!UI.SearchableView.SearchConfig} searchConfig |
| * @return {?function(!Profiler.ProfileDataGridNode):boolean} |
| */ |
| _matchFunction(searchConfig) { |
| const query = searchConfig.query.trim(); |
| if (!query.length) { |
| return null; |
| } |
| |
| const greaterThan = (query.startsWith('>')); |
| const lessThan = (query.startsWith('<')); |
| let equalTo = (query.startsWith('=') || ((greaterThan || lessThan) && query.indexOf('=') === 1)); |
| const percentUnits = (query.endsWith('%')); |
| const millisecondsUnits = (query.length > 2 && query.endsWith('ms')); |
| const secondsUnits = (!millisecondsUnits && query.endsWith('s')); |
| |
| let queryNumber = parseFloat(query); |
| if (greaterThan || lessThan || equalTo) { |
| if (equalTo && (greaterThan || lessThan)) { |
| queryNumber = parseFloat(query.substring(2)); |
| } else { |
| queryNumber = parseFloat(query.substring(1)); |
| } |
| } |
| |
| const queryNumberMilliseconds = (secondsUnits ? (queryNumber * 1000) : queryNumber); |
| |
| // Make equalTo implicitly true if it wasn't specified there is no other operator. |
| if (!isNaN(queryNumber) && !(greaterThan || lessThan)) { |
| equalTo = true; |
| } |
| |
| const matcher = createPlainTextSearchRegex(query, 'i'); |
| |
| /** |
| * @param {!Profiler.ProfileDataGridNode} profileDataGridNode |
| * @return {boolean} |
| */ |
| function matchesQuery(profileDataGridNode) { |
| profileDataGridNode._searchMatchedSelfColumn = false; |
| profileDataGridNode._searchMatchedTotalColumn = false; |
| profileDataGridNode._searchMatchedFunctionColumn = false; |
| |
| if (percentUnits) { |
| if (lessThan) { |
| if (profileDataGridNode.selfPercent < queryNumber) { |
| profileDataGridNode._searchMatchedSelfColumn = true; |
| } |
| if (profileDataGridNode.totalPercent < queryNumber) { |
| profileDataGridNode._searchMatchedTotalColumn = true; |
| } |
| } else if (greaterThan) { |
| if (profileDataGridNode.selfPercent > queryNumber) { |
| profileDataGridNode._searchMatchedSelfColumn = true; |
| } |
| if (profileDataGridNode.totalPercent > queryNumber) { |
| profileDataGridNode._searchMatchedTotalColumn = true; |
| } |
| } |
| |
| if (equalTo) { |
| if (profileDataGridNode.selfPercent === queryNumber) { |
| profileDataGridNode._searchMatchedSelfColumn = true; |
| } |
| if (profileDataGridNode.totalPercent === queryNumber) { |
| profileDataGridNode._searchMatchedTotalColumn = true; |
| } |
| } |
| } else if (millisecondsUnits || secondsUnits) { |
| if (lessThan) { |
| if (profileDataGridNode.self < queryNumberMilliseconds) { |
| profileDataGridNode._searchMatchedSelfColumn = true; |
| } |
| if (profileDataGridNode.total < queryNumberMilliseconds) { |
| profileDataGridNode._searchMatchedTotalColumn = true; |
| } |
| } else if (greaterThan) { |
| if (profileDataGridNode.self > queryNumberMilliseconds) { |
| profileDataGridNode._searchMatchedSelfColumn = true; |
| } |
| if (profileDataGridNode.total > queryNumberMilliseconds) { |
| profileDataGridNode._searchMatchedTotalColumn = true; |
| } |
| } |
| |
| if (equalTo) { |
| if (profileDataGridNode.self === queryNumberMilliseconds) { |
| profileDataGridNode._searchMatchedSelfColumn = true; |
| } |
| if (profileDataGridNode.total === queryNumberMilliseconds) { |
| profileDataGridNode._searchMatchedTotalColumn = true; |
| } |
| } |
| } |
| |
| if (profileDataGridNode.functionName.match(matcher) || |
| (profileDataGridNode.url && profileDataGridNode.url.match(matcher))) { |
| profileDataGridNode._searchMatchedFunctionColumn = true; |
| } |
| |
| if (profileDataGridNode._searchMatchedSelfColumn || profileDataGridNode._searchMatchedTotalColumn || |
| profileDataGridNode._searchMatchedFunctionColumn) { |
| profileDataGridNode.refresh(); |
| return true; |
| } |
| |
| return false; |
| } |
| return matchesQuery; |
| } |
| |
| /** |
| * @override |
| * @param {!UI.SearchableView.SearchConfig} searchConfig |
| * @param {boolean} shouldJump |
| * @param {boolean=} jumpBackwards |
| */ |
| performSearch(searchConfig, shouldJump, jumpBackwards) { |
| this.searchCanceled(); |
| const matchesQuery = this._matchFunction(searchConfig); |
| if (!matchesQuery) { |
| return; |
| } |
| |
| this._searchResults = []; |
| const deepSearch = this.deepSearch; |
| for (let current = this.children[0]; current; current = current.traverseNextNode(!deepSearch, null, !deepSearch)) { |
| if (matchesQuery(current)) { |
| this._searchResults.push({profileNode: current}); |
| } |
| } |
| this._searchResultIndex = jumpBackwards ? 0 : this._searchResults.length - 1; |
| this._searchableView.updateSearchMatchesCount(this._searchResults.length); |
| this._searchableView.updateCurrentMatchIndex(this._searchResultIndex); |
| } |
| |
| /** |
| * @override |
| */ |
| searchCanceled() { |
| if (this._searchResults) { |
| for (let i = 0; i < this._searchResults.length; ++i) { |
| const profileNode = this._searchResults[i].profileNode; |
| profileNode._searchMatchedSelfColumn = false; |
| profileNode._searchMatchedTotalColumn = false; |
| profileNode._searchMatchedFunctionColumn = false; |
| profileNode.refresh(); |
| } |
| } |
| |
| this._searchResults = []; |
| this._searchResultIndex = -1; |
| } |
| |
| /** |
| * @override |
| */ |
| jumpToNextSearchResult() { |
| if (!this._searchResults || !this._searchResults.length) { |
| return; |
| } |
| this._searchResultIndex = (this._searchResultIndex + 1) % this._searchResults.length; |
| this._jumpToSearchResult(this._searchResultIndex); |
| } |
| |
| /** |
| * @override |
| */ |
| jumpToPreviousSearchResult() { |
| if (!this._searchResults || !this._searchResults.length) { |
| return; |
| } |
| this._searchResultIndex = (this._searchResultIndex - 1 + this._searchResults.length) % this._searchResults.length; |
| this._jumpToSearchResult(this._searchResultIndex); |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| supportsCaseSensitiveSearch() { |
| return true; |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| supportsRegexSearch() { |
| return false; |
| } |
| |
| /** |
| * @param {number} index |
| */ |
| _jumpToSearchResult(index) { |
| const searchResult = this._searchResults[index]; |
| if (!searchResult) { |
| return; |
| } |
| const profileNode = searchResult.profileNode; |
| profileNode.revealAndSelect(); |
| this._searchableView.updateCurrentMatchIndex(index); |
| } |
| }; |
| |
| Profiler.ProfileDataGridTree.propertyComparators = [{}, {}]; |
| |
| |
| /** |
| * @interface |
| */ |
| Profiler.ProfileDataGridNode.Formatter = function() {}; |
| |
| Profiler.ProfileDataGridNode.Formatter.prototype = { |
| /** |
| * @param {number} value |
| * @param {!Profiler.ProfileDataGridNode} node |
| * @return {string} |
| */ |
| formatValue(value, node) {}, |
| |
| /** |
| * @param {number} value |
| * @return {string} |
| */ |
| formatValueAccessibleText(value) {}, |
| |
| /** |
| * @param {number} value |
| * @param {!Profiler.ProfileDataGridNode} node |
| * @return {string} |
| */ |
| formatPercent(value, node) {}, |
| |
| /** |
| * @param {!Profiler.ProfileDataGridNode} node |
| * @return {?Element} |
| */ |
| linkifyNode(node) {} |
| }; |