Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2008 Apple Inc. All Rights Reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions
      6  * are met:
      7  * 1. Redistributions of source code must retain the above copyright
      8  *    notice, this list of conditions and the following disclaimer.
      9  * 2. Redistributions in binary form must reproduce the above copyright
     10  *    notice, this list of conditions and the following disclaimer in the
     11  *    documentation and/or other materials provided with the distribution.
     12  *
     13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
     14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
     17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
     21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     24  */
     25 
     26 const UserInitiatedProfileName = "org.webkit.profiles.user-initiated";
     27 
     28 WebInspector.ProfileType = function(id, name)
     29 {
     30     this._id = id;
     31     this._name = name;
     32 }
     33 
     34 WebInspector.ProfileType.URLRegExp = /webkit-profile:\/\/(.+)\/(.+)#([0-9]+)/;
     35 
     36 WebInspector.ProfileType.prototype = {
     37     get buttonTooltip()
     38     {
     39         return "";
     40     },
     41 
     42     get buttonStyle()
     43     {
     44         return undefined;
     45     },
     46 
     47     get buttonCaption()
     48     {
     49         return this.name;
     50     },
     51 
     52     get id()
     53     {
     54         return this._id;
     55     },
     56 
     57     get name()
     58     {
     59         return this._name;
     60     },
     61 
     62     buttonClicked: function()
     63     {
     64     },
     65 
     66     viewForProfile: function(profile)
     67     {
     68         if (!profile._profileView)
     69             profile._profileView = this.createView(profile);
     70         return profile._profileView;
     71     },
     72 
     73     get welcomeMessage()
     74     {
     75         return "";
     76     },
     77 
     78     // Must be implemented by subclasses.
     79     createView: function(profile)
     80     {
     81         throw new Error("Needs implemented.");
     82     },
     83 
     84     // Must be implemented by subclasses.
     85     createSidebarTreeElementForProfile: function(profile)
     86     {
     87         throw new Error("Needs implemented.");
     88     }
     89 }
     90 
     91 WebInspector.ProfilesPanel = function()
     92 {
     93     WebInspector.Panel.call(this);
     94 
     95     this.createSidebar();
     96 
     97     this.element.addStyleClass("profiles");
     98     this._profileTypesByIdMap = {};
     99     this._profileTypeButtonsByIdMap = {};
    100 
    101     var panelEnablerHeading = WebInspector.UIString("You need to enable profiling before you can use the Profiles panel.");
    102     var panelEnablerDisclaimer = WebInspector.UIString("Enabling profiling will make scripts run slower.");
    103     var panelEnablerButton = WebInspector.UIString("Enable Profiling");
    104     this.panelEnablerView = new WebInspector.PanelEnablerView("profiles", panelEnablerHeading, panelEnablerDisclaimer, panelEnablerButton);
    105     this.panelEnablerView.addEventListener("enable clicked", this._enableProfiling, this);
    106 
    107     this.element.appendChild(this.panelEnablerView.element);
    108 
    109     this.profileViews = document.createElement("div");
    110     this.profileViews.id = "profile-views";
    111     this.element.appendChild(this.profileViews);
    112 
    113     this.enableToggleButton = new WebInspector.StatusBarButton("", "enable-toggle-status-bar-item");
    114     this.enableToggleButton.addEventListener("click", this._toggleProfiling.bind(this), false);
    115 
    116     this.profileViewStatusBarItemsContainer = document.createElement("div");
    117     this.profileViewStatusBarItemsContainer.id = "profile-view-status-bar-items";
    118 
    119     this.welcomeView = new WebInspector.WelcomeView("profiles", WebInspector.UIString("Welcome to the Profiles panel"));
    120     this.element.appendChild(this.welcomeView.element);
    121 
    122     this._profiles = [];
    123     this.reset();
    124 }
    125 
    126 WebInspector.ProfilesPanel.prototype = {
    127     toolbarItemClass: "profiles",
    128 
    129     get toolbarItemLabel()
    130     {
    131         return WebInspector.UIString("Profiles");
    132     },
    133 
    134     get statusBarItems()
    135     {
    136         function clickHandler(profileType, buttonElement)
    137         {
    138             profileType.buttonClicked.call(profileType);
    139             this.updateProfileTypeButtons();
    140         }
    141 
    142         var items = [this.enableToggleButton.element];
    143         // FIXME: Generate a single "combo-button".
    144         for (var typeId in this._profileTypesByIdMap) {
    145             var profileType = this.getProfileType(typeId);
    146             if (profileType.buttonStyle) {
    147                 var button = new WebInspector.StatusBarButton(profileType.buttonTooltip, profileType.buttonStyle, profileType.buttonCaption);
    148                 this._profileTypeButtonsByIdMap[typeId] = button.element;
    149                 button.element.addEventListener("click", clickHandler.bind(this, profileType, button.element), false);
    150                 items.push(button.element);
    151             }
    152         }
    153         items.push(this.profileViewStatusBarItemsContainer);
    154         return items;
    155     },
    156 
    157     show: function()
    158     {
    159         WebInspector.Panel.prototype.show.call(this);
    160         if (this._shouldPopulateProfiles)
    161             this._populateProfiles();
    162     },
    163 
    164     populateInterface: function()
    165     {
    166         if (this.visible)
    167             this._populateProfiles();
    168         else
    169             this._shouldPopulateProfiles = true;
    170     },
    171 
    172     profilerWasEnabled: function()
    173     {
    174         this.reset();
    175         this.populateInterface();
    176     },
    177 
    178     profilerWasDisabled: function()
    179     {
    180         this.reset();
    181     },
    182 
    183     reset: function()
    184     {
    185         for (var i = 0; i < this._profiles.length; ++i)
    186             delete this._profiles[i]._profileView;
    187         delete this.visibleView;
    188 
    189         delete this.currentQuery;
    190         this.searchCanceled();
    191 
    192         this._profiles = [];
    193         this._profilesIdMap = {};
    194         this._profileGroups = {};
    195         this._profileGroupsForLinks = {}
    196 
    197         this.sidebarTreeElement.removeStyleClass("some-expandable");
    198 
    199         for (var typeId in this._profileTypesByIdMap)
    200             this.getProfileType(typeId).treeElement.removeChildren();
    201 
    202         this.profileViews.removeChildren();
    203 
    204         this.profileViewStatusBarItemsContainer.removeChildren();
    205 
    206         this._updateInterface();
    207         this.welcomeView.show();
    208     },
    209 
    210     registerProfileType: function(profileType)
    211     {
    212         this._profileTypesByIdMap[profileType.id] = profileType;
    213         profileType.treeElement = new WebInspector.SidebarSectionTreeElement(profileType.name, null, true);
    214         this.sidebarTree.appendChild(profileType.treeElement);
    215         profileType.treeElement.expand();
    216         this._addWelcomeMessage(profileType);
    217     },
    218 
    219     _addWelcomeMessage: function(profileType)
    220     {
    221         var message = profileType.welcomeMessage;
    222         // Message text is supposed to have a '%s' substring as a placeholder
    223         // for a status bar button. If it is there, we split the message in two
    224         // parts, and insert the button between them.
    225         var buttonPos = message.indexOf("%s");
    226         if (buttonPos > -1) {
    227             var container = document.createDocumentFragment();
    228             var part1 = document.createElement("span");
    229             part1.innerHTML = message.substr(0, buttonPos);
    230             container.appendChild(part1);
    231 
    232             var button = new WebInspector.StatusBarButton(profileType.buttonTooltip, profileType.buttonStyle, profileType.buttonCaption);
    233             button.element.addEventListener("click", profileType.buttonClicked.bind(profileType), false);
    234             container.appendChild(button.element);
    235 
    236             var part2 = document.createElement("span");
    237             part2.innerHTML = message.substr(buttonPos + 2);
    238             container.appendChild(part2);
    239             this.welcomeView.addMessage(container);
    240         } else
    241             this.welcomeView.addMessage(message);
    242     },
    243 
    244     _makeKey: function(text, profileTypeId)
    245     {
    246         return escape(text) + '/' + escape(profileTypeId);
    247     },
    248 
    249     addProfileHeader: function(profile)
    250     {
    251         var typeId = profile.typeId;
    252         var profileType = this.getProfileType(typeId);
    253         var sidebarParent = profileType.treeElement;
    254         var small = false;
    255         var alternateTitle;
    256 
    257         profile.__profilesPanelProfileType = profileType;
    258         this._profiles.push(profile);
    259         this._profilesIdMap[this._makeKey(profile.uid, typeId)] = profile;
    260 
    261         if (profile.title.indexOf(UserInitiatedProfileName) !== 0) {
    262             var profileTitleKey = this._makeKey(profile.title, typeId);
    263             if (!(profileTitleKey in this._profileGroups))
    264                 this._profileGroups[profileTitleKey] = [];
    265 
    266             var group = this._profileGroups[profileTitleKey];
    267             group.push(profile);
    268 
    269             if (group.length === 2) {
    270                 // Make a group TreeElement now that there are 2 profiles.
    271                 group._profilesTreeElement = new WebInspector.ProfileGroupSidebarTreeElement(profile.title);
    272 
    273                 // Insert at the same index for the first profile of the group.
    274                 var index = sidebarParent.children.indexOf(group[0]._profilesTreeElement);
    275                 sidebarParent.insertChild(group._profilesTreeElement, index);
    276 
    277                 // Move the first profile to the group.
    278                 var selected = group[0]._profilesTreeElement.selected;
    279                 sidebarParent.removeChild(group[0]._profilesTreeElement);
    280                 group._profilesTreeElement.appendChild(group[0]._profilesTreeElement);
    281                 if (selected) {
    282                     group[0]._profilesTreeElement.select();
    283                     group[0]._profilesTreeElement.reveal();
    284                 }
    285 
    286                 group[0]._profilesTreeElement.small = true;
    287                 group[0]._profilesTreeElement.mainTitle = WebInspector.UIString("Run %d", 1);
    288 
    289                 this.sidebarTreeElement.addStyleClass("some-expandable");
    290             }
    291 
    292             if (group.length >= 2) {
    293                 sidebarParent = group._profilesTreeElement;
    294                 alternateTitle = WebInspector.UIString("Run %d", group.length);
    295                 small = true;
    296             }
    297         }
    298 
    299         var profileTreeElement = profileType.createSidebarTreeElementForProfile(profile);
    300         profileTreeElement.small = small;
    301         if (alternateTitle)
    302             profileTreeElement.mainTitle = alternateTitle;
    303         profile._profilesTreeElement = profileTreeElement;
    304 
    305         sidebarParent.appendChild(profileTreeElement);
    306         this.welcomeView.hide();
    307         if (!this.visibleView)
    308             this.showProfile(profile);
    309     },
    310 
    311     showProfile: function(profile)
    312     {
    313         if (!profile)
    314             return;
    315 
    316         this.closeVisibleView();
    317 
    318         var view = profile.__profilesPanelProfileType.viewForProfile(profile);
    319 
    320         view.show(this.profileViews);
    321 
    322         profile._profilesTreeElement.select(true);
    323         profile._profilesTreeElement.reveal();
    324 
    325         this.visibleView = view;
    326 
    327         this.profileViewStatusBarItemsContainer.removeChildren();
    328 
    329         var statusBarItems = view.statusBarItems;
    330         for (var i = 0; i < statusBarItems.length; ++i)
    331             this.profileViewStatusBarItemsContainer.appendChild(statusBarItems[i]);
    332     },
    333 
    334     showView: function(view)
    335     {
    336         this.showProfile(view.profile);
    337     },
    338 
    339     getProfileType: function(typeId)
    340     {
    341         return this._profileTypesByIdMap[typeId];
    342     },
    343 
    344     showProfileForURL: function(url)
    345     {
    346         var match = url.match(WebInspector.ProfileType.URLRegExp);
    347         if (!match)
    348             return;
    349         this.showProfile(this._profilesIdMap[this._makeKey(match[3], match[1])]);
    350     },
    351 
    352     updateProfileTypeButtons: function()
    353     {
    354         for (var typeId in this._profileTypeButtonsByIdMap) {
    355             var buttonElement = this._profileTypeButtonsByIdMap[typeId];
    356             var profileType = this.getProfileType(typeId);
    357             buttonElement.className = profileType.buttonStyle;
    358             buttonElement.title = profileType.buttonTooltip;
    359             // FIXME: Apply profileType.buttonCaption once captions are added to button controls.
    360         }
    361     },
    362 
    363     closeVisibleView: function()
    364     {
    365         if (this.visibleView)
    366             this.visibleView.hide();
    367         delete this.visibleView;
    368     },
    369 
    370     displayTitleForProfileLink: function(title, typeId)
    371     {
    372         title = unescape(title);
    373         if (title.indexOf(UserInitiatedProfileName) === 0) {
    374             title = WebInspector.UIString("Profile %d", title.substring(UserInitiatedProfileName.length + 1));
    375         } else {
    376             var titleKey = this._makeKey(title, typeId);
    377             if (!(titleKey in this._profileGroupsForLinks))
    378                 this._profileGroupsForLinks[titleKey] = 0;
    379 
    380             groupNumber = ++this._profileGroupsForLinks[titleKey];
    381 
    382             if (groupNumber > 2)
    383                 // The title is used in the console message announcing that a profile has started so it gets
    384                 // incremented twice as often as it's displayed
    385                 title += " " + WebInspector.UIString("Run %d", groupNumber / 2);
    386         }
    387 
    388         return title;
    389     },
    390 
    391     get searchableViews()
    392     {
    393         var views = [];
    394 
    395         const visibleView = this.visibleView;
    396         if (visibleView && visibleView.performSearch)
    397             views.push(visibleView);
    398 
    399         var profilesLength = this._profiles.length;
    400         for (var i = 0; i < profilesLength; ++i) {
    401             var profile = this._profiles[i];
    402             var view = profile.__profilesPanelProfileType.viewForProfile(profile);
    403             if (!view.performSearch || view === visibleView)
    404                 continue;
    405             views.push(view);
    406         }
    407 
    408         return views;
    409     },
    410 
    411     searchMatchFound: function(view, matches)
    412     {
    413         view.profile._profilesTreeElement.searchMatches = matches;
    414     },
    415 
    416     searchCanceled: function(startingNewSearch)
    417     {
    418         WebInspector.Panel.prototype.searchCanceled.call(this, startingNewSearch);
    419 
    420         if (!this._profiles)
    421             return;
    422 
    423         for (var i = 0; i < this._profiles.length; ++i) {
    424             var profile = this._profiles[i];
    425             profile._profilesTreeElement.searchMatches = 0;
    426         }
    427     },
    428 
    429     _updateInterface: function()
    430     {
    431         // FIXME: Replace ProfileType-specific button visibility changes by a single ProfileType-agnostic "combo-button" visibility change.
    432         if (InspectorBackend.profilerEnabled()) {
    433             this.enableToggleButton.title = WebInspector.UIString("Profiling enabled. Click to disable.");
    434             this.enableToggleButton.toggled = true;
    435             for (var typeId in this._profileTypeButtonsByIdMap)
    436                 this._profileTypeButtonsByIdMap[typeId].removeStyleClass("hidden");
    437             this.profileViewStatusBarItemsContainer.removeStyleClass("hidden");
    438             this.panelEnablerView.visible = false;
    439         } else {
    440             this.enableToggleButton.title = WebInspector.UIString("Profiling disabled. Click to enable.");
    441             this.enableToggleButton.toggled = false;
    442             for (var typeId in this._profileTypeButtonsByIdMap)
    443                 this._profileTypeButtonsByIdMap[typeId].addStyleClass("hidden");
    444             this.profileViewStatusBarItemsContainer.addStyleClass("hidden");
    445             this.panelEnablerView.visible = true;
    446         }
    447     },
    448 
    449     _enableProfiling: function()
    450     {
    451         if (InspectorBackend.profilerEnabled())
    452             return;
    453         this._toggleProfiling(this.panelEnablerView.alwaysEnabled);
    454     },
    455 
    456     _toggleProfiling: function(optionalAlways)
    457     {
    458         if (InspectorBackend.profilerEnabled())
    459             InspectorBackend.disableProfiler(true);
    460         else
    461             InspectorBackend.enableProfiler(!!optionalAlways);
    462     },
    463 
    464     _populateProfiles: function()
    465     {
    466         var sidebarTreeChildrenCount = this.sidebarTree.children.length;
    467         for (var i = 0; i < sidebarTreeChildrenCount; ++i) {
    468             var treeElement = this.sidebarTree.children[i];
    469             if (treeElement.children.length)
    470                 return;
    471         }
    472 
    473         function populateCallback(profileHeaders) {
    474             profileHeaders.sort(function(a, b) { return a.uid - b.uid; });
    475             var profileHeadersLength = profileHeaders.length;
    476             for (var i = 0; i < profileHeadersLength; ++i)
    477                 WebInspector.addProfileHeader(profileHeaders[i]);
    478         }
    479 
    480         var callId = WebInspector.Callback.wrap(populateCallback);
    481         InspectorBackend.getProfileHeaders(callId);
    482 
    483         delete this._shouldPopulateProfiles;
    484     },
    485 
    486     updateMainViewWidth: function(width)
    487     {
    488         this.welcomeView.element.style.left = width + "px";
    489         this.profileViews.style.left = width + "px";
    490         this.profileViewStatusBarItemsContainer.style.left = width + "px";
    491         this.resize();
    492     }
    493 }
    494 
    495 WebInspector.ProfilesPanel.prototype.__proto__ = WebInspector.Panel.prototype;
    496 
    497 WebInspector.ProfileSidebarTreeElement = function(profile)
    498 {
    499     this.profile = profile;
    500 
    501     if (this.profile.title.indexOf(UserInitiatedProfileName) === 0)
    502         this._profileNumber = this.profile.title.substring(UserInitiatedProfileName.length + 1);
    503 
    504     WebInspector.SidebarTreeElement.call(this, "profile-sidebar-tree-item", "", "", profile, false);
    505 
    506     this.refreshTitles();
    507 }
    508 
    509 WebInspector.ProfileSidebarTreeElement.prototype = {
    510     onselect: function()
    511     {
    512         WebInspector.panels.profiles.showProfile(this.profile);
    513     },
    514 
    515     get mainTitle()
    516     {
    517         if (this._mainTitle)
    518             return this._mainTitle;
    519         if (this.profile.title.indexOf(UserInitiatedProfileName) === 0)
    520             return WebInspector.UIString("Profile %d", this._profileNumber);
    521         return this.profile.title;
    522     },
    523 
    524     set mainTitle(x)
    525     {
    526         this._mainTitle = x;
    527         this.refreshTitles();
    528     },
    529 
    530     get subtitle()
    531     {
    532         // There is no subtitle.
    533     },
    534 
    535     set subtitle(x)
    536     {
    537         // Can't change subtitle.
    538     },
    539 
    540     set searchMatches(matches)
    541     {
    542         if (!matches) {
    543             if (!this.bubbleElement)
    544                 return;
    545             this.bubbleElement.removeStyleClass("search-matches");
    546             this.bubbleText = "";
    547             return;
    548         }
    549 
    550         this.bubbleText = matches;
    551         this.bubbleElement.addStyleClass("search-matches");
    552     }
    553 }
    554 
    555 WebInspector.ProfileSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype;
    556 
    557 WebInspector.ProfileGroupSidebarTreeElement = function(title, subtitle)
    558 {
    559     WebInspector.SidebarTreeElement.call(this, "profile-group-sidebar-tree-item", title, subtitle, null, true);
    560 }
    561 
    562 WebInspector.ProfileGroupSidebarTreeElement.prototype = {
    563     onselect: function()
    564     {
    565         WebInspector.panels.profiles.showProfile(this.children[this.children.length - 1].profile);
    566     }
    567 }
    568 
    569 WebInspector.ProfileGroupSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype;
    570 
    571 WebInspector.didGetProfileHeaders = WebInspector.Callback.processCallback;
    572 WebInspector.didGetProfile = WebInspector.Callback.processCallback;
    573