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