• 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
26// FIXME: Rename the file.
27
28WebInspector.CPUProfileView = function(profile)
29{
30    WebInspector.View.call(this);
31
32    this.element.addStyleClass("profile-view");
33
34    this.showSelfTimeAsPercent = true;
35    this.showTotalTimeAsPercent = true;
36    this.showAverageTimeAsPercent = true;
37
38    var columns = { "self": { title: WebInspector.UIString("Self"), width: "72px", sort: "descending", sortable: true },
39                    "total": { title: WebInspector.UIString("Total"), width: "72px", sortable: true },
40                    "average": { title: WebInspector.UIString("Average"), width: "72px", sortable: true },
41                    "calls": { title: WebInspector.UIString("Calls"), width: "54px", sortable: true },
42                    "function": { title: WebInspector.UIString("Function"), disclosure: true, sortable: true } };
43
44    if (Preferences.samplingCPUProfiler) {
45        delete columns.average;
46        delete columns.calls;
47    }
48
49    this.dataGrid = new WebInspector.DataGrid(columns);
50    this.dataGrid.addEventListener("sorting changed", this._sortData, this);
51    this.dataGrid.element.addEventListener("mousedown", this._mouseDownInDataGrid.bind(this), true);
52    this.element.appendChild(this.dataGrid.element);
53
54    this.viewSelectElement = document.createElement("select");
55    this.viewSelectElement.className = "status-bar-item";
56    this.viewSelectElement.addEventListener("change", this._changeView.bind(this), false);
57    this.view = "Heavy";
58
59    var heavyViewOption = document.createElement("option");
60    heavyViewOption.label = WebInspector.UIString("Heavy (Bottom Up)");
61    var treeViewOption = document.createElement("option");
62    treeViewOption.label = WebInspector.UIString("Tree (Top Down)");
63    this.viewSelectElement.appendChild(heavyViewOption);
64    this.viewSelectElement.appendChild(treeViewOption);
65
66    this.percentButton = new WebInspector.StatusBarButton("", "percent-time-status-bar-item");
67    this.percentButton.addEventListener("click", this._percentClicked.bind(this), false);
68
69    this.focusButton = new WebInspector.StatusBarButton(WebInspector.UIString("Focus selected function."), "focus-profile-node-status-bar-item");
70    this.focusButton.disabled = true;
71    this.focusButton.addEventListener("click", this._focusClicked.bind(this), false);
72
73    this.excludeButton = new WebInspector.StatusBarButton(WebInspector.UIString("Exclude selected function."), "exclude-profile-node-status-bar-item");
74    this.excludeButton.disabled = true;
75    this.excludeButton.addEventListener("click", this._excludeClicked.bind(this), false);
76
77    this.resetButton = new WebInspector.StatusBarButton(WebInspector.UIString("Restore all functions."), "reset-profile-status-bar-item");
78    this.resetButton.visible = false;
79    this.resetButton.addEventListener("click", this._resetClicked.bind(this), false);
80
81    this.profile = profile;
82
83    var self = this;
84    function profileCallback(profile)
85    {
86        self.profile = profile;
87        self._assignParentsInProfile();
88
89        self.profileDataGridTree = self.bottomUpProfileDataGridTree;
90        self.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator("selfTime", false));
91
92        self.refresh();
93
94        self._updatePercentButton();
95    }
96
97    var callId = WebInspector.Callback.wrap(profileCallback);
98    InspectorBackend.getProfile(callId, this.profile.uid);
99}
100
101WebInspector.CPUProfileView.prototype = {
102    get statusBarItems()
103    {
104        return [this.viewSelectElement, this.percentButton.element, this.focusButton.element, this.excludeButton.element, this.resetButton.element];
105    },
106
107    get profile()
108    {
109        return this._profile;
110    },
111
112    set profile(profile)
113    {
114        this._profile = profile;
115    },
116
117    get bottomUpProfileDataGridTree()
118    {
119        if (!this._bottomUpProfileDataGridTree)
120            this._bottomUpProfileDataGridTree = new WebInspector.BottomUpProfileDataGridTree(this, this.profile.head);
121        return this._bottomUpProfileDataGridTree;
122    },
123
124    get topDownProfileDataGridTree()
125    {
126        if (!this._topDownProfileDataGridTree)
127            this._topDownProfileDataGridTree = new WebInspector.TopDownProfileDataGridTree(this, this.profile.head);
128        return this._topDownProfileDataGridTree;
129    },
130
131    get currentTree()
132    {
133        return this._currentTree;
134    },
135
136    set currentTree(tree)
137    {
138        this._currentTree = tree;
139        this.refresh();
140    },
141
142    get topDownTree()
143    {
144        if (!this._topDownTree) {
145            this._topDownTree = WebInspector.TopDownTreeFactory.create(this.profile.head);
146            this._sortProfile(this._topDownTree);
147        }
148
149        return this._topDownTree;
150    },
151
152    get bottomUpTree()
153    {
154        if (!this._bottomUpTree) {
155            this._bottomUpTree = WebInspector.BottomUpTreeFactory.create(this.profile.head);
156            this._sortProfile(this._bottomUpTree);
157        }
158
159        return this._bottomUpTree;
160    },
161
162    show: function(parentElement)
163    {
164        WebInspector.View.prototype.show.call(this, parentElement);
165        this.dataGrid.updateWidths();
166    },
167
168    hide: function()
169    {
170        WebInspector.View.prototype.hide.call(this);
171        this._currentSearchResultIndex = -1;
172    },
173
174    resize: function()
175    {
176        if (this.dataGrid)
177            this.dataGrid.updateWidths();
178    },
179
180    refresh: function()
181    {
182        var selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null;
183
184        this.dataGrid.removeChildren();
185
186        var children = this.profileDataGridTree.children;
187        var count = children.length;
188
189        for (var index = 0; index < count; ++index)
190            this.dataGrid.appendChild(children[index]);
191
192        if (selectedProfileNode)
193            selectedProfileNode.selected = true;
194    },
195
196    refreshVisibleData: function()
197    {
198        var child = this.dataGrid.children[0];
199        while (child) {
200            child.refresh();
201            child = child.traverseNextNode(false, null, true);
202        }
203    },
204
205    refreshShowAsPercents: function()
206    {
207        this._updatePercentButton();
208        this.refreshVisibleData();
209    },
210
211    searchCanceled: function()
212    {
213        if (this._searchResults) {
214            for (var i = 0; i < this._searchResults.length; ++i) {
215                var profileNode = this._searchResults[i].profileNode;
216
217                delete profileNode._searchMatchedSelfColumn;
218                delete profileNode._searchMatchedTotalColumn;
219                delete profileNode._searchMatchedCallsColumn;
220                delete profileNode._searchMatchedFunctionColumn;
221
222                profileNode.refresh();
223            }
224        }
225
226        delete this._searchFinishedCallback;
227        this._currentSearchResultIndex = -1;
228        this._searchResults = [];
229    },
230
231    performSearch: function(query, finishedCallback)
232    {
233        // Call searchCanceled since it will reset everything we need before doing a new search.
234        this.searchCanceled();
235
236        query = query.trim();
237
238        if (!query.length)
239            return;
240
241        this._searchFinishedCallback = finishedCallback;
242
243        var greaterThan = (query.indexOf(">") === 0);
244        var lessThan = (query.indexOf("<") === 0);
245        var equalTo = (query.indexOf("=") === 0 || ((greaterThan || lessThan) && query.indexOf("=") === 1));
246        var percentUnits = (query.lastIndexOf("%") === (query.length - 1));
247        var millisecondsUnits = (query.length > 2 && query.lastIndexOf("ms") === (query.length - 2));
248        var secondsUnits = (!millisecondsUnits && query.lastIndexOf("s") === (query.length - 1));
249
250        var queryNumber = parseFloat(query);
251        if (greaterThan || lessThan || equalTo) {
252            if (equalTo && (greaterThan || lessThan))
253                queryNumber = parseFloat(query.substring(2));
254            else
255                queryNumber = parseFloat(query.substring(1));
256        }
257
258        var queryNumberMilliseconds = (secondsUnits ? (queryNumber * 1000) : queryNumber);
259
260        // Make equalTo implicitly true if it wasn't specified there is no other operator.
261        if (!isNaN(queryNumber) && !(greaterThan || lessThan))
262            equalTo = true;
263
264        function matchesQuery(/*ProfileDataGridNode*/ profileDataGridNode)
265        {
266            delete profileDataGridNode._searchMatchedSelfColumn;
267            delete profileDataGridNode._searchMatchedTotalColumn;
268            delete profileDataGridNode._searchMatchedAverageColumn;
269            delete profileDataGridNode._searchMatchedCallsColumn;
270            delete profileDataGridNode._searchMatchedFunctionColumn;
271
272            if (percentUnits) {
273                if (lessThan) {
274                    if (profileDataGridNode.selfPercent < queryNumber)
275                        profileDataGridNode._searchMatchedSelfColumn = true;
276                    if (profileDataGridNode.totalPercent < queryNumber)
277                        profileDataGridNode._searchMatchedTotalColumn = true;
278                    if (profileDataGridNode.averagePercent < queryNumberMilliseconds)
279                        profileDataGridNode._searchMatchedAverageColumn = true;
280                } else if (greaterThan) {
281                    if (profileDataGridNode.selfPercent > queryNumber)
282                        profileDataGridNode._searchMatchedSelfColumn = true;
283                    if (profileDataGridNode.totalPercent > queryNumber)
284                        profileDataGridNode._searchMatchedTotalColumn = true;
285                    if (profileDataGridNode.averagePercent < queryNumberMilliseconds)
286                        profileDataGridNode._searchMatchedAverageColumn = true;
287                }
288
289                if (equalTo) {
290                    if (profileDataGridNode.selfPercent == queryNumber)
291                        profileDataGridNode._searchMatchedSelfColumn = true;
292                    if (profileDataGridNode.totalPercent == queryNumber)
293                        profileDataGridNode._searchMatchedTotalColumn = true;
294                    if (profileDataGridNode.averagePercent < queryNumberMilliseconds)
295                        profileDataGridNode._searchMatchedAverageColumn = true;
296                }
297            } else if (millisecondsUnits || secondsUnits) {
298                if (lessThan) {
299                    if (profileDataGridNode.selfTime < queryNumberMilliseconds)
300                        profileDataGridNode._searchMatchedSelfColumn = true;
301                    if (profileDataGridNode.totalTime < queryNumberMilliseconds)
302                        profileDataGridNode._searchMatchedTotalColumn = true;
303                    if (profileDataGridNode.averageTime < queryNumberMilliseconds)
304                        profileDataGridNode._searchMatchedAverageColumn = true;
305                } else if (greaterThan) {
306                    if (profileDataGridNode.selfTime > queryNumberMilliseconds)
307                        profileDataGridNode._searchMatchedSelfColumn = true;
308                    if (profileDataGridNode.totalTime > queryNumberMilliseconds)
309                        profileDataGridNode._searchMatchedTotalColumn = true;
310                    if (profileDataGridNode.averageTime > queryNumberMilliseconds)
311                        profileDataGridNode._searchMatchedAverageColumn = true;
312                }
313
314                if (equalTo) {
315                    if (profileDataGridNode.selfTime == queryNumberMilliseconds)
316                        profileDataGridNode._searchMatchedSelfColumn = true;
317                    if (profileDataGridNode.totalTime == queryNumberMilliseconds)
318                        profileDataGridNode._searchMatchedTotalColumn = true;
319                    if (profileDataGridNode.averageTime == queryNumberMilliseconds)
320                        profileDataGridNode._searchMatchedAverageColumn = true;
321                }
322            } else {
323                if (equalTo && profileDataGridNode.numberOfCalls == queryNumber)
324                    profileDataGridNode._searchMatchedCallsColumn = true;
325                if (greaterThan && profileDataGridNode.numberOfCalls > queryNumber)
326                    profileDataGridNode._searchMatchedCallsColumn = true;
327                if (lessThan && profileDataGridNode.numberOfCalls < queryNumber)
328                    profileDataGridNode._searchMatchedCallsColumn = true;
329            }
330
331            if (profileDataGridNode.functionName.hasSubstring(query, true) || profileDataGridNode.url.hasSubstring(query, true))
332                profileDataGridNode._searchMatchedFunctionColumn = true;
333
334            if (profileDataGridNode._searchMatchedSelfColumn ||
335                profileDataGridNode._searchMatchedTotalColumn ||
336                profileDataGridNode._searchMatchedAverageColumn ||
337                profileDataGridNode._searchMatchedCallsColumn ||
338                profileDataGridNode._searchMatchedFunctionColumn)
339            {
340                profileDataGridNode.refresh();
341                return true;
342            }
343
344            return false;
345        }
346
347        var current = this.profileDataGridTree.children[0];
348
349        while (current) {
350            if (matchesQuery(current)) {
351                this._searchResults.push({ profileNode: current });
352            }
353
354            current = current.traverseNextNode(false, null, false);
355        }
356
357        finishedCallback(this, this._searchResults.length);
358    },
359
360    jumpToFirstSearchResult: function()
361    {
362        if (!this._searchResults || !this._searchResults.length)
363            return;
364        this._currentSearchResultIndex = 0;
365        this._jumpToSearchResult(this._currentSearchResultIndex);
366    },
367
368    jumpToLastSearchResult: function()
369    {
370        if (!this._searchResults || !this._searchResults.length)
371            return;
372        this._currentSearchResultIndex = (this._searchResults.length - 1);
373        this._jumpToSearchResult(this._currentSearchResultIndex);
374    },
375
376    jumpToNextSearchResult: function()
377    {
378        if (!this._searchResults || !this._searchResults.length)
379            return;
380        if (++this._currentSearchResultIndex >= this._searchResults.length)
381            this._currentSearchResultIndex = 0;
382        this._jumpToSearchResult(this._currentSearchResultIndex);
383    },
384
385    jumpToPreviousSearchResult: function()
386    {
387        if (!this._searchResults || !this._searchResults.length)
388            return;
389        if (--this._currentSearchResultIndex < 0)
390            this._currentSearchResultIndex = (this._searchResults.length - 1);
391        this._jumpToSearchResult(this._currentSearchResultIndex);
392    },
393
394    showingFirstSearchResult: function()
395    {
396        return (this._currentSearchResultIndex === 0);
397    },
398
399    showingLastSearchResult: function()
400    {
401        return (this._searchResults && this._currentSearchResultIndex === (this._searchResults.length - 1));
402    },
403
404    _jumpToSearchResult: function(index)
405    {
406        var searchResult = this._searchResults[index];
407        if (!searchResult)
408            return;
409
410        var profileNode = searchResult.profileNode;
411        profileNode.reveal();
412        profileNode.select();
413    },
414
415    _changeView: function(event)
416    {
417        if (!event || !this.profile)
418            return;
419
420        if (event.target.selectedIndex == 1 && this.view == "Heavy") {
421            this.profileDataGridTree = this.topDownProfileDataGridTree;
422            this._sortProfile();
423            this.view = "Tree";
424        } else if (event.target.selectedIndex == 0 && this.view == "Tree") {
425            this.profileDataGridTree = this.bottomUpProfileDataGridTree;
426            this._sortProfile();
427            this.view = "Heavy";
428        }
429
430        if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults)
431            return;
432
433        // The current search needs to be performed again. First negate out previous match
434        // count by calling the search finished callback with a negative number of matches.
435        // Then perform the search again the with same query and callback.
436        this._searchFinishedCallback(this, -this._searchResults.length);
437        this.performSearch(this.currentQuery, this._searchFinishedCallback);
438    },
439
440    _percentClicked: function(event)
441    {
442        var currentState = this.showSelfTimeAsPercent && this.showTotalTimeAsPercent && this.showAverageTimeAsPercent;
443        this.showSelfTimeAsPercent = !currentState;
444        this.showTotalTimeAsPercent = !currentState;
445        this.showAverageTimeAsPercent = !currentState;
446        this.refreshShowAsPercents();
447    },
448
449    _updatePercentButton: function()
450    {
451        if (this.showSelfTimeAsPercent && this.showTotalTimeAsPercent && this.showAverageTimeAsPercent) {
452            this.percentButton.title = WebInspector.UIString("Show absolute total and self times.");
453            this.percentButton.toggled = true;
454        } else {
455            this.percentButton.title = WebInspector.UIString("Show total and self times as percentages.");
456            this.percentButton.toggled = false;
457        }
458    },
459
460    _focusClicked: function(event)
461    {
462        if (!this.dataGrid.selectedNode)
463            return;
464
465        this.resetButton.visible = true;
466        this.profileDataGridTree.focus(this.dataGrid.selectedNode);
467        this.refresh();
468        this.refreshVisibleData();
469    },
470
471    _excludeClicked: function(event)
472    {
473        var selectedNode = this.dataGrid.selectedNode
474
475        if (!selectedNode)
476            return;
477
478        selectedNode.deselect();
479
480        this.resetButton.visible = true;
481        this.profileDataGridTree.exclude(selectedNode);
482        this.refresh();
483        this.refreshVisibleData();
484    },
485
486    _resetClicked: function(event)
487    {
488        this.resetButton.visible = false;
489        this.profileDataGridTree.restore();
490        this.refresh();
491        this.refreshVisibleData();
492    },
493
494    _dataGridNodeSelected: function(node)
495    {
496        this.focusButton.disabled = false;
497        this.excludeButton.disabled = false;
498    },
499
500    _dataGridNodeDeselected: function(node)
501    {
502        this.focusButton.disabled = true;
503        this.excludeButton.disabled = true;
504    },
505
506    _sortData: function(event)
507    {
508        this._sortProfile(this.profile);
509    },
510
511    _sortProfile: function()
512    {
513        var sortAscending = this.dataGrid.sortOrder === "ascending";
514        var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier;
515        var sortProperty = {
516                "average": "averageTime",
517                "self": "selfTime",
518                "total": "totalTime",
519                "calls": "numberOfCalls",
520                "function": "functionName"
521            }[sortColumnIdentifier];
522
523        this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator(sortProperty, sortAscending));
524
525        this.refresh();
526    },
527
528    _mouseDownInDataGrid: function(event)
529    {
530        if (event.detail < 2)
531            return;
532
533        var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
534        if (!cell || (!cell.hasStyleClass("total-column") && !cell.hasStyleClass("self-column") && !cell.hasStyleClass("average-column")))
535            return;
536
537        if (cell.hasStyleClass("total-column"))
538            this.showTotalTimeAsPercent = !this.showTotalTimeAsPercent;
539        else if (cell.hasStyleClass("self-column"))
540            this.showSelfTimeAsPercent = !this.showSelfTimeAsPercent;
541        else if (cell.hasStyleClass("average-column"))
542            this.showAverageTimeAsPercent = !this.showAverageTimeAsPercent;
543
544        this.refreshShowAsPercents();
545
546        event.preventDefault();
547        event.stopPropagation();
548    },
549
550    _assignParentsInProfile: function()
551    {
552        var head = this.profile.head;
553        head.parent = null;
554        head.head = null;
555        var nodesToTraverse = [ { parent: head, children: head.children } ];
556        while (nodesToTraverse.length > 0) {
557            var pair = nodesToTraverse.shift();
558            var parent = pair.parent;
559            var children = pair.children;
560            var length = children.length;
561            for (var i = 0; i < length; ++i) {
562                children[i].head = head;
563                children[i].parent = parent;
564                if (children[i].children.length > 0)
565                    nodesToTraverse.push({ parent: children[i], children: children[i].children });
566            }
567        }
568    }
569}
570
571WebInspector.CPUProfileView.prototype.__proto__ = WebInspector.View.prototype;
572
573WebInspector.CPUProfileType = function()
574{
575    WebInspector.ProfileType.call(this, WebInspector.CPUProfileType.TypeId, WebInspector.UIString("CPU PROFILES"));
576    this._recording = false;
577}
578
579WebInspector.CPUProfileType.TypeId = "CPU";
580
581WebInspector.CPUProfileType.prototype = {
582    get buttonTooltip()
583    {
584        return this._recording ? WebInspector.UIString("Stop profiling.") : WebInspector.UIString("Start profiling.");
585    },
586
587    get buttonStyle()
588    {
589        return this._recording ? "record-profile-status-bar-item status-bar-item toggled-on" : "record-profile-status-bar-item status-bar-item";
590    },
591
592    buttonClicked: function()
593    {
594        this._recording = !this._recording;
595
596        if (this._recording)
597            InspectorBackend.startProfiling();
598        else
599            InspectorBackend.stopProfiling();
600    },
601
602    get welcomeMessage()
603    {
604        return WebInspector.UIString("Start CPU profiling by pressing<br>the %s button on the status bar.");
605    },
606
607    setRecordingProfile: function(isProfiling)
608    {
609        this._recording = isProfiling;
610    },
611
612    createSidebarTreeElementForProfile: function(profile)
613    {
614        return new WebInspector.ProfileSidebarTreeElement(profile);
615    },
616
617    createView: function(profile)
618    {
619        return new WebInspector.CPUProfileView(profile);
620    }
621}
622
623WebInspector.CPUProfileType.prototype.__proto__ = WebInspector.ProfileType.prototype;
624