0a4d37cf3ea35ead4157ccc0479ef2972f774531
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / TimelineView.js
1 /*
2  * Copyright (C) 2013 Google Inc. All rights reserved.
3  * Copyright (C) 2012 Intel Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 /**
33  * @constructor
34  * @implements {WebInspector.Searchable}
35  * @extends {WebInspector.View}
36  * @param {!WebInspector.TimelinePanel} panel
37  * @param {!WebInspector.TimelineModel} model
38  * @param {!WebInspector.Setting} glueRecordsSetting
39  * @param {string} mode
40  */
41 WebInspector.TimelineView = function(panel, model, glueRecordsSetting, mode)
42 {
43     WebInspector.View.call(this);
44     this.element.classList.add("timeline-view");
45     this.element.classList.add("hbox");
46
47     this._panel = panel;
48     this._model = model;
49     this._currentMode = mode;
50     this._calculator = new WebInspector.TimelineCalculator(this._model);
51     this._model.addEventListener(WebInspector.TimelineModel.Events.RecordAdded, this._onTimelineEventRecorded, this);
52     this._model.addEventListener(WebInspector.TimelineModel.Events.RecordsCleared, this._onRecordsCleared, this);
53
54     // Create presentation model.
55     this._presentationModel = new WebInspector.TimelinePresentationModel();
56     this._durationFilter = new WebInspector.TimelineIsLongFilter();
57     this._windowFilter = new WebInspector.TimelineWindowFilter();
58
59     this._presentationModel.addFilter(this._windowFilter);
60     this._presentationModel.addFilter(new WebInspector.TimelineCategoryFilter());
61     this._presentationModel.addFilter(this._durationFilter);
62
63     this._frameMode = mode === WebInspector.TimelinePanel.Mode.Frames;
64     this._boundariesAreValid = true;
65     this._scrollTop = 0;
66
67     // Create layout componets.
68
69     // |-------------------------------|
70     // |    |           |              |
71     // |    |  Records  |              |
72     // |    |           |    Details   |
73     // |----------------|              |
74     // |    |  Memory   |              |
75     //  -------------------------------
76
77     // Create top level properties splitter.
78     this._detailsSplitView = new WebInspector.SplitView(false, "timeline-details");
79     this._detailsSplitView.element.classList.add("timeline-details-split");
80     this._detailsSplitView.sidebarElement().classList.add("timeline-details");
81     this._detailsSplitView.setMainElementConstraints(undefined, 40);
82     this._detailsView = new WebInspector.TimelineDetailsView();
83     this._detailsSplitView.setSidebarView(this._detailsView);
84     this._detailsSplitView.installResizer(this._detailsView.titleElement());
85
86     WebInspector.dockController.addEventListener(WebInspector.DockController.Events.DockSideChanged, this._dockSideChanged.bind(this));
87     WebInspector.settings.splitVerticallyWhenDockedToRight.addChangeListener(this._dockSideChanged.bind(this));
88     this._dockSideChanged();
89
90     this._searchableView = new WebInspector.SearchableView(this);
91     this._searchableView.element.classList.add("searchable-view");
92     this._detailsSplitView.setMainView(this._searchableView);
93
94     this._views = [];
95     this._recordsView = this._createRecordsView();
96     this._views.push(this._recordsView);
97
98     this._stackView = new WebInspector.StackView(false);
99     this._stackView.show(this._searchableView.element);
100     this._stackView.element.classList.add("timeline-view-stack");
101     this._recordsViewMainElement = this._stackView.appendView(this._recordsView, "timeline-records").mainElement();
102     this._recordsViewMainElement.classList.add("timeline-records-view");
103     this._recordsViewMainElement.appendChild(this._timelineGrid.gridHeaderElement);
104
105     if (this._currentMode === WebInspector.TimelinePanel.Mode.Memory) {
106         // Create memory statistics as a bottom memory splitter child.
107         this._memoryStatistics = new WebInspector.CountersGraph(this, this._model);
108         this._views.push(this._memoryStatistics);
109         this._memoryStatistics.addEventListener(WebInspector.SidebarView.EventTypes.Resized, this._sidebarResized, this);
110         this._stackView.appendView(this._memoryStatistics, "timeline-memory");
111     }
112
113     this._popoverHelper = new WebInspector.PopoverHelper(this.element, this._getPopoverAnchor.bind(this), this._showPopover.bind(this));
114
115     this.element.addEventListener("mousemove", this._mouseMove.bind(this), false);
116     this.element.addEventListener("mouseout", this._mouseOut.bind(this), false);
117     this.element.addEventListener("keydown", this._keyDown.bind(this), false);
118
119     this._expandOffset = 15;
120
121     this._windowStartTime = 0;
122     this._windowEndTime = Infinity;
123
124     this._allRecordsCount = 0;
125
126     this._presentationModel.setGlueRecords(glueRecordsSetting.get());
127     this._glueRecordsSetting = glueRecordsSetting;
128     this._glueRecordsSetting.addChangeListener(this._onGlueRecordsSettingChanged, this);
129
130     switch (mode) {
131     case WebInspector.TimelinePanel.Mode.Events:
132         this._overviewControl = new WebInspector.TimelineEventOverview(this._model);
133         break;
134     case WebInspector.TimelinePanel.Mode.Frames:
135         this._overviewControl = new WebInspector.TimelineFrameOverview(this._model);
136         this._presentationModel.setGlueRecords(false);
137         this._frameController = new WebInspector.TimelineFrameController(this._model, this._overviewControl, this._presentationModel);
138         break;
139     case WebInspector.TimelinePanel.Mode.Memory:
140         this._overviewControl = new WebInspector.TimelineMemoryOverview(this._model);
141         break;
142     }
143
144     this._detailsSplitView.show(this.element);
145 }
146
147 WebInspector.TimelineView.commonUIFilters = function()
148 {
149     var filters = WebInspector.TimelineView._commonUIFilters;
150     if (filters)
151         return filters;
152
153     filters = {};
154     filters._textFilterUI = new WebInspector.TextFilterUI();
155
156     var durationOptions = [];
157     for (var presetIndex = 0; presetIndex < WebInspector.TimelinePanel.durationFilterPresetsMs.length; ++presetIndex) {
158         var durationMs = WebInspector.TimelinePanel.durationFilterPresetsMs[presetIndex];
159         var durationOption = {};
160         if (!durationMs) {
161             durationOption.label = WebInspector.UIString("All");
162             durationOption.title = WebInspector.UIString("Show all records");
163         } else {
164             durationOption.label = WebInspector.UIString("\u2265 %dms", durationMs);
165             durationOption.title = WebInspector.UIString("Hide records shorter than %dms", durationMs);
166         }
167         durationOption.value = durationMs;
168         durationOptions.push(durationOption);
169     }
170     filters._durationFilterUI = new WebInspector.ComboBoxFilterUI(durationOptions);
171
172     filters._categoryFiltersUI = {};
173     var categoryTypes = [];
174     var categories = WebInspector.TimelinePresentationModel.categories();
175     for (var categoryName in categories) {
176         var category = categories[categoryName];
177         if (category.overviewStripGroupIndex < 0)
178             continue;
179         var filter = new WebInspector.CheckboxFilterUI(category.name, category.title);
180         filters._categoryFiltersUI[category.name] = filter;
181     }
182     WebInspector.TimelineView._commonUIFilters = filters;
183     return filters;
184 }
185
186 WebInspector.TimelineView.prototype = {
187     /**
188      * @return {!WebInspector.SidebarView}
189      */
190     _createRecordsView: function()
191     {
192         // Create records sidebar as a top memory splitter child.
193         var recordsView = new WebInspector.SidebarView(WebInspector.SidebarView.SidebarPosition.Start, "timeline-split");
194         recordsView.addEventListener(WebInspector.SidebarView.EventTypes.Resized, this._sidebarResized, this);
195         recordsView.setSecondIsSidebar(false);
196         this._containerElement = recordsView.element;
197         this._containerElement.tabIndex = 0;
198         this._containerElement.id = "timeline-container";
199         this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
200
201         // Create records list in the records sidebar.
202         recordsView.sidebarElement().createChild("div", "timeline-records-title").textContent = WebInspector.UIString("RECORDS");
203         this._sidebarListElement = recordsView.sidebarElement().createChild("div", "timeline-records-list");
204
205         // Create grid in the records main area.
206         this._gridContainer = new WebInspector.ViewWithResizeCallback(this._onViewportResize.bind(this));
207         this._gridContainer.element.id = "resources-container-content";
208         recordsView.setMainView(this._gridContainer);
209         this._timelineGrid = new WebInspector.TimelineGrid();
210         this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
211         this._itemsGraphsElement.id = "timeline-graphs";
212         this._gridContainer.element.appendChild(this._timelineGrid.element);
213         this._timelineGrid.gridHeaderElement.id = "timeline-grid-header";
214         this._timelineGrid.gridHeaderElement.classList.add("fill");
215
216         // Create gap elements
217         this._topGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap");
218         this._graphRowsElement = this._itemsGraphsElement.createChild("div");
219         this._bottomGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap");
220         this._expandElements = this._itemsGraphsElement.createChild("div");
221         this._expandElements.id = "orphan-expand-elements";
222
223         // Create gpu tasks containers.
224         /** @type {!Array.<!TimelineAgent.TimelineEvent>} */
225         this._mainThreadTasks =  ([]);
226         /** @type {!Array.<!TimelineAgent.TimelineEvent>} */
227         this._gpuTasks = ([]);
228         var utilizationStripsElement = this._timelineGrid.gridHeaderElement.createChild("div", "timeline-utilization-strips vbox");
229         this._cpuBarsElement = utilizationStripsElement.createChild("div", "timeline-utilization-strip");
230         if (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
231             this._gpuBarsElement = utilizationStripsElement.createChild("div", "timeline-utilization-strip gpu");
232
233         return recordsView;
234     },
235
236     /**
237      * @return {!WebInspector.SearchableView}
238      */
239     searchableView: function()
240     {
241         return this._searchableView;
242     },
243
244     /**
245      * @return {boolean}
246      */
247     supportsGlueParentMode: function()
248     {
249         return !this._frameMode;
250     },
251
252     _onGlueRecordsSettingChanged: function()
253     {
254         this._presentationModel.setGlueRecords(this._glueRecordsSetting.get());
255         this._repopulateRecords();
256     },
257
258     /**
259      * @return {number}
260      */
261     windowStartTime: function()
262     {
263         return this._windowStartTime || this._model.minimumRecordTime();
264     },
265
266     /**
267      * @return {number}
268      */
269     windowEndTime: function()
270     {
271         return this._windowEndTime < Infinity ? this._windowEndTime : this._model.maximumRecordTime();
272     },
273
274     /**
275      * @return {!WebInspector.TimelineOverviewBase}
276      */
277     overviewControl: function()
278     {
279         return this._overviewControl;
280     },
281
282     get calculator()
283     {
284         return this._calculator;
285     },
286
287     /**
288      * @param {!WebInspector.FilterBar} filterBar
289      * @return {boolean}
290      */
291     createUIFilters: function(filterBar)
292     {
293         var filters = this._filters;
294         if (!filters) {
295             this._filters = WebInspector.TimelineView.commonUIFilters();
296             filters = this._filters;
297
298             filters._textFilterUI.addEventListener(WebInspector.FilterUI.Events.FilterChanged, this._textFilterChanged, this);
299             filters._durationFilterUI.addEventListener(WebInspector.FilterUI.Events.FilterChanged, this._durationFilterChanged, this);
300             for (var categoryName in filters._categoryFiltersUI)
301                 filters._categoryFiltersUI[categoryName].addEventListener(WebInspector.FilterUI.Events.FilterChanged, this._categoriesFilterChanged.bind(this, categoryName), this);
302         }
303
304         filterBar.addFilter(filters._textFilterUI);
305         filterBar.addFilter(filters._durationFilterUI);
306         for (var categoryName in filters._categoryFiltersUI)
307             filterBar.addFilter(filters._categoryFiltersUI[categoryName]);
308
309         return true;
310     },
311
312     _textFilterChanged: function(event)
313     {
314         var searchQuery = this._filters._textFilterUI.value();
315         this._presentationModel.setSearchFilter(null);
316         delete this._searchFilter;
317
318         function cleanRecord(record)
319         {
320             delete record.clicked;
321         }
322         WebInspector.TimelinePresentationModel.forAllRecords(this._presentationModel.rootRecord().children, cleanRecord);
323
324         this.searchCanceled();
325         if (searchQuery) {
326             this._searchFilter = new WebInspector.TimelineSearchFilter(createPlainTextSearchRegex(searchQuery, "i"));
327             this._presentationModel.setSearchFilter(this._searchFilter);
328         }
329         this._invalidateAndScheduleRefresh(true, true);
330     },
331
332     _durationFilterChanged: function()
333     {
334         var duration = this._filters._durationFilterUI.value();
335         var minimumRecordDuration = +duration / 1000.0;
336         this._durationFilter.setMinimumRecordDuration(minimumRecordDuration);
337         this._invalidateAndScheduleRefresh(true, true);
338     },
339
340     _categoriesFilterChanged: function(name, event)
341     {
342         var categories = WebInspector.TimelinePresentationModel.categories();
343         categories[name].hidden = !this._filters._categoryFiltersUI[name].checked();
344         this._invalidateAndScheduleRefresh(true, true);
345     },
346
347     _dockSideChanged: function()
348     {
349         var dockSide = WebInspector.dockController.dockSide();
350         var vertically = false;
351         if (dockSide === WebInspector.DockController.State.DockedToBottom)
352             vertically = true;
353         else
354             vertically = !WebInspector.settings.splitVerticallyWhenDockedToRight.get();
355         this._detailsSplitView.setVertical(vertically);
356         this._detailsView.setVertical(vertically);
357     },
358
359     _rootRecord: function()
360     {
361         return this._presentationModel.rootRecord();
362     },
363
364     _updateRecordsCounter: function(recordsInWindowCount)
365     {
366         this._panel.recordsCounter.setText(WebInspector.UIString("%d of %d records shown", recordsInWindowCount, this._allRecordsCount));
367     },
368
369     _updateFrameStatistics: function(frames)
370     {
371         this._lastFrameStatistics = frames.length ? new WebInspector.FrameStatistics(frames) : null;
372     },
373
374     _updateEventDividers: function()
375     {
376         this._timelineGrid.removeEventDividers();
377         var clientWidth = this._graphRowsElementWidth;
378         var dividers = [];
379         var eventDividerRecords = this._presentationModel.eventDividerRecords();
380
381         for (var i = 0; i < eventDividerRecords.length; ++i) {
382             var record = eventDividerRecords[i];
383             var positions = this._calculator.computeBarGraphWindowPosition(record);
384             var dividerPosition = Math.round(positions.left);
385             if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition])
386                 continue;
387             var divider = WebInspector.TimelinePresentationModel.createEventDivider(record.type, record.title);
388             divider.style.left = dividerPosition + "px";
389             dividers[dividerPosition] = divider;
390         }
391         this._timelineGrid.addEventDividers(dividers);
392     },
393
394     _updateFrameBars: function(frames)
395     {
396         var clientWidth = this._graphRowsElementWidth;
397         if (this._frameContainer)
398             this._frameContainer.removeChildren();
399         else {
400             const frameContainerBorderWidth = 1;
401             this._frameContainer = document.createElement("div");
402             this._frameContainer.classList.add("fill");
403             this._frameContainer.classList.add("timeline-frame-container");
404             this._frameContainer.style.height = WebInspector.TimelinePanel.rowHeight + frameContainerBorderWidth + "px";
405             this._frameContainer.addEventListener("dblclick", this._onFrameDoubleClicked.bind(this), false);
406         }
407
408         var dividers = [];
409
410         for (var i = 0; i < frames.length; ++i) {
411             var frame = frames[i];
412             var frameStart = this._calculator.computePosition(frame.startTime);
413             var frameEnd = this._calculator.computePosition(frame.endTime);
414
415             var frameStrip = document.createElement("div");
416             frameStrip.className = "timeline-frame-strip";
417             var actualStart = Math.max(frameStart, 0);
418             var width = frameEnd - actualStart;
419             frameStrip.style.left = actualStart + "px";
420             frameStrip.style.width = width + "px";
421             frameStrip._frame = frame;
422
423             const minWidthForFrameInfo = 60;
424             if (width > minWidthForFrameInfo)
425                 frameStrip.textContent = Number.secondsToString(frame.endTime - frame.startTime, true);
426
427             this._frameContainer.appendChild(frameStrip);
428
429             if (actualStart > 0) {
430                 var frameMarker = WebInspector.TimelinePresentationModel.createEventDivider(WebInspector.TimelineModel.RecordType.BeginFrame);
431                 frameMarker.style.left = frameStart + "px";
432                 dividers.push(frameMarker);
433             }
434         }
435         this._timelineGrid.addEventDividers(dividers);
436         this._timelineGrid.gridHeaderElement.appendChild(this._frameContainer);
437     },
438
439     _onFrameDoubleClicked: function(event)
440     {
441         var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip");
442         if (!frameBar)
443             return;
444         this._setWindowTimes(frameBar._frame.startTime, frameBar._frame.endTime);
445     },
446
447     _updateWindowBoundaries: function()
448     {
449         var windowBoundaries = this.overviewControl().windowBoundaries(this._windowStartTime, this._windowEndTime);
450         this._panel.setWindow(windowBoundaries.left, windowBoundaries.right);
451     },
452
453     /**
454      * @return {!{windowStartTime: number, windowEndTime: number}}
455      */
456     windowTimes: function()
457     {
458         return {windowStartTime: this._windowStartTime, windowEndTime: this._windowEndTime};
459     },
460
461     /**
462      * @param {?Object} windowTimes
463      */
464     setWindowTimes: function(windowTimes)
465     {
466         if (!windowTimes)
467             return;
468         this._setWindowTimes(windowTimes.windowStartTime, windowTimes.windowEndTime);
469     },
470
471     /**
472      * @param {number} startTime
473      * @param {number} endTime
474      */
475     _setWindowTimes: function(startTime, endTime)
476     {
477         this._windowStartTime = startTime;
478         this._windowEndTime = endTime;
479         this._windowFilter.setWindowTimes(startTime, endTime);
480         var windowBoundaries = this.overviewControl().windowBoundaries(startTime, endTime);
481         this._panel.setWindow(windowBoundaries.left, windowBoundaries.right);
482     },
483
484     _repopulateRecords: function()
485     {
486         this._resetView();
487         this._automaticallySizeWindow = false;
488         var records = this._model.records;
489         for (var i = 0; i < records.length; ++i)
490             this._innerAddRecordToTimeline(records[i]);
491         this._invalidateAndScheduleRefresh(false, false);
492     },
493
494     _onTimelineEventRecorded: function(event)
495     {
496         if (this._innerAddRecordToTimeline(/** @type {!TimelineAgent.TimelineEvent} */(event.data)))
497             this._invalidateAndScheduleRefresh(false, false);
498     },
499
500     /**
501      * @param {!TimelineAgent.TimelineEvent} record
502      * @return {boolean}
503      */
504     _innerAddRecordToTimeline: function(record)
505     {
506         if (record.type === WebInspector.TimelineModel.RecordType.Program)
507             this._mainThreadTasks.push(record);
508
509         if (record.type === WebInspector.TimelineModel.RecordType.GPUTask) {
510             this._gpuTasks.push(record);
511             return WebInspector.TimelineModel.startTimeInSeconds(record) < this._windowEndTime;
512         }
513
514         var records = this._presentationModel.addRecord(record);
515         this._allRecordsCount += records.length;
516         var hasVisibleRecords = false;
517         var presentationModel = this._presentationModel;
518         function checkVisible(record)
519         {
520             hasVisibleRecords |= presentationModel.isVisible(record);
521         }
522         WebInspector.TimelinePresentationModel.forAllRecords(records, checkVisible);
523
524         function isAdoptedRecord(record)
525         {
526             return record.parent !== presentationModel.rootRecord;
527         }
528         // Tell caller update is necessary either if we added a visible record or if we re-parented a record.
529         return hasVisibleRecords || records.some(isAdoptedRecord);
530     },
531
532     /**
533      * @param {!WebInspector.Event} event
534      */
535     _sidebarResized: function(event)
536     {
537         var width = /** @type {number} */(event.data);
538         this.setSidebarWidth(width);
539         this._panel.setSidebarWidth(width);
540     },
541
542     /**
543      * @param {number} width
544      */
545     setSidebarWidth: function(width)
546     {
547         this._timelineGrid.gridHeaderElement.style.left = width + "px";
548         for (var i = 0; i < this._views.length; ++i)
549             this._views[i].setSidebarWidth(width);
550     },
551
552     _onViewportResize: function()
553     {
554         this._resize(this._recordsView.sidebarWidth());
555     },
556
557     /**
558      * @param {number} sidebarWidth
559      */
560     _resize: function(sidebarWidth)
561     {
562         this._closeRecordDetails();
563         this._graphRowsElementWidth = this._graphRowsElement.offsetWidth;
564         this._containerElementHeight = this._containerElement.clientHeight;
565         this._timelineGrid.gridHeaderElement.style.width = this._itemsGraphsElement.offsetWidth + "px";
566         this._scheduleRefresh(false, true);
567     },
568
569     _resetView: function()
570     {
571         this._presentationModel.reset();
572         this._boundariesAreValid = false;
573         this._adjustScrollPosition(0);
574         this._closeRecordDetails();
575         this._allRecordsCount = 0;
576         this._automaticallySizeWindow = true;
577         this._mainThreadTasks = [];
578         this._gpuTasks = [];
579     },
580
581     _onRecordsCleared: function()
582     {
583         this._windowStartTime = 0;
584         this._windowEndTime = Infinity;
585
586         this._resetView();
587         this.overviewControl().reset();
588         this._windowFilter.reset();
589         this._invalidateAndScheduleRefresh(true, true);
590     },
591
592     /**
593      * @return {!Array.<!Element>}
594      */
595     elementsToRestoreScrollPositionsFor: function()
596     {
597         return [this._containerElement];
598     },
599
600     wasShown: function()
601     {
602         WebInspector.View.prototype.wasShown.call(this);
603
604         this._repopulateRecords();
605         this._updateSelectionDetails();
606         this._updateWindowBoundaries();
607
608         if (!WebInspector.TimelinePanel._categoryStylesInitialized) {
609             WebInspector.TimelinePanel._categoryStylesInitialized = true;
610             this._injectCategoryStyles();
611         }
612         this._onViewportResize();
613         this._refresh();
614     },
615
616     willHide: function()
617     {
618         this._closeRecordDetails();
619         WebInspector.View.prototype.willHide.call(this);
620     },
621
622     _onScroll: function(event)
623     {
624         this._closeRecordDetails();
625         this._scrollTop = this._containerElement.scrollTop;
626         var dividersTop = Math.max(0, this._scrollTop);
627         this._timelineGrid.setScrollAndDividerTop(this._scrollTop, dividersTop);
628         this._scheduleRefresh(true, true);
629     },
630
631     /**
632      * @param {boolean} preserveBoundaries
633      * @param {boolean} userGesture
634      */
635     _invalidateAndScheduleRefresh: function(preserveBoundaries, userGesture)
636     {
637         this._presentationModel.invalidateFilteredRecords();
638         delete this._searchResults;
639         this._scheduleRefresh(preserveBoundaries, userGesture);
640     },
641
642     /**
643      * @param {?WebInspector.TimelinePresentationModel.Record} record
644      */
645     _selectRecord: function(record)
646     {
647         if (record === this._lastSelectedRecord)
648             return;
649
650         // Remove selection rendering.
651         if (this._lastSelectedRecord) {
652             var listRow = /** @type {!WebInspector.TimelineRecordListRow} */ (this._lastSelectedRecord.getUserObject("WebInspector.TimelineRecordListRow"));
653             if (listRow)
654                 listRow.renderAsSelected(false);
655             var graphRow = /** @type {!WebInspector.TimelineRecordGraphRow} */ (this._lastSelectedRecord.getUserObject("WebInspector.TimelineRecordGraphRow"));
656             if (graphRow)
657                 graphRow.renderAsSelected(false);
658         }
659
660         if (!record) {
661             this._updateSelectionDetails();
662             return;
663         }
664
665         this._lastSelectedRecord = record;
666         this._revealRecord(record);
667         var listRow = /** @type {!WebInspector.TimelineRecordListRow} */ (record.getUserObject("WebInspector.TimelineRecordListRow"));
668         if (listRow)
669             listRow.renderAsSelected(true);
670         var graphRow = /** @type {!WebInspector.TimelineRecordListRow} */ (record.getUserObject("WebInspector.TimelineRecordGraphRow"));
671         if (graphRow)
672             graphRow.renderAsSelected(true);
673
674         record.generatePopupContent(showCallback.bind(this));
675
676         /**
677          * @param {!DocumentFragment} element
678          * @this {WebInspector.TimelineView}
679          */
680         function showCallback(element)
681         {
682             this._detailsView.setContent(record.title, element);
683         }
684     },
685
686     _updateSelectionDetails: function()
687     {
688         var startTime = this.windowStartTime() * 1000;
689         var endTime = this.windowEndTime() * 1000;
690         // Return early in case 0 selection window.
691         if (startTime < 0)
692             return;
693
694         var aggregatedStats = {};
695
696         /**
697          * @param {number} value
698          * @param {!TimelineAgent.TimelineEvent} task
699          * @return {number}
700          */
701         function compareEndTime(value, task)
702         {
703             return value < task.endTime ? -1 : 1;
704         }
705
706         /**
707          * @param {!TimelineAgent.TimelineEvent} rawRecord
708          */
709         function aggregateTimeForRecordWithinWindow(rawRecord)
710         {
711             if (!rawRecord.endTime || rawRecord.endTime < startTime || rawRecord.startTime > endTime)
712                 return;
713
714             var childrenTime = 0;
715             var children = rawRecord.children || [];
716             for (var i = 0; i < children.length; ++i) {
717                 var child = children[i];
718                 if (!child.endTime || child.endTime < startTime || child.startTime > endTime)
719                     continue;
720                 childrenTime += Math.min(endTime, child.endTime) - Math.max(startTime, child.startTime);
721                 aggregateTimeForRecordWithinWindow(child);
722             }
723             var categoryName = WebInspector.TimelinePresentationModel.categoryForRecord(rawRecord).name;
724             var ownTime = Math.min(endTime, rawRecord.endTime) - Math.max(startTime, rawRecord.startTime) - childrenTime;
725             aggregatedStats[categoryName] = (aggregatedStats[categoryName] || 0) + ownTime / 1000;
726         }
727
728         var taskIndex = insertionIndexForObjectInListSortedByFunction(startTime, this._mainThreadTasks, compareEndTime);
729         for (; taskIndex < this._mainThreadTasks.length; ++taskIndex) {
730             var task = this._mainThreadTasks[taskIndex];
731             if (task.startTime > endTime)
732                 break;
733             aggregateTimeForRecordWithinWindow(task);
734         }
735
736         var aggregatedTotal = 0;
737         for (var categoryName in aggregatedStats)
738             aggregatedTotal += aggregatedStats[categoryName];
739         aggregatedStats["idle"] = Math.max(0, (endTime - startTime) / 1000 - aggregatedTotal);
740
741         var fragment = document.createDocumentFragment();
742         var pie = WebInspector.TimelinePresentationModel.generatePieChart(aggregatedStats);
743         fragment.appendChild(pie.element);
744
745         if (this._frameMode && this._lastFrameStatistics) {
746             var title = WebInspector.UIString("%s \u2013 %s (%d frames)", Number.secondsToString(this._lastFrameStatistics.startOffset, true), Number.secondsToString(this._lastFrameStatistics.endOffset, true), this._lastFrameStatistics.frameCount);
747             fragment.appendChild(WebInspector.TimelinePresentationModel.generatePopupContentForFrameStatistics(this._lastFrameStatistics));
748         } else {
749             var title = WebInspector.UIString("%s \u2013 %s", this._calculator.formatTime(0, true), this._calculator.formatTime(this._calculator.boundarySpan(), true));
750         }
751         this._detailsView.setContent(title, fragment);
752     },
753
754     /**
755      * @param {number} left
756      * @param {number} right
757      */
758     windowChanged: function(left, right)
759     {
760         var windowTimes = this.overviewControl().windowTimes(left, right);
761         this._windowStartTime = windowTimes.startTime;
762         this._windowEndTime = windowTimes.endTime;
763         this._windowFilter.setWindowTimes(windowTimes.startTime, windowTimes.endTime);
764         this._invalidateAndScheduleRefresh(false, true);
765         this._selectRecord(null);
766     },
767
768     /**
769      * @param {boolean} preserveBoundaries
770      * @param {boolean} userGesture
771      */
772     _scheduleRefresh: function(preserveBoundaries, userGesture)
773     {
774         this._closeRecordDetails();
775         this._boundariesAreValid &= preserveBoundaries;
776
777         if (!this.isShowing())
778             return;
779
780         if (preserveBoundaries || userGesture)
781             this._refresh();
782         else {
783             if (!this._refreshTimeout)
784                 this._refreshTimeout = setTimeout(this._refresh.bind(this), 300);
785         }
786     },
787
788     _refresh: function()
789     {
790         if (this._refreshTimeout) {
791             clearTimeout(this._refreshTimeout);
792             delete this._refreshTimeout;
793         }
794
795         this._timelinePaddingLeft = this._expandOffset;
796         this._calculator.setWindow(this.windowStartTime(), this.windowEndTime());
797         this._calculator.setDisplayWindow(this._timelinePaddingLeft, this._graphRowsElementWidth);
798
799         var recordsInWindowCount = this._refreshRecords();
800         this._updateRecordsCounter(recordsInWindowCount);
801         if (!this._boundariesAreValid) {
802             this._updateEventDividers();
803             var frames = this._frameController && this._presentationModel.filteredFrames(this.windowStartTime(), this.windowEndTime());
804             if (frames) {
805                 this._updateFrameStatistics(frames);
806                 const maxFramesForFrameBars = 30;
807                 if  (frames.length && frames.length < maxFramesForFrameBars) {
808                     this._timelineGrid.removeDividers();
809                     this._updateFrameBars(frames);
810                 } else {
811                     if (this._frameContainer)
812                         this._frameContainer.remove();
813                     this._timelineGrid.updateDividers(this._calculator);
814                 }
815             } else
816                 this._timelineGrid.updateDividers(this._calculator);
817             this._refreshAllUtilizationBars();
818         }
819         if (this._currentMode === WebInspector.TimelinePanel.Mode.Memory)
820             this._memoryStatistics.refresh();
821         this._updateWindowBoundaries();
822         this._boundariesAreValid = true;
823     },
824
825     revealRecordAt: function(time)
826     {
827         var recordToReveal;
828         function findRecordToReveal(record)
829         {
830             if (record.containsTime(time)) {
831                 recordToReveal = record;
832                 return true;
833             }
834             // If there is no record containing the time than use the latest one before that time.
835             if (!recordToReveal || record.endTime < time && recordToReveal.endTime < record.endTime)
836                 recordToReveal = record;
837             return false;
838         }
839         WebInspector.TimelinePresentationModel.forAllRecords(this._presentationModel.rootRecord().children, null, findRecordToReveal);
840
841         // The record ends before the window left bound so scroll to the top.
842         if (!recordToReveal) {
843             this._containerElement.scrollTop = 0;
844             return;
845         }
846
847         this._selectRecord(recordToReveal);
848     },
849
850     /**
851      * @param {!WebInspector.TimelinePresentationModel.Record} recordToReveal
852      */
853     _revealRecord: function(recordToReveal)
854     {
855         var needRefresh = false;
856         // Expand all ancestors.
857         for (var parent = recordToReveal.parent; parent !== this._rootRecord(); parent = parent.parent) {
858             if (!parent.collapsed)
859                 continue;
860             this._presentationModel.invalidateFilteredRecords();
861             parent.collapsed = false;
862             needRefresh = true;
863         }
864         var recordsInWindow = this._presentationModel.filteredRecords();
865         var index = recordsInWindow.indexOf(recordToReveal);
866
867         var itemOffset = index * WebInspector.TimelinePanel.rowHeight;
868         var visibleTop = this._scrollTop - WebInspector.TimelinePanel.headerHeight;
869         var visibleBottom = visibleTop + this._containerElementHeight - WebInspector.TimelinePanel.rowHeight;
870         if (itemOffset < visibleTop)
871             this._containerElement.scrollTop = itemOffset;
872         else if (itemOffset > visibleBottom)
873             this._containerElement.scrollTop = itemOffset - this._containerElementHeight + WebInspector.TimelinePanel.headerHeight + WebInspector.TimelinePanel.rowHeight;
874         else if (needRefresh)
875             this._refreshRecords();
876     },
877
878     _refreshRecords: function()
879     {
880         var recordsInWindow = this._presentationModel.filteredRecords();
881
882         // Calculate the visible area.
883         var visibleTop = this._scrollTop;
884         var visibleBottom = visibleTop + this._containerElementHeight;
885
886         var rowHeight = WebInspector.TimelinePanel.rowHeight;
887         var headerHeight = WebInspector.TimelinePanel.headerHeight;
888
889         // Convert visible area to visible indexes. Always include top-level record for a visible nested record.
890         var startIndex = Math.max(0, Math.min(Math.floor((visibleTop - headerHeight) / rowHeight), recordsInWindow.length - 1));
891         var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
892         var lastVisibleLine = Math.max(0, Math.floor((visibleBottom - headerHeight) / rowHeight));
893         if (this._automaticallySizeWindow && recordsInWindow.length > lastVisibleLine) {
894             this._automaticallySizeWindow = false;
895             this._selectRecord(null);
896             // If we're at the top, always use real timeline start as a left window bound so that expansion arrow padding logic works.
897             var windowStartTime = startIndex ? recordsInWindow[startIndex].startTime : this._model.minimumRecordTime();
898             this._setWindowTimes(windowStartTime, recordsInWindow[Math.max(0, lastVisibleLine - 1)].endTime);
899             recordsInWindow = this._presentationModel.filteredRecords();
900             endIndex = Math.min(recordsInWindow.length, lastVisibleLine);
901         } else {
902             this._updateWindowBoundaries();
903         }
904
905         // Resize gaps first.
906         this._topGapElement.style.height = (startIndex * rowHeight) + "px";
907         this._recordsView.sidebarElement().firstElementChild.style.flexBasis = (startIndex * rowHeight + headerHeight) + "px";
908         this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
909         var rowsHeight = headerHeight + recordsInWindow.length * rowHeight;
910         var totalHeight = Math.max(this._containerElementHeight, rowsHeight);
911
912         this._recordsView.firstElement().style.height = totalHeight + "px";
913         this._recordsView.secondElement().style.height = totalHeight + "px";
914         this._recordsView.resizerElement().style.height = totalHeight + "px";
915
916         // Update visible rows.
917         var listRowElement = this._sidebarListElement.firstChild;
918         var width = this._graphRowsElementWidth;
919         this._itemsGraphsElement.removeChild(this._graphRowsElement);
920         var graphRowElement = this._graphRowsElement.firstChild;
921         var scheduleRefreshCallback = this._invalidateAndScheduleRefresh.bind(this, true, true);
922         var selectRecordCallback = this._selectRecord.bind(this);
923         this._itemsGraphsElement.removeChild(this._expandElements);
924         this._expandElements.removeChildren();
925
926         for (var i = 0; i < endIndex; ++i) {
927             var record = recordsInWindow[i];
928
929             if (i < startIndex) {
930                 var lastChildIndex = i + record.visibleChildrenCount;
931                 if (lastChildIndex >= startIndex && lastChildIndex < endIndex) {
932                     var expandElement = new WebInspector.TimelineExpandableElement(this._expandElements);
933                     var positions = this._calculator.computeBarGraphWindowPosition(record);
934                     expandElement._update(record, i, positions.left - this._expandOffset, positions.width);
935                 }
936             } else {
937                 if (!listRowElement) {
938                     listRowElement = new WebInspector.TimelineRecordListRow(selectRecordCallback, scheduleRefreshCallback).element;
939                     this._sidebarListElement.appendChild(listRowElement);
940                 }
941                 if (!graphRowElement) {
942                     graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, selectRecordCallback, scheduleRefreshCallback).element;
943                     this._graphRowsElement.appendChild(graphRowElement);
944                 }
945
946                 listRowElement.row.update(record, visibleTop);
947                 graphRowElement.row.update(record, this._calculator, this._expandOffset, i);
948                 if (this._lastSelectedRecord === record) {
949                     listRowElement.row.renderAsSelected(true);
950                     graphRowElement.row.renderAsSelected(true);
951                 }
952
953                 listRowElement = listRowElement.nextSibling;
954                 graphRowElement = graphRowElement.nextSibling;
955             }
956         }
957
958         // Remove extra rows.
959         while (listRowElement) {
960             var nextElement = listRowElement.nextSibling;
961             listRowElement.row.dispose();
962             listRowElement = nextElement;
963         }
964         while (graphRowElement) {
965             var nextElement = graphRowElement.nextSibling;
966             graphRowElement.row.dispose();
967             graphRowElement = nextElement;
968         }
969
970         this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
971         this._itemsGraphsElement.appendChild(this._expandElements);
972         this._adjustScrollPosition(recordsInWindow.length * rowHeight + headerHeight);
973         this._updateSearchHighlight(false, true);
974
975         return recordsInWindow.length;
976     },
977
978     _refreshAllUtilizationBars: function()
979     {
980         this._refreshUtilizationBars(WebInspector.UIString("CPU"), this._mainThreadTasks, this._cpuBarsElement);
981         if (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
982             this._refreshUtilizationBars(WebInspector.UIString("GPU"), this._gpuTasks, this._gpuBarsElement);
983     },
984
985     /**
986      * @param {string} name
987      * @param {!Array.<!TimelineAgent.TimelineEvent>} tasks
988      * @param {?Element} container
989      */
990     _refreshUtilizationBars: function(name, tasks, container)
991     {
992         if (!container)
993             return;
994
995         const barOffset = 3;
996         const minGap = 3;
997
998         var minWidth = WebInspector.TimelineCalculator._minWidth;
999         var widthAdjustment = minWidth / 2;
1000
1001         var width = this._graphRowsElementWidth;
1002         var boundarySpan = this.windowEndTime() - this.windowStartTime();
1003         var scale = boundarySpan / (width - minWidth - this._timelinePaddingLeft);
1004         var startTime = (this.windowStartTime() - this._timelinePaddingLeft * scale) * 1000;
1005         var endTime = startTime + width * scale * 1000;
1006
1007         /**
1008          * @param {number} value
1009          * @param {!TimelineAgent.TimelineEvent} task
1010          * @return {number}
1011          */
1012         function compareEndTime(value, task)
1013         {
1014             return value < task.endTime ? -1 : 1;
1015         }
1016
1017         var taskIndex = insertionIndexForObjectInListSortedByFunction(startTime, tasks, compareEndTime);
1018
1019         var foreignStyle = "gpu-task-foreign";
1020         var element = container.firstChild;
1021         var lastElement;
1022         var lastLeft;
1023         var lastRight;
1024
1025         for (; taskIndex < tasks.length; ++taskIndex) {
1026             var task = tasks[taskIndex];
1027             if (task.startTime > endTime)
1028                 break;
1029
1030             var left = Math.max(0, this._calculator.computePosition(WebInspector.TimelineModel.startTimeInSeconds(task)) + barOffset - widthAdjustment);
1031             var right = Math.min(width, this._calculator.computePosition(WebInspector.TimelineModel.endTimeInSeconds(task)) + barOffset + widthAdjustment);
1032
1033             if (lastElement) {
1034                 var gap = Math.floor(left) - Math.ceil(lastRight);
1035                 if (gap < minGap) {
1036                     if (!task.data["foreign"])
1037                         lastElement.classList.remove(foreignStyle);
1038                     lastRight = right;
1039                     lastElement._tasksInfo.lastTaskIndex = taskIndex;
1040                     continue;
1041                 }
1042                 lastElement.style.width = (lastRight - lastLeft) + "px";
1043             }
1044
1045             if (!element)
1046                 element = container.createChild("div", "timeline-graph-bar");
1047             element.style.left = left + "px";
1048             element._tasksInfo = {name: name, tasks: tasks, firstTaskIndex: taskIndex, lastTaskIndex: taskIndex};
1049             if (task.data["foreign"])
1050                 element.classList.add(foreignStyle);
1051             lastLeft = left;
1052             lastRight = right;
1053             lastElement = element;
1054             element = element.nextSibling;
1055         }
1056
1057         if (lastElement)
1058             lastElement.style.width = (lastRight - lastLeft) + "px";
1059
1060         while (element) {
1061             var nextElement = element.nextSibling;
1062             element._tasksInfo = null;
1063             container.removeChild(element);
1064             element = nextElement;
1065         }
1066     },
1067
1068     _adjustScrollPosition: function(totalHeight)
1069     {
1070         // Prevent the container from being scrolled off the end.
1071         if ((this._scrollTop + this._containerElementHeight) > totalHeight + 1)
1072             this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
1073     },
1074
1075     _getPopoverAnchor: function(element)
1076     {
1077         var anchor = element.enclosingNodeOrSelfWithClass("timeline-graph-bar");
1078         if (anchor && anchor._tasksInfo)
1079             return anchor;
1080         return element.enclosingNodeOrSelfWithClass("timeline-frame-strip");
1081     },
1082
1083     _mouseOut: function()
1084     {
1085         this._hideQuadHighlight();
1086     },
1087
1088     /**
1089      * @param {?Event} e
1090      */
1091     _mouseMove: function(e)
1092     {
1093         var rowElement = e.target.enclosingNodeOrSelfWithClass("timeline-tree-item");
1094         if (rowElement && rowElement.row && rowElement.row._record.highlightQuad)
1095             this._highlightQuad(rowElement.row._record.highlightQuad);
1096         else
1097             this._hideQuadHighlight();
1098
1099         var taskBarElement = e.target.enclosingNodeOrSelfWithClass("timeline-graph-bar");
1100         if (taskBarElement && taskBarElement._tasksInfo) {
1101             var offset = taskBarElement.offsetLeft;
1102             this._timelineGrid.showCurtains(offset >= 0 ? offset : 0, taskBarElement.offsetWidth);
1103         } else
1104             this._timelineGrid.hideCurtains();
1105     },
1106
1107     /**
1108      * @param {?Event} event
1109      */
1110     _keyDown: function(event)
1111     {
1112         if (!this._lastSelectedRecord || event.shiftKey || event.metaKey || event.ctrlKey)
1113             return;
1114
1115         var record = this._lastSelectedRecord;
1116         var recordsInWindow = this._presentationModel.filteredRecords();
1117         var index = recordsInWindow.indexOf(record);
1118         var recordsInPage = Math.floor(this._containerElementHeight / WebInspector.TimelinePanel.rowHeight);
1119         var rowHeight = WebInspector.TimelinePanel.rowHeight;
1120
1121         if (index === -1)
1122             index = 0;
1123
1124         switch (event.keyIdentifier) {
1125         case "Left":
1126             if (record.parent) {
1127                 if ((!record.expandable || record.collapsed) && record.parent !== this._presentationModel.rootRecord()) {
1128                     this._selectRecord(record.parent);
1129                 } else {
1130                     record.collapsed = true;
1131                     record.clicked = true;
1132                     this._invalidateAndScheduleRefresh(true, true);
1133                 }
1134             }
1135             event.consume(true);
1136             break;
1137         case "Up":
1138             if (--index < 0)
1139                 break;
1140             this._selectRecord(recordsInWindow[index]);
1141             event.consume(true);
1142             break;
1143         case "Right":
1144             if (record.expandable && record.collapsed) {
1145                 record.collapsed = false;
1146                 record.clicked = true;
1147                 this._invalidateAndScheduleRefresh(true, true);
1148             } else {
1149                 if (++index >= recordsInWindow.length)
1150                     break;
1151                 this._selectRecord(recordsInWindow[index]);
1152             }
1153             event.consume(true);
1154             break;
1155         case "Down":
1156             if (++index >= recordsInWindow.length)
1157                 break;
1158             this._selectRecord(recordsInWindow[index]);
1159             event.consume(true);
1160             break;
1161         case "PageUp":
1162             index = Math.max(0, index - recordsInPage);
1163             this._scrollTop = Math.max(0, this._scrollTop - recordsInPage * rowHeight);
1164             this._containerElement.scrollTop = this._scrollTop;
1165             this._selectRecord(recordsInWindow[index]);
1166             event.consume(true);
1167             break;
1168         case "PageDown":
1169             index = Math.min(recordsInWindow.length - 1, index + recordsInPage);
1170             this._scrollTop = Math.min(this._containerElement.scrollHeight - this._containerElementHeight, this._scrollTop + recordsInPage * rowHeight);
1171             this._containerElement.scrollTop = this._scrollTop;
1172             this._selectRecord(recordsInWindow[index]);
1173             event.consume(true);
1174             break;
1175         case "Home":
1176             index = 0;
1177             this._selectRecord(recordsInWindow[index]);
1178             event.consume(true);
1179             break;
1180         case "End":
1181             index = recordsInWindow.length - 1;
1182             this._selectRecord(recordsInWindow[index]);
1183             event.consume(true);
1184             break;
1185         }
1186     },
1187
1188     /**
1189      * @param {!Array.<number>} quad
1190      */
1191     _highlightQuad: function(quad)
1192     {
1193         if (this._highlightedQuad === quad)
1194             return;
1195         this._highlightedQuad = quad;
1196         DOMAgent.highlightQuad(quad, WebInspector.Color.PageHighlight.Content.toProtocolRGBA(), WebInspector.Color.PageHighlight.ContentOutline.toProtocolRGBA());
1197     },
1198
1199     _hideQuadHighlight: function()
1200     {
1201         if (this._highlightedQuad) {
1202             delete this._highlightedQuad;
1203             DOMAgent.hideHighlight();
1204         }
1205     },
1206
1207     /**
1208      * @param {!Element} anchor
1209      * @param {!WebInspector.Popover} popover
1210      */
1211     _showPopover: function(anchor, popover)
1212     {
1213         if (anchor.classList.contains("timeline-frame-strip")) {
1214             var frame = anchor._frame;
1215             popover.show(WebInspector.TimelinePresentationModel.generatePopupContentForFrame(frame), anchor);
1216         } else {
1217             if (anchor.row && anchor.row._record)
1218                 anchor.row._record.generatePopupContent(showCallback);
1219             else if (anchor._tasksInfo)
1220                 popover.show(this._presentationModel.generateMainThreadBarPopupContent(anchor._tasksInfo), anchor, null, null, WebInspector.Popover.Orientation.Bottom);
1221         }
1222
1223         function showCallback(popupContent)
1224         {
1225             popover.show(popupContent, anchor);
1226         }
1227     },
1228
1229     _closeRecordDetails: function()
1230     {
1231         this._popoverHelper.hidePopover();
1232     },
1233
1234     _injectCategoryStyles: function()
1235     {
1236         var style = document.createElement("style");
1237         var categories = WebInspector.TimelinePresentationModel.categories();
1238
1239         style.textContent = Object.values(categories).map(WebInspector.TimelinePresentationModel.createStyleRuleForCategory).join("\n");
1240         document.head.appendChild(style);
1241     },
1242
1243     jumpToNextSearchResult: function()
1244     {
1245         if (!this._searchResults || !this._searchResults.length)
1246             return;
1247         var index = this._selectedSearchResult ? this._searchResults.indexOf(this._selectedSearchResult) : -1;
1248         this._jumpToSearchResult(index + 1);
1249     },
1250
1251     jumpToPreviousSearchResult: function()
1252     {
1253         if (!this._searchResults || !this._searchResults.length)
1254             return;
1255         var index = this._selectedSearchResult ? this._searchResults.indexOf(this._selectedSearchResult) : 0;
1256         this._jumpToSearchResult(index - 1);
1257     },
1258
1259     _jumpToSearchResult: function(index)
1260     {
1261         this._selectSearchResult((index + this._searchResults.length) % this._searchResults.length);
1262         this._highlightSelectedSearchResult(true);
1263     },
1264
1265     _selectSearchResult: function(index)
1266     {
1267         this._selectedSearchResult = this._searchResults[index];
1268         this._searchableView.updateCurrentMatchIndex(index);
1269     },
1270
1271     /**
1272      * @param {boolean} selectRecord
1273      */
1274     _highlightSelectedSearchResult: function(selectRecord)
1275     {
1276         this._clearHighlight();
1277         if (this._searchFilter)
1278             return;
1279
1280         var record = this._selectedSearchResult;
1281         if (!record)
1282             return;
1283
1284         if (selectRecord)
1285             this._selectRecord(record);
1286
1287         for (var element = this._sidebarListElement.firstChild; element; element = element.nextSibling) {
1288             if (element.row._record === record) {
1289                 element.row.highlight(this._searchRegExp, this._highlightDomChanges);
1290                 break;
1291             }
1292         }
1293     },
1294
1295     _clearHighlight: function()
1296     {
1297         if (this._highlightDomChanges)
1298             WebInspector.revertDomChanges(this._highlightDomChanges);
1299         this._highlightDomChanges = [];
1300     },
1301
1302     /**
1303      * @param {boolean} revealRecord
1304      * @param {boolean} shouldJump
1305      */
1306     _updateSearchHighlight: function(revealRecord, shouldJump)
1307     {
1308         if (this._searchFilter || !this._searchRegExp) {
1309             this._clearHighlight();
1310             return;
1311         }
1312
1313         if (!this._searchResults)
1314             this._updateSearchResults(shouldJump);
1315         this._highlightSelectedSearchResult(revealRecord);
1316     },
1317
1318     _updateSearchResults: function(shouldJump)
1319     {
1320         var searchRegExp = this._searchRegExp;
1321         if (!searchRegExp)
1322             return;
1323
1324         var matches = [];
1325         var presentationModel = this._presentationModel;
1326
1327         function processRecord(record)
1328         {
1329             if (presentationModel.isVisible(record) && WebInspector.TimelineRecordListRow.testContentMatching(record, searchRegExp))
1330                 matches.push(record);
1331             return false;
1332         }
1333         WebInspector.TimelinePresentationModel.forAllRecords(presentationModel.rootRecord().children, processRecord);
1334
1335         var matchesCount = matches.length;
1336         if (matchesCount) {
1337             this._searchResults = matches;
1338             this._searchableView.updateSearchMatchesCount(matchesCount);
1339
1340             var selectedIndex = matches.indexOf(this._selectedSearchResult);
1341             if (shouldJump && selectedIndex === -1)
1342                 selectedIndex = 0;
1343             this._selectSearchResult(selectedIndex);
1344         } else {
1345             this._searchableView.updateSearchMatchesCount(0);
1346             delete this._selectedSearchResult;
1347         }
1348     },
1349
1350     searchCanceled: function()
1351     {
1352         this._clearHighlight();
1353         delete this._searchResults;
1354         delete this._selectedSearchResult;
1355         delete this._searchRegExp;
1356     },
1357
1358     /**
1359      * @param {string} query
1360      * @param {boolean} shouldJump
1361      */
1362     performSearch: function(query, shouldJump)
1363     {
1364         this._searchRegExp = createPlainTextSearchRegex(query, "i");
1365         delete this._searchResults;
1366         this._updateSearchHighlight(true, shouldJump);
1367     },
1368
1369     __proto__: WebInspector.View.prototype
1370 }
1371
1372 /**
1373  * @constructor
1374  * @param {!WebInspector.TimelineModel} model
1375  * @implements {WebInspector.TimelineGrid.Calculator}
1376  */
1377 WebInspector.TimelineCalculator = function(model)
1378 {
1379     this._model = model;
1380 }
1381
1382 WebInspector.TimelineCalculator._minWidth = 5;
1383
1384 WebInspector.TimelineCalculator.prototype = {
1385     /**
1386      * @param {number} time
1387      * @return {number}
1388      */
1389     computePosition: function(time)
1390     {
1391         return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this.paddingLeft;
1392     },
1393
1394     /**
1395      * @return {!{start: number, end: number, endWithChildren: number, cpuWidth: number}}
1396      */
1397     computeBarGraphPercentages: function(record)
1398     {
1399         var start = (record.startTime - this._minimumBoundary) / this.boundarySpan() * 100;
1400         var end = (record.startTime + record.selfTime - this._minimumBoundary) / this.boundarySpan() * 100;
1401         var endWithChildren = (record.lastChildEndTime - this._minimumBoundary) / this.boundarySpan() * 100;
1402         var cpuWidth = record.coalesced ? endWithChildren - start : record.cpuTime / this.boundarySpan() * 100;
1403         return {start: start, end: end, endWithChildren: endWithChildren, cpuWidth: cpuWidth};
1404     },
1405
1406     /**
1407      * @return {!{left: number, width: number, widthWithChildren: number, cpuWidth: number}}
1408      */
1409     computeBarGraphWindowPosition: function(record)
1410     {
1411         var percentages = this.computeBarGraphPercentages(record);
1412         var widthAdjustment = 0;
1413
1414         var left = this.computePosition(record.startTime);
1415         var width = (percentages.end - percentages.start) / 100 * this._workingArea;
1416         if (width < WebInspector.TimelineCalculator._minWidth) {
1417             widthAdjustment = WebInspector.TimelineCalculator._minWidth - width;
1418             width = WebInspector.TimelineCalculator._minWidth;
1419         }
1420         var widthWithChildren = (percentages.endWithChildren - percentages.start) / 100 * this._workingArea + widthAdjustment;
1421         var cpuWidth = percentages.cpuWidth / 100 * this._workingArea + widthAdjustment;
1422         if (percentages.endWithChildren > percentages.end)
1423             widthWithChildren += widthAdjustment;
1424         return {left: left, width: width, widthWithChildren: widthWithChildren, cpuWidth: cpuWidth};
1425     },
1426
1427     setWindow: function(minimumBoundary, maximumBoundary)
1428     {
1429         this._minimumBoundary = minimumBoundary;
1430         this._maximumBoundary = maximumBoundary;
1431     },
1432
1433     /**
1434      * @param {number} paddingLeft
1435      * @param {number} clientWidth
1436      */
1437     setDisplayWindow: function(paddingLeft, clientWidth)
1438     {
1439         this._workingArea = clientWidth - WebInspector.TimelineCalculator._minWidth - paddingLeft;
1440         this.paddingLeft = paddingLeft;
1441     },
1442
1443     /**
1444      * @param {number} value
1445      * @param {boolean=} hires
1446      * @return {string}
1447      */
1448     formatTime: function(value, hires)
1449     {
1450         return Number.secondsToString(value + this._minimumBoundary - this._model.minimumRecordTime(), hires);
1451     },
1452
1453     /**
1454      * @return {number}
1455      */
1456     maximumBoundary: function()
1457     {
1458         return this._maximumBoundary;
1459     },
1460
1461     /**
1462      * @return {number}
1463      */
1464     minimumBoundary: function()
1465     {
1466         return this._minimumBoundary;
1467     },
1468
1469     /**
1470      * @return {number}
1471      */
1472     zeroTime: function()
1473     {
1474         return this._model.minimumRecordTime();
1475     },
1476
1477     /**
1478      * @return {number}
1479      */
1480     boundarySpan: function()
1481     {
1482         return this._maximumBoundary - this._minimumBoundary;
1483     }
1484 }
1485
1486 /**
1487  * @constructor
1488  * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
1489  * @param {function()} scheduleRefresh
1490  */
1491 WebInspector.TimelineRecordListRow = function(selectRecord, scheduleRefresh)
1492 {
1493     this.element = document.createElement("div");
1494     this.element.row = this;
1495     this.element.style.cursor = "pointer";
1496     this.element.addEventListener("click", this._onClick.bind(this), false);
1497     this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
1498     this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false);
1499
1500     // Warning is float right block, it goes first.
1501     this._warningElement = this.element.createChild("div", "timeline-tree-item-warning hidden");
1502
1503     this._expandArrowElement = this.element.createChild("div", "timeline-tree-item-expand-arrow");
1504     this._expandArrowElement.addEventListener("click", this._onExpandClick.bind(this), false);
1505     var iconElement = this.element.createChild("span", "timeline-tree-icon");
1506     this._typeElement = this.element.createChild("span", "type");
1507
1508     this._dataElement = this.element.createChild("span", "data dimmed");
1509     this._scheduleRefresh = scheduleRefresh;
1510     this._selectRecord = selectRecord;
1511 }
1512
1513 WebInspector.TimelineRecordListRow.prototype = {
1514     update: function(record, offset)
1515     {
1516         this._record = record;
1517         this._offset = offset;
1518
1519         this.element.className = "timeline-tree-item timeline-category-" + record.category.name;
1520         var paddingLeft = 5;
1521         var step = -3;
1522         for (var currentRecord = record.parent ? record.parent.parent : null; currentRecord; currentRecord = currentRecord.parent)
1523             paddingLeft += 12 / (Math.max(1, step++));
1524         this.element.style.paddingLeft = paddingLeft + "px";
1525         if (record.isBackground)
1526             this.element.classList.add("background");
1527
1528         this._typeElement.textContent = record.title;
1529
1530         if (this._dataElement.firstChild)
1531             this._dataElement.removeChildren();
1532
1533         this._warningElement.enableStyleClass("hidden", !record.hasWarnings() && !record.childHasWarnings());
1534         this._warningElement.enableStyleClass("timeline-tree-item-child-warning", record.childHasWarnings() && !record.hasWarnings());
1535
1536         if (record.detailsNode())
1537             this._dataElement.appendChild(record.detailsNode());
1538         this._expandArrowElement.enableStyleClass("parent", record.children && record.children.length);
1539         this._expandArrowElement.enableStyleClass("expanded", record.visibleChildrenCount);
1540         this._record.setUserObject("WebInspector.TimelineRecordListRow", this);
1541     },
1542
1543     highlight: function(regExp, domChanges)
1544     {
1545         var matchInfo = this.element.textContent.match(regExp);
1546         if (matchInfo)
1547             WebInspector.highlightSearchResult(this.element, matchInfo.index, matchInfo[0].length, domChanges);
1548     },
1549
1550     dispose: function()
1551     {
1552         this.element.remove();
1553     },
1554
1555     /**
1556      * @param {!Event} event
1557      */
1558     _onExpandClick: function(event)
1559     {
1560         this._record.collapsed = !this._record.collapsed;
1561         this._record.clicked = true;
1562         this._scheduleRefresh();
1563         event.consume(true);
1564     },
1565
1566     /**
1567      * @param {?Event} event
1568      */
1569     _onClick: function(event)
1570     {
1571         this._selectRecord(this._record);
1572     },
1573
1574     /**
1575      * @param {boolean} selected
1576      */
1577     renderAsSelected: function(selected)
1578     {
1579         this.element.enableStyleClass("selected", selected);
1580     },
1581
1582     /**
1583      * @param {?Event} event
1584      */
1585     _onMouseOver: function(event)
1586     {
1587         this.element.classList.add("hovered");
1588         var graphRow = /** @type {!WebInspector.TimelineRecordGraphRow} */ (this._record.getUserObject("WebInspector.TimelineRecordGraphRow"));
1589         graphRow.element.classList.add("hovered");
1590     },
1591
1592     /**
1593      * @param {?Event} event
1594      */
1595     _onMouseOut: function(event)
1596     {
1597         this.element.classList.remove("hovered");
1598         var graphRow = /** @type {!WebInspector.TimelineRecordGraphRow} */ (this._record.getUserObject("WebInspector.TimelineRecordGraphRow"));
1599         graphRow.element.classList.remove("hovered");
1600     }
1601 }
1602
1603 /**
1604  * @param {!WebInspector.TimelinePresentationModel.Record} record
1605  * @param {!RegExp} regExp
1606  */
1607 WebInspector.TimelineRecordListRow.testContentMatching = function(record, regExp)
1608 {
1609     var toSearchText = record.title;
1610     if (record.detailsNode())
1611         toSearchText += " " + record.detailsNode().textContent;
1612     return regExp.test(toSearchText);
1613 }
1614
1615 /**
1616  * @constructor
1617  * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
1618  * @param {function()} scheduleRefresh
1619  */
1620 WebInspector.TimelineRecordGraphRow = function(graphContainer, selectRecord, scheduleRefresh)
1621 {
1622     this.element = document.createElement("div");
1623     this.element.row = this;
1624     this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
1625     this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false);
1626     this.element.addEventListener("click", this._onClick.bind(this), false);
1627
1628     this._barAreaElement = document.createElement("div");
1629     this._barAreaElement.className = "timeline-graph-bar-area";
1630     this.element.appendChild(this._barAreaElement);
1631
1632     this._barWithChildrenElement = document.createElement("div");
1633     this._barWithChildrenElement.className = "timeline-graph-bar with-children";
1634     this._barWithChildrenElement.row = this;
1635     this._barAreaElement.appendChild(this._barWithChildrenElement);
1636
1637     this._barCpuElement = document.createElement("div");
1638     this._barCpuElement.className = "timeline-graph-bar cpu"
1639     this._barCpuElement.row = this;
1640     this._barAreaElement.appendChild(this._barCpuElement);
1641
1642     this._barElement = document.createElement("div");
1643     this._barElement.className = "timeline-graph-bar";
1644     this._barElement.row = this;
1645     this._barAreaElement.appendChild(this._barElement);
1646
1647     this._expandElement = new WebInspector.TimelineExpandableElement(graphContainer);
1648
1649     this._selectRecord = selectRecord;
1650     this._scheduleRefresh = scheduleRefresh;
1651 }
1652
1653 WebInspector.TimelineRecordGraphRow.prototype = {
1654     update: function(record, calculator, expandOffset, index)
1655     {
1656         this._record = record;
1657         this.element.className = "timeline-graph-side timeline-category-" + record.category.name;
1658         if (record.isBackground)
1659             this.element.classList.add("background");
1660
1661         var barPosition = calculator.computeBarGraphWindowPosition(record);
1662         this._barWithChildrenElement.style.left = barPosition.left + "px";
1663         this._barWithChildrenElement.style.width = barPosition.widthWithChildren + "px";
1664         this._barElement.style.left = barPosition.left + "px";
1665         this._barElement.style.width = barPosition.width + "px";
1666         this._barCpuElement.style.left = barPosition.left + "px";
1667         this._barCpuElement.style.width = barPosition.cpuWidth + "px";
1668         this._expandElement._update(record, index, barPosition.left - expandOffset, barPosition.width);
1669
1670         this._record.setUserObject("WebInspector.TimelineRecordGraphRow", this);
1671     },
1672
1673     /**
1674      * @param {?Event} event
1675      */
1676     _onClick: function(event)
1677     {
1678         // check if we click arrow and expand if yes.
1679         if (this._expandElement._arrow.containsEventPoint(event))
1680             this._expand();
1681         this._selectRecord(this._record);
1682     },
1683
1684     /**
1685      * @param {boolean} selected
1686      */
1687     renderAsSelected: function(selected)
1688     {
1689         this.element.enableStyleClass("selected", selected);
1690     },
1691
1692     _expand: function()
1693     {
1694         this._record.collapsed = !this._record.collapsed;
1695         this._record.clicked = true;
1696         this._scheduleRefresh();
1697     },
1698
1699     /**
1700      * @param {?Event} event
1701      */
1702     _onMouseOver: function(event)
1703     {
1704         this.element.classList.add("hovered");
1705         var listRow = /** @type {!WebInspector.TimelineRecordListRow} */ (this._record.getUserObject("WebInspector.TimelineRecordListRow"));
1706         listRow.element.classList.add("hovered");
1707     },
1708
1709     /**
1710      * @param {?Event} event
1711      */
1712     _onMouseOut: function(event)
1713     {
1714         this.element.classList.remove("hovered");
1715         var listRow = /** @type {!WebInspector.TimelineRecordListRow} */ (this._record.getUserObject("WebInspector.TimelineRecordListRow"));
1716         listRow.element.classList.remove("hovered");
1717     },
1718
1719     dispose: function()
1720     {
1721         this.element.remove();
1722         this._expandElement._dispose();
1723     }
1724 }
1725
1726 /**
1727  * @constructor
1728  */
1729 WebInspector.TimelineExpandableElement = function(container)
1730 {
1731     this._element = container.createChild("div", "timeline-expandable");
1732     this._element.createChild("div", "timeline-expandable-left");
1733     this._arrow = this._element.createChild("div", "timeline-expandable-arrow");
1734 }
1735
1736 WebInspector.TimelineExpandableElement.prototype = {
1737     _update: function(record, index, left, width)
1738     {
1739         const rowHeight = WebInspector.TimelinePanel.rowHeight;
1740         if (record.visibleChildrenCount || record.expandable) {
1741             this._element.style.top = index * rowHeight + "px";
1742             this._element.style.left = left + "px";
1743             this._element.style.width = Math.max(12, width + 25) + "px";
1744             if (!record.collapsed) {
1745                 this._element.style.height = (record.visibleChildrenCount + 1) * rowHeight + "px";
1746                 this._element.classList.add("timeline-expandable-expanded");
1747                 this._element.classList.remove("timeline-expandable-collapsed");
1748             } else {
1749                 this._element.style.height = rowHeight + "px";
1750                 this._element.classList.add("timeline-expandable-collapsed");
1751                 this._element.classList.remove("timeline-expandable-expanded");
1752             }
1753             this._element.classList.remove("hidden");
1754         } else
1755             this._element.classList.add("hidden");
1756     },
1757
1758     _dispose: function()
1759     {
1760         this._element.remove();
1761     }
1762 }
1763
1764 /**
1765  * @constructor
1766  * @implements {WebInspector.TimelinePresentationModel.Filter}
1767  */
1768 WebInspector.TimelineCategoryFilter = function()
1769 {
1770 }
1771
1772 WebInspector.TimelineCategoryFilter.prototype = {
1773     /**
1774      * @param {!WebInspector.TimelinePresentationModel.Record} record
1775      * @return {boolean}
1776      */
1777     accept: function(record)
1778     {
1779         return !record.category.hidden && record.type !== WebInspector.TimelineModel.RecordType.BeginFrame;
1780     }
1781 }
1782
1783 /**
1784  * @constructor
1785  * @implements {WebInspector.TimelinePresentationModel.Filter}
1786  */
1787 WebInspector.TimelineIsLongFilter = function()
1788 {
1789     this._minimumRecordDuration = 0;
1790 }
1791
1792 WebInspector.TimelineIsLongFilter.prototype = {
1793     /**
1794      * @param {number} value
1795      */
1796     setMinimumRecordDuration: function(value)
1797     {
1798         this._minimumRecordDuration = value;
1799     },
1800
1801     /**
1802      * @param {!WebInspector.TimelinePresentationModel.Record} record
1803      * @return {boolean}
1804      */
1805     accept: function(record)
1806     {
1807         return this._minimumRecordDuration ? ((record.lastChildEndTime - record.startTime) >= this._minimumRecordDuration) : true;
1808     }
1809 }
1810
1811 /**
1812  * @param {!RegExp} regExp
1813  * @constructor
1814  * @implements {WebInspector.TimelinePresentationModel.Filter}
1815  */
1816 WebInspector.TimelineSearchFilter = function(regExp)
1817 {
1818     this._regExp = regExp;
1819 }
1820
1821 WebInspector.TimelineSearchFilter.prototype = {
1822     /**
1823      * @param {!WebInspector.TimelinePresentationModel.Record} record
1824      * @return {boolean}
1825      */
1826     accept: function(record)
1827     {
1828         return WebInspector.TimelineRecordListRow.testContentMatching(record, this._regExp);
1829     }
1830 }
1831
1832 /**
1833  * @constructor
1834  * @implements {WebInspector.TimelinePresentationModel.Filter}
1835  */
1836 WebInspector.TimelineWindowFilter = function()
1837 {
1838     this.reset();
1839 }
1840
1841 WebInspector.TimelineWindowFilter.prototype = {
1842     reset: function()
1843     {
1844         this._windowStartTime = 0;
1845         this._windowEndTime = Infinity;
1846     },
1847
1848     setWindowTimes: function(windowStartTime, windowEndTime)
1849     {
1850         this._windowStartTime = windowStartTime;
1851         this._windowEndTime = windowEndTime;
1852     },
1853
1854     /**
1855      * @param {!WebInspector.TimelinePresentationModel.Record} record
1856      * @return {boolean}
1857      */
1858     accept: function(record)
1859     {
1860         return record.lastChildEndTime >= this._windowStartTime && record.startTime <= this._windowEndTime;
1861     }
1862 }
1863
1864 /**
1865  * @constructor
1866  * @extends {WebInspector.View}
1867  */
1868 WebInspector.TimelineDetailsView = function()
1869 {
1870     WebInspector.View.call(this);
1871     this.element.classList.add("timeline-details-view");
1872     this._titleElement = this.element.createChild("div", "timeline-details-view-title");
1873     this._titleElement.textContent = WebInspector.UIString("DETAILS");
1874     this._contentElement = this.element.createChild("div", "timeline-details-view-body");
1875 }
1876
1877 WebInspector.TimelineDetailsView.prototype = {
1878     /**
1879      * @return {!Element}
1880      */
1881     titleElement: function()
1882     {
1883         return this._titleElement;
1884     },
1885
1886     /**
1887      * @param {string} title
1888      * @param {!Node} node
1889      */
1890     setContent: function(title, node)
1891     {
1892         this._titleElement.textContent = WebInspector.UIString("DETAILS: %s", title);
1893         this._contentElement.removeChildren();
1894         this._contentElement.appendChild(node);
1895     },
1896
1897     /**
1898      * @param {boolean} vertical
1899      */
1900     setVertical: function(vertical)
1901     {
1902         this._contentElement.enableStyleClass("hbox", !vertical);
1903         this._contentElement.enableStyleClass("vbox", vertical);
1904     },
1905
1906     __proto__: WebInspector.View.prototype
1907 }