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