| /** | 
 |  * Copyright (C) 2013 Google Inc. All rights reserved. | 
 |  * | 
 |  * Redistribution and use in source and binary forms, with or without | 
 |  * modification, are permitted provided that the following conditions are | 
 |  * met: | 
 |  * | 
 |  *     * Redistributions of source code must retain the above copyright | 
 |  * notice, this list of conditions and the following disclaimer. | 
 |  *     * Redistributions in binary form must reproduce the above | 
 |  * copyright notice, this list of conditions and the following disclaimer | 
 |  * in the documentation and/or other materials provided with the | 
 |  * distribution. | 
 |  *     * Neither the name of Google Inc. nor the names of its | 
 |  * contributors may be used to endorse or promote products derived from | 
 |  * this software without specific prior written permission. | 
 |  * | 
 |  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | 
 |  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | 
 |  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
 |  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | 
 |  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
 |  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | 
 |  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
 |  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
 |  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 
 |  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
 |  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
 |  */ | 
 |  | 
 | /** | 
 |  * @interface | 
 |  */ | 
 | PerfUI.FlameChartDelegate = function() {}; | 
 |  | 
 | PerfUI.FlameChartDelegate.prototype = { | 
 |   /** | 
 |    * @param {number} startTime | 
 |    * @param {number} endTime | 
 |    * @param {boolean} animate | 
 |    */ | 
 |   windowChanged(startTime, endTime, animate) {}, | 
 |  | 
 |   /** | 
 |    * @param {number} startTime | 
 |    * @param {number} endTime | 
 |    */ | 
 |   updateRangeSelection(startTime, endTime) {}, | 
 |  | 
 |   /** | 
 |    * @param {!PerfUI.FlameChart} flameChart | 
 |    * @param {?PerfUI.FlameChart.Group} group | 
 |    */ | 
 |   updateSelectedGroup(flameChart, group) {}, | 
 | }; | 
 |  | 
 | /** | 
 |  * @unrestricted | 
 |  * @implements {PerfUI.TimelineGrid.Calculator} | 
 |  * @implements {PerfUI.ChartViewportDelegate} | 
 |  */ | 
 | PerfUI.FlameChart = class extends UI.VBox { | 
 |   /** | 
 |    * @param {!PerfUI.FlameChartDataProvider} dataProvider | 
 |    * @param {!PerfUI.FlameChartDelegate} flameChartDelegate | 
 |    * @param {!Common.Setting=} groupExpansionSetting | 
 |    */ | 
 |   constructor(dataProvider, flameChartDelegate, groupExpansionSetting) { | 
 |     super(true); | 
 |     this.registerRequiredCSS('perf_ui/flameChart.css'); | 
 |     this.contentElement.classList.add('flame-chart-main-pane'); | 
 |     this._groupExpansionSetting = groupExpansionSetting; | 
 |     this._groupExpansionState = groupExpansionSetting && groupExpansionSetting.get() || {}; | 
 |     this._flameChartDelegate = flameChartDelegate; | 
 |  | 
 |     this._useWebGL = Root.Runtime.experiments.isEnabled('timelineWebGL'); | 
 |     this._chartViewport = new PerfUI.ChartViewport(this); | 
 |     this._chartViewport.show(this.contentElement); | 
 |  | 
 |     this._dataProvider = dataProvider; | 
 |  | 
 |     this._viewportElement = this._chartViewport.viewportElement; | 
 |     if (this._useWebGL) { | 
 |       this._canvasGL = /** @type {!HTMLCanvasElement} */ (this._viewportElement.createChild('canvas', 'fill')); | 
 |       this._initWebGL(); | 
 |     } | 
 |     this._canvas = /** @type {!HTMLCanvasElement} */ (this._viewportElement.createChild('canvas', 'fill')); | 
 |  | 
 |     this._canvas.tabIndex = 0; | 
 |     UI.ARIAUtils.setAccessibleName(this._canvas, ls`Flame Chart`); | 
 |     UI.ARIAUtils.markAsTree(this._canvas); | 
 |     this.setDefaultFocusedElement(this._canvas); | 
 |     this._canvas.classList.add('flame-chart-canvas'); | 
 |     this._canvas.addEventListener('mousemove', this._onMouseMove.bind(this), false); | 
 |     this._canvas.addEventListener('mouseout', this._onMouseOut.bind(this), false); | 
 |     this._canvas.addEventListener('click', this._onClick.bind(this), false); | 
 |     this._canvas.addEventListener('keydown', this._onKeyDown.bind(this), false); | 
 |  | 
 |     this._entryInfo = this._viewportElement.createChild('div', 'flame-chart-entry-info'); | 
 |     this._markerHighlighElement = this._viewportElement.createChild('div', 'flame-chart-marker-highlight-element'); | 
 |     this._highlightElement = this._viewportElement.createChild('div', 'flame-chart-highlight-element'); | 
 |     this._selectedElement = this._viewportElement.createChild('div', 'flame-chart-selected-element'); | 
 |     this._canvas.addEventListener('focus', () => { | 
 |       this._selectedElement.classList.remove('flame-chart-unfocused-selected-element'); | 
 |       this.dispatchEventToListeners(PerfUI.FlameChart.Events.CanvasFocused); | 
 |     }, false); | 
 |     this._canvas.addEventListener('blur', () => { | 
 |       this._selectedElement.classList.add('flame-chart-unfocused-selected-element'); | 
 |     }, false); | 
 |  | 
 |     UI.installDragHandle( | 
 |         this._viewportElement, this._startDragging.bind(this), this._dragging.bind(this), this._endDragging.bind(this), | 
 |         null); | 
 |  | 
 |     this._rulerEnabled = true; | 
 |     this._rangeSelectionStart = 0; | 
 |     this._rangeSelectionEnd = 0; | 
 |     this._barHeight = 17; | 
 |     this._textBaseline = 5; | 
 |     this._textPadding = 5; | 
 |     this._markerRadius = 6; | 
 |     this._chartViewport.setWindowTimes( | 
 |         dataProvider.minimumBoundary(), dataProvider.minimumBoundary() + dataProvider.totalTime()); | 
 |  | 
 |     /** @const */ | 
 |     this._headerLeftPadding = 6; | 
 |     /** @const */ | 
 |     this._arrowSide = 8; | 
 |     /** @const */ | 
 |     this._expansionArrowIndent = this._headerLeftPadding + this._arrowSide / 2; | 
 |     /** @const */ | 
 |     this._headerLabelXPadding = 3; | 
 |     /** @const */ | 
 |     this._headerLabelYPadding = 2; | 
 |  | 
 |     this._highlightedMarkerIndex = -1; | 
 |     this._highlightedEntryIndex = -1; | 
 |     this._selectedEntryIndex = -1; | 
 |     this._rawTimelineDataLength = 0; | 
 |     /** @type {!Map<string, !Map<string,number>>} */ | 
 |     this._textWidth = new Map(); | 
 |     /** @type {!Map<number, !{x: number, width: number}>} */ | 
 |     this._markerPositions = new Map(); | 
 |  | 
 |     this._lastMouseOffsetX = 0; | 
 |     this._selectedGroup = -1; | 
 |  | 
 |     // Keyboard focused group is used to navigate groups irrespective of whether they are selectable or not | 
 |     this._keyboardFocusedGroup = -1; | 
 |  | 
 |     this._selectedGroupBackroundColor = UI.themeSupport.patchColorText( | 
 |         PerfUI.FlameChart.Colors.SelectedGroupBackground, UI.ThemeSupport.ColorUsage.Background); | 
 |     this._selectedGroupBorderColor = UI.themeSupport.patchColorText( | 
 |         PerfUI.FlameChart.Colors.SelectedGroupBorder, UI.ThemeSupport.ColorUsage.Background); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    */ | 
 |   willHide() { | 
 |     this.hideHighlight(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} value | 
 |    */ | 
 |   setBarHeight(value) { | 
 |     this._barHeight = value; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} value | 
 |    */ | 
 |   setTextBaseline(value) { | 
 |     this._textBaseline = value; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} value | 
 |    */ | 
 |   setTextPadding(value) { | 
 |     this._textPadding = value; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {boolean} enable | 
 |    */ | 
 |   enableRuler(enable) { | 
 |     this._rulerEnabled = enable; | 
 |   } | 
 |  | 
 |   alwaysShowVerticalScroll() { | 
 |     this._chartViewport.alwaysShowVerticalScroll(); | 
 |   } | 
 |  | 
 |   disableRangeSelection() { | 
 |     this._chartViewport.disableRangeSelection(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    */ | 
 |   highlightEntry(entryIndex) { | 
 |     if (this._highlightedEntryIndex === entryIndex) { | 
 |       return; | 
 |     } | 
 |     if (!this._dataProvider.entryColor(entryIndex)) { | 
 |       return; | 
 |     } | 
 |     this._highlightedEntryIndex = entryIndex; | 
 |     this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex); | 
 |     this.dispatchEventToListeners(PerfUI.FlameChart.Events.EntryHighlighted, entryIndex); | 
 |   } | 
 |  | 
 |   hideHighlight() { | 
 |     this._entryInfo.removeChildren(); | 
 |     this._highlightedEntryIndex = -1; | 
 |     this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex); | 
 |     this.dispatchEventToListeners(PerfUI.FlameChart.Events.EntryHighlighted, -1); | 
 |   } | 
 |  | 
 |   _resetCanvas() { | 
 |     const ratio = window.devicePixelRatio; | 
 |     const width = Math.round(this._offsetWidth * ratio); | 
 |     const height = Math.round(this._offsetHeight * ratio); | 
 |     this._canvas.width = width; | 
 |     this._canvas.height = height; | 
 |     this._canvas.style.width = `${width / ratio}px`; | 
 |     this._canvas.style.height = `${height / ratio}px`; | 
 |     if (this._useWebGL) { | 
 |       this._canvasGL.width = width; | 
 |       this._canvasGL.height = height; | 
 |       this._canvasGL.style.width = `${width / ratio}px`; | 
 |       this._canvasGL.style.height = `${height / ratio}px`; | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @param {number} startTime | 
 |    * @param {number} endTime | 
 |    * @param {boolean} animate | 
 |    */ | 
 |   windowChanged(startTime, endTime, animate) { | 
 |     this._flameChartDelegate.windowChanged(startTime, endTime, animate); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @param {number} startTime | 
 |    * @param {number} endTime | 
 |    */ | 
 |   updateRangeSelection(startTime, endTime) { | 
 |     this._flameChartDelegate.updateRangeSelection(startTime, endTime); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @param {number} width | 
 |    * @param {number} height | 
 |    */ | 
 |   setSize(width, height) { | 
 |     this._offsetWidth = width; | 
 |     this._offsetHeight = height; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!MouseEvent} event | 
 |    */ | 
 |   _startDragging(event) { | 
 |     this.hideHighlight(); | 
 |     this._maxDragOffset = 0; | 
 |     this._dragStartX = event.pageX; | 
 |     this._dragStartY = event.pageY; | 
 |     return true; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!MouseEvent} event | 
 |    */ | 
 |   _dragging(event) { | 
 |     const dx = event.pageX - this._dragStartX; | 
 |     const dy = event.pageY - this._dragStartY; | 
 |     this._maxDragOffset = Math.max(this._maxDragOffset, Math.sqrt(dx * dx + dy * dy)); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!MouseEvent} event | 
 |    */ | 
 |   _endDragging(event) { | 
 |     this._updateHighlight(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @return {?PerfUI.FlameChart.TimelineData} | 
 |    */ | 
 |   _timelineData() { | 
 |     if (!this._dataProvider) { | 
 |       return null; | 
 |     } | 
 |     const timelineData = this._dataProvider.timelineData(); | 
 |     if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength) { | 
 |       this._processTimelineData(timelineData); | 
 |     } | 
 |     return this._rawTimelineData; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    */ | 
 |   _revealEntry(entryIndex) { | 
 |     const timelineData = this._timelineData(); | 
 |     if (!timelineData) { | 
 |       return; | 
 |     } | 
 |     const timeLeft = this._chartViewport.windowLeftTime(); | 
 |     const timeRight = this._chartViewport.windowRightTime(); | 
 |     const entryStartTime = timelineData.entryStartTimes[entryIndex]; | 
 |     const entryTotalTime = timelineData.entryTotalTimes[entryIndex]; | 
 |     const entryEndTime = entryStartTime + entryTotalTime; | 
 |     let minEntryTimeWindow = Math.min(entryTotalTime, timeRight - timeLeft); | 
 |  | 
 |     const level = timelineData.entryLevels[entryIndex]; | 
 |     this._chartViewport.setScrollOffset(this._levelToOffset(level), this._levelHeight(level)); | 
 |  | 
 |     const minVisibleWidthPx = 30; | 
 |     const futurePixelToTime = (timeRight - timeLeft) / this._offsetWidth; | 
 |     minEntryTimeWindow = Math.max(minEntryTimeWindow, futurePixelToTime * minVisibleWidthPx); | 
 |     if (timeLeft > entryEndTime) { | 
 |       const delta = timeLeft - entryEndTime + minEntryTimeWindow; | 
 |       this.windowChanged(timeLeft - delta, timeRight - delta, /* animate */ true); | 
 |     } else if (timeRight < entryStartTime) { | 
 |       const delta = entryStartTime - timeRight + minEntryTimeWindow; | 
 |       this.windowChanged(timeLeft + delta, timeRight + delta, /* animate */ true); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} startTime | 
 |    * @param {number} endTime | 
 |    * @param {boolean=} animate | 
 |    */ | 
 |   setWindowTimes(startTime, endTime, animate) { | 
 |     this._chartViewport.setWindowTimes(startTime, endTime, animate); | 
 |     this._updateHighlight(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!Event} event | 
 |    */ | 
 |   _onMouseMove(event) { | 
 |     this._lastMouseOffsetX = event.offsetX; | 
 |     this._lastMouseOffsetY = event.offsetY; | 
 |     if (!this._enabled()) { | 
 |       return; | 
 |     } | 
 |     if (this._chartViewport.isDragging()) { | 
 |       return; | 
 |     } | 
 |     if (this._coordinatesToGroupIndex(event.offsetX, event.offsetY, true /* headerOnly */) >= 0) { | 
 |       this.hideHighlight(); | 
 |       this._viewportElement.style.cursor = 'pointer'; | 
 |       return; | 
 |     } | 
 |     this._updateHighlight(); | 
 |   } | 
 |  | 
 |   _updateHighlight() { | 
 |     const entryIndex = this._coordinatesToEntryIndex(this._lastMouseOffsetX, this._lastMouseOffsetY); | 
 |     if (entryIndex === -1) { | 
 |       this.hideHighlight(); | 
 |       const group = | 
 |           this._coordinatesToGroupIndex(this._lastMouseOffsetX, this._lastMouseOffsetY, false /* headerOnly */); | 
 |       if (group >= 0 && this._rawTimelineData.groups[group].selectable) { | 
 |         this._viewportElement.style.cursor = 'pointer'; | 
 |       } else { | 
 |         this._viewportElement.style.cursor = 'default'; | 
 |       } | 
 |       return; | 
 |     } | 
 |     if (this._chartViewport.isDragging()) { | 
 |       return; | 
 |     } | 
 |     this._updatePopover(entryIndex); | 
 |     this._viewportElement.style.cursor = this._dataProvider.canJumpToEntry(entryIndex) ? 'pointer' : 'default'; | 
 |     this.highlightEntry(entryIndex); | 
 |   } | 
 |  | 
 |   _onMouseOut() { | 
 |     this._lastMouseOffsetX = -1; | 
 |     this._lastMouseOffsetY = -1; | 
 |     this.hideHighlight(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    */ | 
 |   _updatePopover(entryIndex) { | 
 |     if (entryIndex === this._highlightedEntryIndex) { | 
 |       this._updatePopoverOffset(); | 
 |       return; | 
 |     } | 
 |     this._entryInfo.removeChildren(); | 
 |     const popoverElement = this._dataProvider.prepareHighlightedEntryInfo(entryIndex); | 
 |     if (popoverElement) { | 
 |       this._entryInfo.appendChild(popoverElement); | 
 |       this._updatePopoverOffset(); | 
 |     } | 
 |   } | 
 |  | 
 |   _updatePopoverOffset() { | 
 |     const mouseX = this._lastMouseOffsetX; | 
 |     const mouseY = this._lastMouseOffsetY; | 
 |     const parentWidth = this._entryInfo.parentElement.clientWidth; | 
 |     const parentHeight = this._entryInfo.parentElement.clientHeight; | 
 |     const infoWidth = this._entryInfo.clientWidth; | 
 |     const infoHeight = this._entryInfo.clientHeight; | 
 |     const /** @const */ offsetX = 10; | 
 |     const /** @const */ offsetY = 6; | 
 |     let x; | 
 |     let y; | 
 |     for (let quadrant = 0; quadrant < 4; ++quadrant) { | 
 |       const dx = quadrant & 2 ? -offsetX - infoWidth : offsetX; | 
 |       const dy = quadrant & 1 ? -offsetY - infoHeight : offsetY; | 
 |       x = Number.constrain(mouseX + dx, 0, parentWidth - infoWidth); | 
 |       y = Number.constrain(mouseY + dy, 0, parentHeight - infoHeight); | 
 |       if (x >= mouseX || mouseX >= x + infoWidth || y >= mouseY || mouseY >= y + infoHeight) { | 
 |         break; | 
 |       } | 
 |     } | 
 |     this._entryInfo.style.left = x + 'px'; | 
 |     this._entryInfo.style.top = y + 'px'; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!Event} event | 
 |    */ | 
 |   _onClick(event) { | 
 |     this.focus(); | 
 |     // onClick comes after dragStart and dragEnd events. | 
 |     // So if there was drag (mouse move) in the middle of that events | 
 |     // we skip the click. Otherwise we jump to the sources. | 
 |     const /** @const */ clickThreshold = 5; | 
 |     if (this._maxDragOffset > clickThreshold) { | 
 |       return; | 
 |     } | 
 |  | 
 |     this._selectGroup(this._coordinatesToGroupIndex(event.offsetX, event.offsetY, false /* headerOnly */)); | 
 |     this._toggleGroupExpand(this._coordinatesToGroupIndex(event.offsetX, event.offsetY, true /* headerOnly */)); | 
 |     const timelineData = this._timelineData(); | 
 |     if (event.shiftKey && this._highlightedEntryIndex !== -1 && timelineData) { | 
 |       const start = timelineData.entryStartTimes[this._highlightedEntryIndex]; | 
 |       const end = start + timelineData.entryTotalTimes[this._highlightedEntryIndex]; | 
 |       this._chartViewport.setRangeSelection(start, end); | 
 |     } else { | 
 |       this._chartViewport.onClick(event); | 
 |       this.dispatchEventToListeners(PerfUI.FlameChart.Events.EntryInvoked, this._highlightedEntryIndex); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} groupIndex | 
 |    */ | 
 |   _selectGroup(groupIndex) { | 
 |     if (groupIndex < 0 || this._selectedGroup === groupIndex) { | 
 |       return; | 
 |     } | 
 |     const groups = this._rawTimelineData.groups; | 
 |     this._keyboardFocusedGroup = groupIndex; | 
 |     if (!groups[groupIndex].selectable) { | 
 |       this._deselectAllGroups(); | 
 |     } else { | 
 |       this._selectedGroup = groupIndex; | 
 |       this._flameChartDelegate.updateSelectedGroup(this, groups[groupIndex]); | 
 |       this._resetCanvas(); | 
 |       this._draw(); | 
 |  | 
 |       const groupName = groups[groupIndex].name; | 
 |       UI.ARIAUtils.alert(ls`${groupName} selected`, this._canvas); | 
 |     } | 
 |   } | 
 |  | 
 |   _deselectAllGroups() { | 
 |     this._selectedGroup = -1; | 
 |     this._flameChartDelegate.updateSelectedGroup(this, null); | 
 |     this._resetCanvas(); | 
 |     this._draw(); | 
 |   } | 
 |  | 
 |   _deselectAllEntries() { | 
 |     this._selectedEntryIndex = -1; | 
 |     this._resetCanvas(); | 
 |     this._draw(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} index | 
 |    */ | 
 |   _isGroupFocused(index) { | 
 |     return index === this._selectedGroup || index === this._keyboardFocusedGroup; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} groupIndex | 
 |    */ | 
 |   _toggleGroupExpand(groupIndex) { | 
 |     if (groupIndex < 0 || !this._isGroupCollapsible(groupIndex)) { | 
 |       return; | 
 |     } | 
 |  | 
 |     this._expandGroup(groupIndex, !this._rawTimelineData.groups[groupIndex].expanded /* setExpanded */); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} groupIndex | 
 |    * @param {boolean=} setExpanded | 
 |    * @param {boolean=} propagatedExpand | 
 |    */ | 
 |   _expandGroup(groupIndex, setExpanded = true, propagatedExpand = false) { | 
 |     if (groupIndex < 0 || !this._isGroupCollapsible(groupIndex)) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const groups = this._rawTimelineData.groups; | 
 |     const group = groups[groupIndex]; | 
 |     group.expanded = setExpanded; | 
 |  | 
 |     this._groupExpansionState[group.name] = group.expanded; | 
 |     if (this._groupExpansionSetting) { | 
 |       this._groupExpansionSetting.set(this._groupExpansionState); | 
 |     } | 
 |     this._updateLevelPositions(); | 
 |  | 
 |     this._updateHighlight(); | 
 |     if (!group.expanded) { | 
 |       const timelineData = this._timelineData(); | 
 |       const level = timelineData.entryLevels[this._selectedEntryIndex]; | 
 |       if (this._selectedEntryIndex >= 0 && level >= group.startLevel && | 
 |           (groupIndex >= groups.length - 1 || groups[groupIndex + 1].startLevel > level)) { | 
 |         this._selectedEntryIndex = -1; | 
 |       } | 
 |     } | 
 |  | 
 |     this._updateHeight(); | 
 |     this._resetCanvas(); | 
 |     this._draw(); | 
 |  | 
 |     // We only want to read expanded/collapsed state on user inputted expand/collapse | 
 |     if (!propagatedExpand) { | 
 |       const groupName = groups[groupIndex].name; | 
 |       const content = group.expanded ? ls`${groupName} expanded` : ls`${groupName} collapsed`; | 
 |       UI.ARIAUtils.alert(content, this._canvas); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!Event} e | 
 |    */ | 
 |   _onKeyDown(e) { | 
 |     if (!UI.KeyboardShortcut.hasNoModifiers(e) || !this._timelineData()) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const eventHandled = this._handleSelectionNavigation(e); | 
 |  | 
 |     // Handle keyboard navigation in groups | 
 |     if (!eventHandled && this._rawTimelineData && this._rawTimelineData.groups) { | 
 |       this._handleKeyboardGroupNavigation(e); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!Event} e | 
 |    */ | 
 |   _handleKeyboardGroupNavigation(e) { | 
 |     let handled = false; | 
 |     let entrySelected = false; | 
 |  | 
 |     if (e.code === 'ArrowUp') { | 
 |       handled = this._selectPreviousGroup(); | 
 |     } else if (e.code === 'ArrowDown') { | 
 |       handled = this._selectNextGroup(); | 
 |     } else if (e.code === 'ArrowLeft') { | 
 |       if (this._keyboardFocusedGroup >= 0) { | 
 |         this._expandGroup(this._keyboardFocusedGroup, false /* setExpanded */); | 
 |         handled = true; | 
 |       } | 
 |     } else if (e.code === 'ArrowRight') { | 
 |       if (this._keyboardFocusedGroup >= 0) { | 
 |         this._expandGroup(this._keyboardFocusedGroup, true /* setExpanded */); | 
 |         this._selectFirstChild(); | 
 |         handled = true; | 
 |       } | 
 |     } else if (isEnterKey(e)) { | 
 |       entrySelected = this._selectFirstEntryInCurrentGroup(); | 
 |       handled = entrySelected; | 
 |     } | 
 |  | 
 |     if (handled && !entrySelected) { | 
 |       this._deselectAllEntries(); | 
 |     } | 
 |  | 
 |     if (handled) { | 
 |       e.consume(true); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @return {boolean} | 
 |    */ | 
 |   _selectFirstEntryInCurrentGroup() { | 
 |     const allGroups = this._rawTimelineData.groups; | 
 |  | 
 |     if (this._keyboardFocusedGroup < 0) { | 
 |       return false; | 
 |     } | 
 |  | 
 |     const group = allGroups[this._keyboardFocusedGroup]; | 
 |     const startLevelInGroup = group.startLevel; | 
 |  | 
 |     // Return if no levels in this group | 
 |     if (startLevelInGroup < 0) { | 
 |       return false; | 
 |     } | 
 |  | 
 |     // Make sure this is the innermost nested group with this startLevel | 
 |     // This is because a parent group also contains levels of all its child groups | 
 |     // So check if the next group has the same level, if it does, user should | 
 |     // go to that child group to select this entry | 
 |     if (this._keyboardFocusedGroup < allGroups.length - 1 && | 
 |         allGroups[this._keyboardFocusedGroup + 1].startLevel === startLevelInGroup) { | 
 |       return false; | 
 |     } | 
 |  | 
 |  | 
 |     // Get first (default) entry in startLevel of selected group | 
 |     const firstEntryIndex = this._timelineLevels[startLevelInGroup][0]; | 
 |  | 
 |     this._expandGroup(this._keyboardFocusedGroup, true /* setExpanded */); | 
 |     this.setSelectedEntry(firstEntryIndex); | 
 |     return true; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @return {boolean} | 
 |    */ | 
 |   _selectPreviousGroup() { | 
 |     if (this._keyboardFocusedGroup <= 0) { | 
 |       return false; | 
 |     } | 
 |  | 
 |     const groupIndexToSelect = this._getGroupIndexToSelect(-1 /* offset */); | 
 |     this._selectGroup(groupIndexToSelect); | 
 |     return true; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @return {boolean} | 
 |    */ | 
 |   _selectNextGroup() { | 
 |     if (this._keyboardFocusedGroup >= this._rawTimelineData.groups.length - 1) { | 
 |       return false; | 
 |     } | 
 |  | 
 |     const groupIndexToSelect = this._getGroupIndexToSelect(1 /* offset */); | 
 |     this._selectGroup(groupIndexToSelect); | 
 |     return true; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} offset | 
 |    * @return {number} | 
 |    */ | 
 |   _getGroupIndexToSelect(offset) { | 
 |     const allGroups = this._rawTimelineData.groups; | 
 |     let groupIndexToSelect = this._keyboardFocusedGroup; | 
 |     let groupName, groupWithSubNestingLevel; | 
 |  | 
 |     do { | 
 |       groupIndexToSelect += offset; | 
 |       groupName = this._rawTimelineData.groups[groupIndexToSelect].name; | 
 |       groupWithSubNestingLevel = this._keyboardFocusedGroup !== -1 && | 
 |           allGroups[groupIndexToSelect].style.nestingLevel > allGroups[this._keyboardFocusedGroup].style.nestingLevel; | 
 |     } while (groupIndexToSelect > 0 && groupIndexToSelect < allGroups.length - 1 && | 
 |              (!groupName || groupWithSubNestingLevel)); | 
 |  | 
 |     return groupIndexToSelect; | 
 |   } | 
 |  | 
 |   _selectFirstChild() { | 
 |     const allGroups = this._rawTimelineData.groups; | 
 |     if (this._keyboardFocusedGroup < 0 || this._keyboardFocusedGroup >= allGroups.length - 1) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const groupIndexToSelect = this._keyboardFocusedGroup + 1; | 
 |     if (allGroups[groupIndexToSelect].style.nestingLevel > allGroups[this._keyboardFocusedGroup].style.nestingLevel) { | 
 |       this._selectGroup(groupIndexToSelect); | 
 |       this._expandGroup(groupIndexToSelect, true /* setExpanded */, true /* propagatedExpand */); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!Event} e | 
 |    * @return {boolean} | 
 |    */ | 
 |   _handleSelectionNavigation(e) { | 
 |     if (this._selectedEntryIndex === -1) { | 
 |       return false; | 
 |     } | 
 |     const timelineData = this._timelineData(); | 
 |     if (!timelineData) { | 
 |       return false; | 
 |     } | 
 |  | 
 |     /** | 
 |      * @param {number} time | 
 |      * @param {number} entryIndex | 
 |      * @return {number} | 
 |      */ | 
 |     function timeComparator(time, entryIndex) { | 
 |       return time - timelineData.entryStartTimes[entryIndex]; | 
 |     } | 
 |  | 
 |     /** | 
 |      * @param {number} entry1 | 
 |      * @param {number} entry2 | 
 |      * @return {boolean} | 
 |      */ | 
 |     function entriesIntersect(entry1, entry2) { | 
 |       const start1 = timelineData.entryStartTimes[entry1]; | 
 |       const start2 = timelineData.entryStartTimes[entry2]; | 
 |       const end1 = start1 + timelineData.entryTotalTimes[entry1]; | 
 |       const end2 = start2 + timelineData.entryTotalTimes[entry2]; | 
 |       return start1 < end2 && start2 < end1; | 
 |     } | 
 |  | 
 |     const keys = UI.KeyboardShortcut.Keys; | 
 |     if (e.keyCode === keys.Left.code || e.keyCode === keys.Right.code) { | 
 |       const level = timelineData.entryLevels[this._selectedEntryIndex]; | 
 |       const levelIndexes = this._timelineLevels[level]; | 
 |       let indexOnLevel = levelIndexes.lowerBound(this._selectedEntryIndex); | 
 |       indexOnLevel += e.keyCode === keys.Left.code ? -1 : 1; | 
 |       e.consume(true); | 
 |       if (indexOnLevel >= 0 && indexOnLevel < levelIndexes.length) { | 
 |         this.dispatchEventToListeners(PerfUI.FlameChart.Events.EntrySelected, levelIndexes[indexOnLevel]); | 
 |       } | 
 |       return true; | 
 |     } | 
 |     if (e.keyCode === keys.Up.code || e.keyCode === keys.Down.code) { | 
 |       let level = timelineData.entryLevels[this._selectedEntryIndex]; | 
 |       level += e.keyCode === keys.Up.code ? -1 : 1; | 
 |       if (level < 0 || level >= this._timelineLevels.length) { | 
 |         this._deselectAllEntries(); | 
 |         e.consume(true); | 
 |         return true; | 
 |       } | 
 |       const entryTime = timelineData.entryStartTimes[this._selectedEntryIndex] + | 
 |           timelineData.entryTotalTimes[this._selectedEntryIndex] / 2; | 
 |       const levelIndexes = this._timelineLevels[level]; | 
 |       let indexOnLevel = levelIndexes.upperBound(entryTime, timeComparator) - 1; | 
 |       if (!entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel])) { | 
 |         ++indexOnLevel; | 
 |         if (indexOnLevel >= levelIndexes.length || | 
 |             !entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel])) { | 
 |           if (e.code === 'ArrowDown') { | 
 |             return false; | 
 |           } | 
 |  | 
 |           // Stay in the current group and give focus to the parent group instead of entries | 
 |           this._deselectAllEntries(); | 
 |           e.consume(true); | 
 |           return true; | 
 |         } | 
 |       } | 
 |       e.consume(true); | 
 |       this.dispatchEventToListeners(PerfUI.FlameChart.Events.EntrySelected, levelIndexes[indexOnLevel]); | 
 |       return true; | 
 |     } | 
 |     if (isEnterKey(e)) { | 
 |       e.consume(true); | 
 |       this.dispatchEventToListeners(PerfUI.FlameChart.Events.EntryInvoked, this._selectedEntryIndex); | 
 |       return true; | 
 |     } | 
 |     return false; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} x | 
 |    * @param {number} y | 
 |    * @return {number} | 
 |    */ | 
 |   _coordinatesToEntryIndex(x, y) { | 
 |     if (x < 0 || y < 0) { | 
 |       return -1; | 
 |     } | 
 |     const timelineData = this._timelineData(); | 
 |     if (!timelineData) { | 
 |       return -1; | 
 |     } | 
 |     y += this._chartViewport.scrollOffset(); | 
 |     const cursorLevel = this._visibleLevelOffsets.upperBound(y) - 1; | 
 |     if (cursorLevel < 0 || !this._visibleLevels[cursorLevel]) { | 
 |       return -1; | 
 |     } | 
 |     const offsetFromLevel = y - this._visibleLevelOffsets[cursorLevel]; | 
 |     if (offsetFromLevel > this._levelHeight(cursorLevel)) { | 
 |       return -1; | 
 |     } | 
 |  | 
 |     // Check markers first. | 
 |     for (const [index, pos] of this._markerPositions) { | 
 |       if (timelineData.entryLevels[index] !== cursorLevel) { | 
 |         continue; | 
 |       } | 
 |       if (pos.x <= x && x < pos.x + pos.width) { | 
 |         return /** @type {number} */ (index); | 
 |       } | 
 |     } | 
 |  | 
 |     // Check regular entries. | 
 |     const entryStartTimes = timelineData.entryStartTimes; | 
 |     const entriesOnLevel = this._timelineLevels[cursorLevel]; | 
 |     if (!entriesOnLevel || !entriesOnLevel.length) { | 
 |       return -1; | 
 |     } | 
 |  | 
 |     const cursorTime = this._chartViewport.pixelToTime(x); | 
 |     const indexOnLevel = Math.max( | 
 |         entriesOnLevel.upperBound(cursorTime, (time, entryIndex) => time - entryStartTimes[entryIndex]) - 1, 0); | 
 |  | 
 |     /** | 
 |      * @this {PerfUI.FlameChart} | 
 |      * @param {number|undefined} entryIndex | 
 |      * @return {boolean} | 
 |      */ | 
 |     function checkEntryHit(entryIndex) { | 
 |       if (entryIndex === undefined) { | 
 |         return false; | 
 |       } | 
 |       const startTime = entryStartTimes[entryIndex]; | 
 |       const duration = timelineData.entryTotalTimes[entryIndex]; | 
 |       const startX = this._chartViewport.timeToPosition(startTime); | 
 |       const endX = this._chartViewport.timeToPosition(startTime + duration); | 
 |       const barThresholdPx = 3; | 
 |       return startX - barThresholdPx < x && x < endX + barThresholdPx; | 
 |     } | 
 |  | 
 |     let entryIndex = entriesOnLevel[indexOnLevel]; | 
 |     if (checkEntryHit.call(this, entryIndex)) { | 
 |       return entryIndex; | 
 |     } | 
 |     entryIndex = entriesOnLevel[indexOnLevel + 1]; | 
 |     if (checkEntryHit.call(this, entryIndex)) { | 
 |       return entryIndex; | 
 |     } | 
 |     return -1; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} x | 
 |    * @param {number} y | 
 |    * @param {boolean} headerOnly | 
 |    * @return {number} | 
 |    */ | 
 |   _coordinatesToGroupIndex(x, y, headerOnly) { | 
 |     if (x < 0 || y < 0) { | 
 |       return -1; | 
 |     } | 
 |     y += this._chartViewport.scrollOffset(); | 
 |     const groups = this._rawTimelineData.groups || []; | 
 |     const group = this._groupOffsets.upperBound(y) - 1; | 
 |     if (group < 0 || group >= groups.length) { | 
 |       return -1; | 
 |     } | 
 |     const height = headerOnly ? groups[group].style.height : this._groupOffsets[group + 1] - this._groupOffsets[group]; | 
 |     if (y - this._groupOffsets[group] >= height) { | 
 |       return -1; | 
 |     } | 
 |     if (!headerOnly) { | 
 |       return group; | 
 |     } | 
 |  | 
 |     const context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getContext('2d')); | 
 |     context.save(); | 
 |     context.font = groups[group].style.font; | 
 |     const right = this._headerLeftPadding + this._labelWidthForGroup(context, groups[group]); | 
 |     context.restore(); | 
 |     if (x > right) { | 
 |       return -1; | 
 |     } | 
 |  | 
 |     return group; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} x | 
 |    * @return {number} | 
 |    */ | 
 |   _markerIndexAtPosition(x) { | 
 |     const markers = this._timelineData().markers; | 
 |     if (!markers) { | 
 |       return -1; | 
 |     } | 
 |     const /** @const */ accurracyOffsetPx = 4; | 
 |     const time = this._chartViewport.pixelToTime(x); | 
 |     const leftTime = this._chartViewport.pixelToTime(x - accurracyOffsetPx); | 
 |     const rightTime = this._chartViewport.pixelToTime(x + accurracyOffsetPx); | 
 |     const left = this._markerIndexBeforeTime(leftTime); | 
 |     let markerIndex = -1; | 
 |     let distance = Infinity; | 
 |     for (let i = left; i < markers.length && markers[i].startTime() < rightTime; i++) { | 
 |       const nextDistance = Math.abs(markers[i].startTime() - time); | 
 |       if (nextDistance < distance) { | 
 |         markerIndex = i; | 
 |         distance = nextDistance; | 
 |       } | 
 |     } | 
 |     return markerIndex; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} time | 
 |    * @return {number} | 
 |    */ | 
 |   _markerIndexBeforeTime(time) { | 
 |     return this._timelineData().markers.lowerBound( | 
 |         time, (markerTimestamp, marker) => markerTimestamp - marker.startTime()); | 
 |   } | 
 |  | 
 |   _draw() { | 
 |     const timelineData = this._timelineData(); | 
 |     if (!timelineData) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const width = this._offsetWidth; | 
 |     const height = this._offsetHeight; | 
 |     const context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getContext('2d')); | 
 |     context.save(); | 
 |     const ratio = window.devicePixelRatio; | 
 |     const top = this._chartViewport.scrollOffset(); | 
 |     context.scale(ratio, ratio); | 
 |     context.fillStyle = 'rgba(0, 0, 0, 0)'; | 
 |     context.fillRect(0, 0, width, height); | 
 |     context.translate(0, -top); | 
 |     const defaultFont = '11px ' + Host.fontFamily(); | 
 |     context.font = defaultFont; | 
 |  | 
 |     const entryTotalTimes = timelineData.entryTotalTimes; | 
 |     const entryStartTimes = timelineData.entryStartTimes; | 
 |     const entryLevels = timelineData.entryLevels; | 
 |     const timeToPixel = this._chartViewport.timeToPixel(); | 
 |  | 
 |     const titleIndices = []; | 
 |     const markerIndices = []; | 
 |     const textPadding = this._textPadding; | 
 |     const minTextWidth = 2 * textPadding + UI.measureTextWidth(context, '\u2026'); | 
 |     const minTextWidthDuration = this._chartViewport.pixelToTimeOffset(minTextWidth); | 
 |     const minVisibleBarLevel = Math.max(this._visibleLevelOffsets.upperBound(top) - 1, 0); | 
 |     this._markerPositions.clear(); | 
 |  | 
 |     /** @type {!Map<string, !Array<number>>} */ | 
 |     const colorBuckets = new Map(); | 
 |     for (let level = minVisibleBarLevel; level < this._dataProvider.maxStackDepth(); ++level) { | 
 |       if (this._levelToOffset(level) > top + height) { | 
 |         break; | 
 |       } | 
 |       if (!this._visibleLevels[level]) { | 
 |         continue; | 
 |       } | 
 |  | 
 |       // Entries are ordered by start time within a level, so find the last visible entry. | 
 |       const levelIndexes = this._timelineLevels[level]; | 
 |       const rightIndexOnLevel = | 
 |           levelIndexes.lowerBound( | 
 |               this._chartViewport.windowRightTime(), (time, entryIndex) => time - entryStartTimes[entryIndex]) - | 
 |           1; | 
 |       let lastDrawOffset = Infinity; | 
 |       for (let entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) { | 
 |         const entryIndex = levelIndexes[entryIndexOnLevel]; | 
 |         const duration = entryTotalTimes[entryIndex]; | 
 |         if (isNaN(duration)) { | 
 |           markerIndices.push(entryIndex); | 
 |           continue; | 
 |         } | 
 |         if (duration >= minTextWidthDuration || this._forceDecorationCache[entryIndex]) { | 
 |           titleIndices.push(entryIndex); | 
 |         } | 
 |  | 
 |         const entryStartTime = entryStartTimes[entryIndex]; | 
 |         const entryOffsetRight = entryStartTime + duration; | 
 |         if (entryOffsetRight <= this._chartViewport.windowLeftTime()) { | 
 |           break; | 
 |         } | 
 |         if (this._useWebGL) { | 
 |           continue; | 
 |         } | 
 |  | 
 |         const barX = this._timeToPositionClipped(entryStartTime); | 
 |         // Check if the entry entirely fits into an already drawn pixel, we can just skip drawing it. | 
 |         if (barX >= lastDrawOffset) { | 
 |           continue; | 
 |         } | 
 |         lastDrawOffset = barX; | 
 |  | 
 |         const color = this._entryColorsCache[entryIndex]; | 
 |         let bucket = colorBuckets.get(color); | 
 |         if (!bucket) { | 
 |           bucket = []; | 
 |           colorBuckets.set(color, bucket); | 
 |         } | 
 |         bucket.push(entryIndex); | 
 |       } | 
 |     } | 
 |  | 
 |     if (this._useWebGL) { | 
 |       this._drawGL(); | 
 |     } else { | 
 |       context.save(); | 
 |       this._forEachGroupInViewport((offset, index, group, isFirst, groupHeight) => { | 
 |         if (this._isGroupFocused(index)) { | 
 |           context.fillStyle = this._selectedGroupBackroundColor; | 
 |           context.fillRect(0, offset, width, groupHeight - group.style.padding); | 
 |         } | 
 |       }); | 
 |       context.restore(); | 
 |  | 
 |       for (const [color, indexes] of colorBuckets) { | 
 |         context.beginPath(); | 
 |         for (let i = 0; i < indexes.length; ++i) { | 
 |           const entryIndex = indexes[i]; | 
 |           const duration = entryTotalTimes[entryIndex]; | 
 |           if (isNaN(duration)) { | 
 |             continue; | 
 |           } | 
 |           const entryStartTime = entryStartTimes[entryIndex]; | 
 |           const barX = this._timeToPositionClipped(entryStartTime); | 
 |           const barLevel = entryLevels[entryIndex]; | 
 |           const barHeight = this._levelHeight(barLevel); | 
 |           const barY = this._levelToOffset(barLevel); | 
 |           const barRight = this._timeToPositionClipped(entryStartTime + duration); | 
 |           const barWidth = Math.max(barRight - barX, 1); | 
 |           context.rect(barX, barY, barWidth - 0.4, barHeight - 1); | 
 |         } | 
 |         context.fillStyle = color; | 
 |         context.fill(); | 
 |       } | 
 |     } | 
 |  | 
 |     context.textBaseline = 'alphabetic'; | 
 |     context.beginPath(); | 
 |     let lastMarkerLevel = -1; | 
 |     let lastMarkerX = -Infinity; | 
 |     // Markers are sorted top to bottom, right to left. | 
 |     for (let m = markerIndices.length - 1; m >= 0; --m) { | 
 |       const entryIndex = markerIndices[m]; | 
 |       const title = this._dataProvider.entryTitle(entryIndex); | 
 |       if (!title) { | 
 |         continue; | 
 |       } | 
 |       const entryStartTime = entryStartTimes[entryIndex]; | 
 |       const level = entryLevels[entryIndex]; | 
 |       if (lastMarkerLevel !== level) { | 
 |         lastMarkerX = -Infinity; | 
 |       } | 
 |       const x = Math.max(this._chartViewport.timeToPosition(entryStartTime), lastMarkerX); | 
 |       const y = this._levelToOffset(level); | 
 |       const h = this._levelHeight(level); | 
 |       const padding = 4; | 
 |       const width = Math.ceil(UI.measureTextWidth(context, title)) + 2 * padding; | 
 |       lastMarkerX = x + width + 1; | 
 |       lastMarkerLevel = level; | 
 |       this._markerPositions.set(entryIndex, {x, width}); | 
 |       context.fillStyle = this._dataProvider.entryColor(entryIndex); | 
 |       context.fillRect(x, y, width, h - 1); | 
 |       context.fillStyle = 'white'; | 
 |       context.fillText(title, x + padding, y + h - this._textBaseline); | 
 |     } | 
 |     context.strokeStyle = 'rgba(0, 0, 0, 0.2)'; | 
 |     context.stroke(); | 
 |  | 
 |     for (let i = 0; i < titleIndices.length; ++i) { | 
 |       const entryIndex = titleIndices[i]; | 
 |       const entryStartTime = entryStartTimes[entryIndex]; | 
 |       const barX = this._timeToPositionClipped(entryStartTime); | 
 |       const barRight = Math.min(this._timeToPositionClipped(entryStartTime + entryTotalTimes[entryIndex]), width) + 1; | 
 |       const barWidth = barRight - barX; | 
 |       const barLevel = entryLevels[entryIndex]; | 
 |       const barY = this._levelToOffset(barLevel); | 
 |       let text = this._dataProvider.entryTitle(entryIndex); | 
 |       if (text && text.length) { | 
 |         context.font = this._dataProvider.entryFont(entryIndex) || defaultFont; | 
 |         text = UI.trimTextMiddle(context, text, barWidth - 2 * textPadding); | 
 |       } | 
 |       const unclippedBarX = this._chartViewport.timeToPosition(entryStartTime); | 
 |       const barHeight = this._levelHeight(barLevel); | 
 |       if (this._dataProvider.decorateEntry( | 
 |               entryIndex, context, text, barX, barY, barWidth, barHeight, unclippedBarX, timeToPixel)) { | 
 |         continue; | 
 |       } | 
 |       if (!text || !text.length) { | 
 |         continue; | 
 |       } | 
 |       context.fillStyle = this._dataProvider.textColor(entryIndex); | 
 |       context.fillText(text, barX + textPadding, barY + barHeight - this._textBaseline); | 
 |     } | 
 |  | 
 |     context.restore(); | 
 |  | 
 |     this._drawGroupHeaders(width, height); | 
 |     this._drawFlowEvents(context, width, height); | 
 |     this._drawMarkers(); | 
 |     const dividersData = PerfUI.TimelineGrid.calculateGridOffsets(this); | 
 |     PerfUI.TimelineGrid.drawCanvasGrid(context, dividersData); | 
 |     if (this._rulerEnabled) { | 
 |       PerfUI.TimelineGrid.drawCanvasHeaders( | 
 |           context, dividersData, time => this.formatValue(time, dividersData.precision), 3, | 
 |           PerfUI.FlameChart.HeaderHeight); | 
 |     } | 
 |  | 
 |     this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex); | 
 |     this._updateElementPosition(this._selectedElement, this._selectedEntryIndex); | 
 |     this._updateMarkerHighlight(); | 
 |   } | 
 |  | 
 |   _initWebGL() { | 
 |     const gl = /** @type {?WebGLRenderingContext} */ (this._canvasGL.getContext('webgl')); | 
 |     if (!gl) { | 
 |       console.error('Failed to obtain WebGL context.'); | 
 |       this._useWebGL = false;  // Fallback to use canvas. | 
 |       return; | 
 |     } | 
 |  | 
 |     const vertexShaderSource = ` | 
 |       attribute vec2 aVertexPosition; | 
 |       attribute float aVertexColor; | 
 |  | 
 |       uniform vec2 uScalingFactor; | 
 |       uniform vec2 uShiftVector; | 
 |  | 
 |       varying mediump vec2 vPalettePosition; | 
 |  | 
 |       void main() { | 
 |         vec2 shiftedPosition = aVertexPosition - uShiftVector; | 
 |         gl_Position = vec4(shiftedPosition * uScalingFactor + vec2(-1.0, 1.0), 0.0, 1.0); | 
 |         vPalettePosition = vec2(aVertexColor, 0.5); | 
 |       }`; | 
 |  | 
 |     const fragmentShaderSource = ` | 
 |       varying mediump vec2 vPalettePosition; | 
 |       uniform sampler2D uSampler; | 
 |  | 
 |       void main() { | 
 |         gl_FragColor = texture2D(uSampler, vPalettePosition); | 
 |       }`; | 
 |  | 
 |     /** | 
 |      * @param {!WebGLRenderingContext} gl | 
 |      * @param {number} type | 
 |      * @param {string} source | 
 |      * @return {?WebGLShader} | 
 |      */ | 
 |     function loadShader(gl, type, source) { | 
 |       const shader = gl.createShader(type); | 
 |       gl.shaderSource(shader, source); | 
 |       gl.compileShader(shader); | 
 |       if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | 
 |         return shader; | 
 |       } | 
 |       console.error('Shader compile error: ' + gl.getShaderInfoLog(shader)); | 
 |       gl.deleteShader(shader); | 
 |       return null; | 
 |     } | 
 |  | 
 |     const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); | 
 |     const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); | 
 |  | 
 |     const shaderProgram = gl.createProgram(); | 
 |     gl.attachShader(shaderProgram, vertexShader); | 
 |     gl.attachShader(shaderProgram, fragmentShader); | 
 |     gl.linkProgram(shaderProgram); | 
 |  | 
 |     if (gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { | 
 |       this._shaderProgram = shaderProgram; | 
 |       gl.useProgram(shaderProgram); | 
 |     } else { | 
 |       console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram)); | 
 |       this._shaderProgram = null; | 
 |     } | 
 |  | 
 |     this._vertexBuffer = gl.createBuffer(); | 
 |     this._colorBuffer = gl.createBuffer(); | 
 |  | 
 |     this._uScalingFactor = gl.getUniformLocation(shaderProgram, 'uScalingFactor'); | 
 |     this._uShiftVector = gl.getUniformLocation(shaderProgram, 'uShiftVector'); | 
 |     const uSampler = gl.getUniformLocation(shaderProgram, 'uSampler'); | 
 |     gl.uniform1i(uSampler, 0); | 
 |     this._aVertexPosition = gl.getAttribLocation(this._shaderProgram, 'aVertexPosition'); | 
 |     this._aVertexColor = gl.getAttribLocation(this._shaderProgram, 'aVertexColor'); | 
 |     gl.enableVertexAttribArray(this._aVertexPosition); | 
 |     gl.enableVertexAttribArray(this._aVertexColor); | 
 |   } | 
 |  | 
 |   _setupGLGeometry() { | 
 |     const gl = /** @type {?WebGLRenderingContext} */ (this._canvasGL.getContext('webgl')); | 
 |     if (!gl) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const timelineData = this._timelineData(); | 
 |     if (!timelineData) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const entryTotalTimes = timelineData.entryTotalTimes; | 
 |     const entryStartTimes = timelineData.entryStartTimes; | 
 |     const entryLevels = timelineData.entryLevels; | 
 |  | 
 |     const verticesPerBar = 6; | 
 |     const vertexArray = new Float32Array(entryTotalTimes.length * verticesPerBar * 2); | 
 |     let colorArray = new Uint8Array(entryTotalTimes.length * verticesPerBar); | 
 |     let vertex = 0; | 
 |     /** @type {!Map<string, number>} */ | 
 |     const parsedColorCache = new Map(); | 
 |     /** @type {!Array<number>} */ | 
 |     const colors = []; | 
 |  | 
 |     const collapsedOverviewLevels = new Array(this._visibleLevels.length); | 
 |     const groups = this._rawTimelineData.groups || []; | 
 |     this._forEachGroup((offset, index, group) => { | 
 |       if (group.style.useFirstLineForOverview || !this._isGroupCollapsible(index) || group.expanded) { | 
 |         return; | 
 |       } | 
 |       let nextGroup = index + 1; | 
 |       while (nextGroup < groups.length && groups[nextGroup].style.nestingLevel > group.style.nestingLevel) { | 
 |         ++nextGroup; | 
 |       } | 
 |       const endLevel = nextGroup < groups.length ? groups[nextGroup].startLevel : this._dataProvider.maxStackDepth(); | 
 |       for (let i = group.startLevel; i < endLevel; ++i) { | 
 |         collapsedOverviewLevels[i] = offset; | 
 |       } | 
 |     }); | 
 |  | 
 |     for (let i = 0; i < entryTotalTimes.length; ++i) { | 
 |       const level = entryLevels[i]; | 
 |       const collapsedGroupOffset = collapsedOverviewLevels[level]; | 
 |       if (!this._visibleLevels[level] && !collapsedGroupOffset) { | 
 |         continue; | 
 |       } | 
 |       const color = this._entryColorsCache[i]; | 
 |       if (!color) { | 
 |         continue; | 
 |       } | 
 |       let colorIndex = parsedColorCache.get(color); | 
 |       if (colorIndex === undefined) { | 
 |         const rgba = Common.Color.parse(color).canonicalRGBA(); | 
 |         rgba[3] = Math.round(rgba[3] * 255); | 
 |         colorIndex = colors.length / 4; | 
 |         colors.push(...rgba); | 
 |         if (colorIndex === 256) { | 
 |           colorArray = new Uint16Array(colorArray); | 
 |         } | 
 |         parsedColorCache.set(color, colorIndex); | 
 |       } | 
 |       for (let j = 0; j < verticesPerBar; ++j) { | 
 |         colorArray[vertex + j] = colorIndex; | 
 |       } | 
 |  | 
 |       const vpos = vertex * 2; | 
 |       const x0 = entryStartTimes[i] - this._minimumBoundary; | 
 |       const x1 = x0 + entryTotalTimes[i]; | 
 |       const y0 = collapsedGroupOffset || this._levelToOffset(level); | 
 |       const y1 = y0 + this._levelHeight(level) - 1; | 
 |       vertexArray[vpos + 0] = x0; | 
 |       vertexArray[vpos + 1] = y0; | 
 |       vertexArray[vpos + 2] = x1; | 
 |       vertexArray[vpos + 3] = y0; | 
 |       vertexArray[vpos + 4] = x0; | 
 |       vertexArray[vpos + 5] = y1; | 
 |       vertexArray[vpos + 6] = x0; | 
 |       vertexArray[vpos + 7] = y1; | 
 |       vertexArray[vpos + 8] = x1; | 
 |       vertexArray[vpos + 9] = y0; | 
 |       vertexArray[vpos + 10] = x1; | 
 |       vertexArray[vpos + 11] = y1; | 
 |  | 
 |       vertex += verticesPerBar; | 
 |     } | 
 |     this._vertexCount = vertex; | 
 |  | 
 |     const paletteTexture = gl.createTexture(); | 
 |     gl.bindTexture(gl.TEXTURE_2D, paletteTexture); | 
 |     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | 
 |     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | 
 |     gl.activeTexture(gl.TEXTURE0); | 
 |  | 
 |     const numColors = colors.length / 4; | 
 |     const useShortForColors = numColors >= 256; | 
 |     const width = !useShortForColors ? 256 : Math.min(1 << 16, gl.getParameter(gl.MAX_TEXTURE_SIZE)); | 
 |     console.assert(numColors <= width, 'Too many colors'); | 
 |     const height = 1; | 
 |     const colorIndexType = useShortForColors ? gl.UNSIGNED_SHORT : gl.UNSIGNED_BYTE; | 
 |     if (useShortForColors) { | 
 |       const factor = (1 << 16) / width; | 
 |       for (let i = 0; i < vertex; ++i) { | 
 |         colorArray[i] *= factor; | 
 |       } | 
 |     } | 
 |  | 
 |     const pixels = new Uint8Array(width * 4); | 
 |     pixels.set(colors); | 
 |     gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels); | 
 |  | 
 |     gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer); | 
 |     gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW); | 
 |     gl.vertexAttribPointer(this._aVertexPosition, /* vertexComponents */ 2, gl.FLOAT, false, 0, 0); | 
 |  | 
 |     gl.bindBuffer(gl.ARRAY_BUFFER, this._colorBuffer); | 
 |     gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW); | 
 |     gl.vertexAttribPointer(this._aVertexColor, /* colorComponents */ 1, colorIndexType, true, 0, 0); | 
 |   } | 
 |  | 
 |   _drawGL() { | 
 |     const gl = /** @type {?WebGLRenderingContext} */ (this._canvasGL.getContext('webgl')); | 
 |     if (!gl) { | 
 |       return; | 
 |     } | 
 |     const timelineData = this._timelineData(); | 
 |     if (!timelineData) { | 
 |       return; | 
 |     } | 
 |  | 
 |     if (!this._prevTimelineData || timelineData.entryTotalTimes !== this._prevTimelineData.entryTotalTimes) { | 
 |       this._prevTimelineData = timelineData; | 
 |       this._setupGLGeometry(); | 
 |     } | 
 |  | 
 |     gl.viewport(0, 0, this._canvasGL.width, this._canvasGL.height); | 
 |  | 
 |     if (!this._vertexCount) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const viewportScale = [2.0 / this.boundarySpan(), -2.0 * window.devicePixelRatio / this._canvasGL.height]; | 
 |     const viewportShift = [this.minimumBoundary() - this.zeroTime(), this._chartViewport.scrollOffset()]; | 
 |     gl.uniform2fv(this._uScalingFactor, viewportScale); | 
 |     gl.uniform2fv(this._uShiftVector, viewportShift); | 
 |  | 
 |     gl.drawArrays(gl.TRIANGLES, 0, this._vertexCount); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} width | 
 |    * @param {number} height | 
 |    */ | 
 |   _drawGroupHeaders(width, height) { | 
 |     const context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getContext('2d')); | 
 |     const top = this._chartViewport.scrollOffset(); | 
 |     const ratio = window.devicePixelRatio; | 
 |     const groups = this._rawTimelineData.groups || []; | 
 |     if (!groups.length) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const groupOffsets = this._groupOffsets; | 
 |     const lastGroupOffset = Array.prototype.peekLast.call(groupOffsets); | 
 |     const colorUsage = UI.ThemeSupport.ColorUsage; | 
 |  | 
 |     context.save(); | 
 |     context.scale(ratio, ratio); | 
 |     context.translate(0, -top); | 
 |     const defaultFont = '11px ' + Host.fontFamily(); | 
 |     context.font = defaultFont; | 
 |  | 
 |     context.fillStyle = UI.themeSupport.patchColorText('#fff', colorUsage.Background); | 
 |     this._forEachGroupInViewport((offset, index, group) => { | 
 |       const paddingHeight = group.style.padding; | 
 |       if (paddingHeight < 5) { | 
 |         return; | 
 |       } | 
 |       context.fillRect(0, offset - paddingHeight + 2, width, paddingHeight - 4); | 
 |     }); | 
 |     if (groups.length && lastGroupOffset < top + height) { | 
 |       context.fillRect(0, lastGroupOffset + 2, width, top + height - lastGroupOffset); | 
 |     } | 
 |  | 
 |     context.strokeStyle = UI.themeSupport.patchColorText('#eee', colorUsage.Background); | 
 |     context.beginPath(); | 
 |     this._forEachGroupInViewport((offset, index, group, isFirst) => { | 
 |       if (isFirst || group.style.padding < 4) { | 
 |         return; | 
 |       } | 
 |       hLine(offset - 2.5); | 
 |     }); | 
 |     hLine(lastGroupOffset + 1.5); | 
 |     context.stroke(); | 
 |  | 
 |     this._forEachGroupInViewport((offset, index, group) => { | 
 |       if (group.style.useFirstLineForOverview) { | 
 |         return; | 
 |       } | 
 |       if (!this._isGroupCollapsible(index) || group.expanded) { | 
 |         if (!group.style.shareHeaderLine && this._isGroupFocused(index)) { | 
 |           context.fillStyle = group.style.backgroundColor; | 
 |           context.fillRect(0, offset, width, group.style.height); | 
 |         } | 
 |         return; | 
 |       } | 
 |       if (this._useWebGL) { | 
 |         return; | 
 |       } | 
 |       let nextGroup = index + 1; | 
 |       while (nextGroup < groups.length && groups[nextGroup].style.nestingLevel > group.style.nestingLevel) { | 
 |         nextGroup++; | 
 |       } | 
 |       const endLevel = nextGroup < groups.length ? groups[nextGroup].startLevel : this._dataProvider.maxStackDepth(); | 
 |       this._drawCollapsedOverviewForGroup(group, offset, endLevel); | 
 |     }); | 
 |  | 
 |     context.save(); | 
 |     this._forEachGroupInViewport((offset, index, group) => { | 
 |       context.font = group.style.font; | 
 |       if (this._isGroupCollapsible(index) && !group.expanded || group.style.shareHeaderLine) { | 
 |         const width = this._labelWidthForGroup(context, group) + 2; | 
 |         if (this._isGroupFocused(index)) { | 
 |           context.fillStyle = this._selectedGroupBackroundColor; | 
 |         } else { | 
 |           context.fillStyle = Common.Color.parse(group.style.backgroundColor).setAlpha(0.8).asString(null); | 
 |         } | 
 |  | 
 |         context.fillRect( | 
 |             this._headerLeftPadding - this._headerLabelXPadding, offset + this._headerLabelYPadding, width, | 
 |             group.style.height - 2 * this._headerLabelYPadding); | 
 |       } | 
 |       context.fillStyle = group.style.color; | 
 |       context.fillText( | 
 |           group.name, Math.floor(this._expansionArrowIndent * (group.style.nestingLevel + 1) + this._arrowSide), | 
 |           offset + group.style.height - this._textBaseline); | 
 |     }); | 
 |     context.restore(); | 
 |  | 
 |     context.fillStyle = UI.themeSupport.patchColorText('#6e6e6e', colorUsage.Foreground); | 
 |     context.beginPath(); | 
 |     this._forEachGroupInViewport((offset, index, group) => { | 
 |       if (this._isGroupCollapsible(index)) { | 
 |         drawExpansionArrow.call( | 
 |             this, this._expansionArrowIndent * (group.style.nestingLevel + 1), | 
 |             offset + group.style.height - this._textBaseline - this._arrowSide / 2, !!group.expanded); | 
 |       } | 
 |     }); | 
 |     context.fill(); | 
 |  | 
 |     context.strokeStyle = UI.themeSupport.patchColorText('#ddd', colorUsage.Background); | 
 |     context.beginPath(); | 
 |     context.stroke(); | 
 |  | 
 |     this._forEachGroupInViewport((offset, index, group, isFirst, groupHeight) => { | 
 |       if (this._isGroupFocused(index)) { | 
 |         const lineWidth = 2; | 
 |         const bracketLength = 10; | 
 |         context.fillStyle = this._selectedGroupBorderColor; | 
 |         context.fillRect(0, offset - lineWidth, lineWidth, groupHeight - group.style.padding + 2 * lineWidth); | 
 |         context.fillRect(0, offset - lineWidth, bracketLength, lineWidth); | 
 |         context.fillRect(0, offset + groupHeight - group.style.padding, bracketLength, lineWidth); | 
 |       } | 
 |     }); | 
 |  | 
 |     context.restore(); | 
 |  | 
 |     /** | 
 |      * @param {number} y | 
 |      */ | 
 |     function hLine(y) { | 
 |       context.moveTo(0, y); | 
 |       context.lineTo(width, y); | 
 |     } | 
 |  | 
 |     /** | 
 |      * @param {number} x | 
 |      * @param {number} y | 
 |      * @param {boolean} expanded | 
 |      * @this {PerfUI.FlameChart} | 
 |      */ | 
 |     function drawExpansionArrow(x, y, expanded) { | 
 |       const arrowHeight = this._arrowSide * Math.sqrt(3) / 2; | 
 |       const arrowCenterOffset = Math.round(arrowHeight / 2); | 
 |       context.save(); | 
 |       context.translate(x, y); | 
 |       context.rotate(expanded ? Math.PI / 2 : 0); | 
 |       context.moveTo(-arrowCenterOffset, -this._arrowSide / 2); | 
 |       context.lineTo(-arrowCenterOffset, this._arrowSide / 2); | 
 |       context.lineTo(arrowHeight - arrowCenterOffset, 0); | 
 |       context.restore(); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {function(number, number, !PerfUI.FlameChart.Group, boolean, number)} callback | 
 |    */ | 
 |   _forEachGroup(callback) { | 
 |     const groups = this._rawTimelineData.groups || []; | 
 |     if (!groups.length) { | 
 |       return; | 
 |     } | 
 |     const groupOffsets = this._groupOffsets; | 
 |     /** @type !Array<{nestingLevel: number, visible: boolean}> */ | 
 |     const groupStack = [{nestingLevel: -1, visible: true}]; | 
 |     for (let i = 0; i < groups.length; ++i) { | 
 |       const groupTop = groupOffsets[i]; | 
 |       const group = groups[i]; | 
 |       let firstGroup = true; | 
 |       while (groupStack.peekLast().nestingLevel >= group.style.nestingLevel) { | 
 |         groupStack.pop(); | 
 |         firstGroup = false; | 
 |       } | 
 |       const parentGroupVisible = groupStack.peekLast().visible; | 
 |       const thisGroupVisible = parentGroupVisible && (!this._isGroupCollapsible(i) || group.expanded); | 
 |       groupStack.push({nestingLevel: group.style.nestingLevel, visible: thisGroupVisible}); | 
 |       const nextOffset = i === groups.length - 1 ? groupOffsets[i + 1] + group.style.padding : groupOffsets[i + 1]; | 
 |       if (!parentGroupVisible) { | 
 |         continue; | 
 |       } | 
 |       callback(groupTop, i, group, firstGroup, nextOffset - groupTop); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {function(number, number, !PerfUI.FlameChart.Group, boolean, number)} callback | 
 |    */ | 
 |   _forEachGroupInViewport(callback) { | 
 |     const top = this._chartViewport.scrollOffset(); | 
 |     this._forEachGroup((groupTop, index, group, firstGroup, height) => { | 
 |       if (groupTop - group.style.padding > top + this._offsetHeight) { | 
 |         return; | 
 |       } | 
 |       if (groupTop + height < top) { | 
 |         return; | 
 |       } | 
 |       callback(groupTop, index, group, firstGroup, height); | 
 |     }); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!CanvasRenderingContext2D} context | 
 |    * @param {!PerfUI.FlameChart.Group} group | 
 |    * @return {number} | 
 |    */ | 
 |   _labelWidthForGroup(context, group) { | 
 |     return UI.measureTextWidth(context, group.name) + this._expansionArrowIndent * (group.style.nestingLevel + 1) + | 
 |         2 * this._headerLabelXPadding; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!PerfUI.FlameChart.Group} group | 
 |    * @param {number} y | 
 |    * @param {number} endLevel | 
 |    */ | 
 |   _drawCollapsedOverviewForGroup(group, y, endLevel) { | 
 |     const range = new Common.SegmentedRange(mergeCallback); | 
 |     const timeWindowLeft = this._chartViewport.windowLeftTime(); | 
 |     const timeWindowRight = this._chartViewport.windowRightTime(); | 
 |     const context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getContext('2d')); | 
 |     const barHeight = group.style.height; | 
 |     const entryStartTimes = this._rawTimelineData.entryStartTimes; | 
 |     const entryTotalTimes = this._rawTimelineData.entryTotalTimes; | 
 |     const timeToPixel = this._chartViewport.timeToPixel(); | 
 |  | 
 |     for (let level = group.startLevel; level < endLevel; ++level) { | 
 |       const levelIndexes = this._timelineLevels[level]; | 
 |       const rightIndexOnLevel = | 
 |           levelIndexes.lowerBound(timeWindowRight, (time, entryIndex) => time - entryStartTimes[entryIndex]) - 1; | 
 |       let lastDrawOffset = Infinity; | 
 |  | 
 |       for (let entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) { | 
 |         const entryIndex = levelIndexes[entryIndexOnLevel]; | 
 |         const entryStartTime = entryStartTimes[entryIndex]; | 
 |         const barX = this._timeToPositionClipped(entryStartTime); | 
 |         const entryEndTime = entryStartTime + entryTotalTimes[entryIndex]; | 
 |         if (isNaN(entryEndTime) || barX >= lastDrawOffset) { | 
 |           continue; | 
 |         } | 
 |         if (entryEndTime <= timeWindowLeft) { | 
 |           break; | 
 |         } | 
 |         lastDrawOffset = barX; | 
 |         const color = this._entryColorsCache[entryIndex]; | 
 |         const endBarX = this._timeToPositionClipped(entryEndTime); | 
 |         if (group.style.useDecoratorsForOverview && this._dataProvider.forceDecoration(entryIndex)) { | 
 |           const unclippedBarX = this._chartViewport.timeToPosition(entryStartTime); | 
 |           const barWidth = endBarX - barX; | 
 |           context.beginPath(); | 
 |           context.fillStyle = color; | 
 |           context.fillRect(barX, y, barWidth, barHeight - 1); | 
 |           this._dataProvider.decorateEntry( | 
 |               entryIndex, context, '', barX, y, barWidth, barHeight, unclippedBarX, timeToPixel); | 
 |           continue; | 
 |         } | 
 |         range.append(new Common.Segment(barX, endBarX, color)); | 
 |       } | 
 |     } | 
 |  | 
 |     const segments = range.segments().slice().sort((a, b) => a.data.localeCompare(b.data)); | 
 |     let lastColor; | 
 |     context.beginPath(); | 
 |     for (let i = 0; i < segments.length; ++i) { | 
 |       const segment = segments[i]; | 
 |       if (lastColor !== segments[i].data) { | 
 |         context.fill(); | 
 |         context.beginPath(); | 
 |         lastColor = segments[i].data; | 
 |         context.fillStyle = lastColor; | 
 |       } | 
 |       context.rect(segment.begin, y, segment.end - segment.begin, barHeight); | 
 |     } | 
 |     context.fill(); | 
 |  | 
 |     /** | 
 |      * @param {!Common.Segment} a | 
 |      * @param {!Common.Segment} b | 
 |      * @return {?Common.Segment} | 
 |      */ | 
 |     function mergeCallback(a, b) { | 
 |       return a.data === b.data && a.end + 0.4 > b.end ? a : null; | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!CanvasRenderingContext2D} context | 
 |    * @param {number} height | 
 |    * @param {number} width | 
 |    */ | 
 |   _drawFlowEvents(context, width, height) { | 
 |     context.save(); | 
 |     const ratio = window.devicePixelRatio; | 
 |     const top = this._chartViewport.scrollOffset(); | 
 |     const arrowWidth = 6; | 
 |     context.scale(ratio, ratio); | 
 |     context.translate(0, -top); | 
 |  | 
 |     context.fillStyle = '#7f5050'; | 
 |     context.strokeStyle = '#7f5050'; | 
 |     const td = this._timelineData(); | 
 |     const endIndex = td.flowStartTimes.lowerBound(this._chartViewport.windowRightTime()); | 
 |  | 
 |     context.lineWidth = 0.5; | 
 |     for (let i = 0; i < endIndex; ++i) { | 
 |       if (!td.flowEndTimes[i] || td.flowEndTimes[i] < this._chartViewport.windowLeftTime()) { | 
 |         continue; | 
 |       } | 
 |       const startX = this._chartViewport.timeToPosition(td.flowStartTimes[i]); | 
 |       const endX = this._chartViewport.timeToPosition(td.flowEndTimes[i]); | 
 |       const startLevel = td.flowStartLevels[i]; | 
 |       const endLevel = td.flowEndLevels[i]; | 
 |       const startY = this._levelToOffset(startLevel) + this._levelHeight(startLevel) / 2; | 
 |       const endY = this._levelToOffset(endLevel) + this._levelHeight(endLevel) / 2; | 
 |  | 
 |  | 
 |       const segment = Math.min((endX - startX) / 4, 40); | 
 |       const distanceTime = td.flowEndTimes[i] - td.flowStartTimes[i]; | 
 |       const distanceY = (endY - startY) / 10; | 
 |       const spread = 30; | 
 |       const lineY = distanceTime < 1 ? startY : spread + Math.max(0, startY + distanceY * (i % spread)); | 
 |  | 
 |       const p = []; | 
 |       p.push({x: startX, y: startY}); | 
 |       p.push({x: startX + arrowWidth, y: startY}); | 
 |       p.push({x: startX + segment + 2 * arrowWidth, y: startY}); | 
 |       p.push({x: startX + segment, y: lineY}); | 
 |       p.push({x: startX + segment * 2, y: lineY}); | 
 |       p.push({x: endX - segment * 2, y: lineY}); | 
 |       p.push({x: endX - segment, y: lineY}); | 
 |       p.push({x: endX - segment - 2 * arrowWidth, y: endY}); | 
 |       p.push({x: endX - arrowWidth, y: endY}); | 
 |  | 
 |       context.beginPath(); | 
 |       context.moveTo(p[0].x, p[0].y); | 
 |       context.lineTo(p[1].x, p[1].y); | 
 |       context.bezierCurveTo(p[2].x, p[2].y, p[3].x, p[3].y, p[4].x, p[4].y); | 
 |       context.lineTo(p[5].x, p[5].y); | 
 |       context.bezierCurveTo(p[6].x, p[6].y, p[7].x, p[7].y, p[8].x, p[8].y); | 
 |       context.stroke(); | 
 |  | 
 |       context.beginPath(); | 
 |       context.arc(startX, startY, 2, -Math.PI / 2, Math.PI / 2, false); | 
 |       context.fill(); | 
 |  | 
 |       context.beginPath(); | 
 |       context.moveTo(endX, endY); | 
 |       context.lineTo(endX - arrowWidth, endY - 3); | 
 |       context.lineTo(endX - arrowWidth, endY + 3); | 
 |       context.fill(); | 
 |     } | 
 |     context.restore(); | 
 |   } | 
 |  | 
 |   _drawMarkers() { | 
 |     const markers = this._timelineData().markers; | 
 |     const left = this._markerIndexBeforeTime(this.minimumBoundary()); | 
 |     const rightBoundary = this.maximumBoundary(); | 
 |     const timeToPixel = this._chartViewport.timeToPixel(); | 
 |  | 
 |     const context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getContext('2d')); | 
 |     context.save(); | 
 |     const ratio = window.devicePixelRatio; | 
 |     context.scale(ratio, ratio); | 
 |     context.translate(0, 3); | 
 |     const height = PerfUI.FlameChart.HeaderHeight - 1; | 
 |     for (let i = left; i < markers.length; i++) { | 
 |       const timestamp = markers[i].startTime(); | 
 |       if (timestamp > rightBoundary) { | 
 |         break; | 
 |       } | 
 |       markers[i].draw(context, this._chartViewport.timeToPosition(timestamp), height, timeToPixel); | 
 |     } | 
 |     context.restore(); | 
 |   } | 
 |  | 
 |   _updateMarkerHighlight() { | 
 |     const element = this._markerHighlighElement; | 
 |     if (element.parentElement) { | 
 |       element.remove(); | 
 |     } | 
 |     const markerIndex = this._highlightedMarkerIndex; | 
 |     if (markerIndex === -1) { | 
 |       return; | 
 |     } | 
 |     const marker = this._timelineData().markers[markerIndex]; | 
 |     const barX = this._timeToPositionClipped(marker.startTime()); | 
 |     element.title = marker.title(); | 
 |     const style = element.style; | 
 |     style.left = barX + 'px'; | 
 |     style.backgroundColor = marker.color(); | 
 |     this._viewportElement.appendChild(element); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {?PerfUI.FlameChart.TimelineData} timelineData | 
 |    */ | 
 |   _processTimelineData(timelineData) { | 
 |     if (!timelineData) { | 
 |       this._timelineLevels = null; | 
 |       this._visibleLevelOffsets = null; | 
 |       this._visibleLevels = null; | 
 |       this._groupOffsets = null; | 
 |       this._rawTimelineData = null; | 
 |       this._forceDecorationCache = null; | 
 |       this._entryColorsCache = null; | 
 |       this._rawTimelineDataLength = 0; | 
 |       this._selectedGroup = -1; | 
 |       this._keyboardFocusedGroup = -1; | 
 |       this._flameChartDelegate.updateSelectedGroup(this, null); | 
 |       return; | 
 |     } | 
 |  | 
 |     this._rawTimelineData = timelineData; | 
 |     this._rawTimelineDataLength = timelineData.entryStartTimes.length; | 
 |     this._forceDecorationCache = new Int8Array(this._rawTimelineDataLength); | 
 |     this._entryColorsCache = new Array(this._rawTimelineDataLength); | 
 |     for (let i = 0; i < this._rawTimelineDataLength; ++i) { | 
 |       this._forceDecorationCache[i] = this._dataProvider.forceDecoration(i) ? 1 : 0; | 
 |       this._entryColorsCache[i] = this._dataProvider.entryColor(i); | 
 |     } | 
 |  | 
 |     const entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1); | 
 |     for (let i = 0; i < timelineData.entryLevels.length; ++i) { | 
 |       ++entryCounters[timelineData.entryLevels[i]]; | 
 |     } | 
 |     const levelIndexes = new Array(entryCounters.length); | 
 |     for (let i = 0; i < levelIndexes.length; ++i) { | 
 |       levelIndexes[i] = new Uint32Array(entryCounters[i]); | 
 |       entryCounters[i] = 0; | 
 |     } | 
 |  | 
 |     for (let i = 0; i < timelineData.entryLevels.length; ++i) { | 
 |       const level = timelineData.entryLevels[i]; | 
 |       levelIndexes[level][entryCounters[level]++] = i; | 
 |     } | 
 |     this._timelineLevels = levelIndexes; | 
 |     const groups = this._rawTimelineData.groups || []; | 
 |     for (let i = 0; i < groups.length; ++i) { | 
 |       const expanded = this._groupExpansionState[groups[i].name]; | 
 |       if (expanded !== undefined) { | 
 |         groups[i].expanded = expanded; | 
 |       } | 
 |     } | 
 |     this._updateLevelPositions(); | 
 |     this._updateHeight(); | 
 |  | 
 |     this._selectedGroup = timelineData.selectedGroup ? groups.indexOf(timelineData.selectedGroup) : -1; | 
 |     this._keyboardFocusedGroup = this._selectedGroup; | 
 |     this._flameChartDelegate.updateSelectedGroup(this, timelineData.selectedGroup); | 
 |   } | 
 |  | 
 |   _updateLevelPositions() { | 
 |     const levelCount = this._dataProvider.maxStackDepth(); | 
 |     const groups = this._rawTimelineData.groups || []; | 
 |     this._visibleLevelOffsets = new Uint32Array(levelCount + 1); | 
 |     this._visibleLevelHeights = new Uint32Array(levelCount); | 
 |     this._visibleLevels = new Uint16Array(levelCount); | 
 |     this._groupOffsets = new Uint32Array(groups.length + 1); | 
 |  | 
 |     let groupIndex = -1; | 
 |     let currentOffset = this._rulerEnabled ? PerfUI.FlameChart.HeaderHeight + 2 : 2; | 
 |     let visible = true; | 
 |     /** @type !Array<{nestingLevel: number, visible: boolean}> */ | 
 |     const groupStack = [{nestingLevel: -1, visible: true}]; | 
 |     const lastGroupLevel = Math.max(levelCount, groups.length ? groups.peekLast().startLevel + 1 : 0); | 
 |     let level; | 
 |     for (level = 0; level < lastGroupLevel; ++level) { | 
 |       let parentGroupIsVisible = true; | 
 |       let style; | 
 |       while (groupIndex < groups.length - 1 && level === groups[groupIndex + 1].startLevel) { | 
 |         ++groupIndex; | 
 |         style = groups[groupIndex].style; | 
 |         let nextLevel = true; | 
 |         while (groupStack.peekLast().nestingLevel >= style.nestingLevel) { | 
 |           groupStack.pop(); | 
 |           nextLevel = false; | 
 |         } | 
 |         const thisGroupIsVisible = | 
 |             groupIndex >= 0 && this._isGroupCollapsible(groupIndex) ? groups[groupIndex].expanded : true; | 
 |         parentGroupIsVisible = groupStack.peekLast().visible; | 
 |         visible = thisGroupIsVisible && parentGroupIsVisible; | 
 |         groupStack.push({nestingLevel: style.nestingLevel, visible: visible}); | 
 |         if (parentGroupIsVisible) { | 
 |           currentOffset += nextLevel ? 0 : style.padding; | 
 |         } | 
 |         this._groupOffsets[groupIndex] = currentOffset; | 
 |         if (parentGroupIsVisible && !style.shareHeaderLine) { | 
 |           currentOffset += style.height; | 
 |         } | 
 |       } | 
 |       if (level >= levelCount) { | 
 |         continue; | 
 |       } | 
 |       const isFirstOnLevel = groupIndex >= 0 && level === groups[groupIndex].startLevel; | 
 |       const thisLevelIsVisible = | 
 |           parentGroupIsVisible && (visible || isFirstOnLevel && groups[groupIndex].style.useFirstLineForOverview); | 
 |       let height; | 
 |       if (groupIndex >= 0) { | 
 |         const group = groups[groupIndex]; | 
 |         const styleB = group.style; | 
 |         height = isFirstOnLevel && !styleB.shareHeaderLine || (styleB.collapsible && !group.expanded) ? | 
 |             styleB.height : | 
 |             (styleB.itemsHeight || this._barHeight); | 
 |       } else { | 
 |         height = this._barHeight; | 
 |       } | 
 |       this._visibleLevels[level] = thisLevelIsVisible; | 
 |       this._visibleLevelOffsets[level] = currentOffset; | 
 |       this._visibleLevelHeights[level] = height; | 
 |       if (thisLevelIsVisible || (parentGroupIsVisible && style && style.shareHeaderLine && isFirstOnLevel)) { | 
 |         currentOffset += this._visibleLevelHeights[level]; | 
 |       } | 
 |     } | 
 |     if (groupIndex >= 0) { | 
 |       this._groupOffsets[groupIndex + 1] = currentOffset; | 
 |     } | 
 |     this._visibleLevelOffsets[level] = currentOffset; | 
 |     if (this._useWebGL) { | 
 |       this._setupGLGeometry(); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} index | 
 |    */ | 
 |   _isGroupCollapsible(index) { | 
 |     const groups = this._rawTimelineData.groups || []; | 
 |     const style = groups[index].style; | 
 |     if (!style.shareHeaderLine || !style.collapsible) { | 
 |       return !!style.collapsible; | 
 |     } | 
 |     const isLastGroup = index + 1 >= groups.length; | 
 |     if (!isLastGroup && groups[index + 1].style.nestingLevel > style.nestingLevel) { | 
 |       return true; | 
 |     } | 
 |     const nextGroupLevel = isLastGroup ? this._dataProvider.maxStackDepth() : groups[index + 1].startLevel; | 
 |     if (nextGroupLevel !== groups[index].startLevel + 1) { | 
 |       return true; | 
 |     } | 
 |     // For groups that only have one line and share header line, pretend these are not collapsible | 
 |     // unless the itemsHeight does not match the headerHeight | 
 |     return style.height !== style.itemsHeight; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    */ | 
 |   setSelectedEntry(entryIndex) { | 
 |     if (this._selectedEntryIndex === entryIndex) { | 
 |       return; | 
 |     } | 
 |     if (entryIndex !== -1) { | 
 |       this._chartViewport.hideRangeSelection(); | 
 |     } | 
 |     this._selectedEntryIndex = entryIndex; | 
 |     this._revealEntry(entryIndex); | 
 |     this._updateElementPosition(this._selectedElement, this._selectedEntryIndex); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!Element} element | 
 |    * @param {number} entryIndex | 
 |    */ | 
 |   _updateElementPosition(element, entryIndex) { | 
 |     const elementMinWidthPx = 2; | 
 |     element.classList.add('hidden'); | 
 |     if (entryIndex === -1) { | 
 |       return; | 
 |     } | 
 |     const timelineData = this._timelineData(); | 
 |     const startTime = timelineData.entryStartTimes[entryIndex]; | 
 |     const duration = timelineData.entryTotalTimes[entryIndex]; | 
 |     let barX = 0; | 
 |     let barWidth = 0; | 
 |     let visible = true; | 
 |     if (Number.isNaN(duration)) { | 
 |       const position = this._markerPositions.get(entryIndex); | 
 |       if (position) { | 
 |         barX = position.x; | 
 |         barWidth = position.width; | 
 |       } else { | 
 |         visible = false; | 
 |       } | 
 |     } else { | 
 |       barX = this._chartViewport.timeToPosition(startTime); | 
 |       barWidth = duration * this._chartViewport.timeToPixel(); | 
 |     } | 
 |     if (barX + barWidth <= 0 || barX >= this._offsetWidth) { | 
 |       return; | 
 |     } | 
 |     const barCenter = barX + barWidth / 2; | 
 |     barWidth = Math.max(barWidth, elementMinWidthPx); | 
 |     barX = barCenter - barWidth / 2; | 
 |     const entryLevel = timelineData.entryLevels[entryIndex]; | 
 |     const barY = this._levelToOffset(entryLevel) - this._chartViewport.scrollOffset(); | 
 |     const barHeight = this._levelHeight(entryLevel); | 
 |     const style = element.style; | 
 |     style.left = barX + 'px'; | 
 |     style.top = barY + 'px'; | 
 |     style.width = barWidth + 'px'; | 
 |     style.height = barHeight - 1 + 'px'; | 
 |     element.classList.toggle('hidden', !visible); | 
 |     this._viewportElement.appendChild(element); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} time | 
 |    * @return {number} | 
 |    */ | 
 |   _timeToPositionClipped(time) { | 
 |     return Number.constrain(this._chartViewport.timeToPosition(time), 0, this._offsetWidth); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} level | 
 |    * @return {number} | 
 |    */ | 
 |   _levelToOffset(level) { | 
 |     return this._visibleLevelOffsets[level]; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {number} level | 
 |    * @return {number} | 
 |    */ | 
 |   _levelHeight(level) { | 
 |     return this._visibleLevelHeights[level]; | 
 |   } | 
 |  | 
 |   _updateBoundaries() { | 
 |     this._totalTime = this._dataProvider.totalTime(); | 
 |     this._minimumBoundary = this._dataProvider.minimumBoundary(); | 
 |     this._chartViewport.setBoundaries(this._minimumBoundary, this._totalTime); | 
 |   } | 
 |  | 
 |   _updateHeight() { | 
 |     const height = this._levelToOffset(this._dataProvider.maxStackDepth()) + 2; | 
 |     this._chartViewport.setContentHeight(height); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    */ | 
 |   onResize() { | 
 |     this.scheduleUpdate(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    */ | 
 |   update() { | 
 |     if (!this._timelineData()) { | 
 |       return; | 
 |     } | 
 |     this._resetCanvas(); | 
 |     this._updateHeight(); | 
 |     this._updateBoundaries(); | 
 |     this._draw(); | 
 |     if (!this._chartViewport.isDragging()) { | 
 |       this._updateHighlight(); | 
 |     } | 
 |   } | 
 |  | 
 |   reset() { | 
 |     this._chartViewport.reset(); | 
 |     this._rawTimelineData = null; | 
 |     this._rawTimelineDataLength = 0; | 
 |     this._highlightedMarkerIndex = -1; | 
 |     this._highlightedEntryIndex = -1; | 
 |     this._selectedEntryIndex = -1; | 
 |     /** @type {!Map<string,!Map<string,number>>} */ | 
 |     this._textWidth = new Map(); | 
 |     this._chartViewport.scheduleUpdate(); | 
 |   } | 
 |  | 
 |   scheduleUpdate() { | 
 |     this._chartViewport.scheduleUpdate(); | 
 |   } | 
 |  | 
 |   _enabled() { | 
 |     return this._rawTimelineDataLength !== 0; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @param {number} time | 
 |    * @return {number} | 
 |    */ | 
 |   computePosition(time) { | 
 |     return this._chartViewport.timeToPosition(time); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @param {number} value | 
 |    * @param {number=} precision | 
 |    * @return {string} | 
 |    */ | 
 |   formatValue(value, precision) { | 
 |     return this._dataProvider.formatValue(value - this.zeroTime(), precision); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @return {number} | 
 |    */ | 
 |   maximumBoundary() { | 
 |     return this._chartViewport.windowRightTime(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @return {number} | 
 |    */ | 
 |   minimumBoundary() { | 
 |     return this._chartViewport.windowLeftTime(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @return {number} | 
 |    */ | 
 |   zeroTime() { | 
 |     return this._dataProvider.minimumBoundary(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @override | 
 |    * @return {number} | 
 |    */ | 
 |   boundarySpan() { | 
 |     return this.maximumBoundary() - this.minimumBoundary(); | 
 |   } | 
 | }; | 
 |  | 
 | PerfUI.FlameChart.HeaderHeight = 15; | 
 |  | 
 | PerfUI.FlameChart.MinimalTimeWindowMs = 0.5; | 
 |  | 
 | /** | 
 |  * @interface | 
 |  */ | 
 | PerfUI.FlameChartDataProvider = function() {}; | 
 |  | 
 | /** | 
 |  * @typedef {!{ | 
 |  *     name: string, | 
 |  *     startLevel: number, | 
 |  *     expanded: (boolean|undefined), | 
 |  *     selectable: (boolean|undefined), | 
 |  *     style: !PerfUI.FlameChart.GroupStyle | 
 |  * }} | 
 |  */ | 
 | PerfUI.FlameChart.Group; | 
 |  | 
 | /** | 
 |  * @typedef {!{ | 
 |  *     height: number, | 
 |  *     padding: number, | 
 |  *     collapsible: boolean, | 
 |  *     font: string, | 
 |  *     color: string, | 
 |  *     backgroundColor: string, | 
 |  *     nestingLevel: number, | 
 |  *     itemsHeight: (number|undefined), | 
 |  *     shareHeaderLine: (boolean|undefined), | 
 |  *     useFirstLineForOverview: (boolean|undefined), | 
 |  *     useDecoratorsForOverview: (boolean|undefined) | 
 |  * }} | 
 |  */ | 
 | PerfUI.FlameChart.GroupStyle; | 
 |  | 
 | /** | 
 |  * @unrestricted | 
 |  */ | 
 | PerfUI.FlameChart.TimelineData = class { | 
 |   /** | 
 |    * @param {!Array<number>|!Uint16Array} entryLevels | 
 |    * @param {!Array<number>|!Float32Array} entryTotalTimes | 
 |    * @param {!Array<number>|!Float64Array} entryStartTimes | 
 |    * @param {?Array<!PerfUI.FlameChart.Group>} groups | 
 |    */ | 
 |   constructor(entryLevels, entryTotalTimes, entryStartTimes, groups) { | 
 |     this.entryLevels = entryLevels; | 
 |     this.entryTotalTimes = entryTotalTimes; | 
 |     this.entryStartTimes = entryStartTimes; | 
 |     this.groups = groups; | 
 |     /** @type {!Array.<!PerfUI.FlameChartMarker>} */ | 
 |     this.markers = []; | 
 |     this.flowStartTimes = []; | 
 |     this.flowStartLevels = []; | 
 |     this.flowEndTimes = []; | 
 |     this.flowEndLevels = []; | 
 |     /** @type {?PerfUI.FlameChart.Group} */ | 
 |     this.selectedGroup = null; | 
 |   } | 
 | }; | 
 |  | 
 | PerfUI.FlameChartDataProvider.prototype = { | 
 |   /** | 
 |    * @return {number} | 
 |    */ | 
 |   minimumBoundary() {}, | 
 |  | 
 |   /** | 
 |    * @return {number} | 
 |    */ | 
 |   totalTime() {}, | 
 |  | 
 |   /** | 
 |    * @param {number} value | 
 |    * @param {number=} precision | 
 |    * @return {string} | 
 |    */ | 
 |   formatValue(value, precision) {}, | 
 |  | 
 |   /** | 
 |    * @return {number} | 
 |    */ | 
 |   maxStackDepth() {}, | 
 |  | 
 |   /** | 
 |    * @return {?PerfUI.FlameChart.TimelineData} | 
 |    */ | 
 |   timelineData() {}, | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    * @return {?Element} | 
 |    */ | 
 |   prepareHighlightedEntryInfo(entryIndex) {}, | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    * @return {boolean} | 
 |    */ | 
 |   canJumpToEntry(entryIndex) {}, | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    * @return {?string} | 
 |    */ | 
 |   entryTitle(entryIndex) {}, | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    * @return {?string} | 
 |    */ | 
 |   entryFont(entryIndex) {}, | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    * @return {string} | 
 |    */ | 
 |   entryColor(entryIndex) {}, | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    * @param {!CanvasRenderingContext2D} context | 
 |    * @param {?string} text | 
 |    * @param {number} barX | 
 |    * @param {number} barY | 
 |    * @param {number} barWidth | 
 |    * @param {number} barHeight | 
 |    * @param {number} unclippedBarX | 
 |    * @param {number} timeToPixelRatio | 
 |    * @return {boolean} | 
 |    */ | 
 |   decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, unclippedBarX, timeToPixelRatio) {}, | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    * @return {boolean} | 
 |    */ | 
 |   forceDecoration(entryIndex) {}, | 
 |  | 
 |   /** | 
 |    * @param {number} entryIndex | 
 |    * @return {string} | 
 |    */ | 
 |   textColor(entryIndex) {}, | 
 | }; | 
 |  | 
 | /** | 
 |  * @interface | 
 |  */ | 
 | PerfUI.FlameChartMarker = function() {}; | 
 |  | 
 | PerfUI.FlameChartMarker.prototype = { | 
 |   /** | 
 |    * @return {number} | 
 |    */ | 
 |   startTime() {}, | 
 |  | 
 |   /** | 
 |    * @return {string} | 
 |    */ | 
 |   color() {}, | 
 |  | 
 |   /** | 
 |    * @return {?string} | 
 |    */ | 
 |   title() {}, | 
 |  | 
 |   /** | 
 |    * @param {!CanvasRenderingContext2D} context | 
 |    * @param {number} x | 
 |    * @param {number} height | 
 |    * @param {number} pixelsPerMillisecond | 
 |    */ | 
 |   draw(context, x, height, pixelsPerMillisecond) {}, | 
 | }; | 
 |  | 
 | /** @enum {symbol} */ | 
 | PerfUI.FlameChart.Events = { | 
 |   CanvasFocused: Symbol('CanvasFocused'), | 
 |   EntryInvoked: Symbol('EntryInvoked'), | 
 |   EntrySelected: Symbol('EntrySelected'), | 
 |   EntryHighlighted: Symbol('EntryHighlighted') | 
 | }; | 
 |  | 
 | PerfUI.FlameChart.Colors = { | 
 |   SelectedGroupBackground: 'hsl(215, 85%, 98%)', | 
 |   SelectedGroupBorder: 'hsl(216, 68%, 54%)', | 
 | }; |