blob: c69445bb03f4d82d667814341180ef980b45cc56 [file] [log] [blame]
/*
* 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) {}
};