/* * 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. */ const UserInitiatedProfileName = "org.webkit.profiles.user-initiated"; WebInspector.ProfileType = function(id, name) { this._id = id; this._name = name; } WebInspector.ProfileType.URLRegExp = /webkit-profile:\/\/(.+)\/(.+)#([0-9]+)/; WebInspector.ProfileType.prototype = { get buttonTooltip() { return ""; }, get buttonStyle() { return undefined; }, get buttonCaption() { return this.name; }, get id() { return this._id; }, get name() { return this._name; }, buttonClicked: function() { }, viewForProfile: function(profile) { if (!profile._profileView) profile._profileView = this.createView(profile); return profile._profileView; }, get welcomeMessage() { return ""; }, // Must be implemented by subclasses. createView: function(profile) { throw new Error("Needs implemented."); }, // Must be implemented by subclasses. createSidebarTreeElementForProfile: function(profile) { throw new Error("Needs implemented."); } } WebInspector.ProfilesPanel = function() { WebInspector.Panel.call(this, "profiles"); this.createSidebar(); this._profileTypesByIdMap = {}; this._profileTypeButtonsByIdMap = {}; var panelEnablerHeading = WebInspector.UIString("You need to enable profiling before you can use the Profiles panel."); var panelEnablerDisclaimer = WebInspector.UIString("Enabling profiling will make scripts run slower."); var panelEnablerButton = WebInspector.UIString("Enable Profiling"); this.panelEnablerView = new WebInspector.PanelEnablerView("profiles", panelEnablerHeading, panelEnablerDisclaimer, panelEnablerButton); this.panelEnablerView.addEventListener("enable clicked", this._enableProfiling, this); this.element.appendChild(this.panelEnablerView.element); this.profileViews = document.createElement("div"); this.profileViews.id = "profile-views"; this.element.appendChild(this.profileViews); this.enableToggleButton = new WebInspector.StatusBarButton("", "enable-toggle-status-bar-item"); this.enableToggleButton.addEventListener("click", this._toggleProfiling.bind(this), false); this.clearResultsButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear CPU profiles."), "clear-status-bar-item"); this.clearResultsButton.addEventListener("click", this._clearProfiles.bind(this), false); this.profileViewStatusBarItemsContainer = document.createElement("div"); this.profileViewStatusBarItemsContainer.className = "status-bar-items"; this.welcomeView = new WebInspector.WelcomeView("profiles", WebInspector.UIString("Welcome to the Profiles panel")); this.element.appendChild(this.welcomeView.element); this._profiles = []; this._profilerEnabled = Preferences.profilerAlwaysEnabled; this._reset(); } WebInspector.ProfilesPanel.prototype = { get toolbarItemLabel() { return WebInspector.UIString("Profiles"); }, get statusBarItems() { function clickHandler(profileType, buttonElement) { profileType.buttonClicked.call(profileType); this.updateProfileTypeButtons(); } var items = [this.enableToggleButton.element]; // FIXME: Generate a single "combo-button". for (var typeId in this._profileTypesByIdMap) { var profileType = this.getProfileType(typeId); if (profileType.buttonStyle) { var button = new WebInspector.StatusBarButton(profileType.buttonTooltip, profileType.buttonStyle, profileType.buttonCaption); this._profileTypeButtonsByIdMap[typeId] = button.element; button.element.addEventListener("click", clickHandler.bind(this, profileType, button.element), false); items.push(button.element); } } items.push(this.clearResultsButton.element, this.profileViewStatusBarItemsContainer); return items; }, show: function() { WebInspector.Panel.prototype.show.call(this); this._populateProfiles(); }, profilerWasEnabled: function() { if (this._profilerEnabled) return; this._profilerEnabled = true; this._reset(); if (this.visible) this._populateProfiles(); }, profilerWasDisabled: function() { if (!this._profilerEnabled) return; this._profilerEnabled = false; this._reset(); }, resetProfiles: function() { this._reset(); }, _reset: function() { for (var i = 0; i < this._profiles.length; ++i) delete this._profiles[i]._profileView; delete this.visibleView; delete this.currentQuery; this.searchCanceled(); this._profiles = []; this._profilesIdMap = {}; this._profileGroups = {}; this._profileGroupsForLinks = {} this._profilesWereRequested = false; this.sidebarTreeElement.removeStyleClass("some-expandable"); for (var typeId in this._profileTypesByIdMap) this.getProfileType(typeId).treeElement.removeChildren(); this.profileViews.removeChildren(); this.profileViewStatusBarItemsContainer.removeChildren(); this._updateInterface(); this.welcomeView.show(); }, _clearProfiles: function() { InspectorBackend.clearProfiles(); this._reset(); }, registerProfileType: function(profileType) { this._profileTypesByIdMap[profileType.id] = profileType; profileType.treeElement = new WebInspector.SidebarSectionTreeElement(profileType.name, null, true); this.sidebarTree.appendChild(profileType.treeElement); profileType.treeElement.expand(); this._addWelcomeMessage(profileType); }, _addWelcomeMessage: function(profileType) { var message = profileType.welcomeMessage; // Message text is supposed to have a '%s' substring as a placeholder // for a status bar button. If it is there, we split the message in two // parts, and insert the button between them. var buttonPos = message.indexOf("%s"); if (buttonPos > -1) { var container = document.createDocumentFragment(); var part1 = document.createElement("span"); part1.innerHTML = message.substr(0, buttonPos); container.appendChild(part1); var button = new WebInspector.StatusBarButton(profileType.buttonTooltip, profileType.buttonStyle, profileType.buttonCaption); container.appendChild(button.element); var part2 = document.createElement("span"); part2.innerHTML = message.substr(buttonPos + 2); container.appendChild(part2); this.welcomeView.addMessage(container); } else this.welcomeView.addMessage(message); }, _makeKey: function(text, profileTypeId) { return escape(text) + '/' + escape(profileTypeId); }, addProfileHeader: function(profile) { var typeId = profile.typeId; var profileType = this.getProfileType(typeId); var sidebarParent = profileType.treeElement; var small = false; var alternateTitle; profile.__profilesPanelProfileType = profileType; this._profiles.push(profile); this._profilesIdMap[this._makeKey(profile.uid, typeId)] = profile; if (profile.title.indexOf(UserInitiatedProfileName) !== 0) { var profileTitleKey = this._makeKey(profile.title, typeId); if (!(profileTitleKey in this._profileGroups)) this._profileGroups[profileTitleKey] = []; var group = this._profileGroups[profileTitleKey]; group.push(profile); if (group.length === 2) { // Make a group TreeElement now that there are 2 profiles. group._profilesTreeElement = new WebInspector.ProfileGroupSidebarTreeElement(profile.title); // Insert at the same index for the first profile of the group. var index = sidebarParent.children.indexOf(group[0]._profilesTreeElement); sidebarParent.insertChild(group._profilesTreeElement, index); // Move the first profile to the group. var selected = group[0]._profilesTreeElement.selected; sidebarParent.removeChild(group[0]._profilesTreeElement); group._profilesTreeElement.appendChild(group[0]._profilesTreeElement); if (selected) { group[0]._profilesTreeElement.select(); group[0]._profilesTreeElement.reveal(); } group[0]._profilesTreeElement.small = true; group[0]._profilesTreeElement.mainTitle = WebInspector.UIString("Run %d", 1); this.sidebarTreeElement.addStyleClass("some-expandable"); } if (group.length >= 2) { sidebarParent = group._profilesTreeElement; alternateTitle = WebInspector.UIString("Run %d", group.length); small = true; } } var profileTreeElement = profileType.createSidebarTreeElementForProfile(profile); profileTreeElement.small = small; if (alternateTitle) profileTreeElement.mainTitle = alternateTitle; profile._profilesTreeElement = profileTreeElement; sidebarParent.appendChild(profileTreeElement); if (!profile.isTemporary) { this.welcomeView.hide(); if (!this.visibleView) this.showProfile(profile); this.dispatchEventToListeners("profile added"); } }, removeProfileHeader: function(profile) { var typeId = profile.typeId; var profileType = this.getProfileType(typeId); var sidebarParent = profileType.treeElement; for (var i = 0; i < this._profiles.length; ++i) { if (this._profiles[i].uid === profile.uid) { profile = this._profiles[i]; this._profiles.splice(i, 1); break; } } delete this._profilesIdMap[this._makeKey(profile.uid, typeId)]; var profileTitleKey = this._makeKey(profile.title, typeId); delete this._profileGroups[profileTitleKey]; sidebarParent.removeChild(profile._profilesTreeElement); if (!profile.isTemporary) InspectorBackend.removeProfile(profile.typeId, profile.uid); // No other item will be selected if there aren't any other profiles, so // make sure that view gets cleared when the last profile is removed. if (!this._profiles.length) this.closeVisibleView(); }, showProfile: function(profile) { if (!profile || profile.isTemporary) return; this.closeVisibleView(); var view = profile.__profilesPanelProfileType.viewForProfile(profile); view.show(this.profileViews); profile._profilesTreeElement.select(true); profile._profilesTreeElement.reveal(); this.visibleView = view; this.profileViewStatusBarItemsContainer.removeChildren(); var statusBarItems = view.statusBarItems; for (var i = 0; i < statusBarItems.length; ++i) this.profileViewStatusBarItemsContainer.appendChild(statusBarItems[i]); }, getProfiles: function(typeId) { var result = []; var profilesCount = this._profiles.length; for (var i = 0; i < profilesCount; ++i) if (this._profiles[i].typeId === typeId) result.push(this._profiles[i]); return result; }, hasTemporaryProfile: function(typeId) { var profilesCount = this._profiles.length; for (var i = 0; i < profilesCount; ++i) if (this._profiles[i].typeId === typeId && this._profiles[i].isTemporary) return true; return false; }, hasProfile: function(profile) { return !!this._profilesIdMap[this._makeKey(profile.uid, profile.typeId)]; }, updateProfile: function(profile) { var profilesCount = this._profiles.length; for (var i = 0; i < profilesCount; ++i) if (this._profiles[i].typeId === profile.typeId && this._profiles[i].uid === profile.uid) { this._profiles[i] = profile; break; } }, showView: function(view) { this.showProfile(view.profile); }, getProfileType: function(typeId) { return this._profileTypesByIdMap[typeId]; }, showProfileForURL: function(url) { var match = url.match(WebInspector.ProfileType.URLRegExp); if (!match) return; this.showProfile(this._profilesIdMap[this._makeKey(match[3], match[1])]); }, updateProfileTypeButtons: function() { for (var typeId in this._profileTypeButtonsByIdMap) { var buttonElement = this._profileTypeButtonsByIdMap[typeId]; var profileType = this.getProfileType(typeId); buttonElement.className = profileType.buttonStyle; buttonElement.title = profileType.buttonTooltip; // FIXME: Apply profileType.buttonCaption once captions are added to button controls. } }, closeVisibleView: function() { if (this.visibleView) this.visibleView.hide(); delete this.visibleView; }, displayTitleForProfileLink: function(title, typeId) { title = unescape(title); if (title.indexOf(UserInitiatedProfileName) === 0) { title = WebInspector.UIString("Profile %d", title.substring(UserInitiatedProfileName.length + 1)); } else { var titleKey = this._makeKey(title, typeId); if (!(titleKey in this._profileGroupsForLinks)) this._profileGroupsForLinks[titleKey] = 0; var groupNumber = ++this._profileGroupsForLinks[titleKey]; if (groupNumber > 2) // The title is used in the console message announcing that a profile has started so it gets // incremented twice as often as it's displayed title += " " + WebInspector.UIString("Run %d", (groupNumber + 1) / 2); } return title; }, get searchableViews() { var views = []; const visibleView = this.visibleView; if (visibleView && visibleView.performSearch) views.push(visibleView); var profilesLength = this._profiles.length; for (var i = 0; i < profilesLength; ++i) { var profile = this._profiles[i]; var view = profile.__profilesPanelProfileType.viewForProfile(profile); if (!view.performSearch || view === visibleView) continue; views.push(view); } return views; }, searchMatchFound: function(view, matches) { view.profile._profilesTreeElement.searchMatches = matches; }, searchCanceled: function(startingNewSearch) { WebInspector.Panel.prototype.searchCanceled.call(this, startingNewSearch); if (!this._profiles) return; for (var i = 0; i < this._profiles.length; ++i) { var profile = this._profiles[i]; profile._profilesTreeElement.searchMatches = 0; } }, _updateInterface: function() { // FIXME: Replace ProfileType-specific button visibility changes by a single ProfileType-agnostic "combo-button" visibility change. if (this._profilerEnabled) { this.enableToggleButton.title = WebInspector.UIString("Profiling enabled. Click to disable."); this.enableToggleButton.toggled = true; for (var typeId in this._profileTypeButtonsByIdMap) this._profileTypeButtonsByIdMap[typeId].removeStyleClass("hidden"); this.profileViewStatusBarItemsContainer.removeStyleClass("hidden"); this.clearResultsButton.element.removeStyleClass("hidden"); this.panelEnablerView.visible = false; } else { this.enableToggleButton.title = WebInspector.UIString("Profiling disabled. Click to enable."); this.enableToggleButton.toggled = false; for (var typeId in this._profileTypeButtonsByIdMap) this._profileTypeButtonsByIdMap[typeId].addStyleClass("hidden"); this.profileViewStatusBarItemsContainer.addStyleClass("hidden"); this.clearResultsButton.element.addStyleClass("hidden"); this.panelEnablerView.visible = true; } }, _enableProfiling: function() { if (this._profilerEnabled) return; this._toggleProfiling(this.panelEnablerView.alwaysEnabled); }, _toggleProfiling: function(optionalAlways) { if (this._profilerEnabled) InspectorBackend.disableProfiler(true); else InspectorBackend.enableProfiler(!!optionalAlways); }, _populateProfiles: function() { if (!this._profilerEnabled || this._profilesWereRequested) return; function populateCallback(profileHeaders) { profileHeaders.sort(function(a, b) { return a.uid - b.uid; }); var profileHeadersLength = profileHeaders.length; for (var i = 0; i < profileHeadersLength; ++i) if (!this.hasProfile(profileHeaders[i])) WebInspector.addProfileHeader(profileHeaders[i]); } InspectorBackend.getProfileHeaders(populateCallback.bind(this)); this._profilesWereRequested = true; }, updateMainViewWidth: function(width) { this.welcomeView.element.style.left = width + "px"; this.profileViews.style.left = width + "px"; this.profileViewStatusBarItemsContainer.style.left = Math.max(155, width) + "px"; this.resize(); } } WebInspector.ProfilesPanel.prototype.__proto__ = WebInspector.Panel.prototype; WebInspector.ProfileSidebarTreeElement = function(profile, titleFormat, className) { this.profile = profile; this._titleFormat = titleFormat; if (this.profile.title.indexOf(UserInitiatedProfileName) === 0) this._profileNumber = this.profile.title.substring(UserInitiatedProfileName.length + 1); WebInspector.SidebarTreeElement.call(this, className, "", "", profile, false); this.refreshTitles(); } WebInspector.ProfileSidebarTreeElement.prototype = { onselect: function() { this.treeOutline.panel.showProfile(this.profile); }, ondelete: function() { this.treeOutline.panel.removeProfileHeader(this.profile); return true; }, get mainTitle() { if (this._mainTitle) return this._mainTitle; if (this.profile.title.indexOf(UserInitiatedProfileName) === 0) return WebInspector.UIString(this._titleFormat, this._profileNumber); return this.profile.title; }, set mainTitle(x) { this._mainTitle = x; this.refreshTitles(); }, get subtitle() { // There is no subtitle. }, set subtitle(x) { // Can't change subtitle. }, set searchMatches(matches) { if (!matches) { if (!this.bubbleElement) return; this.bubbleElement.removeStyleClass("search-matches"); this.bubbleText = ""; return; } this.bubbleText = matches; this.bubbleElement.addStyleClass("search-matches"); } } WebInspector.ProfileSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype; WebInspector.ProfileGroupSidebarTreeElement = function(title, subtitle) { WebInspector.SidebarTreeElement.call(this, "profile-group-sidebar-tree-item", title, subtitle, null, true); } WebInspector.ProfileGroupSidebarTreeElement.prototype = { onselect: function() { if (this.children.length > 0) WebInspector.panels.profiles.showProfile(this.children[this.children.length - 1].profile); } } WebInspector.ProfileGroupSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype;