Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / ui_lazy / FlameChart.js
blob3163b9a199aff762507a2a17fdb87769fc7122ed
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  */
31 /**
32  * @interface
33  */
34 WebInspector.FlameChartDelegate = function() { }
36 WebInspector.FlameChartDelegate.prototype = {
37     /**
38      * @param {number} startTime
39      * @param {number} endTime
40      */
41     requestWindowTimes: function(startTime, endTime) { },
43     /**
44      * @param {number} startTime
45      * @param {number} endTime
46      */
47     updateRangeSelection: function(startTime, endTime) { },
49     endRangeSelection: function() { }
52 /**
53  * @constructor
54  * @extends {WebInspector.HBox}
55  * @param {!WebInspector.FlameChartDataProvider} dataProvider
56  * @param {!WebInspector.FlameChartDelegate} flameChartDelegate
57  * @param {boolean} isTopDown
58  */
59 WebInspector.FlameChart = function(dataProvider, flameChartDelegate, isTopDown)
61     WebInspector.HBox.call(this, true);
62     this.registerRequiredCSS("ui_lazy/flameChart.css");
63     this.contentElement.classList.add("flame-chart-main-pane");
64     this._flameChartDelegate = flameChartDelegate;
65     this._isTopDown = isTopDown;
67     this._calculator = new WebInspector.FlameChart.Calculator();
69     this._canvas = this.contentElement.createChild("canvas");
70     this._canvas.tabIndex = 1;
71     this.setDefaultFocusedElement(this._canvas);
72     this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), false);
73     this._canvas.addEventListener("mouseout", this._onMouseOut.bind(this), false);
74     this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
75     this._canvas.addEventListener("click", this._onClick.bind(this), false);
76     this._canvas.addEventListener("keydown", this._onKeyDown.bind(this), false);
77     WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "-webkit-grabbing", null);
78     WebInspector.installDragHandle(this._canvas, this._startRangeSelection.bind(this), this._rangeSelectionDragging.bind(this), this._endRangeSelection.bind(this), "text", null);
80     this._vScrollElement = this.contentElement.createChild("div", "flame-chart-v-scroll");
81     this._vScrollContent = this._vScrollElement.createChild("div");
82     this._vScrollElement.addEventListener("scroll", this._onScroll.bind(this), false);
83     this._scrollTop = 0;
85     this._entryInfo = this.contentElement.createChild("div", "flame-chart-entry-info");
86     this._markerHighlighElement = this.contentElement.createChild("div", "flame-chart-marker-highlight-element");
87     this._highlightElement = this.contentElement.createChild("div", "flame-chart-highlight-element");
88     this._selectedElement = this.contentElement.createChild("div", "flame-chart-selected-element");
89     this._selectionOverlay = this.contentElement.createChild("div", "flame-chart-selection-overlay hidden");
90     this._selectedTimeSpanLabel = this._selectionOverlay.createChild("div", "time-span");
92     this._dataProvider = dataProvider;
94     this._windowLeft = 0.0;
95     this._windowRight = 1.0;
96     this._windowWidth = 1.0;
97     this._timeWindowLeft = 0;
98     this._timeWindowRight = Infinity;
99     this._barHeight = dataProvider.barHeight();
100     this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight;
101     this._paddingLeft = this._dataProvider.paddingLeft();
102     this._markerPadding = 2;
103     this._markerRadius = this._barHeight / 2 - this._markerPadding;
104     this._highlightedMarkerIndex = -1;
105     this._highlightedEntryIndex = -1;
106     this._selectedEntryIndex = -1;
107     this._rawTimelineDataLength = 0;
108     this._textWidth = {};
110     this._lastMouseOffsetX = 0;
113 WebInspector.FlameChart.DividersBarHeight = 18;
115 WebInspector.FlameChart.MinimalTimeWindowMs = 0.01;
118  * @interface
119  */
120 WebInspector.FlameChartDataProvider = function()
125  * @constructor
126  * @param {!Array.<number>|!Uint8Array} entryLevels
127  * @param {!Array.<number>|!Float32Array} entryTotalTimes
128  * @param {!Array.<number>|!Float64Array} entryStartTimes
129  */
130 WebInspector.FlameChart.TimelineData = function(entryLevels, entryTotalTimes, entryStartTimes)
132     this.entryLevels = entryLevels;
133     this.entryTotalTimes = entryTotalTimes;
134     this.entryStartTimes = entryStartTimes;
135     /** @type {!Array.<!WebInspector.FlameChartMarker>} */
136     this.markers = [];
137     this.flowStartTimes = [];
138     this.flowStartLevels = [];
139     this.flowEndTimes = [];
140     this.flowEndLevels = [];
143 WebInspector.FlameChartDataProvider.prototype = {
144     /**
145      * @return {number}
146      */
147     barHeight: function() { },
149     /**
150      * @param {number} startTime
151      * @param {number} endTime
152      * @return {?Array.<number>}
153      */
154     dividerOffsets: function(startTime, endTime) { },
156     /**
157      * @return {number}
158      */
159     minimumBoundary: function() { },
161     /**
162      * @return {number}
163      */
164     totalTime: function() { },
166     /**
167      * @return {number}
168      */
169     maxStackDepth: function() { },
171     /**
172      * @return {?WebInspector.FlameChart.TimelineData}
173      */
174     timelineData: function() { },
176     /**
177      * @param {number} entryIndex
178      * @return {?Array.<!{title: string, value: (string|!Element)}>}
179      */
180     prepareHighlightedEntryInfo: function(entryIndex) { },
182     /**
183      * @param {number} entryIndex
184      * @return {boolean}
185      */
186     canJumpToEntry: function(entryIndex) { },
188     /**
189      * @param {number} entryIndex
190      * @return {?string}
191      */
192     entryTitle: function(entryIndex) { },
194     /**
195      * @param {number} entryIndex
196      * @return {?string}
197      */
198     entryFont: function(entryIndex) { },
200     /**
201      * @param {number} entryIndex
202      * @return {string}
203      */
204     entryColor: function(entryIndex) { },
206     /**
207      * @param {number} entryIndex
208      * @param {!CanvasRenderingContext2D} context
209      * @param {?string} text
210      * @param {number} barX
211      * @param {number} barY
212      * @param {number} barWidth
213      * @param {number} barHeight
214      * @return {boolean}
215      */
216     decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight) { },
218     /**
219      * @param {number} entryIndex
220      * @return {boolean}
221      */
222     forceDecoration: function(entryIndex) { },
224     /**
225      * @param {number} entryIndex
226      * @return {string}
227      */
228     textColor: function(entryIndex) { },
230     /**
231      * @return {number}
232      */
233     textBaseline: function() { },
235     /**
236      * @return {number}
237      */
238     textPadding: function() { },
240     /**
241      * @return {?{startTime: number, endTime: number}}
242      */
243     highlightTimeRange: function(entryIndex) { },
245     /**
246      * @return {number}
247      */
248     paddingLeft: function() { },
252  * @interface
253  */
254 WebInspector.FlameChartMarker = function()
258 WebInspector.FlameChartMarker.prototype = {
259     /**
260      * @return {number}
261      */
262     startTime: function() { },
264     /**
265      * @return {string}
266      */
267     color: function() { },
269     /**
270      * @return {string}
271      */
272     title: function() { },
274     /**
275      * @param {!CanvasRenderingContext2D} context
276      * @param {number} x
277      * @param {number} height
278      * @param {number} pixelsPerMillisecond
279      */
280     draw: function(context, x, height, pixelsPerMillisecond) { },
283 WebInspector.FlameChart.Events = {
284     EntrySelected: "EntrySelected"
289  * @constructor
290  * @param {!{min: number, max: number, count: number}|number=} hueSpace
291  * @param {!{min: number, max: number, count: number}|number=} satSpace
292  * @param {!{min: number, max: number, count: number}|number=} lightnessSpace
293  * @param {!{min: number, max: number, count: number}|number=} alphaSpace
294  */
295 WebInspector.FlameChart.ColorGenerator = function(hueSpace, satSpace, lightnessSpace, alphaSpace)
297     this._hueSpace = hueSpace || { min: 0, max: 360, count: 20 };
298     this._satSpace = satSpace || 67;
299     this._lightnessSpace = lightnessSpace || 80;
300     this._alphaSpace = alphaSpace || 1;
301     this._colors = {};
304 WebInspector.FlameChart.ColorGenerator.prototype = {
305     /**
306      * @param {string} id
307      * @param {string|!CanvasGradient} color
308      */
309     setColorForID: function(id, color)
310     {
311         this._colors[id] = color;
312     },
314     /**
315      * @param {string} id
316      * @return {string}
317      */
318     colorForID: function(id)
319     {
320         var color = this._colors[id];
321         if (!color) {
322             color = this._generateColorForID(id);
323             this._colors[id] = color;
324         }
325         return color;
326     },
328     /**
329      * @param {string} id
330      * @return {string}
331      */
332     _generateColorForID: function(id)
333     {
334         var hash = Math.abs(String.hashCode(id));
335         var h = this._indexToValueInSpace(hash, this._hueSpace);
336         var s = this._indexToValueInSpace(hash, this._satSpace);
337         var l = this._indexToValueInSpace(hash, this._lightnessSpace);
338         var a = this._indexToValueInSpace(hash, this._alphaSpace);
339         return "hsla(" + h + ", " + s + "%, " + l + "%, " + a + ")";
340     },
342     /**
343      * @param {number} index
344      * @param {!{min: number, max: number, count: number}|number} space
345      * @return {number}
346      */
347     _indexToValueInSpace: function(index, space)
348     {
349         if (typeof space === "number")
350             return space;
351         index %= space.count;
352         return space.min + Math.floor(index / (space.count - 1) * (space.max - space.min));
353     }
358  * @constructor
359  * @implements {WebInspector.TimelineGrid.Calculator}
360  */
361 WebInspector.FlameChart.Calculator = function()
363     this._paddingLeft = 0;
366 WebInspector.FlameChart.Calculator.prototype = {
367     /**
368      * @override
369      * @return {number}
370      */
371     paddingLeft: function()
372     {
373         return this._paddingLeft;
374     },
376     /**
377      * @param {!WebInspector.FlameChart} mainPane
378      */
379     _updateBoundaries: function(mainPane)
380     {
381         this._totalTime = mainPane._dataProvider.totalTime();
382         this._zeroTime = mainPane._dataProvider.minimumBoundary();
383         this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime;
384         this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime;
385         this._paddingLeft = mainPane._paddingLeft;
386         this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft;
387         this._timeToPixel = this._width / this.boundarySpan();
388     },
390     /**
391      * @override
392      * @param {number} time
393      * @return {number}
394      */
395     computePosition: function(time)
396     {
397         return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft);
398     },
400     /**
401      * @override
402      * @param {number} value
403      * @param {number=} precision
404      * @return {string}
405      */
406     formatTime: function(value, precision)
407     {
408         return Number.preciseMillisToString(value - this._zeroTime, precision);
409     },
411     /**
412      * @override
413      * @return {number}
414      */
415     maximumBoundary: function()
416     {
417         return this._maximumBoundaries;
418     },
420     /**
421      * @override
422      * @return {number}
423      */
424     minimumBoundary: function()
425     {
426         return this._minimumBoundaries;
427     },
429     /**
430      * @override
431      * @return {number}
432      */
433     zeroTime: function()
434     {
435         return this._zeroTime;
436     },
438     /**
439      * @override
440      * @return {number}
441      */
442     boundarySpan: function()
443     {
444         return this._maximumBoundaries - this._minimumBoundaries;
445     }
448 WebInspector.FlameChart.prototype = {
449     _resetCanvas: function()
450     {
451         var ratio = window.devicePixelRatio;
452         this._canvas.width = this._offsetWidth * ratio;
453         this._canvas.height = this._offsetHeight * ratio;
454         this._canvas.style.width = this._offsetWidth + "px";
455         this._canvas.style.height = this._offsetHeight + "px";
456     },
458     /**
459      * @return {?WebInspector.FlameChart.TimelineData}
460      */
461     _timelineData: function()
462     {
463         var timelineData = this._dataProvider.timelineData();
464         if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength)
465             this._processTimelineData(timelineData);
466         return this._rawTimelineData;
467     },
469     _cancelAnimation: function()
470     {
471         if (this._cancelWindowTimesAnimation) {
472             this._timeWindowLeft = this._pendingAnimationTimeLeft;
473             this._timeWindowRight = this._pendingAnimationTimeRight;
474             this._cancelWindowTimesAnimation();
475             delete this._cancelWindowTimesAnimation;
476         }
477     },
479     /**
480      * @param {number} entryIndex
481      */
482     _revealEntry: function(entryIndex)
483     {
484         var timelineData = this._timelineData();
485         if (!timelineData)
486             return;
487         // Think in terms of not where we are, but where we'll be after animation (if present)
488         var timeLeft = this._cancelWindowTimesAnimation ? this._pendingAnimationTimeLeft : this._timeWindowLeft;
489         var timeRight = this._cancelWindowTimesAnimation ? this._pendingAnimationTimeRight : this._timeWindowRight;
490         var entryStartTime = timelineData.entryStartTimes[entryIndex];
491         var entryTotalTime = timelineData.entryTotalTimes[entryIndex];
492         var entryEndTime = entryStartTime + entryTotalTime;
493         var minEntryTimeWindow = Math.min(entryTotalTime, timeRight - timeLeft);
495         var y = this._levelToHeight(timelineData.entryLevels[entryIndex]);
496         if (y < this._vScrollElement.scrollTop)
497             this._vScrollElement.scrollTop = y;
498         else if (y > this._vScrollElement.scrollTop + this._offsetHeight + this._barHeightDelta)
499             this._vScrollElement.scrollTop = y - this._offsetHeight - this._barHeightDelta;
501         if (timeLeft > entryEndTime) {
502             var delta = timeLeft - entryEndTime + minEntryTimeWindow;
503             this._flameChartDelegate.requestWindowTimes(timeLeft - delta, timeRight - delta);
504         } else if (timeRight < entryStartTime) {
505             var delta = entryStartTime - timeRight + minEntryTimeWindow;
506             this._flameChartDelegate.requestWindowTimes(timeRight + delta, timeRight + delta);
507         }
508     },
510     /**
511      * @param {number} startTime
512      * @param {number} endTime
513      */
514     setWindowTimes: function(startTime, endTime)
515     {
516         if (this._muteAnimation || this._timeWindowLeft === 0 || this._timeWindowRight === Infinity || (startTime === 0 && endTime === Infinity) || (startTime === Infinity && endTime === Infinity)) {
517             // Initial setup.
518             this._timeWindowLeft = startTime;
519             this._timeWindowRight = endTime;
520             this.scheduleUpdate();
521             return;
522         }
524         this._cancelAnimation();
525         this._cancelWindowTimesAnimation = WebInspector.animateFunction(this.element.window(), this._animateWindowTimes.bind(this),
526             [{from: this._timeWindowLeft, to: startTime}, {from: this._timeWindowRight, to: endTime}], 5,
527             this._animationCompleted.bind(this));
528         this._pendingAnimationTimeLeft = startTime;
529         this._pendingAnimationTimeRight = endTime;
530     },
532     /**
533      * @param {number} startTime
534      * @param {number} endTime
535      */
536     _animateWindowTimes: function(startTime, endTime)
537     {
538         this._timeWindowLeft = startTime;
539         this._timeWindowRight = endTime;
540         this.update();
541     },
543     _animationCompleted: function()
544     {
545         delete this._cancelWindowTimesAnimation;
546     },
548     /**
549      * @param {!MouseEvent} event
550      */
551     _initMaxDragOffset: function(event)
552     {
553         this._maxDragOffsetSquared = 0;
554         this._dragStartX = event.pageX;
555         this._dragStartY = event.pageY;
556     },
558     /**
559      * @param {!MouseEvent} event
560      */
561     _updateMaxDragOffset: function(event)
562     {
563         var dx = event.pageX - this._dragStartX;
564         var dy = event.pageY - this._dragStartY;
565         var dragOffsetSquared = dx * dx + dy * dy;
566         this._maxDragOffsetSquared = Math.max(this._maxDragOffsetSquared, dragOffsetSquared);
567     },
569     /**
570      * @return {number}
571      */
572     _maxDragOffset: function()
573     {
574         return Math.sqrt(this._maxDragOffsetSquared);
575     },
577     /**
578      * @param {!MouseEvent} event
579      * @return {boolean}
580      */
581     _startCanvasDragging: function(event)
582     {
583         if (event.shiftKey)
584             return false;
585         if (!this._timelineData() || this._timeWindowRight === Infinity)
586             return false;
587         this._isDragging = true;
588         this._initMaxDragOffset(event);
589         this._dragStartPointX = event.pageX;
590         this._dragStartPointY = event.pageY;
591         this._dragStartScrollTop = this._vScrollElement.scrollTop;
592         this._dragStartWindowLeft = this._timeWindowLeft;
593         this._dragStartWindowRight = this._timeWindowRight;
594         this._canvas.style.cursor = "";
595         return true;
596     },
598     /**
599      * @param {!MouseEvent} event
600      */
601     _canvasDragging: function(event)
602     {
603         var pixelShift = this._dragStartPointX - event.pageX;
604         this._dragStartPointX = event.pageX;
605         this._muteAnimation = true;
606         this._handlePanGesture(pixelShift * this._pixelToTime);
607         this._muteAnimation = false;
609         var pixelScroll = this._dragStartPointY - event.pageY;
610         this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
611         this._updateMaxDragOffset(event);
612     },
614     _endCanvasDragging: function()
615     {
616         this._isDragging = false;
617     },
619     /**
620      * @param {!MouseEvent} event
621      * @return {boolean}
622      */
623     _startRangeSelection: function(event)
624     {
625         if (!event.shiftKey)
626             return false;
627         this._isDragging = true;
628         this._initMaxDragOffset(event);
629         this._selectionOffsetShiftX = event.offsetX - event.pageX;
630         this._selectionOffsetShiftY = event.offsetY - event.pageY;
631         this._selectionStartX = event.offsetX;
632         var style = this._selectionOverlay.style;
633         style.left = this._selectionStartX + "px";
634         style.width = "1px";
635         this._selectedTimeSpanLabel.textContent = "";
636         this._selectionOverlay.classList.remove("hidden");
637         return true;
638     },
640     _endRangeSelection: function()
641     {
642         this._isDragging = false;
643         this._flameChartDelegate.endRangeSelection();
644     },
646     _hideRangeSelection: function()
647     {
648         this._selectionOverlay.classList.add("hidden");
649     },
651     /**
652      * @param {!MouseEvent} event
653      */
654     _rangeSelectionDragging: function(event)
655     {
656         this._updateMaxDragOffset(event);
657         var x = Number.constrain(event.pageX + this._selectionOffsetShiftX, 0, this._offsetWidth);
658         var start = this._cursorTime(this._selectionStartX);
659         var end = this._cursorTime(x);
660         this._rangeSelectionStart = Math.min(start, end);
661         this._rangeSelectionEnd = Math.max(start, end);
662         this._updateRangeSelectionOverlay();
663         this._flameChartDelegate.updateRangeSelection(this._rangeSelectionStart, this._rangeSelectionEnd);
664     },
666     _updateRangeSelectionOverlay: function()
667     {
668         var margin = 100;
669         var left = Number.constrain(this._timeToPosition(this._rangeSelectionStart), -margin, this._offsetWidth + margin);
670         var right = Number.constrain(this._timeToPosition(this._rangeSelectionEnd), -margin, this._offsetWidth + margin);
671         var style = this._selectionOverlay.style;
672         style.left = left + "px";
673         style.width = (right - left) + "px";
674         var timeSpan = this._rangeSelectionEnd - this._rangeSelectionStart;
675         this._selectedTimeSpanLabel.textContent = Number.preciseMillisToString(timeSpan, 2);
676     },
678     /**
679      * @param {!Event} event
680      */
681     _onMouseMove: function(event)
682     {
683         this._lastMouseOffsetX = event.offsetX;
685         if (!this._enabled())
686             return;
688         if (this._isDragging)
689             return;
691         var inDividersBar = event.offsetY < WebInspector.FlameChart.DividersBarHeight;
692         this._highlightedMarkerIndex = inDividersBar ? this._markerIndexAtPosition(event.offsetX) : -1;
693         this._updateMarkerHighlight();
695         this._highlightEntry(this._coordinatesToEntryIndex(event.offsetX, event.offsetY));
696     },
698     _onMouseOut: function()
699     {
700         this._highlightEntry(-1);
701     },
703     /**
704      * @param {number} entryIndex
705      */
706     _highlightEntry: function(entryIndex)
707     {
708         if (this._highlightedEntryIndex === entryIndex)
709             return;
711         if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
712             this._canvas.style.cursor = "default";
713         else
714             this._canvas.style.cursor = "pointer";
716         this._highlightedEntryIndex = entryIndex;
718         this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
719         this._entryInfo.removeChildren();
721         if (this._highlightedEntryIndex === -1)
722             return;
724         if (!this._isDragging) {
725             var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex);
726             if (entryInfo)
727                 this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
728         }
729     },
731     _onClick: function()
732     {
733         this.focus();
734         // onClick comes after dragStart and dragEnd events.
735         // So if there was drag (mouse move) in the middle of that events
736         // we skip the click. Otherwise we jump to the sources.
737         const clickThreshold = 5;
738         if (this._maxDragOffset() > clickThreshold)
739             return;
740         this._hideRangeSelection();
741         this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex);
742     },
744     /**
745      * @param {!Event} e
746      */
747     _onMouseWheel: function(e)
748     {
749         if (!this._enabled())
750             return;
751         // Pan vertically when shift down only.
752         var panVertically = e.shiftKey && (e.wheelDeltaY || Math.abs(e.wheelDeltaX) === 120);
753         var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey;
754         if (panVertically) {
755             this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 * this._offsetHeight / 8;
756         } else if (panHorizontally) {
757             var shift = -e.wheelDeltaX * this._pixelToTime;
758             this._muteAnimation = true;
759             this._handlePanGesture(shift);
760             this._muteAnimation = false;
761         } else {  // Zoom.
762             const mouseWheelZoomSpeed = 1 / 120;
763             this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1);
764         }
766         // Block swipe gesture.
767         e.consume(true);
768     },
770     /**
771      * @param {!Event} e
772      */
773     _onKeyDown: function(e)
774     {
775         this._handleZoomPanKeys(e);
776         this._handleSelectionNavigation(e);
777     },
779     /**
780      * @param {!Event} e
781      */
782     _handleSelectionNavigation: function(e)
783     {
784         if (!WebInspector.KeyboardShortcut.hasNoModifiers(e))
785             return;
786         if (this._selectedEntryIndex === -1)
787             return;
788         var timelineData = this._timelineData();
789         if (!timelineData)
790             return;
792         /**
793          * @param {number} time
794          * @param {number} entryIndex
795          * @return {number}
796          */
797         function timeComparator(time, entryIndex)
798         {
799             return time - timelineData.entryStartTimes[entryIndex];
800         }
802         /**
803          * @param {number} entry1
804          * @param {number} entry2
805          * @return {boolean}
806          */
807         function entriesIntersect(entry1, entry2)
808         {
809             var start1 = timelineData.entryStartTimes[entry1];
810             var start2 = timelineData.entryStartTimes[entry2];
811             var end1 = start1 + timelineData.entryTotalTimes[entry1];
812             var end2 = start2 + timelineData.entryTotalTimes[entry2];
813             return start1 < end2 && start2 < end1;
814         }
816         var keys = WebInspector.KeyboardShortcut.Keys;
817         if (e.keyCode === keys.Left.code || e.keyCode === keys.Right.code) {
818             var level = timelineData.entryLevels[this._selectedEntryIndex];
819             var levelIndexes = this._timelineLevels[level];
820             var indexOnLevel = levelIndexes.lowerBound(this._selectedEntryIndex);
821             indexOnLevel += e.keyCode === keys.Left.code ? -1 : 1;
822             e.consume(true);
823             if (indexOnLevel >= 0 && indexOnLevel < levelIndexes.length)
824                 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, levelIndexes[indexOnLevel]);
825             return;
826         }
827         if (e.keyCode === keys.Up.code || e.keyCode === keys.Down.code) {
828             var level = timelineData.entryLevels[this._selectedEntryIndex];
829             var delta = e.keyCode === keys.Up.code ? 1 : -1;
830             e.consume(true);
831             if (this._isTopDown)
832                 delta = -delta;
833             level += delta;
834             if (level < 0 || level >= this._timelineLevels.length)
835                 return;
836             var entryTime = timelineData.entryStartTimes[this._selectedEntryIndex] + timelineData.entryTotalTimes[this._selectedEntryIndex] / 2;
837             var levelIndexes = this._timelineLevels[level];
838             var indexOnLevel = levelIndexes.upperBound(entryTime, timeComparator) - 1;
839             if (!entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel])) {
840                 ++indexOnLevel;
841                 if (indexOnLevel >= levelIndexes.length || !entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel]))
842                     return;
843             }
844             this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, levelIndexes[indexOnLevel]);
845         }
846     },
848     /**
849      * @param {!Event} e
850      */
851     _handleZoomPanKeys: function(e)
852     {
853         if (!WebInspector.KeyboardShortcut.hasNoModifiers(e))
854             return;
855         var zoomMultiplier = e.shiftKey ? 0.8 : 0.3;
856         var panMultiplier = e.shiftKey ? 320 : 80;
857         if (e.keyCode === "A".charCodeAt(0)) {
858             this._handlePanGesture(-panMultiplier * this._pixelToTime);
859             e.consume(true);
860         } else if (e.keyCode === "D".charCodeAt(0)) {
861             this._handlePanGesture(panMultiplier * this._pixelToTime);
862             e.consume(true);
863         } else if (e.keyCode === "W".charCodeAt(0)) {
864             this._handleZoomGesture(-zoomMultiplier);
865             e.consume(true);
866         } else if (e.keyCode === "S".charCodeAt(0)) {
867             this._handleZoomGesture(zoomMultiplier);
868             e.consume(true);
869         }
870     },
872     /**
873      * @param {number} zoom
874      */
875     _handleZoomGesture: function(zoom)
876     {
877         this._cancelAnimation();
878         var bounds = this._windowForGesture();
879         var cursorTime = this._cursorTime(this._lastMouseOffsetX);
880         bounds.left += (bounds.left - cursorTime) * zoom;
881         bounds.right += (bounds.right - cursorTime) * zoom;
882         this._requestWindowTimes(bounds);
883     },
885     /**
886      * @param {number} shift
887      */
888     _handlePanGesture: function(shift)
889     {
890         this._cancelAnimation();
891         var bounds = this._windowForGesture();
892         shift = Number.constrain(shift, this._minimumBoundary - bounds.left, this._totalTime + this._minimumBoundary - bounds.right);
893         bounds.left += shift;
894         bounds.right += shift;
895         this._requestWindowTimes(bounds);
896     },
898     /**
899      * @return {{left: number, right: number}}
900      */
901     _windowForGesture: function()
902     {
903         var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
904         var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
905         return {left: windowLeft, right: windowRight};
906     },
908     /**
909      * @param {{left: number, right: number}} bounds
910      */
911     _requestWindowTimes: function(bounds)
912     {
913         bounds.left = Number.constrain(bounds.left, this._minimumBoundary, this._totalTime + this._minimumBoundary);
914         bounds.right = Number.constrain(bounds.right, this._minimumBoundary, this._totalTime + this._minimumBoundary);
915         if (bounds.right - bounds.left < WebInspector.FlameChart.MinimalTimeWindowMs)
916             return;
917         this._flameChartDelegate.requestWindowTimes(bounds.left, bounds.right);
918     },
920     /**
921      * @param {number} x
922      * @return {number}
923      */
924     _cursorTime: function(x)
925     {
926         return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._minimumBoundary;
927     },
929     /**
930      * @param {number} x
931      * @param {number} y
932      * @return {number}
933      */
934     _coordinatesToEntryIndex: function(x, y)
935     {
936         y += this._scrollTop;
937         var timelineData = this._timelineData();
938         if (!timelineData)
939             return -1;
940         var cursorTime = this._cursorTime(x);
941         var cursorLevel;
942         var offsetFromLevel;
943         if (this._isTopDown) {
944             cursorLevel = Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight);
945             offsetFromLevel = y - WebInspector.FlameChart.DividersBarHeight - cursorLevel * this._barHeight;
946         } else {
947             cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
948             offsetFromLevel = this._canvas.height / window.devicePixelRatio - cursorLevel * this._barHeight;
949         }
950         var entryStartTimes = timelineData.entryStartTimes;
951         var entryTotalTimes = timelineData.entryTotalTimes;
952         var entryIndexes = this._timelineLevels[cursorLevel];
953         if (!entryIndexes || !entryIndexes.length)
954             return -1;
956         /**
957          * @param {number} time
958          * @param {number} entryIndex
959          * @return {number}
960          */
961         function comparator(time, entryIndex)
962         {
963             return time - entryStartTimes[entryIndex];
964         }
965         var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator) - 1, 0);
967         /**
968          * @this {WebInspector.FlameChart}
969          * @param {number} entryIndex
970          * @return {boolean}
971          */
972         function checkEntryHit(entryIndex)
973         {
974             if (entryIndex === undefined)
975                 return false;
976             var startTime = entryStartTimes[entryIndex];
977             var duration = entryTotalTimes[entryIndex];
978             if (isNaN(duration)) {
979                 var dx = (startTime - cursorTime) / this._pixelToTime;
980                 var dy = this._barHeight / 2 - offsetFromLevel;
981                 return dx * dx + dy * dy < this._markerRadius * this._markerRadius;
982             }
983             var endTime = startTime + duration;
984             var barThreshold = 3 * this._pixelToTime;
985             return startTime - barThreshold < cursorTime && cursorTime < endTime + barThreshold;
986         }
988         var entryIndex = entryIndexes[indexOnLevel];
989         if (checkEntryHit.call(this, entryIndex))
990             return entryIndex;
991         entryIndex = entryIndexes[indexOnLevel + 1];
992         if (checkEntryHit.call(this, entryIndex))
993             return entryIndex;
994         return -1;
995     },
997     /**
998      * @param {number} x
999      * @return {number}
1000      */
1001     _markerIndexAtPosition: function(x)
1002     {
1003         var markers = this._timelineData().markers;
1004         if (!markers)
1005             return -1;
1006         var accurracyOffsetPx = 1;
1007         var time = this._cursorTime(x);
1008         var leftTime = this._cursorTime(x - accurracyOffsetPx);
1009         var rightTime = this._cursorTime(x + accurracyOffsetPx);
1011         var left = this._markerIndexBeforeTime(leftTime);
1012         var markerIndex = -1;
1013         var distance = Infinity;
1014         for (var i = left; i < markers.length && markers[i].startTime() < rightTime; i++) {
1015             var nextDistance = Math.abs(markers[i].startTime() - time);
1016             if (nextDistance < distance) {
1017                 markerIndex = i;
1018                 distance = nextDistance;
1019             }
1020         }
1021         return markerIndex;
1022     },
1024     /**
1025      * @param {number} time
1026      * @return {number}
1027      */
1028     _markerIndexBeforeTime: function(time)
1029     {
1030         /**
1031          * @param {number} markerTimestamp
1032          * @param {!WebInspector.FlameChartMarker} marker
1033          * @return {number}
1034          */
1035         function comparator(markerTimestamp, marker)
1036         {
1037             return markerTimestamp - marker.startTime();
1038         }
1039         return this._timelineData().markers.lowerBound(time, comparator);
1040     },
1042     /**
1043      * @param {number} height
1044      * @param {number} width
1045      */
1046     _draw: function(width, height)
1047     {
1048         var timelineData = this._timelineData();
1049         if (!timelineData)
1050             return;
1052         var context = this._canvas.getContext("2d");
1053         context.save();
1054         var ratio = window.devicePixelRatio;
1055         context.scale(ratio, ratio);
1057         var timeWindowRight = this._timeWindowRight;
1058         var timeWindowLeft = this._timeWindowLeft - this._paddingLeftTime;
1059         var entryTotalTimes = timelineData.entryTotalTimes;
1060         var entryStartTimes = timelineData.entryStartTimes;
1061         var entryLevels = timelineData.entryLevels;
1063         var titleIndices = new Uint32Array(entryTotalTimes.length);
1064         var nextTitleIndex = 0;
1065         var markerIndices = new Uint32Array(entryTotalTimes.length);
1066         var nextMarkerIndex = 0;
1067         var textPadding = this._dataProvider.textPadding();
1068         this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026");
1069         var minTextWidth = this._minTextWidth;
1070         var unclippedWidth = width - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth);
1072         var barHeight = this._barHeight;
1074         var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline();
1075         var colorBuckets = {};
1076         var minVisibleBarLevel = Math.max(Math.floor((this._scrollTop - this._baseHeight) / barHeight), 0);
1077         var maxVisibleBarLevel = Math.min(Math.floor((this._scrollTop - this._baseHeight + height) / barHeight), this._dataProvider.maxStackDepth());
1079         context.translate(0, -this._scrollTop);
1081         function comparator(time, entryIndex)
1082         {
1083             return time - entryStartTimes[entryIndex];
1084         }
1086         for (var level = minVisibleBarLevel; level <= maxVisibleBarLevel; ++level) {
1087             // Entries are ordered by start time within a level, so find the last visible entry.
1088             var levelIndexes = this._timelineLevels[level];
1089             var rightIndexOnLevel = levelIndexes.lowerBound(timeWindowRight, comparator) - 1;
1090             var lastDrawOffset = Infinity;
1091             for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
1092                 var entryIndex = levelIndexes[entryIndexOnLevel];
1093                 var entryStartTime = entryStartTimes[entryIndex];
1094                 var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]);
1095                 if (entryOffsetRight <= timeWindowLeft)
1096                     break;
1098                 var barX = this._timeToPositionClipped(entryStartTime);
1099                 // Check if the entry entirely fits into an already drawn pixel, we can just skip drawing it.
1100                 if (barX >= lastDrawOffset)
1101                     continue;
1102                 lastDrawOffset = barX;
1104                 var color = this._dataProvider.entryColor(entryIndex);
1105                 var bucket = colorBuckets[color];
1106                 if (!bucket) {
1107                     bucket = [];
1108                     colorBuckets[color] = bucket;
1109                 }
1110                 bucket.push(entryIndex);
1111             }
1112         }
1114         var colors = Object.keys(colorBuckets);
1115         // We don't use for-in here because it couldn't be optimized.
1116         for (var c = 0; c < colors.length; ++c) {
1117             var color = colors[c];
1118             context.fillStyle = color;
1119             context.strokeStyle = color;
1120             var indexes = colorBuckets[color];
1122             // First fill the boxes.
1123             context.beginPath();
1124             for (var i = 0; i < indexes.length; ++i) {
1125                 var entryIndex = indexes[i];
1126                 var entryStartTime = entryStartTimes[entryIndex];
1127                 var barX = this._timeToPositionClipped(entryStartTime);
1128                 var barRight = this._timeToPositionClipped(entryStartTime + entryTotalTimes[entryIndex]);
1129                 var barWidth = Math.max(barRight - barX, 1);
1130                 var barLevel = entryLevels[entryIndex];
1131                 var barY = this._levelToHeight(barLevel);
1132                 if (isNaN(entryTotalTimes[entryIndex])) {
1133                     context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
1134                     context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
1135                     markerIndices[nextMarkerIndex++] = entryIndex;
1136                 } else {
1137                     context.rect(barX, barY, barWidth - 0.4, barHeight - 1);
1138                     if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex))
1139                         titleIndices[nextTitleIndex++] = entryIndex;
1140                 }
1141             }
1142             context.fill();
1143         }
1145         context.strokeStyle = "rgb(0, 0, 0)";
1146         context.beginPath();
1147         for (var m = 0; m < nextMarkerIndex; ++m) {
1148             var entryIndex = markerIndices[m];
1149             var entryStartTime = entryStartTimes[entryIndex];
1150             var barX = this._timeToPositionClipped(entryStartTime);
1151             var barLevel = entryLevels[entryIndex];
1152             var barY = this._levelToHeight(barLevel);
1153             context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
1154             context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
1155         }
1156         context.stroke();
1158         context.textBaseline = "alphabetic";
1160         for (var i = 0; i < nextTitleIndex; ++i) {
1161             var entryIndex = titleIndices[i];
1162             var entryStartTime = entryStartTimes[entryIndex];
1163             var barX = this._timeToPositionClipped(entryStartTime);
1164             var barRight = Math.min(this._timeToPositionClipped(entryStartTime + entryTotalTimes[entryIndex]), unclippedWidth) + 1;
1165             var barWidth = barRight - barX;
1166             var barLevel = entryLevels[entryIndex];
1167             var barY = this._levelToHeight(barLevel);
1168             var text = this._dataProvider.entryTitle(entryIndex);
1169             if (text && text.length) {
1170                 context.font = this._dataProvider.entryFont(entryIndex);
1171                 text = this._prepareText(context, text, barWidth - 2 * textPadding);
1172             }
1174             if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight))
1175                 continue;
1176             if (!text || !text.length)
1177                 continue;
1179             context.fillStyle = this._dataProvider.textColor(entryIndex);
1180             context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta);
1181         }
1183         this._drawFlowEvents(context, width, height);
1185         context.restore();
1187         var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary());
1188         WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets);
1189         this._drawMarkers();
1191         this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
1192         this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
1193         this._updateMarkerHighlight();
1194         this._updateRangeSelectionOverlay();
1195     },
1197     /**
1198      * @param {!CanvasRenderingContext2D} context
1199      * @param {number} height
1200      * @param {number} width
1201      */
1202     _drawFlowEvents: function(context, width, height)
1203     {
1204         var timelineData = this._timelineData();
1205         var timeWindowRight = this._timeWindowRight;
1206         var timeWindowLeft = this._timeWindowLeft;
1207         var flowStartTimes = timelineData.flowStartTimes;
1208         var flowEndTimes = timelineData.flowEndTimes;
1209         var flowStartLevels = timelineData.flowStartLevels;
1210         var flowEndLevels = timelineData.flowEndLevels;
1211         var flowCount = flowStartTimes.length;
1212         var endIndex = flowStartTimes.lowerBound(timeWindowRight);
1214         var color = [];
1215         var fadeColorsCount = 8;
1216         for (var i = 0; i <= fadeColorsCount; ++i)
1217             color[i] = "rgba(128, 0, 0, " + i / fadeColorsCount + ")";
1218         var fadeColorsRange = color.length;
1219         var minimumFlowDistancePx = 15;
1220         var flowArcHeight = 4 * this._barHeight;
1221         var colorIndex = 0;
1222         context.lineWidth = 0.5;
1223         for (var i = 0; i < endIndex; ++i) {
1224             if (flowEndTimes[i] < timeWindowLeft)
1225                 continue;
1226             var startX = this._timeToPosition(flowStartTimes[i]);
1227             var endX = this._timeToPosition(flowEndTimes[i]);
1228             if (endX - startX < minimumFlowDistancePx)
1229                 continue;
1230             if (startX < -minimumFlowDistancePx && endX > width + minimumFlowDistancePx)
1231                 continue;
1232             // Assign a trasparent color if the flow is small enough or if the previous color was a transparent color.
1233             if (endX - startX < minimumFlowDistancePx + fadeColorsRange || colorIndex !== color.length - 1) {
1234                 colorIndex = Math.min(fadeColorsRange - 1, Math.floor(endX - startX - minimumFlowDistancePx));
1235                 context.strokeStyle = color[colorIndex];
1236             }
1237             var startY = this._levelToHeight(flowStartLevels[i]) + this._barHeight;
1238             var endY = this._levelToHeight(flowEndLevels[i]);
1239             context.beginPath();
1240             context.moveTo(startX, startY);
1241             var arcHeight = Math.max(Math.sqrt(Math.abs(startY - endY)), flowArcHeight) + 5;
1242             context.bezierCurveTo(startX, startY + arcHeight,
1243                                   endX, endY + arcHeight,
1244                                   endX, endY + this._barHeight);
1245             context.stroke();
1246         }
1247     },
1249     _drawMarkers: function()
1250     {
1251         var markers = this._timelineData().markers;
1252         var left = this._markerIndexBeforeTime(this._calculator.minimumBoundary());
1253         var rightBoundary = this._calculator.maximumBoundary();
1255         var context = this._canvas.getContext("2d");
1256         context.save();
1257         var ratio = window.devicePixelRatio;
1258         context.scale(ratio, ratio);
1259         var height = WebInspector.FlameChart.DividersBarHeight - 1;
1260         for (var i = left; i < markers.length; i++) {
1261             var timestamp = markers[i].startTime();
1262             if (timestamp > rightBoundary)
1263                 break;
1264             markers[i].draw(context, this._calculator.computePosition(timestamp), height, this._timeToPixel);
1265         }
1266         context.restore();
1267     },
1269     _updateMarkerHighlight: function()
1270     {
1271         var element = this._markerHighlighElement;
1272         if (element.parentElement)
1273             element.remove();
1274         var markerIndex = this._highlightedMarkerIndex;
1275         if (markerIndex === -1)
1276             return;
1277         var marker = this._timelineData().markers[markerIndex];
1278         var barX = this._timeToPositionClipped(marker.startTime());
1279         element.title = marker.title();
1280         var style = element.style;
1281         style.left = barX + "px";
1282         style.backgroundColor = marker.color();
1283         this.contentElement.appendChild(element);
1284     },
1286     /**
1287      * @param {?WebInspector.FlameChart.TimelineData} timelineData
1288      */
1289     _processTimelineData: function(timelineData)
1290     {
1291         if (!timelineData) {
1292             this._timelineLevels = null;
1293             this._rawTimelineData = null;
1294             this._rawTimelineDataLength = 0;
1295             return;
1296         }
1298         var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1);
1299         for (var i = 0; i < timelineData.entryLevels.length; ++i)
1300             ++entryCounters[timelineData.entryLevels[i]];
1301         var levelIndexes = new Array(entryCounters.length);
1302         for (var i = 0; i < levelIndexes.length; ++i) {
1303             levelIndexes[i] = new Uint32Array(entryCounters[i]);
1304             entryCounters[i] = 0;
1305         }
1306         for (var i = 0; i < timelineData.entryLevels.length; ++i) {
1307             var level = timelineData.entryLevels[i];
1308             levelIndexes[level][entryCounters[level]++] = i;
1309         }
1310         this._timelineLevels = levelIndexes;
1311         this._rawTimelineData = timelineData;
1312         this._rawTimelineDataLength = timelineData.entryStartTimes.length;
1313     },
1315     /**
1316      * @param {number} entryIndex
1317      */
1318     setSelectedEntry: function(entryIndex)
1319     {
1320         if (entryIndex === -1 && !this._isDragging)
1321             this._hideRangeSelection();
1322         if (this._selectedEntryIndex === entryIndex)
1323             return;
1324         this._selectedEntryIndex = entryIndex;
1325         this._revealEntry(entryIndex);
1326         this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
1327     },
1329     /**
1330      * @param {!Element} element
1331      * @param {number} entryIndex
1332      */
1333     _updateElementPosition: function(element, entryIndex)
1334     {
1335         /** @const */ var elementMinWidth = 2;
1336         if (element.parentElement)
1337             element.remove();
1338         if (entryIndex === -1)
1339             return;
1340         var timeRange = this._dataProvider.highlightTimeRange(entryIndex);
1341         if (!timeRange)
1342             return;
1343         var timelineData = this._timelineData();
1344         var barX = this._timeToPositionClipped(timeRange.startTime);
1345         var barRight = this._timeToPositionClipped(timeRange.endTime);
1346         if (barRight === 0 || barX === this._canvas.width)
1347             return;
1348         var barWidth = barRight - barX;
1349         var barCenter = barX + barWidth / 2;
1350         barWidth = Math.max(barWidth, elementMinWidth);
1351         barX = barCenter - barWidth / 2;
1352         var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop;
1353         var style = element.style;
1354         style.left = barX + "px";
1355         style.top = barY + "px";
1356         style.width = barWidth + "px";
1357         style.height = this._barHeight - 1 + "px";
1358         this.contentElement.appendChild(element);
1359     },
1361     /**
1362      * @param {number} time
1363      * @return {number}
1364      */
1365     _timeToPositionClipped: function(time)
1366     {
1367         return Number.constrain(this._timeToPosition(time), 0, this._canvas.width);
1368     },
1370     /**
1371      * @param {number} time
1372      * @return {number}
1373      */
1374     _timeToPosition: function(time)
1375     {
1376         return Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
1377     },
1379     /**
1380      * @param {number} level
1381      * @return {number}
1382      */
1383     _levelToHeight: function(level)
1384     {
1385          return this._baseHeight - level * this._barHeightDelta;
1386     },
1388     /**
1389      * @param {!Array<!{title: string, value: (string|!Element)}>} entryInfo
1390      * @return {!Element}
1391      */
1392     _buildEntryInfo: function(entryInfo)
1393     {
1394         var infoTable = createElementWithClass("table", "info-table");
1395         for (var entry of entryInfo) {
1396             var row = infoTable.createChild("tr");
1397             row.createChild("td", "title").textContent = entry.title;
1398             row.createChild("td").textContent = typeof entry.value === "string" ? entry.value : entry.value.textContent;
1399         }
1400         return infoTable;
1401     },
1403     /**
1404      * @param {!CanvasRenderingContext2D} context
1405      * @param {string} title
1406      * @param {number} maxSize
1407      * @return {string}
1408      */
1409     _prepareText: function(context, title, maxSize)
1410     {
1411         var titleWidth = this._measureWidth(context, title);
1412         if (maxSize >= titleWidth)
1413             return title;
1415         var l = 2;
1416         var r = title.length;
1417         while (l < r) {
1418             var m = (l + r) >> 1;
1419             if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize)
1420                 l = m + 1;
1421             else
1422                 r = m;
1423         }
1424         title = title.trimMiddle(r - 1);
1425         return title !== "\u2026" ? title : "";
1426     },
1428     /**
1429      * @param {!CanvasRenderingContext2D} context
1430      * @param {string} text
1431      * @return {number}
1432      */
1433     _measureWidth: function(context, text)
1434     {
1435         if (text.length > 20)
1436             return context.measureText(text).width;
1438         var font = context.font;
1439         var textWidths = this._textWidth[font];
1440         if (!textWidths) {
1441             textWidths = {};
1442             this._textWidth[font] = textWidths;
1443         }
1444         var width = textWidths[text];
1445         if (!width) {
1446             width = context.measureText(text).width;
1447             textWidths[text] = width;
1448         }
1449         return width;
1450     },
1452     _updateBoundaries: function()
1453     {
1454         this._totalTime = this._dataProvider.totalTime();
1455         this._minimumBoundary = this._dataProvider.minimumBoundary();
1457         if (this._timeWindowRight !== Infinity) {
1458             this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime;
1459             this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime;
1460             this._windowWidth = this._windowRight - this._windowLeft;
1461         } else if (this._timeWindowLeft === Infinity) {
1462             this._windowLeft = Infinity;
1463             this._windowRight = Infinity;
1464             this._windowWidth = 1;
1465         } else {
1466             this._windowLeft = 0;
1467             this._windowRight = 1;
1468             this._windowWidth = 1;
1469         }
1471         this._pixelWindowWidth = this._offsetWidth - this._paddingLeft;
1472         this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
1473         this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
1475         this._timeToPixel = this._totalPixels / this._totalTime;
1476         this._pixelToTime = this._totalTime / this._totalPixels;
1477         this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
1479         this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight;
1481         this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth());
1482         this._vScrollContent.style.height = this._totalHeight + "px";
1483         this._updateScrollBar();
1484     },
1486     onResize: function()
1487     {
1488         this._updateScrollBar();
1489         this._updateContentElementSize();
1490         this.scheduleUpdate();
1491     },
1493     _updateScrollBar: function()
1494     {
1495         var showScroll = this._totalHeight > this._offsetHeight;
1496         if (this._vScrollElement.classList.contains("hidden") === showScroll) {
1497             this._vScrollElement.classList.toggle("hidden", !showScroll);
1498             this._updateContentElementSize();
1499         }
1500     },
1502     _updateContentElementSize: function()
1503     {
1504         this._offsetWidth = this.contentElement.offsetWidth;
1505         this._offsetHeight = this.contentElement.offsetHeight;
1506     },
1508     _onScroll: function()
1509     {
1510         this._scrollTop = this._vScrollElement.scrollTop;
1511         this.scheduleUpdate();
1512     },
1514     scheduleUpdate: function()
1515     {
1516         if (this._updateTimerId || this._cancelWindowTimesAnimation)
1517             return;
1518         this._updateTimerId = this.element.window().requestAnimationFrame(this.update.bind(this));
1519     },
1521     update: function()
1522     {
1523         this._updateTimerId = 0;
1524         if (!this._timelineData())
1525             return;
1526         this._resetCanvas();
1527         this._updateBoundaries();
1528         this._calculator._updateBoundaries(this);
1529         this._draw(this._offsetWidth, this._offsetHeight);
1530     },
1532     reset: function()
1533     {
1534         this._vScrollElement.scrollTop = 0;
1535         this._highlightedMarkerIndex = -1;
1536         this._highlightedEntryIndex = -1;
1537         this._selectedEntryIndex = -1;
1538         this._textWidth = {};
1539         this.update();
1540     },
1542     _enabled: function()
1543     {
1544         return this._rawTimelineDataLength !== 0;
1545     },
1547     __proto__: WebInspector.HBox.prototype