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.tabIndex = 1;
     62     this.setDefaultFocusedElement(this._canvas);
     63     this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), false);
     64     this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
     65     this._canvas.addEventListener("click", this._onClick.bind(this), false);
     66     this._canvas.addEventListener("keydown", this._onKeyDown.bind(this), false);
     67     WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "move", null);
     68 
     69     this._vScrollElement = this.element.createChild("div", "flame-chart-v-scroll");
     70     this._vScrollContent = this._vScrollElement.createChild("div");
     71     this._vScrollElement.addEventListener("scroll", this.scheduleUpdate.bind(this), false);
     72 
     73     this._entryInfo = this.element.createChild("div", "profile-entry-info");
     74     this._markerHighlighElement = this.element.createChild("div", "flame-chart-marker-highlight-element");
     75     this._highlightElement = this.element.createChild("div", "flame-chart-highlight-element");
     76     this._selectedElement = this.element.createChild("div", "flame-chart-selected-element");
     77 
     78     this._dataProvider = dataProvider;
     79 
     80     this._windowLeft = 0.0;
     81     this._windowRight = 1.0;
     82     this._windowWidth = 1.0;
     83     this._timeWindowLeft = 0;
     84     this._timeWindowRight = Infinity;
     85     this._barHeight = dataProvider.barHeight();
     86     this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight;
     87     this._minWidth = 1;
     88     this._paddingLeft = this._dataProvider.paddingLeft();
     89     this._markerPadding = 2;
     90     this._markerRadius = this._barHeight / 2 - this._markerPadding;
     91     this._highlightedMarkerIndex = -1;
     92     this._highlightedEntryIndex = -1;
     93     this._selectedEntryIndex = -1;
     94     this._textWidth = {};
     95 }
     96 
     97 WebInspector.FlameChart.DividersBarHeight = 20;
     98 
     99 /**
    100  * @interface
    101  */
    102 WebInspector.FlameChartDataProvider = function()
    103 {
    104 }
    105 
    106 /**
    107  * @constructor
    108  * @param {!Array.<number>|!Uint8Array} entryLevels
    109  * @param {!Array.<number>|!Float32Array} entryTotalTimes
    110  * @param {!Array.<number>|!Float64Array} entryStartTimes
    111  */
    112 WebInspector.FlameChart.TimelineData = function(entryLevels, entryTotalTimes, entryStartTimes)
    113 {
    114     this.entryLevels = entryLevels;
    115     this.entryTotalTimes = entryTotalTimes;
    116     this.entryStartTimes = entryStartTimes;
    117     /** @type {!Array.<number>} */
    118     this.markerTimestamps = [];
    119 }
    120 
    121 WebInspector.FlameChartDataProvider.prototype = {
    122     /**
    123      * @return {number}
    124      */
    125     barHeight: function() { },
    126 
    127     /**
    128      * @param {number} startTime
    129      * @param {number} endTime
    130      * @return {?Array.<number>}
    131      */
    132     dividerOffsets: function(startTime, endTime) { },
    133 
    134     /**
    135      * @param {number} index
    136      * @return {string}
    137      */
    138     markerColor: function(index) { },
    139 
    140     /**
    141      * @param {number} index
    142      * @return {string}
    143      */
    144     markerTitle: function(index) { },
    145 
    146     /**
    147      * @return {number}
    148      */
    149     minimumBoundary: function() { },
    150 
    151     /**
    152      * @return {number}
    153      */
    154     totalTime: function() { },
    155 
    156     /**
    157      * @return {number}
    158      */
    159     maxStackDepth: function() { },
    160 
    161     /**
    162      * @return {?WebInspector.FlameChart.TimelineData}
    163      */
    164     timelineData: function() { },
    165 
    166     /**
    167      * @param {number} entryIndex
    168      * @return {?Array.<!{title: string, text: string}>}
    169      */
    170     prepareHighlightedEntryInfo: function(entryIndex) { },
    171 
    172     /**
    173      * @param {number} entryIndex
    174      * @return {boolean}
    175      */
    176     canJumpToEntry: function(entryIndex) { },
    177 
    178     /**
    179      * @param {number} entryIndex
    180      * @return {?string}
    181      */
    182     entryTitle: function(entryIndex) { },
    183 
    184     /**
    185      * @param {number} entryIndex
    186      * @return {?string}
    187      */
    188     entryFont: function(entryIndex) { },
    189 
    190     /**
    191      * @param {number} entryIndex
    192      * @return {string}
    193      */
    194     entryColor: function(entryIndex) { },
    195 
    196     /**
    197      * @param {number} entryIndex
    198      * @param {!CanvasRenderingContext2D} context
    199      * @param {?string} text
    200      * @param {number} barX
    201      * @param {number} barY
    202      * @param {number} barWidth
    203      * @param {number} barHeight
    204      * @param {function(number):number} timeToPosition
    205      * @return {boolean}
    206      */
    207     decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) { },
    208 
    209     /**
    210      * @param {number} entryIndex
    211      * @return {boolean}
    212      */
    213     forceDecoration: function(entryIndex) { },
    214 
    215     /**
    216      * @param {number} entryIndex
    217      * @return {string}
    218      */
    219     textColor: function(entryIndex) { },
    220 
    221     /**
    222      * @return {number}
    223      */
    224     textBaseline: function() { },
    225 
    226     /**
    227      * @return {number}
    228      */
    229     textPadding: function() { },
    230 
    231     /**
    232      * @return {?{startTime: number, endTime: number}}
    233      */
    234     highlightTimeRange: function(entryIndex) { },
    235 
    236     /**
    237      * @return {number}
    238      */
    239     paddingLeft: function() { },
    240 }
    241 
    242 WebInspector.FlameChart.Events = {
    243     EntrySelected: "EntrySelected"
    244 }
    245 
    246 
    247 /**
    248  * @constructor
    249  * @param {!{min: number, max: number, count: number}|number=} hueSpace
    250  * @param {!{min: number, max: number, count: number}|number=} satSpace
    251  * @param {!{min: number, max: number, count: number}|number=} lightnessSpace
    252  * @param {!{min: number, max: number, count: number}|number=} alphaSpace
    253  */
    254 WebInspector.FlameChart.ColorGenerator = function(hueSpace, satSpace, lightnessSpace, alphaSpace)
    255 {
    256     this._hueSpace = hueSpace || { min: 0, max: 360, count: 20 };
    257     this._satSpace = satSpace || 67;
    258     this._lightnessSpace = lightnessSpace || 80;
    259     this._alphaSpace = alphaSpace || 1;
    260     this._colors = {};
    261 }
    262 
    263 WebInspector.FlameChart.ColorGenerator.prototype = {
    264     /**
    265      * @param {string} id
    266      * @param {string|!CanvasGradient} color
    267      */
    268     setColorForID: function(id, color)
    269     {
    270         this._colors[id] = color;
    271     },
    272 
    273     /**
    274      * @param {string} id
    275      * @return {string}
    276      */
    277     colorForID: function(id)
    278     {
    279         var color = this._colors[id];
    280         if (!color) {
    281             color = this._generateColorForID(id);
    282             this._colors[id] = color;
    283         }
    284         return color;
    285     },
    286 
    287     /**
    288      * @param {string} id
    289      * @return {string}
    290      */
    291     _generateColorForID: function(id)
    292     {
    293         var hash = id.hashCode();
    294         var h = this._indexToValueInSpace(hash, this._hueSpace);
    295         var s = this._indexToValueInSpace(hash, this._satSpace);
    296         var l = this._indexToValueInSpace(hash, this._lightnessSpace);
    297         var a = this._indexToValueInSpace(hash, this._alphaSpace);
    298         return "hsla(" + h + ", " + s + "%, " + l + "%, " + a + ")";
    299     },
    300 
    301     /**
    302      * @param {number} index
    303      * @param {!{min: number, max: number, count: number}|number} space
    304      * @return {number}
    305      */
    306     _indexToValueInSpace: function(index, space)
    307     {
    308         if (typeof space === "number")
    309             return space;
    310         index %= space.count;
    311         return space.min + Math.floor(index / space.count * (space.max - space.min));
    312     }
    313 }
    314 
    315 
    316 /**
    317  * @constructor
    318  * @implements {WebInspector.TimelineGrid.Calculator}
    319  */
    320 WebInspector.FlameChart.Calculator = function()
    321 {
    322     this._paddingLeft = 0;
    323 }
    324 
    325 WebInspector.FlameChart.Calculator.prototype = {
    326     /**
    327      * @return {number}
    328      */
    329     paddingLeft: function()
    330     {
    331         return this._paddingLeft;
    332     },
    333 
    334     /**
    335      * @param {!WebInspector.FlameChart} mainPane
    336      */
    337     _updateBoundaries: function(mainPane)
    338     {
    339         this._totalTime = mainPane._dataProvider.totalTime();
    340         this._zeroTime = mainPane._dataProvider.minimumBoundary();
    341         this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime;
    342         this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime;
    343         this._paddingLeft = mainPane._paddingLeft;
    344         this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft;
    345         this._timeToPixel = this._width / this.boundarySpan();
    346     },
    347 
    348     /**
    349      * @param {number} time
    350      * @return {number}
    351      */
    352     computePosition: function(time)
    353     {
    354         return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft);
    355     },
    356 
    357     /**
    358      * @param {number} value
    359      * @param {number=} precision
    360      * @return {string}
    361      */
    362     formatTime: function(value, precision)
    363     {
    364         return Number.preciseMillisToString(value - this._zeroTime, precision);
    365     },
    366 
    367     /**
    368      * @return {number}
    369      */
    370     maximumBoundary: function()
    371     {
    372         return this._maximumBoundaries;
    373     },
    374 
    375     /**
    376      * @return {number}
    377      */
    378     minimumBoundary: function()
    379     {
    380         return this._minimumBoundaries;
    381     },
    382 
    383     /**
    384      * @return {number}
    385      */
    386     zeroTime: function()
    387     {
    388         return this._zeroTime;
    389     },
    390 
    391     /**
    392      * @return {number}
    393      */
    394     boundarySpan: function()
    395     {
    396         return this._maximumBoundaries - this._minimumBoundaries;
    397     }
    398 }
    399 
    400 WebInspector.FlameChart.prototype = {
    401     _resetCanvas: function()
    402     {
    403         var ratio = window.devicePixelRatio;
    404         this._canvas.width = this._offsetWidth * ratio;
    405         this._canvas.height = this._offsetHeight * ratio;
    406         this._canvas.style.width = this._offsetWidth + "px";
    407         this._canvas.style.height = this._offsetHeight + "px";
    408     },
    409 
    410     /**
    411      * @return {?WebInspector.FlameChart.TimelineData}
    412      */
    413     _timelineData: function()
    414     {
    415         var timelineData = this._dataProvider.timelineData();
    416         if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength)
    417             this._processTimelineData(timelineData);
    418         return this._rawTimelineData;
    419     },
    420 
    421     _cancelAnimation: function()
    422     {
    423         if (this._cancelWindowTimesAnimation) {
    424             this._timeWindowLeft = this._pendingAnimationTimeLeft;
    425             this._timeWindowRight = this._pendingAnimationTimeRight;
    426             this._cancelWindowTimesAnimation();
    427             delete this._cancelWindowTimesAnimation;
    428         }
    429     },
    430 
    431     /**
    432      * @param {number} startTime
    433      * @param {number} endTime
    434      */
    435     setWindowTimes: function(startTime, endTime)
    436     {
    437         if (this._muteAnimation || this._timeWindowLeft === 0 || this._timeWindowRight === Infinity) {
    438             // Initial setup.
    439             this._timeWindowLeft = startTime;
    440             this._timeWindowRight = endTime;
    441             this.scheduleUpdate();
    442             return;
    443         }
    444 
    445         this._cancelAnimation();
    446         this._cancelWindowTimesAnimation = WebInspector.animateFunction(this._animateWindowTimes.bind(this),
    447             [{from: this._timeWindowLeft, to: startTime}, {from: this._timeWindowRight, to: endTime}], 5,
    448             this._animationCompleted.bind(this));
    449         this._pendingAnimationTimeLeft = startTime;
    450         this._pendingAnimationTimeRight = endTime;
    451     },
    452 
    453     /**
    454      * @param {number} startTime
    455      * @param {number} endTime
    456      */
    457     _animateWindowTimes: function(startTime, endTime)
    458     {
    459         this._timeWindowLeft = startTime;
    460         this._timeWindowRight = endTime;
    461         this.update();
    462     },
    463 
    464     _animationCompleted: function()
    465     {
    466         delete this._cancelWindowTimesAnimation;
    467     },
    468 
    469     /**
    470      * @param {!MouseEvent} event
    471      */
    472     _startCanvasDragging: function(event)
    473     {
    474         if (!this._timelineData() || this._timeWindowRight === Infinity)
    475             return false;
    476         this._isDragging = true;
    477         this._maxDragOffset = 0;
    478         this._dragStartPointX = event.pageX;
    479         this._dragStartPointY = event.pageY;
    480         this._dragStartScrollTop = this._vScrollElement.scrollTop;
    481         this._dragStartWindowLeft = this._timeWindowLeft;
    482         this._dragStartWindowRight = this._timeWindowRight;
    483         this._canvas.style.cursor = "";
    484 
    485         return true;
    486     },
    487 
    488     /**
    489      * @param {!MouseEvent} event
    490      */
    491     _canvasDragging: function(event)
    492     {
    493         var pixelShift = this._dragStartPointX - event.pageX;
    494         this._dragStartPointX = event.pageX;
    495         this._muteAnimation = true;
    496         this._handlePanGesture(pixelShift * this._pixelToTime);
    497         this._muteAnimation = false;
    498 
    499         var pixelScroll = this._dragStartPointY - event.pageY;
    500         this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
    501         this._maxDragOffset = Math.max(this._maxDragOffset, Math.abs(pixelShift));
    502     },
    503 
    504     _endCanvasDragging: function()
    505     {
    506         this._isDragging = false;
    507     },
    508 
    509     /**
    510      * @param {!Event} event
    511      */
    512     _onMouseMove: function(event)
    513     {
    514         this._lastMouseOffsetX = event.offsetX;
    515 
    516         if (this._isDragging)
    517             return;
    518 
    519         var inDividersBar = event.offsetY < WebInspector.FlameChart.DividersBarHeight;
    520         this._highlightedMarkerIndex = inDividersBar ? this._markerIndexAtPosition(event.offsetX) : -1;
    521         this._updateMarkerHighlight();
    522         if (inDividersBar)
    523             return;
    524 
    525         var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);
    526 
    527         if (this._highlightedEntryIndex === entryIndex)
    528             return;
    529 
    530         if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
    531             this._canvas.style.cursor = "default";
    532         else
    533             this._canvas.style.cursor = "pointer";
    534 
    535         this._highlightedEntryIndex = entryIndex;
    536 
    537         this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
    538         this._entryInfo.removeChildren();
    539 
    540         if (this._highlightedEntryIndex === -1)
    541             return;
    542 
    543         if (!this._isDragging) {
    544             var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex);
    545             if (entryInfo)
    546                 this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
    547         }
    548     },
    549 
    550     _onClick: function()
    551     {
    552         this.focus();
    553         // onClick comes after dragStart and dragEnd events.
    554         // So if there was drag (mouse move) in the middle of that events
    555         // we skip the click. Otherwise we jump to the sources.
    556         const clickThreshold = 5;
    557         if (this._maxDragOffset > clickThreshold)
    558             return;
    559         if (this._highlightedEntryIndex === -1)
    560             return;
    561         this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex);
    562     },
    563 
    564     /**
    565      * @param {!Event} e
    566      */
    567     _onMouseWheel: function(e)
    568     {
    569         // Pan vertically when shift down only.
    570         var panVertically = e.shiftKey && (e.wheelDeltaY || Math.abs(e.wheelDeltaX) === 120);
    571         var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey;
    572         if (panVertically) {
    573             this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 * this._offsetHeight / 8;
    574         } else if (panHorizontally) {
    575             var shift = -e.wheelDeltaX * this._pixelToTime;
    576             this._muteAnimation = true;
    577             this._handlePanGesture(shift);
    578             this._muteAnimation = false;
    579         } else {  // Zoom.
    580             const mouseWheelZoomSpeed = 1 / 120;
    581             this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1);
    582         }
    583 
    584         // Block swipe gesture.
    585         e.consume(true);
    586     },
    587 
    588     /**
    589      * @param {!Event} e
    590      */
    591     _onKeyDown: function(e)
    592     {
    593         if (e.altKey || e.ctrlKey || e.metaKey)
    594             return;
    595         var zoomMultiplier = e.shiftKey ? 0.8 : 0.3;
    596         var panMultiplier = e.shiftKey ? 320 : 80;
    597         if (e.keyCode === "A".charCodeAt(0)) {
    598             this._handlePanGesture(-panMultiplier * this._pixelToTime);
    599             e.consume(true);
    600         } else if (e.keyCode === "D".charCodeAt(0)) {
    601             this._handlePanGesture(panMultiplier * this._pixelToTime);
    602             e.consume(true);
    603         } else if (e.keyCode === "W".charCodeAt(0)) {
    604             this._handleZoomGesture(-zoomMultiplier);
    605             e.consume(true);
    606         } else if (e.keyCode === "S".charCodeAt(0)) {
    607             this._handleZoomGesture(zoomMultiplier);
    608             e.consume(true);
    609         }
    610     },
    611 
    612     /**
    613      * @param {number} zoom
    614      */
    615     _handleZoomGesture: function(zoom)
    616     {
    617         this._cancelAnimation();
    618         var bounds = this._windowForGesture();
    619         var cursorTime = this._cursorTime(this._lastMouseOffsetX);
    620         bounds.left += (bounds.left - cursorTime) * zoom;
    621         bounds.right += (bounds.right - cursorTime) * zoom;
    622         this._requestWindowTimes(bounds);
    623     },
    624 
    625     /**
    626      * @param {number} shift
    627      */
    628     _handlePanGesture: function(shift)
    629     {
    630         this._cancelAnimation();
    631         var bounds = this._windowForGesture();
    632         shift = Number.constrain(shift, this._minimumBoundary - bounds.left, this._totalTime + this._minimumBoundary - bounds.right);
    633         bounds.left += shift;
    634         bounds.right += shift;
    635         this._requestWindowTimes(bounds);
    636     },
    637 
    638     /**
    639      * @return {{left: number, right: number}}
    640      */
    641     _windowForGesture: function()
    642     {
    643         var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
    644         var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
    645         return {left: windowLeft, right: windowRight};
    646     },
    647 
    648     /**
    649      * @param {{left: number, right: number}} bounds
    650      */
    651     _requestWindowTimes: function(bounds)
    652     {
    653         bounds.left = Number.constrain(bounds.left, this._minimumBoundary, this._totalTime + this._minimumBoundary);
    654         bounds.right = Number.constrain(bounds.right, this._minimumBoundary, this._totalTime + this._minimumBoundary);
    655         this._flameChartDelegate.requestWindowTimes(bounds.left, bounds.right);
    656     },
    657 
    658     /**
    659      * @param {number} x
    660      * @return {number}
    661      */
    662     _cursorTime: function(x)
    663     {
    664         return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._minimumBoundary;
    665     },
    666 
    667     /**
    668      * @param {number} x
    669      * @param {number} y
    670      * @return {number}
    671      */
    672     _coordinatesToEntryIndex: function(x, y)
    673     {
    674         y += this._scrollTop;
    675         var timelineData = this._timelineData();
    676         if (!timelineData)
    677             return -1;
    678         var cursorTime = this._cursorTime(x);
    679         var cursorLevel;
    680         var offsetFromLevel;
    681         if (this._isTopDown) {
    682             cursorLevel = Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight);
    683             offsetFromLevel = y - WebInspector.FlameChart.DividersBarHeight - cursorLevel * this._barHeight;
    684         } else {
    685             cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
    686             offsetFromLevel = this._canvas.height / window.devicePixelRatio - cursorLevel * this._barHeight;
    687         }
    688         var entryStartTimes = timelineData.entryStartTimes;
    689         var entryTotalTimes = timelineData.entryTotalTimes;
    690         var entryIndexes = this._timelineLevels[cursorLevel];
    691         if (!entryIndexes || !entryIndexes.length)
    692             return -1;
    693 
    694         /**
    695          * @param {number} time
    696          * @param {number} entryIndex
    697          * @return {number}
    698          */
    699         function comparator(time, entryIndex)
    700         {
    701             return time - entryStartTimes[entryIndex];
    702         }
    703         var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator) - 1, 0);
    704 
    705         /**
    706          * @this {WebInspector.FlameChart}
    707          * @param {number} entryIndex
    708          * @return {boolean}
    709          */
    710         function checkEntryHit(entryIndex)
    711         {
    712             if (entryIndex === undefined)
    713                 return false;
    714             var startTime = entryStartTimes[entryIndex];
    715             var duration = entryTotalTimes[entryIndex];
    716             if (isNaN(duration)) {
    717                 var dx = (startTime - cursorTime) / this._pixelToTime;
    718                 var dy = this._barHeight / 2 - offsetFromLevel;
    719                 return dx * dx + dy * dy < this._markerRadius * this._markerRadius;
    720             }
    721             var endTime = startTime + duration;
    722             var barThreshold = 3 * this._pixelToTime;
    723             return startTime - barThreshold < cursorTime && cursorTime < endTime + barThreshold;
    724         }
    725 
    726         var entryIndex = entryIndexes[indexOnLevel];
    727         if (checkEntryHit.call(this, entryIndex))
    728             return entryIndex;
    729         entryIndex = entryIndexes[indexOnLevel + 1];
    730         if (checkEntryHit.call(this, entryIndex))
    731             return entryIndex;
    732         return -1;
    733     },
    734 
    735     /**
    736      * @param {number} x
    737      * @return {number}
    738      */
    739     _markerIndexAtPosition: function(x)
    740     {
    741         var markers = this._timelineData().markerTimestamps;
    742         if (!markers)
    743             return -1;
    744         var accurracyOffsetPx = 1;
    745         var time = this._cursorTime(x);
    746         var leftTime = this._cursorTime(x - accurracyOffsetPx);
    747         var rightTime = this._cursorTime(x + accurracyOffsetPx);
    748 
    749         /**
    750          * @param {number} time
    751          * @param {number} markerTimestamp
    752          * @return {number}
    753          */
    754         function comparator(time, markerTimestamp)
    755         {
    756             return time - markerTimestamp;
    757         }
    758         var left = markers.lowerBound(leftTime, comparator);
    759         var markerIndex = -1;
    760         var distance = Infinity;
    761         for (var i = left; i < markers.length && markers[i] < rightTime; i++) {
    762             var nextDistance = Math.abs(markers[i] - time);
    763             if (nextDistance < distance) {
    764                 markerIndex = i;
    765                 distance = nextDistance;
    766             }
    767         }
    768         return markerIndex;
    769     },
    770 
    771     /**
    772      * @param {number} height
    773      * @param {number} width
    774      */
    775     _draw: function(width, height)
    776     {
    777         var timelineData = this._timelineData();
    778         if (!timelineData)
    779             return;
    780 
    781         var context = this._canvas.getContext("2d");
    782         context.save();
    783         var ratio = window.devicePixelRatio;
    784         context.scale(ratio, ratio);
    785 
    786         var timeWindowRight = this._timeWindowRight;
    787         var timeWindowLeft = this._timeWindowLeft;
    788         var timeToPixel = this._timeToPixel;
    789         var pixelWindowLeft = this._pixelWindowLeft;
    790         var paddingLeft = this._paddingLeft;
    791         var minWidth = this._minWidth;
    792         var entryTotalTimes = timelineData.entryTotalTimes;
    793         var entryStartTimes = timelineData.entryStartTimes;
    794         var entryLevels = timelineData.entryLevels;
    795 
    796         var titleIndices = new Uint32Array(timelineData.entryTotalTimes);
    797         var nextTitleIndex = 0;
    798         var markerIndices = new Uint32Array(timelineData.entryTotalTimes);
    799         var nextMarkerIndex = 0;
    800         var textPadding = this._dataProvider.textPadding();
    801         this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026");
    802         var minTextWidth = this._minTextWidth;
    803 
    804         var barHeight = this._barHeight;
    805 
    806         var timeToPosition = this._timeToPosition.bind(this);
    807         var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline();
    808         var colorBuckets = {};
    809         var minVisibleBarLevel = Math.max(Math.floor((this._scrollTop - this._baseHeight) / barHeight), 0);
    810         var maxVisibleBarLevel = Math.min(Math.floor((this._scrollTop - this._baseHeight + height) / barHeight), this._dataProvider.maxStackDepth());
    811 
    812         context.translate(0, -this._scrollTop);
    813 
    814         function comparator(time, entryIndex)
    815         {
    816             return time - entryStartTimes[entryIndex];
    817         }
    818 
    819         for (var level = minVisibleBarLevel; level <= maxVisibleBarLevel; ++level) {
    820             // Entries are ordered by start time within a level, so find the last visible entry.
    821             var levelIndexes = this._timelineLevels[level];
    822             var rightIndexOnLevel = levelIndexes.lowerBound(timeWindowRight, comparator) - 1;
    823             var lastDrawOffset = Infinity;
    824             for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
    825                 var entryIndex = levelIndexes[entryIndexOnLevel];
    826                 var entryStartTime = entryStartTimes[entryIndex];
    827                 var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]);
    828                 if (entryOffsetRight <= timeWindowLeft)
    829                     break;
    830 
    831                 var barX = this._timeToPosition(entryStartTime);
    832                 if (barX >= lastDrawOffset)
    833                     continue;
    834                 var barRight = Math.min(this._timeToPosition(entryOffsetRight), lastDrawOffset);
    835                 lastDrawOffset = barX;
    836 
    837                 var color = this._dataProvider.entryColor(entryIndex);
    838                 var bucket = colorBuckets[color];
    839                 if (!bucket) {
    840                     bucket = [];
    841                     colorBuckets[color] = bucket;
    842                 }
    843                 bucket.push(entryIndex);
    844             }
    845         }
    846 
    847         var colors = Object.keys(colorBuckets);
    848         // We don't use for-in here because it couldn't be optimized.
    849         for (var c = 0; c < colors.length; ++c) {
    850             var color = colors[c];
    851             context.fillStyle = color;
    852             context.strokeStyle = color;
    853             var indexes = colorBuckets[color];
    854 
    855             // First fill the boxes.
    856             context.beginPath();
    857             for (var i = 0; i < indexes.length; ++i) {
    858                 var entryIndex = indexes[i];
    859                 var entryStartTime = entryStartTimes[entryIndex];
    860                 var barX = this._timeToPosition(entryStartTime);
    861                 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]);
    862                 var barWidth = Math.max(barRight - barX, minWidth);
    863                 var barLevel = entryLevels[entryIndex];
    864                 var barY = this._levelToHeight(barLevel);
    865                 if (isNaN(entryTotalTimes[entryIndex])) {
    866                     context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
    867                     context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
    868                     markerIndices[nextMarkerIndex++] = entryIndex;
    869                 } else {
    870                     context.rect(barX, barY, barWidth, barHeight);
    871                     if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex))
    872                         titleIndices[nextTitleIndex++] = entryIndex;
    873                 }
    874             }
    875             context.fill();
    876         }
    877 
    878         context.strokeStyle = "rgb(0, 0, 0)";
    879         context.beginPath();
    880         for (var m = 0; m < nextMarkerIndex; ++m) {
    881             var entryIndex = markerIndices[m];
    882             var entryStartTime = entryStartTimes[entryIndex];
    883             var barX = this._timeToPosition(entryStartTime);
    884             var barLevel = entryLevels[entryIndex];
    885             var barY = this._levelToHeight(barLevel);
    886             context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
    887             context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
    888         }
    889         context.stroke();
    890 
    891         context.textBaseline = "alphabetic";
    892 
    893         for (var i = 0; i < nextTitleIndex; ++i) {
    894             var entryIndex = titleIndices[i];
    895             var entryStartTime = entryStartTimes[entryIndex];
    896             var barX = this._timeToPosition(entryStartTime);
    897             var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]);
    898             var barWidth = Math.max(barRight - barX, minWidth);
    899             var barLevel = entryLevels[entryIndex];
    900             var barY = this._levelToHeight(barLevel);
    901             var text = this._dataProvider.entryTitle(entryIndex);
    902             if (text && text.length) {
    903                 context.font = this._dataProvider.entryFont(entryIndex);
    904                 text = this._prepareText(context, text, barWidth - 2 * textPadding);
    905             }
    906 
    907             if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition))
    908                 continue;
    909             if (!text || !text.length)
    910                 continue;
    911 
    912             context.fillStyle = this._dataProvider.textColor(entryIndex);
    913             context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta);
    914         }
    915         context.restore();
    916 
    917         var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary());
    918         WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets);
    919         this._drawMarkers();
    920 
    921         this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
    922         this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
    923         this._updateMarkerHighlight();
    924     },
    925 
    926     _drawMarkers: function()
    927     {
    928         var markerTimestamps = this._timelineData().markerTimestamps;
    929         /**
    930          * @param {number} time
    931          * @param {number} markerTimestamp
    932          * @return {number}
    933          */
    934         function compare(time, markerTimestamp)
    935         {
    936             return time - markerTimestamp;
    937         }
    938         var left = markerTimestamps.lowerBound(this._calculator.minimumBoundary(), compare);
    939         var rightBoundary = this._calculator.maximumBoundary();
    940 
    941         var context = this._canvas.getContext("2d");
    942         context.save();
    943         var ratio = window.devicePixelRatio;
    944         context.scale(ratio, ratio);
    945         var height = WebInspector.FlameChart.DividersBarHeight - 1;
    946         context.lineWidth = 2;
    947         for (var i = left; i < markerTimestamps.length; i++) {
    948             var timestamp = markerTimestamps[i];
    949             if (timestamp > rightBoundary)
    950                 break;
    951             var position = this._calculator.computePosition(timestamp);
    952             context.strokeStyle = this._dataProvider.markerColor(i);
    953             context.beginPath();
    954             context.moveTo(position, 0);
    955             context.lineTo(position, height);
    956             context.stroke();
    957         }
    958         context.restore();
    959     },
    960 
    961     _updateMarkerHighlight: function()
    962     {
    963         var element = this._markerHighlighElement;
    964         if (element.parentElement)
    965             element.remove();
    966         var markerIndex = this._highlightedMarkerIndex;
    967         if (markerIndex === -1)
    968             return;
    969         var barX = this._timeToPosition(this._timelineData().markerTimestamps[markerIndex]);
    970         element.title = this._dataProvider.markerTitle(markerIndex);
    971         var style = element.style;
    972         style.left = barX + "px";
    973         style.backgroundColor = this._dataProvider.markerColor(markerIndex);
    974         this.element.appendChild(element);
    975     },
    976 
    977     /**
    978      * @param {?WebInspector.FlameChart.TimelineData} timelineData
    979      */
    980     _processTimelineData: function(timelineData)
    981     {
    982         if (!timelineData) {
    983             this._timelineLevels = null;
    984             this._rawTimelineData = null;
    985             this._rawTimelineDataLength = 0;
    986             return;
    987         }
    988 
    989         var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1);
    990         for (var i = 0; i < timelineData.entryLevels.length; ++i)
    991             ++entryCounters[timelineData.entryLevels[i]];
    992         var levelIndexes = new Array(entryCounters.length);
    993         for (var i = 0; i < levelIndexes.length; ++i) {
    994             levelIndexes[i] = new Uint32Array(entryCounters[i]);
    995             entryCounters[i] = 0;
    996         }
    997         for (var i = 0; i < timelineData.entryLevels.length; ++i) {
    998             var level = timelineData.entryLevels[i];
    999             levelIndexes[level][entryCounters[level]++] = i;
   1000         }
   1001         this._timelineLevels = levelIndexes;
   1002         this._rawTimelineData = timelineData;
   1003         this._rawTimelineDataLength = timelineData.entryStartTimes.length;
   1004     },
   1005 
   1006     /**
   1007      * @param {number} entryIndex
   1008      */
   1009     setSelectedEntry: function(entryIndex)
   1010     {
   1011         this._selectedEntryIndex = entryIndex;
   1012         this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
   1013     },
   1014 
   1015     _updateElementPosition: function(element, entryIndex)
   1016     {
   1017         if (element.parentElement)
   1018             element.remove();
   1019         if (entryIndex === -1)
   1020             return;
   1021         var timeRange = this._dataProvider.highlightTimeRange(entryIndex);
   1022         if (!timeRange)
   1023             return;
   1024         var timelineData = this._timelineData();
   1025         var barX = this._timeToPosition(timeRange.startTime);
   1026         var barRight = this._timeToPosition(timeRange.endTime);
   1027         if (barRight === 0 || barX === this._canvas.width)
   1028             return;
   1029         var barWidth = Math.max(barRight - barX, this._minWidth);
   1030         var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop;
   1031         var style = element.style;
   1032         style.left = barX + "px";
   1033         style.top = barY + "px";
   1034         style.width = barWidth + "px";
   1035         style.height = this._barHeight + "px";
   1036         this.element.appendChild(element);
   1037     },
   1038 
   1039     /**
   1040      * @param {number} time
   1041      */
   1042     _timeToPosition: function(time)
   1043     {
   1044         var value = Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
   1045         return Math.min(this._canvas.width, Math.max(0, value));
   1046     },
   1047 
   1048     _levelToHeight: function(level)
   1049     {
   1050          return this._baseHeight - level * this._barHeightDelta;
   1051     },
   1052 
   1053     _buildEntryInfo: function(entryInfo)
   1054     {
   1055         var infoTable = document.createElementWithClass("table", "info-table");
   1056         for (var i = 0; i < entryInfo.length; ++i) {
   1057             var row = infoTable.createChild("tr");
   1058             row.createChild("td", "title").textContent = entryInfo[i].title;
   1059             row.createChild("td").textContent = entryInfo[i].text;
   1060         }
   1061         return infoTable;
   1062     },
   1063 
   1064     /**
   1065      * @param {!CanvasRenderingContext2D} context
   1066      * @param {string} title
   1067      * @param {number} maxSize
   1068      * @return {string}
   1069      */
   1070     _prepareText: function(context, title, maxSize)
   1071     {
   1072         var titleWidth = this._measureWidth(context, title);
   1073         if (maxSize >= titleWidth)
   1074             return title;
   1075 
   1076         var l = 2;
   1077         var r = title.length;
   1078         while (l < r) {
   1079             var m = (l + r) >> 1;
   1080             if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize)
   1081                 l = m + 1;
   1082             else
   1083                 r = m;
   1084         }
   1085         title = title.trimMiddle(r - 1);
   1086         return title !== "\u2026" ? title : "";
   1087     },
   1088 
   1089     /**
   1090      * @param {!CanvasRenderingContext2D} context
   1091      * @param {string} text
   1092      * @return {number}
   1093      */
   1094     _measureWidth: function(context, text)
   1095     {
   1096         if (text.length > 20)
   1097             return context.measureText(text).width;
   1098 
   1099         var font = context.font;
   1100         var textWidths = this._textWidth[font];
   1101         if (!textWidths) {
   1102             textWidths = {};
   1103             this._textWidth[font] = textWidths;
   1104         }
   1105         var width = textWidths[text];
   1106         if (!width) {
   1107             width = context.measureText(text).width;
   1108             textWidths[text] = width;
   1109         }
   1110         return width;
   1111     },
   1112 
   1113     _updateBoundaries: function()
   1114     {
   1115         this._totalTime = this._dataProvider.totalTime();
   1116         this._minimumBoundary = this._dataProvider.minimumBoundary();
   1117 
   1118         if (this._timeWindowRight !== Infinity) {
   1119             this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime;
   1120             this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime;
   1121             this._windowWidth = this._windowRight - this._windowLeft;
   1122         } else {
   1123             this._windowLeft = 0;
   1124             this._windowRight = 1;
   1125             this._windowWidth = 1;
   1126         }
   1127 
   1128         this._pixelWindowWidth = this._offsetWidth - this._paddingLeft;
   1129         this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
   1130         this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
   1131         this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
   1132 
   1133         this._timeToPixel = this._totalPixels / this._totalTime;
   1134         this._pixelToTime = this._totalTime / this._totalPixels;
   1135         this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
   1136 
   1137         this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight;
   1138 
   1139         this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth() + 1);
   1140         this._vScrollContent.style.height = this._totalHeight + "px";
   1141         this._scrollTop = this._vScrollElement.scrollTop;
   1142         this._updateScrollBar();
   1143     },
   1144 
   1145     onResize: function()
   1146     {
   1147         this._updateScrollBar();
   1148         this.scheduleUpdate();
   1149     },
   1150 
   1151     _updateScrollBar: function()
   1152     {
   1153         var showScroll = this._totalHeight > this._offsetHeight;
   1154         this._vScrollElement.classList.toggle("hidden", !showScroll);
   1155         this._offsetWidth = this.element.offsetWidth - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth);
   1156         this._offsetHeight = this.element.offsetHeight;
   1157     },
   1158 
   1159     scheduleUpdate: function()
   1160     {
   1161         if (this._updateTimerId || this._cancelWindowTimesAnimation)
   1162             return;
   1163         this._updateTimerId = requestAnimationFrame(this.update.bind(this));
   1164     },
   1165 
   1166     update: function()
   1167     {
   1168         this._updateTimerId = 0;
   1169         if (!this._timelineData())
   1170             return;
   1171         this._resetCanvas();
   1172         this._updateBoundaries();
   1173         this._calculator._updateBoundaries(this);
   1174         this._draw(this._offsetWidth, this._offsetHeight);
   1175     },
   1176 
   1177     reset: function()
   1178     {
   1179         this._highlightedMarkerIndex = -1;
   1180         this._highlightedEntryIndex = -1;
   1181         this._selectedEntryIndex = -1;
   1182         this._textWidth = {};
   1183         this.update();
   1184     },
   1185 
   1186     __proto__: WebInspector.HBox.prototype
   1187 }
   1188