Home | History | Annotate | Download | only in front_end
      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 /**
     27  * @constructor
     28  * @extends {WebInspector.View}
     29  * @param {WebInspector.CPUProfileHeader} profileHeader
     30  */
     31 WebInspector.CPUProfileView = function(profileHeader)
     32 {
     33     WebInspector.View.call(this);
     34 
     35     this.element.addStyleClass("profile-view");
     36 
     37     this.showSelfTimeAsPercent = WebInspector.settings.createSetting("cpuProfilerShowSelfTimeAsPercent", true);
     38     this.showTotalTimeAsPercent = WebInspector.settings.createSetting("cpuProfilerShowTotalTimeAsPercent", true);
     39     this.showAverageTimeAsPercent = WebInspector.settings.createSetting("cpuProfilerShowAverageTimeAsPercent", true);
     40     this._viewType = WebInspector.settings.createSetting("cpuProfilerView", WebInspector.CPUProfileView._TypeHeavy);
     41 
     42     var columns = [];
     43     columns.push({id: "self", title: WebInspector.UIString("Self"), width: "72px", sort: WebInspector.DataGrid.Order.Descending, sortable: true});
     44     columns.push({id: "total", title: WebInspector.UIString("Total"), width: "72px", sortable: true});
     45     columns.push({id: "function", title: WebInspector.UIString("Function"), disclosure: true, sortable: true});
     46 
     47     this.dataGrid = new WebInspector.DataGrid(columns);
     48     this.dataGrid.addEventListener(WebInspector.DataGrid.Events.SortingChanged, this._sortProfile, this);
     49     this.dataGrid.element.addEventListener("mousedown", this._mouseDownInDataGrid.bind(this), true);
     50     this.dataGrid.show(this.element);
     51 
     52     this.viewSelectComboBox = new WebInspector.StatusBarComboBox(this._changeView.bind(this));
     53 
     54     var options = {};
     55     options[WebInspector.CPUProfileView._TypeFlame] = this.viewSelectComboBox.createOption(WebInspector.UIString("Flame Chart"), "", WebInspector.CPUProfileView._TypeFlame);
     56     options[WebInspector.CPUProfileView._TypeHeavy] = this.viewSelectComboBox.createOption(WebInspector.UIString("Heavy (Bottom Up)"), "", WebInspector.CPUProfileView._TypeHeavy);
     57     options[WebInspector.CPUProfileView._TypeTree] = this.viewSelectComboBox.createOption(WebInspector.UIString("Tree (Top Down)"), "", WebInspector.CPUProfileView._TypeTree);
     58 
     59     var optionName = this._viewType.get() || WebInspector.CPUProfileView._TypeFlame;
     60     var option = options[optionName] || options[WebInspector.CPUProfileView._TypeFlame];
     61     this.viewSelectComboBox.select(option);
     62 
     63     this._statusBarButtonsElement = document.createElement("span");
     64 
     65     this.percentButton = new WebInspector.StatusBarButton("", "percent-time-status-bar-item");
     66     this.percentButton.addEventListener("click", this._percentClicked, this);
     67     this._statusBarButtonsElement.appendChild(this.percentButton.element);
     68 
     69     this.focusButton = new WebInspector.StatusBarButton(WebInspector.UIString("Focus selected function."), "focus-profile-node-status-bar-item");
     70     this.focusButton.setEnabled(false);
     71     this.focusButton.addEventListener("click", this._focusClicked, this);
     72     this._statusBarButtonsElement.appendChild(this.focusButton.element);
     73 
     74     this.excludeButton = new WebInspector.StatusBarButton(WebInspector.UIString("Exclude selected function."), "exclude-profile-node-status-bar-item");
     75     this.excludeButton.setEnabled(false);
     76     this.excludeButton.addEventListener("click", this._excludeClicked, this);
     77     this._statusBarButtonsElement.appendChild(this.excludeButton.element);
     78 
     79     this.resetButton = new WebInspector.StatusBarButton(WebInspector.UIString("Restore all functions."), "reset-profile-status-bar-item");
     80     this.resetButton.visible = false;
     81     this.resetButton.addEventListener("click", this._resetClicked, this);
     82     this._statusBarButtonsElement.appendChild(this.resetButton.element);
     83 
     84     this.profileHead = /** @type {?ProfilerAgent.CPUProfileNode} */ (null);
     85     this.profile = profileHeader;
     86 
     87     this._linkifier = new WebInspector.Linkifier(new WebInspector.Linkifier.DefaultFormatter(30));
     88 
     89     if (this.profile._profile) // If the profile has been loaded from file then use it.
     90         this._processProfileData(this.profile._profile);
     91     else
     92         ProfilerAgent.getCPUProfile(this.profile.uid, this._getCPUProfileCallback.bind(this));
     93 }
     94 
     95 WebInspector.CPUProfileView._TypeFlame = "Flame";
     96 WebInspector.CPUProfileView._TypeTree = "Tree";
     97 WebInspector.CPUProfileView._TypeHeavy = "Heavy";
     98 
     99 WebInspector.CPUProfileView.prototype = {
    100     /**
    101      * @param {!number} timeLeft
    102      * @param {!number} timeRight
    103      */
    104     selectRange: function(timeLeft, timeRight)
    105     {
    106         if (!this._flameChart)
    107             return;
    108         this._flameChart.selectRange(timeLeft, timeRight);
    109     },
    110 
    111     _revealProfilerNode: function(event)
    112     {
    113         var current = this.profileDataGridTree.children[0];
    114 
    115         while (current && current.profileNode !== event.data)
    116             current = current.traverseNextNode(false, null, false);
    117 
    118         if (current)
    119             current.revealAndSelect();
    120     },
    121 
    122     /**
    123      * @param {?Protocol.Error} error
    124      * @param {ProfilerAgent.CPUProfile} profile
    125      */
    126     _getCPUProfileCallback: function(error, profile)
    127     {
    128         if (error)
    129             return;
    130 
    131         if (!profile.head) {
    132             // Profiling was tentatively terminated with the "Clear all profiles." button.
    133             return;
    134         }
    135 
    136         this._processProfileData(profile);
    137     },
    138 
    139     _processProfileData: function(profile)
    140     {
    141         this.profileHead = profile.head;
    142         this.samples = profile.samples;
    143 
    144         this._calculateTimes(profile);
    145 
    146         if (profile.idleTime)
    147             this._injectIdleTimeNode(profile);
    148 
    149         this._assignParentsInProfile();
    150         if (this.samples)
    151             this._buildIdToNodeMap();
    152         this._changeView();
    153         this._updatePercentButton();
    154         if (this._flameChart)
    155             this._flameChart.update();
    156     },
    157 
    158     get statusBarItems()
    159     {
    160         return [this.viewSelectComboBox.element, this._statusBarButtonsElement];
    161     },
    162 
    163     /**
    164      * @return {!WebInspector.ProfileDataGridTree}
    165      */
    166     _getBottomUpProfileDataGridTree: function()
    167     {
    168         if (!this._bottomUpProfileDataGridTree)
    169             this._bottomUpProfileDataGridTree = new WebInspector.BottomUpProfileDataGridTree(this, this.profileHead);
    170         return this._bottomUpProfileDataGridTree;
    171     },
    172 
    173     /**
    174      * @return {!WebInspector.ProfileDataGridTree}
    175      */
    176     _getTopDownProfileDataGridTree: function()
    177     {
    178         if (!this._topDownProfileDataGridTree)
    179             this._topDownProfileDataGridTree = new WebInspector.TopDownProfileDataGridTree(this, this.profileHead);
    180         return this._topDownProfileDataGridTree;
    181     },
    182 
    183     willHide: function()
    184     {
    185         this._currentSearchResultIndex = -1;
    186     },
    187 
    188     refresh: function()
    189     {
    190         var selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null;
    191 
    192         this.dataGrid.rootNode().removeChildren();
    193 
    194         var children = this.profileDataGridTree.children;
    195         var count = children.length;
    196 
    197         for (var index = 0; index < count; ++index)
    198             this.dataGrid.rootNode().appendChild(children[index]);
    199 
    200         if (selectedProfileNode)
    201             selectedProfileNode.selected = true;
    202     },
    203 
    204     refreshVisibleData: function()
    205     {
    206         var child = this.dataGrid.rootNode().children[0];
    207         while (child) {
    208             child.refresh();
    209             child = child.traverseNextNode(false, null, true);
    210         }
    211     },
    212 
    213     refreshShowAsPercents: function()
    214     {
    215         this._updatePercentButton();
    216         this.refreshVisibleData();
    217     },
    218 
    219     searchCanceled: function()
    220     {
    221         if (this._searchResults) {
    222             for (var i = 0; i < this._searchResults.length; ++i) {
    223                 var profileNode = this._searchResults[i].profileNode;
    224 
    225                 delete profileNode._searchMatchedSelfColumn;
    226                 delete profileNode._searchMatchedTotalColumn;
    227                 delete profileNode._searchMatchedFunctionColumn;
    228 
    229                 profileNode.refresh();
    230             }
    231         }
    232 
    233         delete this._searchFinishedCallback;
    234         this._currentSearchResultIndex = -1;
    235         this._searchResults = [];
    236     },
    237 
    238     performSearch: function(query, finishedCallback)
    239     {
    240         // Call searchCanceled since it will reset everything we need before doing a new search.
    241         this.searchCanceled();
    242 
    243         query = query.trim();
    244 
    245         if (!query.length)
    246             return;
    247 
    248         this._searchFinishedCallback = finishedCallback;
    249 
    250         var greaterThan = (query.startsWith(">"));
    251         var lessThan = (query.startsWith("<"));
    252         var equalTo = (query.startsWith("=") || ((greaterThan || lessThan) && query.indexOf("=") === 1));
    253         var percentUnits = (query.lastIndexOf("%") === (query.length - 1));
    254         var millisecondsUnits = (query.length > 2 && query.lastIndexOf("ms") === (query.length - 2));
    255         var secondsUnits = (!millisecondsUnits && query.lastIndexOf("s") === (query.length - 1));
    256 
    257         var queryNumber = parseFloat(query);
    258         if (greaterThan || lessThan || equalTo) {
    259             if (equalTo && (greaterThan || lessThan))
    260                 queryNumber = parseFloat(query.substring(2));
    261             else
    262                 queryNumber = parseFloat(query.substring(1));
    263         }
    264 
    265         var queryNumberMilliseconds = (secondsUnits ? (queryNumber * 1000) : queryNumber);
    266 
    267         // Make equalTo implicitly true if it wasn't specified there is no other operator.
    268         if (!isNaN(queryNumber) && !(greaterThan || lessThan))
    269             equalTo = true;
    270 
    271         var matcher = createPlainTextSearchRegex(query, "i");
    272 
    273         function matchesQuery(/*ProfileDataGridNode*/ profileDataGridNode)
    274         {
    275             delete profileDataGridNode._searchMatchedSelfColumn;
    276             delete profileDataGridNode._searchMatchedTotalColumn;
    277             delete profileDataGridNode._searchMatchedFunctionColumn;
    278 
    279             if (percentUnits) {
    280                 if (lessThan) {
    281                     if (profileDataGridNode.selfPercent < queryNumber)
    282                         profileDataGridNode._searchMatchedSelfColumn = true;
    283                     if (profileDataGridNode.totalPercent < queryNumber)
    284                         profileDataGridNode._searchMatchedTotalColumn = true;
    285                 } else if (greaterThan) {
    286                     if (profileDataGridNode.selfPercent > queryNumber)
    287                         profileDataGridNode._searchMatchedSelfColumn = true;
    288                     if (profileDataGridNode.totalPercent > queryNumber)
    289                         profileDataGridNode._searchMatchedTotalColumn = true;
    290                 }
    291 
    292                 if (equalTo) {
    293                     if (profileDataGridNode.selfPercent == queryNumber)
    294                         profileDataGridNode._searchMatchedSelfColumn = true;
    295                     if (profileDataGridNode.totalPercent == queryNumber)
    296                         profileDataGridNode._searchMatchedTotalColumn = true;
    297                 }
    298             } else if (millisecondsUnits || secondsUnits) {
    299                 if (lessThan) {
    300                     if (profileDataGridNode.selfTime < queryNumberMilliseconds)
    301                         profileDataGridNode._searchMatchedSelfColumn = true;
    302                     if (profileDataGridNode.totalTime < queryNumberMilliseconds)
    303                         profileDataGridNode._searchMatchedTotalColumn = true;
    304                 } else if (greaterThan) {
    305                     if (profileDataGridNode.selfTime > queryNumberMilliseconds)
    306                         profileDataGridNode._searchMatchedSelfColumn = true;
    307                     if (profileDataGridNode.totalTime > queryNumberMilliseconds)
    308                         profileDataGridNode._searchMatchedTotalColumn = true;
    309                 }
    310 
    311                 if (equalTo) {
    312                     if (profileDataGridNode.selfTime == queryNumberMilliseconds)
    313                         profileDataGridNode._searchMatchedSelfColumn = true;
    314                     if (profileDataGridNode.totalTime == queryNumberMilliseconds)
    315                         profileDataGridNode._searchMatchedTotalColumn = true;
    316                 }
    317             }
    318 
    319             if (profileDataGridNode.functionName.match(matcher) || (profileDataGridNode.url && profileDataGridNode.url.match(matcher)))
    320                 profileDataGridNode._searchMatchedFunctionColumn = true;
    321 
    322             if (profileDataGridNode._searchMatchedSelfColumn ||
    323                 profileDataGridNode._searchMatchedTotalColumn ||
    324                 profileDataGridNode._searchMatchedFunctionColumn)
    325             {
    326                 profileDataGridNode.refresh();
    327                 return true;
    328             }
    329 
    330             return false;
    331         }
    332 
    333         var current = this.profileDataGridTree.children[0];
    334 
    335         while (current) {
    336             if (matchesQuery(current)) {
    337                 this._searchResults.push({ profileNode: current });
    338             }
    339 
    340             current = current.traverseNextNode(false, null, false);
    341         }
    342 
    343         finishedCallback(this, this._searchResults.length);
    344     },
    345 
    346     jumpToFirstSearchResult: function()
    347     {
    348         if (!this._searchResults || !this._searchResults.length)
    349             return;
    350         this._currentSearchResultIndex = 0;
    351         this._jumpToSearchResult(this._currentSearchResultIndex);
    352     },
    353 
    354     jumpToLastSearchResult: function()
    355     {
    356         if (!this._searchResults || !this._searchResults.length)
    357             return;
    358         this._currentSearchResultIndex = (this._searchResults.length - 1);
    359         this._jumpToSearchResult(this._currentSearchResultIndex);
    360     },
    361 
    362     jumpToNextSearchResult: function()
    363     {
    364         if (!this._searchResults || !this._searchResults.length)
    365             return;
    366         if (++this._currentSearchResultIndex >= this._searchResults.length)
    367             this._currentSearchResultIndex = 0;
    368         this._jumpToSearchResult(this._currentSearchResultIndex);
    369     },
    370 
    371     jumpToPreviousSearchResult: function()
    372     {
    373         if (!this._searchResults || !this._searchResults.length)
    374             return;
    375         if (--this._currentSearchResultIndex < 0)
    376             this._currentSearchResultIndex = (this._searchResults.length - 1);
    377         this._jumpToSearchResult(this._currentSearchResultIndex);
    378     },
    379 
    380     showingFirstSearchResult: function()
    381     {
    382         return (this._currentSearchResultIndex === 0);
    383     },
    384 
    385     showingLastSearchResult: function()
    386     {
    387         return (this._searchResults && this._currentSearchResultIndex === (this._searchResults.length - 1));
    388     },
    389 
    390     _jumpToSearchResult: function(index)
    391     {
    392         var searchResult = this._searchResults[index];
    393         if (!searchResult)
    394             return;
    395 
    396         var profileNode = searchResult.profileNode;
    397         profileNode.revealAndSelect();
    398     },
    399 
    400     _ensureFlameChartCreated: function()
    401     {
    402         if (this._flameChart)
    403             return;
    404         this._flameChart = new WebInspector.FlameChart(this);
    405         this._flameChart.addEventListener(WebInspector.FlameChart.Events.SelectedNode, this._onSelectedNode.bind(this));
    406     },
    407 
    408     /**
    409      * @param {WebInspector.Event} event
    410      */
    411     _onSelectedNode: function(event)
    412     {
    413         var node = event.data;
    414         if (!node || !node.scriptId)
    415             return;
    416         var script = WebInspector.debuggerModel.scriptForId(node.scriptId)
    417         if (!script)
    418             return;
    419         var uiLocation = script.rawLocationToUILocation(node.lineNumber);
    420         if (!uiLocation)
    421             return;
    422         WebInspector.showPanel("scripts").showUISourceCode(uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber);
    423     },
    424 
    425     _changeView: function()
    426     {
    427         if (!this.profile)
    428             return;
    429 
    430         switch (this.viewSelectComboBox.selectedOption().value) {
    431         case WebInspector.CPUProfileView._TypeFlame:
    432             this._ensureFlameChartCreated();
    433             this.dataGrid.detach();
    434             this._flameChart.show(this.element);
    435             this._viewType.set(WebInspector.CPUProfileView._TypeFlame);
    436             this._statusBarButtonsElement.enableStyleClass("hidden", true);
    437             return;
    438         case WebInspector.CPUProfileView._TypeTree:
    439             this.profileDataGridTree = this._getTopDownProfileDataGridTree();
    440             this._sortProfile();
    441             this._viewType.set(WebInspector.CPUProfileView._TypeTree);
    442             break;
    443         case WebInspector.CPUProfileView._TypeHeavy:
    444             this.profileDataGridTree = this._getBottomUpProfileDataGridTree();
    445             this._sortProfile();
    446             this._viewType.set(WebInspector.CPUProfileView._TypeHeavy);
    447             break;
    448         }
    449 
    450         this._statusBarButtonsElement.enableStyleClass("hidden", false);
    451 
    452         if (this._flameChart)
    453             this._flameChart.detach();
    454         this.dataGrid.show(this.element);
    455 
    456         if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults)
    457             return;
    458 
    459         // The current search needs to be performed again. First negate out previous match
    460         // count by calling the search finished callback with a negative number of matches.
    461         // Then perform the search again the with same query and callback.
    462         this._searchFinishedCallback(this, -this._searchResults.length);
    463         this.performSearch(this.currentQuery, this._searchFinishedCallback);
    464     },
    465 
    466     _percentClicked: function(event)
    467     {
    468         var currentState = this.showSelfTimeAsPercent.get() && this.showTotalTimeAsPercent.get() && this.showAverageTimeAsPercent.get();
    469         this.showSelfTimeAsPercent.set(!currentState);
    470         this.showTotalTimeAsPercent.set(!currentState);
    471         this.showAverageTimeAsPercent.set(!currentState);
    472         this.refreshShowAsPercents();
    473     },
    474 
    475     _updatePercentButton: function()
    476     {
    477         if (this.showSelfTimeAsPercent.get() && this.showTotalTimeAsPercent.get() && this.showAverageTimeAsPercent.get()) {
    478             this.percentButton.title = WebInspector.UIString("Show absolute total and self times.");
    479             this.percentButton.toggled = true;
    480         } else {
    481             this.percentButton.title = WebInspector.UIString("Show total and self times as percentages.");
    482             this.percentButton.toggled = false;
    483         }
    484     },
    485 
    486     _focusClicked: function(event)
    487     {
    488         if (!this.dataGrid.selectedNode)
    489             return;
    490 
    491         this.resetButton.visible = true;
    492         this.profileDataGridTree.focus(this.dataGrid.selectedNode);
    493         this.refresh();
    494         this.refreshVisibleData();
    495     },
    496 
    497     _excludeClicked: function(event)
    498     {
    499         var selectedNode = this.dataGrid.selectedNode
    500 
    501         if (!selectedNode)
    502             return;
    503 
    504         selectedNode.deselect();
    505 
    506         this.resetButton.visible = true;
    507         this.profileDataGridTree.exclude(selectedNode);
    508         this.refresh();
    509         this.refreshVisibleData();
    510     },
    511 
    512     _resetClicked: function(event)
    513     {
    514         this.resetButton.visible = false;
    515         this.profileDataGridTree.restore();
    516         this._linkifier.reset();
    517         this.refresh();
    518         this.refreshVisibleData();
    519     },
    520 
    521     _dataGridNodeSelected: function(node)
    522     {
    523         this.focusButton.setEnabled(true);
    524         this.excludeButton.setEnabled(true);
    525     },
    526 
    527     _dataGridNodeDeselected: function(node)
    528     {
    529         this.focusButton.setEnabled(false);
    530         this.excludeButton.setEnabled(false);
    531     },
    532 
    533     _sortProfile: function()
    534     {
    535         var sortAscending = this.dataGrid.isSortOrderAscending();
    536         var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier();
    537         var sortProperty = {
    538                 "self": "selfTime",
    539                 "total": "totalTime",
    540                 "function": "functionName"
    541             }[sortColumnIdentifier];
    542 
    543         this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator(sortProperty, sortAscending));
    544 
    545         this.refresh();
    546     },
    547 
    548     _mouseDownInDataGrid: function(event)
    549     {
    550         if (event.detail < 2)
    551             return;
    552 
    553         var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
    554         if (!cell || (!cell.hasStyleClass("total-column") && !cell.hasStyleClass("self-column") && !cell.hasStyleClass("average-column")))
    555             return;
    556 
    557         if (cell.hasStyleClass("total-column"))
    558             this.showTotalTimeAsPercent.set(!this.showTotalTimeAsPercent.get());
    559         else if (cell.hasStyleClass("self-column"))
    560             this.showSelfTimeAsPercent.set(!this.showSelfTimeAsPercent.get());
    561         else if (cell.hasStyleClass("average-column"))
    562             this.showAverageTimeAsPercent.set(!this.showAverageTimeAsPercent.get());
    563 
    564         this.refreshShowAsPercents();
    565 
    566         event.consume(true);
    567     },
    568 
    569     _calculateTimes: function(profile)
    570     {
    571         function totalHitCount(node) {
    572             var result = node.hitCount;
    573             for (var i = 0; i < node.children.length; i++)
    574                 result += totalHitCount(node.children[i]);
    575             return result;
    576         }
    577         profile.totalHitCount = totalHitCount(profile.head);
    578 
    579         var durationMs = 1000 * profile.endTime - 1000 * profile.startTime;
    580         var samplingRate = profile.totalHitCount / durationMs;
    581 
    582         function calculateTimesForNode(node) {
    583             node.selfTime = node.hitCount * samplingRate;
    584             var totalTime = node.selfTime;
    585             for (var i = 0; i < node.children.length; i++)
    586                 totalTime += calculateTimesForNode(node.children[i]);
    587             node.totalTime = totalTime;
    588             return totalTime;
    589         }
    590         calculateTimesForNode(profile.head);
    591     },
    592 
    593     _assignParentsInProfile: function()
    594     {
    595         var head = this.profileHead;
    596         head.parent = null;
    597         head.head = null;
    598         var nodesToTraverse = [ { parent: head, children: head.children } ];
    599         while (nodesToTraverse.length > 0) {
    600             var pair = nodesToTraverse.pop();
    601             var parent = pair.parent;
    602             var children = pair.children;
    603             var length = children.length;
    604             for (var i = 0; i < length; ++i) {
    605                 children[i].head = head;
    606                 children[i].parent = parent;
    607                 if (children[i].children.length > 0)
    608                     nodesToTraverse.push({ parent: children[i], children: children[i].children });
    609             }
    610         }
    611     },
    612 
    613     _buildIdToNodeMap: function()
    614     {
    615         var idToNode = this._idToNode = {};
    616         var stack = [this.profileHead];
    617         while (stack.length) {
    618             var node = stack.pop();
    619             idToNode[node.id] = node;
    620             for (var i = 0; i < node.children.length; i++)
    621                 stack.push(node.children[i]);
    622         }
    623 
    624         var topLevelNodes = this.profileHead.children;
    625         for (var i = 0; i < topLevelNodes.length; i++) {
    626             var node = topLevelNodes[i];
    627             if (node.functionName == "(garbage collector)") {
    628                 this._gcNode = node;
    629                 break;
    630             }
    631         }
    632     },
    633 
    634     /**
    635      * @param {ProfilerAgent.CPUProfile} profile
    636      */
    637     _injectIdleTimeNode: function(profile)
    638     {
    639         var idleTime = profile.idleTime;
    640         var nodes = profile.head.children;
    641 
    642         var programNode = {selfTime: 0};
    643         for (var i = nodes.length - 1; i >= 0; --i) {
    644             if (nodes[i].functionName === "(program)") {
    645                 programNode = nodes[i];
    646                 break;
    647             }
    648         }
    649         var programTime = programNode.selfTime;
    650         if (idleTime > programTime)
    651             idleTime = programTime;
    652         programTime = programTime - idleTime;
    653         programNode.selfTime = programTime;
    654         programNode.totalTime = programTime;
    655         var idleNode = {
    656             functionName: "(idle)",
    657             url: null,
    658             lineNumber: 0,
    659             totalTime: idleTime,
    660             selfTime: idleTime,
    661             callUID: 0,
    662             children: []
    663         };
    664         nodes.push(idleNode);
    665     },
    666 
    667     __proto__: WebInspector.View.prototype
    668 }
    669 
    670 /**
    671  * @constructor
    672  * @extends {WebInspector.ProfileType}
    673  * @implements {ProfilerAgent.Dispatcher}
    674  */
    675 WebInspector.CPUProfileType = function()
    676 {
    677     WebInspector.ProfileType.call(this, WebInspector.CPUProfileType.TypeId, WebInspector.UIString("Collect JavaScript CPU Profile"));
    678     InspectorBackend.registerProfilerDispatcher(this);
    679     this._recording = false;
    680     WebInspector.CPUProfileType.instance = this;
    681 }
    682 
    683 WebInspector.CPUProfileType.TypeId = "CPU";
    684 
    685 WebInspector.CPUProfileType.prototype = {
    686     /**
    687      * @override
    688      * @return {string}
    689      */
    690     fileExtension: function()
    691     {
    692         return ".cpuprofile";
    693     },
    694 
    695     get buttonTooltip()
    696     {
    697         return this._recording ? WebInspector.UIString("Stop CPU profiling.") : WebInspector.UIString("Start CPU profiling.");
    698     },
    699 
    700     /**
    701      * @override
    702      * @return {boolean}
    703      */
    704     buttonClicked: function()
    705     {
    706         if (this._recording) {
    707             this.stopRecordingProfile();
    708             return false;
    709         } else {
    710             this.startRecordingProfile();
    711             return true;
    712         }
    713     },
    714 
    715     get treeItemTitle()
    716     {
    717         return WebInspector.UIString("CPU PROFILES");
    718     },
    719 
    720     get description()
    721     {
    722         return WebInspector.UIString("CPU profiles show where the execution time is spent in your page's JavaScript functions.");
    723     },
    724 
    725     /**
    726      * @param {ProfilerAgent.ProfileHeader} profileHeader
    727      */
    728     addProfileHeader: function(profileHeader)
    729     {
    730         this.addProfile(this.createProfile(profileHeader));
    731     },
    732 
    733     isRecordingProfile: function()
    734     {
    735         return this._recording;
    736     },
    737 
    738     startRecordingProfile: function()
    739     {
    740         this._recording = true;
    741         WebInspector.userMetrics.ProfilesCPUProfileTaken.record();
    742         ProfilerAgent.start();
    743     },
    744 
    745     stopRecordingProfile: function()
    746     {
    747         this._recording = false;
    748         ProfilerAgent.stop();
    749     },
    750 
    751     /**
    752      * @param {boolean} isProfiling
    753      */
    754     setRecordingProfile: function(isProfiling)
    755     {
    756         this._recording = isProfiling;
    757     },
    758 
    759     /**
    760      * @override
    761      * @param {string=} title
    762      * @return {!WebInspector.ProfileHeader}
    763      */
    764     createTemporaryProfile: function(title)
    765     {
    766         title = title || WebInspector.UIString("Recording\u2026");
    767         return new WebInspector.CPUProfileHeader(this, title);
    768     },
    769 
    770     /**
    771      * @override
    772      * @param {ProfilerAgent.ProfileHeader} profile
    773      * @return {!WebInspector.ProfileHeader}
    774      */
    775     createProfile: function(profile)
    776     {
    777         return new WebInspector.CPUProfileHeader(this, profile.title, profile.uid);
    778     },
    779 
    780     /**
    781      * @override
    782      * @param {!WebInspector.ProfileHeader} profile
    783      */
    784     removeProfile: function(profile)
    785     {
    786         WebInspector.ProfileType.prototype.removeProfile.call(this, profile);
    787         if (!profile.isTemporary)
    788             ProfilerAgent.removeProfile(this.id, profile.uid);
    789     },
    790 
    791     /**
    792      * @override
    793      * @param {function(this:WebInspector.ProfileType, ?string, Array.<ProfilerAgent.ProfileHeader>)} populateCallback
    794      */
    795     _requestProfilesFromBackend: function(populateCallback)
    796     {
    797         ProfilerAgent.getProfileHeaders(populateCallback);
    798     },
    799 
    800     /**
    801      * @override
    802      */
    803     resetProfiles: function()
    804     {
    805         this._reset();
    806     },
    807 
    808     /** @deprecated To be removed from the protocol */
    809     addHeapSnapshotChunk: function(uid, chunk)
    810     {
    811         throw new Error("Never called");
    812     },
    813 
    814     /** @deprecated To be removed from the protocol */
    815     finishHeapSnapshot: function(uid)
    816     {
    817         throw new Error("Never called");
    818     },
    819 
    820     /** @deprecated To be removed from the protocol */
    821     reportHeapSnapshotProgress: function(done, total)
    822     {
    823         throw new Error("Never called");
    824     },
    825 
    826     __proto__: WebInspector.ProfileType.prototype
    827 }
    828 
    829 /**
    830  * @constructor
    831  * @extends {WebInspector.ProfileHeader}
    832  * @implements {WebInspector.OutputStream}
    833  * @implements {WebInspector.OutputStreamDelegate}
    834  * @param {!WebInspector.CPUProfileType} type
    835  * @param {string} title
    836  * @param {number=} uid
    837  */
    838 WebInspector.CPUProfileHeader = function(type, title, uid)
    839 {
    840     WebInspector.ProfileHeader.call(this, type, title, uid);
    841 }
    842 
    843 WebInspector.CPUProfileHeader.prototype = {
    844     onTransferStarted: function()
    845     {
    846         this._jsonifiedProfile = "";
    847         this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026 %s", Number.bytesToString(this._jsonifiedProfile.length));
    848     },
    849 
    850     /**
    851      * @param {WebInspector.ChunkedReader} reader
    852      */
    853     onChunkTransferred: function(reader)
    854     {
    855         this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026 %d\%", Number.bytesToString(this._jsonifiedProfile.length));
    856     },
    857 
    858     onTransferFinished: function()
    859     {
    860 
    861         this.sidebarElement.subtitle = WebInspector.UIString("Parsing\u2026");
    862         this._profile = JSON.parse(this._jsonifiedProfile);
    863         this._jsonifiedProfile = null;
    864         this.sidebarElement.subtitle = WebInspector.UIString("Loaded");
    865         this.isTemporary = false;
    866     },
    867 
    868     /**
    869      * @param {WebInspector.ChunkedReader} reader
    870      */
    871     onError: function(reader, e)
    872     {
    873         switch(e.target.error.code) {
    874         case e.target.error.NOT_FOUND_ERR:
    875             this.sidebarElement.subtitle = WebInspector.UIString("'%s' not found.", reader.fileName());
    876         break;
    877         case e.target.error.NOT_READABLE_ERR:
    878             this.sidebarElement.subtitle = WebInspector.UIString("'%s' is not readable", reader.fileName());
    879         break;
    880         case e.target.error.ABORT_ERR:
    881             break;
    882         default:
    883             this.sidebarElement.subtitle = WebInspector.UIString("'%s' error %d", reader.fileName(), e.target.error.code);
    884         }
    885     },
    886 
    887     /**
    888      * @param {string} text
    889      */
    890     write: function(text)
    891     {
    892         this._jsonifiedProfile += text;
    893     },
    894 
    895     close: function() { },
    896 
    897     /**
    898      * @override
    899      */
    900     createSidebarTreeElement: function()
    901     {
    902         return new WebInspector.ProfileSidebarTreeElement(this, WebInspector.UIString("Profile %d"), "profile-sidebar-tree-item");
    903     },
    904 
    905     /**
    906      * @override
    907      * @param {WebInspector.ProfilesPanel} profilesPanel
    908      */
    909     createView: function(profilesPanel)
    910     {
    911         return new WebInspector.CPUProfileView(this);
    912     },
    913 
    914     /**
    915      * @override
    916      * @return {boolean}
    917      */
    918     canSaveToFile: function()
    919     {
    920         return true;
    921     },
    922 
    923     saveToFile: function()
    924     {
    925         var fileOutputStream = new WebInspector.FileOutputStream();
    926 
    927         /**
    928          * @param {?Protocol.Error} error
    929          * @param {ProfilerAgent.CPUProfile} profile
    930          */
    931         function getCPUProfileCallback(error, profile)
    932         {
    933             if (error) {
    934                 fileOutputStream.close();
    935                 return;
    936             }
    937 
    938             if (!profile.head) {
    939                 // Profiling was tentatively terminated with the "Clear all profiles." button.
    940                 fileOutputStream.close();
    941                 return;
    942             }
    943 
    944             fileOutputStream.write(JSON.stringify(profile), fileOutputStream.close.bind(fileOutputStream));
    945         }
    946 
    947         function onOpen()
    948         {
    949             ProfilerAgent.getCPUProfile(this.uid, getCPUProfileCallback.bind(this));
    950         }
    951 
    952         this._fileName = this._fileName || "CPU-" + new Date().toISO8601Compact() + this._profileType.fileExtension();
    953         fileOutputStream.open(this._fileName, onOpen.bind(this));
    954     },
    955 
    956     /**
    957      * @param {File} file
    958      */
    959     loadFromFile: function(file)
    960     {
    961         this.title = file.name;
    962         this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026");
    963         this.sidebarElement.wait = true;
    964 
    965         var fileReader = new WebInspector.ChunkedFileReader(file, 10000000, this);
    966         fileReader.start(this);
    967     },
    968 
    969     __proto__: WebInspector.ProfileHeader.prototype
    970 }
    971