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