1/*
2 * Copyright (C) 2011 Google 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 are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @constructor
33 * @extends {WebInspector.View}
34 * @param {!WebInspector.ProfilesPanel} parent
35 * @param {!WebInspector.HeapProfileHeader} profile
36 */
37WebInspector.HeapSnapshotView = function(parent, profile)
38{
39    WebInspector.View.call(this);
40
41    this.element.classList.add("heap-snapshot-view");
42
43    this.parent = parent;
44    this.parent.addEventListener("profile added", this._onProfileHeaderAdded, this);
45
46    if (profile._profileType.id === WebInspector.TrackingHeapSnapshotProfileType.TypeId) {
47        this._trackingOverviewGrid = new WebInspector.HeapTrackingOverviewGrid(profile);
48        this._trackingOverviewGrid.addEventListener(WebInspector.HeapTrackingOverviewGrid.IdsRangeChanged, this._onIdsRangeChanged.bind(this));
49        this._trackingOverviewGrid.show(this.element);
50    }
51
52    this.viewsContainer = document.createElement("div");
53    this.viewsContainer.classList.add("views-container");
54    this.element.appendChild(this.viewsContainer);
55
56    this.containmentView = new WebInspector.View();
57    this.containmentView.element.classList.add("view");
58    this.containmentDataGrid = new WebInspector.HeapSnapshotContainmentDataGrid();
59    this.containmentDataGrid.element.addEventListener("mousedown", this._mouseDownInContentsGrid.bind(this), true);
60    this.containmentDataGrid.show(this.containmentView.element);
61    this.containmentDataGrid.addEventListener(WebInspector.DataGrid.Events.SelectedNode, this._selectionChanged, this);
62
63    this.constructorsView = new WebInspector.View();
64    this.constructorsView.element.classList.add("view");
65    this.constructorsView.element.appendChild(this._createToolbarWithClassNameFilter());
66
67    this.constructorsDataGrid = new WebInspector.HeapSnapshotConstructorsDataGrid();
68    this.constructorsDataGrid.element.classList.add("class-view-grid");
69    this.constructorsDataGrid.element.addEventListener("mousedown", this._mouseDownInContentsGrid.bind(this), true);
70    this.constructorsDataGrid.show(this.constructorsView.element);
71    this.constructorsDataGrid.addEventListener(WebInspector.DataGrid.Events.SelectedNode, this._selectionChanged, this);
72
73    this.dataGrid = /** @type {!WebInspector.HeapSnapshotSortableDataGrid} */ (this.constructorsDataGrid);
74    this.currentView = this.constructorsView;
75    this.currentView.show(this.viewsContainer);
76
77    this.diffView = new WebInspector.View();
78    this.diffView.element.classList.add("view");
79    this.diffView.element.appendChild(this._createToolbarWithClassNameFilter());
80
81    this.diffDataGrid = new WebInspector.HeapSnapshotDiffDataGrid();
82    this.diffDataGrid.element.classList.add("class-view-grid");
83    this.diffDataGrid.show(this.diffView.element);
84    this.diffDataGrid.addEventListener(WebInspector.DataGrid.Events.SelectedNode, this._selectionChanged, this);
85
86    this.dominatorView = new WebInspector.View();
87    this.dominatorView.element.classList.add("view");
88    this.dominatorDataGrid = new WebInspector.HeapSnapshotDominatorsDataGrid();
89    this.dominatorDataGrid.element.addEventListener("mousedown", this._mouseDownInContentsGrid.bind(this), true);
90    this.dominatorDataGrid.show(this.dominatorView.element);
91    this.dominatorDataGrid.addEventListener(WebInspector.DataGrid.Events.SelectedNode, this._selectionChanged, this);
92
93    if (WebInspector.HeapSnapshot.enableAllocationProfiler) {
94        this.allocationView = new WebInspector.View();
95        this.allocationView.element.classList.add("view");
96        this.allocationDataGrid = new WebInspector.AllocationDataGrid();
97        this.allocationDataGrid.element.addEventListener("mousedown", this._mouseDownInContentsGrid.bind(this), true);
98        this.allocationDataGrid.show(this.allocationView.element);
99        this.allocationDataGrid.addEventListener(WebInspector.DataGrid.Events.SelectedNode, this._selectionChanged, this);
100    }
101
102    this.retainmentViewHeader = document.createElement("div");
103    this.retainmentViewHeader.classList.add("retainers-view-header");
104    WebInspector.installDragHandle(this.retainmentViewHeader, this._startRetainersHeaderDragging.bind(this), this._retainersHeaderDragging.bind(this), this._endRetainersHeaderDragging.bind(this), "row-resize");
105    var retainingPathsTitleDiv = document.createElement("div");
106    retainingPathsTitleDiv.className = "title";
107    var retainingPathsTitle = document.createElement("span");
108    retainingPathsTitle.textContent = WebInspector.UIString("Object's retaining tree");
109    retainingPathsTitleDiv.appendChild(retainingPathsTitle);
110    this.retainmentViewHeader.appendChild(retainingPathsTitleDiv);
111    this.element.appendChild(this.retainmentViewHeader);
112
113    this.retainmentView = new WebInspector.View();
114    this.retainmentView.element.classList.add("view");
115    this.retainmentView.element.classList.add("retaining-paths-view");
116    this.retainmentDataGrid = new WebInspector.HeapSnapshotRetainmentDataGrid();
117    this.retainmentDataGrid.show(this.retainmentView.element);
118    this.retainmentDataGrid.addEventListener(WebInspector.DataGrid.Events.SelectedNode, this._inspectedObjectChanged, this);
119    this.retainmentView.show(this.element);
120    this.retainmentDataGrid.reset();
121
122    this.viewSelect = new WebInspector.StatusBarComboBox(this._onSelectedViewChanged.bind(this));
123
124    this.views = [{title: "Summary", view: this.constructorsView, grid: this.constructorsDataGrid},
125                  {title: "Comparison", view: this.diffView, grid: this.diffDataGrid},
126                  {title: "Containment", view: this.containmentView, grid: this.containmentDataGrid}];
127    if (WebInspector.settings.showAdvancedHeapSnapshotProperties.get())
128        this.views.push({title: "Dominators", view: this.dominatorView, grid: this.dominatorDataGrid});
129    if (WebInspector.HeapSnapshot.enableAllocationProfiler)
130        this.views.push({title: "Allocation", view: this.allocationView, grid: this.allocationDataGrid});
131    this.views.current = 0;
132    for (var i = 0; i < this.views.length; ++i)
133        this.viewSelect.createOption(WebInspector.UIString(this.views[i].title));
134
135    this._profile = profile;
136
137    this.baseSelect = new WebInspector.StatusBarComboBox(this._changeBase.bind(this));
138    this.baseSelect.element.classList.add("hidden");
139    this._updateBaseOptions();
140
141    this.filterSelect = new WebInspector.StatusBarComboBox(this._changeFilter.bind(this));
142    this._updateFilterOptions();
143
144    this.selectedSizeText = new WebInspector.StatusBarText("");
145
146    this._popoverHelper = new WebInspector.ObjectPopoverHelper(this.element, this._getHoverAnchor.bind(this), this._resolveObjectForPopover.bind(this), undefined, true);
147
148    this._refreshView();
149}
150
151WebInspector.HeapSnapshotView.prototype = {
152    _refreshView: function()
153    {
154        this.profile.load(profileCallback.bind(this));
155
156        /**
157         * @this {WebInspector.HeapSnapshotView}
158         */
159        function profileCallback(heapSnapshotProxy)
160        {
161            var list = this._profiles();
162            var profileIndex = list.indexOf(this._profile);
163            this.baseSelect.setSelectedIndex(Math.max(0, profileIndex - 1));
164            this.dataGrid.setDataSource(heapSnapshotProxy);
165            if (this._trackingOverviewGrid)
166                this._trackingOverviewGrid._updateGrid();
167        }
168    },
169
170    _onIdsRangeChanged: function(event)
171    {
172        var minId = event.data.minId;
173        var maxId = event.data.maxId;
174        this.selectedSizeText.setText(WebInspector.UIString("Selected size: %s", Number.bytesToString(event.data.size)));
175        if (this.constructorsDataGrid.snapshot)
176            this.constructorsDataGrid.setSelectionRange(minId, maxId);
177    },
178
179    dispose: function()
180    {
181        this.parent.removeEventListener("profile added", this._onProfileHeaderAdded, this);
182        this.profile.dispose();
183        if (this.baseProfile)
184            this.baseProfile.dispose();
185        this.containmentDataGrid.dispose();
186        this.constructorsDataGrid.dispose();
187        this.diffDataGrid.dispose();
188        this.dominatorDataGrid.dispose();
189        this.retainmentDataGrid.dispose();
190    },
191
192    get statusBarItems()
193    {
194        return [this.viewSelect.element, this.baseSelect.element, this.filterSelect.element, this.selectedSizeText.element];
195    },
196
197    get profile()
198    {
199        return this._profile;
200    },
201
202    get baseProfile()
203    {
204        return this._profile.profileType().getProfile(this._baseProfileUid);
205    },
206
207    wasShown: function()
208    {
209        // FIXME: load base and current snapshots in parallel
210        this.profile.load(profileCallback.bind(this));
211
212        /**
213         * @this {WebInspector.HeapSnapshotView}
214         */
215        function profileCallback() {
216            this.profile._wasShown();
217            if (this.baseProfile)
218                this.baseProfile.load(function() { });
219        }
220    },
221
222    willHide: function()
223    {
224        this._currentSearchResultIndex = -1;
225        this._popoverHelper.hidePopover();
226        if (this.helpPopover && this.helpPopover.isShowing())
227            this.helpPopover.hide();
228    },
229
230    onResize: function()
231    {
232        var height = this.retainmentView.element.clientHeight;
233        this._updateRetainmentViewHeight(height);
234    },
235
236    searchCanceled: function()
237    {
238        if (this._searchResults) {
239            for (var i = 0; i < this._searchResults.length; ++i) {
240                var node = this._searchResults[i].node;
241                delete node._searchMatched;
242                node.refresh();
243            }
244        }
245
246        delete this._searchFinishedCallback;
247        this._currentSearchResultIndex = -1;
248        this._searchResults = [];
249    },
250
251    /**
252     * @param {string} query
253     * @param {function(!WebInspector.View, number)} finishedCallback
254     */
255    performSearch: function(query, finishedCallback)
256    {
257        // Call searchCanceled since it will reset everything we need before doing a new search.
258        this.searchCanceled();
259
260        query = query.trim();
261
262        if (!query)
263            return;
264        if (this.currentView !== this.constructorsView && this.currentView !== this.diffView)
265            return;
266
267        /**
268         * @param {boolean} found
269         * @this {WebInspector.HeapSnapshotView}
270         */
271        function didHighlight(found)
272        {
273            finishedCallback(this, found ? 1 : 0);
274        }
275
276        if (query.charAt(0) === "@") {
277            var snapshotNodeId = parseInt(query.substring(1), 10);
278            if (!isNaN(snapshotNodeId))
279                this.dataGrid.highlightObjectByHeapSnapshotId(String(snapshotNodeId), didHighlight.bind(this));
280            else
281                finishedCallback(this, 0);
282            return;
283        }
284
285        this._searchFinishedCallback = finishedCallback;
286        var nameRegExp = createPlainTextSearchRegex(query, "i");
287
288        function matchesByName(gridNode) {
289            return ("_name" in gridNode) && nameRegExp.test(gridNode._name);
290        }
291
292        function matchesQuery(gridNode)
293        {
294            delete gridNode._searchMatched;
295            if (matchesByName(gridNode)) {
296                gridNode._searchMatched = true;
297                gridNode.refresh();
298                return true;
299            }
300            return false;
301        }
302
303        var current = this.dataGrid.rootNode().children[0];
304        var depth = 0;
305        var info = {};
306
307        // Restrict to type nodes and instances.
308        const maxDepth = 1;
309
310        while (current) {
311            if (matchesQuery(current))
312                this._searchResults.push({ node: current });
313            current = current.traverseNextNode(false, null, (depth >= maxDepth), info);
314            depth += info.depthChange;
315        }
316
317        finishedCallback(this, this._searchResults.length);
318    },
319
320    jumpToFirstSearchResult: function()
321    {
322        if (!this._searchResults || !this._searchResults.length)
323            return;
324        this._currentSearchResultIndex = 0;
325        this._jumpToSearchResult(this._currentSearchResultIndex);
326    },
327
328    jumpToLastSearchResult: function()
329    {
330        if (!this._searchResults || !this._searchResults.length)
331            return;
332        this._currentSearchResultIndex = (this._searchResults.length - 1);
333        this._jumpToSearchResult(this._currentSearchResultIndex);
334    },
335
336    jumpToNextSearchResult: function()
337    {
338        if (!this._searchResults || !this._searchResults.length)
339            return;
340        if (++this._currentSearchResultIndex >= this._searchResults.length)
341            this._currentSearchResultIndex = 0;
342        this._jumpToSearchResult(this._currentSearchResultIndex);
343    },
344
345    jumpToPreviousSearchResult: function()
346    {
347        if (!this._searchResults || !this._searchResults.length)
348            return;
349        if (--this._currentSearchResultIndex < 0)
350            this._currentSearchResultIndex = (this._searchResults.length - 1);
351        this._jumpToSearchResult(this._currentSearchResultIndex);
352    },
353
354    showingFirstSearchResult: function()
355    {
356        return (this._currentSearchResultIndex === 0);
357    },
358
359    showingLastSearchResult: function()
360    {
361        return (this._searchResults && this._currentSearchResultIndex === (this._searchResults.length - 1));
362    },
363
364    currentSearchResultIndex: function() {
365        return this._currentSearchResultIndex;
366    },
367
368    _jumpToSearchResult: function(index)
369    {
370        var searchResult = this._searchResults[index];
371        if (!searchResult)
372            return;
373
374        var node = searchResult.node;
375        node.revealAndSelect();
376    },
377
378    refreshVisibleData: function()
379    {
380        var child = this.dataGrid.rootNode().children[0];
381        while (child) {
382            child.refresh();
383            child = child.traverseNextNode(false, null, true);
384        }
385    },
386
387    _changeBase: function()
388    {
389        if (this._baseProfileUid === this._profiles()[this.baseSelect.selectedIndex()].uid)
390            return;
391
392        this._baseProfileUid = this._profiles()[this.baseSelect.selectedIndex()].uid;
393        var dataGrid = /** @type {!WebInspector.HeapSnapshotDiffDataGrid} */ (this.dataGrid);
394        // Change set base data source only if main data source is already set.
395        if (dataGrid.snapshot)
396            this.baseProfile.load(dataGrid.setBaseDataSource.bind(dataGrid));
397
398        if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults)
399            return;
400
401        // The current search needs to be performed again. First negate out previous match
402        // count by calling the search finished callback with a negative number of matches.
403        // Then perform the search again with the same query and callback.
404        this._searchFinishedCallback(this, -this._searchResults.length);
405        this.performSearch(this.currentQuery, this._searchFinishedCallback);
406    },
407
408    _changeFilter: function()
409    {
410        var profileIndex = this.filterSelect.selectedIndex() - 1;
411        this.dataGrid.filterSelectIndexChanged(this._profiles(), profileIndex);
412
413        WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
414            action: WebInspector.UserMetrics.UserActionNames.HeapSnapshotFilterChanged,
415            label: this.filterSelect.selectedOption().label
416        });
417
418        if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults)
419            return;
420
421        // The current search needs to be performed again. First negate out previous match
422        // count by calling the search finished callback with a negative number of matches.
423        // Then perform the search again with the same query and callback.
424        this._searchFinishedCallback(this, -this._searchResults.length);
425        this.performSearch(this.currentQuery, this._searchFinishedCallback);
426    },
427
428    _createToolbarWithClassNameFilter: function()
429    {
430        var toolbar = document.createElement("div");
431        toolbar.classList.add("class-view-toolbar");
432        var classNameFilter = document.createElement("input");
433        classNameFilter.classList.add("class-name-filter");
434        classNameFilter.setAttribute("placeholder", WebInspector.UIString("Class filter"));
435        classNameFilter.addEventListener("keyup", this._changeNameFilter.bind(this, classNameFilter), false);
436        toolbar.appendChild(classNameFilter);
437        return toolbar;
438    },
439
440    _changeNameFilter: function(classNameInputElement)
441    {
442        var filter = classNameInputElement.value;
443        this.dataGrid.changeNameFilter(filter);
444    },
445
446    /**
447     * @return {!Array.<!WebInspector.ProfileHeader>}
448     */
449    _profiles: function()
450    {
451        return this._profile.profileType().getProfiles();
452    },
453
454    /**
455     * @param {!WebInspector.ContextMenu} contextMenu
456     * @param {?Event} event
457     */
458    populateContextMenu: function(contextMenu, event)
459    {
460        this.dataGrid.populateContextMenu(this.parent, contextMenu, event);
461    },
462
463    _selectionChanged: function(event)
464    {
465        var selectedNode = event.target.selectedNode;
466        this._setRetainmentDataGridSource(selectedNode);
467        this._inspectedObjectChanged(event);
468    },
469
470    _inspectedObjectChanged: function(event)
471    {
472        var selectedNode = event.target.selectedNode;
473        if (!this.profile.fromFile() && selectedNode instanceof WebInspector.HeapSnapshotGenericObjectNode)
474            ConsoleAgent.addInspectedHeapObject(selectedNode.snapshotNodeId);
475    },
476
477    _setRetainmentDataGridSource: function(nodeItem)
478    {
479        if (nodeItem && nodeItem.snapshotNodeIndex)
480            this.retainmentDataGrid.setDataSource(nodeItem.isDeletedNode ? nodeItem.dataGrid.baseSnapshot : nodeItem.dataGrid.snapshot, nodeItem.snapshotNodeIndex);
481        else
482            this.retainmentDataGrid.reset();
483    },
484
485    _mouseDownInContentsGrid: function(event)
486    {
487        if (event.detail < 2)
488            return;
489
490        var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
491        if (!cell || (!cell.classList.contains("count-column") && !cell.classList.contains("shallowSize-column") && !cell.classList.contains("retainedSize-column")))
492            return;
493
494        event.consume(true);
495    },
496
497    changeView: function(viewTitle, callback)
498    {
499        var viewIndex = null;
500        for (var i = 0; i < this.views.length; ++i) {
501            if (this.views[i].title === viewTitle) {
502                viewIndex = i;
503                break;
504            }
505        }
506        if (this.views.current === viewIndex || viewIndex == null) {
507            setTimeout(callback, 0);
508            return;
509        }
510
511        /**
512         * @this {WebInspector.HeapSnapshotView}
513         */
514        function dataGridContentShown(event)
515        {
516            var dataGrid = event.data;
517            dataGrid.removeEventListener(WebInspector.HeapSnapshotSortableDataGrid.Events.ContentShown, dataGridContentShown, this);
518            if (dataGrid === this.dataGrid)
519                callback();
520        }
521        this.views[viewIndex].grid.addEventListener(WebInspector.HeapSnapshotSortableDataGrid.Events.ContentShown, dataGridContentShown, this);
522
523        this.viewSelect.setSelectedIndex(viewIndex);
524        this._changeView(viewIndex);
525    },
526
527    _updateDataSourceAndView: function()
528    {
529        var dataGrid = this.dataGrid;
530        if (dataGrid.snapshot)
531            return;
532
533        this.profile.load(didLoadSnapshot.bind(this));
534
535        /**
536         * @this {WebInspector.HeapSnapshotView}
537         */
538        function didLoadSnapshot(snapshotProxy)
539        {
540            if (this.dataGrid !== dataGrid)
541                return;
542            if (dataGrid.snapshot !== snapshotProxy)
543                dataGrid.setDataSource(snapshotProxy);
544            if (dataGrid === this.diffDataGrid) {
545                if (!this._baseProfileUid)
546                    this._baseProfileUid = this._profiles()[this.baseSelect.selectedIndex()].uid;
547                this.baseProfile.load(didLoadBaseSnaphot.bind(this));
548            }
549        }
550
551        /**
552         * @this {WebInspector.HeapSnapshotView}
553         */
554        function didLoadBaseSnaphot(baseSnapshotProxy)
555        {
556            if (this.diffDataGrid.baseSnapshot !== baseSnapshotProxy)
557                this.diffDataGrid.setBaseDataSource(baseSnapshotProxy);
558        }
559    },
560
561    _onSelectedViewChanged: function(event)
562    {
563        this._changeView(event.target.selectedIndex);
564    },
565
566    _updateSelectorsVisibility: function()
567    {
568        if (this.currentView === this.diffView)
569            this.baseSelect.element.classList.remove("hidden");
570        else
571            this.baseSelect.element.classList.add("hidden");
572
573        if (this.currentView === this.constructorsView) {
574            if (this._trackingOverviewGrid) {
575                this._trackingOverviewGrid.element.classList.remove("hidden");
576                this._trackingOverviewGrid.update();
577                this.viewsContainer.classList.add("reserve-80px-at-top");
578            }
579            this.filterSelect.element.classList.remove("hidden");
580        } else {
581            this.filterSelect.element.classList.add("hidden");
582            if (this._trackingOverviewGrid) {
583                this._trackingOverviewGrid.element.classList.add("hidden");
584                this.viewsContainer.classList.remove("reserve-80px-at-top");
585            }
586        }
587    },
588
589    _changeView: function(selectedIndex)
590    {
591        if (selectedIndex === this.views.current)
592            return;
593
594        this.views.current = selectedIndex;
595        this.currentView.detach();
596        var view = this.views[this.views.current];
597        this.currentView = view.view;
598        this.dataGrid = view.grid;
599        this.currentView.show(this.viewsContainer);
600        this.refreshVisibleData();
601        this.dataGrid.updateWidths();
602
603        this._updateSelectorsVisibility();
604
605        this._updateDataSourceAndView();
606
607        if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults)
608            return;
609
610        // The current search needs to be performed again. First negate out previous match
611        // count by calling the search finished callback with a negative number of matches.
612        // Then perform the search again the with same query and callback.
613        this._searchFinishedCallback(this, -this._searchResults.length);
614        this.performSearch(this.currentQuery, this._searchFinishedCallback);
615    },
616
617    _getHoverAnchor: function(target)
618    {
619        var span = target.enclosingNodeOrSelfWithNodeName("span");
620        if (!span)
621            return;
622        var row = target.enclosingNodeOrSelfWithNodeName("tr");
623        if (!row)
624            return;
625        span.node = row._dataGridNode;
626        return span;
627    },
628
629    _resolveObjectForPopover: function(element, showCallback, objectGroupName)
630    {
631        if (this.profile.fromFile())
632            return;
633        element.node.queryObjectContent(showCallback, objectGroupName);
634    },
635
636    /**
637     * @return {boolean}
638     */
639    _startRetainersHeaderDragging: function(event)
640    {
641        if (!this.isShowing())
642            return false;
643
644        this._previousDragPosition = event.pageY;
645        return true;
646    },
647
648    _retainersHeaderDragging: function(event)
649    {
650        var height = this.retainmentView.element.clientHeight;
651        height += this._previousDragPosition - event.pageY;
652        this._previousDragPosition = event.pageY;
653        this._updateRetainmentViewHeight(height);
654        event.consume(true);
655    },
656
657    _endRetainersHeaderDragging: function(event)
658    {
659        delete this._previousDragPosition;
660        event.consume();
661    },
662
663    _updateRetainmentViewHeight: function(height)
664    {
665        height = Number.constrain(height, Preferences.minConsoleHeight, this.element.clientHeight - Preferences.minConsoleHeight);
666        this.viewsContainer.style.bottom = (height + this.retainmentViewHeader.clientHeight) + "px";
667        if (this._trackingOverviewGrid && this.currentView === this.constructorsView)
668            this.viewsContainer.classList.add("reserve-80px-at-top");
669        this.retainmentView.element.style.height = height + "px";
670        this.retainmentViewHeader.style.bottom = height + "px";
671        this.currentView.doResize();
672    },
673
674    _updateBaseOptions: function()
675    {
676        var list = this._profiles();
677        // We're assuming that snapshots can only be added.
678        if (this.baseSelect.size() === list.length)
679            return;
680
681        for (var i = this.baseSelect.size(), n = list.length; i < n; ++i) {
682            var title = list[i].title;
683            this.baseSelect.createOption(title);
684        }
685    },
686
687    _updateFilterOptions: function()
688    {
689        var list = this._profiles();
690        // We're assuming that snapshots can only be added.
691        if (this.filterSelect.size() - 1 === list.length)
692            return;
693
694        if (!this.filterSelect.size())
695            this.filterSelect.createOption(WebInspector.UIString("All objects"));
696
697        for (var i = this.filterSelect.size() - 1, n = list.length; i < n; ++i) {
698            var title = list[i].title;
699            if (!i)
700                title = WebInspector.UIString("Objects allocated before %s", title);
701            else
702                title = WebInspector.UIString("Objects allocated between %s and %s", list[i - 1].title, title);
703            this.filterSelect.createOption(title);
704        }
705    },
706
707    /**
708     * @param {!WebInspector.Event} event
709     */
710    _onProfileHeaderAdded: function(event)
711    {
712        if (!event.data || event.data.type !== this._profile.profileType().id)
713            return;
714        this._updateBaseOptions();
715        this._updateFilterOptions();
716    },
717
718    __proto__: WebInspector.View.prototype
719}
720
721/**
722 * @constructor
723 * @implements {HeapProfilerAgent.Dispatcher}
724 */
725WebInspector.HeapProfilerDispatcher = function()
726{
727    this._dispatchers = [];
728    InspectorBackend.registerHeapProfilerDispatcher(this);
729}
730
731WebInspector.HeapProfilerDispatcher.prototype = {
732    /**
733     * @param {!HeapProfilerAgent.Dispatcher} dispatcher
734     */
735    register: function(dispatcher)
736    {
737        this._dispatchers.push(dispatcher);
738    },
739
740    _genericCaller: function(eventName)
741    {
742        var args = Array.prototype.slice.call(arguments.callee.caller.arguments);
743        for (var i = 0; i < this._dispatchers.length; ++i)
744            this._dispatchers[i][eventName].apply(this._dispatchers[i], args);
745    },
746
747    /**
748     * @override
749     * @param {!Array.<number>} samples
750     */
751    heapStatsUpdate: function(samples)
752    {
753        this._genericCaller("heapStatsUpdate");
754    },
755
756    /**
757     * @override
758     * @param {number} lastSeenObjectId
759     * @param {number} timestamp
760     */
761    lastSeenObjectId: function(lastSeenObjectId, timestamp)
762    {
763        this._genericCaller("lastSeenObjectId");
764    },
765
766    /**
767     * @param {!HeapProfilerAgent.ProfileHeader} profileHeader
768     */
769    addProfileHeader: function(profileHeader)
770    {
771        this._genericCaller("addProfileHeader");
772    },
773
774    /**
775     * @override
776     * @param {number} uid
777     * @param {string} chunk
778     */
779    addHeapSnapshotChunk: function(uid, chunk)
780    {
781        this._genericCaller("addHeapSnapshotChunk");
782    },
783
784    /**
785     * @override
786     * @param {number} done
787     * @param {number} total
788     */
789    reportHeapSnapshotProgress: function(done, total)
790    {
791        this._genericCaller("reportHeapSnapshotProgress");
792    },
793
794    /**
795     * @override
796     */
797    resetProfiles: function()
798    {
799        this._genericCaller("resetProfiles");
800    }
801}
802
803WebInspector.HeapProfilerDispatcher._dispatcher = new WebInspector.HeapProfilerDispatcher();
804
805/**
806 * @constructor
807 * @extends {WebInspector.ProfileType}
808 * @implements {HeapProfilerAgent.Dispatcher}
809 */
810WebInspector.HeapSnapshotProfileType = function()
811{
812    WebInspector.ProfileType.call(this, WebInspector.HeapSnapshotProfileType.TypeId, WebInspector.UIString("Take Heap Snapshot"));
813    WebInspector.HeapProfilerDispatcher._dispatcher.register(this);
814}
815
816WebInspector.HeapSnapshotProfileType.TypeId = "HEAP";
817WebInspector.HeapSnapshotProfileType.SnapshotReceived = "SnapshotReceived";
818
819WebInspector.HeapSnapshotProfileType.prototype = {
820    /**
821     * @override
822     * @return {string}
823     */
824    fileExtension: function()
825    {
826        return ".heapsnapshot";
827    },
828
829    get buttonTooltip()
830    {
831        return WebInspector.UIString("Take heap snapshot.");
832    },
833
834    /**
835     * @override
836     * @return {boolean}
837     */
838    isInstantProfile: function()
839    {
840        return true;
841    },
842
843    /**
844     * @override
845     * @return {boolean}
846     */
847    buttonClicked: function()
848    {
849        this._takeHeapSnapshot(function() {});
850        WebInspector.userMetrics.ProfilesHeapProfileTaken.record();
851        return false;
852    },
853
854    /**
855     * @override
856     * @param {!Array.<number>} samples
857     */
858    heapStatsUpdate: function(samples)
859    {
860    },
861
862    /**
863     * @override
864     * @param {number} lastSeenObjectId
865     * @param {number} timestamp
866     */
867    lastSeenObjectId: function(lastSeenObjectId, timestamp)
868    {
869    },
870
871    get treeItemTitle()
872    {
873        return WebInspector.UIString("HEAP SNAPSHOTS");
874    },
875
876    get description()
877    {
878        return WebInspector.UIString("Heap snapshot profiles show memory distribution among your page's JavaScript objects and related DOM nodes.");
879    },
880
881    /**
882     * @override
883     * @param {!string} title
884     * @return {!WebInspector.ProfileHeader}
885     */
886    createProfileLoadedFromFile: function(title)
887    {
888        return new WebInspector.HeapProfileHeader(this, title);
889    },
890
891    _takeHeapSnapshot: function(callback)
892    {
893        if (this.profileBeingRecorded())
894            return;
895        this._profileBeingRecorded = new WebInspector.HeapProfileHeader(this, WebInspector.UIString("Snapshotting\u2026"));
896        this.addProfile(this._profileBeingRecorded);
897        HeapProfilerAgent.takeHeapSnapshot(true, callback);
898    },
899
900    /**
901     * @param {!HeapProfilerAgent.ProfileHeader} profileHeader
902     */
903    addProfileHeader: function(profileHeader)
904    {
905        var profile = this.profileBeingRecorded();
906        if (!profile)
907            return;
908        profile.title = profileHeader.title;
909        profile.uid = profileHeader.uid;
910        profile.maxJSObjectId = profileHeader.maxJSObjectId || 0;
911
912        profile.sidebarElement.mainTitle = profile.title;
913        profile.sidebarElement.subtitle = "";
914        profile.sidebarElement.wait = false;
915
916        this._profileSamples = null;
917        this._profileBeingRecorded = null;
918
919        WebInspector.panels.profiles._showProfile(profile);
920        profile.existingView()._refreshView();
921    },
922
923    /**
924     * @override
925     * @param {number} uid
926     * @param {string} chunk
927     */
928    addHeapSnapshotChunk: function(uid, chunk)
929    {
930        var profile = this.getProfile(uid);
931        if (profile)
932            profile.transferChunk(chunk);
933    },
934
935    /**
936     * @override
937     * @param {number} done
938     * @param {number} total
939     */
940    reportHeapSnapshotProgress: function(done, total)
941    {
942        var profile = this.profileBeingRecorded();
943        if (!profile)
944            return;
945        profile.sidebarElement.subtitle = WebInspector.UIString("%.0f%", (done / total) * 100);
946        profile.sidebarElement.wait = true;
947    },
948
949    /**
950     * @override
951     */
952    resetProfiles: function()
953    {
954        this._reset();
955    },
956
957    /**
958     * @override
959     * @param {!WebInspector.ProfileHeader} profile
960     */
961    removeProfile: function(profile)
962    {
963        if (this._profileBeingRecorded !== profile && !profile.fromFile())
964            HeapProfilerAgent.removeProfile(profile.uid);
965        WebInspector.ProfileType.prototype.removeProfile.call(this, profile);
966    },
967
968    _snapshotReceived: function(profile)
969    {
970        if (this._profileBeingRecorded === profile)
971            this._profileBeingRecorded = null;
972        this.dispatchEventToListeners(WebInspector.HeapSnapshotProfileType.SnapshotReceived, profile);
973    },
974
975    __proto__: WebInspector.ProfileType.prototype
976}
977
978
979/**
980 * @constructor
981 * @extends {WebInspector.HeapSnapshotProfileType}
982 * @param {!WebInspector.ProfilesPanel} profilesPanel
983 */
984WebInspector.TrackingHeapSnapshotProfileType = function(profilesPanel)
985{
986    WebInspector.ProfileType.call(this, WebInspector.TrackingHeapSnapshotProfileType.TypeId, WebInspector.UIString("Record Heap Allocations"));
987    this._profilesPanel = profilesPanel;
988    WebInspector.HeapProfilerDispatcher._dispatcher.register(this);
989}
990
991WebInspector.TrackingHeapSnapshotProfileType.TypeId = "HEAP-RECORD";
992
993WebInspector.TrackingHeapSnapshotProfileType.HeapStatsUpdate = "HeapStatsUpdate";
994WebInspector.TrackingHeapSnapshotProfileType.TrackingStarted = "TrackingStarted";
995WebInspector.TrackingHeapSnapshotProfileType.TrackingStopped = "TrackingStopped";
996
997WebInspector.TrackingHeapSnapshotProfileType.prototype = {
998
999    /**
1000     * @override
1001     * @param {!Array.<number>} samples
1002     */
1003    heapStatsUpdate: function(samples)
1004    {
1005        if (!this._profileSamples)
1006            return;
1007        var index;
1008        for (var i = 0; i < samples.length; i += 3) {
1009            index = samples[i];
1010            var count = samples[i+1];
1011            var size  = samples[i+2];
1012            this._profileSamples.sizes[index] = size;
1013            if (!this._profileSamples.max[index] || size > this._profileSamples.max[index])
1014                this._profileSamples.max[index] = size;
1015        }
1016        this._lastUpdatedIndex = index;
1017    },
1018
1019    /**
1020     * @override
1021     * @param {number} lastSeenObjectId
1022     * @param {number} timestamp
1023     */
1024    lastSeenObjectId: function(lastSeenObjectId, timestamp)
1025    {
1026        var profileSamples = this._profileSamples;
1027        if (!profileSamples)
1028            return;
1029        var currentIndex = Math.max(profileSamples.ids.length, profileSamples.max.length - 1);
1030        profileSamples.ids[currentIndex] = lastSeenObjectId;
1031        if (!profileSamples.max[currentIndex]) {
1032            profileSamples.max[currentIndex] = 0;
1033            profileSamples.sizes[currentIndex] = 0;
1034        }
1035        profileSamples.timestamps[currentIndex] = timestamp;
1036        if (profileSamples.totalTime < timestamp - profileSamples.timestamps[0])
1037            profileSamples.totalTime *= 2;
1038        this.dispatchEventToListeners(WebInspector.TrackingHeapSnapshotProfileType.HeapStatsUpdate, this._profileSamples);
1039        var profile = this._profileBeingRecorded;
1040        profile.sidebarElement.wait = true;
1041        if (profile.sidebarElement && !profile.sidebarElement.wait)
1042            profile.sidebarElement.wait = true;
1043    },
1044
1045    /**
1046     * @override
1047     * @return {boolean}
1048     */
1049    hasTemporaryView: function()
1050    {
1051        return true;
1052    },
1053
1054    get buttonTooltip()
1055    {
1056        return this._recording ? WebInspector.UIString("Stop recording heap profile.") : WebInspector.UIString("Start recording heap profile.");
1057    },
1058
1059    /**
1060     * @override
1061     * @return {boolean}
1062     */
1063    isInstantProfile: function()
1064    {
1065        return false;
1066    },
1067
1068    /**
1069     * @override
1070     * @return {boolean}
1071     */
1072    buttonClicked: function()
1073    {
1074        return this._toggleRecording();
1075    },
1076
1077    _startRecordingProfile: function()
1078    {
1079        if (this.profileBeingRecorded())
1080            return;
1081        this._profileBeingRecorded = new WebInspector.HeapProfileHeader(this, WebInspector.UIString("Recording\u2026"));
1082        this._lastSeenIndex = -1;
1083        this._profileSamples = {
1084            'sizes': [],
1085            'ids': [],
1086            'timestamps': [],
1087            'max': [],
1088            'totalTime': 30000
1089        };
1090        this._profileBeingRecorded._profileSamples = this._profileSamples;
1091        this._recording = true;
1092        this.addProfile(this._profileBeingRecorded);
1093        HeapProfilerAgent.startTrackingHeapObjects();
1094        this.dispatchEventToListeners(WebInspector.TrackingHeapSnapshotProfileType.TrackingStarted);
1095    },
1096
1097    _stopRecordingProfile: function()
1098    {
1099        HeapProfilerAgent.stopTrackingHeapObjects(true);
1100        this._recording = false;
1101        this.dispatchEventToListeners(WebInspector.TrackingHeapSnapshotProfileType.TrackingStopped);
1102    },
1103
1104    _toggleRecording: function()
1105    {
1106        if (this._recording)
1107            this._stopRecordingProfile();
1108        else
1109            this._startRecordingProfile();
1110        return this._recording;
1111    },
1112
1113    get treeItemTitle()
1114    {
1115        return WebInspector.UIString("HEAP TIMELINES");
1116    },
1117
1118    get description()
1119    {
1120        return WebInspector.UIString("Record JavaScript object allocations over time. Use this profile type to isolate memory leaks.");
1121    },
1122
1123    _reset: function()
1124    {
1125        WebInspector.HeapSnapshotProfileType.prototype._reset.call(this);
1126        if (this._recording)
1127            this._stopRecordingProfile();
1128        this._profileSamples = null;
1129        this._lastSeenIndex = -1;
1130    },
1131
1132    __proto__: WebInspector.HeapSnapshotProfileType.prototype
1133}
1134
1135/**
1136 * @constructor
1137 * @extends {WebInspector.ProfileHeader}
1138 * @param {!WebInspector.ProfileType} type
1139 * @param {string} title
1140 * @param {number=} uid
1141 * @param {number=} maxJSObjectId
1142 */
1143WebInspector.HeapProfileHeader = function(type, title, uid, maxJSObjectId)
1144{
1145    WebInspector.ProfileHeader.call(this, type, title, uid);
1146    this.maxJSObjectId = maxJSObjectId;
1147    /**
1148     * @type {?WebInspector.OutputStream}
1149     */
1150    this._receiver = null;
1151    /**
1152     * @type {?WebInspector.HeapSnapshotProxy}
1153     */
1154    this._snapshotProxy = null;
1155    this._totalNumberOfChunks = 0;
1156    this._transferHandler = null;
1157}
1158
1159WebInspector.HeapProfileHeader.prototype = {
1160    /**
1161     * @override
1162     */
1163    createSidebarTreeElement: function()
1164    {
1165        return new WebInspector.ProfileSidebarTreeElement(this, "heap-snapshot-sidebar-tree-item");
1166    },
1167
1168    /**
1169     * @override
1170     * @param {!WebInspector.ProfilesPanel} profilesPanel
1171     */
1172    createView: function(profilesPanel)
1173    {
1174        return new WebInspector.HeapSnapshotView(profilesPanel, this);
1175    },
1176
1177    /**
1178     * @override
1179     * @param {function(!WebInspector.HeapSnapshotProxy):void} callback
1180     */
1181    load: function(callback)
1182    {
1183        if (this.uid === -1)
1184            return;
1185        if (this._snapshotProxy) {
1186            callback(this._snapshotProxy);
1187            return;
1188        }
1189
1190        this._numberOfChunks = 0;
1191        if (!this._receiver) {
1192            this._setupWorker();
1193            this._transferHandler = new WebInspector.BackendSnapshotLoader(this);
1194            this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026");
1195            this.sidebarElement.wait = true;
1196            this._transferSnapshot();
1197        }
1198        var loaderProxy = /** @type {?WebInspector.HeapSnapshotLoaderProxy} */ (this._receiver);
1199        console.assert(loaderProxy);
1200        loaderProxy.addConsumer(callback);
1201    },
1202
1203    _transferSnapshot: function()
1204    {
1205        /**
1206         * @this {WebInspector.HeapProfileHeader}
1207         */
1208        function finishTransfer()
1209        {
1210            if (this._transferHandler) {
1211                this._transferHandler.finishTransfer();
1212                this._totalNumberOfChunks = this._transferHandler._totalNumberOfChunks;
1213            }
1214        }
1215        HeapProfilerAgent.getHeapSnapshot(this.uid, finishTransfer.bind(this));
1216    },
1217
1218    snapshotConstructorName: function()
1219    {
1220        return "JSHeapSnapshot";
1221    },
1222
1223    snapshotProxyConstructor: function()
1224    {
1225        return WebInspector.HeapSnapshotProxy;
1226    },
1227
1228    _setupWorker: function()
1229    {
1230        /**
1231         * @this {WebInspector.HeapProfileHeader}
1232         */
1233        function setProfileWait(event)
1234        {
1235            this.sidebarElement.wait = event.data;
1236        }
1237        var worker = new WebInspector.HeapSnapshotWorkerProxy(this._handleWorkerEvent.bind(this));
1238        worker.addEventListener("wait", setProfileWait, this);
1239        var loaderProxy = worker.createLoader(this.snapshotConstructorName(), this.snapshotProxyConstructor());
1240        loaderProxy.addConsumer(this._snapshotReceived.bind(this));
1241        this._receiver = loaderProxy;
1242    },
1243
1244    /**
1245     * @param {string} eventName
1246     * @param {*} data
1247     */
1248    _handleWorkerEvent: function(eventName, data)
1249    {
1250        if (WebInspector.HeapSnapshotProgress.Event.Update !== eventName)
1251            return;
1252        this._updateSubtitle(data);
1253    },
1254
1255    /**
1256     * @override
1257     */
1258    dispose: function()
1259    {
1260        if (this._receiver)
1261            this._receiver.close();
1262        else if (this._snapshotProxy)
1263            this._snapshotProxy.dispose();
1264        if (this._view) {
1265            var view = this._view;
1266            this._view = null;
1267            view.dispose();
1268        }
1269    },
1270
1271    _updateSubtitle: function(value)
1272    {
1273        this.sidebarElement.subtitle = value;
1274    },
1275
1276    _didCompleteSnapshotTransfer: function()
1277    {
1278        this.sidebarElement.subtitle = Number.bytesToString(this._snapshotProxy.totalSize);
1279        this.sidebarElement.wait = false;
1280    },
1281
1282    /**
1283     * @param {string} chunk
1284     */
1285    transferChunk: function(chunk)
1286    {
1287        this._transferHandler.transferChunk(chunk);
1288    },
1289
1290    _snapshotReceived: function(snapshotProxy)
1291    {
1292        this._receiver = null;
1293        if (snapshotProxy)
1294            this._snapshotProxy = snapshotProxy;
1295        this._didCompleteSnapshotTransfer();
1296        var worker = /** @type {!WebInspector.HeapSnapshotWorkerProxy} */ (this._snapshotProxy.worker);
1297        worker.startCheckingForLongRunningCalls();
1298        this.notifySnapshotReceived();
1299
1300        /**
1301         * @this {WebInspector.HeapProfileHeader}
1302         */
1303        function didGetMaxNodeId(id)
1304        {
1305           this.maxJSObjectId = id;
1306        }
1307
1308        if (this.fromFile())
1309            snapshotProxy.maxJsNodeId(didGetMaxNodeId.bind(this));
1310    },
1311
1312    notifySnapshotReceived: function()
1313    {
1314        this._profileType._snapshotReceived(this);
1315    },
1316
1317    // Hook point for tests.
1318    _wasShown: function()
1319    {
1320    },
1321
1322    /**
1323     * @override
1324     * @return {boolean}
1325     */
1326    canSaveToFile: function()
1327    {
1328        return !this.fromFile() && !!this._snapshotProxy && !this._receiver;
1329    },
1330
1331    /**
1332     * @override
1333     */
1334    saveToFile: function()
1335    {
1336        var fileOutputStream = new WebInspector.FileOutputStream();
1337
1338        /**
1339         * @param {boolean} accepted
1340         * @this {WebInspector.HeapProfileHeader}
1341         */
1342        function onOpen(accepted)
1343        {
1344            if (!accepted)
1345                return;
1346            this._receiver = fileOutputStream;
1347            this._transferHandler = new WebInspector.SaveSnapshotHandler(this);
1348            this._transferSnapshot();
1349        }
1350        this._fileName = this._fileName || "Heap-" + new Date().toISO8601Compact() + this._profileType.fileExtension();
1351        fileOutputStream.open(this._fileName, onOpen.bind(this));
1352    },
1353
1354    /**
1355     * @override
1356     * @param {!File} file
1357     */
1358    loadFromFile: function(file)
1359    {
1360        this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026");
1361        this.sidebarElement.wait = true;
1362        this._setupWorker();
1363
1364        var delegate = new WebInspector.HeapSnapshotLoadFromFileDelegate(this);
1365        var fileReader = this._createFileReader(file, delegate);
1366        fileReader.start(this._receiver);
1367    },
1368
1369    _createFileReader: function(file, delegate)
1370    {
1371        return new WebInspector.ChunkedFileReader(file, 10000000, delegate);
1372    },
1373
1374    __proto__: WebInspector.ProfileHeader.prototype
1375}
1376
1377
1378/**
1379 * @constructor
1380 * @param {!WebInspector.HeapProfileHeader} header
1381 * @param {string} title
1382 */
1383WebInspector.SnapshotTransferHandler = function(header, title)
1384{
1385    this._numberOfChunks = 0;
1386    this._savedChunks = 0;
1387    this._header = header;
1388    this._totalNumberOfChunks = 0;
1389    this._title = title;
1390}
1391
1392
1393WebInspector.SnapshotTransferHandler.prototype = {
1394    /**
1395     * @param {string} chunk
1396     */
1397    transferChunk: function(chunk)
1398    {
1399        ++this._numberOfChunks;
1400        this._header._receiver.write(chunk, this._didTransferChunk.bind(this));
1401    },
1402
1403    finishTransfer: function()
1404    {
1405    },
1406
1407    _didTransferChunk: function()
1408    {
1409        this._updateProgress(++this._savedChunks, this._totalNumberOfChunks);
1410    },
1411
1412    _updateProgress: function(value, total)
1413    {
1414    }
1415}
1416
1417
1418/**
1419 * @constructor
1420 * @param {!WebInspector.HeapProfileHeader} header
1421 * @extends {WebInspector.SnapshotTransferHandler}
1422 */
1423WebInspector.SaveSnapshotHandler = function(header)
1424{
1425    WebInspector.SnapshotTransferHandler.call(this, header, "Saving\u2026 %d\%");
1426    this._totalNumberOfChunks = header._totalNumberOfChunks;
1427    this._updateProgress(0, this._totalNumberOfChunks);
1428}
1429
1430
1431WebInspector.SaveSnapshotHandler.prototype = {
1432    _updateProgress: function(value, total)
1433    {
1434        var percentValue = ((total ? (value / total) : 0) * 100).toFixed(0);
1435        this._header._updateSubtitle(WebInspector.UIString(this._title, percentValue));
1436        if (value === total) {
1437            this._header._receiver.close();
1438            this._header._didCompleteSnapshotTransfer();
1439        }
1440    },
1441
1442    __proto__: WebInspector.SnapshotTransferHandler.prototype
1443}
1444
1445
1446/**
1447 * @constructor
1448 * @param {!WebInspector.HeapProfileHeader} header
1449 * @extends {WebInspector.SnapshotTransferHandler}
1450 */
1451WebInspector.BackendSnapshotLoader = function(header)
1452{
1453    WebInspector.SnapshotTransferHandler.call(this, header, "Loading\u2026 %d\%");
1454}
1455
1456
1457WebInspector.BackendSnapshotLoader.prototype = {
1458    finishTransfer: function()
1459    {
1460        this._header._receiver.close(this._didFinishTransfer.bind(this));
1461        this._totalNumberOfChunks = this._numberOfChunks;
1462    },
1463
1464    _didFinishTransfer: function()
1465    {
1466        console.assert(this._totalNumberOfChunks === this._savedChunks, "Not all chunks were transfered.");
1467    },
1468
1469    __proto__: WebInspector.SnapshotTransferHandler.prototype
1470}
1471
1472
1473/**
1474 * @constructor
1475 * @implements {WebInspector.OutputStreamDelegate}
1476 */
1477WebInspector.HeapSnapshotLoadFromFileDelegate = function(snapshotHeader)
1478{
1479    this._snapshotHeader = snapshotHeader;
1480}
1481
1482WebInspector.HeapSnapshotLoadFromFileDelegate.prototype = {
1483    onTransferStarted: function()
1484    {
1485    },
1486
1487    /**
1488     * @param {!WebInspector.ChunkedReader} reader
1489     */
1490    onChunkTransferred: function(reader)
1491    {
1492    },
1493
1494    onTransferFinished: function()
1495    {
1496    },
1497
1498    /**
1499     * @param {!WebInspector.ChunkedReader} reader
1500     */
1501    onError: function (reader, e)
1502    {
1503        switch(e.target.error.code) {
1504        case e.target.error.NOT_FOUND_ERR:
1505            this._snapshotHeader._updateSubtitle(WebInspector.UIString("'%s' not found.", reader.fileName()));
1506        break;
1507        case e.target.error.NOT_READABLE_ERR:
1508            this._snapshotHeader._updateSubtitle(WebInspector.UIString("'%s' is not readable", reader.fileName()));
1509        break;
1510        case e.target.error.ABORT_ERR:
1511            break;
1512        default:
1513            this._snapshotHeader._updateSubtitle(WebInspector.UIString("'%s' error %d", reader.fileName(), e.target.error.code));
1514        }
1515    }
1516}
1517
1518/**
1519 * @constructor
1520 * @extends {WebInspector.View}
1521 * @param {!WebInspector.HeapProfileHeader} heapProfileHeader
1522 */
1523WebInspector.HeapTrackingOverviewGrid = function(heapProfileHeader)
1524{
1525    WebInspector.View.call(this);
1526    this.registerRequiredCSS("flameChart.css");
1527    this.element.id = "heap-recording-view";
1528
1529    this._overviewContainer = this.element.createChild("div", "overview-container");
1530    this._overviewGrid = new WebInspector.OverviewGrid("heap-recording");
1531    this._overviewGrid.element.classList.add("fill");
1532
1533    this._overviewCanvas = this._overviewContainer.createChild("canvas", "heap-recording-overview-canvas");
1534    this._overviewContainer.appendChild(this._overviewGrid.element);
1535    this._overviewCalculator = new WebInspector.HeapTrackingOverviewGrid.OverviewCalculator();
1536    this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
1537
1538    this._profileSamples = heapProfileHeader._profileSamples;
1539    if (heapProfileHeader.profileType().profileBeingRecorded() === heapProfileHeader) {
1540        this._profileType = heapProfileHeader._profileType;
1541        this._profileType.addEventListener(WebInspector.TrackingHeapSnapshotProfileType.HeapStatsUpdate, this._onHeapStatsUpdate, this);
1542        this._profileType.addEventListener(WebInspector.TrackingHeapSnapshotProfileType.TrackingStopped, this._onStopTracking, this);
1543    }
1544    var timestamps = this._profileSamples.timestamps;
1545    var totalTime = this._profileSamples.totalTime;
1546    this._windowLeft = 0.0;
1547    this._windowRight = totalTime && timestamps.length ? (timestamps[timestamps.length - 1] - timestamps[0]) / totalTime : 1.0;
1548    this._overviewGrid.setWindow(this._windowLeft, this._windowRight);
1549    this._yScale = new WebInspector.HeapTrackingOverviewGrid.SmoothScale();
1550    this._xScale = new WebInspector.HeapTrackingOverviewGrid.SmoothScale();
1551}
1552
1553WebInspector.HeapTrackingOverviewGrid.IdsRangeChanged = "IdsRangeChanged";
1554
1555WebInspector.HeapTrackingOverviewGrid.prototype = {
1556    _onStopTracking: function(event)
1557    {
1558        this._profileType.removeEventListener(WebInspector.TrackingHeapSnapshotProfileType.HeapStatsUpdate, this._onHeapStatsUpdate, this);
1559        this._profileType.removeEventListener(WebInspector.TrackingHeapSnapshotProfileType.TrackingStopped, this._onStopTracking, this);
1560    },
1561
1562    _onHeapStatsUpdate: function(event)
1563    {
1564        this._profileSamples = event.data;
1565        this._scheduleUpdate();
1566    },
1567
1568     /**
1569      * @param {number} width
1570      * @param {number} height
1571      */
1572    _drawOverviewCanvas: function(width, height)
1573    {
1574        if (!this._profileSamples)
1575            return;
1576        var profileSamples = this._profileSamples;
1577        var sizes = profileSamples.sizes;
1578        var topSizes = profileSamples.max;
1579        var timestamps = profileSamples.timestamps;
1580        var startTime = timestamps[0];
1581        var endTime = timestamps[timestamps.length - 1];
1582
1583        var scaleFactor = this._xScale.nextScale(width / profileSamples.totalTime);
1584        var maxSize = 0;
1585        /**
1586          * @param {!Array.<number>} sizes
1587          * @param {function(number, number):void} callback
1588          */
1589        function aggregateAndCall(sizes, callback)
1590        {
1591            var size = 0;
1592            var currentX = 0;
1593            for (var i = 1; i < timestamps.length; ++i) {
1594                var x = Math.floor((timestamps[i] - startTime) * scaleFactor);
1595                if (x !== currentX) {
1596                    if (size)
1597                        callback(currentX, size);
1598                    size = 0;
1599                    currentX = x;
1600                }
1601                size += sizes[i];
1602            }
1603            callback(currentX, size);
1604        }
1605
1606        /**
1607          * @param {number} x
1608          * @param {number} size
1609          */
1610        function maxSizeCallback(x, size)
1611        {
1612            maxSize = Math.max(maxSize, size);
1613        }
1614
1615        aggregateAndCall(sizes, maxSizeCallback);
1616
1617        var yScaleFactor = this._yScale.nextScale(maxSize ? height / (maxSize * 1.1) : 0.0);
1618
1619        this._overviewCanvas.width = width * window.devicePixelRatio;
1620        this._overviewCanvas.height = height * window.devicePixelRatio;
1621        this._overviewCanvas.style.width = width + "px";
1622        this._overviewCanvas.style.height = height + "px";
1623
1624        var context = this._overviewCanvas.getContext("2d");
1625        context.scale(window.devicePixelRatio, window.devicePixelRatio);
1626
1627        context.beginPath();
1628        context.lineWidth = 2;
1629        context.strokeStyle = "rgba(192, 192, 192, 0.6)";
1630        var currentX = (endTime - startTime) * scaleFactor;
1631        context.moveTo(currentX, height - 1);
1632        context.lineTo(currentX, 0);
1633        context.stroke();
1634        context.closePath();
1635
1636        var gridY;
1637        var gridValue;
1638        var gridLabelHeight = 14;
1639        if (yScaleFactor) {
1640            const maxGridValue = (height - gridLabelHeight) / yScaleFactor;
1641            // The round value calculation is a bit tricky, because
1642            // it has a form k*10^n*1024^m, where k=[1,5], n=[0..3], m is an integer,
1643            // e.g. a round value 10KB is 10240 bytes.
1644            gridValue = Math.pow(1024, Math.floor(Math.log(maxGridValue) / Math.log(1024)));
1645            gridValue *= Math.pow(10, Math.floor(Math.log(maxGridValue / gridValue) / Math.LN10));
1646            if (gridValue * 5 <= maxGridValue)
1647                gridValue *= 5;
1648            gridY = Math.round(height - gridValue * yScaleFactor - 0.5) + 0.5;
1649            context.beginPath();
1650            context.lineWidth = 1;
1651            context.strokeStyle = "rgba(0, 0, 0, 0.2)";
1652            context.moveTo(0, gridY);
1653            context.lineTo(width, gridY);
1654            context.stroke();
1655            context.closePath();
1656        }
1657
1658        /**
1659          * @param {number} x
1660          * @param {number} size
1661          */
1662        function drawBarCallback(x, size)
1663        {
1664            context.moveTo(x, height - 1);
1665            context.lineTo(x, Math.round(height - size * yScaleFactor - 1));
1666        }
1667
1668        context.beginPath();
1669        context.lineWidth = 2;
1670        context.strokeStyle = "rgba(192, 192, 192, 0.6)";
1671        aggregateAndCall(topSizes, drawBarCallback);
1672        context.stroke();
1673        context.closePath();
1674
1675        context.beginPath();
1676        context.lineWidth = 2;
1677        context.strokeStyle = "rgba(0, 0, 192, 0.8)";
1678        aggregateAndCall(sizes, drawBarCallback);
1679        context.stroke();
1680        context.closePath();
1681
1682        if (gridValue) {
1683            var label = Number.bytesToString(gridValue);
1684            var labelPadding = 4;
1685            var labelX = 0;
1686            var labelY = gridY - 0.5;
1687            var labelWidth = 2 * labelPadding + context.measureText(label).width;
1688            context.beginPath();
1689            context.textBaseline = "bottom";
1690            context.font = "10px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family");
1691            context.fillStyle = "rgba(255, 255, 255, 0.75)";
1692            context.fillRect(labelX, labelY - gridLabelHeight, labelWidth, gridLabelHeight);
1693            context.fillStyle = "rgb(64, 64, 64)";
1694            context.fillText(label, labelX + labelPadding, labelY);
1695            context.fill();
1696            context.closePath();
1697        }
1698    },
1699
1700    onResize: function()
1701    {
1702        this._updateOverviewCanvas = true;
1703        this._scheduleUpdate();
1704    },
1705
1706    _onWindowChanged: function()
1707    {
1708        if (!this._updateGridTimerId)
1709            this._updateGridTimerId = setTimeout(this._updateGrid.bind(this), 10);
1710    },
1711
1712    _scheduleUpdate: function()
1713    {
1714        if (this._updateTimerId)
1715            return;
1716        this._updateTimerId = setTimeout(this.update.bind(this), 10);
1717    },
1718
1719    _updateBoundaries: function()
1720    {
1721        this._windowLeft = this._overviewGrid.windowLeft();
1722        this._windowRight = this._overviewGrid.windowRight();
1723        this._windowWidth = this._windowRight - this._windowLeft;
1724    },
1725
1726    update: function()
1727    {
1728        this._updateTimerId = null;
1729        if (!this.isShowing())
1730            return;
1731        this._updateBoundaries();
1732        this._overviewCalculator._updateBoundaries(this);
1733        this._overviewGrid.updateDividers(this._overviewCalculator);
1734        this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20);
1735    },
1736
1737    _updateGrid: function()
1738    {
1739        this._updateGridTimerId = 0;
1740        this._updateBoundaries();
1741        var ids = this._profileSamples.ids;
1742        var timestamps = this._profileSamples.timestamps;
1743        var sizes = this._profileSamples.sizes;
1744        var startTime = timestamps[0];
1745        var totalTime = this._profileSamples.totalTime;
1746        var timeLeft = startTime + totalTime * this._windowLeft;
1747        var timeRight = startTime + totalTime * this._windowRight;
1748        var minId = 0;
1749        var maxId = ids[ids.length - 1] + 1;
1750        var size = 0;
1751        for (var i = 0; i < timestamps.length; ++i) {
1752            if (!timestamps[i])
1753                continue;
1754            if (timestamps[i] > timeRight)
1755                break;
1756            maxId = ids[i];
1757            if (timestamps[i] < timeLeft) {
1758                minId = ids[i];
1759                continue;
1760            }
1761            size += sizes[i];
1762        }
1763
1764        this.dispatchEventToListeners(WebInspector.HeapTrackingOverviewGrid.IdsRangeChanged, {minId: minId, maxId: maxId, size: size});
1765    },
1766
1767    __proto__: WebInspector.View.prototype
1768}
1769
1770
1771/**
1772 * @constructor
1773 */
1774WebInspector.HeapTrackingOverviewGrid.SmoothScale = function()
1775{
1776    this._lastUpdate = 0;
1777    this._currentScale = 0.0;
1778}
1779
1780WebInspector.HeapTrackingOverviewGrid.SmoothScale.prototype = {
1781    /**
1782     * @param {number} target
1783     * @return {number}
1784     */
1785    nextScale: function(target) {
1786        target = target || this._currentScale;
1787        if (this._currentScale) {
1788            var now = Date.now();
1789            var timeDeltaMs = now - this._lastUpdate;
1790            this._lastUpdate = now;
1791            var maxChangePerSec = 20;
1792            var maxChangePerDelta = Math.pow(maxChangePerSec, timeDeltaMs / 1000);
1793            var scaleChange = target / this._currentScale;
1794            this._currentScale *= Number.constrain(scaleChange, 1 / maxChangePerDelta, maxChangePerDelta);
1795        } else
1796            this._currentScale = target;
1797        return this._currentScale;
1798    }
1799}
1800
1801
1802/**
1803 * @constructor
1804 * @implements {WebInspector.TimelineGrid.Calculator}
1805 */
1806WebInspector.HeapTrackingOverviewGrid.OverviewCalculator = function()
1807{
1808}
1809
1810WebInspector.HeapTrackingOverviewGrid.OverviewCalculator.prototype = {
1811    /**
1812     * @param {!WebInspector.HeapTrackingOverviewGrid} chart
1813     */
1814    _updateBoundaries: function(chart)
1815    {
1816        this._minimumBoundaries = 0;
1817        this._maximumBoundaries = chart._profileSamples.totalTime;
1818        this._xScaleFactor = chart._overviewContainer.clientWidth / this._maximumBoundaries;
1819    },
1820
1821    /**
1822     * @param {number} time
1823     */
1824    computePosition: function(time)
1825    {
1826        return (time - this._minimumBoundaries) * this._xScaleFactor;
1827    },
1828
1829    /**
1830     * @param {number} value
1831     * @param {boolean=} hires
1832     * @return {string}
1833     */
1834    formatTime: function(value, hires)
1835    {
1836        return Number.secondsToString((value + this._minimumBoundaries) / 1000, hires);
1837    },
1838
1839    maximumBoundary: function()
1840    {
1841        return this._maximumBoundaries;
1842    },
1843
1844    minimumBoundary: function()
1845    {
1846        return this._minimumBoundaries;
1847    },
1848
1849    zeroTime: function()
1850    {
1851        return this._minimumBoundaries;
1852    },
1853
1854    boundarySpan: function()
1855    {
1856        return this._maximumBoundaries - this._minimumBoundaries;
1857    }
1858}
1859