blob: faab86b8f33439c5efbdf67851bc114b4d264a12 [file] [log] [blame]
/*
* Copyright (C) 2008 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. ``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.
*/
/**
* @constructor
* @implements {WebInspector.Searchable}
* @extends {WebInspector.VBox}
* @param {!WebInspector.CPUProfileHeader} profileHeader
*/
WebInspector.CPUProfileView = function(profileHeader)
{
WebInspector.VBox.call(this);
this.element.classList.add("cpu-profile-view");
this._searchableView = new WebInspector.SearchableView(this);
this._searchableView.setPlaceholder(WebInspector.UIString("Find by cost (>50ms), name or file"));
this._searchableView.show(this.element);
this._viewType = WebInspector.settings.createSetting("cpuProfilerView", WebInspector.CPUProfileView._TypeHeavy);
var columns = [];
columns.push({id: "self", title: WebInspector.UIString("Self"), width: "120px", sort: WebInspector.DataGrid.Order.Descending, sortable: true});
columns.push({id: "total", title: WebInspector.UIString("Total"), width: "120px", sortable: true});
columns.push({id: "function", title: WebInspector.UIString("Function"), disclosure: true, sortable: true});
this.dataGrid = new WebInspector.DataGrid(columns);
this.dataGrid.addEventListener(WebInspector.DataGrid.Events.SortingChanged, this._sortProfile, this);
this.viewSelectComboBox = new WebInspector.ToolbarComboBox(this._changeView.bind(this));
var options = {};
options[WebInspector.CPUProfileView._TypeFlame] = this.viewSelectComboBox.createOption(WebInspector.UIString("Chart"), "", WebInspector.CPUProfileView._TypeFlame);
options[WebInspector.CPUProfileView._TypeHeavy] = this.viewSelectComboBox.createOption(WebInspector.UIString("Heavy (Bottom Up)"), "", WebInspector.CPUProfileView._TypeHeavy);
options[WebInspector.CPUProfileView._TypeTree] = this.viewSelectComboBox.createOption(WebInspector.UIString("Tree (Top Down)"), "", WebInspector.CPUProfileView._TypeTree);
var optionName = this._viewType.get() || WebInspector.CPUProfileView._TypeFlame;
var option = options[optionName] || options[WebInspector.CPUProfileView._TypeFlame];
this.viewSelectComboBox.select(option);
this.focusButton = new WebInspector.ToolbarButton(WebInspector.UIString("Focus selected function"), "visibility-toolbar-item");
this.focusButton.setEnabled(false);
this.focusButton.addEventListener("click", this._focusClicked, this);
this.excludeButton = new WebInspector.ToolbarButton(WebInspector.UIString("Exclude selected function"), "delete-toolbar-item");
this.excludeButton.setEnabled(false);
this.excludeButton.addEventListener("click", this._excludeClicked, this);
this.resetButton = new WebInspector.ToolbarButton(WebInspector.UIString("Restore all functions"), "refresh-toolbar-item");
this.resetButton.setEnabled(false);
this.resetButton.addEventListener("click", this._resetClicked, this);
this._profileHeader = profileHeader;
this._linkifier = new WebInspector.Linkifier(new WebInspector.Linkifier.DefaultFormatter(30));
this.profile = new WebInspector.CPUProfileDataModel(profileHeader._profile || profileHeader.protocolProfile());
this._changeView();
if (this._flameChart)
this._flameChart.update();
}
WebInspector.CPUProfileView._TypeFlame = "Flame";
WebInspector.CPUProfileView._TypeTree = "Tree";
WebInspector.CPUProfileView._TypeHeavy = "Heavy";
/**
* @interface
*/
WebInspector.CPUProfileView.Searchable = function()
{
}
WebInspector.CPUProfileView.Searchable.prototype = {
jumpToNextSearchResult: function() {},
jumpToPreviousSearchResult: function() {},
searchCanceled: function() {},
/**
* @param {!WebInspector.SearchableView.SearchConfig} searchConfig
* @param {boolean} shouldJump
* @param {boolean=} jumpBackwards
* @return {number}
*/
performSearch: function(searchConfig, shouldJump, jumpBackwards) {},
/**
* @return {number}
*/
currentSearchResultIndex: function() {}
}
WebInspector.CPUProfileView.prototype = {
focus: function()
{
if (this._flameChart)
this._flameChart.focus();
else
WebInspector.Widget.prototype.focus.call(this);
},
/**
* @return {?WebInspector.Target}
*/
target: function()
{
return this._profileHeader.target();
},
/**
* @param {number} timeLeft
* @param {number} timeRight
*/
selectRange: function(timeLeft, timeRight)
{
if (!this._flameChart)
return;
this._flameChart.selectRange(timeLeft, timeRight);
},
/**
* @return {!Array.<!WebInspector.ToolbarItem>}
*/
toolbarItems: function()
{
return [this.viewSelectComboBox, this.focusButton, this.excludeButton, this.resetButton];
},
/**
* @return {!WebInspector.ProfileDataGridTree}
*/
_getBottomUpProfileDataGridTree: function()
{
if (!this._bottomUpProfileDataGridTree)
this._bottomUpProfileDataGridTree = new WebInspector.BottomUpProfileDataGridTree(this, /** @type {!ProfilerAgent.CPUProfileNode} */ (this.profile.profileHead));
return this._bottomUpProfileDataGridTree;
},
/**
* @return {!WebInspector.ProfileDataGridTree}
*/
_getTopDownProfileDataGridTree: function()
{
if (!this._topDownProfileDataGridTree)
this._topDownProfileDataGridTree = new WebInspector.TopDownProfileDataGridTree(this, /** @type {!ProfilerAgent.CPUProfileNode} */ (this.profile.profileHead));
return this._topDownProfileDataGridTree;
},
willHide: function()
{
this._currentSearchResultIndex = -1;
},
refresh: function()
{
var selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null;
this.dataGrid.rootNode().removeChildren();
var children = this.profileDataGridTree.children;
var count = children.length;
for (var index = 0; index < count; ++index)
this.dataGrid.rootNode().appendChild(children[index]);
if (selectedProfileNode)
selectedProfileNode.selected = true;
},
refreshVisibleData: function()
{
var child = this.dataGrid.rootNode().children[0];
while (child) {
child.refresh();
child = child.traverseNextNode(false, null, true);
}
},
/**
* @return {!WebInspector.SearchableView}
*/
searchableView: function()
{
return this._searchableView;
},
/**
* @override
* @return {boolean}
*/
supportsCaseSensitiveSearch: function()
{
return true;
},
/**
* @override
* @return {boolean}
*/
supportsRegexSearch: function()
{
return false;
},
/**
* @override
*/
searchCanceled: function()
{
this._searchableElement.searchCanceled();
},
/**
* @override
* @param {!WebInspector.SearchableView.SearchConfig} searchConfig
* @param {boolean} shouldJump
* @param {boolean=} jumpBackwards
*/
performSearch: function(searchConfig, shouldJump, jumpBackwards)
{
var matchesCount = this._searchableElement.performSearch(searchConfig, shouldJump, jumpBackwards);
this._searchableView.updateSearchMatchesCount(matchesCount);
this._searchableView.updateCurrentMatchIndex(this._searchableElement.currentSearchResultIndex());
},
/**
* @override
*/
jumpToNextSearchResult: function()
{
this._searchableElement.jumpToNextSearchResult();
this._searchableView.updateCurrentMatchIndex(this._searchableElement.currentSearchResultIndex());
},
/**
* @override
*/
jumpToPreviousSearchResult: function()
{
this._searchableElement.jumpToPreviousSearchResult();
this._searchableView.updateCurrentMatchIndex(this._searchableElement.currentSearchResultIndex());
},
_ensureFlameChartCreated: function()
{
if (this._flameChart)
return;
this._dataProvider = new WebInspector.CPUFlameChartDataProvider(this.profile, this._profileHeader.target());
this._flameChart = new WebInspector.CPUProfileFlameChart(this._dataProvider);
this._flameChart.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected.bind(this));
},
/**
* @param {!WebInspector.Event} event
*/
_onEntrySelected: function(event)
{
var entryIndex = event.data;
var node = this._dataProvider._entryNodes[entryIndex];
var debuggerModel = this._profileHeader._debuggerModel;
if (!node || !node.scriptId || !debuggerModel)
return;
var script = debuggerModel.scriptForId(node.scriptId);
if (!script)
return;
var location = /** @type {!WebInspector.DebuggerModel.Location} */ (debuggerModel.createRawLocation(script, node.lineNumber, node.columnNumber));
WebInspector.Revealer.reveal(WebInspector.debuggerWorkspaceBinding.rawLocationToUILocation(location));
},
_changeView: function()
{
if (!this.profile)
return;
this._searchableView.closeSearch();
if (this._visibleView)
this._visibleView.detach();
this._viewType.set(this.viewSelectComboBox.selectedOption().value);
switch (this._viewType.get()) {
case WebInspector.CPUProfileView._TypeFlame:
this._ensureFlameChartCreated();
this._visibleView = this._flameChart;
this._searchableElement = this._flameChart;
break;
case WebInspector.CPUProfileView._TypeTree:
this.profileDataGridTree = this._getTopDownProfileDataGridTree();
this._sortProfile();
this._visibleView = this.dataGrid;
this._searchableElement = this.profileDataGridTree;
break;
case WebInspector.CPUProfileView._TypeHeavy:
this.profileDataGridTree = this._getBottomUpProfileDataGridTree();
this._sortProfile();
this._visibleView = this.dataGrid;
this._searchableElement = this.profileDataGridTree;
break;
}
var isFlame = this._viewType.get() === WebInspector.CPUProfileView._TypeFlame;
this.focusButton.setVisible(!isFlame);
this.excludeButton.setVisible(!isFlame);
this.resetButton.setVisible(!isFlame);
this._visibleView.show(this._searchableView.element);
},
_focusClicked: function(event)
{
if (!this.dataGrid.selectedNode)
return;
this.resetButton.setEnabled(true);
this.profileDataGridTree.focus(this.dataGrid.selectedNode);
this.refresh();
this.refreshVisibleData();
},
_excludeClicked: function(event)
{
var selectedNode = this.dataGrid.selectedNode;
if (!selectedNode)
return;
selectedNode.deselect();
this.resetButton.setEnabled(true);
this.profileDataGridTree.exclude(selectedNode);
this.refresh();
this.refreshVisibleData();
},
_resetClicked: function(event)
{
this.resetButton.setEnabled(false);
this.profileDataGridTree.restore();
this._linkifier.reset();
this.refresh();
this.refreshVisibleData();
},
_dataGridNodeSelected: function(node)
{
this.focusButton.setEnabled(true);
this.excludeButton.setEnabled(true);
},
_dataGridNodeDeselected: function(node)
{
this.focusButton.setEnabled(false);
this.excludeButton.setEnabled(false);
},
_sortProfile: function()
{
var sortAscending = this.dataGrid.isSortOrderAscending();
var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier();
var sortProperty = {
"self": "selfTime",
"total": "totalTime",
"function": "functionName"
}[sortColumnIdentifier];
this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator(sortProperty, sortAscending));
this.refresh();
},
__proto__: WebInspector.VBox.prototype
}
/**
* @constructor
* @extends {WebInspector.ProfileType}
*/
WebInspector.CPUProfileType = function()
{
WebInspector.ProfileType.call(this, WebInspector.CPUProfileType.TypeId, WebInspector.UIString("Collect JavaScript CPU Profile"));
this._recording = false;
this._nextAnonymousConsoleProfileNumber = 1;
this._anonymousConsoleProfileIdToTitle = {};
WebInspector.CPUProfileType.instance = this;
WebInspector.targetManager.addModelListener(WebInspector.CPUProfilerModel, WebInspector.CPUProfilerModel.EventTypes.ConsoleProfileStarted, this._consoleProfileStarted, this);
WebInspector.targetManager.addModelListener(WebInspector.CPUProfilerModel, WebInspector.CPUProfilerModel.EventTypes.ConsoleProfileFinished, this._consoleProfileFinished, this);
}
WebInspector.CPUProfileType.TypeId = "CPU";
WebInspector.CPUProfileType.prototype = {
/**
* @override
* @return {string}
*/
fileExtension: function()
{
return ".cpuprofile";
},
get buttonTooltip()
{
return this._recording ? WebInspector.UIString("Stop CPU profiling") : WebInspector.UIString("Start CPU profiling");
},
/**
* @override
* @return {boolean}
*/
buttonClicked: function()
{
if (this._recording) {
this.stopRecordingProfile();
return false;
} else {
this.startRecordingProfile();
return true;
}
},
get treeItemTitle()
{
return WebInspector.UIString("CPU PROFILES");
},
get description()
{
return WebInspector.UIString("CPU profiles show where the execution time is spent in your page's JavaScript functions.");
},
/**
* @param {!WebInspector.Event} event
*/
_consoleProfileStarted: function(event)
{
var protocolId = /** @type {string} */ (event.data.protocolId);
var scriptLocation = /** @type {!WebInspector.DebuggerModel.Location} */ (event.data.scriptLocation);
var resolvedTitle = /** @type {string|undefined} */ (event.data.title);
if (!resolvedTitle) {
resolvedTitle = WebInspector.UIString("Profile %s", this._nextAnonymousConsoleProfileNumber++);
this._anonymousConsoleProfileIdToTitle[protocolId] = resolvedTitle;
}
this._addMessageToConsole(WebInspector.ConsoleMessage.MessageType.Profile, scriptLocation, WebInspector.UIString("Profile '%s' started.", resolvedTitle));
},
/**
* @param {!WebInspector.Event} event
*/
_consoleProfileFinished: function(event)
{
var protocolId = /** @type {string} */ (event.data.protocolId);
var scriptLocation = /** @type {!WebInspector.DebuggerModel.Location} */ (event.data.scriptLocation);
var cpuProfile = /** @type {!ProfilerAgent.CPUProfile} */ (event.data.cpuProfile);
var resolvedTitle = /** @type {string|undefined} */ (event.data.title);
if (typeof resolvedTitle === "undefined") {
resolvedTitle = this._anonymousConsoleProfileIdToTitle[protocolId];
delete this._anonymousConsoleProfileIdToTitle[protocolId];
}
var profile = new WebInspector.CPUProfileHeader(scriptLocation.target(), this, resolvedTitle);
profile.setProtocolProfile(cpuProfile);
this.addProfile(profile);
this._addMessageToConsole(WebInspector.ConsoleMessage.MessageType.ProfileEnd, scriptLocation, WebInspector.UIString("Profile '%s' finished.", resolvedTitle));
},
/**
* @param {string} type
* @param {!WebInspector.DebuggerModel.Location} scriptLocation
* @param {string} messageText
*/
_addMessageToConsole: function(type, scriptLocation, messageText)
{
var script = scriptLocation.script();
var target = scriptLocation.target();
var message = new WebInspector.ConsoleMessage(
target,
WebInspector.ConsoleMessage.MessageSource.ConsoleAPI,
WebInspector.ConsoleMessage.MessageLevel.Debug,
messageText,
type,
undefined,
undefined,
undefined,
undefined,
undefined,
[{
functionName: "",
scriptId: scriptLocation.scriptId,
url: script ? script.contentURL() : "",
lineNumber: scriptLocation.lineNumber,
columnNumber: scriptLocation.columnNumber || 0
}]);
target.consoleModel.addMessage(message);
},
startRecordingProfile: function()
{
var target = WebInspector.context.flavor(WebInspector.Target);
if (this._profileBeingRecorded || !target)
return;
var profile = new WebInspector.CPUProfileHeader(target, this);
this.setProfileBeingRecorded(profile);
this.addProfile(profile);
profile.updateStatus(WebInspector.UIString("Recording\u2026"));
this._recording = true;
target.cpuProfilerModel.startRecording();
},
stopRecordingProfile: function()
{
this._recording = false;
if (!this._profileBeingRecorded || !this._profileBeingRecorded.target())
return;
/**
* @param {?ProfilerAgent.CPUProfile} profile
* @this {WebInspector.CPUProfileType}
*/
function didStopProfiling(profile)
{
if (!this._profileBeingRecorded)
return;
console.assert(profile);
this._profileBeingRecorded.setProtocolProfile(profile);
this._profileBeingRecorded.updateStatus("");
var recordedProfile = this._profileBeingRecorded;
this.setProfileBeingRecorded(null);
this.dispatchEventToListeners(WebInspector.ProfileType.Events.ProfileComplete, recordedProfile);
}
this._profileBeingRecorded.target().cpuProfilerModel.stopRecording().then(didStopProfiling.bind(this));
},
/**
* @override
* @param {string} title
* @return {!WebInspector.ProfileHeader}
*/
createProfileLoadedFromFile: function(title)
{
return new WebInspector.CPUProfileHeader(null, this, title);
},
/**
* @override
*/
profileBeingRecordedRemoved: function()
{
this.stopRecordingProfile();
},
__proto__: WebInspector.ProfileType.prototype
}
/**
* @constructor
* @extends {WebInspector.ProfileHeader}
* @implements {WebInspector.OutputStream}
* @implements {WebInspector.OutputStreamDelegate}
* @param {?WebInspector.Target} target
* @param {!WebInspector.CPUProfileType} type
* @param {string=} title
*/
WebInspector.CPUProfileHeader = function(target, type, title)
{
WebInspector.ProfileHeader.call(this, target, type, title || WebInspector.UIString("Profile %d", type.nextProfileUid()));
this._debuggerModel = WebInspector.DebuggerModel.fromTarget(target);
this._tempFile = null;
}
WebInspector.CPUProfileHeader.prototype = {
/**
* @override
*/
onTransferStarted: function()
{
this._jsonifiedProfile = "";
this.updateStatus(WebInspector.UIString("Loading\u2026 %s", Number.bytesToString(this._jsonifiedProfile.length)), true);
},
/**
* @override
* @param {!WebInspector.ChunkedReader} reader
*/
onChunkTransferred: function(reader)
{
this.updateStatus(WebInspector.UIString("Loading\u2026 %d%%", Number.bytesToString(this._jsonifiedProfile.length)));
},
/**
* @override
*/
onTransferFinished: function()
{
this.updateStatus(WebInspector.UIString("Parsing\u2026"), true);
this._profile = JSON.parse(this._jsonifiedProfile);
this._jsonifiedProfile = null;
this.updateStatus(WebInspector.UIString("Loaded"), false);
if (this._profileType.profileBeingRecorded() === this)
this._profileType.setProfileBeingRecorded(null);
},
/**
* @override
* @param {!WebInspector.ChunkedReader} reader
* @param {!Event} e
*/
onError: function(reader, e)
{
var subtitle;
switch(e.target.error.code) {
case e.target.error.NOT_FOUND_ERR:
subtitle = WebInspector.UIString("'%s' not found.", reader.fileName());
break;
case e.target.error.NOT_READABLE_ERR:
subtitle = WebInspector.UIString("'%s' is not readable", reader.fileName());
break;
case e.target.error.ABORT_ERR:
return;
default:
subtitle = WebInspector.UIString("'%s' error %d", reader.fileName(), e.target.error.code);
}
this.updateStatus(subtitle);
},
/**
* @override
* @param {string} text
*/
write: function(text)
{
this._jsonifiedProfile += text;
},
/**
* @override
*/
close: function() { },
/**
* @override
*/
dispose: function()
{
this.removeTempFile();
},
/**
* @override
* @param {!WebInspector.ProfileType.DataDisplayDelegate} panel
* @return {!WebInspector.ProfileSidebarTreeElement}
*/
createSidebarTreeElement: function(panel)
{
return new WebInspector.ProfileSidebarTreeElement(panel, this, "profile-sidebar-tree-item");
},
/**
* @override
* @return {!WebInspector.CPUProfileView}
*/
createView: function()
{
return new WebInspector.CPUProfileView(this);
},
/**
* @override
* @return {boolean}
*/
canSaveToFile: function()
{
return !this.fromFile() && this._protocolProfile;
},
saveToFile: function()
{
var fileOutputStream = new WebInspector.FileOutputStream();
/**
* @param {boolean} accepted
* @this {WebInspector.CPUProfileHeader}
*/
function onOpenForSave(accepted)
{
if (!accepted)
return;
function didRead(data)
{
if (data)
fileOutputStream.write(data, fileOutputStream.close.bind(fileOutputStream));
else
fileOutputStream.close();
}
if (this._failedToCreateTempFile) {
WebInspector.console.error("Failed to open temp file with heap snapshot");
fileOutputStream.close();
} else if (this._tempFile) {
this._tempFile.read(didRead);
} else {
this._onTempFileReady = onOpenForSave.bind(this, accepted);
}
}
this._fileName = this._fileName || "CPU-" + new Date().toISO8601Compact() + this._profileType.fileExtension();
fileOutputStream.open(this._fileName, onOpenForSave.bind(this));
},
/**
* @override
* @param {!File} file
*/
loadFromFile: function(file)
{
this.updateStatus(WebInspector.UIString("Loading\u2026"), true);
var fileReader = new WebInspector.ChunkedFileReader(file, 10000000, this);
fileReader.start(this);
},
/**
* @return {?ProfilerAgent.CPUProfile}
*/
protocolProfile: function()
{
return this._protocolProfile;
},
/**
* @param {!ProfilerAgent.CPUProfile} cpuProfile
*/
setProtocolProfile: function(cpuProfile)
{
this._protocolProfile = cpuProfile;
this._saveProfileDataToTempFile(cpuProfile);
if (this.canSaveToFile())
this.dispatchEventToListeners(WebInspector.ProfileHeader.Events.ProfileReceived);
},
/**
* @param {!ProfilerAgent.CPUProfile} data
*/
_saveProfileDataToTempFile: function(data)
{
var serializedData = JSON.stringify(data);
/**
* @this {WebInspector.CPUProfileHeader}
*/
function didCreateTempFile(tempFile)
{
this._writeToTempFile(tempFile, serializedData);
}
WebInspector.TempFile.create("cpu-profiler", String(this.uid))
.then(didCreateTempFile.bind(this));
},
/**
* @param {?WebInspector.TempFile} tempFile
* @param {string} serializedData
*/
_writeToTempFile: function(tempFile, serializedData)
{
this._tempFile = tempFile;
if (!tempFile) {
this._failedToCreateTempFile = true;
this._notifyTempFileReady();
return;
}
/**
* @param {number} fileSize
* @this {WebInspector.CPUProfileHeader}
*/
function didWriteToTempFile(fileSize)
{
if (!fileSize)
this._failedToCreateTempFile = true;
tempFile.finishWriting();
this._notifyTempFileReady();
}
tempFile.write([serializedData], didWriteToTempFile.bind(this));
},
_notifyTempFileReady: function()
{
if (this._onTempFileReady) {
this._onTempFileReady();
this._onTempFileReady = null;
}
},
__proto__: WebInspector.ProfileHeader.prototype
}