• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
26const UserInitiatedProfileName = "org.webkit.profiles.user-initiated";
27
28WebInspector.ProfileType = function(id, name)
29{
30    this._id = id;
31    this._name = name;
32}
33
34WebInspector.ProfileType.URLRegExp = /webkit-profile:\/\/(.+)\/(.+)#([0-9]+)/;
35
36WebInspector.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
91WebInspector.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
126WebInspector.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
495WebInspector.ProfilesPanel.prototype.__proto__ = WebInspector.Panel.prototype;
496
497WebInspector.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
509WebInspector.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
555WebInspector.ProfileSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype;
556
557WebInspector.ProfileGroupSidebarTreeElement = function(title, subtitle)
558{
559    WebInspector.SidebarTreeElement.call(this, "profile-group-sidebar-tree-item", title, subtitle, null, true);
560}
561
562WebInspector.ProfileGroupSidebarTreeElement.prototype = {
563    onselect: function()
564    {
565        WebInspector.panels.profiles.showProfile(this.children[this.children.length - 1].profile);
566    }
567}
568
569WebInspector.ProfileGroupSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype;
570
571WebInspector.didGetProfileHeaders = WebInspector.Callback.processCallback;
572WebInspector.didGetProfile = WebInspector.Callback.processCallback;
573