Home | History | Annotate | Download | only in components
      1 /**
      2  * Copyright (C) 2013 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 /**
     32  * @interface
     33  */
     34 WebInspector.FlameChartDelegate = function() { }
     35 
     36 WebInspector.FlameChartDelegate.prototype = {
     37     /**
     38      * @param {number} startTime
     39      * @param {number} endTime
     40      */
     41     requestWindowTimes: function(startTime, endTime) { }
     42 }
     43 
     44 /**
     45  * @constructor
     46  * @extends {WebInspector.HBox}
     47  * @param {!WebInspector.FlameChartDataProvider} dataProvider
     48  * @param {!WebInspector.FlameChartDelegate} flameChartDelegate
     49  * @param {boolean} isTopDown
     50  */
     51 WebInspector.FlameChart = function(dataProvider, flameChartDelegate, isTopDown)
     52 {
     53     WebInspector.HBox.call(this);
     54     this.element.classList.add("flame-chart-main-pane");
     55     this._flameChartDelegate = flameChartDelegate;
     56     this._isTopDown = isTopDown;
     57 
     58     this._calculator = new WebInspector.FlameChart.Calculator();
     59 
     60     this._canvas = this.element.createChild("canvas");
     61     this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), false);
     62     this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
     63     this._canvas.addEventListener("click", this._onClick.bind(this), false);
     64     WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "move", null);
     65 
     66     this._vScrollElement = this.element.createChild("div", "flame-chart-v-scroll");
     67     this._vScrollContent = this._vScrollElement.createChild("div");
     68     this._vScrollElement.addEventListener("scroll", this._scheduleUpdate.bind(this), false);
     69 
     70     this._entryInfo = this.element.createChild("div", "profile-entry-info");
     71     this._highlightElement = this.element.createChild("div", "flame-chart-highlight-element");
     72     this._selectedElement = this.element.createChild("div", "flame-chart-selected-element");
     73 
     74     this._dataProvider = dataProvider;
     75 
     76     this._windowLeft = 0.0;
     77     this._windowRight = 1.0;
     78     this._windowWidth = 1.0;
     79     this._timeWindowLeft = 0;
     80     this._timeWindowRight = Infinity;
     81     this._barHeight = dataProvider.barHeight();
     82     this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight;
     83     this._minWidth = 1;
     84     this._paddingLeft = this._dataProvider.paddingLeft();
     85     this._markerPadding = 2;
     86     this._markerRadius = this._barHeight / 2 - this._markerPadding;
     87     this._highlightedEntryIndex = -1;
     88     this._selectedEntryIndex = -1;
     89     this._textWidth = {};
     90 }
     91 
     92 WebInspector.FlameChart.DividersBarHeight = 20;
     93 
     94 /**
     95  * @interface
     96  */
     97 WebInspector.FlameChartDataProvider = function()
     98 {
     99 }
    100 
    101 /** @typedef {!{
    102         entryLevels: (!Array.<number>|!Uint8Array),
    103         entryTotalTimes: (!Array.<number>|!Float32Array),
    104         entryStartTimes: (!Array.<number>|!Float64Array)
    105     }}
    106  */
    107 WebInspector.FlameChart.TimelineData;
    108 
    109 WebInspector.FlameChartDataProvider.prototype = {
    110     /**
    111      * @return {number}
    112      */
    113     barHeight: function() { },
    114 
    115     /**
    116      * @param {number} startTime
    117      * @param {number} endTime
    118      * @return {?Array.<number>}
    119      */
    120     dividerOffsets: function(startTime, endTime) { },
    121 
    122     /**
    123      * @return {number}
    124      */
    125     minimumBoundary: function() { },
    126 
    127     /**
    128      * @return {number}
    129      */
    130     totalTime: function() { },
    131 
    132     /**
    133      * @return {number}
    134      */
    135     maxStackDepth: function() { },
    136 
    137     /**
    138      * @return {?WebInspector.FlameChart.TimelineData}
    139      */
    140     timelineData: function() { },
    141 
    142     /**
    143      * @param {number} entryIndex
    144      * @return {?Array.<!{title: string, text: string}>}
    145      */
    146     prepareHighlightedEntryInfo: function(entryIndex) { },
    147 
    148     /**
    149      * @param {number} entryIndex
    150      * @return {boolean}
    151      */
    152     canJumpToEntry: function(entryIndex) { },
    153 
    154     /**
    155      * @param {number} entryIndex
    156      * @return {?string}
    157      */
    158     entryTitle: function(entryIndex) { },
    159 
    160     /**
    161      * @param {number} entryIndex
    162      * @return {?string}
    163      */
    164     entryFont: function(entryIndex) { },
    165 
    166     /**
    167      * @param {number} entryIndex
    168      * @return {string}
    169      */
    170     entryColor: function(entryIndex) { },
    171 
    172     /**
    173      * @param {number} entryIndex
    174      * @param {!CanvasRenderingContext2D} context
    175      * @param {?string} text
    176      * @param {number} barX
    177      * @param {number} barY
    178      * @param {number} barWidth
    179      * @param {number} barHeight
    180      * @param {function(number):number} timeToPosition
    181      * @return {boolean}
    182      */
    183     decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) { },
    184 
    185     /**
    186      * @param {number} entryIndex
    187      * @return {boolean}
    188      */
    189     forceDecoration: function(entryIndex) { },
    190 
    191     /**
    192      * @param {number} entryIndex
    193      * @return {string}
    194      */
    195     textColor: function(entryIndex) { },
    196 
    197     /**
    198      * @return {number}
    199      */
    200     textBaseline: function() { },
    201 
    202     /**
    203      * @return {number}
    204      */
    205     textPadding: function() { },
    206 
    207     /**
    208      * @return {?{startTime: number, endTime: number}}
    209      */
    210     highlightTimeRange: function(entryIndex) { },
    211 
    212     /**
    213      * @return {number}
    214      */
    215     paddingLeft: function() { },
    216 }
    217 
    218 WebInspector.FlameChart.Events = {
    219     EntrySelected: "EntrySelected"
    220 }
    221 
    222 
    223 /**
    224  * @constructor
    225  * @param {!{min: number, max: number, count: number}|number=} hueSpace
    226  * @param {!{min: number, max: number, count: number}|number=} satSpace
    227  * @param {!{min: number, max: number, count: number}|number=} lightnessSpace
    228  */
    229 WebInspector.FlameChart.ColorGenerator = function(hueSpace, satSpace, lightnessSpace)
    230 {
    231     this._hueSpace = hueSpace || { min: 0, max: 360, count: 20 };
    232     this._satSpace = satSpace || 67;
    233     this._lightnessSpace = lightnessSpace || 80;
    234     this._colors = {};
    235 }
    236 
    237 WebInspector.FlameChart.ColorGenerator.prototype = {
    238     /**
    239      * @param {string} id
    240      * @param {string|!CanvasGradient} color
    241      */
    242     setColorForID: function(id, color)
    243     {
    244         this._colors[id] = color;
    245     },
    246 
    247     /**
    248      * @param {string} id
    249      * @return {string}
    250      */
    251     colorForID: function(id)
    252     {
    253         var color = this._colors[id];
    254         if (!color) {
    255             color = this._generateColorForID(id);
    256             this._colors[id] = color;
    257         }
    258         return color;
    259     },
    260 
    261     /**
    262      * @param {string} id
    263      * @return {string}
    264      */
    265     _generateColorForID: function(id)
    266     {
    267         var hash = id.hashCode();
    268         var h = this._indexToValueInSpace(hash, this._hueSpace);
    269         var s = this._indexToValueInSpace(hash, this._satSpace);
    270         var l = this._indexToValueInSpace(hash, this._lightnessSpace);
    271         return "hsl(" + h + ", " + s + "%, " + l + "%)";
    272     },
    273 
    274     /**
    275      * @param {number} index
    276      * @param {!{min: number, max: number, count: number}|number} space
    277      * @return {number}
    278      */
    279     _indexToValueInSpace: function(index, space)
    280     {
    281         if (typeof space === "number")
    282             return space;
    283         index %= space.count;
    284         return space.min + Math.floor(index / space.count * (space.max - space.min));
    285     }
    286 }
    287 
    288 
    289 /**
    290  * @constructor
    291  * @implements {WebInspector.TimelineGrid.Calculator}
    292  */
    293 WebInspector.FlameChart.Calculator = function()
    294 {
    295     this._paddingLeft = 0;
    296 }
    297 
    298 WebInspector.FlameChart.Calculator.prototype = {
    299     /**
    300      * @return {number}
    301      */
    302     paddingLeft: function()
    303     {
    304         return this._paddingLeft;
    305     },
    306 
    307     /**
    308      * @param {!WebInspector.FlameChart} mainPane
    309      */
    310     _updateBoundaries: function(mainPane)
    311     {
    312         this._totalTime = mainPane._dataProvider.totalTime();
    313         this._zeroTime = mainPane._dataProvider.minimumBoundary();
    314         this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime;
    315         this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime;
    316         this._paddingLeft = mainPane._paddingLeft;
    317         this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft;
    318         this._timeToPixel = this._width / this.boundarySpan();
    319     },
    320 
    321     /**
    322      * @param {number} time
    323      * @return {number}
    324      */
    325     computePosition: function(time)
    326     {
    327         return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft);
    328     },
    329 
    330     /**
    331      * @param {number} value
    332      * @param {number=} precision
    333      * @return {string}
    334      */
    335     formatTime: function(value, precision)
    336     {
    337         return Number.preciseMillisToString(value - this._zeroTime, precision);
    338     },
    339 
    340     /**
    341      * @return {number}
    342      */
    343     maximumBoundary: function()
    344     {
    345         return this._maximumBoundaries;
    346     },
    347 
    348     /**
    349      * @return {number}
    350      */
    351     minimumBoundary: function()
    352     {
    353         return this._minimumBoundaries;
    354     },
    355 
    356     /**
    357      * @return {number}
    358      */
    359     zeroTime: function()
    360     {
    361         return this._zeroTime;
    362     },
    363 
    364     /**
    365      * @return {number}
    366      */
    367     boundarySpan: function()
    368     {
    369         return this._maximumBoundaries - this._minimumBoundaries;
    370     }
    371 }
    372 
    373 WebInspector.FlameChart.prototype = {
    374     _resetCanvas: function()
    375     {
    376         var ratio = window.devicePixelRatio;
    377         this._canvas.width = this._offsetWidth * ratio;
    378         this._canvas.height = this._offsetHeight * ratio;
    379         this._canvas.style.width = this._offsetWidth + "px";
    380         this._canvas.style.height = this._offsetHeight + "px";
    381     },
    382 
    383     /**
    384      * @return {?WebInspector.FlameChart.TimelineData}
    385      */
    386     _timelineData: function()
    387     {
    388         var timelineData = this._dataProvider.timelineData();
    389         if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength)
    390             this._processTimelineData(timelineData);
    391         return this._rawTimelineData;
    392     },
    393 
    394     /**
    395      * @param {number} startTime
    396      * @param {number} endTime
    397      */
    398     setWindowTimes: function(startTime, endTime)
    399     {
    400         this._timeWindowLeft = startTime;
    401         this._timeWindowRight = endTime;
    402         this._scheduleUpdate();
    403     },
    404 
    405     /**
    406      * @param {!MouseEvent} event
    407      */
    408     _startCanvasDragging: function(event)
    409     {
    410         if (!this._timelineData() || this._timeWindowRight === Infinity)
    411             return false;
    412         this._isDragging = true;
    413         this._maxDragOffset = 0;
    414         this._dragStartPointX = event.pageX;
    415         this._dragStartPointY = event.pageY;
    416         this._dragStartScrollTop = this._vScrollElement.scrollTop;
    417         this._dragStartWindowLeft = this._timeWindowLeft;
    418         this._dragStartWindowRight = this._timeWindowRight;
    419         this._canvas.style.cursor = "";
    420 
    421         return true;
    422     },
    423 
    424     /**
    425      * @param {!MouseEvent} event
    426      */
    427     _canvasDragging: function(event)
    428     {
    429         var pixelShift = this._dragStartPointX - event.pageX;
    430         var pixelScroll = this._dragStartPointY - event.pageY;
    431         this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
    432         var windowShift = pixelShift / this._totalPixels;
    433         var windowTime = this._windowWidth * this._totalTime;
    434         var timeShift = windowTime * pixelShift / this._pixelWindowWidth;
    435         timeShift = Number.constrain(
    436             timeShift,
    437             this._minimumBoundary - this._dragStartWindowLeft,
    438             this._minimumBoundary + this._totalTime - this._dragStartWindowRight
    439         );
    440         var windowLeft = this._dragStartWindowLeft + timeShift;
    441         var windowRight = this._dragStartWindowRight + timeShift;
    442         this._flameChartDelegate.requestWindowTimes(windowLeft, windowRight);
    443         this._maxDragOffset = Math.max(this._maxDragOffset, Math.abs(pixelShift));
    444     },
    445 
    446     _endCanvasDragging: function()
    447     {
    448         this._isDragging = false;
    449     },
    450 
    451     /**
    452      * @param {?Event} event
    453      */
    454     _onMouseMove: function(event)
    455     {
    456         if (this._isDragging)
    457             return;
    458         var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);
    459 
    460         if (this._highlightedEntryIndex === entryIndex)
    461             return;
    462 
    463         if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
    464             this._canvas.style.cursor = "default";
    465         else
    466             this._canvas.style.cursor = "pointer";
    467 
    468         this._highlightedEntryIndex = entryIndex;
    469 
    470         this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
    471         this._entryInfo.removeChildren();
    472 
    473         if (this._highlightedEntryIndex === -1)
    474             return;
    475 
    476         if (!this._isDragging) {
    477             var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex);
    478             if (entryInfo)
    479                 this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
    480         }
    481     },
    482 
    483     _onClick: function()
    484     {
    485         // onClick comes after dragStart and dragEnd events.
    486         // So if there was drag (mouse move) in the middle of that events
    487         // we skip the click. Otherwise we jump to the sources.
    488         const clickThreshold = 5;
    489         if (this._maxDragOffset > clickThreshold)
    490             return;
    491         if (this._highlightedEntryIndex === -1)
    492             return;
    493         this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex);
    494     },
    495 
    496     /**
    497      * @param {?Event} e
    498      */
    499     _onMouseWheel: function(e)
    500     {
    501         var scrollIsThere = this._totalHeight > this._offsetHeight;
    502         var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
    503         var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
    504 
    505         var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey;
    506         var panVertically = scrollIsThere && ((e.wheelDeltaY && !e.shiftKey) || (Math.abs(e.wheelDeltaX) === 120 && !e.shiftKey));
    507         if (panVertically) {
    508             this._vScrollElement.scrollTop -= e.wheelDeltaY / 120 * this._offsetHeight / 8;
    509         } else if (panHorizontally) {
    510             var shift = -e.wheelDeltaX * this._pixelToTime;
    511             shift = Number.constrain(shift, this._minimumBoundary - windowLeft, this._totalTime + this._minimumBoundary - windowRight);
    512             windowLeft += shift;
    513             windowRight += shift;
    514         } else {  // Zoom.
    515             const mouseWheelZoomSpeed = 1 / 120;
    516             var zoom = Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1;
    517             var cursorTime = this._cursorTime(e.offsetX);
    518             windowLeft += (windowLeft - cursorTime) * zoom;
    519             windowRight += (windowRight - cursorTime) * zoom;
    520         }
    521         windowLeft = Number.constrain(windowLeft, this._minimumBoundary, this._totalTime + this._minimumBoundary);
    522         windowRight = Number.constrain(windowRight, this._minimumBoundary, this._totalTime + this._minimumBoundary);
    523         this._flameChartDelegate.requestWindowTimes(windowLeft, windowRight);
    524 
    525         // Block swipe gesture.
    526         e.consume(true);
    527     },
    528 
    529     /**
    530      * @param {number} x
    531      * @return {number}
    532      */
    533     _cursorTime: function(x)
    534     {
    535         return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._minimumBoundary;
    536     },
    537 
    538     /**
    539      * @param {number} x
    540      * @param {number} y
    541      * @return {number}
    542      */
    543     _coordinatesToEntryIndex: function(x, y)
    544     {
    545         y += this._scrollTop;
    546         var timelineData = this._timelineData();
    547         if (!timelineData)
    548             return -1;
    549         var cursorTime = this._cursorTime(x);
    550         var cursorLevel;
    551         var offsetFromLevel;
    552         if (this._isTopDown) {
    553             cursorLevel = Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight);
    554             offsetFromLevel = y - WebInspector.FlameChart.DividersBarHeight - cursorLevel * this._barHeight;
    555         } else {
    556             cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
    557             offsetFromLevel = this._canvas.height / window.devicePixelRatio - cursorLevel * this._barHeight;
    558         }
    559         var entryStartTimes = timelineData.entryStartTimes;
    560         var entryTotalTimes = timelineData.entryTotalTimes;
    561         var entryIndexes = this._timelineLevels[cursorLevel];
    562         if (!entryIndexes || !entryIndexes.length)
    563             return -1;
    564 
    565         function comparator(time, entryIndex)
    566         {
    567             return time - entryStartTimes[entryIndex];
    568         }
    569         var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator) - 1, 0);
    570 
    571         /**
    572          * @this {WebInspector.FlameChart}
    573          * @param {number} entryIndex
    574          * @return {boolean}
    575          */
    576         function checkEntryHit(entryIndex)
    577         {
    578             if (entryIndex === undefined)
    579                 return false;
    580             var startTime = entryStartTimes[entryIndex];
    581             var duration = entryTotalTimes[entryIndex];
    582             if (isNaN(duration)) {
    583                 var dx = (startTime - cursorTime) / this._pixelToTime;
    584                 var dy = this._barHeight / 2 - offsetFromLevel;
    585                 return dx * dx + dy * dy < this._markerRadius * this._markerRadius;
    586             }
    587             var endTime = startTime + duration;
    588             var barThreshold = 3 * this._pixelToTime;
    589             return startTime - barThreshold < cursorTime && cursorTime < endTime + barThreshold;
    590         }
    591 
    592         var entryIndex = entryIndexes[indexOnLevel];
    593         if (checkEntryHit.call(this, entryIndex))
    594             return entryIndex;
    595         entryIndex = entryIndexes[indexOnLevel + 1];
    596         if (checkEntryHit.call(this, entryIndex))
    597             return entryIndex;
    598         return -1;
    599     },
    600 
    601     /**
    602      * @param {number} height
    603      * @param {number} width
    604      */
    605     _draw: function(width, height)
    606     {
    607         var timelineData = this._timelineData();
    608         if (!timelineData)
    609             return;
    610 
    611         var context = this._canvas.getContext("2d");
    612         context.save();
    613         var ratio = window.devicePixelRatio;
    614         context.scale(ratio, ratio);
    615 
    616         var timeWindowRight = this._timeWindowRight;
    617         var timeWindowLeft = this._timeWindowLeft;
    618         var timeToPixel = this._timeToPixel;
    619         var pixelWindowLeft = this._pixelWindowLeft;
    620         var paddingLeft = this._paddingLeft;
    621         var minWidth = this._minWidth;
    622         var entryTotalTimes = timelineData.entryTotalTimes;
    623         var entryStartTimes = timelineData.entryStartTimes;
    624         var entryLevels = timelineData.entryLevels;
    625 
    626         var titleIndices = new Uint32Array(timelineData.entryTotalTimes);
    627         var nextTitleIndex = 0;
    628         var markerIndices = new Uint32Array(timelineData.entryTotalTimes);
    629         var nextMarkerIndex = 0;
    630         var textPadding = this._dataProvider.textPadding();
    631         this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026");
    632         var minTextWidth = this._minTextWidth;
    633 
    634         var barHeight = this._barHeight;
    635 
    636         var timeToPosition = this._timeToPosition.bind(this);
    637         var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline();
    638         var colorBuckets = {};
    639         var minVisibleBarLevel = Math.max(Math.floor((this._scrollTop - this._baseHeight) / barHeight), 0);
    640         var maxVisibleBarLevel = Math.min(Math.floor((this._scrollTop - this._baseHeight + height) / barHeight), this._dataProvider.maxStackDepth());
    641 
    642         context.translate(0, -this._scrollTop);
    643 
    644         function comparator(time, entryIndex)
    645         {
    646             return time - entryStartTimes[entryIndex];
    647         }
    648 
    649         for (var level = minVisibleBarLevel; level <= maxVisibleBarLevel; ++level) {
    650             // Entries are ordered by start time within a level, so find the last visible entry.
    651             var levelIndexes = this._timelineLevels[level];
    652             var rightIndexOnLevel = levelIndexes.lowerBound(timeWindowRight, comparator) - 1;
    653             var lastDrawOffset = Infinity;
    654             for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
    655                 var entryIndex = levelIndexes[entryIndexOnLevel];
    656                 var entryStartTime = entryStartTimes[entryIndex];
    657                 var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]);
    658                 if (entryOffsetRight <= timeWindowLeft)
    659                     break;
    660 
    661                 var barX = this._timeToPosition(entryStartTime);
    662                 if (barX >= lastDrawOffset)
    663                     continue;
    664                 var barRight = Math.min(this._timeToPosition(entryOffsetRight), lastDrawOffset);
    665                 lastDrawOffset = barX;
    666 
    667                 var color = this._dataProvider.entryColor(entryIndex);
    668                 var bucket = colorBuckets[color];
    669                 if (!bucket) {
    670                     bucket = [];
    671                     colorBuckets[color] = bucket;
    672                 }
    673                 bucket.push(entryIndex);
    674             }
    675         }
    676 
    677         var colors = Object.keys(colorBuckets);
    678         // We don't use for-in here because it couldn't be optimized.
    679         for (var c = 0; c < colors.length; ++c) {
    680             var color = colors[c];
    681             context.fillStyle = color;
    682             context.strokeStyle = color;
    683             var indexes = colorBuckets[color];
    684 
    685             // First fill the boxes.
    686             context.beginPath();
    687             for (var i = 0; i < indexes.length; ++i) {
    688                 var entryIndex = indexes[i];
    689                 var entryStartTime = entryStartTimes[entryIndex];
    690                 var barX = this._timeToPosition(entryStartTime);
    691                 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]);
    692                 var barWidth = Math.max(barRight - barX, minWidth);
    693                 var barLevel = entryLevels[entryIndex];
    694                 var barY = this._levelToHeight(barLevel);
    695                 if (isNaN(entryTotalTimes[entryIndex])) {
    696                     context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
    697                     context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
    698                     markerIndices[nextMarkerIndex++] = entryIndex;
    699                 } else {
    700                     context.rect(barX, barY, barWidth, barHeight);
    701                     if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex))
    702                         titleIndices[nextTitleIndex++] = entryIndex;
    703                 }
    704             }
    705             context.fill();
    706         }
    707 
    708         context.strokeStyle = "rgb(0, 0, 0)";
    709         context.beginPath();
    710         for (var m = 0; m < nextMarkerIndex; ++m) {
    711             var entryIndex = markerIndices[m];
    712             var entryStartTime = entryStartTimes[entryIndex];
    713             var barX = this._timeToPosition(entryStartTime);
    714             var barLevel = entryLevels[entryIndex];
    715             var barY = this._levelToHeight(barLevel);
    716             context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
    717             context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
    718         }
    719         context.stroke();
    720 
    721         context.textBaseline = "alphabetic";
    722 
    723         for (var i = 0; i < nextTitleIndex; ++i) {
    724             var entryIndex = titleIndices[i];
    725             var entryStartTime = entryStartTimes[entryIndex];
    726             var barX = this._timeToPosition(entryStartTime);
    727             var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]);
    728             var barWidth = Math.max(barRight - barX, minWidth);
    729             var barLevel = entryLevels[entryIndex];
    730             var barY = this._levelToHeight(barLevel);
    731             var text = this._dataProvider.entryTitle(entryIndex);
    732             if (text && text.length) {
    733                 context.font = this._dataProvider.entryFont(entryIndex);
    734                 text = this._prepareText(context, text, barWidth - 2 * textPadding);
    735             }
    736 
    737             if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition))
    738                 continue;
    739             if (!text || !text.length)
    740                 continue;
    741 
    742             context.fillStyle = this._dataProvider.textColor(entryIndex);
    743             context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta);
    744         }
    745         context.restore();
    746 
    747         var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary());
    748         WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets);
    749 
    750         this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
    751         this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
    752     },
    753 
    754     /**
    755      * @param {?WebInspector.FlameChart.TimelineData} timelineData
    756      */
    757     _processTimelineData: function(timelineData)
    758     {
    759         if (!timelineData) {
    760             this._timelineLevels = null;
    761             this._rawTimelineData = null;
    762             this._rawTimelineDataLength = 0;
    763             return;
    764         }
    765 
    766         var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1);
    767         for (var i = 0; i < timelineData.entryLevels.length; ++i)
    768             ++entryCounters[timelineData.entryLevels[i]];
    769         var levelIndexes = new Array(entryCounters.length);
    770         for (var i = 0; i < levelIndexes.length; ++i) {
    771             levelIndexes[i] = new Uint32Array(entryCounters[i]);
    772             entryCounters[i] = 0;
    773         }
    774         for (var i = 0; i < timelineData.entryLevels.length; ++i) {
    775             var level = timelineData.entryLevels[i];
    776             levelIndexes[level][entryCounters[level]++] = i;
    777         }
    778         this._timelineLevels = levelIndexes;
    779         this._rawTimelineData = timelineData;
    780         this._rawTimelineDataLength = timelineData.entryStartTimes.length;
    781     },
    782 
    783     /**
    784      * @param {number} entryIndex
    785      */
    786     setSelectedEntry: function(entryIndex)
    787     {
    788         this._selectedEntryIndex = entryIndex;
    789         this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
    790     },
    791 
    792     _updateElementPosition: function(element, entryIndex)
    793     {
    794         if (element.parentElement)
    795             element.remove();
    796         if (entryIndex === -1)
    797             return;
    798         var timeRange = this._dataProvider.highlightTimeRange(entryIndex);
    799         if (!timeRange)
    800             return;
    801         var timelineData = this._timelineData();
    802         var barX = this._timeToPosition(timeRange.startTime);
    803         var barRight = this._timeToPosition(timeRange.endTime);
    804         if (barRight === 0 || barX === this._canvas.width)
    805             return;
    806         var barWidth = Math.max(barRight - barX, this._minWidth);
    807         var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop;
    808         var style = element.style;
    809         style.left = barX + "px";
    810         style.top = barY + "px";
    811         style.width = barWidth + "px";
    812         style.height = this._barHeight + "px";
    813         this.element.appendChild(element);
    814     },
    815 
    816     /**
    817      * @param {number} time
    818      */
    819     _timeToPosition: function(time)
    820     {
    821         var value = Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
    822         return Math.min(this._canvas.width, Math.max(0, value));
    823     },
    824 
    825     _levelToHeight: function(level)
    826     {
    827          return this._baseHeight - level * this._barHeightDelta;
    828     },
    829 
    830     _buildEntryInfo: function(entryInfo)
    831     {
    832         var infoTable = document.createElement("table");
    833         infoTable.className = "info-table";
    834         for (var i = 0; i < entryInfo.length; ++i) {
    835             var row = infoTable.createChild("tr");
    836             var titleCell = row.createChild("td");
    837             titleCell.textContent = entryInfo[i].title;
    838             titleCell.className = "title";
    839             var textCell = row.createChild("td");
    840             textCell.textContent = entryInfo[i].text;
    841         }
    842         return infoTable;
    843     },
    844 
    845     /**
    846      * @param {!CanvasRenderingContext2D} context
    847      * @param {string} title
    848      * @param {number} maxSize
    849      * @return {string}
    850      */
    851     _prepareText: function(context, title, maxSize)
    852     {
    853         var titleWidth = this._measureWidth(context, title);
    854         if (maxSize >= titleWidth)
    855             return title;
    856 
    857         var l = 2;
    858         var r = title.length;
    859         while (l < r) {
    860             var m = (l + r) >> 1;
    861             if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize)
    862                 l = m + 1;
    863             else
    864                 r = m;
    865         }
    866         title = title.trimMiddle(r - 1);
    867         return title !== "\u2026" ? title : "";
    868     },
    869 
    870     /**
    871      * @param {!CanvasRenderingContext2D} context
    872      * @param {string} text
    873      * @return {number}
    874      */
    875     _measureWidth: function(context, text)
    876     {
    877         if (text.length > 20)
    878             return context.measureText(text).width;
    879 
    880         var font = context.font;
    881         var textWidths = this._textWidth[font];
    882         if (!textWidths) {
    883             textWidths = {};
    884             this._textWidth[font] = textWidths;
    885         }
    886         var width = textWidths[text];
    887         if (!width) {
    888             width = context.measureText(text).width;
    889             textWidths[text] = width;
    890         }
    891         return width;
    892     },
    893 
    894     _updateBoundaries: function()
    895     {
    896         this._totalTime = this._dataProvider.totalTime();
    897         this._minimumBoundary = this._dataProvider.minimumBoundary();
    898 
    899         if (this._timeWindowRight !== Infinity) {
    900             this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime;
    901             this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime;
    902             this._windowWidth = this._windowRight - this._windowLeft;
    903         } else {
    904             this._windowLeft = 0;
    905             this._windowRight = 1;
    906             this._windowWidth = 1;
    907         }
    908 
    909         this._pixelWindowWidth = this._offsetWidth - this._paddingLeft;
    910         this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
    911         this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
    912         this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
    913 
    914         this._timeToPixel = this._totalPixels / this._totalTime;
    915         this._pixelToTime = this._totalTime / this._totalPixels;
    916         this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
    917 
    918         this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight;
    919 
    920         this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth() + 1);
    921         this._vScrollContent.style.height = this._totalHeight + "px";
    922         this._scrollTop = this._vScrollElement.scrollTop;
    923         this._updateScrollBar();
    924     },
    925 
    926     onResize: function()
    927     {
    928         this._updateScrollBar();
    929         this._scheduleUpdate();
    930     },
    931 
    932     _updateScrollBar: function()
    933     {
    934         var showScroll = this._totalHeight > this._offsetHeight;
    935         this._vScrollElement.classList.toggle("hidden", !showScroll);
    936         this._offsetWidth = this.element.offsetWidth - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth);
    937         this._offsetHeight = this.element.offsetHeight;
    938     },
    939 
    940     _scheduleUpdate: function()
    941     {
    942         if (this._updateTimerId)
    943             return;
    944         this._updateTimerId = requestAnimationFrame(this.update.bind(this));
    945     },
    946 
    947     update: function()
    948     {
    949         this._updateTimerId = 0;
    950         if (!this._timelineData())
    951             return;
    952         this._resetCanvas();
    953         this._updateBoundaries();
    954         this._calculator._updateBoundaries(this);
    955         this._draw(this._offsetWidth, this._offsetHeight);
    956     },
    957 
    958     reset: function()
    959     {
    960         this._highlightedEntryIndex = -1;
    961         this._selectedEntryIndex = -1;
    962         this._textWidth = {};
    963         this.update();
    964     },
    965 
    966     __proto__: WebInspector.HBox.prototype
    967 }
    968