Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / components / FlameChart.js
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 }