Upstream version 7.35.144.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  * @extends {WebInspector.HBox}
35  * @implements {WebInspector.TimelineModeView}
36  * @param {!WebInspector.TimelineModeViewDelegate} delegate
37  * @param {!WebInspector.TimelineModel} model
38  */
39 WebInspector.TimelineView = function(delegate, model)
40 {
41     WebInspector.HBox.call(this);
42     this.element.classList.add("timeline-view");
43
44     this._delegate = delegate;
45     this._model = model;
46     this._presentationModel = new WebInspector.TimelinePresentationModel(model);
47     this._calculator = new WebInspector.TimelineCalculator(model);
48     this._linkifier = new WebInspector.Linkifier();
49
50     this._boundariesAreValid = true;
51     this._scrollTop = 0;
52
53     this._recordsView = this._createRecordsView();
54     this._recordsView.addEventListener(WebInspector.SplitView.Events.SidebarSizeChanged, this._sidebarResized, this);
55     this._recordsView.show(this.element);
56     this._headerElement = this.element.createChild("div", "fill");
57     this._headerElement.id = "timeline-graph-records-header";
58
59     // Create gpu tasks containers.
60     this._cpuBarsElement = this._headerElement.createChild("div", "timeline-utilization-strip");
61     if (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
62         this._gpuBarsElement = this._headerElement.createChild("div", "timeline-utilization-strip gpu");
63
64     this._popoverHelper = new WebInspector.PopoverHelper(this.element, this._getPopoverAnchor.bind(this), this._showPopover.bind(this));
65
66     this.element.addEventListener("mousemove", this._mouseMove.bind(this), false);
67     this.element.addEventListener("mouseout", this._mouseOut.bind(this), false);
68     this.element.addEventListener("keydown", this._keyDown.bind(this), false);
69
70     this._expandOffset = 15;
71 }
72
73 WebInspector.TimelineView.prototype = {
74     /**
75      * @param {?WebInspector.TimelineFrameModel} frameModel
76      */
77     setFrameModel: function(frameModel)
78     {
79         this._frameModel = frameModel;
80     },
81
82     /**
83      * @return {!WebInspector.SplitView}
84      */
85     _createRecordsView: function()
86     {
87         var recordsView = new WebInspector.SplitView(true, false, "timelinePanelRecorsSplitViewState");
88         this._containerElement = recordsView.element;
89         this._containerElement.tabIndex = 0;
90         this._containerElement.id = "timeline-container";
91         this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
92
93         // Create records list in the records sidebar.
94         recordsView.sidebarElement().createChild("div", "timeline-records-title").textContent = WebInspector.UIString("RECORDS");
95         this._sidebarListElement = recordsView.sidebarElement().createChild("div", "timeline-records-list");
96
97         // Create grid in the records main area.
98         this._gridContainer = new WebInspector.VBoxWithResizeCallback(this._onViewportResize.bind(this));
99         this._gridContainer.element.id = "resources-container-content";
100         this._gridContainer.show(recordsView.mainElement());
101         this._timelineGrid = new WebInspector.TimelineGrid();
102         this._gridContainer.element.appendChild(this._timelineGrid.element);
103
104         this._itemsGraphsElement = this._gridContainer.element.createChild("div");
105         this._itemsGraphsElement.id = "timeline-graphs";
106
107         // Create gap elements
108         this._topGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap");
109         this._graphRowsElement = this._itemsGraphsElement.createChild("div");
110         this._bottomGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap");
111         this._expandElements = this._itemsGraphsElement.createChild("div");
112         this._expandElements.id = "orphan-expand-elements";
113
114         return recordsView;
115     },
116
117     _rootRecord: function()
118     {
119         return this._presentationModel.rootRecord();
120     },
121
122     _updateEventDividers: function()
123     {
124         this._timelineGrid.removeEventDividers();
125         var clientWidth = this._graphRowsElementWidth;
126         var dividers = [];
127         var eventDividerRecords = this._model.eventDividerRecords();
128
129         for (var i = 0; i < eventDividerRecords.length; ++i) {
130             var record = eventDividerRecords[i];
131             var positions = this._calculator.computeBarGraphWindowPosition(record);
132             var dividerPosition = Math.round(positions.left);
133             if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition])
134                 continue;
135             var divider = WebInspector.TimelineUIUtils.createEventDivider(record.type, WebInspector.TimelineUIUtils.recordTitle(record));
136             divider.style.left = dividerPosition + "px";
137             dividers[dividerPosition] = divider;
138         }
139         this._timelineGrid.addEventDividers(dividers);
140     },
141
142     _updateFrameBars: function(frames)
143     {
144         var clientWidth = this._graphRowsElementWidth;
145         if (this._frameContainer)
146             this._frameContainer.removeChildren();
147         else {
148             const frameContainerBorderWidth = 1;
149             this._frameContainer = document.createElement("div");
150             this._frameContainer.classList.add("fill");
151             this._frameContainer.classList.add("timeline-frame-container");
152             this._frameContainer.style.height = WebInspector.TimelinePanel.rowHeight + frameContainerBorderWidth + "px";
153             this._frameContainer.addEventListener("dblclick", this._onFrameDoubleClicked.bind(this), false);
154         }
155
156         var dividers = [];
157
158         for (var i = 0; i < frames.length; ++i) {
159             var frame = frames[i];
160             var frameStart = this._calculator.computePosition(frame.startTime);
161             var frameEnd = this._calculator.computePosition(frame.endTime);
162
163             var frameStrip = document.createElement("div");
164             frameStrip.className = "timeline-frame-strip";
165             var actualStart = Math.max(frameStart, 0);
166             var width = frameEnd - actualStart;
167             frameStrip.style.left = actualStart + "px";
168             frameStrip.style.width = width + "px";
169             frameStrip._frame = frame;
170
171             const minWidthForFrameInfo = 60;
172             if (width > minWidthForFrameInfo)
173                 frameStrip.textContent = Number.millisToString(frame.endTime - frame.startTime, true);
174
175             this._frameContainer.appendChild(frameStrip);
176
177             if (actualStart > 0) {
178                 var frameMarker = WebInspector.TimelineUIUtils.createEventDivider(WebInspector.TimelineModel.RecordType.BeginFrame);
179                 frameMarker.style.left = frameStart + "px";
180                 dividers.push(frameMarker);
181             }
182         }
183         this._timelineGrid.addEventDividers(dividers);
184         this._headerElement.appendChild(this._frameContainer);
185     },
186
187     _onFrameDoubleClicked: function(event)
188     {
189         var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip");
190         if (!frameBar)
191             return;
192         this._delegate.requestWindowTimes(frameBar._frame.startTime, frameBar._frame.endTime);
193     },
194
195     /**
196      * @param {!WebInspector.TimelineModel.Record} record
197      */
198     addRecord: function(record)
199     {
200         this._presentationModel.addRecord(record);
201         this._invalidateAndScheduleRefresh(false, false);
202     },
203
204     /**
205      * @param {number} width
206      */
207     setSidebarSize: function(width)
208     {
209         this._recordsView.setSidebarSize(width);
210     },
211
212     /**
213      * @param {!WebInspector.Event} event
214      */
215     _sidebarResized: function(event)
216     {
217         this.dispatchEventToListeners(WebInspector.SplitView.Events.SidebarSizeChanged, event.data);
218     },
219
220     _onViewportResize: function()
221     {
222         this._resize(this._recordsView.sidebarSize());
223     },
224
225     /**
226      * @param {number} sidebarWidth
227      */
228     _resize: function(sidebarWidth)
229     {
230         this._closeRecordDetails();
231         this._graphRowsElementWidth = this._graphRowsElement.offsetWidth;
232         this._headerElement.style.left = sidebarWidth + "px";
233         this._headerElement.style.width = this._itemsGraphsElement.offsetWidth + "px";
234         this._scheduleRefresh(false, true);
235     },
236
237     _resetView: function()
238     {
239         this._windowStartTime = -1;
240         this._windowEndTime = -1;
241         this._boundariesAreValid = false;
242         this._adjustScrollPosition(0);
243         this._linkifier.reset();
244         this._closeRecordDetails();
245         this._automaticallySizeWindow = true;
246         this._presentationModel.reset();
247     },
248
249     reset: function()
250     {
251         this._resetView();
252         this._invalidateAndScheduleRefresh(true, true);
253     },
254
255     /**
256      * @return {!Array.<!Element>}
257      */
258     elementsToRestoreScrollPositionsFor: function()
259     {
260         return [this._containerElement];
261     },
262
263     /**
264      * @param {?RegExp} textFilter
265      */
266     refreshRecords: function(textFilter)
267     {
268         this._presentationModel.reset();
269         var records = this._model.records();
270         for (var i = 0; i < records.length; ++i)
271             this.addRecord(records[i]);
272         this._automaticallySizeWindow = false;
273         this._presentationModel.setTextFilter(textFilter);
274         this._invalidateAndScheduleRefresh(false, true);
275     },
276
277     wasShown: function()
278     {
279         WebInspector.View.prototype.wasShown.call(this);
280         this._onViewportResize();
281         this._refresh();
282     },
283
284     willHide: function()
285     {
286         this._closeRecordDetails();
287         WebInspector.View.prototype.willHide.call(this);
288     },
289
290     _onScroll: function(event)
291     {
292         this._closeRecordDetails();
293         this._scrollTop = this._containerElement.scrollTop;
294         var dividersTop = Math.max(0, this._scrollTop);
295         this._timelineGrid.setScrollAndDividerTop(this._scrollTop, dividersTop);
296         this._scheduleRefresh(true, true);
297     },
298
299     /**
300      * @param {boolean} preserveBoundaries
301      * @param {boolean} userGesture
302      */
303     _invalidateAndScheduleRefresh: function(preserveBoundaries, userGesture)
304     {
305         this._presentationModel.invalidateFilteredRecords();
306         this._scheduleRefresh(preserveBoundaries, userGesture);
307     },
308
309     /**
310      * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord
311      */
312     _selectRecord: function(presentationRecord)
313     {
314         if (presentationRecord && presentationRecord.coalesced()) {
315             // Presentation record does not have model record to highlight.
316             this._innerSetSelectedRecord(presentationRecord);
317             var aggregatedStats = {};
318             var presentationChildren = presentationRecord.presentationChildren();
319             for (var i = 0; i < presentationChildren.length; ++i)
320                 WebInspector.TimelineUIUtils.aggregateTimeByCategory(aggregatedStats, presentationChildren[i].record().aggregatedStats);
321             var idle = presentationRecord.record().endTime - presentationRecord.record().startTime;
322             for (var category in aggregatedStats)
323                 idle -= aggregatedStats[category];
324             aggregatedStats["idle"] = idle;
325             this._delegate.showAggregatedStatsInDetails(WebInspector.TimelineUIUtils.recordStyle(presentationRecord.record()).title, aggregatedStats);
326             return;
327         }
328         this._delegate.selectRecord(presentationRecord ? presentationRecord.record() : null);
329     },
330
331     /**
332      * @param {?WebInspector.TimelineModel.Record} record
333      */
334     setSelectedRecord: function(record)
335     {
336         this._innerSetSelectedRecord(this._presentationModel.toPresentationRecord(record));
337     },
338
339     /**
340      * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord
341      */
342     _innerSetSelectedRecord: function(presentationRecord)
343     {
344         if (presentationRecord === this._lastSelectedRecord)
345             return;
346
347         // Remove selection rendering.p
348         if (this._lastSelectedRecord) {
349             if (this._lastSelectedRecord.listRow())
350                 this._lastSelectedRecord.listRow().renderAsSelected(false);
351             if (this._lastSelectedRecord.graphRow())
352                 this._lastSelectedRecord.graphRow().renderAsSelected(false);
353         }
354
355         this._lastSelectedRecord = presentationRecord;
356         if (!presentationRecord)
357             return;
358
359         this._innerRevealRecord(presentationRecord);
360         if (presentationRecord.listRow())
361             presentationRecord.listRow().renderAsSelected(true);
362         if (presentationRecord.graphRow())
363             presentationRecord.graphRow().renderAsSelected(true);
364     },
365
366     /**
367      * @param {number} startTime
368      * @param {number} endTime
369      */
370     setWindowTimes: function(startTime, endTime)
371     {
372         this._windowStartTime = startTime;
373         this._windowEndTime = endTime;
374         this._presentationModel.setWindowTimes(startTime, endTime);
375         this._automaticallySizeWindow = false;
376         this._invalidateAndScheduleRefresh(false, true);
377         this._selectRecord(null);
378     },
379
380     /**
381      * @param {boolean} preserveBoundaries
382      * @param {boolean} userGesture
383      */
384     _scheduleRefresh: function(preserveBoundaries, userGesture)
385     {
386         this._closeRecordDetails();
387         this._boundariesAreValid &= preserveBoundaries;
388
389         if (!this.isShowing())
390             return;
391
392         if (preserveBoundaries || userGesture)
393             this._refresh();
394         else {
395             if (!this._refreshTimeout)
396                 this._refreshTimeout = setTimeout(this._refresh.bind(this), 300);
397         }
398     },
399
400     _refresh: function()
401     {
402         if (this._refreshTimeout) {
403             clearTimeout(this._refreshTimeout);
404             delete this._refreshTimeout;
405         }
406         var windowStartTime = this._windowStartTime;
407         var windowEndTime = this._windowEndTime;
408         this._timelinePaddingLeft = this._expandOffset;
409         if (windowStartTime === -1)
410             windowStartTime = this._model.minimumRecordTime();
411         if (windowEndTime === -1)
412             windowEndTime = this._model.maximumRecordTime();
413         this._calculator.setWindow(windowStartTime, windowEndTime);
414         this._calculator.setDisplayWindow(this._timelinePaddingLeft, this._graphRowsElementWidth);
415
416         this._refreshRecords();
417         if (!this._boundariesAreValid) {
418             this._updateEventDividers();
419             if (this._frameContainer)
420                 this._frameContainer.remove();
421             if (this._frameModel) {
422                 var frames = this._frameModel.filteredFrames(windowStartTime, windowEndTime);
423                 const maxFramesForFrameBars = 30;
424                 if  (frames.length && frames.length < maxFramesForFrameBars) {
425                     this._timelineGrid.removeDividers();
426                     this._updateFrameBars(frames);
427                 } else {
428                     this._timelineGrid.updateDividers(this._calculator);
429                 }
430             } else
431                 this._timelineGrid.updateDividers(this._calculator);
432             this._refreshAllUtilizationBars();
433         }
434         this._boundariesAreValid = true;
435     },
436
437     /**
438      * @param {!WebInspector.TimelinePresentationModel.Record} recordToReveal
439      */
440     _innerRevealRecord: function(recordToReveal)
441     {
442         var needRefresh = false;
443         // Expand all ancestors.
444         for (var parent = recordToReveal.presentationParent(); parent !== this._rootRecord(); parent = parent.presentationParent()) {
445             if (!parent.collapsed())
446                 continue;
447             this._presentationModel.invalidateFilteredRecords();
448             parent.setCollapsed(false);
449             needRefresh = true;
450         }
451         var recordsInWindow = this._presentationModel.filteredRecords();
452         var index = recordsInWindow.indexOf(recordToReveal);
453
454         var itemOffset = index * WebInspector.TimelinePanel.rowHeight;
455         var visibleTop = this._scrollTop - WebInspector.TimelinePanel.headerHeight;
456         var visibleBottom = visibleTop + this._containerElementHeight - WebInspector.TimelinePanel.rowHeight;
457         if (itemOffset < visibleTop)
458             this._containerElement.scrollTop = itemOffset;
459         else if (itemOffset > visibleBottom)
460             this._containerElement.scrollTop = itemOffset - this._containerElementHeight + WebInspector.TimelinePanel.headerHeight + WebInspector.TimelinePanel.rowHeight;
461         else if (needRefresh)
462             this._refreshRecords();
463     },
464
465     _refreshRecords: function()
466     {
467         this._containerElementHeight = this._containerElement.clientHeight;
468         var recordsInWindow = this._presentationModel.filteredRecords();
469
470         // Calculate the visible area.
471         var visibleTop = this._scrollTop;
472         var visibleBottom = visibleTop + this._containerElementHeight;
473
474         var rowHeight = WebInspector.TimelinePanel.rowHeight;
475         var headerHeight = WebInspector.TimelinePanel.headerHeight;
476
477         // Convert visible area to visible indexes. Always include top-level record for a visible nested record.
478         var startIndex = Math.max(0, Math.min(Math.floor((visibleTop - headerHeight) / rowHeight), recordsInWindow.length - 1));
479         var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
480         var lastVisibleLine = Math.max(0, Math.floor((visibleBottom - headerHeight) / rowHeight));
481         if (this._automaticallySizeWindow && recordsInWindow.length > lastVisibleLine) {
482             this._automaticallySizeWindow = false;
483             this._selectRecord(null);
484             // If we're at the top, always use real timeline start as a left window bound so that expansion arrow padding logic works.
485             var windowStartTime = startIndex ? recordsInWindow[startIndex].record().startTime : this._model.minimumRecordTime();
486             var windowEndTime = recordsInWindow[Math.max(0, lastVisibleLine - 1)].record().endTime;
487             this._delegate.requestWindowTimes(windowStartTime, windowEndTime);
488             recordsInWindow = this._presentationModel.filteredRecords();
489             endIndex = Math.min(recordsInWindow.length, lastVisibleLine);
490         }
491
492         // Resize gaps first.
493         this._topGapElement.style.height = (startIndex * rowHeight) + "px";
494         this._recordsView.sidebarElement().firstElementChild.style.flexBasis = (startIndex * rowHeight + headerHeight) + "px";
495         this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
496         var rowsHeight = headerHeight + recordsInWindow.length * rowHeight;
497         var totalHeight = Math.max(this._containerElementHeight, rowsHeight);
498
499         this._recordsView.mainElement().style.height = totalHeight + "px";
500         this._recordsView.sidebarElement().style.height = totalHeight + "px";
501         this._recordsView.resizerElement().style.height = totalHeight + "px";
502
503         // Update visible rows.
504         var listRowElement = this._sidebarListElement.firstChild;
505         var width = this._graphRowsElementWidth;
506         this._itemsGraphsElement.removeChild(this._graphRowsElement);
507         var graphRowElement = this._graphRowsElement.firstChild;
508         var scheduleRefreshCallback = this._invalidateAndScheduleRefresh.bind(this, true, true);
509         var selectRecordCallback = this._selectRecord.bind(this);
510         this._itemsGraphsElement.removeChild(this._expandElements);
511         this._expandElements.removeChildren();
512
513         for (var i = 0; i < endIndex; ++i) {
514             var record = recordsInWindow[i];
515
516             if (i < startIndex) {
517                 var lastChildIndex = i + record.visibleChildrenCount();
518                 if (lastChildIndex >= startIndex && lastChildIndex < endIndex) {
519                     var expandElement = new WebInspector.TimelineExpandableElement(this._expandElements);
520                     var positions = this._calculator.computeBarGraphWindowPosition(record);
521                     expandElement._update(record, i, positions.left - this._expandOffset, positions.width);
522                 }
523             } else {
524                 if (!listRowElement) {
525                     listRowElement = new WebInspector.TimelineRecordListRow(this._linkifier, selectRecordCallback, scheduleRefreshCallback).element;
526                     this._sidebarListElement.appendChild(listRowElement);
527                 }
528                 if (!graphRowElement) {
529                     graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, selectRecordCallback, scheduleRefreshCallback).element;
530                     this._graphRowsElement.appendChild(graphRowElement);
531                 }
532
533                 listRowElement.row.update(record, visibleTop);
534                 graphRowElement.row.update(record, this._calculator, this._expandOffset, i);
535                 if (this._lastSelectedRecord === record) {
536                     listRowElement.row.renderAsSelected(true);
537                     graphRowElement.row.renderAsSelected(true);
538                 }
539
540                 listRowElement = listRowElement.nextSibling;
541                 graphRowElement = graphRowElement.nextSibling;
542             }
543         }
544
545         // Remove extra rows.
546         while (listRowElement) {
547             var nextElement = listRowElement.nextSibling;
548             listRowElement.row.dispose();
549             listRowElement = nextElement;
550         }
551         while (graphRowElement) {
552             var nextElement = graphRowElement.nextSibling;
553             graphRowElement.row.dispose();
554             graphRowElement = nextElement;
555         }
556
557         this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
558         this._itemsGraphsElement.appendChild(this._expandElements);
559         this._adjustScrollPosition(recordsInWindow.length * rowHeight + headerHeight);
560
561         return recordsInWindow.length;
562     },
563
564     _refreshAllUtilizationBars: function()
565     {
566         this._refreshUtilizationBars(WebInspector.UIString("CPU"), this._model.mainThreadTasks(), this._cpuBarsElement);
567         if (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
568             this._refreshUtilizationBars(WebInspector.UIString("GPU"), this._model.gpuThreadTasks(), this._gpuBarsElement);
569     },
570
571     /**
572      * @param {string} name
573      * @param {!Array.<!WebInspector.TimelineModel.Record>} tasks
574      * @param {?Element} container
575      */
576     _refreshUtilizationBars: function(name, tasks, container)
577     {
578         if (!container)
579             return;
580
581         const barOffset = 3;
582         const minGap = 3;
583
584         var minWidth = WebInspector.TimelineCalculator._minWidth;
585         var widthAdjustment = minWidth / 2;
586
587         var width = this._graphRowsElementWidth;
588         var boundarySpan = this._windowEndTime - this._windowStartTime;
589         var scale = boundarySpan / (width - minWidth - this._timelinePaddingLeft);
590         var startTime = (this._windowStartTime - this._timelinePaddingLeft * scale);
591         var endTime = startTime + width * scale;
592
593         /**
594          * @param {number} value
595          * @param {!WebInspector.TimelineModel.Record} task
596          * @return {number}
597          */
598         function compareEndTime(value, task)
599         {
600             return value < task.endTime ? -1 : 1;
601         }
602
603         var taskIndex = insertionIndexForObjectInListSortedByFunction(startTime, tasks, compareEndTime);
604
605         var foreignStyle = "gpu-task-foreign";
606         var element = container.firstChild;
607         var lastElement;
608         var lastLeft;
609         var lastRight;
610
611         for (; taskIndex < tasks.length; ++taskIndex) {
612             var task = tasks[taskIndex];
613             if (task.startTime > endTime)
614                 break;
615
616             var left = Math.max(0, this._calculator.computePosition(task.startTime) + barOffset - widthAdjustment);
617             var right = Math.min(width, this._calculator.computePosition(task.endTime || 0) + barOffset + widthAdjustment);
618
619             if (lastElement) {
620                 var gap = Math.floor(left) - Math.ceil(lastRight);
621                 if (gap < minGap) {
622                     if (!task.data["foreign"])
623                         lastElement.classList.remove(foreignStyle);
624                     lastRight = right;
625                     lastElement._tasksInfo.lastTaskIndex = taskIndex;
626                     continue;
627                 }
628                 lastElement.style.width = (lastRight - lastLeft) + "px";
629             }
630
631             if (!element)
632                 element = container.createChild("div", "timeline-graph-bar");
633             element.style.left = left + "px";
634             element._tasksInfo = {name: name, tasks: tasks, firstTaskIndex: taskIndex, lastTaskIndex: taskIndex};
635             if (task.data["foreign"])
636                 element.classList.add(foreignStyle);
637             lastLeft = left;
638             lastRight = right;
639             lastElement = element;
640             element = element.nextSibling;
641         }
642
643         if (lastElement)
644             lastElement.style.width = (lastRight - lastLeft) + "px";
645
646         while (element) {
647             var nextElement = element.nextSibling;
648             element._tasksInfo = null;
649             container.removeChild(element);
650             element = nextElement;
651         }
652     },
653
654     _adjustScrollPosition: function(totalHeight)
655     {
656         // Prevent the container from being scrolled off the end.
657         if ((this._scrollTop + this._containerElementHeight) > totalHeight + 1)
658             this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
659     },
660
661     _getPopoverAnchor: function(element)
662     {
663         var anchor = element.enclosingNodeOrSelfWithClass("timeline-graph-bar");
664         if (anchor && anchor._tasksInfo)
665             return anchor;
666         return element.enclosingNodeOrSelfWithClass("timeline-frame-strip");
667     },
668
669     _mouseOut: function()
670     {
671         this._hideQuadHighlight();
672     },
673
674     /**
675      * @param {?Event} e
676      */
677     _mouseMove: function(e)
678     {
679         var rowElement = e.target.enclosingNodeOrSelfWithClass("timeline-tree-item");
680         if (rowElement && rowElement.row && rowElement.row._record.record().highlightQuad)
681             this._highlightQuad(rowElement.row._record.record().highlightQuad);
682         else
683             this._hideQuadHighlight();
684
685         var taskBarElement = e.target.enclosingNodeOrSelfWithClass("timeline-graph-bar");
686         if (taskBarElement && taskBarElement._tasksInfo) {
687             var offset = taskBarElement.offsetLeft;
688             this._timelineGrid.showCurtains(offset >= 0 ? offset : 0, taskBarElement.offsetWidth);
689         } else
690             this._timelineGrid.hideCurtains();
691     },
692
693     /**
694      * @param {?Event} event
695      */
696     _keyDown: function(event)
697     {
698         if (!this._lastSelectedRecord || event.shiftKey || event.metaKey || event.ctrlKey)
699             return;
700
701         var record = this._lastSelectedRecord;
702         var recordsInWindow = this._presentationModel.filteredRecords();
703         var index = recordsInWindow.indexOf(record);
704         var recordsInPage = Math.floor(this._containerElementHeight / WebInspector.TimelinePanel.rowHeight);
705         var rowHeight = WebInspector.TimelinePanel.rowHeight;
706
707         if (index === -1)
708             index = 0;
709
710         switch (event.keyIdentifier) {
711         case "Left":
712             if (record.presentationParent()) {
713                 if ((!record.expandable() || record.collapsed()) && record.presentationParent() !== this._presentationModel.rootRecord()) {
714                     this._selectRecord(record.presentationParent());
715                 } else {
716                     record.setCollapsed(true);
717                     this._invalidateAndScheduleRefresh(true, true);
718                 }
719             }
720             event.consume(true);
721             break;
722         case "Up":
723             if (--index < 0)
724                 break;
725             this._selectRecord(recordsInWindow[index]);
726             event.consume(true);
727             break;
728         case "Right":
729             if (record.expandable() && record.collapsed()) {
730                 record.setCollapsed(false);
731                 this._invalidateAndScheduleRefresh(true, true);
732             } else {
733                 if (++index >= recordsInWindow.length)
734                     break;
735                 this._selectRecord(recordsInWindow[index]);
736             }
737             event.consume(true);
738             break;
739         case "Down":
740             if (++index >= recordsInWindow.length)
741                 break;
742             this._selectRecord(recordsInWindow[index]);
743             event.consume(true);
744             break;
745         case "PageUp":
746             index = Math.max(0, index - recordsInPage);
747             this._scrollTop = Math.max(0, this._scrollTop - recordsInPage * rowHeight);
748             this._containerElement.scrollTop = this._scrollTop;
749             this._selectRecord(recordsInWindow[index]);
750             event.consume(true);
751             break;
752         case "PageDown":
753             index = Math.min(recordsInWindow.length - 1, index + recordsInPage);
754             this._scrollTop = Math.min(this._containerElement.scrollHeight - this._containerElementHeight, this._scrollTop + recordsInPage * rowHeight);
755             this._containerElement.scrollTop = this._scrollTop;
756             this._selectRecord(recordsInWindow[index]);
757             event.consume(true);
758             break;
759         case "Home":
760             index = 0;
761             this._selectRecord(recordsInWindow[index]);
762             event.consume(true);
763             break;
764         case "End":
765             index = recordsInWindow.length - 1;
766             this._selectRecord(recordsInWindow[index]);
767             event.consume(true);
768             break;
769         }
770     },
771
772     /**
773      * @param {!Array.<number>} quad
774      */
775     _highlightQuad: function(quad)
776     {
777         if (this._highlightedQuad === quad)
778             return;
779         this._highlightedQuad = quad;
780         DOMAgent.highlightQuad(quad, WebInspector.Color.PageHighlight.Content.toProtocolRGBA(), WebInspector.Color.PageHighlight.ContentOutline.toProtocolRGBA());
781     },
782
783     _hideQuadHighlight: function()
784     {
785         if (this._highlightedQuad) {
786             delete this._highlightedQuad;
787             DOMAgent.hideHighlight();
788         }
789     },
790
791     /**
792      * @param {!Element} anchor
793      * @param {!WebInspector.Popover} popover
794      */
795     _showPopover: function(anchor, popover)
796     {
797         if (anchor.classList.contains("timeline-frame-strip")) {
798             var frame = anchor._frame;
799             popover.show(WebInspector.TimelineUIUtils.generatePopupContentForFrame(frame), anchor);
800         } else if (anchor._tasksInfo) {
801             popover.show(WebInspector.TimelineUIUtils.generateMainThreadBarPopupContent(this._model, anchor._tasksInfo), anchor, null, null, WebInspector.Popover.Orientation.Bottom);
802         }
803
804         function showCallback(popupContent)
805         {
806             popover.show(popupContent, anchor);
807         }
808     },
809
810     _closeRecordDetails: function()
811     {
812         this._popoverHelper.hidePopover();
813     },
814
815     /**
816      * @param {?WebInspector.TimelineModel.Record} record
817      * @param {string=} regex
818      * @param {boolean=} selectRecord
819      */
820     highlightSearchResult: function(record, regex, selectRecord)
821     {
822        if (this._highlightDomChanges)
823             WebInspector.revertDomChanges(this._highlightDomChanges);
824         this._highlightDomChanges = [];
825
826         var presentationRecord = this._presentationModel.toPresentationRecord(record);
827         if (!presentationRecord)
828             return;
829
830         if (selectRecord)
831             this._selectRecord(presentationRecord);
832
833         for (var element = this._sidebarListElement.firstChild; element; element = element.nextSibling) {
834             if (element.row._record === presentationRecord) {
835                 element.row.highlight(regex, this._highlightDomChanges);
836                 break;
837             }
838         }
839     },
840
841     __proto__: WebInspector.HBox.prototype
842 }
843
844 /**
845  * @constructor
846  * @param {!WebInspector.TimelineModel} model
847  * @implements {WebInspector.TimelineGrid.Calculator}
848  */
849 WebInspector.TimelineCalculator = function(model)
850 {
851     this._model = model;
852 }
853
854 WebInspector.TimelineCalculator._minWidth = 5;
855
856 WebInspector.TimelineCalculator.prototype = {
857     /**
858      * @return {number}
859      */
860     paddingLeft: function()
861     {
862         return this._paddingLeft;
863     },
864
865     /**
866      * @param {number} time
867      * @return {number}
868      */
869     computePosition: function(time)
870     {
871         return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this._paddingLeft;
872     },
873
874     /**
875      * @return {!{start: number, end: number, endWithChildren: number, cpuWidth: number}}
876      */
877     computeBarGraphPercentages: function(record)
878     {
879         var start = (record.startTime - this._minimumBoundary) / this.boundarySpan() * 100;
880         var end = (record.startTime + record.selfTime - this._minimumBoundary) / this.boundarySpan() * 100;
881         var endWithChildren = (record.lastChildEndTime - this._minimumBoundary) / this.boundarySpan() * 100;
882         var cpuWidth = record.cpuTime / this.boundarySpan() * 100;
883         return {start: start, end: end, endWithChildren: endWithChildren, cpuWidth: cpuWidth};
884     },
885
886     /**
887      * @return {!{left: number, width: number, widthWithChildren: number, cpuWidth: number}}
888      */
889     computeBarGraphWindowPosition: function(record)
890     {
891         var percentages = this.computeBarGraphPercentages(record);
892         var widthAdjustment = 0;
893
894         var left = this.computePosition(record.startTime);
895         var width = (percentages.end - percentages.start) / 100 * this._workingArea;
896         if (width < WebInspector.TimelineCalculator._minWidth) {
897             widthAdjustment = WebInspector.TimelineCalculator._minWidth - width;
898             width = WebInspector.TimelineCalculator._minWidth;
899         }
900         var widthWithChildren = (percentages.endWithChildren - percentages.start) / 100 * this._workingArea + widthAdjustment;
901         var cpuWidth = percentages.cpuWidth / 100 * this._workingArea + widthAdjustment;
902         if (percentages.endWithChildren > percentages.end)
903             widthWithChildren += widthAdjustment;
904         return {left: left, width: width, widthWithChildren: widthWithChildren, cpuWidth: cpuWidth};
905     },
906
907     setWindow: function(minimumBoundary, maximumBoundary)
908     {
909         this._minimumBoundary = minimumBoundary;
910         this._maximumBoundary = maximumBoundary;
911     },
912
913     /**
914      * @param {number} paddingLeft
915      * @param {number} clientWidth
916      */
917     setDisplayWindow: function(paddingLeft, clientWidth)
918     {
919         this._workingArea = clientWidth - WebInspector.TimelineCalculator._minWidth - paddingLeft;
920         this._paddingLeft = paddingLeft;
921     },
922
923     /**
924      * @param {number} value
925      * @param {number=} precision
926      * @return {string}
927      */
928     formatTime: function(value, precision)
929     {
930         return Number.preciseMillisToString(value - this.zeroTime(), precision);
931     },
932
933     /**
934      * @return {number}
935      */
936     maximumBoundary: function()
937     {
938         return this._maximumBoundary;
939     },
940
941     /**
942      * @return {number}
943      */
944     minimumBoundary: function()
945     {
946         return this._minimumBoundary;
947     },
948
949     /**
950      * @return {number}
951      */
952     zeroTime: function()
953     {
954         return this._model.minimumRecordTime();
955     },
956
957     /**
958      * @return {number}
959      */
960     boundarySpan: function()
961     {
962         return this._maximumBoundary - this._minimumBoundary;
963     }
964 }
965
966 /**
967  * @constructor
968  * @param {!WebInspector.Linkifier} linkifier
969  * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
970  * @param {function()} scheduleRefresh
971  */
972 WebInspector.TimelineRecordListRow = function(linkifier, selectRecord, scheduleRefresh)
973 {
974     this.element = document.createElement("div");
975     this.element.row = this;
976     this.element.style.cursor = "pointer";
977     this.element.addEventListener("click", this._onClick.bind(this), false);
978     this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
979     this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false);
980     this._linkifier = linkifier;
981
982     // Warning is float right block, it goes first.
983     this._warningElement = this.element.createChild("div", "timeline-tree-item-warning hidden");
984
985     this._expandArrowElement = this.element.createChild("div", "timeline-tree-item-expand-arrow");
986     this._expandArrowElement.addEventListener("click", this._onExpandClick.bind(this), false);
987     var iconElement = this.element.createChild("span", "timeline-tree-icon");
988     this._typeElement = this.element.createChild("span", "type");
989
990     this._dataElement = this.element.createChild("span", "data dimmed");
991     this._scheduleRefresh = scheduleRefresh;
992     this._selectRecord = selectRecord;
993 }
994
995 WebInspector.TimelineRecordListRow.prototype = {
996     /**
997      * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
998      * @param {number} offset
999      */
1000     update: function(presentationRecord, offset)
1001     {
1002         this._record = presentationRecord;
1003         var record = presentationRecord.record();
1004         this._offset = offset;
1005
1006         this.element.className = "timeline-tree-item timeline-category-" + record.category.name;
1007         var paddingLeft = 5;
1008         var step = -3;
1009         for (var currentRecord = presentationRecord.presentationParent() ? presentationRecord.presentationParent().presentationParent() : null; currentRecord; currentRecord = currentRecord.presentationParent())
1010             paddingLeft += 12 / (Math.max(1, step++));
1011         this.element.style.paddingLeft = paddingLeft + "px";
1012         if (record.thread)
1013             this.element.classList.add("background");
1014
1015         this._typeElement.textContent = record.title();
1016
1017         if (this._dataElement.firstChild)
1018             this._dataElement.removeChildren();
1019
1020         this._warningElement.classList.toggle("hidden", !presentationRecord.hasWarnings() && !presentationRecord.childHasWarnings());
1021         this._warningElement.classList.toggle("timeline-tree-item-child-warning", presentationRecord.childHasWarnings() && !presentationRecord.hasWarnings());
1022
1023         if (presentationRecord.coalesced()) {
1024             this._dataElement.createTextChild(WebInspector.UIString("× %d", presentationRecord.presentationChildren().length));
1025         } else {
1026             var detailsNode = WebInspector.TimelineUIUtils.buildDetailsNode(record, this._linkifier);
1027             if (detailsNode) {
1028                 this._dataElement.appendChild(document.createTextNode("("));
1029                 this._dataElement.appendChild(detailsNode);
1030                 this._dataElement.appendChild(document.createTextNode(")"));
1031             }
1032         }
1033
1034         this._expandArrowElement.classList.toggle("parent", presentationRecord.expandable());
1035         this._expandArrowElement.classList.toggle("expanded", !!presentationRecord.visibleChildrenCount());
1036         this._record.setListRow(this);
1037     },
1038
1039     highlight: function(regExp, domChanges)
1040     {
1041         var matchInfo = this.element.textContent.match(regExp);
1042         if (matchInfo)
1043             WebInspector.highlightSearchResult(this.element, matchInfo.index, matchInfo[0].length, domChanges);
1044     },
1045
1046     dispose: function()
1047     {
1048         this.element.remove();
1049     },
1050
1051     /**
1052      * @param {!Event} event
1053      */
1054     _onExpandClick: function(event)
1055     {
1056         this._record.setCollapsed(!this._record.collapsed());
1057         this._scheduleRefresh();
1058         event.consume(true);
1059     },
1060
1061     /**
1062      * @param {?Event} event
1063      */
1064     _onClick: function(event)
1065     {
1066         this._selectRecord(this._record);
1067     },
1068
1069     /**
1070      * @param {boolean} selected
1071      */
1072     renderAsSelected: function(selected)
1073     {
1074         this.element.classList.toggle("selected", selected);
1075     },
1076
1077     /**
1078      * @param {?Event} event
1079      */
1080     _onMouseOver: function(event)
1081     {
1082         this.element.classList.add("hovered");
1083         if (this._record.graphRow())
1084             this._record.graphRow().element.classList.add("hovered");
1085     },
1086
1087     /**
1088      * @param {?Event} event
1089      */
1090     _onMouseOut: function(event)
1091     {
1092         this.element.classList.remove("hovered");
1093     if (this._record.graphRow())
1094         this._record.graphRow().element.classList.remove("hovered");
1095     }
1096 }
1097
1098 /**
1099  * @constructor
1100  * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
1101  * @param {function()} scheduleRefresh
1102  */
1103 WebInspector.TimelineRecordGraphRow = function(graphContainer, selectRecord, scheduleRefresh)
1104 {
1105     this.element = document.createElement("div");
1106     this.element.row = this;
1107     this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
1108     this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false);
1109     this.element.addEventListener("click", this._onClick.bind(this), false);
1110
1111     this._barAreaElement = document.createElement("div");
1112     this._barAreaElement.className = "timeline-graph-bar-area";
1113     this.element.appendChild(this._barAreaElement);
1114
1115     this._barWithChildrenElement = document.createElement("div");
1116     this._barWithChildrenElement.className = "timeline-graph-bar with-children";
1117     this._barWithChildrenElement.row = this;
1118     this._barAreaElement.appendChild(this._barWithChildrenElement);
1119
1120     this._barCpuElement = document.createElement("div");
1121     this._barCpuElement.className = "timeline-graph-bar cpu"
1122     this._barCpuElement.row = this;
1123     this._barAreaElement.appendChild(this._barCpuElement);
1124
1125     this._barElement = document.createElement("div");
1126     this._barElement.className = "timeline-graph-bar";
1127     this._barElement.row = this;
1128     this._barAreaElement.appendChild(this._barElement);
1129
1130     this._expandElement = new WebInspector.TimelineExpandableElement(graphContainer);
1131
1132     this._selectRecord = selectRecord;
1133     this._scheduleRefresh = scheduleRefresh;
1134 }
1135
1136 WebInspector.TimelineRecordGraphRow.prototype = {
1137     /**
1138      * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
1139      * @param {!WebInspector.TimelineCalculator} calculator
1140      * @param {number} expandOffset
1141      * @param {number} index
1142      */
1143     update: function(presentationRecord, calculator, expandOffset, index)
1144     {
1145         this._record = presentationRecord;
1146         var record = presentationRecord.record();
1147         this.element.className = "timeline-graph-side timeline-category-" + record.category.name;
1148         if (record.thread)
1149             this.element.classList.add("background");
1150
1151         var barPosition = calculator.computeBarGraphWindowPosition(record);
1152         this._barWithChildrenElement.style.left = barPosition.left + "px";
1153         this._barWithChildrenElement.style.width = barPosition.widthWithChildren + "px";
1154         this._barElement.style.left = barPosition.left + "px";
1155         this._barElement.style.width = (presentationRecord.coalesced() ? barPosition.widthWithChildren : barPosition.width) + "px";
1156         this._barCpuElement.style.left = barPosition.left + "px";
1157         this._barCpuElement.style.width = (presentationRecord.coalesced() ? barPosition.widthWithChildren : barPosition.cpuWidth) + "px";
1158         this._expandElement._update(presentationRecord, index, barPosition.left - expandOffset, barPosition.width);
1159         this._record.setGraphRow(this);
1160     },
1161
1162     /**
1163      * @param {?Event} event
1164      */
1165     _onClick: function(event)
1166     {
1167         // check if we click arrow and expand if yes.
1168         if (this._expandElement._arrow.containsEventPoint(event))
1169             this._expand();
1170         this._selectRecord(this._record);
1171     },
1172
1173     /**
1174      * @param {boolean} selected
1175      */
1176     renderAsSelected: function(selected)
1177     {
1178         this.element.classList.toggle("selected", selected);
1179     },
1180
1181     _expand: function()
1182     {
1183         this._record.setCollapsed(!this._record.collapsed());
1184         this._scheduleRefresh();
1185     },
1186
1187     /**
1188      * @param {?Event} event
1189      */
1190     _onMouseOver: function(event)
1191     {
1192         this.element.classList.add("hovered");
1193         if (this._record.listRow())
1194             this._record.listRow().element.classList.add("hovered");
1195     },
1196
1197     /**
1198      * @param {?Event} event
1199      */
1200     _onMouseOut: function(event)
1201     {
1202         this.element.classList.remove("hovered");
1203         if (this._record.listRow())
1204             this._record.listRow().element.classList.remove("hovered");
1205     },
1206
1207     dispose: function()
1208     {
1209         this.element.remove();
1210         this._expandElement._dispose();
1211     }
1212 }
1213
1214 /**
1215  * @constructor
1216  */
1217 WebInspector.TimelineExpandableElement = function(container)
1218 {
1219     this._element = container.createChild("div", "timeline-expandable");
1220     this._element.createChild("div", "timeline-expandable-left");
1221     this._arrow = this._element.createChild("div", "timeline-expandable-arrow");
1222 }
1223
1224 WebInspector.TimelineExpandableElement.prototype = {
1225     /**
1226      * @param {!WebInspector.TimelinePresentationModel.Record} record
1227      */
1228     _update: function(record, index, left, width)
1229     {
1230         const rowHeight = WebInspector.TimelinePanel.rowHeight;
1231         if (record.visibleChildrenCount() || record.expandable()) {
1232             this._element.style.top = index * rowHeight + "px";
1233             this._element.style.left = left + "px";
1234             this._element.style.width = Math.max(12, width + 25) + "px";
1235             if (!record.collapsed()) {
1236                 this._element.style.height = (record.visibleChildrenCount() + 1) * rowHeight + "px";
1237                 this._element.classList.add("timeline-expandable-expanded");
1238                 this._element.classList.remove("timeline-expandable-collapsed");
1239             } else {
1240                 this._element.style.height = rowHeight + "px";
1241                 this._element.classList.add("timeline-expandable-collapsed");
1242                 this._element.classList.remove("timeline-expandable-expanded");
1243             }
1244             this._element.classList.remove("hidden");
1245         } else
1246             this._element.classList.add("hidden");
1247     },
1248
1249     _dispose: function()
1250     {
1251         this._element.remove();
1252     }
1253 }