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