• 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 * @constructor
33 * @extends {WebInspector.View}
34 * @param {!WebInspector.FlameChartDataProvider} dataProvider
35 */
36WebInspector.FlameChart = function(dataProvider)
37{
38    WebInspector.View.call(this);
39    this.registerRequiredCSS("flameChart.css");
40    this.element.className = "fill";
41    this.element.id = "cpu-flame-chart";
42
43    this._overviewPane = new WebInspector.FlameChart.OverviewPane(dataProvider);
44    this._overviewPane.show(this.element);
45
46    this._mainPane = new WebInspector.FlameChart.MainPane(dataProvider, this._overviewPane);
47    this._mainPane.show(this.element);
48    this._mainPane.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this);
49    this._overviewPane._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
50
51    if (!WebInspector.FlameChart._colorGenerator)
52        WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator();
53}
54
55WebInspector.FlameChart.prototype = {
56    /**
57     * @param {!WebInspector.Event} event
58     */
59    _onWindowChanged: function(event)
60    {
61        this._mainPane.changeWindow(this._overviewPane._overviewGrid.windowLeft(), this._overviewPane._overviewGrid.windowRight());
62    },
63
64    /**
65     * @param {!number} timeLeft
66     * @param {!number} timeRight
67     */
68    selectRange: function(timeLeft, timeRight)
69    {
70        this._overviewPane._selectRange(timeLeft, timeRight);
71    },
72
73    /**
74     * @param {!WebInspector.Event} event
75     */
76    _onEntrySelected: function(event)
77    {
78        this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, event.data);
79    },
80
81    update: function()
82    {
83        this._overviewPane.update();
84        this._mainPane.update();
85    },
86
87    __proto__: WebInspector.View.prototype
88};
89
90/**
91 * @interface
92 */
93WebInspector.FlameChartDataProvider = function()
94{
95}
96
97WebInspector.FlameChartDataProvider.prototype = {
98    /**
99     * @param {!WebInspector.FlameChart.ColorGenerator} colorGenerator
100     * @return {!Object}
101     */
102    timelineData: function(colorGenerator) { },
103
104    /**
105     * @param {number} entryIndex
106     */
107    prepareHighlightedEntryInfo: function(entryIndex) { },
108
109    /**
110     * @param {number} entryIndex
111     * @return {boolean}
112     */
113    canJumpToEntry: function(entryIndex) { },
114
115    /**
116     * @param {number} entryIndex
117     * @return {!Object}
118     */
119    entryData: function(entryIndex) { }
120}
121
122/**
123 * @constructor
124 * @implements {WebInspector.TimelineGrid.Calculator}
125 */
126WebInspector.FlameChart.Calculator = function()
127{
128}
129
130WebInspector.FlameChart.Calculator.prototype = {
131    /**
132     * @param {!WebInspector.FlameChart.MainPane} mainPane
133     */
134    _updateBoundaries: function(mainPane)
135    {
136        function log10(x)
137        {
138            return Math.log(x) / Math.LN10;
139        }
140        this._decimalDigits = Math.max(0, -Math.floor(log10(mainPane._timelineGrid.gridSliceTime * 1.01)));
141        var totalTime = mainPane._timelineData().totalTime;
142        this._minimumBoundaries = mainPane._windowLeft * totalTime;
143        this._maximumBoundaries = mainPane._windowRight * totalTime;
144        this.paddingLeft = mainPane._paddingLeft;
145        this._width = mainPane._canvas.width - this.paddingLeft;
146        this._timeToPixel = this._width / this.boundarySpan();
147    },
148
149    /**
150     * @param {number} time
151     * @return {number}
152     */
153    computePosition: function(time)
154    {
155        return (time - this._minimumBoundaries) * this._timeToPixel + this.paddingLeft;
156    },
157
158    /**
159     * @param {number} value
160     * @param {boolean=} hires
161     * @return {string}
162     */
163    formatTime: function(value, hires)
164    {
165        var format = "%." + this._decimalDigits + "f\u2009ms";
166        return WebInspector.UIString(format, value + this._minimumBoundaries);
167    },
168
169    /**
170     * @return {number}
171     */
172    maximumBoundary: function()
173    {
174        return this._maximumBoundaries;
175    },
176
177    /**
178     * @return {number}
179     */
180    minimumBoundary: function()
181    {
182        return this._minimumBoundaries;
183    },
184
185    /**
186     * @return {number}
187     */
188    zeroTime: function()
189    {
190        return 0;
191    },
192
193    /**
194     * @return {number}
195     */
196    boundarySpan: function()
197    {
198        return this._maximumBoundaries - this._minimumBoundaries;
199    }
200}
201
202/**
203 * @constructor
204 * @implements {WebInspector.TimelineGrid.Calculator}
205 */
206WebInspector.FlameChart.OverviewCalculator = function()
207{
208}
209
210WebInspector.FlameChart.OverviewCalculator.prototype = {
211    /**
212     * @param {!WebInspector.FlameChart.OverviewPane} overviewPane
213     */
214    _updateBoundaries: function(overviewPane)
215    {
216        this._minimumBoundaries = 0;
217        var totalTime = overviewPane._timelineData().totalTime;
218        this._maximumBoundaries = totalTime;
219        this._xScaleFactor = overviewPane._overviewCanvas.width / totalTime;
220    },
221
222    /**
223     * @param {number} time
224     * @return {number}
225     */
226    computePosition: function(time)
227    {
228        return (time - this._minimumBoundaries) * this._xScaleFactor;
229    },
230
231    /**
232     * @param {number} value
233     * @param {boolean=} hires
234     * @return {string}
235     */
236    formatTime: function(value, hires)
237    {
238        return Number.secondsToString((value + this._minimumBoundaries) / 1000, hires);
239    },
240
241    /**
242     * @return {number}
243     */
244    maximumBoundary: function()
245    {
246        return this._maximumBoundaries;
247    },
248
249    /**
250     * @return {number}
251     */
252    minimumBoundary: function()
253    {
254        return this._minimumBoundaries;
255    },
256
257    /**
258     * @return {number}
259     */
260    zeroTime: function()
261    {
262        return this._minimumBoundaries;
263    },
264
265    /**
266     * @return {number}
267     */
268    boundarySpan: function()
269    {
270        return this._maximumBoundaries - this._minimumBoundaries;
271    }
272}
273
274WebInspector.FlameChart.Events = {
275    EntrySelected: "EntrySelected"
276}
277
278/**
279 * @constructor
280 */
281WebInspector.FlameChart.ColorGenerator = function()
282{
283    this._colorPairs = {};
284    this._colorIndexes = [];
285    this._currentColorIndex = 0;
286    this._colorPairForID("(idle)::0", 50);
287    this._colorPairForID("(program)::0", 50);
288    this._colorPairForID("(garbage collector)::0", 50);
289}
290
291WebInspector.FlameChart.ColorGenerator.prototype = {
292    /**
293     * @param {!string} id
294     * @param {number=} sat
295     */
296    _colorPairForID: function(id, sat)
297    {
298        if (typeof sat !== "number")
299            sat = 100;
300        var colorPairs = this._colorPairs;
301        var colorPair = colorPairs[id];
302        if (!colorPair) {
303            colorPairs[id] = colorPair = this._createPair(this._currentColorIndex++, sat);
304            this._colorIndexes[colorPair.index] = colorPair;
305        }
306        return colorPair;
307    },
308
309    /**
310     * @param {!number} index
311     */
312    _colorPairForIndex: function(index)
313    {
314        return this._colorIndexes[index];
315    },
316
317    /**
318     * @param {!number} index
319     * @param {!number} sat
320     */
321    _createPair: function(index, sat)
322    {
323        var hue = (index * 7 + 12 * (index % 2)) % 360;
324        return {index: index, highlighted: "hsla(" + hue + ", " + sat + "%, 33%, 0.7)", normal: "hsla(" + hue + ", " + sat + "%, 66%, 0.7)"}
325    }
326}
327
328/**
329 * @interface
330 */
331WebInspector.FlameChart.OverviewPaneInterface = function()
332{
333}
334
335WebInspector.FlameChart.OverviewPaneInterface.prototype = {
336    /**
337     * @param {number} zoom
338     * @param {number} referencePoint
339     */
340    zoom: function(zoom, referencePoint) { },
341
342    /**
343     * @param {number} windowLeft
344     * @param {number} windowRight
345     */
346    setWindow: function(windowLeft, windowRight) { },
347}
348
349/**
350 * @constructor
351 * @extends {WebInspector.View}
352 * @implements {WebInspector.FlameChart.OverviewPaneInterface}
353 * @param {!WebInspector.FlameChartDataProvider} dataProvider
354 */
355WebInspector.FlameChart.OverviewPane = function(dataProvider)
356{
357    WebInspector.View.call(this);
358    this._overviewContainer = this.element.createChild("div", "overview-container");
359    this._overviewGrid = new WebInspector.OverviewGrid("flame-chart");
360    this._overviewGrid.element.classList.add("fill");
361    this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas");
362    this._overviewContainer.appendChild(this._overviewGrid.element);
363    this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator();
364    this._dataProvider = dataProvider;
365}
366
367WebInspector.FlameChart.OverviewPane.prototype = {
368    /**
369     * @param {number} zoom
370     * @param {number} referencePoint
371     */
372    zoom: function(zoom, referencePoint)
373    {
374        this._overviewGrid.zoom(zoom, referencePoint);
375    },
376
377    /**
378     * @param {number} windowLeft
379     * @param {number} windowRight
380     */
381    setWindow: function(windowLeft, windowRight)
382    {
383        this._overviewGrid.setWindow(windowLeft, windowRight);
384    },
385
386    /**
387     * @param {!number} timeLeft
388     * @param {!number} timeRight
389     */
390    _selectRange: function(timeLeft, timeRight)
391    {
392        var timelineData = this._timelineData();
393        if (!timelineData)
394            return;
395        this._overviewGrid.setWindow(timeLeft / timelineData._totalTime, timeRight / timelineData._totalTime);
396    },
397
398    _timelineData: function()
399    {
400        return this._dataProvider.timelineData(WebInspector.FlameChart._colorGenerator);
401    },
402
403    onResize: function()
404    {
405        this._scheduleUpdate();
406    },
407
408    _scheduleUpdate: function()
409    {
410        if (this._updateTimerId)
411            return;
412        this._updateTimerId = setTimeout(this.update.bind(this), 10);
413    },
414
415    update: function()
416    {
417        this._updateTimerId = 0;
418        var timelineData = this._timelineData();
419        if (!timelineData)
420            return;
421        this._resetCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20);
422        this._overviewCalculator._updateBoundaries(this);
423        this._overviewGrid.updateDividers(this._overviewCalculator);
424        WebInspector.FlameChart.OverviewPane.drawOverviewCanvas(
425            timelineData,
426            this._overviewCanvas.getContext("2d"),
427            this._overviewContainer.clientWidth,
428            this._overviewContainer.clientHeight - 20
429        );
430    },
431
432    /**
433     * @param {!number} width
434     * @param {!number} height
435     */
436    _resetCanvas: function(width, height)
437    {
438        var ratio = window.devicePixelRatio;
439        this._overviewCanvas.width = width * ratio;
440        this._overviewCanvas.height = height * ratio;
441    },
442
443    __proto__: WebInspector.View.prototype
444}
445
446/**
447 * @param {!Object} timelineData
448 * @param {!number} width
449 */
450WebInspector.FlameChart.OverviewPane.calculateDrawData = function(timelineData, width)
451{
452    var entryOffsets = timelineData.entryOffsets;
453    var entryTotalTimes = timelineData.entryTotalTimes;
454    var entryLevels = timelineData.entryLevels;
455    var length = entryOffsets.length;
456
457    var drawData = new Uint8Array(width);
458    var scaleFactor = width / timelineData.totalTime;
459
460    for (var entryIndex = 0; entryIndex < length; ++entryIndex) {
461        var start = Math.floor(entryOffsets[entryIndex] * scaleFactor);
462        var finish = Math.floor((entryOffsets[entryIndex] + entryTotalTimes[entryIndex]) * scaleFactor);
463        for (var x = start; x <= finish; ++x)
464            drawData[x] = Math.max(drawData[x], entryLevels[entryIndex] + 1);
465    }
466    return drawData;
467}
468
469/**
470 * @param {!Object} timelineData
471 * @param {!Object} context
472 * @param {!number} width
473 * @param {!number} height
474 */
475WebInspector.FlameChart.OverviewPane.drawOverviewCanvas = function(timelineData, context, width, height)
476{
477    var drawData = WebInspector.FlameChart.OverviewPane.calculateDrawData(timelineData, width);
478    if (!drawData)
479        return;
480
481    var ratio = window.devicePixelRatio;
482    var canvasWidth = width * ratio;
483    var canvasHeight = height * ratio;
484
485    var yScaleFactor = canvasHeight / (timelineData.maxStackDepth * 1.1);
486    context.lineWidth = 1;
487    context.translate(0.5, 0.5);
488    context.strokeStyle = "rgba(20,0,0,0.4)";
489    context.fillStyle = "rgba(214,225,254,0.8)";
490    context.moveTo(-1, canvasHeight - 1);
491    if (drawData)
492      context.lineTo(-1, Math.round(height - drawData[0] * yScaleFactor - 1));
493    var value;
494    for (var x = 0; x < width; ++x) {
495        value = Math.round(canvasHeight - drawData[x] * yScaleFactor - 1);
496        context.lineTo(x * ratio, value);
497    }
498    context.lineTo(canvasWidth + 1, value);
499    context.lineTo(canvasWidth + 1, canvasHeight - 1);
500    context.fill();
501    context.stroke();
502    context.closePath();
503}
504
505/**
506 * @constructor
507 * @extends {WebInspector.View}
508 * @param {!WebInspector.FlameChartDataProvider} dataProvider
509 * @param {!WebInspector.FlameChart.OverviewPaneInterface} overviewPane
510 */
511WebInspector.FlameChart.MainPane = function(dataProvider, overviewPane)
512{
513    WebInspector.View.call(this);
514    this._overviewPane = overviewPane;
515    this._chartContainer = this.element.createChild("div", "chart-container");
516    this._timelineGrid = new WebInspector.TimelineGrid();
517    this._chartContainer.appendChild(this._timelineGrid.element);
518    this._calculator = new WebInspector.FlameChart.Calculator();
519
520    this._canvas = this._chartContainer.createChild("canvas");
521    this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this));
522    this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
523    this._canvas.addEventListener("click", this._onClick.bind(this), false);
524    WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize");
525
526    this._entryInfo = this._chartContainer.createChild("div", "entry-info");
527
528    this._dataProvider = dataProvider;
529
530    this._windowLeft = 0.0;
531    this._windowRight = 1.0;
532    this._windowWidth = 1.0;
533    this._barHeight = 15;
534    this._minWidth = 1;
535    this._paddingLeft = 15;
536    this._highlightedEntryIndex = -1;
537}
538
539WebInspector.FlameChart.MainPane.prototype = {
540    _timelineData: function()
541    {
542        return this._dataProvider.timelineData(WebInspector.FlameChart._colorGenerator);
543    },
544
545    /**
546     * @param {!number} windowLeft
547     * @param {!number} windowRight
548     */
549    changeWindow: function(windowLeft, windowRight)
550    {
551        this._windowLeft = windowLeft;
552        this._windowRight = windowRight;
553        this._windowWidth = this._windowRight - this._windowLeft;
554
555        this._scheduleUpdate();
556    },
557
558    /**
559     * @param {!MouseEvent} event
560     */
561    _startCanvasDragging: function(event)
562    {
563        if (!this._timelineData())
564            return false;
565        this._isDragging = true;
566        this._wasDragged = false;
567        this._dragStartPoint = event.pageX;
568        this._dragStartWindowLeft = this._windowLeft;
569        this._dragStartWindowRight = this._windowRight;
570
571        return true;
572    },
573
574    /**
575     * @param {!MouseEvent} event
576     */
577    _canvasDragging: function(event)
578    {
579        var pixelShift = this._dragStartPoint - event.pageX;
580        var windowShift = pixelShift / this._totalPixels;
581
582        var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift);
583        if (windowLeft === this._windowLeft)
584            return;
585        windowShift = windowLeft - this._dragStartWindowLeft;
586
587        var windowRight = Math.min(1, this._dragStartWindowRight + windowShift);
588        if (windowRight === this._windowRight)
589            return;
590        windowShift = windowRight - this._dragStartWindowRight;
591        this._overviewPane.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift);
592        this._wasDragged = true;
593    },
594
595    _endCanvasDragging: function()
596    {
597        this._isDragging = false;
598    },
599
600    /**
601     * @param {?MouseEvent} event
602     */
603    _onMouseMove: function(event)
604    {
605        if (this._isDragging)
606            return;
607
608        var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);
609
610        if (this._highlightedEntryIndex === entryIndex)
611            return;
612
613        if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
614            this._canvas.style.cursor = "default";
615        else
616            this._canvas.style.cursor = "pointer";
617
618        this._highlightedEntryIndex = entryIndex;
619        this._scheduleUpdate();
620    },
621
622    _onClick: function()
623    {
624        // onClick comes after dragStart and dragEnd events.
625        // So if there was drag (mouse move) in the middle of that events
626        // we skip the click. Otherwise we jump to the sources.
627        if (this._wasDragged)
628            return;
629        if (this._highlightedEntryIndex === -1)
630            return;
631        var data = this._dataProvider.entryData(this._highlightedEntryIndex);
632        this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, data);
633    },
634
635    /**
636     * @param {?MouseEvent} e
637     */
638    _onMouseWheel: function(e)
639    {
640        if (e.wheelDeltaY) {
641            const zoomFactor = 1.1;
642            const mouseWheelZoomSpeed = 1 / 120;
643
644            var zoom = Math.pow(zoomFactor, -e.wheelDeltaY * mouseWheelZoomSpeed);
645            var referencePoint = (this._pixelWindowLeft + e.offsetX - this._paddingLeft) / this._totalPixels;
646            this._overviewPane.zoom(zoom, referencePoint);
647        } else {
648            var shift = Number.constrain(-1 * this._windowWidth / 4 * e.wheelDeltaX / 120, -this._windowLeft, 1 - this._windowRight);
649            this._overviewPane.setWindow(this._windowLeft + shift, this._windowRight + shift);
650        }
651    },
652
653    /**
654     * @param {!number} x
655     * @param {!number} y
656     */
657    _coordinatesToEntryIndex: function(x, y)
658    {
659        var timelineData = this._timelineData();
660        if (!timelineData)
661            return -1;
662        var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime;
663        var cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
664
665        var entryOffsets = timelineData.entryOffsets;
666        var entryTotalTimes = timelineData.entryTotalTimes;
667        var entryLevels = timelineData.entryLevels;
668        var length = entryOffsets.length;
669        for (var i = 0; i < length; ++i) {
670            if (cursorTime < entryOffsets[i])
671                return -1;
672            if (cursorTime < (entryOffsets[i] + entryTotalTimes[i])
673                && cursorLevel === entryLevels[i])
674                return i;
675        }
676        return -1;
677    },
678
679    /**
680     * @param {!number} height
681     * @param {!number} width
682     */
683    draw: function(width, height)
684    {
685        var timelineData = this._timelineData();
686        if (!timelineData)
687            return;
688
689        var ratio = window.devicePixelRatio;
690        this._canvas.width = width * ratio;
691        this._canvas.height = height * ratio;
692        this._canvas.style.width = width + "px";
693        this._canvas.style.height = height + "px";
694
695        var context = this._canvas.getContext("2d");
696        context.scale(ratio, ratio);
697        var timeWindowRight = this._timeWindowRight;
698        var timeToPixel = this._timeToPixel;
699        var pixelWindowLeft = this._pixelWindowLeft;
700        var paddingLeft = this._paddingLeft;
701        var minWidth = this._minWidth;
702        var entryTotalTimes = timelineData.entryTotalTimes;
703        var entryOffsets = timelineData.entryOffsets;
704        var entryLevels = timelineData.entryLevels;
705        var colorEntryIndexes = timelineData.colorEntryIndexes;
706        var entryTitles = timelineData.entryTitles;
707        var entryDeoptFlags = timelineData.entryDeoptFlags;
708
709        var colorGenerator = WebInspector.FlameChart._colorGenerator;
710        var titleIndexes = new Uint32Array(timelineData.entryTotalTimes);
711        var lastTitleIndex = 0;
712        var dotsWidth = context.measureText("\u2026").width;
713        var textPaddingLeft = 2;
714        this._minTextWidth = context.measureText("\u2026").width + textPaddingLeft;
715        var minTextWidth = this._minTextWidth;
716
717        var marksField = [];
718        for (var i = 0; i < timelineData.maxStackDepth; ++i)
719            marksField.push(new Uint16Array(width));
720
721        var barHeight = this._barHeight;
722        var barX = 0;
723        var barWidth = 0;
724        var barRight = 0;
725        var barLevel = 0;
726        var bHeight = height - barHeight;
727        context.strokeStyle = "black";
728        var colorPair;
729        var entryIndex = 0;
730        var entryOffset = 0;
731        for (var colorIndex = 0; colorIndex < colorEntryIndexes.length; ++colorIndex) {
732            colorPair = colorGenerator._colorPairForIndex(colorIndex);
733            context.fillStyle = colorPair.normal;
734            var indexes = colorEntryIndexes[colorIndex];
735            if (!indexes)
736                continue;
737            context.beginPath();
738            for (var i = 0; i < indexes.length; ++i) {
739                entryIndex = indexes[i];
740                entryOffset = entryOffsets[entryIndex];
741                if (entryOffset > timeWindowRight)
742                    break;
743                barX = Math.ceil(entryOffset * timeToPixel) - pixelWindowLeft + paddingLeft;
744                if (barX >= width)
745                    continue;
746                barRight = Math.floor((entryOffset + entryTotalTimes[entryIndex]) * timeToPixel) - pixelWindowLeft + paddingLeft;
747                if (barRight < 0)
748                    continue;
749                barWidth = (barRight - barX) || minWidth;
750                barLevel = entryLevels[entryIndex];
751                var marksRow = marksField[barLevel];
752                if (barWidth <= marksRow[barX])
753                    continue;
754                marksRow[barX] = barWidth;
755                if (entryIndex === this._highlightedEntryIndex) {
756                    context.fill();
757                    context.beginPath();
758                    context.fillStyle = colorPair.highlighted;
759                }
760                context.rect(barX, bHeight - barLevel * barHeight, barWidth, barHeight);
761                if (entryIndex === this._highlightedEntryIndex) {
762                    context.fill();
763                    context.beginPath();
764                    context.fillStyle = colorPair.normal;
765                }
766                if (barWidth > minTextWidth)
767                    titleIndexes[lastTitleIndex++] = entryIndex;
768            }
769            context.fill();
770        }
771
772        var font = (barHeight - 4) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family");
773        var boldFont = "bold " + font;
774        var isBoldFontSelected = false;
775        context.font = font;
776        context.textBaseline = "alphabetic";
777        context.fillStyle = "#333";
778        this._dotsWidth = context.measureText("\u2026").width;
779
780        var textBaseHeight = bHeight + barHeight - 4;
781        for (var i = 0; i < lastTitleIndex; ++i) {
782            entryIndex = titleIndexes[i];
783            if (isBoldFontSelected) {
784                if (!entryDeoptFlags[entryIndex]) {
785                    context.font = font;
786                    isBoldFontSelected = false;
787                }
788            } else {
789                if (entryDeoptFlags[entryIndex]) {
790                    context.font = boldFont;
791                    isBoldFontSelected = true;
792                }
793            }
794
795            entryOffset = entryOffsets[entryIndex];
796            barX = Math.floor(entryOffset * timeToPixel) - pixelWindowLeft + paddingLeft;
797            barRight = Math.ceil((entryOffset + entryTotalTimes[entryIndex]) * timeToPixel) - pixelWindowLeft + paddingLeft;
798            barWidth = (barRight - barX) || minWidth;
799            var xText = Math.max(0, barX);
800            var widthText = barWidth - textPaddingLeft + barX - xText;
801            var title = this._prepareText(context, entryTitles[entryIndex], widthText);
802            if (title)
803                context.fillText(title, xText + textPaddingLeft, textBaseHeight - entryLevels[entryIndex] * barHeight);
804        }
805
806        this._entryInfo.removeChildren();
807        if (!this._isDragging) {
808            var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex);
809            if (entryInfo)
810                this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
811        }
812    },
813
814    _buildEntryInfo: function(entryInfo)
815    {
816        var infoTable = document.createElement("table");
817        infoTable.className = "info-table";
818        for (var i = 0; i < entryInfo.length; ++i) {
819            var row = infoTable.createChild("tr");
820            var titleCell = row.createChild("td");
821            titleCell.textContent = entryInfo[i].title;
822            titleCell.className = "title";
823            var textCell = row.createChild("td");
824            textCell.textContent = entryInfo[i].text;
825        }
826        return infoTable;
827    },
828
829    _prepareText: function(context, title, maxSize)
830    {
831        if (maxSize < this._dotsWidth)
832            return null;
833        var titleWidth = context.measureText(title).width;
834        if (maxSize > titleWidth)
835            return title;
836        maxSize -= this._dotsWidth;
837        var dotRegExp=/[\.\$]/g;
838        var match = dotRegExp.exec(title);
839        if (!match) {
840            var visiblePartSize = maxSize / titleWidth;
841            var newTextLength = Math.floor(title.length * visiblePartSize) + 1;
842            var minTextLength = 4;
843            if (newTextLength < minTextLength)
844                return null;
845            var substring;
846            do {
847                --newTextLength;
848                substring = title.substring(0, newTextLength);
849            } while (context.measureText(substring).width > maxSize);
850            return title.substring(0, newTextLength) + "\u2026";
851        }
852        while (match) {
853            var substring = title.substring(match.index + 1);
854            var width = context.measureText(substring).width;
855            if (maxSize > width)
856                return "\u2026" + substring;
857            match = dotRegExp.exec(title);
858        }
859        var i = 0;
860        do {
861            ++i;
862        } while (context.measureText(title.substring(0, i)).width < maxSize);
863        return title.substring(0, i - 1) + "\u2026";
864    },
865
866    _updateBoundaries: function()
867    {
868        this._totalTime = this._timelineData().totalTime;
869        this._timeWindowLeft = this._windowLeft * this._totalTime;
870        this._timeWindowRight = this._windowRight * this._totalTime;
871
872        this._pixelWindowWidth = this._chartContainer.clientWidth - this._paddingLeft;
873        this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
874        this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
875        this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
876
877        this._timeToPixel = this._totalPixels / this._totalTime;
878        this._pixelToTime = this._totalTime / this._totalPixels;
879        this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
880    },
881
882    onResize: function()
883    {
884        this._scheduleUpdate();
885    },
886
887    _scheduleUpdate: function()
888    {
889        if (this._updateTimerId)
890            return;
891        this._updateTimerId = setTimeout(this.update.bind(this), 10);
892    },
893
894    update: function()
895    {
896        this._updateTimerId = 0;
897        if (!this._timelineData())
898            return;
899        this._updateBoundaries();
900        this.draw(this._chartContainer.clientWidth, this._chartContainer.clientHeight);
901        this._calculator._updateBoundaries(this);
902        this._timelineGrid.element.style.width = this.element.clientWidth;
903        this._timelineGrid.updateDividers(this._calculator);
904    },
905
906    __proto__: WebInspector.View.prototype
907}
908