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