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 updateRangeSelection: function(startTime, endTime) { },
49 endRangeSelection: function() { }
54 * @extends {WebInspector.HBox}
55 * @param {!WebInspector.FlameChartDataProvider} dataProvider
56 * @param {!WebInspector.FlameChartDelegate} flameChartDelegate
57 * @param {boolean} isTopDown
59 WebInspector.FlameChart = function(dataProvider, flameChartDelegate, isTopDown)
61 WebInspector.HBox.call(this, true);
62 this.registerRequiredCSS("ui_lazy/flameChart.css");
63 this.contentElement.classList.add("flame-chart-main-pane");
64 this._flameChartDelegate = flameChartDelegate;
65 this._isTopDown = isTopDown;
67 this._calculator = new WebInspector.FlameChart.Calculator();
69 this._canvas = this.contentElement.createChild("canvas");
70 this._canvas.tabIndex = 1;
71 this.setDefaultFocusedElement(this._canvas);
72 this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), false);
73 this._canvas.addEventListener("mouseout", this._onMouseOut.bind(this), false);
74 this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
75 this._canvas.addEventListener("click", this._onClick.bind(this), false);
76 this._canvas.addEventListener("keydown", this._onKeyDown.bind(this), false);
77 WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "-webkit-grabbing", null);
78 WebInspector.installDragHandle(this._canvas, this._startRangeSelection.bind(this), this._rangeSelectionDragging.bind(this), this._endRangeSelection.bind(this), "text", null);
80 this._vScrollElement = this.contentElement.createChild("div", "flame-chart-v-scroll");
81 this._vScrollContent = this._vScrollElement.createChild("div");
82 this._vScrollElement.addEventListener("scroll", this._onScroll.bind(this), false);
85 this._entryInfo = this.contentElement.createChild("div", "flame-chart-entry-info");
86 this._markerHighlighElement = this.contentElement.createChild("div", "flame-chart-marker-highlight-element");
87 this._highlightElement = this.contentElement.createChild("div", "flame-chart-highlight-element");
88 this._selectedElement = this.contentElement.createChild("div", "flame-chart-selected-element");
89 this._selectionOverlay = this.contentElement.createChild("div", "flame-chart-selection-overlay hidden");
90 this._selectedTimeSpanLabel = this._selectionOverlay.createChild("div", "time-span");
92 this._dataProvider = dataProvider;
94 this._windowLeft = 0.0;
95 this._windowRight = 1.0;
96 this._windowWidth = 1.0;
97 this._timeWindowLeft = 0;
98 this._timeWindowRight = Infinity;
99 this._barHeight = dataProvider.barHeight();
100 this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight;
101 this._paddingLeft = this._dataProvider.paddingLeft();
102 this._markerPadding = 2;
103 this._markerRadius = this._barHeight / 2 - this._markerPadding;
104 this._highlightedMarkerIndex = -1;
105 this._highlightedEntryIndex = -1;
106 this._selectedEntryIndex = -1;
107 this._rawTimelineDataLength = 0;
108 this._textWidth = {};
110 this._lastMouseOffsetX = 0;
113 WebInspector.FlameChart.DividersBarHeight = 18;
115 WebInspector.FlameChart.MinimalTimeWindowMs = 0.01;
120 WebInspector.FlameChartDataProvider = function()
126 * @param {!Array.<number>|!Uint8Array} entryLevels
127 * @param {!Array.<number>|!Float32Array} entryTotalTimes
128 * @param {!Array.<number>|!Float64Array} entryStartTimes
130 WebInspector.FlameChart.TimelineData = function(entryLevels, entryTotalTimes, entryStartTimes)
132 this.entryLevels = entryLevels;
133 this.entryTotalTimes = entryTotalTimes;
134 this.entryStartTimes = entryStartTimes;
135 /** @type {!Array.<!WebInspector.FlameChartMarker>} */
137 this.flowStartTimes = [];
138 this.flowStartLevels = [];
139 this.flowEndTimes = [];
140 this.flowEndLevels = [];
143 WebInspector.FlameChartDataProvider.prototype = {
147 barHeight: function() { },
150 * @param {number} startTime
151 * @param {number} endTime
152 * @return {?Array.<number>}
154 dividerOffsets: function(startTime, endTime) { },
159 minimumBoundary: function() { },
164 totalTime: function() { },
169 maxStackDepth: function() { },
172 * @return {?WebInspector.FlameChart.TimelineData}
174 timelineData: function() { },
177 * @param {number} entryIndex
178 * @return {?Array.<!{title: string, value: (string|!Element)}>}
180 prepareHighlightedEntryInfo: function(entryIndex) { },
183 * @param {number} entryIndex
186 canJumpToEntry: function(entryIndex) { },
189 * @param {number} entryIndex
192 entryTitle: function(entryIndex) { },
195 * @param {number} entryIndex
198 entryFont: function(entryIndex) { },
201 * @param {number} entryIndex
204 entryColor: function(entryIndex) { },
207 * @param {number} entryIndex
208 * @param {!CanvasRenderingContext2D} context
209 * @param {?string} text
210 * @param {number} barX
211 * @param {number} barY
212 * @param {number} barWidth
213 * @param {number} barHeight
216 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight) { },
219 * @param {number} entryIndex
222 forceDecoration: function(entryIndex) { },
225 * @param {number} entryIndex
228 textColor: function(entryIndex) { },
233 textBaseline: function() { },
238 textPadding: function() { },
241 * @return {?{startTime: number, endTime: number}}
243 highlightTimeRange: function(entryIndex) { },
248 paddingLeft: function() { },
254 WebInspector.FlameChartMarker = function()
258 WebInspector.FlameChartMarker.prototype = {
262 startTime: function() { },
267 color: function() { },
272 title: function() { },
275 * @param {!CanvasRenderingContext2D} context
277 * @param {number} height
278 * @param {number} pixelsPerMillisecond
280 draw: function(context, x, height, pixelsPerMillisecond) { },
283 WebInspector.FlameChart.Events = {
284 EntrySelected: "EntrySelected"
290 * @param {!{min: number, max: number, count: number}|number=} hueSpace
291 * @param {!{min: number, max: number, count: number}|number=} satSpace
292 * @param {!{min: number, max: number, count: number}|number=} lightnessSpace
293 * @param {!{min: number, max: number, count: number}|number=} alphaSpace
295 WebInspector.FlameChart.ColorGenerator = function(hueSpace, satSpace, lightnessSpace, alphaSpace)
297 this._hueSpace = hueSpace || { min: 0, max: 360, count: 20 };
298 this._satSpace = satSpace || 67;
299 this._lightnessSpace = lightnessSpace || 80;
300 this._alphaSpace = alphaSpace || 1;
304 WebInspector.FlameChart.ColorGenerator.prototype = {
307 * @param {string|!CanvasGradient} color
309 setColorForID: function(id, color)
311 this._colors[id] = color;
318 colorForID: function(id)
320 var color = this._colors[id];
322 color = this._generateColorForID(id);
323 this._colors[id] = color;
332 _generateColorForID: function(id)
334 var hash = Math.abs(String.hashCode(id));
335 var h = this._indexToValueInSpace(hash, this._hueSpace);
336 var s = this._indexToValueInSpace(hash, this._satSpace);
337 var l = this._indexToValueInSpace(hash, this._lightnessSpace);
338 var a = this._indexToValueInSpace(hash, this._alphaSpace);
339 return "hsla(" + h + ", " + s + "%, " + l + "%, " + a + ")";
343 * @param {number} index
344 * @param {!{min: number, max: number, count: number}|number} space
347 _indexToValueInSpace: function(index, space)
349 if (typeof space === "number")
351 index %= space.count;
352 return space.min + Math.floor(index / (space.count - 1) * (space.max - space.min));
359 * @implements {WebInspector.TimelineGrid.Calculator}
361 WebInspector.FlameChart.Calculator = function()
363 this._paddingLeft = 0;
366 WebInspector.FlameChart.Calculator.prototype = {
371 paddingLeft: function()
373 return this._paddingLeft;
377 * @param {!WebInspector.FlameChart} mainPane
379 _updateBoundaries: function(mainPane)
381 this._totalTime = mainPane._dataProvider.totalTime();
382 this._zeroTime = mainPane._dataProvider.minimumBoundary();
383 this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime;
384 this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime;
385 this._paddingLeft = mainPane._paddingLeft;
386 this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft;
387 this._timeToPixel = this._width / this.boundarySpan();
392 * @param {number} time
395 computePosition: function(time)
397 return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft);
402 * @param {number} value
403 * @param {number=} precision
406 formatTime: function(value, precision)
408 return Number.preciseMillisToString(value - this._zeroTime, precision);
415 maximumBoundary: function()
417 return this._maximumBoundaries;
424 minimumBoundary: function()
426 return this._minimumBoundaries;
435 return this._zeroTime;
442 boundarySpan: function()
444 return this._maximumBoundaries - this._minimumBoundaries;
448 WebInspector.FlameChart.prototype = {
449 _resetCanvas: function()
451 var ratio = window.devicePixelRatio;
452 this._canvas.width = this._offsetWidth * ratio;
453 this._canvas.height = this._offsetHeight * ratio;
454 this._canvas.style.width = this._offsetWidth + "px";
455 this._canvas.style.height = this._offsetHeight + "px";
459 * @return {?WebInspector.FlameChart.TimelineData}
461 _timelineData: function()
463 var timelineData = this._dataProvider.timelineData();
464 if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength)
465 this._processTimelineData(timelineData);
466 return this._rawTimelineData;
469 _cancelAnimation: function()
471 if (this._cancelWindowTimesAnimation) {
472 this._timeWindowLeft = this._pendingAnimationTimeLeft;
473 this._timeWindowRight = this._pendingAnimationTimeRight;
474 this._cancelWindowTimesAnimation();
475 delete this._cancelWindowTimesAnimation;
480 * @param {number} entryIndex
482 _revealEntry: function(entryIndex)
484 var timelineData = this._timelineData();
487 // Think in terms of not where we are, but where we'll be after animation (if present)
488 var timeLeft = this._cancelWindowTimesAnimation ? this._pendingAnimationTimeLeft : this._timeWindowLeft;
489 var timeRight = this._cancelWindowTimesAnimation ? this._pendingAnimationTimeRight : this._timeWindowRight;
490 var entryStartTime = timelineData.entryStartTimes[entryIndex];
491 var entryTotalTime = timelineData.entryTotalTimes[entryIndex];
492 var entryEndTime = entryStartTime + entryTotalTime;
493 var minEntryTimeWindow = Math.min(entryTotalTime, timeRight - timeLeft);
495 var y = this._levelToHeight(timelineData.entryLevels[entryIndex]);
496 if (y < this._vScrollElement.scrollTop)
497 this._vScrollElement.scrollTop = y;
498 else if (y > this._vScrollElement.scrollTop + this._offsetHeight + this._barHeightDelta)
499 this._vScrollElement.scrollTop = y - this._offsetHeight - this._barHeightDelta;
501 if (timeLeft > entryEndTime) {
502 var delta = timeLeft - entryEndTime + minEntryTimeWindow;
503 this._flameChartDelegate.requestWindowTimes(timeLeft - delta, timeRight - delta);
504 } else if (timeRight < entryStartTime) {
505 var delta = entryStartTime - timeRight + minEntryTimeWindow;
506 this._flameChartDelegate.requestWindowTimes(timeRight + delta, timeRight + delta);
511 * @param {number} startTime
512 * @param {number} endTime
514 setWindowTimes: function(startTime, endTime)
516 if (this._muteAnimation || this._timeWindowLeft === 0 || this._timeWindowRight === Infinity || (startTime === 0 && endTime === Infinity) || (startTime === Infinity && endTime === Infinity)) {
518 this._timeWindowLeft = startTime;
519 this._timeWindowRight = endTime;
520 this.scheduleUpdate();
524 this._cancelAnimation();
525 this._cancelWindowTimesAnimation = WebInspector.animateFunction(this.element.window(), this._animateWindowTimes.bind(this),
526 [{from: this._timeWindowLeft, to: startTime}, {from: this._timeWindowRight, to: endTime}], 5,
527 this._animationCompleted.bind(this));
528 this._pendingAnimationTimeLeft = startTime;
529 this._pendingAnimationTimeRight = endTime;
533 * @param {number} startTime
534 * @param {number} endTime
536 _animateWindowTimes: function(startTime, endTime)
538 this._timeWindowLeft = startTime;
539 this._timeWindowRight = endTime;
543 _animationCompleted: function()
545 delete this._cancelWindowTimesAnimation;
549 * @param {!MouseEvent} event
551 _initMaxDragOffset: function(event)
553 this._maxDragOffsetSquared = 0;
554 this._dragStartX = event.pageX;
555 this._dragStartY = event.pageY;
559 * @param {!MouseEvent} event
561 _updateMaxDragOffset: function(event)
563 var dx = event.pageX - this._dragStartX;
564 var dy = event.pageY - this._dragStartY;
565 var dragOffsetSquared = dx * dx + dy * dy;
566 this._maxDragOffsetSquared = Math.max(this._maxDragOffsetSquared, dragOffsetSquared);
572 _maxDragOffset: function()
574 return Math.sqrt(this._maxDragOffsetSquared);
578 * @param {!MouseEvent} event
581 _startCanvasDragging: function(event)
585 if (!this._timelineData() || this._timeWindowRight === Infinity)
587 this._isDragging = true;
588 this._initMaxDragOffset(event);
589 this._dragStartPointX = event.pageX;
590 this._dragStartPointY = event.pageY;
591 this._dragStartScrollTop = this._vScrollElement.scrollTop;
592 this._dragStartWindowLeft = this._timeWindowLeft;
593 this._dragStartWindowRight = this._timeWindowRight;
594 this._canvas.style.cursor = "";
599 * @param {!MouseEvent} event
601 _canvasDragging: function(event)
603 var pixelShift = this._dragStartPointX - event.pageX;
604 this._dragStartPointX = event.pageX;
605 this._muteAnimation = true;
606 this._handlePanGesture(pixelShift * this._pixelToTime);
607 this._muteAnimation = false;
609 var pixelScroll = this._dragStartPointY - event.pageY;
610 this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
611 this._updateMaxDragOffset(event);
614 _endCanvasDragging: function()
616 this._isDragging = false;
620 * @param {!MouseEvent} event
623 _startRangeSelection: function(event)
627 this._isDragging = true;
628 this._initMaxDragOffset(event);
629 this._selectionOffsetShiftX = event.offsetX - event.pageX;
630 this._selectionOffsetShiftY = event.offsetY - event.pageY;
631 this._selectionStartX = event.offsetX;
632 var style = this._selectionOverlay.style;
633 style.left = this._selectionStartX + "px";
635 this._selectedTimeSpanLabel.textContent = "";
636 this._selectionOverlay.classList.remove("hidden");
640 _endRangeSelection: function()
642 this._isDragging = false;
643 this._flameChartDelegate.endRangeSelection();
646 _hideRangeSelection: function()
648 this._selectionOverlay.classList.add("hidden");
652 * @param {!MouseEvent} event
654 _rangeSelectionDragging: function(event)
656 this._updateMaxDragOffset(event);
657 var x = Number.constrain(event.pageX + this._selectionOffsetShiftX, 0, this._offsetWidth);
658 var start = this._cursorTime(this._selectionStartX);
659 var end = this._cursorTime(x);
660 this._rangeSelectionStart = Math.min(start, end);
661 this._rangeSelectionEnd = Math.max(start, end);
662 this._updateRangeSelectionOverlay();
663 this._flameChartDelegate.updateRangeSelection(this._rangeSelectionStart, this._rangeSelectionEnd);
666 _updateRangeSelectionOverlay: function()
669 var left = Number.constrain(this._timeToPosition(this._rangeSelectionStart), -margin, this._offsetWidth + margin);
670 var right = Number.constrain(this._timeToPosition(this._rangeSelectionEnd), -margin, this._offsetWidth + margin);
671 var style = this._selectionOverlay.style;
672 style.left = left + "px";
673 style.width = (right - left) + "px";
674 var timeSpan = this._rangeSelectionEnd - this._rangeSelectionStart;
675 this._selectedTimeSpanLabel.textContent = Number.preciseMillisToString(timeSpan, 2);
679 * @param {!Event} event
681 _onMouseMove: function(event)
683 this._lastMouseOffsetX = event.offsetX;
685 if (!this._enabled())
688 if (this._isDragging)
691 var inDividersBar = event.offsetY < WebInspector.FlameChart.DividersBarHeight;
692 this._highlightedMarkerIndex = inDividersBar ? this._markerIndexAtPosition(event.offsetX) : -1;
693 this._updateMarkerHighlight();
695 this._highlightEntry(this._coordinatesToEntryIndex(event.offsetX, event.offsetY));
698 _onMouseOut: function()
700 this._highlightEntry(-1);
704 * @param {number} entryIndex
706 _highlightEntry: function(entryIndex)
708 if (this._highlightedEntryIndex === entryIndex)
711 if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
712 this._canvas.style.cursor = "default";
714 this._canvas.style.cursor = "pointer";
716 this._highlightedEntryIndex = entryIndex;
718 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
719 this._entryInfo.removeChildren();
721 if (this._highlightedEntryIndex === -1)
724 if (!this._isDragging) {
725 var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex);
727 this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
734 // onClick comes after dragStart and dragEnd events.
735 // So if there was drag (mouse move) in the middle of that events
736 // we skip the click. Otherwise we jump to the sources.
737 const clickThreshold = 5;
738 if (this._maxDragOffset() > clickThreshold)
740 this._hideRangeSelection();
741 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex);
747 _onMouseWheel: function(e)
749 if (!this._enabled())
751 // Pan vertically when shift down only.
752 var panVertically = e.shiftKey && (e.wheelDeltaY || Math.abs(e.wheelDeltaX) === 120);
753 var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey;
755 this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 * this._offsetHeight / 8;
756 } else if (panHorizontally) {
757 var shift = -e.wheelDeltaX * this._pixelToTime;
758 this._muteAnimation = true;
759 this._handlePanGesture(shift);
760 this._muteAnimation = false;
762 const mouseWheelZoomSpeed = 1 / 120;
763 this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1);
766 // Block swipe gesture.
773 _onKeyDown: function(e)
775 this._handleZoomPanKeys(e);
776 this._handleSelectionNavigation(e);
782 _handleSelectionNavigation: function(e)
784 if (!WebInspector.KeyboardShortcut.hasNoModifiers(e))
786 if (this._selectedEntryIndex === -1)
788 var timelineData = this._timelineData();
793 * @param {number} time
794 * @param {number} entryIndex
797 function timeComparator(time, entryIndex)
799 return time - timelineData.entryStartTimes[entryIndex];
803 * @param {number} entry1
804 * @param {number} entry2
807 function entriesIntersect(entry1, entry2)
809 var start1 = timelineData.entryStartTimes[entry1];
810 var start2 = timelineData.entryStartTimes[entry2];
811 var end1 = start1 + timelineData.entryTotalTimes[entry1];
812 var end2 = start2 + timelineData.entryTotalTimes[entry2];
813 return start1 < end2 && start2 < end1;
816 var keys = WebInspector.KeyboardShortcut.Keys;
817 if (e.keyCode === keys.Left.code || e.keyCode === keys.Right.code) {
818 var level = timelineData.entryLevels[this._selectedEntryIndex];
819 var levelIndexes = this._timelineLevels[level];
820 var indexOnLevel = levelIndexes.lowerBound(this._selectedEntryIndex);
821 indexOnLevel += e.keyCode === keys.Left.code ? -1 : 1;
823 if (indexOnLevel >= 0 && indexOnLevel < levelIndexes.length)
824 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, levelIndexes[indexOnLevel]);
827 if (e.keyCode === keys.Up.code || e.keyCode === keys.Down.code) {
828 var level = timelineData.entryLevels[this._selectedEntryIndex];
829 var delta = e.keyCode === keys.Up.code ? 1 : -1;
834 if (level < 0 || level >= this._timelineLevels.length)
836 var entryTime = timelineData.entryStartTimes[this._selectedEntryIndex] + timelineData.entryTotalTimes[this._selectedEntryIndex] / 2;
837 var levelIndexes = this._timelineLevels[level];
838 var indexOnLevel = levelIndexes.upperBound(entryTime, timeComparator) - 1;
839 if (!entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel])) {
841 if (indexOnLevel >= levelIndexes.length || !entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel]))
844 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, levelIndexes[indexOnLevel]);
851 _handleZoomPanKeys: function(e)
853 if (!WebInspector.KeyboardShortcut.hasNoModifiers(e))
855 var zoomMultiplier = e.shiftKey ? 0.8 : 0.3;
856 var panMultiplier = e.shiftKey ? 320 : 80;
857 if (e.keyCode === "A".charCodeAt(0)) {
858 this._handlePanGesture(-panMultiplier * this._pixelToTime);
860 } else if (e.keyCode === "D".charCodeAt(0)) {
861 this._handlePanGesture(panMultiplier * this._pixelToTime);
863 } else if (e.keyCode === "W".charCodeAt(0)) {
864 this._handleZoomGesture(-zoomMultiplier);
866 } else if (e.keyCode === "S".charCodeAt(0)) {
867 this._handleZoomGesture(zoomMultiplier);
873 * @param {number} zoom
875 _handleZoomGesture: function(zoom)
877 this._cancelAnimation();
878 var bounds = this._windowForGesture();
879 var cursorTime = this._cursorTime(this._lastMouseOffsetX);
880 bounds.left += (bounds.left - cursorTime) * zoom;
881 bounds.right += (bounds.right - cursorTime) * zoom;
882 this._requestWindowTimes(bounds);
886 * @param {number} shift
888 _handlePanGesture: function(shift)
890 this._cancelAnimation();
891 var bounds = this._windowForGesture();
892 shift = Number.constrain(shift, this._minimumBoundary - bounds.left, this._totalTime + this._minimumBoundary - bounds.right);
893 bounds.left += shift;
894 bounds.right += shift;
895 this._requestWindowTimes(bounds);
899 * @return {{left: number, right: number}}
901 _windowForGesture: function()
903 var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
904 var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
905 return {left: windowLeft, right: windowRight};
909 * @param {{left: number, right: number}} bounds
911 _requestWindowTimes: function(bounds)
913 bounds.left = Number.constrain(bounds.left, this._minimumBoundary, this._totalTime + this._minimumBoundary);
914 bounds.right = Number.constrain(bounds.right, this._minimumBoundary, this._totalTime + this._minimumBoundary);
915 if (bounds.right - bounds.left < WebInspector.FlameChart.MinimalTimeWindowMs)
917 this._flameChartDelegate.requestWindowTimes(bounds.left, bounds.right);
924 _cursorTime: function(x)
926 return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._minimumBoundary;
934 _coordinatesToEntryIndex: function(x, y)
936 y += this._scrollTop;
937 var timelineData = this._timelineData();
940 var cursorTime = this._cursorTime(x);
943 if (this._isTopDown) {
944 cursorLevel = Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight);
945 offsetFromLevel = y - WebInspector.FlameChart.DividersBarHeight - cursorLevel * this._barHeight;
947 cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
948 offsetFromLevel = this._canvas.height / window.devicePixelRatio - cursorLevel * this._barHeight;
950 var entryStartTimes = timelineData.entryStartTimes;
951 var entryTotalTimes = timelineData.entryTotalTimes;
952 var entryIndexes = this._timelineLevels[cursorLevel];
953 if (!entryIndexes || !entryIndexes.length)
957 * @param {number} time
958 * @param {number} entryIndex
961 function comparator(time, entryIndex)
963 return time - entryStartTimes[entryIndex];
965 var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator) - 1, 0);
968 * @this {WebInspector.FlameChart}
969 * @param {number} entryIndex
972 function checkEntryHit(entryIndex)
974 if (entryIndex === undefined)
976 var startTime = entryStartTimes[entryIndex];
977 var duration = entryTotalTimes[entryIndex];
978 if (isNaN(duration)) {
979 var dx = (startTime - cursorTime) / this._pixelToTime;
980 var dy = this._barHeight / 2 - offsetFromLevel;
981 return dx * dx + dy * dy < this._markerRadius * this._markerRadius;
983 var endTime = startTime + duration;
984 var barThreshold = 3 * this._pixelToTime;
985 return startTime - barThreshold < cursorTime && cursorTime < endTime + barThreshold;
988 var entryIndex = entryIndexes[indexOnLevel];
989 if (checkEntryHit.call(this, entryIndex))
991 entryIndex = entryIndexes[indexOnLevel + 1];
992 if (checkEntryHit.call(this, entryIndex))
1001 _markerIndexAtPosition: function(x)
1003 var markers = this._timelineData().markers;
1006 var accurracyOffsetPx = 1;
1007 var time = this._cursorTime(x);
1008 var leftTime = this._cursorTime(x - accurracyOffsetPx);
1009 var rightTime = this._cursorTime(x + accurracyOffsetPx);
1011 var left = this._markerIndexBeforeTime(leftTime);
1012 var markerIndex = -1;
1013 var distance = Infinity;
1014 for (var i = left; i < markers.length && markers[i].startTime() < rightTime; i++) {
1015 var nextDistance = Math.abs(markers[i].startTime() - time);
1016 if (nextDistance < distance) {
1018 distance = nextDistance;
1025 * @param {number} time
1028 _markerIndexBeforeTime: function(time)
1031 * @param {number} markerTimestamp
1032 * @param {!WebInspector.FlameChartMarker} marker
1035 function comparator(markerTimestamp, marker)
1037 return markerTimestamp - marker.startTime();
1039 return this._timelineData().markers.lowerBound(time, comparator);
1043 * @param {number} height
1044 * @param {number} width
1046 _draw: function(width, height)
1048 var timelineData = this._timelineData();
1052 var context = this._canvas.getContext("2d");
1054 var ratio = window.devicePixelRatio;
1055 context.scale(ratio, ratio);
1057 var timeWindowRight = this._timeWindowRight;
1058 var timeWindowLeft = this._timeWindowLeft - this._paddingLeftTime;
1059 var entryTotalTimes = timelineData.entryTotalTimes;
1060 var entryStartTimes = timelineData.entryStartTimes;
1061 var entryLevels = timelineData.entryLevels;
1063 var titleIndices = new Uint32Array(entryTotalTimes.length);
1064 var nextTitleIndex = 0;
1065 var markerIndices = new Uint32Array(entryTotalTimes.length);
1066 var nextMarkerIndex = 0;
1067 var textPadding = this._dataProvider.textPadding();
1068 this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026");
1069 var minTextWidth = this._minTextWidth;
1070 var unclippedWidth = width - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth);
1072 var barHeight = this._barHeight;
1074 var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline();
1075 var colorBuckets = {};
1076 var minVisibleBarLevel = Math.max(Math.floor((this._scrollTop - this._baseHeight) / barHeight), 0);
1077 var maxVisibleBarLevel = Math.min(Math.floor((this._scrollTop - this._baseHeight + height) / barHeight), this._dataProvider.maxStackDepth());
1079 context.translate(0, -this._scrollTop);
1081 function comparator(time, entryIndex)
1083 return time - entryStartTimes[entryIndex];
1086 for (var level = minVisibleBarLevel; level <= maxVisibleBarLevel; ++level) {
1087 // Entries are ordered by start time within a level, so find the last visible entry.
1088 var levelIndexes = this._timelineLevels[level];
1089 var rightIndexOnLevel = levelIndexes.lowerBound(timeWindowRight, comparator) - 1;
1090 var lastDrawOffset = Infinity;
1091 for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
1092 var entryIndex = levelIndexes[entryIndexOnLevel];
1093 var entryStartTime = entryStartTimes[entryIndex];
1094 var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]);
1095 if (entryOffsetRight <= timeWindowLeft)
1098 var barX = this._timeToPositionClipped(entryStartTime);
1099 // Check if the entry entirely fits into an already drawn pixel, we can just skip drawing it.
1100 if (barX >= lastDrawOffset)
1102 lastDrawOffset = barX;
1104 var color = this._dataProvider.entryColor(entryIndex);
1105 var bucket = colorBuckets[color];
1108 colorBuckets[color] = bucket;
1110 bucket.push(entryIndex);
1114 var colors = Object.keys(colorBuckets);
1115 // We don't use for-in here because it couldn't be optimized.
1116 for (var c = 0; c < colors.length; ++c) {
1117 var color = colors[c];
1118 context.fillStyle = color;
1119 context.strokeStyle = color;
1120 var indexes = colorBuckets[color];
1122 // First fill the boxes.
1123 context.beginPath();
1124 for (var i = 0; i < indexes.length; ++i) {
1125 var entryIndex = indexes[i];
1126 var entryStartTime = entryStartTimes[entryIndex];
1127 var barX = this._timeToPositionClipped(entryStartTime);
1128 var barRight = this._timeToPositionClipped(entryStartTime + entryTotalTimes[entryIndex]);
1129 var barWidth = Math.max(barRight - barX, 1);
1130 var barLevel = entryLevels[entryIndex];
1131 var barY = this._levelToHeight(barLevel);
1132 if (isNaN(entryTotalTimes[entryIndex])) {
1133 context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
1134 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
1135 markerIndices[nextMarkerIndex++] = entryIndex;
1137 context.rect(barX, barY, barWidth - 0.4, barHeight - 1);
1138 if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex))
1139 titleIndices[nextTitleIndex++] = entryIndex;
1145 context.strokeStyle = "rgb(0, 0, 0)";
1146 context.beginPath();
1147 for (var m = 0; m < nextMarkerIndex; ++m) {
1148 var entryIndex = markerIndices[m];
1149 var entryStartTime = entryStartTimes[entryIndex];
1150 var barX = this._timeToPositionClipped(entryStartTime);
1151 var barLevel = entryLevels[entryIndex];
1152 var barY = this._levelToHeight(barLevel);
1153 context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
1154 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
1158 context.textBaseline = "alphabetic";
1160 for (var i = 0; i < nextTitleIndex; ++i) {
1161 var entryIndex = titleIndices[i];
1162 var entryStartTime = entryStartTimes[entryIndex];
1163 var barX = this._timeToPositionClipped(entryStartTime);
1164 var barRight = Math.min(this._timeToPositionClipped(entryStartTime + entryTotalTimes[entryIndex]), unclippedWidth) + 1;
1165 var barWidth = barRight - barX;
1166 var barLevel = entryLevels[entryIndex];
1167 var barY = this._levelToHeight(barLevel);
1168 var text = this._dataProvider.entryTitle(entryIndex);
1169 if (text && text.length) {
1170 context.font = this._dataProvider.entryFont(entryIndex);
1171 text = this._prepareText(context, text, barWidth - 2 * textPadding);
1174 if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight))
1176 if (!text || !text.length)
1179 context.fillStyle = this._dataProvider.textColor(entryIndex);
1180 context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta);
1183 this._drawFlowEvents(context, width, height);
1187 var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary());
1188 WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets);
1189 this._drawMarkers();
1191 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
1192 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
1193 this._updateMarkerHighlight();
1194 this._updateRangeSelectionOverlay();
1198 * @param {!CanvasRenderingContext2D} context
1199 * @param {number} height
1200 * @param {number} width
1202 _drawFlowEvents: function(context, width, height)
1204 var timelineData = this._timelineData();
1205 var timeWindowRight = this._timeWindowRight;
1206 var timeWindowLeft = this._timeWindowLeft;
1207 var flowStartTimes = timelineData.flowStartTimes;
1208 var flowEndTimes = timelineData.flowEndTimes;
1209 var flowStartLevels = timelineData.flowStartLevels;
1210 var flowEndLevels = timelineData.flowEndLevels;
1211 var flowCount = flowStartTimes.length;
1212 var endIndex = flowStartTimes.lowerBound(timeWindowRight);
1215 var fadeColorsCount = 8;
1216 for (var i = 0; i <= fadeColorsCount; ++i)
1217 color[i] = "rgba(128, 0, 0, " + i / fadeColorsCount + ")";
1218 var fadeColorsRange = color.length;
1219 var minimumFlowDistancePx = 15;
1220 var flowArcHeight = 4 * this._barHeight;
1222 context.lineWidth = 0.5;
1223 for (var i = 0; i < endIndex; ++i) {
1224 if (flowEndTimes[i] < timeWindowLeft)
1226 var startX = this._timeToPosition(flowStartTimes[i]);
1227 var endX = this._timeToPosition(flowEndTimes[i]);
1228 if (endX - startX < minimumFlowDistancePx)
1230 if (startX < -minimumFlowDistancePx && endX > width + minimumFlowDistancePx)
1232 // Assign a trasparent color if the flow is small enough or if the previous color was a transparent color.
1233 if (endX - startX < minimumFlowDistancePx + fadeColorsRange || colorIndex !== color.length - 1) {
1234 colorIndex = Math.min(fadeColorsRange - 1, Math.floor(endX - startX - minimumFlowDistancePx));
1235 context.strokeStyle = color[colorIndex];
1237 var startY = this._levelToHeight(flowStartLevels[i]) + this._barHeight;
1238 var endY = this._levelToHeight(flowEndLevels[i]);
1239 context.beginPath();
1240 context.moveTo(startX, startY);
1241 var arcHeight = Math.max(Math.sqrt(Math.abs(startY - endY)), flowArcHeight) + 5;
1242 context.bezierCurveTo(startX, startY + arcHeight,
1243 endX, endY + arcHeight,
1244 endX, endY + this._barHeight);
1249 _drawMarkers: function()
1251 var markers = this._timelineData().markers;
1252 var left = this._markerIndexBeforeTime(this._calculator.minimumBoundary());
1253 var rightBoundary = this._calculator.maximumBoundary();
1255 var context = this._canvas.getContext("2d");
1257 var ratio = window.devicePixelRatio;
1258 context.scale(ratio, ratio);
1259 var height = WebInspector.FlameChart.DividersBarHeight - 1;
1260 for (var i = left; i < markers.length; i++) {
1261 var timestamp = markers[i].startTime();
1262 if (timestamp > rightBoundary)
1264 markers[i].draw(context, this._calculator.computePosition(timestamp), height, this._timeToPixel);
1269 _updateMarkerHighlight: function()
1271 var element = this._markerHighlighElement;
1272 if (element.parentElement)
1274 var markerIndex = this._highlightedMarkerIndex;
1275 if (markerIndex === -1)
1277 var marker = this._timelineData().markers[markerIndex];
1278 var barX = this._timeToPositionClipped(marker.startTime());
1279 element.title = marker.title();
1280 var style = element.style;
1281 style.left = barX + "px";
1282 style.backgroundColor = marker.color();
1283 this.contentElement.appendChild(element);
1287 * @param {?WebInspector.FlameChart.TimelineData} timelineData
1289 _processTimelineData: function(timelineData)
1291 if (!timelineData) {
1292 this._timelineLevels = null;
1293 this._rawTimelineData = null;
1294 this._rawTimelineDataLength = 0;
1298 var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1);
1299 for (var i = 0; i < timelineData.entryLevels.length; ++i)
1300 ++entryCounters[timelineData.entryLevels[i]];
1301 var levelIndexes = new Array(entryCounters.length);
1302 for (var i = 0; i < levelIndexes.length; ++i) {
1303 levelIndexes[i] = new Uint32Array(entryCounters[i]);
1304 entryCounters[i] = 0;
1306 for (var i = 0; i < timelineData.entryLevels.length; ++i) {
1307 var level = timelineData.entryLevels[i];
1308 levelIndexes[level][entryCounters[level]++] = i;
1310 this._timelineLevels = levelIndexes;
1311 this._rawTimelineData = timelineData;
1312 this._rawTimelineDataLength = timelineData.entryStartTimes.length;
1316 * @param {number} entryIndex
1318 setSelectedEntry: function(entryIndex)
1320 if (entryIndex === -1 && !this._isDragging)
1321 this._hideRangeSelection();
1322 if (this._selectedEntryIndex === entryIndex)
1324 this._selectedEntryIndex = entryIndex;
1325 this._revealEntry(entryIndex);
1326 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
1330 * @param {!Element} element
1331 * @param {number} entryIndex
1333 _updateElementPosition: function(element, entryIndex)
1335 /** @const */ var elementMinWidth = 2;
1336 if (element.parentElement)
1338 if (entryIndex === -1)
1340 var timeRange = this._dataProvider.highlightTimeRange(entryIndex);
1343 var timelineData = this._timelineData();
1344 var barX = this._timeToPositionClipped(timeRange.startTime);
1345 var barRight = this._timeToPositionClipped(timeRange.endTime);
1346 if (barRight === 0 || barX === this._canvas.width)
1348 var barWidth = barRight - barX;
1349 var barCenter = barX + barWidth / 2;
1350 barWidth = Math.max(barWidth, elementMinWidth);
1351 barX = barCenter - barWidth / 2;
1352 var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop;
1353 var style = element.style;
1354 style.left = barX + "px";
1355 style.top = barY + "px";
1356 style.width = barWidth + "px";
1357 style.height = this._barHeight - 1 + "px";
1358 this.contentElement.appendChild(element);
1362 * @param {number} time
1365 _timeToPositionClipped: function(time)
1367 return Number.constrain(this._timeToPosition(time), 0, this._canvas.width);
1371 * @param {number} time
1374 _timeToPosition: function(time)
1376 return Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
1380 * @param {number} level
1383 _levelToHeight: function(level)
1385 return this._baseHeight - level * this._barHeightDelta;
1389 * @param {!Array<!{title: string, value: (string|!Element)}>} entryInfo
1390 * @return {!Element}
1392 _buildEntryInfo: function(entryInfo)
1394 var infoTable = createElementWithClass("table", "info-table");
1395 for (var entry of entryInfo) {
1396 var row = infoTable.createChild("tr");
1397 row.createChild("td", "title").textContent = entry.title;
1398 row.createChild("td").textContent = typeof entry.value === "string" ? entry.value : entry.value.textContent;
1404 * @param {!CanvasRenderingContext2D} context
1405 * @param {string} title
1406 * @param {number} maxSize
1409 _prepareText: function(context, title, maxSize)
1411 var titleWidth = this._measureWidth(context, title);
1412 if (maxSize >= titleWidth)
1416 var r = title.length;
1418 var m = (l + r) >> 1;
1419 if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize)
1424 title = title.trimMiddle(r - 1);
1425 return title !== "\u2026" ? title : "";
1429 * @param {!CanvasRenderingContext2D} context
1430 * @param {string} text
1433 _measureWidth: function(context, text)
1435 if (text.length > 20)
1436 return context.measureText(text).width;
1438 var font = context.font;
1439 var textWidths = this._textWidth[font];
1442 this._textWidth[font] = textWidths;
1444 var width = textWidths[text];
1446 width = context.measureText(text).width;
1447 textWidths[text] = width;
1452 _updateBoundaries: function()
1454 this._totalTime = this._dataProvider.totalTime();
1455 this._minimumBoundary = this._dataProvider.minimumBoundary();
1457 if (this._timeWindowRight !== Infinity) {
1458 this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime;
1459 this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime;
1460 this._windowWidth = this._windowRight - this._windowLeft;
1461 } else if (this._timeWindowLeft === Infinity) {
1462 this._windowLeft = Infinity;
1463 this._windowRight = Infinity;
1464 this._windowWidth = 1;
1466 this._windowLeft = 0;
1467 this._windowRight = 1;
1468 this._windowWidth = 1;
1471 this._pixelWindowWidth = this._offsetWidth - this._paddingLeft;
1472 this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
1473 this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
1475 this._timeToPixel = this._totalPixels / this._totalTime;
1476 this._pixelToTime = this._totalTime / this._totalPixels;
1477 this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
1479 this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight;
1481 this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth());
1482 this._vScrollContent.style.height = this._totalHeight + "px";
1483 this._updateScrollBar();
1486 onResize: function()
1488 this._updateScrollBar();
1489 this._updateContentElementSize();
1490 this.scheduleUpdate();
1493 _updateScrollBar: function()
1495 var showScroll = this._totalHeight > this._offsetHeight;
1496 if (this._vScrollElement.classList.contains("hidden") === showScroll) {
1497 this._vScrollElement.classList.toggle("hidden", !showScroll);
1498 this._updateContentElementSize();
1502 _updateContentElementSize: function()
1504 this._offsetWidth = this.contentElement.offsetWidth;
1505 this._offsetHeight = this.contentElement.offsetHeight;
1508 _onScroll: function()
1510 this._scrollTop = this._vScrollElement.scrollTop;
1511 this.scheduleUpdate();
1514 scheduleUpdate: function()
1516 if (this._updateTimerId || this._cancelWindowTimesAnimation)
1518 this._updateTimerId = this.element.window().requestAnimationFrame(this.update.bind(this));
1523 this._updateTimerId = 0;
1524 if (!this._timelineData())
1526 this._resetCanvas();
1527 this._updateBoundaries();
1528 this._calculator._updateBoundaries(this);
1529 this._draw(this._offsetWidth, this._offsetHeight);
1534 this._vScrollElement.scrollTop = 0;
1535 this._highlightedMarkerIndex = -1;
1536 this._highlightedEntryIndex = -1;
1537 this._selectedEntryIndex = -1;
1538 this._textWidth = {};
1542 _enabled: function()
1544 return this._rawTimelineDataLength !== 0;
1547 __proto__: WebInspector.HBox.prototype