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