blob: e29449e1b1f9ba28aa1115c62d60360796b61cc7 [file] [log] [blame]
/*
* 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: <https://webkit.org/b/155072> 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 \u201C%s\u201D 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: <https://webkit.org/b/155072> 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();
}
};