• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
34WebInspector.FlameChartDelegate = function() { }
35
36WebInspector.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 */
51WebInspector.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), false);
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
92WebInspector.FlameChart.DividersBarHeight = 20;
93
94/**
95 * @interface
96 */
97WebInspector.FlameChartDataProvider = function()
98{
99}
100
101/** @typedef {!{
102        entryLevels: (!Array.<number>|!Uint8Array),
103        entryTotalTimes: (!Array.<number>|!Float32Array),
104        entryStartTimes: (!Array.<number>|!Float64Array)
105    }}
106 */
107WebInspector.FlameChart.TimelineData;
108
109WebInspector.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    minimumBoundary: 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} timeToPosition
181     * @return {boolean}
182     */
183    decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) { },
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 {?{startTime: number, endTime: number}}
209     */
210    highlightTimeRange: function(entryIndex) { },
211
212    /**
213     * @return {number}
214     */
215    paddingLeft: function() { },
216}
217
218WebInspector.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 */
229WebInspector.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}
236
237WebInspector.FlameChart.ColorGenerator.prototype = {
238    /**
239     * @param {string} id
240     * @param {string|!CanvasGradient} color
241     */
242    setColorForID: function(id, color)
243    {
244        this._colors[id] = color;
245    },
246
247    /**
248     * @param {string} id
249     * @return {string}
250     */
251    colorForID: function(id)
252    {
253        var color = this._colors[id];
254        if (!color) {
255            color = this._generateColorForID(id);
256            this._colors[id] = color;
257        }
258        return color;
259    },
260
261    /**
262     * @param {string} id
263     * @return {string}
264     */
265    _generateColorForID: function(id)
266    {
267        var hash = id.hashCode();
268        var h = this._indexToValueInSpace(hash, this._hueSpace);
269        var s = this._indexToValueInSpace(hash, this._satSpace);
270        var l = this._indexToValueInSpace(hash, 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 + Math.floor(index / space.count * (space.max - space.min));
285    }
286}
287
288
289/**
290 * @constructor
291 * @implements {WebInspector.TimelineGrid.Calculator}
292 */
293WebInspector.FlameChart.Calculator = function()
294{
295    this._paddingLeft = 0;
296}
297
298WebInspector.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.minimumBoundary();
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
373WebInspector.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.entryStartTimes.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._minimumBoundary - this._dragStartWindowLeft,
438            this._minimumBoundary + 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 {?Event} 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 {?Event} e
498     */
499    _onMouseWheel: function(e)
500    {
501        var scrollIsThere = this._totalHeight > this._offsetHeight;
502        var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
503        var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
504
505        var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !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._minimumBoundary - windowLeft, this._totalTime + this._minimumBoundary - 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._minimumBoundary, this._totalTime + this._minimumBoundary);
522        windowRight = Number.constrain(windowRight, this._minimumBoundary, this._totalTime + this._minimumBoundary);
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._minimumBoundary;
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 cursorTime = this._cursorTime(x);
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 entryStartTimes = timelineData.entryStartTimes;
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 - entryStartTimes[entryIndex];
568        }
569        var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, 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 = entryStartTimes[entryIndex];
581            var duration = entryTotalTimes[entryIndex];
582            if (isNaN(duration)) {
583                var dx = (startTime - cursorTime) / 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 < cursorTime && cursorTime < 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;
617        var timeWindowLeft = this._timeWindowLeft;
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 entryStartTimes = timelineData.entryStartTimes;
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 timeToPosition = this._timeToPosition.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 - entryStartTimes[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 entryStartTime = entryStartTimes[entryIndex];
657                var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]);
658                if (entryOffsetRight <= timeWindowLeft)
659                    break;
660
661                var barX = this._timeToPosition(entryStartTime);
662                if (barX >= lastDrawOffset)
663                    continue;
664                var barRight = Math.min(this._timeToPosition(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 entryStartTime = entryStartTimes[entryIndex];
690                var barX = this._timeToPosition(entryStartTime);
691                var barRight = this._timeToPosition(entryStartTime + 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 entryStartTime = entryStartTimes[entryIndex];
713            var barX = this._timeToPosition(entryStartTime);
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 entryStartTime = entryStartTimes[entryIndex];
726            var barX = this._timeToPosition(entryStartTime);
727            var barRight = this._timeToPosition(entryStartTime + 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, timeToPosition))
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.entryStartTimes.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._timeToPosition(timeRange.startTime);
803        var barRight = this._timeToPosition(timeRange.endTime);
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    /**
817     * @param {number} time
818     */
819    _timeToPosition: function(time)
820    {
821        var value = Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
822        return Math.min(this._canvas.width, Math.max(0, value));
823    },
824
825    _levelToHeight: function(level)
826    {
827         return this._baseHeight - level * this._barHeightDelta;
828    },
829
830    _buildEntryInfo: function(entryInfo)
831    {
832        var infoTable = document.createElement("table");
833        infoTable.className = "info-table";
834        for (var i = 0; i < entryInfo.length; ++i) {
835            var row = infoTable.createChild("tr");
836            var titleCell = row.createChild("td");
837            titleCell.textContent = entryInfo[i].title;
838            titleCell.className = "title";
839            var textCell = row.createChild("td");
840            textCell.textContent = entryInfo[i].text;
841        }
842        return infoTable;
843    },
844
845    /**
846     * @param {!CanvasRenderingContext2D} context
847     * @param {string} title
848     * @param {number} maxSize
849     * @return {string}
850     */
851    _prepareText: function(context, title, maxSize)
852    {
853        var titleWidth = this._measureWidth(context, title);
854        if (maxSize >= titleWidth)
855            return title;
856
857        var l = 2;
858        var r = title.length;
859        while (l < r) {
860            var m = (l + r) >> 1;
861            if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize)
862                l = m + 1;
863            else
864                r = m;
865        }
866        title = title.trimMiddle(r - 1);
867        return title !== "\u2026" ? title : "";
868    },
869
870    /**
871     * @param {!CanvasRenderingContext2D} context
872     * @param {string} text
873     * @return {number}
874     */
875    _measureWidth: function(context, text)
876    {
877        if (text.length > 20)
878            return context.measureText(text).width;
879
880        var font = context.font;
881        var textWidths = this._textWidth[font];
882        if (!textWidths) {
883            textWidths = {};
884            this._textWidth[font] = textWidths;
885        }
886        var width = textWidths[text];
887        if (!width) {
888            width = context.measureText(text).width;
889            textWidths[text] = width;
890        }
891        return width;
892    },
893
894    _updateBoundaries: function()
895    {
896        this._totalTime = this._dataProvider.totalTime();
897        this._minimumBoundary = this._dataProvider.minimumBoundary();
898
899        if (this._timeWindowRight !== Infinity) {
900            this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime;
901            this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime;
902            this._windowWidth = this._windowRight - this._windowLeft;
903        } else {
904            this._windowLeft = 0;
905            this._windowRight = 1;
906            this._windowWidth = 1;
907        }
908
909        this._pixelWindowWidth = this._offsetWidth - this._paddingLeft;
910        this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
911        this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
912        this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
913
914        this._timeToPixel = this._totalPixels / this._totalTime;
915        this._pixelToTime = this._totalTime / this._totalPixels;
916        this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
917
918        this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight;
919
920        this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth() + 1);
921        this._vScrollContent.style.height = this._totalHeight + "px";
922        this._scrollTop = this._vScrollElement.scrollTop;
923        this._updateScrollBar();
924    },
925
926    onResize: function()
927    {
928        this._updateScrollBar();
929        this._scheduleUpdate();
930    },
931
932    _updateScrollBar: function()
933    {
934        var showScroll = this._totalHeight > this._offsetHeight;
935        this._vScrollElement.classList.toggle("hidden", !showScroll);
936        this._offsetWidth = this.element.offsetWidth - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth);
937        this._offsetHeight = this.element.offsetHeight;
938    },
939
940    _scheduleUpdate: function()
941    {
942        if (this._updateTimerId)
943            return;
944        this._updateTimerId = requestAnimationFrame(this.update.bind(this));
945    },
946
947    update: function()
948    {
949        this._updateTimerId = 0;
950        if (!this._timelineData())
951            return;
952        this._resetCanvas();
953        this._updateBoundaries();
954        this._calculator._updateBoundaries(this);
955        this._draw(this._offsetWidth, this._offsetHeight);
956    },
957
958    reset: function()
959    {
960        this._highlightedEntryIndex = -1;
961        this._selectedEntryIndex = -1;
962        this._textWidth = {};
963        this.update();
964    },
965
966    __proto__: WebInspector.HBox.prototype
967}
968