911b1daf6261296e3e59661c433129568b43dd4e
[WebKit-https.git] / WebCore / inspector / front-end / ProfilesPanel.js
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, "profiles");
94
95     this.createSidebar();
96
97     this._profileTypesByIdMap = {};
98     this._profileTypeButtonsByIdMap = {};
99
100     var panelEnablerHeading = WebInspector.UIString("You need to enable profiling before you can use the Profiles panel.");
101     var panelEnablerDisclaimer = WebInspector.UIString("Enabling profiling will make scripts run slower.");
102     var panelEnablerButton = WebInspector.UIString("Enable Profiling");
103     this.panelEnablerView = new WebInspector.PanelEnablerView("profiles", panelEnablerHeading, panelEnablerDisclaimer, panelEnablerButton);
104     this.panelEnablerView.addEventListener("enable clicked", this._enableProfiling, this);
105
106     this.element.appendChild(this.panelEnablerView.element);
107
108     this.profileViews = document.createElement("div");
109     this.profileViews.id = "profile-views";
110     this.element.appendChild(this.profileViews);
111
112     this.enableToggleButton = new WebInspector.StatusBarButton("", "enable-toggle-status-bar-item");
113     this.enableToggleButton.addEventListener("click", this._toggleProfiling.bind(this), false);
114
115     this.clearResultsButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear CPU profiles."), "clear-status-bar-item");
116     this.clearResultsButton.addEventListener("click", this._clearProfiles.bind(this), false);
117
118     this.profileViewStatusBarItemsContainer = document.createElement("div");
119     this.profileViewStatusBarItemsContainer.id = "profile-view-status-bar-items";
120
121     this.welcomeView = new WebInspector.WelcomeView("profiles", WebInspector.UIString("Welcome to the Profiles panel"));
122     this.element.appendChild(this.welcomeView.element);
123
124     this._profiles = [];
125     this._profilerEnabled = Preferences.profilerAlwaysEnabled;
126     this._reset();
127 }
128
129 WebInspector.ProfilesPanel.prototype = {
130     get toolbarItemLabel()
131     {
132         return WebInspector.UIString("Profiles");
133     },
134
135     get statusBarItems()
136     {
137         function clickHandler(profileType, buttonElement)
138         {
139             profileType.buttonClicked.call(profileType);
140             this.updateProfileTypeButtons();
141         }
142
143         var items = [this.enableToggleButton.element];
144         // FIXME: Generate a single "combo-button".
145         for (var typeId in this._profileTypesByIdMap) {
146             var profileType = this.getProfileType(typeId);
147             if (profileType.buttonStyle) {
148                 var button = new WebInspector.StatusBarButton(profileType.buttonTooltip, profileType.buttonStyle, profileType.buttonCaption);
149                 this._profileTypeButtonsByIdMap[typeId] = button.element;
150                 button.element.addEventListener("click", clickHandler.bind(this, profileType, button.element), false);
151                 items.push(button.element);
152             }
153         }
154         items.push(this.clearResultsButton.element, this.profileViewStatusBarItemsContainer);
155         return items;
156     },
157
158     show: function()
159     {
160         WebInspector.Panel.prototype.show.call(this);
161         if (this._shouldPopulateProfiles)
162             this._populateProfiles();
163     },
164
165     populateInterface: function()
166     {
167         this._reset();
168         if (this.visible)
169             this._populateProfiles();
170         else
171             this._shouldPopulateProfiles = true;
172     },
173
174     profilerWasEnabled: function()
175     {
176         if (this._profilerEnabled)
177             return;
178
179         this._profilerEnabled = true;
180         this.populateInterface();
181     },
182
183     profilerWasDisabled: function()
184     {
185         if (!this._profilerEnabled)
186             return;
187
188         this._profilerEnabled = false;
189         this._reset();
190     },
191
192     resetProfiles: function()
193     {
194         this._reset();
195     },
196
197     _reset: function()
198     {
199         for (var i = 0; i < this._profiles.length; ++i)
200             delete this._profiles[i]._profileView;
201         delete this.visibleView;
202
203         delete this.currentQuery;
204         this.searchCanceled();
205
206         this._profiles = [];
207         this._profilesIdMap = {};
208         this._profileGroups = {};
209         this._profileGroupsForLinks = {}
210
211         this.sidebarTreeElement.removeStyleClass("some-expandable");
212
213         for (var typeId in this._profileTypesByIdMap)
214             this.getProfileType(typeId).treeElement.removeChildren();
215
216         this.profileViews.removeChildren();
217
218         this.profileViewStatusBarItemsContainer.removeChildren();
219
220         this._updateInterface();
221         this.welcomeView.show();
222     },
223
224     _clearProfiles: function()
225     {
226         InspectorBackend.clearProfiles();
227         this._reset();
228     },
229
230     registerProfileType: function(profileType)
231     {
232         this._profileTypesByIdMap[profileType.id] = profileType;
233         profileType.treeElement = new WebInspector.SidebarSectionTreeElement(profileType.name, null, true);
234         this.sidebarTree.appendChild(profileType.treeElement);
235         profileType.treeElement.expand();
236         this._addWelcomeMessage(profileType);
237     },
238
239     _addWelcomeMessage: function(profileType)
240     {
241         var message = profileType.welcomeMessage;
242         // Message text is supposed to have a '%s' substring as a placeholder
243         // for a status bar button. If it is there, we split the message in two
244         // parts, and insert the button between them.
245         var buttonPos = message.indexOf("%s");
246         if (buttonPos > -1) {
247             var container = document.createDocumentFragment();
248             var part1 = document.createElement("span");
249             part1.innerHTML = message.substr(0, buttonPos);
250             container.appendChild(part1);
251      
252             var button = new WebInspector.StatusBarButton(profileType.buttonTooltip, profileType.buttonStyle, profileType.buttonCaption);
253             container.appendChild(button.element);
254        
255             var part2 = document.createElement("span");
256             part2.innerHTML = message.substr(buttonPos + 2);
257             container.appendChild(part2);
258             this.welcomeView.addMessage(container);
259         } else
260             this.welcomeView.addMessage(message);
261     },
262
263     _makeKey: function(text, profileTypeId)
264     {
265         return escape(text) + '/' + escape(profileTypeId);
266     },
267
268     addProfileHeader: function(profile)
269     {
270         var typeId = profile.typeId;
271         var profileType = this.getProfileType(typeId);
272         var sidebarParent = profileType.treeElement;
273         var small = false;
274         var alternateTitle;
275
276         profile.__profilesPanelProfileType = profileType;
277         this._profiles.push(profile);
278         this._profilesIdMap[this._makeKey(profile.uid, typeId)] = profile;
279
280         if (profile.title.indexOf(UserInitiatedProfileName) !== 0) {
281             var profileTitleKey = this._makeKey(profile.title, typeId);
282             if (!(profileTitleKey in this._profileGroups))
283                 this._profileGroups[profileTitleKey] = [];
284
285             var group = this._profileGroups[profileTitleKey];
286             group.push(profile);
287
288             if (group.length === 2) {
289                 // Make a group TreeElement now that there are 2 profiles.
290                 group._profilesTreeElement = new WebInspector.ProfileGroupSidebarTreeElement(profile.title);
291
292                 // Insert at the same index for the first profile of the group.
293                 var index = sidebarParent.children.indexOf(group[0]._profilesTreeElement);
294                 sidebarParent.insertChild(group._profilesTreeElement, index);
295
296                 // Move the first profile to the group.
297                 var selected = group[0]._profilesTreeElement.selected;
298                 sidebarParent.removeChild(group[0]._profilesTreeElement);
299                 group._profilesTreeElement.appendChild(group[0]._profilesTreeElement);
300                 if (selected) {
301                     group[0]._profilesTreeElement.select();
302                     group[0]._profilesTreeElement.reveal();
303                 }
304
305                 group[0]._profilesTreeElement.small = true;
306                 group[0]._profilesTreeElement.mainTitle = WebInspector.UIString("Run %d", 1);
307
308                 this.sidebarTreeElement.addStyleClass("some-expandable");
309             }
310
311             if (group.length >= 2) {
312                 sidebarParent = group._profilesTreeElement;
313                 alternateTitle = WebInspector.UIString("Run %d", group.length);
314                 small = true;
315             }
316         }
317
318         var profileTreeElement = profileType.createSidebarTreeElementForProfile(profile);
319         profileTreeElement.small = small;
320         if (alternateTitle)
321             profileTreeElement.mainTitle = alternateTitle;
322         profile._profilesTreeElement = profileTreeElement;
323
324         sidebarParent.appendChild(profileTreeElement);
325         if (!profile.isTemporary) {
326             this.welcomeView.hide();
327             if (!this.visibleView)
328                 this.showProfile(profile);
329         }
330     },
331
332     removeProfileHeader: function(profile)
333     {
334         var typeId = profile.typeId;
335         var profileType = this.getProfileType(typeId);
336         var sidebarParent = profileType.treeElement;
337
338         for (var i = 0; i < this._profiles.length; ++i) {
339             if (this._profiles[i].uid === profile.uid) {
340                 profile = this._profiles[i];
341                 this._profiles.splice(i, 1);
342                 break;
343             }
344         }
345         delete this._profilesIdMap[this._makeKey(profile.uid, typeId)];
346
347         var profileTitleKey = this._makeKey(profile.title, typeId);
348         delete this._profileGroups[profileTitleKey];
349
350         sidebarParent.removeChild(profile._profilesTreeElement);
351
352         if (!profile.isTemporary)
353             InspectorBackend.removeProfile(profile.uid);
354
355         // No other item will be selected if there aren't any other profiles, so
356         // make sure that view gets cleared when the last profile is removed.
357         if (!this._profiles.length)
358             this.closeVisibleView();
359     },
360
361     showProfile: function(profile)
362     {
363         if (!profile || profile.isTemporary)
364             return;
365
366         this.closeVisibleView();
367
368         var view = profile.__profilesPanelProfileType.viewForProfile(profile);
369
370         view.show(this.profileViews);
371
372         profile._profilesTreeElement.select(true);
373         profile._profilesTreeElement.reveal();
374
375         this.visibleView = view;
376
377         this.profileViewStatusBarItemsContainer.removeChildren();
378
379         var statusBarItems = view.statusBarItems;
380         for (var i = 0; i < statusBarItems.length; ++i)
381             this.profileViewStatusBarItemsContainer.appendChild(statusBarItems[i]);
382     },
383
384     showView: function(view)
385     {
386         this.showProfile(view.profile);
387     },
388
389     getProfileType: function(typeId)
390     {
391         return this._profileTypesByIdMap[typeId];
392     },
393
394     showProfileForURL: function(url)
395     {
396         var match = url.match(WebInspector.ProfileType.URLRegExp);
397         if (!match)
398             return;
399         this.showProfile(this._profilesIdMap[this._makeKey(match[3], match[1])]);
400     },
401
402     updateProfileTypeButtons: function()
403     {
404         for (var typeId in this._profileTypeButtonsByIdMap) {
405             var buttonElement = this._profileTypeButtonsByIdMap[typeId];
406             var profileType = this.getProfileType(typeId);
407             buttonElement.className = profileType.buttonStyle;
408             buttonElement.title = profileType.buttonTooltip;
409             // FIXME: Apply profileType.buttonCaption once captions are added to button controls.
410         }
411     },
412
413     closeVisibleView: function()
414     {
415         if (this.visibleView)
416             this.visibleView.hide();
417         delete this.visibleView;
418     },
419
420     displayTitleForProfileLink: function(title, typeId)
421     {
422         title = unescape(title);
423         if (title.indexOf(UserInitiatedProfileName) === 0) {
424             title = WebInspector.UIString("Profile %d", title.substring(UserInitiatedProfileName.length + 1));
425         } else {
426             var titleKey = this._makeKey(title, typeId);
427             if (!(titleKey in this._profileGroupsForLinks))
428                 this._profileGroupsForLinks[titleKey] = 0;
429
430             groupNumber = ++this._profileGroupsForLinks[titleKey];
431
432             if (groupNumber > 2)
433                 // The title is used in the console message announcing that a profile has started so it gets
434                 // incremented twice as often as it's displayed
435                 title += " " + WebInspector.UIString("Run %d", groupNumber / 2);
436         }
437         
438         return title;
439     },
440
441     get searchableViews()
442     {
443         var views = [];
444
445         const visibleView = this.visibleView;
446         if (visibleView && visibleView.performSearch)
447             views.push(visibleView);
448
449         var profilesLength = this._profiles.length;
450         for (var i = 0; i < profilesLength; ++i) {
451             var profile = this._profiles[i];
452             var view = profile.__profilesPanelProfileType.viewForProfile(profile);
453             if (!view.performSearch || view === visibleView)
454                 continue;
455             views.push(view);
456         }
457
458         return views;
459     },
460
461     searchMatchFound: function(view, matches)
462     {
463         view.profile._profilesTreeElement.searchMatches = matches;
464     },
465
466     searchCanceled: function(startingNewSearch)
467     {
468         WebInspector.Panel.prototype.searchCanceled.call(this, startingNewSearch);
469
470         if (!this._profiles)
471             return;
472
473         for (var i = 0; i < this._profiles.length; ++i) {
474             var profile = this._profiles[i];
475             profile._profilesTreeElement.searchMatches = 0;
476         }
477     },
478
479     _updateInterface: function()
480     {
481         // FIXME: Replace ProfileType-specific button visibility changes by a single ProfileType-agnostic "combo-button" visibility change.
482         if (this._profilerEnabled) {
483             this.enableToggleButton.title = WebInspector.UIString("Profiling enabled. Click to disable.");
484             this.enableToggleButton.toggled = true;
485             for (var typeId in this._profileTypeButtonsByIdMap)
486                 this._profileTypeButtonsByIdMap[typeId].removeStyleClass("hidden");
487             this.profileViewStatusBarItemsContainer.removeStyleClass("hidden");
488             this.clearResultsButton.element.removeStyleClass("hidden");
489             this.panelEnablerView.visible = false;
490         } else {
491             this.enableToggleButton.title = WebInspector.UIString("Profiling disabled. Click to enable.");
492             this.enableToggleButton.toggled = false;
493             for (var typeId in this._profileTypeButtonsByIdMap)
494                 this._profileTypeButtonsByIdMap[typeId].addStyleClass("hidden");
495             this.profileViewStatusBarItemsContainer.addStyleClass("hidden");
496             this.clearResultsButton.element.addStyleClass("hidden");
497             this.panelEnablerView.visible = true;
498         }
499     },
500
501     _enableProfiling: function()
502     {
503         if (this._profilerEnabled)
504             return;
505         this._toggleProfiling(this.panelEnablerView.alwaysEnabled);
506     },
507
508     _toggleProfiling: function(optionalAlways)
509     {
510         if (this._profilerEnabled)
511             InspectorBackend.disableProfiler(true);
512         else
513             InspectorBackend.enableProfiler(!!optionalAlways);
514     },
515
516     _populateProfiles: function()
517     {
518         var sidebarTreeChildrenCount = this.sidebarTree.children.length;
519         for (var i = 0; i < sidebarTreeChildrenCount; ++i) {
520             var treeElement = this.sidebarTree.children[i];
521             if (treeElement.children.length)
522                 return;
523         }
524
525         function populateCallback(profileHeaders) {
526             profileHeaders.sort(function(a, b) { return a.uid - b.uid; });
527             var profileHeadersLength = profileHeaders.length;
528             for (var i = 0; i < profileHeadersLength; ++i)
529                 WebInspector.addProfileHeader(profileHeaders[i]);
530         }
531
532         var callId = WebInspector.Callback.wrap(populateCallback);
533         InspectorBackend.getProfileHeaders(callId);
534
535         delete this._shouldPopulateProfiles;
536     },
537
538     updateMainViewWidth: function(width)
539     {
540         this.welcomeView.element.style.left = width + "px";
541         this.profileViews.style.left = width + "px";
542         this.profileViewStatusBarItemsContainer.style.left = width + "px";
543         this.resize();
544     }
545 }
546
547 WebInspector.ProfilesPanel.prototype.__proto__ = WebInspector.Panel.prototype;
548
549 WebInspector.ProfileSidebarTreeElement = function(profile)
550 {
551     this.profile = profile;
552
553     if (this.profile.title.indexOf(UserInitiatedProfileName) === 0)
554         this._profileNumber = this.profile.title.substring(UserInitiatedProfileName.length + 1);
555
556     WebInspector.SidebarTreeElement.call(this, "profile-sidebar-tree-item", "", "", profile, false);
557
558     this.refreshTitles();
559 }
560
561 WebInspector.ProfileSidebarTreeElement.prototype = {
562     onselect: function()
563     {
564         this.treeOutline.panel.showProfile(this.profile);
565     },
566
567     ondelete: function()
568     {
569         this.treeOutline.panel.removeProfileHeader(this.profile);
570         return true;
571     },
572
573     get mainTitle()
574     {
575         if (this._mainTitle)
576             return this._mainTitle;
577         if (this.profile.title.indexOf(UserInitiatedProfileName) === 0)
578             return WebInspector.UIString("Profile %d", this._profileNumber);
579         return this.profile.title;
580     },
581
582     set mainTitle(x)
583     {
584         this._mainTitle = x;
585         this.refreshTitles();
586     },
587
588     get subtitle()
589     {
590         // There is no subtitle.
591     },
592
593     set subtitle(x)
594     {
595         // Can't change subtitle.
596     },
597
598     set searchMatches(matches)
599     {
600         if (!matches) {
601             if (!this.bubbleElement)
602                 return;
603             this.bubbleElement.removeStyleClass("search-matches");
604             this.bubbleText = "";
605             return;
606         }
607
608         this.bubbleText = matches;
609         this.bubbleElement.addStyleClass("search-matches");
610     }
611 }
612
613 WebInspector.ProfileSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype;
614
615 WebInspector.ProfileGroupSidebarTreeElement = function(title, subtitle)
616 {
617     WebInspector.SidebarTreeElement.call(this, "profile-group-sidebar-tree-item", title, subtitle, null, true);
618 }
619
620 WebInspector.ProfileGroupSidebarTreeElement.prototype = {
621     onselect: function()
622     {
623         if (this.children.length > 0)
624             WebInspector.panels.profiles.showProfile(this.children[this.children.length - 1].profile);
625     }
626 }
627
628 WebInspector.ProfileGroupSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype;
629
630 WebInspector.didGetProfileHeaders = WebInspector.Callback.processCallback;
631 WebInspector.didGetProfile = WebInspector.Callback.processCallback;