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) { },
44 * @param {number} startTime
45 * @param {number} endTime
47 updateBoxSelection: function(startTime, endTime) { }
52 * @extends {WebInspector.HBox}
53 * @param {!WebInspector.FlameChartDataProvider} dataProvider
54 * @param {!WebInspector.FlameChartDelegate} flameChartDelegate
55 * @param {boolean} isTopDown
57 WebInspector.FlameChart = function(dataProvider, flameChartDelegate, isTopDown)
59 WebInspector.HBox.call(this, true);
60 this.contentElement.appendChild(WebInspector.View.createStyleElement("components/flameChart.css"));
61 this.contentElement.classList.add("flame-chart-main-pane");
62 this._flameChartDelegate = flameChartDelegate;
63 this._isTopDown = isTopDown;
65 this._calculator = new WebInspector.FlameChart.Calculator();
67 this._canvas = this.contentElement.createChild("canvas");
68 this._canvas.tabIndex = 1;
69 this.setDefaultFocusedElement(this._canvas);
70 this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), false);
71 this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
72 this._canvas.addEventListener("click", this._onClick.bind(this), false);
73 this._canvas.addEventListener("keydown", this._onKeyDown.bind(this), false);
74 WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "move", null);
76 this._vScrollElement = this.contentElement.createChild("div", "flame-chart-v-scroll");
77 this._vScrollContent = this._vScrollElement.createChild("div");
78 this._vScrollElement.addEventListener("scroll", this.scheduleUpdate.bind(this), false);
80 this._entryInfo = this.contentElement.createChild("div", "flame-chart-entry-info");
81 this._markerHighlighElement = this.contentElement.createChild("div", "flame-chart-marker-highlight-element");
82 this._highlightElement = this.contentElement.createChild("div", "flame-chart-highlight-element");
83 this._selectedElement = this.contentElement.createChild("div", "flame-chart-selected-element");
84 this._selectionOverlay = this.contentElement.createChild("div", "flame-chart-selection-overlay hidden");
85 this._selectedTimeSpanLabel = this._selectionOverlay.createChild("div", "time-span");
87 this._dataProvider = dataProvider;
89 this._windowLeft = 0.0;
90 this._windowRight = 1.0;
91 this._windowWidth = 1.0;
92 this._timeWindowLeft = 0;
93 this._timeWindowRight = Infinity;
94 this._barHeight = dataProvider.barHeight();
95 this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight;
97 this._paddingLeft = this._dataProvider.paddingLeft();
98 this._markerPadding = 2;
99 this._markerRadius = this._barHeight / 2 - this._markerPadding;
100 this._highlightedMarkerIndex = -1;
101 this._highlightedEntryIndex = -1;
102 this._selectedEntryIndex = -1;
103 this._rawTimelineDataLength = 0;
104 this._textWidth = {};
107 WebInspector.FlameChart.DividersBarHeight = 20;
109 WebInspector.FlameChart.MinimalTimeWindowMs = 0.01;
114 WebInspector.FlameChartDataProvider = function()
120 * @param {!Array.<number>|!Uint8Array} entryLevels
121 * @param {!Array.<number>|!Float32Array} entryTotalTimes
122 * @param {!Array.<number>|!Float64Array} entryStartTimes
124 WebInspector.FlameChart.TimelineData = function(entryLevels, entryTotalTimes, entryStartTimes)
126 this.entryLevels = entryLevels;
127 this.entryTotalTimes = entryTotalTimes;
128 this.entryStartTimes = entryStartTimes;
129 /** @type {!Array.<number>} */
130 this.markerTimestamps = [];
133 WebInspector.FlameChartDataProvider.prototype = {
137 barHeight: function() { },
140 * @param {number} startTime
141 * @param {number} endTime
142 * @return {?Array.<number>}
144 dividerOffsets: function(startTime, endTime) { },
147 * @param {number} index
150 markerColor: function(index) { },
153 * @param {number} index
156 markerTitle: function(index) { },
159 * @param {number} index
162 isTallMarker: function(index) { },
167 minimumBoundary: function() { },
172 totalTime: function() { },
177 maxStackDepth: function() { },
180 * @return {?WebInspector.FlameChart.TimelineData}
182 timelineData: function() { },
185 * @param {number} entryIndex
186 * @return {?Array.<!{title: string, text: string}>}
188 prepareHighlightedEntryInfo: function(entryIndex) { },
191 * @param {number} entryIndex
194 canJumpToEntry: function(entryIndex) { },
197 * @param {number} entryIndex
200 entryTitle: function(entryIndex) { },
203 * @param {number} entryIndex
206 entryFont: function(entryIndex) { },
209 * @param {number} entryIndex
212 entryColor: function(entryIndex) { },
215 * @param {number} entryIndex
216 * @param {!CanvasRenderingContext2D} context
217 * @param {?string} text
218 * @param {number} barX
219 * @param {number} barY
220 * @param {number} barWidth
221 * @param {number} barHeight
222 * @param {function(number):number} timeToPosition
225 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) { },
228 * @param {number} entryIndex
231 forceDecoration: function(entryIndex) { },
234 * @param {number} entryIndex
237 textColor: function(entryIndex) { },
242 textBaseline: function() { },
247 textPadding: function() { },
250 * @return {?{startTime: number, endTime: number}}
252 highlightTimeRange: function(entryIndex) { },
257 paddingLeft: function() { },
260 WebInspector.FlameChart.Events = {
261 EntrySelected: "EntrySelected"
267 * @param {!{min: number, max: number, count: number}|number=} hueSpace
268 * @param {!{min: number, max: number, count: number}|number=} satSpace
269 * @param {!{min: number, max: number, count: number}|number=} lightnessSpace
270 * @param {!{min: number, max: number, count: number}|number=} alphaSpace
272 WebInspector.FlameChart.ColorGenerator = function(hueSpace, satSpace, lightnessSpace, alphaSpace)
274 this._hueSpace = hueSpace || { min: 0, max: 360, count: 20 };
275 this._satSpace = satSpace || 67;
276 this._lightnessSpace = lightnessSpace || 80;
277 this._alphaSpace = alphaSpace || 1;
281 WebInspector.FlameChart.ColorGenerator.prototype = {
284 * @param {string|!CanvasGradient} color
286 setColorForID: function(id, color)
288 this._colors[id] = color;
295 colorForID: function(id)
297 var color = this._colors[id];
299 color = this._generateColorForID(id);
300 this._colors[id] = color;
309 _generateColorForID: function(id)
311 var hash = id.hashCode();
312 var h = this._indexToValueInSpace(hash, this._hueSpace);
313 var s = this._indexToValueInSpace(hash, this._satSpace);
314 var l = this._indexToValueInSpace(hash, this._lightnessSpace);
315 var a = this._indexToValueInSpace(hash, this._alphaSpace);
316 return "hsla(" + h + ", " + s + "%, " + l + "%, " + a + ")";
320 * @param {number} index
321 * @param {!{min: number, max: number, count: number}|number} space
324 _indexToValueInSpace: function(index, space)
326 if (typeof space === "number")
328 index %= space.count;
329 return space.min + Math.floor(index / space.count * (space.max - space.min));
336 * @implements {WebInspector.TimelineGrid.Calculator}
338 WebInspector.FlameChart.Calculator = function()
340 this._paddingLeft = 0;
343 WebInspector.FlameChart.Calculator.prototype = {
347 paddingLeft: function()
349 return this._paddingLeft;
353 * @param {!WebInspector.FlameChart} mainPane
355 _updateBoundaries: function(mainPane)
357 this._totalTime = mainPane._dataProvider.totalTime();
358 this._zeroTime = mainPane._dataProvider.minimumBoundary();
359 this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime;
360 this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime;
361 this._paddingLeft = mainPane._paddingLeft;
362 this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft;
363 this._timeToPixel = this._width / this.boundarySpan();
367 * @param {number} time
370 computePosition: function(time)
372 return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft);
376 * @param {number} value
377 * @param {number=} precision
380 formatTime: function(value, precision)
382 return Number.preciseMillisToString(value - this._zeroTime, precision);
388 maximumBoundary: function()
390 return this._maximumBoundaries;
396 minimumBoundary: function()
398 return this._minimumBoundaries;
406 return this._zeroTime;
412 boundarySpan: function()
414 return this._maximumBoundaries - this._minimumBoundaries;
418 WebInspector.FlameChart.prototype = {
419 _resetCanvas: function()
421 var ratio = window.devicePixelRatio;
422 this._canvas.width = this._offsetWidth * ratio;
423 this._canvas.height = this._offsetHeight * ratio;
424 this._canvas.style.width = this._offsetWidth + "px";
425 this._canvas.style.height = this._offsetHeight + "px";
429 * @return {?WebInspector.FlameChart.TimelineData}
431 _timelineData: function()
433 var timelineData = this._dataProvider.timelineData();
434 if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength)
435 this._processTimelineData(timelineData);
436 return this._rawTimelineData;
439 _cancelAnimation: function()
441 if (this._cancelWindowTimesAnimation) {
442 this._timeWindowLeft = this._pendingAnimationTimeLeft;
443 this._timeWindowRight = this._pendingAnimationTimeRight;
444 this._cancelWindowTimesAnimation();
445 delete this._cancelWindowTimesAnimation;
450 * @param {number} startTime
451 * @param {number} endTime
453 setWindowTimes: function(startTime, endTime)
455 if (this._muteAnimation || this._timeWindowLeft === 0 || this._timeWindowRight === Infinity || (startTime === 0 && endTime === Infinity)) {
457 this._timeWindowLeft = startTime;
458 this._timeWindowRight = endTime;
459 this.scheduleUpdate();
463 this._cancelAnimation();
464 this._cancelWindowTimesAnimation = WebInspector.animateFunction(this._animateWindowTimes.bind(this),
465 [{from: this._timeWindowLeft, to: startTime}, {from: this._timeWindowRight, to: endTime}], 5,
466 this._animationCompleted.bind(this));
467 this._pendingAnimationTimeLeft = startTime;
468 this._pendingAnimationTimeRight = endTime;
472 * @param {number} startTime
473 * @param {number} endTime
475 _animateWindowTimes: function(startTime, endTime)
477 this._timeWindowLeft = startTime;
478 this._timeWindowRight = endTime;
482 _animationCompleted: function()
484 delete this._cancelWindowTimesAnimation;
488 * @param {!MouseEvent} event
490 _startCanvasDragging: function(event)
492 if (event.shiftKey) {
493 this._startBoxSelection(event);
494 this._isDragging = true;
497 if (!this._timelineData() || this._timeWindowRight === Infinity)
499 this._isDragging = true;
500 this._maxDragOffset = 0;
501 this._dragStartPointX = event.pageX;
502 this._dragStartPointY = event.pageY;
503 this._dragStartScrollTop = this._vScrollElement.scrollTop;
504 this._dragStartWindowLeft = this._timeWindowLeft;
505 this._dragStartWindowRight = this._timeWindowRight;
506 this._canvas.style.cursor = "";
512 * @param {!MouseEvent} event
514 _canvasDragging: function(event)
516 if (this._isSelecting) {
517 this._updateBoxSelection(event);
520 var pixelShift = this._dragStartPointX - event.pageX;
521 this._dragStartPointX = event.pageX;
522 this._muteAnimation = true;
523 this._handlePanGesture(pixelShift * this._pixelToTime);
524 this._muteAnimation = false;
526 var pixelScroll = this._dragStartPointY - event.pageY;
527 this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
528 this._maxDragOffset = Math.max(this._maxDragOffset, Math.abs(pixelShift));
531 _endCanvasDragging: function()
533 this._hideBoxSelection();
534 this._isDragging = false;
538 * @param {!MouseEvent} event
540 _startBoxSelection: function(event)
542 this._selectionOffsetShiftX = event.offsetX - event.pageX;
543 this._selectionOffsetShiftY = event.offsetY - event.pageY;
544 this._selectionStartX = event.offsetX;
545 this._selectionStartY = event.offsetY;
546 this._isSelecting = true;
547 var style = this._selectionOverlay.style;
548 style.left = this._selectionStartX + "px";
549 style.top = this._selectionStartY + "px";
551 style.height = "1px";
552 this._selectedTimeSpanLabel.textContent = "";
553 this._selectionOverlay.classList.remove("hidden");
556 _hideBoxSelection: function()
558 this._selectionOffsetShiftX = null;
559 this._selectionOffsetShiftY = null;
560 this._selectionStartX = null;
561 this._selectionStartY = null;
562 this._isSelecting = false;
563 this._selectionOverlay.classList.add("hidden");
567 * @param {!MouseEvent} event
569 _updateBoxSelection: function(event)
571 var x = event.pageX + this._selectionOffsetShiftX;
572 var y = event.pageY + this._selectionOffsetShiftY;
573 x = Number.constrain(x, 0, this._offsetWidth);
574 y = Number.constrain(y, 0, this._offsetHeight);
575 var style = this._selectionOverlay.style;
576 style.left = Math.min(x, this._selectionStartX) + "px";
577 style.top = Math.min(y, this._selectionStartY) + "px";
578 var selectionWidth = Math.abs(x - this._selectionStartX)
579 style.width = selectionWidth + "px";
580 style.height = Math.abs(y - this._selectionStartY) + "px";
582 var timeSpan = selectionWidth * this._pixelToTime;
583 this._selectedTimeSpanLabel.textContent = Number.preciseMillisToString(timeSpan, 2);
584 var start = this._cursorTime(this._selectionStartX);
585 var end = this._cursorTime(x);
587 this._flameChartDelegate.updateBoxSelection(start, end);
589 this._flameChartDelegate.updateBoxSelection(end, start);
593 * @param {!Event} event
595 _onMouseMove: function(event)
597 this._lastMouseOffsetX = event.offsetX;
599 if (!this._enabled())
602 if (this._isDragging)
605 var inDividersBar = event.offsetY < WebInspector.FlameChart.DividersBarHeight;
606 this._highlightedMarkerIndex = inDividersBar ? this._markerIndexAtPosition(event.offsetX) : -1;
607 this._updateMarkerHighlight();
611 var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);
613 if (this._highlightedEntryIndex === entryIndex)
616 if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
617 this._canvas.style.cursor = "default";
619 this._canvas.style.cursor = "pointer";
621 this._highlightedEntryIndex = entryIndex;
623 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
624 this._entryInfo.removeChildren();
626 if (this._highlightedEntryIndex === -1)
629 if (!this._isDragging) {
630 var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex);
632 this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
639 // onClick comes after dragStart and dragEnd events.
640 // So if there was drag (mouse move) in the middle of that events
641 // we skip the click. Otherwise we jump to the sources.
642 const clickThreshold = 5;
643 if (this._maxDragOffset > clickThreshold)
645 if (this._highlightedEntryIndex === -1)
647 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex);
653 _onMouseWheel: function(e)
655 if (!this._enabled())
657 // Pan vertically when shift down only.
658 var panVertically = e.shiftKey && (e.wheelDeltaY || Math.abs(e.wheelDeltaX) === 120);
659 var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey;
661 this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 * this._offsetHeight / 8;
662 } else if (panHorizontally) {
663 var shift = -e.wheelDeltaX * this._pixelToTime;
664 this._muteAnimation = true;
665 this._handlePanGesture(shift);
666 this._muteAnimation = false;
668 const mouseWheelZoomSpeed = 1 / 120;
669 this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1);
672 // Block swipe gesture.
679 _onKeyDown: function(e)
681 if (e.altKey || e.ctrlKey || e.metaKey)
683 var zoomMultiplier = e.shiftKey ? 0.8 : 0.3;
684 var panMultiplier = e.shiftKey ? 320 : 80;
685 if (e.keyCode === "A".charCodeAt(0)) {
686 this._handlePanGesture(-panMultiplier * this._pixelToTime);
688 } else if (e.keyCode === "D".charCodeAt(0)) {
689 this._handlePanGesture(panMultiplier * this._pixelToTime);
691 } else if (e.keyCode === "W".charCodeAt(0)) {
692 this._handleZoomGesture(-zoomMultiplier);
694 } else if (e.keyCode === "S".charCodeAt(0)) {
695 this._handleZoomGesture(zoomMultiplier);
701 * @param {number} zoom
703 _handleZoomGesture: function(zoom)
705 this._cancelAnimation();
706 var bounds = this._windowForGesture();
707 var cursorTime = this._cursorTime(this._lastMouseOffsetX);
708 bounds.left += (bounds.left - cursorTime) * zoom;
709 bounds.right += (bounds.right - cursorTime) * zoom;
710 this._requestWindowTimes(bounds);
714 * @param {number} shift
716 _handlePanGesture: function(shift)
718 this._cancelAnimation();
719 var bounds = this._windowForGesture();
720 shift = Number.constrain(shift, this._minimumBoundary - bounds.left, this._totalTime + this._minimumBoundary - bounds.right);
721 bounds.left += shift;
722 bounds.right += shift;
723 this._requestWindowTimes(bounds);
727 * @return {{left: number, right: number}}
729 _windowForGesture: function()
731 var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
732 var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
733 return {left: windowLeft, right: windowRight};
737 * @param {{left: number, right: number}} bounds
739 _requestWindowTimes: function(bounds)
741 bounds.left = Number.constrain(bounds.left, this._minimumBoundary, this._totalTime + this._minimumBoundary);
742 bounds.right = Number.constrain(bounds.right, this._minimumBoundary, this._totalTime + this._minimumBoundary);
743 if (bounds.right - bounds.left < WebInspector.FlameChart.MinimalTimeWindowMs)
745 this._flameChartDelegate.requestWindowTimes(bounds.left, bounds.right);
752 _cursorTime: function(x)
754 return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._minimumBoundary;
762 _coordinatesToEntryIndex: function(x, y)
764 y += this._scrollTop;
765 var timelineData = this._timelineData();
768 var cursorTime = this._cursorTime(x);
771 if (this._isTopDown) {
772 cursorLevel = Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight);
773 offsetFromLevel = y - WebInspector.FlameChart.DividersBarHeight - cursorLevel * this._barHeight;
775 cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
776 offsetFromLevel = this._canvas.height / window.devicePixelRatio - cursorLevel * this._barHeight;
778 var entryStartTimes = timelineData.entryStartTimes;
779 var entryTotalTimes = timelineData.entryTotalTimes;
780 var entryIndexes = this._timelineLevels[cursorLevel];
781 if (!entryIndexes || !entryIndexes.length)
785 * @param {number} time
786 * @param {number} entryIndex
789 function comparator(time, entryIndex)
791 return time - entryStartTimes[entryIndex];
793 var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator) - 1, 0);
796 * @this {WebInspector.FlameChart}
797 * @param {number} entryIndex
800 function checkEntryHit(entryIndex)
802 if (entryIndex === undefined)
804 var startTime = entryStartTimes[entryIndex];
805 var duration = entryTotalTimes[entryIndex];
806 if (isNaN(duration)) {
807 var dx = (startTime - cursorTime) / this._pixelToTime;
808 var dy = this._barHeight / 2 - offsetFromLevel;
809 return dx * dx + dy * dy < this._markerRadius * this._markerRadius;
811 var endTime = startTime + duration;
812 var barThreshold = 3 * this._pixelToTime;
813 return startTime - barThreshold < cursorTime && cursorTime < endTime + barThreshold;
816 var entryIndex = entryIndexes[indexOnLevel];
817 if (checkEntryHit.call(this, entryIndex))
819 entryIndex = entryIndexes[indexOnLevel + 1];
820 if (checkEntryHit.call(this, entryIndex))
829 _markerIndexAtPosition: function(x)
831 var markers = this._timelineData().markerTimestamps;
834 var accurracyOffsetPx = 1;
835 var time = this._cursorTime(x);
836 var leftTime = this._cursorTime(x - accurracyOffsetPx);
837 var rightTime = this._cursorTime(x + accurracyOffsetPx);
840 * @param {number} time
841 * @param {number} markerTimestamp
844 function comparator(time, markerTimestamp)
846 return time - markerTimestamp;
848 var left = markers.lowerBound(leftTime, comparator);
849 var markerIndex = -1;
850 var distance = Infinity;
851 for (var i = left; i < markers.length && markers[i] < rightTime; i++) {
852 var nextDistance = Math.abs(markers[i] - time);
853 if (nextDistance < distance) {
855 distance = nextDistance;
862 * @param {number} height
863 * @param {number} width
865 _draw: function(width, height)
867 var timelineData = this._timelineData();
871 var context = this._canvas.getContext("2d");
873 var ratio = window.devicePixelRatio;
874 context.scale(ratio, ratio);
876 var timeWindowRight = this._timeWindowRight;
877 var timeWindowLeft = this._timeWindowLeft;
878 var timeToPixel = this._timeToPixel;
879 var pixelWindowLeft = this._pixelWindowLeft;
880 var paddingLeft = this._paddingLeft;
881 var minWidth = this._minWidth;
882 var entryTotalTimes = timelineData.entryTotalTimes;
883 var entryStartTimes = timelineData.entryStartTimes;
884 var entryLevels = timelineData.entryLevels;
886 var titleIndices = new Uint32Array(timelineData.entryTotalTimes);
887 var nextTitleIndex = 0;
888 var markerIndices = new Uint32Array(timelineData.entryTotalTimes);
889 var nextMarkerIndex = 0;
890 var textPadding = this._dataProvider.textPadding();
891 this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026");
892 var minTextWidth = this._minTextWidth;
894 var barHeight = this._barHeight;
896 var timeToPosition = this._timeToPosition.bind(this);
897 var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline();
898 var colorBuckets = {};
899 var minVisibleBarLevel = Math.max(Math.floor((this._scrollTop - this._baseHeight) / barHeight), 0);
900 var maxVisibleBarLevel = Math.min(Math.floor((this._scrollTop - this._baseHeight + height) / barHeight), this._dataProvider.maxStackDepth());
902 context.translate(0, -this._scrollTop);
904 function comparator(time, entryIndex)
906 return time - entryStartTimes[entryIndex];
909 for (var level = minVisibleBarLevel; level <= maxVisibleBarLevel; ++level) {
910 // Entries are ordered by start time within a level, so find the last visible entry.
911 var levelIndexes = this._timelineLevels[level];
912 var rightIndexOnLevel = levelIndexes.lowerBound(timeWindowRight, comparator) - 1;
913 var lastDrawOffset = Infinity;
914 for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
915 var entryIndex = levelIndexes[entryIndexOnLevel];
916 var entryStartTime = entryStartTimes[entryIndex];
917 var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]);
918 if (entryOffsetRight <= timeWindowLeft)
921 var barX = this._timeToPosition(entryStartTime);
922 if (barX >= lastDrawOffset)
924 var barRight = Math.min(this._timeToPosition(entryOffsetRight), lastDrawOffset);
925 lastDrawOffset = barX;
927 var color = this._dataProvider.entryColor(entryIndex);
928 var bucket = colorBuckets[color];
931 colorBuckets[color] = bucket;
933 bucket.push(entryIndex);
937 var colors = Object.keys(colorBuckets);
938 // We don't use for-in here because it couldn't be optimized.
939 for (var c = 0; c < colors.length; ++c) {
940 var color = colors[c];
941 context.fillStyle = color;
942 context.strokeStyle = color;
943 var indexes = colorBuckets[color];
945 // First fill the boxes.
947 for (var i = 0; i < indexes.length; ++i) {
948 var entryIndex = indexes[i];
949 var entryStartTime = entryStartTimes[entryIndex];
950 var barX = this._timeToPosition(entryStartTime);
951 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]);
952 var barWidth = Math.max(barRight - barX, minWidth);
953 var barLevel = entryLevels[entryIndex];
954 var barY = this._levelToHeight(barLevel);
955 if (isNaN(entryTotalTimes[entryIndex])) {
956 context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
957 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
958 markerIndices[nextMarkerIndex++] = entryIndex;
960 context.rect(barX, barY, barWidth, barHeight);
961 if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex))
962 titleIndices[nextTitleIndex++] = entryIndex;
968 context.strokeStyle = "rgb(0, 0, 0)";
970 for (var m = 0; m < nextMarkerIndex; ++m) {
971 var entryIndex = markerIndices[m];
972 var entryStartTime = entryStartTimes[entryIndex];
973 var barX = this._timeToPosition(entryStartTime);
974 var barLevel = entryLevels[entryIndex];
975 var barY = this._levelToHeight(barLevel);
976 context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
977 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
981 context.textBaseline = "alphabetic";
983 for (var i = 0; i < nextTitleIndex; ++i) {
984 var entryIndex = titleIndices[i];
985 var entryStartTime = entryStartTimes[entryIndex];
986 var barX = this._timeToPosition(entryStartTime);
987 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]);
988 var barWidth = Math.max(barRight - barX, minWidth);
989 var barLevel = entryLevels[entryIndex];
990 var barY = this._levelToHeight(barLevel);
991 var text = this._dataProvider.entryTitle(entryIndex);
992 if (text && text.length) {
993 context.font = this._dataProvider.entryFont(entryIndex);
994 text = this._prepareText(context, text, barWidth - 2 * textPadding);
997 if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition))
999 if (!text || !text.length)
1002 context.fillStyle = this._dataProvider.textColor(entryIndex);
1003 context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta);
1007 var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary());
1008 WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets);
1009 this._drawMarkers();
1011 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
1012 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
1013 this._updateMarkerHighlight();
1016 _drawMarkers: function()
1018 var markerTimestamps = this._timelineData().markerTimestamps;
1020 * @param {number} time
1021 * @param {number} markerTimestamp
1024 function compare(time, markerTimestamp)
1026 return time - markerTimestamp;
1028 var left = markerTimestamps.lowerBound(this._calculator.minimumBoundary(), compare);
1029 var rightBoundary = this._calculator.maximumBoundary();
1031 var context = this._canvas.getContext("2d");
1033 var ratio = window.devicePixelRatio;
1034 context.scale(ratio, ratio);
1035 var height = WebInspector.FlameChart.DividersBarHeight - 1;
1036 context.lineWidth = 2;
1037 for (var i = left; i < markerTimestamps.length; i++) {
1038 var timestamp = markerTimestamps[i];
1039 if (timestamp > rightBoundary)
1041 var position = this._calculator.computePosition(timestamp);
1042 context.strokeStyle = this._dataProvider.markerColor(i);
1043 context.beginPath();
1044 context.moveTo(position, 0);
1045 context.lineTo(position, height);
1047 if (this._dataProvider.isTallMarker(i)) {
1049 context.lineWidth = 0.5;
1050 context.translate(0.5, 0.5);
1051 context.beginPath();
1052 context.moveTo(position, height);
1053 context.setLineDash([10, 5]);
1054 context.lineTo(position, this._canvas.height);
1062 _updateMarkerHighlight: function()
1064 var element = this._markerHighlighElement;
1065 if (element.parentElement)
1067 var markerIndex = this._highlightedMarkerIndex;
1068 if (markerIndex === -1)
1070 var barX = this._timeToPosition(this._timelineData().markerTimestamps[markerIndex]);
1071 element.title = this._dataProvider.markerTitle(markerIndex);
1072 var style = element.style;
1073 style.left = barX + "px";
1074 style.backgroundColor = this._dataProvider.markerColor(markerIndex);
1075 this.contentElement.appendChild(element);
1079 * @param {?WebInspector.FlameChart.TimelineData} timelineData
1081 _processTimelineData: function(timelineData)
1083 if (!timelineData) {
1084 this._timelineLevels = null;
1085 this._rawTimelineData = null;
1086 this._rawTimelineDataLength = 0;
1090 var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1);
1091 for (var i = 0; i < timelineData.entryLevels.length; ++i)
1092 ++entryCounters[timelineData.entryLevels[i]];
1093 var levelIndexes = new Array(entryCounters.length);
1094 for (var i = 0; i < levelIndexes.length; ++i) {
1095 levelIndexes[i] = new Uint32Array(entryCounters[i]);
1096 entryCounters[i] = 0;
1098 for (var i = 0; i < timelineData.entryLevels.length; ++i) {
1099 var level = timelineData.entryLevels[i];
1100 levelIndexes[level][entryCounters[level]++] = i;
1102 this._timelineLevels = levelIndexes;
1103 this._rawTimelineData = timelineData;
1104 this._rawTimelineDataLength = timelineData.entryStartTimes.length;
1108 * @param {number} entryIndex
1110 setSelectedEntry: function(entryIndex)
1112 this._selectedEntryIndex = entryIndex;
1113 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
1116 _updateElementPosition: function(element, entryIndex)
1118 if (element.parentElement)
1120 if (entryIndex === -1)
1122 var timeRange = this._dataProvider.highlightTimeRange(entryIndex);
1125 var timelineData = this._timelineData();
1126 var barX = this._timeToPosition(timeRange.startTime);
1127 var barRight = this._timeToPosition(timeRange.endTime);
1128 if (barRight === 0 || barX === this._canvas.width)
1130 var barWidth = Math.max(barRight - barX, this._minWidth);
1131 var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop;
1132 var style = element.style;
1133 style.left = barX + "px";
1134 style.top = barY + "px";
1135 style.width = barWidth + "px";
1136 style.height = this._barHeight + "px";
1137 this.contentElement.appendChild(element);
1141 * @param {number} time
1143 _timeToPosition: function(time)
1145 var value = Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
1146 return Math.min(this._canvas.width, Math.max(0, value));
1149 _levelToHeight: function(level)
1151 return this._baseHeight - level * this._barHeightDelta;
1154 _buildEntryInfo: function(entryInfo)
1156 var infoTable = createElementWithClass("table", "info-table");
1157 for (var i = 0; i < entryInfo.length; ++i) {
1158 var row = infoTable.createChild("tr");
1159 row.createChild("td", "title").textContent = entryInfo[i].title;
1160 row.createChild("td").textContent = entryInfo[i].text;
1166 * @param {!CanvasRenderingContext2D} context
1167 * @param {string} title
1168 * @param {number} maxSize
1171 _prepareText: function(context, title, maxSize)
1173 var titleWidth = this._measureWidth(context, title);
1174 if (maxSize >= titleWidth)
1178 var r = title.length;
1180 var m = (l + r) >> 1;
1181 if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize)
1186 title = title.trimMiddle(r - 1);
1187 return title !== "\u2026" ? title : "";
1191 * @param {!CanvasRenderingContext2D} context
1192 * @param {string} text
1195 _measureWidth: function(context, text)
1197 if (text.length > 20)
1198 return context.measureText(text).width;
1200 var font = context.font;
1201 var textWidths = this._textWidth[font];
1204 this._textWidth[font] = textWidths;
1206 var width = textWidths[text];
1208 width = context.measureText(text).width;
1209 textWidths[text] = width;
1214 _updateBoundaries: function()
1216 this._totalTime = this._dataProvider.totalTime();
1217 this._minimumBoundary = this._dataProvider.minimumBoundary();
1219 if (this._timeWindowRight !== Infinity) {
1220 this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime;
1221 this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime;
1222 this._windowWidth = this._windowRight - this._windowLeft;
1224 this._windowLeft = 0;
1225 this._windowRight = 1;
1226 this._windowWidth = 1;
1229 this._pixelWindowWidth = this._offsetWidth - this._paddingLeft;
1230 this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
1231 this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
1232 this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
1234 this._timeToPixel = this._totalPixels / this._totalTime;
1235 this._pixelToTime = this._totalTime / this._totalPixels;
1236 this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
1238 this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight;
1240 this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth() + 1);
1241 this._vScrollContent.style.height = this._totalHeight + "px";
1242 this._scrollTop = this._vScrollElement.scrollTop;
1243 this._updateScrollBar();
1246 onResize: function()
1248 this._updateScrollBar();
1249 this.scheduleUpdate();
1252 _updateScrollBar: function()
1254 var showScroll = this._totalHeight > this._offsetHeight;
1255 this._vScrollElement.classList.toggle("hidden", !showScroll);
1256 this._offsetWidth = this.contentElement.offsetWidth - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth);
1257 this._offsetHeight = this.contentElement.offsetHeight;
1260 scheduleUpdate: function()
1262 if (this._updateTimerId || this._cancelWindowTimesAnimation)
1264 this._updateTimerId = requestAnimationFrame(this.update.bind(this));
1269 this._updateTimerId = 0;
1270 if (!this._timelineData())
1272 this._resetCanvas();
1273 this._updateBoundaries();
1274 this._calculator._updateBoundaries(this);
1275 this._draw(this._offsetWidth, this._offsetHeight);
1280 this._highlightedMarkerIndex = -1;
1281 this._highlightedEntryIndex = -1;
1282 this._selectedEntryIndex = -1;
1283 this._textWidth = {};
1287 _enabled: function()
1289 return this._rawTimelineDataLength !== 0;
1292 __proto__: WebInspector.HBox.prototype