• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
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.createElement("div");
154            this._frameContainer.classList.add("fill");
155            this._frameContainer.classList.add("timeline-frame-container");
156            this._frameContainer.style.height = WebInspector.TimelinePanel.rowHeight + frameContainerBorderWidth + "px";
157            this._frameContainer.addEventListener("dblclick", this._onFrameDoubleClicked.bind(this), false);
158            this._frameContainer.addEventListener("click", this._onFrameClicked.bind(this), false);
159        }
160        this._frameStripByFrame.clear();
161
162        var dividers = [];
163
164        for (var i = 0; i < frames.length; ++i) {
165            var frame = frames[i];
166            var frameStart = this._calculator.computePosition(frame.startTime);
167            var frameEnd = this._calculator.computePosition(frame.endTime);
168
169            var frameStrip = document.createElement("div");
170            frameStrip.className = "timeline-frame-strip";
171            var actualStart = Math.max(frameStart, 0);
172            var width = frameEnd - actualStart;
173            frameStrip.style.left = actualStart + "px";
174            frameStrip.style.width = width + "px";
175            frameStrip._frame = frame;
176            this._frameStripByFrame.put(frame, frameStrip);
177
178            const minWidthForFrameInfo = 60;
179            if (width > minWidthForFrameInfo)
180                frameStrip.textContent = Number.millisToString(frame.endTime - frame.startTime, true);
181
182            this._frameContainer.appendChild(frameStrip);
183
184            if (actualStart > 0) {
185                var frameMarker = this._uiUtils.createBeginFrameDivider();
186                frameMarker.style.left = frameStart + "px";
187                dividers.push(frameMarker);
188            }
189        }
190        this._timelineGrid.addEventDividers(dividers);
191        this._headerElement.appendChild(this._frameContainer);
192    },
193
194    _onFrameDoubleClicked: function(event)
195    {
196        var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip");
197        if (!frameBar)
198            return;
199        this._delegate.requestWindowTimes(frameBar._frame.startTime, frameBar._frame.endTime);
200    },
201
202    _onFrameClicked: function(event)
203    {
204        var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip");
205        if (!frameBar)
206            return;
207        this._delegate.select(WebInspector.TimelineSelection.fromFrame(frameBar._frame));
208    },
209
210    /**
211     * @param {!WebInspector.TimelineModel.Record} record
212     */
213    addRecord: function(record)
214    {
215        this._presentationModel.addRecord(record);
216        this._invalidateAndScheduleRefresh(false, false);
217    },
218
219    /**
220     * @param {number} width
221     */
222    setSidebarSize: function(width)
223    {
224        this._recordsView.setSidebarSize(width);
225    },
226
227    /**
228     * @param {!WebInspector.Event} event
229     */
230    _sidebarResized: function(event)
231    {
232        this.dispatchEventToListeners(WebInspector.SplitView.Events.SidebarSizeChanged, event.data);
233    },
234
235    _onViewportResize: function()
236    {
237        this._resize(this._recordsView.sidebarSize());
238    },
239
240    /**
241     * @param {number} sidebarWidth
242     */
243    _resize: function(sidebarWidth)
244    {
245        this._closeRecordDetails();
246        this._graphRowsElementWidth = this._graphRowsElement.offsetWidth;
247        this._headerElement.style.left = sidebarWidth + "px";
248        this._headerElement.style.width = this._itemsGraphsElement.offsetWidth + "px";
249        this._scheduleRefresh(false, true);
250    },
251
252    _resetView: function()
253    {
254        this._windowStartTime = 0;
255        this._windowEndTime = 0;
256        this._boundariesAreValid = false;
257        this._adjustScrollPosition(0);
258        this._linkifier.reset();
259        this._closeRecordDetails();
260        this._automaticallySizeWindow = true;
261        this._presentationModel.reset();
262    },
263
264
265    /**
266     * @return {!WebInspector.View}
267     */
268    view: function()
269    {
270        return this;
271    },
272
273    dispose: function()
274    {
275    },
276
277    reset: function()
278    {
279        this._resetView();
280        this._invalidateAndScheduleRefresh(true, true);
281    },
282
283    /**
284     * @return {!Array.<!Element>}
285     */
286    elementsToRestoreScrollPositionsFor: function()
287    {
288        return [this._containerElement];
289    },
290
291    /**
292     * @param {?RegExp} textFilter
293     */
294    refreshRecords: function(textFilter)
295    {
296        this._presentationModel.reset();
297        var records = this._model.records();
298        for (var i = 0; i < records.length; ++i)
299            this.addRecord(records[i]);
300        this._automaticallySizeWindow = false;
301        this._presentationModel.setTextFilter(textFilter);
302        this._invalidateAndScheduleRefresh(false, true);
303    },
304
305    willHide: function()
306    {
307        this._closeRecordDetails();
308        WebInspector.View.prototype.willHide.call(this);
309    },
310
311    _onScroll: function(event)
312    {
313        this._closeRecordDetails();
314        this._scrollTop = this._containerElement.scrollTop;
315        var dividersTop = Math.max(0, this._scrollTop);
316        this._timelineGrid.setScrollAndDividerTop(this._scrollTop, dividersTop);
317        this._scheduleRefresh(true, true);
318    },
319
320    /**
321     * @param {boolean} preserveBoundaries
322     * @param {boolean} userGesture
323     */
324    _invalidateAndScheduleRefresh: function(preserveBoundaries, userGesture)
325    {
326        this._presentationModel.invalidateFilteredRecords();
327        this._scheduleRefresh(preserveBoundaries, userGesture);
328    },
329
330    _clearSelection: function()
331    {
332        this._delegate.select(null);
333    },
334
335    /**
336     * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord
337     */
338    _selectRecord: function(presentationRecord)
339    {
340        if (presentationRecord.coalesced()) {
341            // Presentation record does not have model record to highlight.
342            this._innerSetSelectedRecord(presentationRecord);
343            var aggregatedStats = {};
344            var presentationChildren = presentationRecord.presentationChildren();
345            for (var i = 0; i < presentationChildren.length; ++i)
346                WebInspector.TimelineUIUtils.aggregateTimeByCategory(aggregatedStats, presentationChildren[i].record().aggregatedStats());
347            var idle = presentationRecord.record().endTime() - presentationRecord.record().startTime();
348            for (var category in aggregatedStats)
349                idle -= aggregatedStats[category];
350            aggregatedStats["idle"] = idle;
351            var pieChart = WebInspector.TimelineUIUtils.generatePieChart(aggregatedStats);
352            this._delegate.showInDetails(WebInspector.TimelineUIUtils.recordStyle(presentationRecord.record()).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._model.loadedFromFile(), this._uiUtils);
586                graphRowElement.row.update(record, this._calculator, this._expandOffset, i);
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 (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
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 = 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    _getPopoverAnchor: function(element)
714    {
715        var anchor = element.enclosingNodeOrSelfWithClass("timeline-graph-bar");
716        if (anchor && anchor._tasksInfo)
717            return anchor;
718        return null;
719    },
720
721    _mouseOut: function()
722    {
723        this._hideQuadHighlight();
724    },
725
726    /**
727     * @param {?Event} e
728     */
729    _mouseMove: function(e)
730    {
731        var rowElement = e.target.enclosingNodeOrSelfWithClass("timeline-tree-item");
732        if (!this._highlightQuad(rowElement))
733            this._hideQuadHighlight();
734
735        var taskBarElement = e.target.enclosingNodeOrSelfWithClass("timeline-graph-bar");
736        if (taskBarElement && taskBarElement._tasksInfo) {
737            var offset = taskBarElement.offsetLeft;
738            this._timelineGrid.showCurtains(offset >= 0 ? offset : 0, taskBarElement.offsetWidth);
739        } else
740            this._timelineGrid.hideCurtains();
741    },
742
743    /**
744     * @param {?Event} event
745     */
746    _keyDown: function(event)
747    {
748        if (!this._lastSelectedRecord || event.shiftKey || event.metaKey || event.ctrlKey)
749            return;
750
751        var record = this._lastSelectedRecord;
752        var recordsInWindow = this._presentationModel.filteredRecords();
753        var index = recordsInWindow.indexOf(record);
754        var recordsInPage = Math.floor(this._containerElementHeight / WebInspector.TimelinePanel.rowHeight);
755        var rowHeight = WebInspector.TimelinePanel.rowHeight;
756
757        if (index === -1)
758            index = 0;
759
760        switch (event.keyIdentifier) {
761        case "Left":
762            if (record.presentationParent()) {
763                if ((!record.expandable() || record.collapsed()) && record.presentationParent() !== this._presentationModel.rootRecord()) {
764                    this._selectRecord(record.presentationParent());
765                } else {
766                    record.setCollapsed(true);
767                    this._invalidateAndScheduleRefresh(true, true);
768                }
769            }
770            event.consume(true);
771            break;
772        case "Up":
773            if (--index < 0)
774                break;
775            this._selectRecord(recordsInWindow[index]);
776            event.consume(true);
777            break;
778        case "Right":
779            if (record.expandable() && record.collapsed()) {
780                record.setCollapsed(false);
781                this._invalidateAndScheduleRefresh(true, true);
782            } else {
783                if (++index >= recordsInWindow.length)
784                    break;
785                this._selectRecord(recordsInWindow[index]);
786            }
787            event.consume(true);
788            break;
789        case "Down":
790            if (++index >= recordsInWindow.length)
791                break;
792            this._selectRecord(recordsInWindow[index]);
793            event.consume(true);
794            break;
795        case "PageUp":
796            index = Math.max(0, index - recordsInPage);
797            this._scrollTop = Math.max(0, this._scrollTop - recordsInPage * rowHeight);
798            this._containerElement.scrollTop = this._scrollTop;
799            this._selectRecord(recordsInWindow[index]);
800            event.consume(true);
801            break;
802        case "PageDown":
803            index = Math.min(recordsInWindow.length - 1, index + recordsInPage);
804            this._scrollTop = Math.min(this._containerElement.scrollHeight - this._containerElementHeight, this._scrollTop + recordsInPage * rowHeight);
805            this._containerElement.scrollTop = this._scrollTop;
806            this._selectRecord(recordsInWindow[index]);
807            event.consume(true);
808            break;
809        case "Home":
810            index = 0;
811            this._selectRecord(recordsInWindow[index]);
812            event.consume(true);
813            break;
814        case "End":
815            index = recordsInWindow.length - 1;
816            this._selectRecord(recordsInWindow[index]);
817            event.consume(true);
818            break;
819        }
820    },
821
822    /**
823     * @param {?Element} rowElement
824     * @return {boolean}
825     */
826    _highlightQuad: function(rowElement)
827    {
828        if (!rowElement || !rowElement.row)
829            return false;
830        var presentationRecord = rowElement.row._record;
831        if (presentationRecord.coalesced())
832            return false;
833        var record = presentationRecord.record();
834        if (this._highlightedQuadRecord === record)
835            return true;
836        this._highlightedQuadRecord = record;
837
838        var quad = this._uiUtils.highlightQuadForRecord(record);
839        if (!quad)
840            return false;
841        record.target().domAgent().highlightQuad(quad, WebInspector.Color.PageHighlight.Content.toProtocolRGBA(), WebInspector.Color.PageHighlight.ContentOutline.toProtocolRGBA());
842        return true;
843    },
844
845    _hideQuadHighlight: function()
846    {
847        if (this._highlightedQuadRecord) {
848            this._highlightedQuadRecord.target().domAgent().hideHighlight();
849            delete this._highlightedQuadRecord;
850        }
851    },
852
853    /**
854     * @param {!Element} anchor
855     * @param {!WebInspector.Popover} popover
856     */
857    _showPopover: function(anchor, popover)
858    {
859        if (!anchor._tasksInfo)
860            return;
861        popover.show(WebInspector.TimelineUIUtils.generateMainThreadBarPopupContent(this._model, anchor._tasksInfo), anchor, null, null, WebInspector.Popover.Orientation.Bottom);
862    },
863
864    _closeRecordDetails: function()
865    {
866        this._popoverHelper.hidePopover();
867    },
868
869    /**
870     * @param {?WebInspector.TimelineModel.Record} record
871     * @param {string=} regex
872     * @param {boolean=} selectRecord
873     */
874    highlightSearchResult: function(record, regex, selectRecord)
875    {
876       if (this._highlightDomChanges)
877            WebInspector.revertDomChanges(this._highlightDomChanges);
878        this._highlightDomChanges = [];
879
880        var presentationRecord = this._presentationModel.toPresentationRecord(record);
881        if (!presentationRecord)
882            return;
883
884        if (selectRecord)
885            this._selectRecord(presentationRecord);
886
887        for (var element = this._sidebarListElement.firstChild; element; element = element.nextSibling) {
888            if (element.row._record === presentationRecord) {
889                element.row.highlight(regex, this._highlightDomChanges);
890                break;
891            }
892        }
893    },
894
895    __proto__: WebInspector.HBox.prototype
896}
897
898/**
899 * @constructor
900 * @param {!WebInspector.TimelineModel} model
901 * @implements {WebInspector.TimelineGrid.Calculator}
902 */
903WebInspector.TimelineCalculator = function(model)
904{
905    this._model = model;
906}
907
908WebInspector.TimelineCalculator._minWidth = 5;
909
910WebInspector.TimelineCalculator.prototype = {
911    /**
912     * @return {number}
913     */
914    paddingLeft: function()
915    {
916        return this._paddingLeft;
917    },
918
919    /**
920     * @param {number} time
921     * @return {number}
922     */
923    computePosition: function(time)
924    {
925        return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this._paddingLeft;
926    },
927
928    /**
929     * @param {!WebInspector.TimelinePresentationModel.Record} record
930     * @return {!{start: number, end: number, cpuWidth: number}}
931     */
932    computeBarGraphPercentages: function(record)
933    {
934        var start = (record.startTime() - this._minimumBoundary) / this.boundarySpan() * 100;
935        var end = (record.startTime() + record.selfTime() - this._minimumBoundary) / this.boundarySpan() * 100;
936        var cpuWidth = (record.endTime() - record.startTime()) / this.boundarySpan() * 100;
937        return {start: start, end: end, cpuWidth: cpuWidth};
938    },
939
940    /**
941     * @param {!WebInspector.TimelinePresentationModel.Record} record
942     * @return {!{left: number, width: number, cpuWidth: number}}
943     */
944    computeBarGraphWindowPosition: function(record)
945    {
946        var percentages = this.computeBarGraphPercentages(record);
947        var widthAdjustment = 0;
948
949        var left = this.computePosition(record.startTime());
950        var width = (percentages.end - percentages.start) / 100 * this._workingArea;
951        if (width < WebInspector.TimelineCalculator._minWidth) {
952            widthAdjustment = WebInspector.TimelineCalculator._minWidth - width;
953            width = WebInspector.TimelineCalculator._minWidth;
954        }
955        var cpuWidth = percentages.cpuWidth / 100 * this._workingArea + widthAdjustment;
956        return {left: left, width: width, cpuWidth: cpuWidth};
957    },
958
959    setWindow: function(minimumBoundary, maximumBoundary)
960    {
961        this._minimumBoundary = minimumBoundary;
962        this._maximumBoundary = maximumBoundary;
963    },
964
965    /**
966     * @param {number} paddingLeft
967     * @param {number} clientWidth
968     */
969    setDisplayWindow: function(paddingLeft, clientWidth)
970    {
971        this._workingArea = clientWidth - WebInspector.TimelineCalculator._minWidth - paddingLeft;
972        this._paddingLeft = paddingLeft;
973    },
974
975    /**
976     * @param {number} value
977     * @param {number=} precision
978     * @return {string}
979     */
980    formatTime: function(value, precision)
981    {
982        return Number.preciseMillisToString(value - this.zeroTime(), precision);
983    },
984
985    /**
986     * @return {number}
987     */
988    maximumBoundary: function()
989    {
990        return this._maximumBoundary;
991    },
992
993    /**
994     * @return {number}
995     */
996    minimumBoundary: function()
997    {
998        return this._minimumBoundary;
999    },
1000
1001    /**
1002     * @return {number}
1003     */
1004    zeroTime: function()
1005    {
1006        return this._model.minimumRecordTime();
1007    },
1008
1009    /**
1010     * @return {number}
1011     */
1012    boundarySpan: function()
1013    {
1014        return this._maximumBoundary - this._minimumBoundary;
1015    }
1016}
1017
1018/**
1019 * @constructor
1020 * @param {!WebInspector.Linkifier} linkifier
1021 * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
1022 * @param {function()} scheduleRefresh
1023 */
1024WebInspector.TimelineRecordListRow = function(linkifier, selectRecord, scheduleRefresh)
1025{
1026    this.element = document.createElement("div");
1027    this.element.row = this;
1028    this.element.style.cursor = "pointer";
1029    this.element.addEventListener("click", this._onClick.bind(this), false);
1030    this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
1031    this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false);
1032    this._linkifier = linkifier;
1033
1034    // Warning is float right block, it goes first.
1035    this._warningElement = this.element.createChild("div", "timeline-tree-item-warning hidden");
1036
1037    this._expandArrowElement = this.element.createChild("div", "timeline-tree-item-expand-arrow");
1038    this._expandArrowElement.addEventListener("click", this._onExpandClick.bind(this), false);
1039    var iconElement = this.element.createChild("span", "timeline-tree-icon");
1040    this._typeElement = this.element.createChild("span", "type");
1041
1042    this._dataElement = this.element.createChild("span", "data dimmed");
1043    this._scheduleRefresh = scheduleRefresh;
1044    this._selectRecord = selectRecord;
1045}
1046
1047WebInspector.TimelineRecordListRow.prototype = {
1048    /**
1049     * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
1050     * @param {number} offset
1051     * @param {boolean} loadedFromFile
1052     * @param {!WebInspector.TimelineUIUtils} uiUtils
1053     */
1054    update: function(presentationRecord, offset, loadedFromFile, uiUtils)
1055    {
1056        this._record = presentationRecord;
1057        var record = presentationRecord.record();
1058        this._offset = offset;
1059
1060        this.element.className = "timeline-tree-item timeline-category-" + record.category().name;
1061        var paddingLeft = 5;
1062        var step = -3;
1063        for (var currentRecord = presentationRecord.presentationParent() ? presentationRecord.presentationParent().presentationParent() : null; currentRecord; currentRecord = currentRecord.presentationParent())
1064            paddingLeft += 12 / (Math.max(1, step++));
1065        this.element.style.paddingLeft = paddingLeft + "px";
1066        if (record.thread())
1067            this.element.classList.add("background");
1068
1069        this._typeElement.textContent = uiUtils.titleForRecord(record);
1070
1071        if (this._dataElement.firstChild)
1072            this._dataElement.removeChildren();
1073
1074        this._warningElement.classList.toggle("hidden", !presentationRecord.hasWarnings() && !presentationRecord.childHasWarnings());
1075        this._warningElement.classList.toggle("timeline-tree-item-child-warning", presentationRecord.childHasWarnings() && !presentationRecord.hasWarnings());
1076
1077        if (presentationRecord.coalesced()) {
1078            this._dataElement.createTextChild(WebInspector.UIString("× %d", presentationRecord.presentationChildren().length));
1079        } else {
1080            var detailsNode = uiUtils.buildDetailsNode(record, this._linkifier, loadedFromFile);
1081            if (detailsNode) {
1082                this._dataElement.appendChild(document.createTextNode("("));
1083                this._dataElement.appendChild(detailsNode);
1084                this._dataElement.appendChild(document.createTextNode(")"));
1085            }
1086        }
1087
1088        this._expandArrowElement.classList.toggle("parent", presentationRecord.expandable());
1089        this._expandArrowElement.classList.toggle("expanded", !!presentationRecord.visibleChildrenCount());
1090        this._record.setListRow(this);
1091    },
1092
1093    highlight: function(regExp, domChanges)
1094    {
1095        var matchInfo = this.element.textContent.match(regExp);
1096        if (matchInfo)
1097            WebInspector.highlightSearchResult(this.element, matchInfo.index, matchInfo[0].length, domChanges);
1098    },
1099
1100    dispose: function()
1101    {
1102        this.element.remove();
1103    },
1104
1105    /**
1106     * @param {?Event} event
1107     */
1108    _onExpandClick: function(event)
1109    {
1110        this._record.setCollapsed(!this._record.collapsed());
1111        this._scheduleRefresh();
1112        event.consume(true);
1113    },
1114
1115    /**
1116     * @param {?Event} event
1117     */
1118    _onClick: function(event)
1119    {
1120        this._selectRecord(this._record);
1121    },
1122
1123    /**
1124     * @param {boolean} selected
1125     */
1126    renderAsSelected: function(selected)
1127    {
1128        this.element.classList.toggle("selected", selected);
1129    },
1130
1131    /**
1132     * @param {?Event} event
1133     */
1134    _onMouseOver: function(event)
1135    {
1136        this.element.classList.add("hovered");
1137        if (this._record.graphRow())
1138            this._record.graphRow().element.classList.add("hovered");
1139    },
1140
1141    /**
1142     * @param {?Event} event
1143     */
1144    _onMouseOut: function(event)
1145    {
1146        this.element.classList.remove("hovered");
1147    if (this._record.graphRow())
1148        this._record.graphRow().element.classList.remove("hovered");
1149    }
1150}
1151
1152/**
1153 * @constructor
1154 * @param {!Element} graphContainer
1155 * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
1156 * @param {function()} scheduleRefresh
1157 */
1158WebInspector.TimelineRecordGraphRow = function(graphContainer, selectRecord, scheduleRefresh)
1159{
1160    this.element = document.createElement("div");
1161    this.element.row = this;
1162    this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
1163    this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false);
1164    this.element.addEventListener("click", this._onClick.bind(this), false);
1165
1166    this._barAreaElement = document.createElement("div");
1167    this._barAreaElement.className = "timeline-graph-bar-area";
1168    this.element.appendChild(this._barAreaElement);
1169
1170    this._barCpuElement = document.createElement("div");
1171    this._barCpuElement.className = "timeline-graph-bar cpu"
1172    this._barCpuElement.row = this;
1173    this._barAreaElement.appendChild(this._barCpuElement);
1174
1175    this._barElement = document.createElement("div");
1176    this._barElement.className = "timeline-graph-bar";
1177    this._barElement.row = this;
1178    this._barAreaElement.appendChild(this._barElement);
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     */
1193    update: function(presentationRecord, calculator, expandOffset, index)
1194    {
1195        this._record = presentationRecord;
1196        var record = presentationRecord.record();
1197        this.element.className = "timeline-graph-side timeline-category-" + record.category().name;
1198        if (record.thread())
1199            this.element.classList.add("background");
1200
1201        var barPosition = calculator.computeBarGraphWindowPosition(presentationRecord);
1202        this._barElement.style.left = barPosition.left + "px";
1203        this._barElement.style.width = barPosition.width + "px";
1204        this._barCpuElement.style.left = barPosition.left + "px";
1205        this._barCpuElement.style.width = barPosition.cpuWidth + "px";
1206        this._expandElement._update(presentationRecord, index, barPosition.left - expandOffset, barPosition.width);
1207        this._record.setGraphRow(this);
1208    },
1209
1210    /**
1211     * @param {?Event} event
1212     */
1213    _onClick: function(event)
1214    {
1215        // check if we click arrow and expand if yes.
1216        if (this._expandElement._arrow.containsEventPoint(event))
1217            this._expand();
1218        this._selectRecord(this._record);
1219    },
1220
1221    /**
1222     * @param {boolean} selected
1223     */
1224    renderAsSelected: function(selected)
1225    {
1226        this.element.classList.toggle("selected", selected);
1227    },
1228
1229    _expand: function()
1230    {
1231        this._record.setCollapsed(!this._record.collapsed());
1232        this._scheduleRefresh();
1233    },
1234
1235    /**
1236     * @param {?Event} event
1237     */
1238    _onMouseOver: function(event)
1239    {
1240        this.element.classList.add("hovered");
1241        if (this._record.listRow())
1242            this._record.listRow().element.classList.add("hovered");
1243    },
1244
1245    /**
1246     * @param {?Event} event
1247     */
1248    _onMouseOut: function(event)
1249    {
1250        this.element.classList.remove("hovered");
1251        if (this._record.listRow())
1252            this._record.listRow().element.classList.remove("hovered");
1253    },
1254
1255    dispose: function()
1256    {
1257        this.element.remove();
1258        this._expandElement._dispose();
1259    }
1260}
1261
1262/**
1263 * @constructor
1264 */
1265WebInspector.TimelineExpandableElement = function(container)
1266{
1267    this._element = container.createChild("div", "timeline-expandable");
1268    this._element.createChild("div", "timeline-expandable-left");
1269    this._arrow = this._element.createChild("div", "timeline-expandable-arrow");
1270}
1271
1272WebInspector.TimelineExpandableElement.prototype = {
1273    /**
1274     * @param {!WebInspector.TimelinePresentationModel.Record} record
1275     * @param {number} index
1276     * @param {number} left
1277     * @param {number} width
1278     */
1279    _update: function(record, index, left, width)
1280    {
1281        const rowHeight = WebInspector.TimelinePanel.rowHeight;
1282        if (record.visibleChildrenCount() || record.expandable()) {
1283            this._element.style.top = index * rowHeight + "px";
1284            this._element.style.left = left + "px";
1285            this._element.style.width = Math.max(12, width + 25) + "px";
1286            if (!record.collapsed()) {
1287                this._element.style.height = (record.visibleChildrenCount() + 1) * rowHeight + "px";
1288                this._element.classList.add("timeline-expandable-expanded");
1289                this._element.classList.remove("timeline-expandable-collapsed");
1290            } else {
1291                this._element.style.height = rowHeight + "px";
1292                this._element.classList.add("timeline-expandable-collapsed");
1293                this._element.classList.remove("timeline-expandable-expanded");
1294            }
1295            this._element.classList.remove("hidden");
1296        } else
1297            this._element.classList.add("hidden");
1298    },
1299
1300    _dispose: function()
1301    {
1302        this._element.remove();
1303    }
1304}
1305