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