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
28/**
29 * @constructor
30 * @extends {WebInspector.Object}
31 * @param {string} id
32 * @param {string} name
33 */
34WebInspector.ProfileType = function(id, name)
35{
36    this._id = id;
37    this._name = name;
38    /** @type {!Array.<!WebInspector.ProfileHeader>} */
39    this._profiles = [];
40    this._profilesIdMap = {};
41    /** @type {WebInspector.SidebarSectionTreeElement} */
42    this.treeElement = null;
43}
44
45WebInspector.ProfileType.Events = {
46    AddProfileHeader: "add-profile-header",
47    RemoveProfileHeader: "remove-profile-header",
48    ProgressUpdated: "progress-updated",
49    ViewUpdated: "view-updated"
50}
51
52WebInspector.ProfileType.prototype = {
53    statusBarItems: function()
54    {
55        return [];
56    },
57
58    get buttonTooltip()
59    {
60        return "";
61    },
62
63    get id()
64    {
65        return this._id;
66    },
67
68    get treeItemTitle()
69    {
70        return this._name;
71    },
72
73    get name()
74    {
75        return this._name;
76    },
77
78    /**
79     * @return {boolean}
80     */
81    buttonClicked: function()
82    {
83        return false;
84    },
85
86    get description()
87    {
88        return "";
89    },
90
91    /**
92     * @return {boolean}
93     */
94    isInstantProfile: function()
95    {
96        return false;
97    },
98
99    /**
100     * @return {!Array.<!WebInspector.ProfileHeader>}
101     */
102    getProfiles: function()
103    {
104        return this._profiles.filter(function(profile) { return !profile.isTemporary; });
105    },
106
107    /**
108     * @return {Element}
109     */
110    decorationElement: function()
111    {
112        return null;
113    },
114
115    /**
116     * @nosideeffects
117     * @param {number} uid
118     * @return {WebInspector.ProfileHeader}
119     */
120    getProfile: function(uid)
121    {
122        return this._profilesIdMap[this._makeKey(uid)];
123    },
124
125    // Must be implemented by subclasses.
126    /**
127     * @param {string=} title
128     * @return {!WebInspector.ProfileHeader}
129     */
130    createTemporaryProfile: function(title)
131    {
132        throw new Error("Needs implemented.");
133    },
134
135    /**
136     * @param {ProfilerAgent.ProfileHeader} profile
137     * @return {!WebInspector.ProfileHeader}
138     */
139    createProfile: function(profile)
140    {
141        throw new Error("Not supported for " + this._name + " profiles.");
142    },
143
144    /**
145     * @nosideeffects
146     * @param {number} id
147     * @return {string}
148     */
149    _makeKey: function(id)
150    {
151        return id + '/' + escape(this.id);
152    },
153
154    /**
155     * @param {!WebInspector.ProfileHeader} profile
156     */
157    addProfile: function(profile)
158    {
159        this._profiles.push(profile);
160        // FIXME: uid only based key should be enough.
161        this._profilesIdMap[this._makeKey(profile.uid)] = profile;
162        this.dispatchEventToListeners(WebInspector.ProfileType.Events.AddProfileHeader, profile);
163    },
164
165    /**
166     * @param {!WebInspector.ProfileHeader} profile
167     */
168    removeProfile: function(profile)
169    {
170        for (var i = 0; i < this._profiles.length; ++i) {
171            if (this._profiles[i].uid === profile.uid) {
172                this._profiles.splice(i, 1);
173                break;
174            }
175        }
176        delete this._profilesIdMap[this._makeKey(profile.uid)];
177    },
178
179    /**
180     * @nosideeffects
181     * @return {WebInspector.ProfileHeader}
182     */
183    findTemporaryProfile: function()
184    {
185        for (var i = 0; i < this._profiles.length; ++i) {
186            if (this._profiles[i].isTemporary)
187                return this._profiles[i];
188        }
189        return null;
190    },
191
192    _reset: function()
193    {
194        var profiles = this._profiles.slice(0);
195        for (var i = 0; i < profiles.length; ++i) {
196            var profile = profiles[i];
197            var view = profile.existingView();
198            if (view) {
199                view.detach();
200                if ("dispose" in view)
201                    view.dispose();
202            }
203            this.dispatchEventToListeners(WebInspector.ProfileType.Events.RemoveProfileHeader, profile);
204        }
205        this.treeElement.removeChildren();
206        this._profiles = [];
207        this._profilesIdMap = {};
208    },
209
210    /**
211     * @param {function(this:WebInspector.ProfileType, ?string, !Array.<!ProfilerAgent.ProfileHeader>)} populateCallback
212     */
213    _requestProfilesFromBackend: function(populateCallback)
214    {
215    },
216
217    _populateProfiles: function()
218    {
219        /**
220         * @param {?string} error
221         * @param {!Array.<!ProfilerAgent.ProfileHeader>} profileHeaders
222         */
223        function populateCallback(error, profileHeaders) {
224            if (error)
225                return;
226            profileHeaders.sort(function(a, b) { return a.uid - b.uid; });
227            var count = profileHeaders.length;
228            for (var i = 0; i < count; ++i)
229                this.addProfile(this.createProfile(profileHeaders[i]));
230        }
231        this._requestProfilesFromBackend(populateCallback.bind(this));
232    },
233
234    __proto__: WebInspector.Object.prototype
235}
236
237/**
238 * @constructor
239 * @param {!WebInspector.ProfileType} profileType
240 * @param {string} title
241 * @param {number=} uid
242 */
243WebInspector.ProfileHeader = function(profileType, title, uid)
244{
245    this._profileType = profileType;
246    this.title = title;
247    this.isTemporary = uid === undefined;
248    this.uid = this.isTemporary ? -1 : uid;
249    this._fromFile = false;
250}
251
252WebInspector.ProfileHeader.prototype = {
253    /**
254     * @return {!WebInspector.ProfileType}
255     */
256    profileType: function()
257    {
258        return this._profileType;
259    },
260
261    /**
262     * Must be implemented by subclasses.
263     * @return {WebInspector.ProfileSidebarTreeElement}
264     */
265    createSidebarTreeElement: function()
266    {
267        throw new Error("Needs implemented.");
268    },
269
270    /**
271     * @return {?WebInspector.View}
272     */
273    existingView: function()
274    {
275        return this._view;
276    },
277
278    /**
279     * @param {!WebInspector.ProfilesPanel} panel
280     * @return {!WebInspector.View}
281     */
282    view: function(panel)
283    {
284        if (!this._view)
285            this._view = this.createView(panel);
286        return this._view;
287    },
288
289    /**
290     * @param {!WebInspector.ProfilesPanel} panel
291     * @return {!WebInspector.View}
292     */
293    createView: function(panel)
294    {
295        throw new Error("Not implemented.");
296    },
297
298    dispose: function()
299    {
300    },
301
302    /**
303     * @param {Function} callback
304     */
305    load: function(callback)
306    {
307    },
308
309    /**
310     * @return {boolean}
311     */
312    canSaveToFile: function()
313    {
314        return false;
315    },
316
317    saveToFile: function()
318    {
319        throw new Error("Needs implemented");
320    },
321
322    /**
323     * @param {File} file
324     */
325    loadFromFile: function(file)
326    {
327        throw new Error("Needs implemented");
328    },
329
330    /**
331     * @return {boolean}
332     */
333    fromFile: function()
334    {
335        return this._fromFile;
336    }
337}
338
339/**
340 * @constructor
341 * @extends {WebInspector.Panel}
342 * @implements {WebInspector.ContextMenu.Provider}
343 * @param {string=} name
344 * @param {WebInspector.ProfileType=} type
345 */
346WebInspector.ProfilesPanel = function(name, type)
347{
348    // If the name is not specified the ProfilesPanel works in multi-profile mode.
349    var singleProfileMode = typeof name !== "undefined";
350    name = name || "profiles";
351    WebInspector.Panel.call(this, name);
352    this.registerRequiredCSS("panelEnablerView.css");
353    this.registerRequiredCSS("heapProfiler.css");
354    this.registerRequiredCSS("profilesPanel.css");
355
356    this.createSidebarViewWithTree();
357
358    this.profilesItemTreeElement = new WebInspector.ProfilesSidebarTreeElement(this);
359    this.sidebarTree.appendChild(this.profilesItemTreeElement);
360
361    this._singleProfileMode = singleProfileMode;
362    this._profileTypesByIdMap = {};
363
364    var panelEnablerHeading = WebInspector.UIString("You need to enable profiling before you can use the Profiles panel.");
365    var panelEnablerDisclaimer = WebInspector.UIString("Enabling profiling will make scripts run slower.");
366    var panelEnablerButton = WebInspector.UIString("Enable Profiling");
367    this.panelEnablerView = new WebInspector.PanelEnablerView(name, panelEnablerHeading, panelEnablerDisclaimer, panelEnablerButton);
368    this.panelEnablerView.addEventListener("enable clicked", this.enableProfiler, this);
369
370    this.profileViews = document.createElement("div");
371    this.profileViews.id = "profile-views";
372    this.splitView.mainElement.appendChild(this.profileViews);
373
374    this._statusBarButtons = [];
375
376    this.enableToggleButton = new WebInspector.StatusBarButton("", "enable-toggle-status-bar-item");
377    if (Capabilities.profilerCausesRecompilation) {
378        this._statusBarButtons.push(this.enableToggleButton);
379        this.enableToggleButton.addEventListener("click", this._onToggleProfiling, this);
380    }
381    this.recordButton = new WebInspector.StatusBarButton("", "record-profile-status-bar-item");
382    this.recordButton.addEventListener("click", this.toggleRecordButton, this);
383    this._statusBarButtons.push(this.recordButton);
384
385    this.clearResultsButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear all profiles."), "clear-status-bar-item");
386    this.clearResultsButton.addEventListener("click", this._clearProfiles, this);
387    this._statusBarButtons.push(this.clearResultsButton);
388
389    this._profileTypeStatusBarItemsContainer = document.createElement("div");
390    this._profileTypeStatusBarItemsContainer.className = "status-bar-items";
391
392    this._profileViewStatusBarItemsContainer = document.createElement("div");
393    this._profileViewStatusBarItemsContainer.className = "status-bar-items";
394
395    this._profilerEnabled = !Capabilities.profilerCausesRecompilation;
396
397    if (singleProfileMode) {
398        this._launcherView = this._createLauncherView();
399        this._registerProfileType(/** @type {!WebInspector.ProfileType} */ (type));
400        this._selectedProfileType = type;
401        this._updateProfileTypeSpecificUI();
402    } else {
403        this._launcherView = new WebInspector.MultiProfileLauncherView(this);
404        this._launcherView.addEventListener(WebInspector.MultiProfileLauncherView.EventTypes.ProfileTypeSelected, this._onProfileTypeSelected, this);
405
406        this._registerProfileType(new WebInspector.CPUProfileType());
407        if (!WebInspector.WorkerManager.isWorkerFrontend())
408            this._registerProfileType(new WebInspector.CSSSelectorProfileType());
409        if (Capabilities.heapProfilerPresent)
410            this._registerProfileType(new WebInspector.HeapSnapshotProfileType());
411        if (!WebInspector.WorkerManager.isWorkerFrontend() && WebInspector.experimentsSettings.canvasInspection.isEnabled())
412            this._registerProfileType(new WebInspector.CanvasProfileType());
413    }
414
415    this._reset();
416
417    this._createFileSelectorElement();
418    this.element.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), true);
419
420    WebInspector.ContextMenu.registerProvider(this);
421}
422
423WebInspector.ProfilesPanel.prototype = {
424    _createFileSelectorElement: function()
425    {
426        if (this._fileSelectorElement)
427            this.element.removeChild(this._fileSelectorElement);
428        this._fileSelectorElement = WebInspector.createFileSelectorElement(this._loadFromFile.bind(this));
429        this.element.appendChild(this._fileSelectorElement);
430    },
431
432    /**
433     * @return {!WebInspector.ProfileLauncherView}
434     */
435    _createLauncherView: function()
436    {
437        return new WebInspector.ProfileLauncherView(this);
438    },
439
440    /**
441     * @param {!File} file
442     */
443    _loadFromFile: function(file)
444    {
445        if (!file.name.endsWith(".heapsnapshot")) {
446            WebInspector.log(WebInspector.UIString("Only heap snapshots from files with extension '.heapsnapshot' can be loaded."));
447            return;
448        }
449
450        var profileType = this.getProfileType(WebInspector.HeapSnapshotProfileType.TypeId);
451        if (!!profileType.findTemporaryProfile()) {
452            WebInspector.log(WebInspector.UIString("Can't load profile when other profile is recording."));
453            return;
454        }
455
456        var temporaryProfile = profileType.createTemporaryProfile(WebInspector.ProfilesPanelDescriptor.UserInitiatedProfileName + "." + file.name);
457        profileType.addProfile(temporaryProfile);
458
459        temporaryProfile._fromFile = true;
460        temporaryProfile.loadFromFile(file);
461        this._createFileSelectorElement();
462    },
463
464    statusBarItems: function()
465    {
466        return this._statusBarButtons.select("element").concat(this._profileTypeStatusBarItemsContainer, this._profileViewStatusBarItemsContainer);
467    },
468
469    toggleRecordButton: function()
470    {
471        var isProfiling = this._selectedProfileType.buttonClicked();
472        this.setRecordingProfile(this._selectedProfileType.id, isProfiling);
473    },
474
475    _populateAllProfiles: function()
476    {
477        if (!this._profilerEnabled || this._profilesWereRequested)
478            return;
479        this._profilesWereRequested = true;
480        for (var typeId in this._profileTypesByIdMap)
481            this._profileTypesByIdMap[typeId]._populateProfiles();
482    },
483
484    wasShown: function()
485    {
486        WebInspector.Panel.prototype.wasShown.call(this);
487        this._populateAllProfiles();
488    },
489
490    _profilerWasEnabled: function()
491    {
492        if (this._profilerEnabled)
493            return;
494
495        this._profilerEnabled = true;
496
497        this._reset();
498        if (this.isShowing())
499            this._populateAllProfiles();
500    },
501
502    _profilerWasDisabled: function()
503    {
504        if (!this._profilerEnabled)
505            return;
506
507        this._profilerEnabled = false;
508        this._reset();
509    },
510
511    /**
512     * @param {WebInspector.Event} event
513     */
514    _onProfileTypeSelected: function(event)
515    {
516        this._selectedProfileType = /** @type {!WebInspector.ProfileType} */ (event.data);
517        this._updateProfileTypeSpecificUI();
518    },
519
520    _updateProfileTypeSpecificUI: function()
521    {
522        this.recordButton.title = this._selectedProfileType.buttonTooltip;
523
524        this._profileTypeStatusBarItemsContainer.removeChildren();
525        var statusBarItems = this._selectedProfileType.statusBarItems();
526        if (statusBarItems) {
527            for (var i = 0; i < statusBarItems.length; ++i)
528                this._profileTypeStatusBarItemsContainer.appendChild(statusBarItems[i]);
529        }
530        this._resize(this.splitView.sidebarWidth());
531    },
532
533    _reset: function()
534    {
535        WebInspector.Panel.prototype.reset.call(this);
536
537        for (var typeId in this._profileTypesByIdMap)
538            this._profileTypesByIdMap[typeId]._reset();
539
540        delete this.visibleView;
541        delete this.currentQuery;
542        this.searchCanceled();
543
544        this._profileGroups = {};
545        this._profilesWereRequested = false;
546        this.recordButton.toggled = false;
547        if (this._selectedProfileType)
548            this.recordButton.title = this._selectedProfileType.buttonTooltip;
549        this._launcherView.profileFinished();
550
551        this.sidebarTreeElement.removeStyleClass("some-expandable");
552
553        this.profileViews.removeChildren();
554        this._profileViewStatusBarItemsContainer.removeChildren();
555
556        this.removeAllListeners();
557
558        this._updateInterface();
559        this.profilesItemTreeElement.select();
560        this._showLauncherView();
561    },
562
563    _showLauncherView: function()
564    {
565        this.closeVisibleView();
566        this._profileViewStatusBarItemsContainer.removeChildren();
567        this._launcherView.show(this.splitView.mainElement);
568        this.visibleView = this._launcherView;
569    },
570
571    _clearProfiles: function()
572    {
573        ProfilerAgent.clearProfiles();
574        HeapProfilerAgent.clearProfiles();
575        this._reset();
576    },
577
578    _garbageCollectButtonClicked: function()
579    {
580        HeapProfilerAgent.collectGarbage();
581    },
582
583    /**
584     * @param {!WebInspector.ProfileType} profileType
585     */
586    _registerProfileType: function(profileType)
587    {
588        this._profileTypesByIdMap[profileType.id] = profileType;
589        this._launcherView.addProfileType(profileType);
590        profileType.treeElement = new WebInspector.SidebarSectionTreeElement(profileType.treeItemTitle, null, true);
591        profileType.treeElement.hidden = !this._singleProfileMode;
592        this.sidebarTree.appendChild(profileType.treeElement);
593        profileType.treeElement.childrenListElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), true);
594        function onAddProfileHeader(event)
595        {
596            this._addProfileHeader(event.data);
597        }
598        function onRemoveProfileHeader(event)
599        {
600            this._removeProfileHeader(event.data);
601        }
602        function onProgressUpdated(event)
603        {
604            this._reportProfileProgress(event.data.profile, event.data.done, event.data.total);
605        }
606        profileType.addEventListener(WebInspector.ProfileType.Events.ViewUpdated, this._updateProfileTypeSpecificUI, this);
607        profileType.addEventListener(WebInspector.ProfileType.Events.AddProfileHeader, onAddProfileHeader, this);
608        profileType.addEventListener(WebInspector.ProfileType.Events.RemoveProfileHeader, onRemoveProfileHeader, this);
609        profileType.addEventListener(WebInspector.ProfileType.Events.ProgressUpdated, onProgressUpdated, this);
610    },
611
612    /**
613     * @param {Event} event
614     */
615    _handleContextMenuEvent: function(event)
616    {
617        var element = event.srcElement;
618        while (element && !element.treeElement && element !== this.element)
619            element = element.parentElement;
620        if (!element)
621            return;
622        if (element.treeElement && element.treeElement.handleContextMenuEvent) {
623            element.treeElement.handleContextMenuEvent(event, this);
624            return;
625        }
626        if (element !== this.element || event.srcElement === this.sidebarElement) {
627            var contextMenu = new WebInspector.ContextMenu(event);
628            if (this.visibleView instanceof WebInspector.HeapSnapshotView)
629                this.visibleView.populateContextMenu(contextMenu, event);
630            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Load heap snapshot\u2026" : "Load Heap Snapshot\u2026"), this._fileSelectorElement.click.bind(this._fileSelectorElement));
631            contextMenu.show();
632        }
633
634    },
635
636    /**
637     * @nosideeffects
638     * @param {string} text
639     * @param {string} profileTypeId
640     * @return {string}
641     */
642    _makeTitleKey: function(text, profileTypeId)
643    {
644        return escape(text) + '/' + escape(profileTypeId);
645    },
646
647    /**
648     * @param {!WebInspector.ProfileHeader} profile
649     */
650    _addProfileHeader: function(profile)
651    {
652        if (!profile.isTemporary)
653            this._removeTemporaryProfile(profile.profileType().id);
654
655        var profileType = profile.profileType();
656        var typeId = profileType.id;
657        var sidebarParent = profileType.treeElement;
658        sidebarParent.hidden = false;
659        var small = false;
660        var alternateTitle;
661
662        if (!WebInspector.ProfilesPanelDescriptor.isUserInitiatedProfile(profile.title) && !profile.isTemporary) {
663            var profileTitleKey = this._makeTitleKey(profile.title, typeId);
664            if (!(profileTitleKey in this._profileGroups))
665                this._profileGroups[profileTitleKey] = [];
666
667            var group = this._profileGroups[profileTitleKey];
668            group.push(profile);
669            if (group.length === 2) {
670                // Make a group TreeElement now that there are 2 profiles.
671                group._profilesTreeElement = new WebInspector.ProfileGroupSidebarTreeElement(this, profile.title);
672
673                // Insert at the same index for the first profile of the group.
674                var index = sidebarParent.children.indexOf(group[0]._profilesTreeElement);
675                sidebarParent.insertChild(group._profilesTreeElement, index);
676
677                // Move the first profile to the group.
678                var selected = group[0]._profilesTreeElement.selected;
679                sidebarParent.removeChild(group[0]._profilesTreeElement);
680                group._profilesTreeElement.appendChild(group[0]._profilesTreeElement);
681                if (selected)
682                    group[0]._profilesTreeElement.revealAndSelect();
683
684                group[0]._profilesTreeElement.small = true;
685                group[0]._profilesTreeElement.mainTitle = WebInspector.UIString("Run %d", 1);
686
687                this.sidebarTreeElement.addStyleClass("some-expandable");
688            }
689
690            if (group.length >= 2) {
691                sidebarParent = group._profilesTreeElement;
692                alternateTitle = WebInspector.UIString("Run %d", group.length);
693                small = true;
694            }
695        }
696
697        var profileTreeElement = profile.createSidebarTreeElement();
698        profile.sidebarElement = profileTreeElement;
699        profileTreeElement.small = small;
700        if (alternateTitle)
701            profileTreeElement.mainTitle = alternateTitle;
702        profile._profilesTreeElement = profileTreeElement;
703
704        sidebarParent.appendChild(profileTreeElement);
705        if (!profile.isTemporary) {
706            if (!this.visibleView)
707                this._showProfile(profile);
708            this.dispatchEventToListeners("profile added", {
709                type: typeId
710            });
711        }
712    },
713
714    /**
715     * @param {!WebInspector.ProfileHeader} profile
716     */
717    _removeProfileHeader: function(profile)
718    {
719        profile.dispose();
720        profile.profileType().removeProfile(profile);
721
722        var sidebarParent = profile.profileType().treeElement;
723        var profileTitleKey = this._makeTitleKey(profile.title, profile.profileType().id);
724        var group = this._profileGroups[profileTitleKey];
725        if (group) {
726            group.splice(group.indexOf(profile), 1);
727            if (group.length === 1) {
728                // Move the last profile out of its group and remove the group.
729                var index = sidebarParent.children.indexOf(group._profilesTreeElement);
730                sidebarParent.insertChild(group[0]._profilesTreeElement, index);
731                group[0]._profilesTreeElement.small = false;
732                group[0]._profilesTreeElement.mainTitle = group[0].title;
733                sidebarParent.removeChild(group._profilesTreeElement);
734            }
735            if (group.length !== 0)
736                sidebarParent = group._profilesTreeElement;
737            else
738                delete this._profileGroups[profileTitleKey];
739        }
740        sidebarParent.removeChild(profile._profilesTreeElement);
741
742        // No other item will be selected if there aren't any other profiles, so
743        // make sure that view gets cleared when the last profile is removed.
744        if (!sidebarParent.children.length) {
745            this.profilesItemTreeElement.select();
746            this._showLauncherView();
747            sidebarParent.hidden = !this._singleProfileMode;
748        }
749    },
750
751    /**
752     * @param {!WebInspector.ProfileHeader} profile
753     */
754    _showProfile: function(profile)
755    {
756        if (!profile || profile.isTemporary)
757            return;
758
759        var view = profile.view(this);
760        if (view === this.visibleView)
761            return;
762
763        this.closeVisibleView();
764
765        view.show(this.profileViews);
766
767        profile._profilesTreeElement._suppressOnSelect = true;
768        profile._profilesTreeElement.revealAndSelect();
769        delete profile._profilesTreeElement._suppressOnSelect;
770
771        this.visibleView = view;
772
773        this._profileViewStatusBarItemsContainer.removeChildren();
774
775        var statusBarItems = view.statusBarItems();
776        if (statusBarItems)
777            for (var i = 0; i < statusBarItems.length; ++i)
778                this._profileViewStatusBarItemsContainer.appendChild(statusBarItems[i]);
779    },
780
781    /**
782     * @param {HeapProfilerAgent.HeapSnapshotObjectId} snapshotObjectId
783     * @param {string} viewName
784     */
785    showObject: function(snapshotObjectId, viewName)
786    {
787        var heapProfiles = this.getProfileType(WebInspector.HeapSnapshotProfileType.TypeId).getProfiles();
788        for (var i = 0; i < heapProfiles.length; i++) {
789            var profile = heapProfiles[i];
790            // FIXME: allow to choose snapshot if there are several options.
791            if (profile.maxJSObjectId >= snapshotObjectId) {
792                this._showProfile(profile);
793                var view = profile.view(this);
794                view.changeView(viewName, function() {
795                    view.dataGrid.highlightObjectByHeapSnapshotId(snapshotObjectId);
796                });
797                break;
798            }
799        }
800    },
801
802    /**
803     * @param {string} typeId
804     */
805    _createTemporaryProfile: function(typeId)
806    {
807        var type = this.getProfileType(typeId);
808        if (!type.findTemporaryProfile())
809            type.addProfile(type.createTemporaryProfile());
810    },
811
812    /**
813     * @param {string} typeId
814     */
815    _removeTemporaryProfile: function(typeId)
816    {
817        var temporaryProfile = this.getProfileType(typeId).findTemporaryProfile();
818        if (!!temporaryProfile)
819            this._removeProfileHeader(temporaryProfile);
820    },
821
822    /**
823     * @param {string} typeId
824     * @param {number} uid
825     */
826    getProfile: function(typeId, uid)
827    {
828        return this.getProfileType(typeId).getProfile(uid);
829    },
830
831    /**
832     * @param {WebInspector.View} view
833     */
834    showView: function(view)
835    {
836        this._showProfile(view.profile);
837    },
838
839    /**
840     * @param {string} typeId
841     */
842    getProfileType: function(typeId)
843    {
844        return this._profileTypesByIdMap[typeId];
845    },
846
847    /**
848     * @param {string} typeId
849     * @param {string} uid
850     */
851    showProfile: function(typeId, uid)
852    {
853        this._showProfile(this.getProfile(typeId, Number(uid)));
854    },
855
856    closeVisibleView: function()
857    {
858        if (this.visibleView)
859            this.visibleView.detach();
860        delete this.visibleView;
861    },
862
863    /**
864     * @param {string} query
865     */
866    performSearch: function(query)
867    {
868        this.searchCanceled();
869
870        var searchableViews = this._searchableViews();
871        if (!searchableViews || !searchableViews.length)
872            return;
873
874        var visibleView = this.visibleView;
875
876        var matchesCountUpdateTimeout = null;
877
878        function updateMatchesCount()
879        {
880            WebInspector.searchController.updateSearchMatchesCount(this._totalSearchMatches, this);
881            WebInspector.searchController.updateCurrentMatchIndex(this._currentSearchResultIndex, this);
882            matchesCountUpdateTimeout = null;
883        }
884
885        function updateMatchesCountSoon()
886        {
887            if (matchesCountUpdateTimeout)
888                return;
889            // Update the matches count every half-second so it doesn't feel twitchy.
890            matchesCountUpdateTimeout = setTimeout(updateMatchesCount.bind(this), 500);
891        }
892
893        function finishedCallback(view, searchMatches)
894        {
895            if (!searchMatches)
896                return;
897
898            this._totalSearchMatches += searchMatches;
899            this._searchResults.push(view);
900
901            if (this.searchMatchFound)
902                this.searchMatchFound(view, searchMatches);
903
904            updateMatchesCountSoon.call(this);
905
906            if (view === visibleView)
907                view.jumpToFirstSearchResult();
908        }
909
910        var i = 0;
911        var panel = this;
912        var boundFinishedCallback = finishedCallback.bind(this);
913        var chunkIntervalIdentifier = null;
914
915        // Split up the work into chunks so we don't block the
916        // UI thread while processing.
917
918        function processChunk()
919        {
920            var view = searchableViews[i];
921
922            if (++i >= searchableViews.length) {
923                if (panel._currentSearchChunkIntervalIdentifier === chunkIntervalIdentifier)
924                    delete panel._currentSearchChunkIntervalIdentifier;
925                clearInterval(chunkIntervalIdentifier);
926            }
927
928            if (!view)
929                return;
930
931            view.currentQuery = query;
932            view.performSearch(query, boundFinishedCallback);
933        }
934
935        processChunk();
936
937        chunkIntervalIdentifier = setInterval(processChunk, 25);
938        this._currentSearchChunkIntervalIdentifier = chunkIntervalIdentifier;
939    },
940
941    jumpToNextSearchResult: function()
942    {
943        if (!this.showView || !this._searchResults || !this._searchResults.length)
944            return;
945
946        var showFirstResult = false;
947
948        this._currentSearchResultIndex = this._searchResults.indexOf(this.visibleView);
949        if (this._currentSearchResultIndex === -1) {
950            this._currentSearchResultIndex = 0;
951            showFirstResult = true;
952        }
953
954        var currentView = this._searchResults[this._currentSearchResultIndex];
955
956        if (currentView.showingLastSearchResult()) {
957            if (++this._currentSearchResultIndex >= this._searchResults.length)
958                this._currentSearchResultIndex = 0;
959            currentView = this._searchResults[this._currentSearchResultIndex];
960            showFirstResult = true;
961        }
962
963        WebInspector.searchController.updateCurrentMatchIndex(this._currentSearchResultIndex, this);
964
965        if (currentView !== this.visibleView) {
966            this.showView(currentView);
967            WebInspector.searchController.showSearchField();
968        }
969
970        if (showFirstResult)
971            currentView.jumpToFirstSearchResult();
972        else
973            currentView.jumpToNextSearchResult();
974    },
975
976    jumpToPreviousSearchResult: function()
977    {
978        if (!this.showView || !this._searchResults || !this._searchResults.length)
979            return;
980
981        var showLastResult = false;
982
983        this._currentSearchResultIndex = this._searchResults.indexOf(this.visibleView);
984        if (this._currentSearchResultIndex === -1) {
985            this._currentSearchResultIndex = 0;
986            showLastResult = true;
987        }
988
989        var currentView = this._searchResults[this._currentSearchResultIndex];
990
991        if (currentView.showingFirstSearchResult()) {
992            if (--this._currentSearchResultIndex < 0)
993                this._currentSearchResultIndex = (this._searchResults.length - 1);
994            currentView = this._searchResults[this._currentSearchResultIndex];
995            showLastResult = true;
996        }
997
998        WebInspector.searchController.updateCurrentMatchIndex(this._currentSearchResultIndex, this);
999
1000        if (currentView !== this.visibleView) {
1001            this.showView(currentView);
1002            WebInspector.searchController.showSearchField();
1003        }
1004
1005        if (showLastResult)
1006            currentView.jumpToLastSearchResult();
1007        else
1008            currentView.jumpToPreviousSearchResult();
1009    },
1010
1011    /**
1012     * @return {!Array.<!WebInspector.ProfileHeader>}
1013     */
1014    _getAllProfiles: function()
1015    {
1016        var profiles = [];
1017        for (var typeId in this._profileTypesByIdMap)
1018            profiles = profiles.concat(this._profileTypesByIdMap[typeId].getProfiles());
1019        return profiles;
1020    },
1021
1022    /**
1023     * @return {!Array.<!WebInspector.View>}
1024     */
1025    _searchableViews: function()
1026    {
1027        var profiles = this._getAllProfiles();
1028        var searchableViews = [];
1029        for (var i = 0; i < profiles.length; ++i) {
1030            var view = profiles[i].view(this);
1031            if (view.performSearch)
1032                searchableViews.push(view)
1033        }
1034        var index = searchableViews.indexOf(this.visibleView);
1035        if (index > 0) {
1036            // Move visibleView to the first position.
1037            searchableViews[index] = searchableViews[0];
1038            searchableViews[0] = this.visibleView;
1039        }
1040        return searchableViews;
1041    },
1042
1043    searchMatchFound: function(view, matches)
1044    {
1045        view.profile._profilesTreeElement.searchMatches = matches;
1046    },
1047
1048    searchCanceled: function()
1049    {
1050        if (this._searchResults) {
1051            for (var i = 0; i < this._searchResults.length; ++i) {
1052                var view = this._searchResults[i];
1053                if (view.searchCanceled)
1054                    view.searchCanceled();
1055                delete view.currentQuery;
1056            }
1057        }
1058
1059        WebInspector.Panel.prototype.searchCanceled.call(this);
1060
1061        if (this._currentSearchChunkIntervalIdentifier) {
1062            clearInterval(this._currentSearchChunkIntervalIdentifier);
1063            delete this._currentSearchChunkIntervalIdentifier;
1064        }
1065
1066        this._totalSearchMatches = 0;
1067        this._currentSearchResultIndex = 0;
1068        this._searchResults = [];
1069
1070        var profiles = this._getAllProfiles();
1071        for (var i = 0; i < profiles.length; ++i)
1072            profiles[i]._profilesTreeElement.searchMatches = 0;
1073    },
1074
1075    _updateInterface: function()
1076    {
1077        // FIXME: Replace ProfileType-specific button visibility changes by a single ProfileType-agnostic "combo-button" visibility change.
1078        if (this._profilerEnabled) {
1079            this.enableToggleButton.title = WebInspector.UIString("Profiling enabled. Click to disable.");
1080            this.enableToggleButton.toggled = true;
1081            this.recordButton.visible = true;
1082            this._profileViewStatusBarItemsContainer.removeStyleClass("hidden");
1083            this.clearResultsButton.element.removeStyleClass("hidden");
1084            this.panelEnablerView.detach();
1085        } else {
1086            this.enableToggleButton.title = WebInspector.UIString("Profiling disabled. Click to enable.");
1087            this.enableToggleButton.toggled = false;
1088            this.recordButton.visible = false;
1089            this._profileViewStatusBarItemsContainer.addStyleClass("hidden");
1090            this.clearResultsButton.element.addStyleClass("hidden");
1091            this.panelEnablerView.show(this.element);
1092        }
1093    },
1094
1095    get profilerEnabled()
1096    {
1097        return this._profilerEnabled;
1098    },
1099
1100    enableProfiler: function()
1101    {
1102        if (this._profilerEnabled)
1103            return;
1104        this._toggleProfiling(this.panelEnablerView.alwaysEnabled);
1105    },
1106
1107    disableProfiler: function()
1108    {
1109        if (!this._profilerEnabled)
1110            return;
1111        this._toggleProfiling(this.panelEnablerView.alwaysEnabled);
1112    },
1113
1114    /**
1115     * @param {WebInspector.Event} event
1116     */
1117    _onToggleProfiling: function(event) {
1118        this._toggleProfiling(true);
1119    },
1120
1121    /**
1122     * @param {boolean} always
1123     */
1124    _toggleProfiling: function(always)
1125    {
1126        if (this._profilerEnabled) {
1127            WebInspector.settings.profilerEnabled.set(false);
1128            ProfilerAgent.disable(this._profilerWasDisabled.bind(this));
1129        } else {
1130            WebInspector.settings.profilerEnabled.set(always);
1131            ProfilerAgent.enable(this._profilerWasEnabled.bind(this));
1132        }
1133    },
1134
1135    /**
1136     * @param {!WebInspector.Event} event
1137     */
1138    sidebarResized: function(event)
1139    {
1140        var sidebarWidth = /** @type {number} */ (event.data);
1141        this._resize(sidebarWidth);
1142    },
1143
1144    onResize: function()
1145    {
1146        this._resize(this.splitView.sidebarWidth());
1147    },
1148
1149    /**
1150     * @param {number} sidebarWidth
1151     */
1152    _resize: function(sidebarWidth)
1153    {
1154        var lastItemElement = this._statusBarButtons[this._statusBarButtons.length - 1].element;
1155        var left = lastItemElement.totalOffsetLeft() + lastItemElement.offsetWidth;
1156        this._profileTypeStatusBarItemsContainer.style.left = left + "px";
1157        left += this._profileTypeStatusBarItemsContainer.offsetWidth - 1;
1158        this._profileViewStatusBarItemsContainer.style.left = Math.max(left, sidebarWidth) + "px";
1159    },
1160
1161    /**
1162     * @param {string} profileType
1163     * @param {boolean} isProfiling
1164     */
1165    setRecordingProfile: function(profileType, isProfiling)
1166    {
1167        var profileTypeObject = this.getProfileType(profileType);
1168        this.recordButton.toggled = isProfiling;
1169        this.recordButton.title = profileTypeObject.buttonTooltip;
1170        if (isProfiling) {
1171            this._launcherView.profileStarted();
1172            this._createTemporaryProfile(profileType);
1173        } else
1174            this._launcherView.profileFinished();
1175    },
1176
1177    /**
1178     * @param {!WebInspector.ProfileHeader} profile
1179     * @param {number} done
1180     * @param {number} total
1181     */
1182    _reportProfileProgress: function(profile, done, total)
1183    {
1184        profile.sidebarElement.subtitle = WebInspector.UIString("%.0f%", (done / total) * 100);
1185        profile.sidebarElement.wait = true;
1186    },
1187
1188    /**
1189     * @param {WebInspector.ContextMenu} contextMenu
1190     * @param {Object} target
1191     */
1192    appendApplicableItems: function(event, contextMenu, target)
1193    {
1194        if (WebInspector.inspectorView.currentPanel() !== this)
1195            return;
1196
1197        var object = /** @type {WebInspector.RemoteObject} */ (target);
1198        var objectId = object.objectId;
1199        if (!objectId)
1200            return;
1201
1202        var heapProfiles = this.getProfileType(WebInspector.HeapSnapshotProfileType.TypeId).getProfiles();
1203        if (!heapProfiles.length)
1204            return;
1205
1206        function revealInView(viewName)
1207        {
1208            HeapProfilerAgent.getHeapObjectId(objectId, didReceiveHeapObjectId.bind(this, viewName));
1209        }
1210
1211        function didReceiveHeapObjectId(viewName, error, result)
1212        {
1213            if (WebInspector.inspectorView.currentPanel() !== this)
1214                return;
1215            if (!error)
1216                this.showObject(result, viewName);
1217        }
1218
1219        contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in Dominators view" : "Reveal in Dominators View"), revealInView.bind(this, "Dominators"));
1220        contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in Summary view" : "Reveal in Summary View"), revealInView.bind(this, "Summary"));
1221    },
1222
1223    __proto__: WebInspector.Panel.prototype
1224}
1225
1226/**
1227 * @constructor
1228 * @extends {WebInspector.SidebarTreeElement}
1229 * @param {!WebInspector.ProfileHeader} profile
1230 * @param {string} titleFormat
1231 * @param {string} className
1232 */
1233WebInspector.ProfileSidebarTreeElement = function(profile, titleFormat, className)
1234{
1235    this.profile = profile;
1236    this._titleFormat = titleFormat;
1237
1238    if (WebInspector.ProfilesPanelDescriptor.isUserInitiatedProfile(this.profile.title))
1239        this._profileNumber = WebInspector.ProfilesPanelDescriptor.userInitiatedProfileIndex(this.profile.title);
1240
1241    WebInspector.SidebarTreeElement.call(this, className, "", "", profile, false);
1242
1243    this.refreshTitles();
1244}
1245
1246WebInspector.ProfileSidebarTreeElement.prototype = {
1247    onselect: function()
1248    {
1249        if (!this._suppressOnSelect)
1250            this.treeOutline.panel._showProfile(this.profile);
1251    },
1252
1253    ondelete: function()
1254    {
1255        this.treeOutline.panel._removeProfileHeader(this.profile);
1256        return true;
1257    },
1258
1259    get mainTitle()
1260    {
1261        if (this._mainTitle)
1262            return this._mainTitle;
1263        if (WebInspector.ProfilesPanelDescriptor.isUserInitiatedProfile(this.profile.title))
1264            return WebInspector.UIString(this._titleFormat, this._profileNumber);
1265        return this.profile.title;
1266    },
1267
1268    set mainTitle(x)
1269    {
1270        this._mainTitle = x;
1271        this.refreshTitles();
1272    },
1273
1274    set searchMatches(matches)
1275    {
1276        if (!matches) {
1277            if (!this.bubbleElement)
1278                return;
1279            this.bubbleElement.removeStyleClass("search-matches");
1280            this.bubbleText = "";
1281            return;
1282        }
1283
1284        this.bubbleText = matches;
1285        this.bubbleElement.addStyleClass("search-matches");
1286    },
1287
1288    /**
1289     * @param {!Event} event
1290     * @param {!WebInspector.ProfilesPanel} panel
1291     */
1292    handleContextMenuEvent: function(event, panel)
1293    {
1294        var profile = this.profile;
1295        var contextMenu = new WebInspector.ContextMenu(event);
1296        // FIXME: use context menu provider
1297        if (profile.canSaveToFile()) {
1298            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Save heap snapshot\u2026" : "Save Heap Snapshot\u2026"), profile.saveToFile.bind(profile));
1299            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Load heap snapshot\u2026" : "Load Heap Snapshot\u2026"), panel._fileSelectorElement.click.bind(panel._fileSelectorElement));
1300            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete heap snapshot" : "Delete Heap Snapshot"), this.ondelete.bind(this));
1301        } else {
1302            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Load heap snapshot\u2026" : "Load Heap Snapshot\u2026"), panel._fileSelectorElement.click.bind(panel._fileSelectorElement));
1303            contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete profile" : "Delete Profile"), this.ondelete.bind(this));
1304        }
1305        contextMenu.show();
1306    },
1307
1308    __proto__: WebInspector.SidebarTreeElement.prototype
1309}
1310
1311/**
1312 * @constructor
1313 * @extends {WebInspector.SidebarTreeElement}
1314 * @param {WebInspector.ProfilesPanel} panel
1315 * @param {string} title
1316 * @param {string=} subtitle
1317 */
1318WebInspector.ProfileGroupSidebarTreeElement = function(panel, title, subtitle)
1319{
1320    WebInspector.SidebarTreeElement.call(this, "profile-group-sidebar-tree-item", title, subtitle, null, true);
1321    this._panel = panel;
1322}
1323
1324WebInspector.ProfileGroupSidebarTreeElement.prototype = {
1325    onselect: function()
1326    {
1327        if (this.children.length > 0)
1328            this._panel._showProfile(this.children[this.children.length - 1].profile);
1329    },
1330
1331    __proto__: WebInspector.SidebarTreeElement.prototype
1332}
1333
1334/**
1335 * @constructor
1336 * @extends {WebInspector.SidebarTreeElement}
1337 * @param {!WebInspector.ProfilesPanel} panel
1338 */
1339WebInspector.ProfilesSidebarTreeElement = function(panel)
1340{
1341    this._panel = panel;
1342    this.small = false;
1343
1344    WebInspector.SidebarTreeElement.call(this, "profile-launcher-view-tree-item", WebInspector.UIString("Profiles"), "", null, false);
1345}
1346
1347WebInspector.ProfilesSidebarTreeElement.prototype = {
1348    onselect: function()
1349    {
1350        this._panel._showLauncherView();
1351    },
1352
1353    get selectable()
1354    {
1355        return true;
1356    },
1357
1358    __proto__: WebInspector.SidebarTreeElement.prototype
1359}
1360
1361
1362/**
1363 * @constructor
1364 * @extends {WebInspector.ProfilesPanel}
1365 */
1366WebInspector.CPUProfilerPanel = function()
1367{
1368    WebInspector.ProfilesPanel.call(this, "cpu-profiler", new WebInspector.CPUProfileType());
1369}
1370
1371WebInspector.CPUProfilerPanel.prototype = {
1372    __proto__: WebInspector.ProfilesPanel.prototype
1373}
1374
1375
1376/**
1377 * @constructor
1378 * @extends {WebInspector.ProfilesPanel}
1379 */
1380WebInspector.CSSSelectorProfilerPanel = function()
1381{
1382    WebInspector.ProfilesPanel.call(this, "css-profiler", new WebInspector.CSSSelectorProfileType());
1383}
1384
1385WebInspector.CSSSelectorProfilerPanel.prototype = {
1386    __proto__: WebInspector.ProfilesPanel.prototype
1387}
1388
1389
1390/**
1391 * @constructor
1392 * @extends {WebInspector.ProfilesPanel}
1393 */
1394WebInspector.HeapProfilerPanel = function()
1395{
1396    WebInspector.ProfilesPanel.call(this, "heap-profiler", new WebInspector.HeapSnapshotProfileType());
1397}
1398
1399WebInspector.HeapProfilerPanel.prototype = {
1400    __proto__: WebInspector.ProfilesPanel.prototype
1401}
1402
1403
1404/**
1405 * @constructor
1406 * @extends {WebInspector.ProfilesPanel}
1407 */
1408WebInspector.CanvasProfilerPanel = function()
1409{
1410    WebInspector.ProfilesPanel.call(this, "canvas-profiler", new WebInspector.CanvasProfileType());
1411}
1412
1413WebInspector.CanvasProfilerPanel.prototype = {
1414    __proto__: WebInspector.ProfilesPanel.prototype
1415}
1416
1417
1418importScript("ProfileDataGridTree.js");
1419importScript("BottomUpProfileDataGridTree.js");
1420importScript("CPUProfileView.js");
1421importScript("CSSSelectorProfileView.js");
1422importScript("FlameChart.js");
1423importScript("HeapSnapshot.js");
1424importScript("HeapSnapshotDataGrids.js");
1425importScript("HeapSnapshotGridNodes.js");
1426importScript("HeapSnapshotLoader.js");
1427importScript("HeapSnapshotProxy.js");
1428importScript("HeapSnapshotView.js");
1429importScript("HeapSnapshotWorkerDispatcher.js");
1430importScript("JSHeapSnapshot.js");
1431importScript("NativeHeapSnapshot.js");
1432importScript("ProfileLauncherView.js");
1433importScript("TopDownProfileDataGridTree.js");
1434importScript("CanvasProfileView.js");
1435