this._calculator = new WebInspector.FlameChart.Calculator();
this._canvas = this.element.createChild("canvas");
+ this._canvas.tabIndex = 1;
+ this.setDefaultFocusedElement(this._canvas);
this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), false);
this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
this._canvas.addEventListener("click", this._onClick.bind(this), false);
+ this._canvas.addEventListener("keydown", this._onKeyDown.bind(this), false);
WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "move", null);
this._vScrollElement = this.element.createChild("div", "flame-chart-v-scroll");
this._vScrollContent = this._vScrollElement.createChild("div");
- this._vScrollElement.addEventListener("scroll", this._scheduleUpdate.bind(this), false);
+ this._vScrollElement.addEventListener("scroll", this.scheduleUpdate.bind(this), false);
this._entryInfo = this.element.createChild("div", "profile-entry-info");
+ this._markerHighlighElement = this.element.createChild("div", "flame-chart-marker-highlight-element");
this._highlightElement = this.element.createChild("div", "flame-chart-highlight-element");
this._selectedElement = this.element.createChild("div", "flame-chart-selected-element");
this._paddingLeft = this._dataProvider.paddingLeft();
this._markerPadding = 2;
this._markerRadius = this._barHeight / 2 - this._markerPadding;
+ this._highlightedMarkerIndex = -1;
this._highlightedEntryIndex = -1;
this._selectedEntryIndex = -1;
this._textWidth = {};
{
}
-/** @typedef {!{
- entryLevels: (!Array.<number>|!Uint8Array),
- entryTotalTimes: (!Array.<number>|!Float32Array),
- entryStartTimes: (!Array.<number>|!Float64Array)
- }}
+/**
+ * @constructor
+ * @param {!Array.<number>|!Uint8Array} entryLevels
+ * @param {!Array.<number>|!Float32Array} entryTotalTimes
+ * @param {!Array.<number>|!Float64Array} entryStartTimes
*/
-WebInspector.FlameChart.TimelineData;
+WebInspector.FlameChart.TimelineData = function(entryLevels, entryTotalTimes, entryStartTimes)
+{
+ this.entryLevels = entryLevels;
+ this.entryTotalTimes = entryTotalTimes;
+ this.entryStartTimes = entryStartTimes;
+ /** @type {!Array.<number>} */
+ this.markerTimestamps = [];
+}
WebInspector.FlameChartDataProvider.prototype = {
/**
dividerOffsets: function(startTime, endTime) { },
/**
+ * @param {number} index
+ * @return {string}
+ */
+ markerColor: function(index) { },
+
+ /**
+ * @param {number} index
+ * @return {string}
+ */
+ markerTitle: function(index) { },
+
+ /**
* @return {number}
*/
minimumBoundary: function() { },
return this._rawTimelineData;
},
+ _cancelAnimation: function()
+ {
+ if (this._cancelWindowTimesAnimation) {
+ this._timeWindowLeft = this._pendingAnimationTimeLeft;
+ this._timeWindowRight = this._pendingAnimationTimeRight;
+ this._cancelWindowTimesAnimation();
+ delete this._cancelWindowTimesAnimation;
+ }
+ },
+
/**
* @param {number} startTime
* @param {number} endTime
*/
setWindowTimes: function(startTime, endTime)
{
+ if (this._muteAnimation || this._timeWindowLeft === 0 || this._timeWindowRight === Infinity) {
+ // Initial setup.
+ this._timeWindowLeft = startTime;
+ this._timeWindowRight = endTime;
+ this.scheduleUpdate();
+ return;
+ }
+
+ this._cancelAnimation();
+ this._cancelWindowTimesAnimation = WebInspector.animateFunction(this._animateWindowTimes.bind(this),
+ [{from: this._timeWindowLeft, to: startTime}, {from: this._timeWindowRight, to: endTime}], 5,
+ this._animationCompleted.bind(this));
+ this._pendingAnimationTimeLeft = startTime;
+ this._pendingAnimationTimeRight = endTime;
+ },
+
+ /**
+ * @param {number} startTime
+ * @param {number} endTime
+ */
+ _animateWindowTimes: function(startTime, endTime)
+ {
this._timeWindowLeft = startTime;
this._timeWindowRight = endTime;
- this._scheduleUpdate();
+ this.update();
+ },
+
+ _animationCompleted: function()
+ {
+ delete this._cancelWindowTimesAnimation;
},
/**
_canvasDragging: function(event)
{
var pixelShift = this._dragStartPointX - event.pageX;
+ this._dragStartPointX = event.pageX;
+ this._muteAnimation = true;
+ this._handlePanGesture(pixelShift * this._pixelToTime);
+ this._muteAnimation = false;
+
var pixelScroll = this._dragStartPointY - event.pageY;
this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
- var windowShift = pixelShift / this._totalPixels;
- var windowTime = this._windowWidth * this._totalTime;
- var timeShift = windowTime * pixelShift / this._pixelWindowWidth;
- timeShift = Number.constrain(
- timeShift,
- this._minimumBoundary - this._dragStartWindowLeft,
- this._minimumBoundary + this._totalTime - this._dragStartWindowRight
- );
- var windowLeft = this._dragStartWindowLeft + timeShift;
- var windowRight = this._dragStartWindowRight + timeShift;
- this._flameChartDelegate.requestWindowTimes(windowLeft, windowRight);
this._maxDragOffset = Math.max(this._maxDragOffset, Math.abs(pixelShift));
},
},
/**
- * @param {?Event} event
+ * @param {!Event} event
*/
_onMouseMove: function(event)
{
+ this._lastMouseOffsetX = event.offsetX;
+
if (this._isDragging)
return;
+
+ var inDividersBar = event.offsetY < WebInspector.FlameChart.DividersBarHeight;
+ this._highlightedMarkerIndex = inDividersBar ? this._markerIndexAtPosition(event.offsetX) : -1;
+ this._updateMarkerHighlight();
+ if (inDividersBar)
+ return;
+
var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);
if (this._highlightedEntryIndex === entryIndex)
_onClick: function()
{
+ this.focus();
// onClick comes after dragStart and dragEnd events.
// So if there was drag (mouse move) in the middle of that events
// we skip the click. Otherwise we jump to the sources.
},
/**
- * @param {?Event} e
+ * @param {!Event} e
*/
_onMouseWheel: function(e)
{
- var scrollIsThere = this._totalHeight > this._offsetHeight;
- var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
- var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
-
+ // Pan vertically when shift down only.
+ var panVertically = e.shiftKey && (e.wheelDeltaY || Math.abs(e.wheelDeltaX) === 120);
var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey;
- var panVertically = scrollIsThere && ((e.wheelDeltaY && !e.shiftKey) || (Math.abs(e.wheelDeltaX) === 120 && !e.shiftKey));
if (panVertically) {
- this._vScrollElement.scrollTop -= e.wheelDeltaY / 120 * this._offsetHeight / 8;
+ this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 * this._offsetHeight / 8;
} else if (panHorizontally) {
var shift = -e.wheelDeltaX * this._pixelToTime;
- shift = Number.constrain(shift, this._minimumBoundary - windowLeft, this._totalTime + this._minimumBoundary - windowRight);
- windowLeft += shift;
- windowRight += shift;
+ this._muteAnimation = true;
+ this._handlePanGesture(shift);
+ this._muteAnimation = false;
} else { // Zoom.
const mouseWheelZoomSpeed = 1 / 120;
- var zoom = Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1;
- var cursorTime = this._cursorTime(e.offsetX);
- windowLeft += (windowLeft - cursorTime) * zoom;
- windowRight += (windowRight - cursorTime) * zoom;
+ this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1);
}
- windowLeft = Number.constrain(windowLeft, this._minimumBoundary, this._totalTime + this._minimumBoundary);
- windowRight = Number.constrain(windowRight, this._minimumBoundary, this._totalTime + this._minimumBoundary);
- this._flameChartDelegate.requestWindowTimes(windowLeft, windowRight);
// Block swipe gesture.
e.consume(true);
},
/**
+ * @param {!Event} e
+ */
+ _onKeyDown: function(e)
+ {
+ var zoomMultiplier = e.shiftKey ? 0.8 : 0.3;
+ var panMultiplier = e.shiftKey ? 320 : 80;
+ if (e.keyCode === "A".charCodeAt(0)) {
+ this._handlePanGesture(-panMultiplier * this._pixelToTime);
+ e.consume(true);
+ } else if (e.keyCode === "D".charCodeAt(0)) {
+ this._handlePanGesture(panMultiplier * this._pixelToTime);
+ e.consume(true);
+ } else if (e.keyCode === "W".charCodeAt(0)) {
+ this._handleZoomGesture(-zoomMultiplier);
+ e.consume(true);
+ } else if (e.keyCode === "S".charCodeAt(0)) {
+ this._handleZoomGesture(zoomMultiplier);
+ e.consume(true);
+ }
+ },
+
+ /**
+ * @param {number} zoom
+ */
+ _handleZoomGesture: function(zoom)
+ {
+ this._cancelAnimation();
+ var bounds = this._windowForGesture();
+ var cursorTime = this._cursorTime(this._lastMouseOffsetX);
+ bounds.left += (bounds.left - cursorTime) * zoom;
+ bounds.right += (bounds.right - cursorTime) * zoom;
+ this._requestWindowTimes(bounds);
+ },
+
+ /**
+ * @param {number} shift
+ */
+ _handlePanGesture: function(shift)
+ {
+ this._cancelAnimation();
+ var bounds = this._windowForGesture();
+ shift = Number.constrain(shift, this._minimumBoundary - bounds.left, this._totalTime + this._minimumBoundary - bounds.right);
+ bounds.left += shift;
+ bounds.right += shift;
+ this._requestWindowTimes(bounds);
+ },
+
+ /**
+ * @return {{left: number, right: number}}
+ */
+ _windowForGesture: function()
+ {
+ var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary();
+ var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime();
+ return {left: windowLeft, right: windowRight};
+ },
+
+ /**
+ * @param {{left: number, right: number}} bounds
+ */
+ _requestWindowTimes: function(bounds)
+ {
+ bounds.left = Number.constrain(bounds.left, this._minimumBoundary, this._totalTime + this._minimumBoundary);
+ bounds.right = Number.constrain(bounds.right, this._minimumBoundary, this._totalTime + this._minimumBoundary);
+ this._flameChartDelegate.requestWindowTimes(bounds.left, bounds.right);
+ },
+
+ /**
* @param {number} x
* @return {number}
*/
if (!entryIndexes || !entryIndexes.length)
return -1;
+ /**
+ * @param {number} time
+ * @param {number} entryIndex
+ * @return {number}
+ */
function comparator(time, entryIndex)
{
return time - entryStartTimes[entryIndex];
},
/**
+ * @param {number} x
+ * @return {number}
+ */
+ _markerIndexAtPosition: function(x)
+ {
+ var markers = this._timelineData().markerTimestamps;
+ if (!markers)
+ return -1;
+ var accurracyOffsetPx = 1;
+ var time = this._cursorTime(x);
+ var leftTime = this._cursorTime(x - accurracyOffsetPx);
+ var rightTime = this._cursorTime(x + accurracyOffsetPx);
+
+ /**
+ * @param {number} time
+ * @param {number} markerTimestamp
+ * @return {number}
+ */
+ function comparator(time, markerTimestamp)
+ {
+ return time - markerTimestamp;
+ }
+ var left = markers.lowerBound(leftTime, comparator);
+ var markerIndex = -1;
+ var distance = Infinity;
+ for (var i = left; i < markers.length && markers[i] < rightTime; i++) {
+ var nextDistance = Math.abs(markers[i] - time);
+ if (nextDistance < distance) {
+ markerIndex = i;
+ distance = nextDistance;
+ }
+ }
+ return markerIndex;
+ },
+
+ /**
* @param {number} height
* @param {number} width
*/
var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary());
WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets);
+ this._drawMarkers();
this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
+ this._updateMarkerHighlight();
+ },
+
+ _drawMarkers: function()
+ {
+ var markerTimestamps = this._timelineData().markerTimestamps;
+ /**
+ * @param {number} time
+ * @param {number} markerTimestamp
+ * @return {number}
+ */
+ function compare(time, markerTimestamp)
+ {
+ return time - markerTimestamp;
+ }
+ var left = markerTimestamps.lowerBound(this._calculator.minimumBoundary(), compare);
+ var rightBoundary = this._calculator.maximumBoundary();
+
+ var context = this._canvas.getContext("2d");
+ context.save();
+ var ratio = window.devicePixelRatio;
+ context.scale(ratio, ratio);
+ var height = WebInspector.FlameChart.DividersBarHeight - 1;
+ context.lineWidth = 2;
+ for (var i = left; i < markerTimestamps.length; i++) {
+ var timestamp = markerTimestamps[i];
+ if (timestamp > rightBoundary)
+ break;
+ var position = this._calculator.computePosition(timestamp);
+ context.strokeStyle = this._dataProvider.markerColor(i);
+ context.beginPath();
+ context.moveTo(position, 0);
+ context.lineTo(position, height);
+ context.stroke();
+ }
+ context.restore();
+ },
+
+ _updateMarkerHighlight: function()
+ {
+ var element = this._markerHighlighElement;
+ if (element.parentElement)
+ element.remove();
+ var markerIndex = this._highlightedMarkerIndex;
+ if (markerIndex === -1)
+ return;
+ var barX = this._timeToPosition(this._timelineData().markerTimestamps[markerIndex]);
+ element.title = this._dataProvider.markerTitle(markerIndex);
+ var style = element.style;
+ style.left = barX + "px";
+ style.backgroundColor = this._dataProvider.markerColor(markerIndex);
+ this.element.appendChild(element);
},
/**
_buildEntryInfo: function(entryInfo)
{
- var infoTable = document.createElement("table");
- infoTable.className = "info-table";
+ var infoTable = document.createElementWithClass("table", "info-table");
for (var i = 0; i < entryInfo.length; ++i) {
var row = infoTable.createChild("tr");
- var titleCell = row.createChild("td");
- titleCell.textContent = entryInfo[i].title;
- titleCell.className = "title";
- var textCell = row.createChild("td");
- textCell.textContent = entryInfo[i].text;
+ row.createChild("td", "title").textContent = entryInfo[i].title;
+ row.createChild("td").textContent = entryInfo[i].text;
}
return infoTable;
},
onResize: function()
{
this._updateScrollBar();
- this._scheduleUpdate();
+ this.scheduleUpdate();
},
_updateScrollBar: function()
this._offsetHeight = this.element.offsetHeight;
},
- _scheduleUpdate: function()
+ scheduleUpdate: function()
{
- if (this._updateTimerId)
+ if (this._updateTimerId || this._cancelWindowTimesAnimation)
return;
this._updateTimerId = requestAnimationFrame(this.update.bind(this));
},
reset: function()
{
+ this._highlightedMarkerIndex = -1;
this._highlightedEntryIndex = -1;
this._selectedEntryIndex = -1;
this._textWidth = {};