2 * Copyright (C) 2013 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
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.
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.
34 WebInspector.FlameChartDelegate = function() { }
36 WebInspector.FlameChartDelegate.prototype = {
38 * @param {number} startTime
39 * @param {number} endTime
41 requestWindowTimes: function(startTime, endTime) { }
46 * @extends {WebInspector.HBox}
47 * @param {!WebInspector.FlameChartDataProvider} dataProvider
48 * @param {!WebInspector.FlameChartDelegate} flameChartDelegate
49 * @param {boolean} isTopDown
51 WebInspector.FlameChart = function(dataProvider, flameChartDelegate, isTopDown)
53 WebInspector.HBox.call(this);
54 this.element.classList.add("flame-chart-main-pane");
55 this._flameChartDelegate = flameChartDelegate;
56 this._isTopDown = isTopDown;
58 this._calculator = new WebInspector.FlameChart.Calculator();
60 this._canvas = this.element.createChild("canvas");
61 this._canvas.tabIndex = 1;
62 this.setDefaultFocusedElement(this._canvas);
63 this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), false);
64 this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
65 this._canvas.addEventListener("click", this._onClick.bind(this), false);
66 this._canvas.addEventListener("keydown", this._onKeyDown.bind(this), false);
67 WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "move", null);
69 this._vScrollElement = this.element.createChild("div", "flame-chart-v-scroll");
70 this._vScrollContent = this._vScrollElement.createChild("div");
71 this._vScrollElement.addEventListener("scroll", this.scheduleUpdate.bind(this), false);
73 this._entryInfo = this.element.createChild("div", "profile-entry-info");
74 this._markerHighlighElement = this.element.createChild("div", "flame-chart-marker-highlight-element");
75 this._highlightElement = this.element.createChild("div", "flame-chart-highlight-element");
76 this._selectedElement = this.element.createChild("div", "flame-chart-selected-element");
78 this._dataProvider = dataProvider;
80 this._windowLeft = 0.0;
81 this._windowRight = 1.0;
82 this._windowWidth = 1.0;
83 this._timeWindowLeft = 0;
84 this._timeWindowRight = Infinity;
85 this._barHeight = dataProvider.barHeight();
86 this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight;
88 this._paddingLeft = this._dataProvider.paddingLeft();
89 this._markerPadding = 2;
90 this._markerRadius = this._barHeight / 2 - this._markerPadding;
91 this._highlightedMarkerIndex = -1;
92 this._highlightedEntryIndex = -1;
93 this._selectedEntryIndex = -1;
97 WebInspector.FlameChart.DividersBarHeight = 20;
102 WebInspector.FlameChartDataProvider = function()
108 * @param {!Array.<number>|!Uint8Array} entryLevels
109 * @param {!Array.<number>|!Float32Array} entryTotalTimes
110 * @param {!Array.<number>|!Float64Array} entryStartTimes
112 WebInspector.FlameChart.TimelineData = function(entryLevels, entryTotalTimes, entryStartTimes)
114 this.entryLevels = entryLevels;
115 this.entryTotalTimes = entryTotalTimes;
116 this.entryStartTimes = entryStartTimes;
117 /** @type {!Array.<number>} */
118 this.markerTimestamps = [];
121 WebInspector.FlameChartDataProvider.prototype = {
125 barHeight: function() { },
128 * @param {number} startTime
129 * @param {number} endTime
130 * @return {?Array.<number>}
132 dividerOffsets: function(startTime, endTime) { },
135 * @param {number} index
138 markerColor: function(index) { },
141 * @param {number} index
144 markerTitle: function(index) { },
149 minimumBoundary: function() { },
154 totalTime: function() { },
159 maxStackDepth: function() { },
162 * @return {?WebInspector.FlameChart.TimelineData}
164 timelineData: function() { },
167 * @param {number} entryIndex
168 * @return {?Array.<!{title: string, text: string}>}
170 prepareHighlightedEntryInfo: function(entryIndex) { },
173 * @param {number} entryIndex
176 canJumpToEntry: function(entryIndex) { },
179 * @param {number} entryIndex
182 entryTitle: function(entryIndex) { },
185 * @param {number} entryIndex
188 entryFont: function(entryIndex) { },
191 * @param {number} entryIndex
194 entryColor: function(entryIndex) { },
197 * @param {number} entryIndex
198 * @param {!CanvasRenderingContext2D} context
199 * @param {?string} text
200 * @param {number} barX
201 * @param {number} barY
202 * @param {number} barWidth
203 * @param {number} barHeight
204 * @param {function(number):number} timeToPosition
207 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) { },
210 * @param {number} entryIndex
213 forceDecoration: function(entryIndex) { },
216 * @param {number} entryIndex
219 textColor: function(entryIndex) { },
224 textBaseline: function() { },
229 textPadding: function() { },
232 * @return {?{startTime: number, endTime: number}}
234 highlightTimeRange: function(entryIndex) { },
239 paddingLeft: function() { },
242 WebInspector.FlameChart.Events = {
243 EntrySelected: "EntrySelected"
249 * @param {!{min: number, max: number, count: number}|number=} hueSpace
250 * @param {!{min: number, max: number, count: number}|number=} satSpace
251 * @param {!{min: number, max: number, count: number}|number=} lightnessSpace
252 * @param {!{min: number, max: number, count: number}|number=} alphaSpace
254 WebInspector.FlameChart.ColorGenerator = function(hueSpace, satSpace, lightnessSpace, alphaSpace)
256 this._hueSpace = hueSpace || { min: 0, max: 360, count: 20 };
257 this._satSpace = satSpace || 67;
258 this._lightnessSpace = lightnessSpace || 80;
259 this._alphaSpace = alphaSpace || 1;
263 WebInspector.FlameChart.ColorGenerator.prototype = {
266 * @param {string|!CanvasGradient} color
268 setColorForID: function(id, color)
270 this._colors[id] = color;
277 colorForID: function(id)
279 var color = this._colors[id];
281 color = this._generateColorForID(id);
282 this._colors[id] = color;
291 _generateColorForID: function(id)
293 var hash = id.hashCode();
294 var h = this._indexToValueInSpace(hash, this._hueSpace);
295 var s = this._indexToValueInSpace(hash, this._satSpace);
296 var l = this._indexToValueInSpace(hash, this._lightnessSpace);
297 var a = this._indexToValueInSpace(hash, this._alphaSpace);
298 return "hsla(" + h + ", " + s + "%, " + l + "%, " + a + ")";
302 * @param {number} index
303 * @param {!{min: number, max: number, count: number}|number} space
306 _indexToValueInSpace: function(index, space)
308 if (typeof space === "number")
310 index %= space.count;
311 return space.min + Math.floor(index / space.count * (space.max - space.min));
318 * @implements {WebInspector.TimelineGrid.Calculator}
320 WebInspector.FlameChart.Calculator = function()
322 this._paddingLeft = 0;
325 WebInspector.FlameChart.Calculator.prototype = {
329 paddingLeft: function()
331 return this._paddingLeft;
335 * @param {!WebInspector.FlameChart} mainPane
337 _updateBoundaries: function(mainPane)
339 this._totalTime = mainPane._dataProvider.totalTime();
340 this._zeroTime = mainPane._dataProvider.minimumBoundary();
341 this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime;
342 this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime;
343 this._paddingLeft = mainPane._paddingLeft;
344 this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft;
345 this._timeToPixel = this._width / this.boundarySpan();
349 * @param {number} time
352 computePosition: function(time)
354 return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft);
358 * @param {number} value
359 * @param {number=} precision
362 formatTime: function(value, precision)
364 return Number.preciseMillisToString(value - this._zeroTime, precision);
370 maximumBoundary: function()
372 return this._maximumBoundaries;
378 minimumBoundary: function()
380 return this._minimumBoundaries;
388 return this._zeroTime;
394 boundarySpan: function()
396 return this._maximumBoundaries - this._minimumBoundaries;
400 WebInspector.FlameChart.prototype = {
401 _resetCanvas: function()
403 var ratio = window.devicePixelRatio;
404 this._canvas.width = this._offsetWidth * ratio;
405 this._canvas.height = this._offsetHeight * ratio;
406 this._canvas.style.width = this._offsetWidth + "px";
407 this._canvas.style.height = this._offsetHeight + "px";
411 * @return {?WebInspector.FlameChart.TimelineData}
413 _timelineData: function()
415 var timelineData = this._dataProvider.timelineData();
416 if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength)
417 this._processTimelineData(timelineData);
418 return this._rawTimelineData;
421 _cancelAnimation: function()
423 if (this._cancelWindowTimesAnimation) {
424 this._timeWindowLeft = this._pendingAnimationTimeLeft;
425 this._timeWindowRight = this._pendingAnimationTimeRight;
426 this._cancelWindowTimesAnimation();
427 delete this._cancelWindowTimesAnimation;
432 * @param {number} startTime
433 * @param {number} endTime
435 setWindowTimes: function(startTime, endTime)
437 if (this._muteAnimation || this._timeWindowLeft === 0 || this._timeWindowRight === Infinity) {
439 this._timeWindowLeft = startTime;
440 this._timeWindowRight = endTime;
441 this.scheduleUpdate();
445 this._cancelAnimation();
446 this._cancelWindowTimesAnimation = WebInspector.animateFunction(this._animateWindowTimes.bind(this),
447 [{from: this._timeWindowLeft, to: startTime}, {from: this._timeWindowRight, to: endTime}], 5,
448 this._animationCompleted.bind(this));
449 this._pendingAnimationTimeLeft = startTime;
450 this._pendingAnimationTimeRight = endTime;
454 * @param {number} startTime
455 * @param {number} endTime
457 _animateWindowTimes: function(startTime, endTime)
459 this._timeWindowLeft = startTime;
460 this._timeWindowRight = endTime;
464 _animationCompleted: function()
466 delete this._cancelWindowTimesAnimation;
470 * @param {!MouseEvent} event
472 _startCanvasDragging: function(event)
474 if (!this._timelineData() || this._timeWindowRight === Infinity)
476 this._isDragging = true;
477 this._maxDragOffset = 0;
478 this._dragStartPointX = event.pageX;
479 this._dragStartPointY = event.pageY;
480 this._dragStartScrollTop = this._vScrollElement.scrollTop;
481 this._dragStartWindowLeft = this._timeWindowLeft;
482 this._dragStartWindowRight = this._timeWindowRight;
483 this._canvas.style.cursor = "";
489 * @param {!MouseEvent} event
491 _canvasDragging: function(event)
493 var pixelShift = this._dragStartPointX - event.pageX;
494 this._dragStartPointX = event.pageX;
495 this._muteAnimation = true;
496 this._handlePanGesture(pixelShift * this._pixelToTime);
497 this._muteAnimation = false;
499 var pixelScroll = this._dragStartPointY - event.pageY;
500 this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
501 this._maxDragOffset = Math.max(this._maxDragOffset, Math.abs(pixelShift));
504 _endCanvasDragging: function()
506 this._isDragging = false;
510 * @param {!Event} event
512 _onMouseMove: function(event)
514 this._lastMouseOffsetX = event.offsetX;
516 if (this._isDragging)
519 var inDividersBar = event.offsetY < WebInspector.FlameChart.DividersBarHeight;
520 this._highlightedMarkerIndex = inDividersBar ? this._markerIndexAtPosition(event.offsetX) : -1;
521 this._updateMarkerHighlight();
525 var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);
527 if (this._highlightedEntryIndex === entryIndex)
530 if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
531 this._canvas.style.cursor = "default";
533 this._canvas.style.cursor = "pointer";
535 this._highlightedEntryIndex = entryIndex;
537 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
538 this._entryInfo.removeChildren();
540 if (this._highlightedEntryIndex === -1)
543 if (!this._isDragging) {
544 var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex);
546 this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
553 // onClick comes after dragStart and dragEnd events.
554 // So if there was drag (mouse move) in the middle of that events
555 // we skip the click. Otherwise we jump to the sources.
556 const clickThreshold = 5;
557 if (this._maxDragOffset > clickThreshold)
559 if (this._highlightedEntryIndex === -1)
561 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex);
567 _onMouseWheel: function(e)
569 // Pan vertically when shift down only.
570 var panVertically = e.shiftKey && (e.wheelDeltaY || Math.abs(e.wheelDeltaX) === 120);
571 var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey;
573 this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 * this._offsetHeight / 8;
574 } else if (panHorizontally) {
575 var shift = -e.wheelDeltaX * this._pixelToTime;
576 this._muteAnimation = true;
577 this._handlePanGesture(shift);
578 this._muteAnimation = false;
580 const mouseWheelZoomSpeed = 1 / 120;
581 this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1);
584 // Block swipe gesture.
591 _onKeyDown: function(e)
593 if (e.altKey || e.ctrlKey || e.metaKey)
595 var zoomMultiplier = e.shiftKey ? 0.8 : 0.3;
596 var panMultiplier = e.shiftKey ? 320 : 80;
597 if (e.keyCode === "A".charCodeAt(0)) {
598 this._handlePanGesture(-panMultiplier * this._pixelToTime);
600 } else if (e.keyCode === "D".charCodeAt(0)) {
601 this._handlePanGesture(panMultiplier * this._pixelToTime);
603 } else if (e.keyCode === "W".charCodeAt(0)) {
604 this._handleZoomGesture(-zoomMultiplier);
606 } else if (e.keyCode === "S".charCodeAt(0)) {
607 this._handleZoomGesture(zoomMultiplier);
613 * @param {number} zoom
615 _handleZoomGesture: function(zoom)
617 this._cancelAnimation();
618 var bounds = this._windowForGesture();
619 var cursorTime = this._cursorTime(this._lastMouseOffsetX);
620 bounds.left += (bounds.left - cursorTime) * zoom;
621 bounds.right += (bounds.right - cursorTime) * zoom;
622 this._requestWindowTimes(bounds);
626 * @param {number} shift
628 _handlePanGesture: function(shift)
630 this._cancelAnimation();
631 var bounds = this._windowForGesture();
632 shift = Number.constrain(shift, this._minimumBoundary - bounds.left, this._totalTime + this._minimumBoundary - bounds.right);
633 bounds.left += shift;
634 bounds.right += shift;
635 this._requestWindowTimes(bounds);
639 * @return {{left: number, right: number}}
641 _windowForGesture: function()
643 var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
644 var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
645 return {left: windowLeft, right: windowRight};
649 * @param {{left: number, right: number}} bounds
651 _requestWindowTimes: function(bounds)
653 bounds.left = Number.constrain(bounds.left, this._minimumBoundary, this._totalTime + this._minimumBoundary);
654 bounds.right = Number.constrain(bounds.right, this._minimumBoundary, this._totalTime + this._minimumBoundary);
655 this._flameChartDelegate.requestWindowTimes(bounds.left, bounds.right);
662 _cursorTime: function(x)
664 return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._minimumBoundary;
672 _coordinatesToEntryIndex: function(x, y)
674 y += this._scrollTop;
675 var timelineData = this._timelineData();
678 var cursorTime = this._cursorTime(x);
681 if (this._isTopDown) {
682 cursorLevel = Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight);
683 offsetFromLevel = y - WebInspector.FlameChart.DividersBarHeight - cursorLevel * this._barHeight;
685 cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
686 offsetFromLevel = this._canvas.height / window.devicePixelRatio - cursorLevel * this._barHeight;
688 var entryStartTimes = timelineData.entryStartTimes;
689 var entryTotalTimes = timelineData.entryTotalTimes;
690 var entryIndexes = this._timelineLevels[cursorLevel];
691 if (!entryIndexes || !entryIndexes.length)
695 * @param {number} time
696 * @param {number} entryIndex
699 function comparator(time, entryIndex)
701 return time - entryStartTimes[entryIndex];
703 var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator) - 1, 0);
706 * @this {WebInspector.FlameChart}
707 * @param {number} entryIndex
710 function checkEntryHit(entryIndex)
712 if (entryIndex === undefined)
714 var startTime = entryStartTimes[entryIndex];
715 var duration = entryTotalTimes[entryIndex];
716 if (isNaN(duration)) {
717 var dx = (startTime - cursorTime) / this._pixelToTime;
718 var dy = this._barHeight / 2 - offsetFromLevel;
719 return dx * dx + dy * dy < this._markerRadius * this._markerRadius;
721 var endTime = startTime + duration;
722 var barThreshold = 3 * this._pixelToTime;
723 return startTime - barThreshold < cursorTime && cursorTime < endTime + barThreshold;
726 var entryIndex = entryIndexes[indexOnLevel];
727 if (checkEntryHit.call(this, entryIndex))
729 entryIndex = entryIndexes[indexOnLevel + 1];
730 if (checkEntryHit.call(this, entryIndex))
739 _markerIndexAtPosition: function(x)
741 var markers = this._timelineData().markerTimestamps;
744 var accurracyOffsetPx = 1;
745 var time = this._cursorTime(x);
746 var leftTime = this._cursorTime(x - accurracyOffsetPx);
747 var rightTime = this._cursorTime(x + accurracyOffsetPx);
750 * @param {number} time
751 * @param {number} markerTimestamp
754 function comparator(time, markerTimestamp)
756 return time - markerTimestamp;
758 var left = markers.lowerBound(leftTime, comparator);
759 var markerIndex = -1;
760 var distance = Infinity;
761 for (var i = left; i < markers.length && markers[i] < rightTime; i++) {
762 var nextDistance = Math.abs(markers[i] - time);
763 if (nextDistance < distance) {
765 distance = nextDistance;
772 * @param {number} height
773 * @param {number} width
775 _draw: function(width, height)
777 var timelineData = this._timelineData();
781 var context = this._canvas.getContext("2d");
783 var ratio = window.devicePixelRatio;
784 context.scale(ratio, ratio);
786 var timeWindowRight = this._timeWindowRight;
787 var timeWindowLeft = this._timeWindowLeft;
788 var timeToPixel = this._timeToPixel;
789 var pixelWindowLeft = this._pixelWindowLeft;
790 var paddingLeft = this._paddingLeft;
791 var minWidth = this._minWidth;
792 var entryTotalTimes = timelineData.entryTotalTimes;
793 var entryStartTimes = timelineData.entryStartTimes;
794 var entryLevels = timelineData.entryLevels;
796 var titleIndices = new Uint32Array(timelineData.entryTotalTimes);
797 var nextTitleIndex = 0;
798 var markerIndices = new Uint32Array(timelineData.entryTotalTimes);
799 var nextMarkerIndex = 0;
800 var textPadding = this._dataProvider.textPadding();
801 this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026");
802 var minTextWidth = this._minTextWidth;
804 var barHeight = this._barHeight;
806 var timeToPosition = this._timeToPosition.bind(this);
807 var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline();
808 var colorBuckets = {};
809 var minVisibleBarLevel = Math.max(Math.floor((this._scrollTop - this._baseHeight) / barHeight), 0);
810 var maxVisibleBarLevel = Math.min(Math.floor((this._scrollTop - this._baseHeight + height) / barHeight), this._dataProvider.maxStackDepth());
812 context.translate(0, -this._scrollTop);
814 function comparator(time, entryIndex)
816 return time - entryStartTimes[entryIndex];
819 for (var level = minVisibleBarLevel; level <= maxVisibleBarLevel; ++level) {
820 // Entries are ordered by start time within a level, so find the last visible entry.
821 var levelIndexes = this._timelineLevels[level];
822 var rightIndexOnLevel = levelIndexes.lowerBound(timeWindowRight, comparator) - 1;
823 var lastDrawOffset = Infinity;
824 for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
825 var entryIndex = levelIndexes[entryIndexOnLevel];
826 var entryStartTime = entryStartTimes[entryIndex];
827 var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]);
828 if (entryOffsetRight <= timeWindowLeft)
831 var barX = this._timeToPosition(entryStartTime);
832 if (barX >= lastDrawOffset)
834 var barRight = Math.min(this._timeToPosition(entryOffsetRight), lastDrawOffset);
835 lastDrawOffset = barX;
837 var color = this._dataProvider.entryColor(entryIndex);
838 var bucket = colorBuckets[color];
841 colorBuckets[color] = bucket;
843 bucket.push(entryIndex);
847 var colors = Object.keys(colorBuckets);
848 // We don't use for-in here because it couldn't be optimized.
849 for (var c = 0; c < colors.length; ++c) {
850 var color = colors[c];
851 context.fillStyle = color;
852 context.strokeStyle = color;
853 var indexes = colorBuckets[color];
855 // First fill the boxes.
857 for (var i = 0; i < indexes.length; ++i) {
858 var entryIndex = indexes[i];
859 var entryStartTime = entryStartTimes[entryIndex];
860 var barX = this._timeToPosition(entryStartTime);
861 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]);
862 var barWidth = Math.max(barRight - barX, minWidth);
863 var barLevel = entryLevels[entryIndex];
864 var barY = this._levelToHeight(barLevel);
865 if (isNaN(entryTotalTimes[entryIndex])) {
866 context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
867 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
868 markerIndices[nextMarkerIndex++] = entryIndex;
870 context.rect(barX, barY, barWidth, barHeight);
871 if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex))
872 titleIndices[nextTitleIndex++] = entryIndex;
878 context.strokeStyle = "rgb(0, 0, 0)";
880 for (var m = 0; m < nextMarkerIndex; ++m) {
881 var entryIndex = markerIndices[m];
882 var entryStartTime = entryStartTimes[entryIndex];
883 var barX = this._timeToPosition(entryStartTime);
884 var barLevel = entryLevels[entryIndex];
885 var barY = this._levelToHeight(barLevel);
886 context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
887 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
891 context.textBaseline = "alphabetic";
893 for (var i = 0; i < nextTitleIndex; ++i) {
894 var entryIndex = titleIndices[i];
895 var entryStartTime = entryStartTimes[entryIndex];
896 var barX = this._timeToPosition(entryStartTime);
897 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]);
898 var barWidth = Math.max(barRight - barX, minWidth);
899 var barLevel = entryLevels[entryIndex];
900 var barY = this._levelToHeight(barLevel);
901 var text = this._dataProvider.entryTitle(entryIndex);
902 if (text && text.length) {
903 context.font = this._dataProvider.entryFont(entryIndex);
904 text = this._prepareText(context, text, barWidth - 2 * textPadding);
907 if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition))
909 if (!text || !text.length)
912 context.fillStyle = this._dataProvider.textColor(entryIndex);
913 context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta);
917 var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary());
918 WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets);
921 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
922 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
923 this._updateMarkerHighlight();
926 _drawMarkers: function()
928 var markerTimestamps = this._timelineData().markerTimestamps;
930 * @param {number} time
931 * @param {number} markerTimestamp
934 function compare(time, markerTimestamp)
936 return time - markerTimestamp;
938 var left = markerTimestamps.lowerBound(this._calculator.minimumBoundary(), compare);
939 var rightBoundary = this._calculator.maximumBoundary();
941 var context = this._canvas.getContext("2d");
943 var ratio = window.devicePixelRatio;
944 context.scale(ratio, ratio);
945 var height = WebInspector.FlameChart.DividersBarHeight - 1;
946 context.lineWidth = 2;
947 for (var i = left; i < markerTimestamps.length; i++) {
948 var timestamp = markerTimestamps[i];
949 if (timestamp > rightBoundary)
951 var position = this._calculator.computePosition(timestamp);
952 context.strokeStyle = this._dataProvider.markerColor(i);
954 context.moveTo(position, 0);
955 context.lineTo(position, height);
961 _updateMarkerHighlight: function()
963 var element = this._markerHighlighElement;
964 if (element.parentElement)
966 var markerIndex = this._highlightedMarkerIndex;
967 if (markerIndex === -1)
969 var barX = this._timeToPosition(this._timelineData().markerTimestamps[markerIndex]);
970 element.title = this._dataProvider.markerTitle(markerIndex);
971 var style = element.style;
972 style.left = barX + "px";
973 style.backgroundColor = this._dataProvider.markerColor(markerIndex);
974 this.element.appendChild(element);
978 * @param {?WebInspector.FlameChart.TimelineData} timelineData
980 _processTimelineData: function(timelineData)
983 this._timelineLevels = null;
984 this._rawTimelineData = null;
985 this._rawTimelineDataLength = 0;
989 var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1);
990 for (var i = 0; i < timelineData.entryLevels.length; ++i)
991 ++entryCounters[timelineData.entryLevels[i]];
992 var levelIndexes = new Array(entryCounters.length);
993 for (var i = 0; i < levelIndexes.length; ++i) {
994 levelIndexes[i] = new Uint32Array(entryCounters[i]);
995 entryCounters[i] = 0;
997 for (var i = 0; i < timelineData.entryLevels.length; ++i) {
998 var level = timelineData.entryLevels[i];
999 levelIndexes[level][entryCounters[level]++] = i;
1001 this._timelineLevels = levelIndexes;
1002 this._rawTimelineData = timelineData;
1003 this._rawTimelineDataLength = timelineData.entryStartTimes.length;
1007 * @param {number} entryIndex
1009 setSelectedEntry: function(entryIndex)
1011 this._selectedEntryIndex = entryIndex;
1012 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
1015 _updateElementPosition: function(element, entryIndex)
1017 if (element.parentElement)
1019 if (entryIndex === -1)
1021 var timeRange = this._dataProvider.highlightTimeRange(entryIndex);
1024 var timelineData = this._timelineData();
1025 var barX = this._timeToPosition(timeRange.startTime);
1026 var barRight = this._timeToPosition(timeRange.endTime);
1027 if (barRight === 0 || barX === this._canvas.width)
1029 var barWidth = Math.max(barRight - barX, this._minWidth);
1030 var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop;
1031 var style = element.style;
1032 style.left = barX + "px";
1033 style.top = barY + "px";
1034 style.width = barWidth + "px";
1035 style.height = this._barHeight + "px";
1036 this.element.appendChild(element);
1040 * @param {number} time
1042 _timeToPosition: function(time)
1044 var value = Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
1045 return Math.min(this._canvas.width, Math.max(0, value));
1048 _levelToHeight: function(level)
1050 return this._baseHeight - level * this._barHeightDelta;
1053 _buildEntryInfo: function(entryInfo)
1055 var infoTable = document.createElementWithClass("table", "info-table");
1056 for (var i = 0; i < entryInfo.length; ++i) {
1057 var row = infoTable.createChild("tr");
1058 row.createChild("td", "title").textContent = entryInfo[i].title;
1059 row.createChild("td").textContent = entryInfo[i].text;
1065 * @param {!CanvasRenderingContext2D} context
1066 * @param {string} title
1067 * @param {number} maxSize
1070 _prepareText: function(context, title, maxSize)
1072 var titleWidth = this._measureWidth(context, title);
1073 if (maxSize >= titleWidth)
1077 var r = title.length;
1079 var m = (l + r) >> 1;
1080 if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize)
1085 title = title.trimMiddle(r - 1);
1086 return title !== "\u2026" ? title : "";
1090 * @param {!CanvasRenderingContext2D} context
1091 * @param {string} text
1094 _measureWidth: function(context, text)
1096 if (text.length > 20)
1097 return context.measureText(text).width;
1099 var font = context.font;
1100 var textWidths = this._textWidth[font];
1103 this._textWidth[font] = textWidths;
1105 var width = textWidths[text];
1107 width = context.measureText(text).width;
1108 textWidths[text] = width;
1113 _updateBoundaries: function()
1115 this._totalTime = this._dataProvider.totalTime();
1116 this._minimumBoundary = this._dataProvider.minimumBoundary();
1118 if (this._timeWindowRight !== Infinity) {
1119 this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime;
1120 this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime;
1121 this._windowWidth = this._windowRight - this._windowLeft;
1123 this._windowLeft = 0;
1124 this._windowRight = 1;
1125 this._windowWidth = 1;
1128 this._pixelWindowWidth = this._offsetWidth - this._paddingLeft;
1129 this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
1130 this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
1131 this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
1133 this._timeToPixel = this._totalPixels / this._totalTime;
1134 this._pixelToTime = this._totalTime / this._totalPixels;
1135 this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
1137 this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight;
1139 this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth() + 1);
1140 this._vScrollContent.style.height = this._totalHeight + "px";
1141 this._scrollTop = this._vScrollElement.scrollTop;
1142 this._updateScrollBar();
1145 onResize: function()
1147 this._updateScrollBar();
1148 this.scheduleUpdate();
1151 _updateScrollBar: function()
1153 var showScroll = this._totalHeight > this._offsetHeight;
1154 this._vScrollElement.classList.toggle("hidden", !showScroll);
1155 this._offsetWidth = this.element.offsetWidth - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth);
1156 this._offsetHeight = this.element.offsetHeight;
1159 scheduleUpdate: function()
1161 if (this._updateTimerId || this._cancelWindowTimesAnimation)
1163 this._updateTimerId = requestAnimationFrame(this.update.bind(this));
1168 this._updateTimerId = 0;
1169 if (!this._timelineData())
1171 this._resetCanvas();
1172 this._updateBoundaries();
1173 this._calculator._updateBoundaries(this);
1174 this._draw(this._offsetWidth, this._offsetHeight);
1179 this._highlightedMarkerIndex = -1;
1180 this._highlightedEntryIndex = -1;
1181 this._selectedEntryIndex = -1;
1182 this._textWidth = {};
1186 __proto__: WebInspector.HBox.prototype