Home | History | Annotate | Download | only in timeline
      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  */
     40 WebInspector.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 
     76 WebInspector.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  */
    903 WebInspector.TimelineCalculator = function(model)
    904 {
    905     this._model = model;
    906 }
    907 
    908 WebInspector.TimelineCalculator._minWidth = 5;
    909 
    910 WebInspector.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  */
   1024 WebInspector.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 
   1047 WebInspector.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  */
   1158 WebInspector.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 
   1186 WebInspector.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  */
   1265 WebInspector.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 
   1272 WebInspector.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