• 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/**
27 * @constructor
28 * @extends {WebInspector.View}
29 * @param {!WebInspector.CPUProfileHeader} profileHeader
30 */
31WebInspector.CPUProfileView = function(profileHeader)
32{
33    WebInspector.View.call(this);
34
35    this.element.classList.add("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        this._processProfileData(this.profile.protocolProfile());
93}
94
95WebInspector.CPUProfileView._TypeFlame = "Flame";
96WebInspector.CPUProfileView._TypeTree = "Tree";
97WebInspector.CPUProfileView._TypeHeavy = "Heavy";
98
99WebInspector.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 {?ProfilerAgent.CPUProfile} profile
124     */
125    _processProfileData: function(profile)
126    {
127        this.profileHead = profile.head;
128        this.samples = profile.samples;
129
130        this._calculateTimes(profile);
131
132        this._assignParentsInProfile();
133        if (this.samples)
134            this._buildIdToNodeMap();
135        this._changeView();
136        this._updatePercentButton();
137        if (this._flameChart)
138            this._flameChart.update();
139    },
140
141    get statusBarItems()
142    {
143        return [this.viewSelectComboBox.element, this._statusBarButtonsElement];
144    },
145
146    /**
147     * @return {!WebInspector.ProfileDataGridTree}
148     */
149    _getBottomUpProfileDataGridTree: function()
150    {
151        if (!this._bottomUpProfileDataGridTree)
152            this._bottomUpProfileDataGridTree = new WebInspector.BottomUpProfileDataGridTree(this, /** @type {!ProfilerAgent.CPUProfileNode} */ (this.profileHead));
153        return this._bottomUpProfileDataGridTree;
154    },
155
156    /**
157     * @return {!WebInspector.ProfileDataGridTree}
158     */
159    _getTopDownProfileDataGridTree: function()
160    {
161        if (!this._topDownProfileDataGridTree)
162            this._topDownProfileDataGridTree = new WebInspector.TopDownProfileDataGridTree(this, /** @type {!ProfilerAgent.CPUProfileNode} */ (this.profileHead));
163        return this._topDownProfileDataGridTree;
164    },
165
166    willHide: function()
167    {
168        this._currentSearchResultIndex = -1;
169    },
170
171    refresh: function()
172    {
173        var selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null;
174
175        this.dataGrid.rootNode().removeChildren();
176
177        var children = this.profileDataGridTree.children;
178        var count = children.length;
179
180        for (var index = 0; index < count; ++index)
181            this.dataGrid.rootNode().appendChild(children[index]);
182
183        if (selectedProfileNode)
184            selectedProfileNode.selected = true;
185    },
186
187    refreshVisibleData: function()
188    {
189        var child = this.dataGrid.rootNode().children[0];
190        while (child) {
191            child.refresh();
192            child = child.traverseNextNode(false, null, true);
193        }
194    },
195
196    refreshShowAsPercents: function()
197    {
198        this._updatePercentButton();
199        this.refreshVisibleData();
200    },
201
202    searchCanceled: function()
203    {
204        if (this._searchResults) {
205            for (var i = 0; i < this._searchResults.length; ++i) {
206                var profileNode = this._searchResults[i].profileNode;
207
208                delete profileNode._searchMatchedSelfColumn;
209                delete profileNode._searchMatchedTotalColumn;
210                delete profileNode._searchMatchedFunctionColumn;
211
212                profileNode.refresh();
213            }
214        }
215
216        delete this._searchFinishedCallback;
217        this._currentSearchResultIndex = -1;
218        this._searchResults = [];
219    },
220
221    performSearch: function(query, finishedCallback)
222    {
223        // Call searchCanceled since it will reset everything we need before doing a new search.
224        this.searchCanceled();
225
226        query = query.trim();
227
228        if (!query.length)
229            return;
230
231        this._searchFinishedCallback = finishedCallback;
232
233        var greaterThan = (query.startsWith(">"));
234        var lessThan = (query.startsWith("<"));
235        var equalTo = (query.startsWith("=") || ((greaterThan || lessThan) && query.indexOf("=") === 1));
236        var percentUnits = (query.lastIndexOf("%") === (query.length - 1));
237        var millisecondsUnits = (query.length > 2 && query.lastIndexOf("ms") === (query.length - 2));
238        var secondsUnits = (!millisecondsUnits && query.lastIndexOf("s") === (query.length - 1));
239
240        var queryNumber = parseFloat(query);
241        if (greaterThan || lessThan || equalTo) {
242            if (equalTo && (greaterThan || lessThan))
243                queryNumber = parseFloat(query.substring(2));
244            else
245                queryNumber = parseFloat(query.substring(1));
246        }
247
248        var queryNumberMilliseconds = (secondsUnits ? (queryNumber * 1000) : queryNumber);
249
250        // Make equalTo implicitly true if it wasn't specified there is no other operator.
251        if (!isNaN(queryNumber) && !(greaterThan || lessThan))
252            equalTo = true;
253
254        var matcher = createPlainTextSearchRegex(query, "i");
255
256        function matchesQuery(/*ProfileDataGridNode*/ profileDataGridNode)
257        {
258            delete profileDataGridNode._searchMatchedSelfColumn;
259            delete profileDataGridNode._searchMatchedTotalColumn;
260            delete profileDataGridNode._searchMatchedFunctionColumn;
261
262            if (percentUnits) {
263                if (lessThan) {
264                    if (profileDataGridNode.selfPercent < queryNumber)
265                        profileDataGridNode._searchMatchedSelfColumn = true;
266                    if (profileDataGridNode.totalPercent < queryNumber)
267                        profileDataGridNode._searchMatchedTotalColumn = true;
268                } else if (greaterThan) {
269                    if (profileDataGridNode.selfPercent > queryNumber)
270                        profileDataGridNode._searchMatchedSelfColumn = true;
271                    if (profileDataGridNode.totalPercent > queryNumber)
272                        profileDataGridNode._searchMatchedTotalColumn = true;
273                }
274
275                if (equalTo) {
276                    if (profileDataGridNode.selfPercent == queryNumber)
277                        profileDataGridNode._searchMatchedSelfColumn = true;
278                    if (profileDataGridNode.totalPercent == queryNumber)
279                        profileDataGridNode._searchMatchedTotalColumn = true;
280                }
281            } else if (millisecondsUnits || secondsUnits) {
282                if (lessThan) {
283                    if (profileDataGridNode.selfTime < queryNumberMilliseconds)
284                        profileDataGridNode._searchMatchedSelfColumn = true;
285                    if (profileDataGridNode.totalTime < queryNumberMilliseconds)
286                        profileDataGridNode._searchMatchedTotalColumn = true;
287                } else if (greaterThan) {
288                    if (profileDataGridNode.selfTime > queryNumberMilliseconds)
289                        profileDataGridNode._searchMatchedSelfColumn = true;
290                    if (profileDataGridNode.totalTime > queryNumberMilliseconds)
291                        profileDataGridNode._searchMatchedTotalColumn = true;
292                }
293
294                if (equalTo) {
295                    if (profileDataGridNode.selfTime == queryNumberMilliseconds)
296                        profileDataGridNode._searchMatchedSelfColumn = true;
297                    if (profileDataGridNode.totalTime == queryNumberMilliseconds)
298                        profileDataGridNode._searchMatchedTotalColumn = true;
299                }
300            }
301
302            if (profileDataGridNode.functionName.match(matcher) || (profileDataGridNode.url && profileDataGridNode.url.match(matcher)))
303                profileDataGridNode._searchMatchedFunctionColumn = true;
304
305            if (profileDataGridNode._searchMatchedSelfColumn ||
306                profileDataGridNode._searchMatchedTotalColumn ||
307                profileDataGridNode._searchMatchedFunctionColumn)
308            {
309                profileDataGridNode.refresh();
310                return true;
311            }
312
313            return false;
314        }
315
316        var current = this.profileDataGridTree.children[0];
317
318        while (current) {
319            if (matchesQuery(current)) {
320                this._searchResults.push({ profileNode: current });
321            }
322
323            current = current.traverseNextNode(false, null, false);
324        }
325
326        finishedCallback(this, this._searchResults.length);
327    },
328
329    jumpToFirstSearchResult: function()
330    {
331        if (!this._searchResults || !this._searchResults.length)
332            return;
333        this._currentSearchResultIndex = 0;
334        this._jumpToSearchResult(this._currentSearchResultIndex);
335    },
336
337    jumpToLastSearchResult: function()
338    {
339        if (!this._searchResults || !this._searchResults.length)
340            return;
341        this._currentSearchResultIndex = (this._searchResults.length - 1);
342        this._jumpToSearchResult(this._currentSearchResultIndex);
343    },
344
345    jumpToNextSearchResult: function()
346    {
347        if (!this._searchResults || !this._searchResults.length)
348            return;
349        if (++this._currentSearchResultIndex >= this._searchResults.length)
350            this._currentSearchResultIndex = 0;
351        this._jumpToSearchResult(this._currentSearchResultIndex);
352    },
353
354    jumpToPreviousSearchResult: function()
355    {
356        if (!this._searchResults || !this._searchResults.length)
357            return;
358        if (--this._currentSearchResultIndex < 0)
359            this._currentSearchResultIndex = (this._searchResults.length - 1);
360        this._jumpToSearchResult(this._currentSearchResultIndex);
361    },
362
363    showingFirstSearchResult: function()
364    {
365        return (this._currentSearchResultIndex === 0);
366    },
367
368    showingLastSearchResult: function()
369    {
370        return (this._searchResults && this._currentSearchResultIndex === (this._searchResults.length - 1));
371    },
372
373    currentSearchResultIndex: function() {
374        return this._currentSearchResultIndex;
375    },
376
377    _jumpToSearchResult: function(index)
378    {
379        var searchResult = this._searchResults[index];
380        if (!searchResult)
381            return;
382
383        var profileNode = searchResult.profileNode;
384        profileNode.revealAndSelect();
385    },
386
387    _ensureFlameChartCreated: function()
388    {
389        if (this._flameChart)
390            return;
391        var dataProvider = new WebInspector.CPUFlameChartDataProvider(this);
392        this._flameChart = new WebInspector.FlameChart(dataProvider);
393        this._flameChart.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected.bind(this));
394    },
395
396    /**
397     * @param {!WebInspector.Event} event
398     */
399    _onEntrySelected: function(event)
400    {
401        var node = event.data;
402        if (!node || !node.scriptId)
403            return;
404        var script = WebInspector.debuggerModel.scriptForId(node.scriptId)
405        if (!script)
406            return;
407        var uiLocation = script.rawLocationToUILocation(node.lineNumber);
408        if (!uiLocation)
409            return;
410        WebInspector.panel("sources").showUILocation(uiLocation);
411    },
412
413    _changeView: function()
414    {
415        if (!this.profile)
416            return;
417
418        switch (this.viewSelectComboBox.selectedOption().value) {
419        case WebInspector.CPUProfileView._TypeFlame:
420            this._ensureFlameChartCreated();
421            this.dataGrid.detach();
422            this._flameChart.show(this.element);
423            this._viewType.set(WebInspector.CPUProfileView._TypeFlame);
424            this._statusBarButtonsElement.enableStyleClass("hidden", true);
425            return;
426        case WebInspector.CPUProfileView._TypeTree:
427            this.profileDataGridTree = this._getTopDownProfileDataGridTree();
428            this._sortProfile();
429            this._viewType.set(WebInspector.CPUProfileView._TypeTree);
430            break;
431        case WebInspector.CPUProfileView._TypeHeavy:
432            this.profileDataGridTree = this._getBottomUpProfileDataGridTree();
433            this._sortProfile();
434            this._viewType.set(WebInspector.CPUProfileView._TypeHeavy);
435            break;
436        }
437
438        this._statusBarButtonsElement.enableStyleClass("hidden", false);
439
440        if (this._flameChart)
441            this._flameChart.detach();
442        this.dataGrid.show(this.element);
443
444        if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults)
445            return;
446
447        // The current search needs to be performed again. First negate out previous match
448        // count by calling the search finished callback with a negative number of matches.
449        // Then perform the search again the with same query and callback.
450        this._searchFinishedCallback(this, -this._searchResults.length);
451        this.performSearch(this.currentQuery, this._searchFinishedCallback);
452    },
453
454    _percentClicked: function(event)
455    {
456        var currentState = this.showSelfTimeAsPercent.get() && this.showTotalTimeAsPercent.get() && this.showAverageTimeAsPercent.get();
457        this.showSelfTimeAsPercent.set(!currentState);
458        this.showTotalTimeAsPercent.set(!currentState);
459        this.showAverageTimeAsPercent.set(!currentState);
460        this.refreshShowAsPercents();
461    },
462
463    _updatePercentButton: function()
464    {
465        if (this.showSelfTimeAsPercent.get() && this.showTotalTimeAsPercent.get() && this.showAverageTimeAsPercent.get()) {
466            this.percentButton.title = WebInspector.UIString("Show absolute total and self times.");
467            this.percentButton.toggled = true;
468        } else {
469            this.percentButton.title = WebInspector.UIString("Show total and self times as percentages.");
470            this.percentButton.toggled = false;
471        }
472    },
473
474    _focusClicked: function(event)
475    {
476        if (!this.dataGrid.selectedNode)
477            return;
478
479        this.resetButton.visible = true;
480        this.profileDataGridTree.focus(this.dataGrid.selectedNode);
481        this.refresh();
482        this.refreshVisibleData();
483    },
484
485    _excludeClicked: function(event)
486    {
487        var selectedNode = this.dataGrid.selectedNode
488
489        if (!selectedNode)
490            return;
491
492        selectedNode.deselect();
493
494        this.resetButton.visible = true;
495        this.profileDataGridTree.exclude(selectedNode);
496        this.refresh();
497        this.refreshVisibleData();
498    },
499
500    _resetClicked: function(event)
501    {
502        this.resetButton.visible = false;
503        this.profileDataGridTree.restore();
504        this._linkifier.reset();
505        this.refresh();
506        this.refreshVisibleData();
507    },
508
509    _dataGridNodeSelected: function(node)
510    {
511        this.focusButton.setEnabled(true);
512        this.excludeButton.setEnabled(true);
513    },
514
515    _dataGridNodeDeselected: function(node)
516    {
517        this.focusButton.setEnabled(false);
518        this.excludeButton.setEnabled(false);
519    },
520
521    _sortProfile: function()
522    {
523        var sortAscending = this.dataGrid.isSortOrderAscending();
524        var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier();
525        var sortProperty = {
526                "self": "selfTime",
527                "total": "totalTime",
528                "function": "functionName"
529            }[sortColumnIdentifier];
530
531        this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator(sortProperty, sortAscending));
532
533        this.refresh();
534    },
535
536    _mouseDownInDataGrid: function(event)
537    {
538        if (event.detail < 2)
539            return;
540
541        var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
542        if (!cell || (!cell.classList.contains("total-column") && !cell.classList.contains("self-column") && !cell.classList.contains("average-column")))
543            return;
544
545        if (cell.classList.contains("total-column"))
546            this.showTotalTimeAsPercent.set(!this.showTotalTimeAsPercent.get());
547        else if (cell.classList.contains("self-column"))
548            this.showSelfTimeAsPercent.set(!this.showSelfTimeAsPercent.get());
549        else if (cell.classList.contains("average-column"))
550            this.showAverageTimeAsPercent.set(!this.showAverageTimeAsPercent.get());
551
552        this.refreshShowAsPercents();
553
554        event.consume(true);
555    },
556
557    _calculateTimes: function(profile)
558    {
559        function totalHitCount(node) {
560            var result = node.hitCount;
561            for (var i = 0; i < node.children.length; i++)
562                result += totalHitCount(node.children[i]);
563            return result;
564        }
565        profile.totalHitCount = totalHitCount(profile.head);
566
567        var durationMs = 1000 * (profile.endTime - profile.startTime);
568        var samplingInterval = durationMs / profile.totalHitCount;
569        this.samplingIntervalMs = samplingInterval;
570
571        function calculateTimesForNode(node) {
572            node.selfTime = node.hitCount * samplingInterval;
573            var totalHitCount = node.hitCount;
574            for (var i = 0; i < node.children.length; i++)
575                totalHitCount += calculateTimesForNode(node.children[i]);
576            node.totalTime = totalHitCount * samplingInterval;
577            return totalHitCount;
578        }
579        calculateTimesForNode(profile.head);
580    },
581
582    _assignParentsInProfile: function()
583    {
584        var head = this.profileHead;
585        head.parent = null;
586        head.head = null;
587        var nodesToTraverse = [ { parent: head, children: head.children } ];
588        while (nodesToTraverse.length > 0) {
589            var pair = nodesToTraverse.pop();
590            var parent = pair.parent;
591            var children = pair.children;
592            var length = children.length;
593            for (var i = 0; i < length; ++i) {
594                children[i].head = head;
595                children[i].parent = parent;
596                if (children[i].children.length > 0)
597                    nodesToTraverse.push({ parent: children[i], children: children[i].children });
598            }
599        }
600    },
601
602    _buildIdToNodeMap: function()
603    {
604        var idToNode = this._idToNode = {};
605        var stack = [this.profileHead];
606        while (stack.length) {
607            var node = stack.pop();
608            idToNode[node.id] = node;
609            for (var i = 0; i < node.children.length; i++)
610                stack.push(node.children[i]);
611        }
612
613        var topLevelNodes = this.profileHead.children;
614        for (var i = 0; i < topLevelNodes.length; i++) {
615            var node = topLevelNodes[i];
616            if (node.functionName == "(garbage collector)") {
617                this._gcNode = node;
618                break;
619            }
620        }
621    },
622
623    __proto__: WebInspector.View.prototype
624}
625
626/**
627 * @constructor
628 * @extends {WebInspector.ProfileType}
629 * @implements {WebInspector.CPUProfilerModelDelegate}
630 */
631WebInspector.CPUProfileType = function()
632{
633    WebInspector.ProfileType.call(this, WebInspector.CPUProfileType.TypeId, WebInspector.UIString("Collect JavaScript CPU Profile"));
634    this._recording = false;
635    this._nextProfileId = 1;
636
637    this._nextAnonymousConsoleProfileNumber = 1;
638    this._anonymousConsoleProfileIdToTitle = {};
639
640    WebInspector.CPUProfileType.instance = this;
641    WebInspector.cpuProfilerModel.setDelegate(this);
642}
643
644WebInspector.CPUProfileType.TypeId = "CPU";
645
646WebInspector.CPUProfileType.prototype = {
647    /**
648     * @override
649     * @return {string}
650     */
651    fileExtension: function()
652    {
653        return ".cpuprofile";
654    },
655
656    get buttonTooltip()
657    {
658        return this._recording ? WebInspector.UIString("Stop CPU profiling.") : WebInspector.UIString("Start CPU profiling.");
659    },
660
661    /**
662     * @override
663     * @return {boolean}
664     */
665    buttonClicked: function()
666    {
667        if (this._recording) {
668            this.stopRecordingProfile();
669            return false;
670        } else {
671            this.startRecordingProfile();
672            return true;
673        }
674    },
675
676    get treeItemTitle()
677    {
678        return WebInspector.UIString("CPU PROFILES");
679    },
680
681    get description()
682    {
683        return WebInspector.UIString("CPU profiles show where the execution time is spent in your page's JavaScript functions.");
684    },
685
686    /**
687     * @param {string} id
688     * @param {!DebuggerAgent.Location} scriptLocation
689     * @param {string=} title
690     */
691    consoleProfile: function(id, scriptLocation, title)
692    {
693        var resolvedTitle = title;
694        if (!resolvedTitle) {
695            resolvedTitle = WebInspector.UIString("Profile %s", this._nextAnonymousConsoleProfileNumber++);
696            this._anonymousConsoleProfileIdToTitle[id] = resolvedTitle;
697        }
698        this._addMessageToConsole(WebInspector.ConsoleMessage.MessageType.Profile, scriptLocation, resolvedTitle);
699    },
700
701    /**
702     * @param {string} protocolId
703     * @param {!DebuggerAgent.Location} scriptLocation
704     * @param {!ProfilerAgent.CPUProfile} cpuProfile
705     * @param {string=} title
706     */
707    consoleProfileEnd: function(protocolId, scriptLocation, cpuProfile, title)
708    {
709        // Make sure ProfilesPanel is initialized and CPUProfileType is created.
710        var resolvedTitle = title;
711        if (typeof title === "undefined") {
712            resolvedTitle = this._anonymousConsoleProfileIdToTitle[protocolId];
713            delete this._anonymousConsoleProfileIdToTitle[protocolId];
714        }
715
716        var id = this._nextProfileId++;
717        var profile = new WebInspector.CPUProfileHeader(this, resolvedTitle, id);
718        profile.setProtocolProfile(cpuProfile);
719        this.addProfile(profile);
720
721        resolvedTitle += "#" + id;
722        this._addMessageToConsole(WebInspector.ConsoleMessage.MessageType.ProfileEnd, scriptLocation, resolvedTitle);
723    },
724
725    /**
726     * @param {string} type
727     * @param {!DebuggerAgent.Location} scriptLocation
728     * @param {string} title
729     */
730    _addMessageToConsole: function(type, scriptLocation, title)
731    {
732        var rawLocation = new WebInspector.DebuggerModel.Location(scriptLocation.scriptId, scriptLocation.lineNumber, scriptLocation.columnNumber || 0);
733        var uiLocation = WebInspector.debuggerModel.rawLocationToUILocation(rawLocation);
734        var url;
735        if (uiLocation)
736            url = uiLocation.url();
737        var message = WebInspector.ConsoleMessage.create(
738            WebInspector.ConsoleMessage.MessageSource.ConsoleAPI,
739            WebInspector.ConsoleMessage.MessageLevel.Debug,
740            title,
741            type,
742            url || undefined,
743            scriptLocation.lineNumber,
744            scriptLocation.columnNumber);
745        WebInspector.console.addMessage(message);
746    },
747
748    /**
749     * @param {!ProfilerAgent.CPUProfile} cpuProfile
750     * @param {string} title
751     */
752    _addProfileHeader: function(cpuProfile, title)
753    {
754        var id = this._nextProfileId++;
755        var profile = new WebInspector.CPUProfileHeader(this, title, id);
756        profile.setProtocolProfile(cpuProfile);
757        this.addProfile(profile);
758    },
759
760    isRecordingProfile: function()
761    {
762        return this._recording;
763    },
764
765    startRecordingProfile: function()
766    {
767        if (this._profileBeingRecorded)
768            return;
769        var id = this._nextProfileId++;
770        this._profileBeingRecorded = new WebInspector.CPUProfileHeader(this, WebInspector.UIString("Recording\u2026"), id);
771        this.addProfile(this._profileBeingRecorded);
772
773        this._recording = true;
774        WebInspector.cpuProfilerModel.setRecording(true);
775        WebInspector.userMetrics.ProfilesCPUProfileTaken.record();
776        ProfilerAgent.start();
777    },
778
779    stopRecordingProfile: function()
780    {
781        this._recording = false;
782        WebInspector.cpuProfilerModel.setRecording(false);
783
784        /**
785         * @param {?string} error
786         * @param {?ProfilerAgent.CPUProfile} profile
787         * @this {WebInspector.CPUProfileType}
788         */
789        function didStopProfiling(error, profile)
790        {
791            if (!this._profileBeingRecorded)
792                return;
793            this._profileBeingRecorded.setProtocolProfile(profile);
794
795            var title = WebInspector.UIString("Profile %d", this._profileBeingRecorded.uid);
796            this._profileBeingRecorded.title = title;
797            this._profileBeingRecorded.sidebarElement.mainTitle = title;
798            var recordedProfile = this._profileBeingRecorded;
799            this._profileBeingRecorded = null;
800            WebInspector.panels.profiles._showProfile(recordedProfile);
801        }
802        ProfilerAgent.stop(didStopProfiling.bind(this));
803    },
804
805    /**
806     * @override
807     * @param {string} title
808     * @return {!WebInspector.ProfileHeader}
809     */
810    createProfileLoadedFromFile: function(title)
811    {
812        return new WebInspector.CPUProfileHeader(this, title);
813    },
814
815    /**
816     * @override
817     */
818    removeProfile: function(profile)
819    {
820        if (this._profileBeingRecorded === profile) {
821            this.stopRecordingProfile();
822            this._profileBeingRecorded = null;
823        }
824        WebInspector.ProfileType.prototype.removeProfile.call(this, profile);
825    },
826
827    /**
828     * @override
829     */
830    resetProfiles: function()
831    {
832        this._reset();
833    },
834
835    __proto__: WebInspector.ProfileType.prototype
836}
837
838/**
839 * @constructor
840 * @extends {WebInspector.ProfileHeader}
841 * @implements {WebInspector.OutputStream}
842 * @implements {WebInspector.OutputStreamDelegate}
843 * @param {!WebInspector.CPUProfileType} type
844 * @param {string} title
845 * @param {number=} uid
846 */
847WebInspector.CPUProfileHeader = function(type, title, uid)
848{
849    WebInspector.ProfileHeader.call(this, type, title, uid);
850    this._tempFile = null;
851}
852
853WebInspector.CPUProfileHeader.prototype = {
854    onTransferStarted: function()
855    {
856        this._jsonifiedProfile = "";
857        this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026 %s", Number.bytesToString(this._jsonifiedProfile.length));
858    },
859
860    /**
861     * @param {!WebInspector.ChunkedReader} reader
862     */
863    onChunkTransferred: function(reader)
864    {
865        this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026 %d\%", Number.bytesToString(this._jsonifiedProfile.length));
866    },
867
868    onTransferFinished: function()
869    {
870        this.sidebarElement.subtitle = WebInspector.UIString("Parsing\u2026");
871        this._profile = JSON.parse(this._jsonifiedProfile);
872        this._jsonifiedProfile = null;
873        this.sidebarElement.subtitle = WebInspector.UIString("Loaded");
874
875        if (this._profileType._profileBeingRecorded === this)
876            this._profileType._profileBeingRecorded = null;
877    },
878
879    /**
880     * @param {!WebInspector.ChunkedReader} reader
881     */
882    onError: function(reader, e)
883    {
884        switch(e.target.error.code) {
885        case e.target.error.NOT_FOUND_ERR:
886            this.sidebarElement.subtitle = WebInspector.UIString("'%s' not found.", reader.fileName());
887        break;
888        case e.target.error.NOT_READABLE_ERR:
889            this.sidebarElement.subtitle = WebInspector.UIString("'%s' is not readable", reader.fileName());
890        break;
891        case e.target.error.ABORT_ERR:
892            break;
893        default:
894            this.sidebarElement.subtitle = WebInspector.UIString("'%s' error %d", reader.fileName(), e.target.error.code);
895        }
896    },
897
898    /**
899     * @param {string} text
900     */
901    write: function(text)
902    {
903        this._jsonifiedProfile += text;
904    },
905
906    close: function() { },
907
908    /**
909     * @override
910     */
911    createSidebarTreeElement: function()
912    {
913        return new WebInspector.ProfileSidebarTreeElement(this, "profile-sidebar-tree-item");
914    },
915
916    /**
917     * @override
918     * @param {!WebInspector.ProfilesPanel} profilesPanel
919     */
920    createView: function(profilesPanel)
921    {
922        return new WebInspector.CPUProfileView(this);
923    },
924
925    /**
926     * @override
927     * @return {boolean}
928     */
929    canSaveToFile: function()
930    {
931        return !!this._tempFile;
932    },
933
934    saveToFile: function()
935    {
936        var fileOutputStream = new WebInspector.FileOutputStream();
937
938        /**
939         * @param {boolean} accepted
940         * @this {WebInspector.CPUProfileHeader}
941         */
942        function onOpenForSave(accepted)
943        {
944            if (!accepted)
945                return;
946            function didRead(data)
947            {
948                if (data)
949                    fileOutputStream.write(data, fileOutputStream.close.bind(fileOutputStream));
950                else
951                    fileOutputStream.close();
952            }
953            this._tempFile.read(didRead.bind(this));
954        }
955        this._fileName = this._fileName || "CPU-" + new Date().toISO8601Compact() + this._profileType.fileExtension();
956        fileOutputStream.open(this._fileName, onOpenForSave.bind(this));
957    },
958
959    /**
960     * @param {!File} file
961     */
962    loadFromFile: function(file)
963    {
964        this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026");
965        this.sidebarElement.wait = true;
966
967        var fileReader = new WebInspector.ChunkedFileReader(file, 10000000, this);
968        fileReader.start(this);
969    },
970
971
972    /**
973     * @return {?ProfilerAgent.CPUProfile}
974     */
975    protocolProfile: function()
976    {
977        return this._protocolProfile;
978    },
979
980    /**
981     * @param {!ProfilerAgent.CPUProfile} cpuProfile
982     */
983    setProtocolProfile: function(cpuProfile)
984    {
985        this._protocolProfile = cpuProfile;
986        this._saveProfileDataToTempFile(cpuProfile);
987    },
988
989    /**
990     * @param {!ProfilerAgent.CPUProfile} data
991     */
992    _saveProfileDataToTempFile: function(data)
993    {
994        var serializedData = JSON.stringify(data);
995
996        /**
997         * @this {WebInspector.CPUProfileHeader}
998         */
999        function didCreateTempFile(tempFile)
1000        {
1001            this._writeToTempFile(tempFile, serializedData);
1002        }
1003        new WebInspector.TempFile("cpu-profiler", this.uid,  didCreateTempFile.bind(this));
1004    },
1005
1006    /**
1007     * @param {?WebInspector.TempFile} tempFile
1008     * @param {string} serializedData
1009     */
1010    _writeToTempFile: function(tempFile, serializedData)
1011    {
1012        this._tempFile = tempFile;
1013        if (tempFile)
1014            tempFile.write(serializedData);
1015    },
1016
1017    __proto__: WebInspector.ProfileHeader.prototype
1018}
1019
1020/**
1021 * @constructor
1022 * @implements {WebInspector.FlameChartDataProvider}
1023 */
1024WebInspector.CPUFlameChartDataProvider = function(cpuProfileView)
1025{
1026    WebInspector.FlameChartDataProvider.call(this);
1027    this._cpuProfileView = cpuProfileView;
1028}
1029
1030WebInspector.CPUFlameChartDataProvider.prototype = {
1031    /**
1032     * @param {!WebInspector.FlameChart.ColorGenerator} colorGenerator
1033     * @return {!Object}
1034     */
1035    timelineData: function(colorGenerator)
1036    {
1037        return this._timelineData || this._calculateTimelineData(colorGenerator);
1038    },
1039
1040    /**
1041     * @param {!WebInspector.FlameChart.ColorGenerator} colorGenerator
1042     * @return {?Object}
1043     */
1044    _calculateTimelineData: function(colorGenerator)
1045    {
1046        if (!this._cpuProfileView.profileHead)
1047            return null;
1048
1049        var samples = this._cpuProfileView.samples;
1050        var idToNode = this._cpuProfileView._idToNode;
1051        var gcNode = this._cpuProfileView._gcNode;
1052        var samplesCount = samples.length;
1053        var samplingInterval = this._cpuProfileView.samplingIntervalMs;
1054
1055        var index = 0;
1056
1057        var openIntervals = [];
1058        var stackTrace = [];
1059        var colorEntryIndexes = [];
1060        var maxDepth = 5; // minimum stack depth for the case when we see no activity.
1061        var depth = 0;
1062
1063        /**
1064         * @constructor
1065         * @param {!Object} colorPair
1066         * @param {!number} depth
1067         * @param {!number} duration
1068         * @param {!number} startTime
1069         * @param {!Object} node
1070         */
1071        function ChartEntry(colorPair, depth, duration, startTime, node)
1072        {
1073            this.colorPair = colorPair;
1074            this.depth = depth;
1075            this.duration = duration;
1076            this.startTime = startTime;
1077            this.node = node;
1078            this.selfTime = 0;
1079        }
1080        var entries = /** @type {!Array.<!ChartEntry>} */ ([]);
1081
1082        for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) {
1083            var node = idToNode[samples[sampleIndex]];
1084            stackTrace.length = 0;
1085            while (node) {
1086                stackTrace.push(node);
1087                node = node.parent;
1088            }
1089            stackTrace.pop(); // Remove (root) node
1090
1091            maxDepth = Math.max(maxDepth, depth);
1092            depth = 0;
1093            node = stackTrace.pop();
1094            var intervalIndex;
1095
1096            // GC samples have no stack, so we just put GC node on top of the last recoreded sample.
1097            if (node === gcNode) {
1098                while (depth < openIntervals.length) {
1099                    intervalIndex = openIntervals[depth].index;
1100                    entries[intervalIndex].duration += samplingInterval;
1101                    ++depth;
1102                }
1103                // If previous stack is also GC then just continue.
1104                if (openIntervals.length > 0 && openIntervals.peekLast().node === node) {
1105                    entries[intervalIndex].selfTime += samplingInterval;
1106                    continue;
1107                }
1108            }
1109
1110            while (node && depth < openIntervals.length && node === openIntervals[depth].node) {
1111                intervalIndex = openIntervals[depth].index;
1112                entries[intervalIndex].duration += samplingInterval;
1113                node = stackTrace.pop();
1114                ++depth;
1115            }
1116            if (depth < openIntervals.length)
1117                openIntervals.length = depth;
1118            if (!node) {
1119                entries[intervalIndex].selfTime += samplingInterval;
1120                continue;
1121            }
1122
1123            while (node) {
1124                var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
1125                var indexesForColor = colorEntryIndexes[colorPair.index];
1126                if (!indexesForColor)
1127                    indexesForColor = colorEntryIndexes[colorPair.index] = [];
1128
1129                var entry = new ChartEntry(colorPair, depth, samplingInterval, sampleIndex * samplingInterval, node);
1130                indexesForColor.push(entries.length);
1131                entries.push(entry);
1132                openIntervals.push({node: node, index: index});
1133                ++index;
1134
1135                node = stackTrace.pop();
1136                ++depth;
1137            }
1138            entries[entries.length - 1].selfTime += samplingInterval;
1139        }
1140
1141        var entryNodes = new Array(entries.length);
1142        var entryColorIndexes = new Uint16Array(entries.length);
1143        var entryLevels = new Uint8Array(entries.length);
1144        var entryTotalTimes = new Float32Array(entries.length);
1145        var entrySelfTimes = new Float32Array(entries.length);
1146        var entryOffsets = new Float32Array(entries.length);
1147        var entryTitles = new Array(entries.length);
1148        var entryDeoptFlags = new Uint8Array(entries.length);
1149
1150        for (var i = 0; i < entries.length; ++i) {
1151            var entry = entries[i];
1152            entryNodes[i] = entry.node;
1153            entryColorIndexes[i] = colorPair.index;
1154            entryLevels[i] = entry.depth;
1155            entryTotalTimes[i] = entry.duration;
1156            entrySelfTimes[i] = entry.selfTime;
1157            entryOffsets[i] = entry.startTime;
1158            entryTitles[i] = entry.node.functionName;
1159            var reason = entry.node.deoptReason;
1160            entryDeoptFlags[i] = (reason && reason !== "no reason");
1161        }
1162
1163        this._timelineData = {
1164            maxStackDepth: Math.max(maxDepth, depth),
1165            totalTime: this._cpuProfileView.profileHead.totalTime,
1166            entryNodes: entryNodes,
1167            entryColorIndexes: entryColorIndexes,
1168            entryLevels: entryLevels,
1169            entryTotalTimes: entryTotalTimes,
1170            entrySelfTimes: entrySelfTimes,
1171            entryOffsets: entryOffsets,
1172            colorEntryIndexes: colorEntryIndexes,
1173            entryTitles: entryTitles,
1174            entryDeoptFlags: entryDeoptFlags
1175        };
1176
1177        return this._timelineData;
1178    },
1179
1180    /**
1181     * @param {number} ms
1182     */
1183    _millisecondsToString: function(ms)
1184    {
1185        if (ms === 0)
1186            return "0";
1187        if (ms < 1000)
1188            return WebInspector.UIString("%.1f\u2009ms", ms);
1189        return Number.secondsToString(ms / 1000, true);
1190    },
1191
1192    /**
1193     * @param {number} entryIndex
1194     */
1195    prepareHighlightedEntryInfo: function(entryIndex)
1196    {
1197        var timelineData = this._timelineData;
1198        var node = timelineData.entryNodes[entryIndex];
1199        if (!node)
1200            return null;
1201
1202        var entryInfo = [];
1203        function pushEntryInfoRow(title, text)
1204        {
1205            var row = {};
1206            row.title = title;
1207            row.text = text;
1208            entryInfo.push(row);
1209        }
1210
1211        pushEntryInfoRow(WebInspector.UIString("Name"), timelineData.entryTitles[entryIndex]);
1212        var selfTime = this._millisecondsToString(timelineData.entrySelfTimes[entryIndex]);
1213        var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[entryIndex]);
1214        pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime);
1215        pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime);
1216        if (node.url)
1217            pushEntryInfoRow(WebInspector.UIString("URL"), node.url + ":" + node.lineNumber);
1218        pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true));
1219        pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true));
1220        if (node.deoptReason && node.deoptReason !== "no reason")
1221            pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason);
1222
1223        return entryInfo;
1224    },
1225
1226    /**
1227     * @param {number} entryIndex
1228     * @return {boolean}
1229     */
1230    canJumpToEntry: function(entryIndex)
1231    {
1232        return this._timelineData.entryNodes[entryIndex].scriptId !== "0";
1233    },
1234
1235    /**
1236     * @param {number} entryIndex
1237     * @return {!Object}
1238     */
1239    entryData: function(entryIndex)
1240    {
1241        return this._timelineData.entryNodes[entryIndex];
1242    },
1243
1244    __proto__: WebInspector.FlameChartDataProvider
1245}
1246
1247