-/*
+/**
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
*/
/**
+ * @interface
+ */
+WebInspector.FlameChartDelegate = function() { }
+
+WebInspector.FlameChartDelegate.prototype = {
+ /**
+ * @param {number} startTime
+ * @param {number} endTime
+ */
+ requestWindowTimes: function(startTime, endTime) { },
+}
+
+/**
* @constructor
- * @extends {WebInspector.View}
- * @param {WebInspector.CPUProfileView} cpuProfileView
+ * @extends {WebInspector.HBox}
+ * @param {!WebInspector.FlameChartDataProvider} dataProvider
+ * @param {!WebInspector.FlameChartDelegate} flameChartDelegate
+ * @param {boolean} isTopDown
+ * @param {boolean} timeBasedWindow
*/
-WebInspector.FlameChart = function(cpuProfileView)
+WebInspector.FlameChart = function(dataProvider, flameChartDelegate, isTopDown, timeBasedWindow)
{
- WebInspector.View.call(this);
- this.registerRequiredCSS("flameChart.css");
- this.element.className = "fill";
- this.element.id = "cpu-flame-chart";
-
- this._overviewContainer = this.element.createChild("div", "overview-container");
- this._overviewGrid = new WebInspector.OverviewGrid("flame-chart");
- this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas");
- this._overviewContainer.appendChild(this._overviewGrid.element);
- this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator();
- this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
-
- this._chartContainer = this.element.createChild("div", "chart-container");
- this._timelineGrid = new WebInspector.TimelineGrid();
- this._chartContainer.appendChild(this._timelineGrid.element);
+ WebInspector.HBox.call(this);
+ this.element.classList.add("flame-chart-main-pane");
+ this._flameChartDelegate = flameChartDelegate;
+ this._isTopDown = isTopDown;
+ this._timeBasedWindow = timeBasedWindow;
+
this._calculator = new WebInspector.FlameChart.Calculator();
- this._canvas = this._chartContainer.createChild("canvas");
+ this._canvas = this.element.createChild("canvas");
this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this));
- WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize");
+ this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
+ this._canvas.addEventListener("click", this._onClick.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._entryInfo = this._chartContainer.createChild("div", "entry-info");
+ this._entryInfo = this.element.createChild("div", "profile-entry-info");
+ this._highlightElement = this.element.createChild("div", "flame-chart-highlight-element");
+ this._selectedElement = this.element.createChild("div", "flame-chart-selected-element");
+
+ this._dataProvider = dataProvider;
- this._cpuProfileView = cpuProfileView;
this._windowLeft = 0.0;
this._windowRight = 1.0;
- this._barHeight = 15;
+ this._windowWidth = 1.0;
+ this._timeWindowLeft = 0;
+ this._timeWindowRight = Infinity;
+ this._barHeight = dataProvider.barHeight();
+ this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight;
this._minWidth = 1;
- this._paddingLeft = 15;
- this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
- this._canvas.addEventListener("click", this._onClick.bind(this), false);
- this._linkifier = new WebInspector.Linkifier();
+ this._paddingLeft = this._dataProvider.paddingLeft();
this._highlightedEntryIndex = -1;
-
- if (!WebInspector.FlameChart._colorGenerator)
- WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator();
+ this._selectedEntryIndex = -1;
+ this._textWidth = {};
}
+WebInspector.FlameChart.DividersBarHeight = 20;
+
/**
- * @constructor
- * @implements {WebInspector.TimelineGrid.Calculator}
+ * @interface
*/
-WebInspector.FlameChart.Calculator = function()
+WebInspector.FlameChartDataProvider = function()
{
}
-WebInspector.FlameChart.Calculator.prototype = {
+/** @typedef {!{
+ entryLevels: !Array.<number>,
+ entryTotalTimes: !Array.<number>,
+ entryOffsets: !Array.<number>
+ }}
+ */
+WebInspector.FlameChart.TimelineData;
+
+WebInspector.FlameChartDataProvider.prototype = {
/**
- * @param {WebInspector.FlameChart} flameChart
+ * @return {number}
*/
- _updateBoundaries: function(flameChart)
- {
- function log10(x)
- {
- return Math.log(x) / Math.LN10;
- }
- this._decimalDigits = Math.max(0, -Math.floor(log10(flameChart._timelineGrid.gridSliceTime * 1.01)));
- this._minimumBoundaries = flameChart._windowLeft * flameChart._timelineData.totalTime;
- this._maximumBoundaries = flameChart._windowRight * flameChart._timelineData.totalTime;
- this.paddingLeft = flameChart._paddingLeft;
- this._width = flameChart._canvas.width - this.paddingLeft;
- this._timeToPixel = this._width / this.boundarySpan();
- },
+ barHeight: function() { },
/**
- * @param {number} time
+ * @param {number} startTime
+ * @param {number} endTime
+ * @return {?Array.<number>}
*/
- computePosition: function(time)
- {
- return (time - this._minimumBoundaries) * this._timeToPixel + this.paddingLeft;
- },
+ dividerOffsets: function(startTime, endTime) { },
- formatTime: function(value)
- {
- var format = "%." + this._decimalDigits + "f\u2009ms";
- return WebInspector.UIString(format, value + this._minimumBoundaries);
- },
+ /**
+ * @return {number}
+ */
+ zeroTime: function() { },
- maximumBoundary: function()
- {
- return this._maximumBoundaries;
- },
+ /**
+ * @return {number}
+ */
+ totalTime: function() { },
- minimumBoundary: function()
- {
- return this._minimumBoundaries;
- },
+ /**
+ * @return {number}
+ */
+ maxStackDepth: function() { },
- zeroTime: function()
- {
- return 0;
- },
+ /**
+ * @return {?WebInspector.FlameChart.TimelineData}
+ */
+ timelineData: function() { },
- boundarySpan: function()
- {
- return this._maximumBoundaries - this._minimumBoundaries;
- }
+ /**
+ * @param {number} entryIndex
+ * @return {?Array.<!{title: string, text: string}>}
+ */
+ prepareHighlightedEntryInfo: function(entryIndex) { },
+
+ /**
+ * @param {number} entryIndex
+ * @return {boolean}
+ */
+ canJumpToEntry: function(entryIndex) { },
+
+ /**
+ * @param {number} entryIndex
+ * @return {?string}
+ */
+ entryTitle: function(entryIndex) { },
+
+ /**
+ * @param {number} entryIndex
+ * @return {?string}
+ */
+ entryFont: function(entryIndex) { },
+
+ /**
+ * @param {number} entryIndex
+ * @return {!string}
+ */
+ entryColor: function(entryIndex) { },
+
+ /**
+ * @param {number} entryIndex
+ * @param {!CanvasRenderingContext2D} context
+ * @param {?string} text
+ * @param {number} barX
+ * @param {number} barY
+ * @param {number} barWidth
+ * @param {number} barHeight
+ * @param {function(number):number} offsetToPosition
+ * @return {boolean}
+ */
+ decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, offsetToPosition) { },
+
+ /**
+ * @param {number} entryIndex
+ * @return {boolean}
+ */
+ forceDecoration: function(entryIndex) { },
+
+ /**
+ * @param {number} entryIndex
+ * @return {!string}
+ */
+ textColor: function(entryIndex) { },
+
+ /**
+ * @return {number}
+ */
+ textBaseline: function() { },
+
+ /**
+ * @return {number}
+ */
+ textPadding: function() { },
+
+ /**
+ * @return {?{startTimeOffset: number, endTimeOffset: number}}
+ */
+ highlightTimeRange: function(entryIndex) { },
+
+ /**
+ * @return {number}
+ */
+ paddingLeft: function() { }
+}
+
+WebInspector.FlameChart.Events = {
+ EntrySelected: "EntrySelected"
}
/**
* @constructor
* @implements {WebInspector.TimelineGrid.Calculator}
*/
-WebInspector.FlameChart.OverviewCalculator = function()
+WebInspector.FlameChart.Calculator = function()
{
+ this._paddingLeft = 0;
}
-WebInspector.FlameChart.OverviewCalculator.prototype = {
+WebInspector.FlameChart.Calculator.prototype = {
/**
- * @param {WebInspector.FlameChart} flameChart
+ * @return {number}
*/
- _updateBoundaries: function(flameChart)
+ paddingLeft: function()
{
- this._minimumBoundaries = 0;
- this._maximumBoundaries = flameChart._timelineData.totalTime;
- this._xScaleFactor = flameChart._canvas.width / flameChart._timelineData.totalTime;
+ return this._paddingLeft;
+ },
+
+ /**
+ * @param {!WebInspector.FlameChart} mainPane
+ */
+ _updateBoundaries: function(mainPane)
+ {
+ this._totalTime = mainPane._dataProvider.totalTime();
+ this._zeroTime = mainPane._dataProvider.zeroTime();
+ this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime;
+ this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime;
+ this._paddingLeft = mainPane._paddingLeft;
+ this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft;
+ this._timeToPixel = this._width / this.boundarySpan();
},
/**
* @param {number} time
+ * @return {number}
*/
computePosition: function(time)
{
- return (time - this._minimumBoundaries) * this._xScaleFactor;
+ return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft);
},
- formatTime: function(value)
+ /**
+ * @param {number} value
+ * @param {number=} precision
+ * @return {string}
+ */
+ formatTime: function(value, precision)
{
- return Number.secondsToString((value + this._minimumBoundaries) / 1000);
+ return Number.preciseMillisToString(value - this._zeroTime, precision);
},
+ /**
+ * @return {number}
+ */
maximumBoundary: function()
{
return this._maximumBoundaries;
},
+ /**
+ * @return {number}
+ */
minimumBoundary: function()
{
return this._minimumBoundaries;
},
+ /**
+ * @return {number}
+ */
zeroTime: function()
{
- return this._minimumBoundaries;
+ return this._zeroTime;
},
+ /**
+ * @return {number}
+ */
boundarySpan: function()
{
return this._maximumBoundaries - this._minimumBoundaries;
}
}
-WebInspector.FlameChart.Events = {
- SelectedNode: "SelectedNode"
-}
-
-/**
- * @constructor
- */
-WebInspector.FlameChart.ColorGenerator = function()
-{
- this._colorPairs = {};
- this._colorIndexes = [];
- this._currentColorIndex = 0;
- this._colorPairForID("(idle)::0", 50);
- this._colorPairForID("(program)::0", 50);
- this._colorPairForID("(garbage collector)::0", 50);
-}
-
-WebInspector.FlameChart.ColorGenerator.prototype = {
- /**
- * @param {!string} id
- * @param {number=} sat
- */
- _colorPairForID: function(id, sat)
+WebInspector.FlameChart.prototype = {
+ _resetCanvas: function()
{
- if (typeof sat !== "number")
- sat = 100;
- var colorPairs = this._colorPairs;
- var colorPair = colorPairs[id];
- if (!colorPair) {
- colorPairs[id] = colorPair = this._createPair(this._currentColorIndex++, sat);
- this._colorIndexes[colorPair.index] = colorPair;
- }
- return colorPair;
+ var ratio = window.devicePixelRatio;
+ this._canvas.width = this._offsetWidth * ratio;
+ this._canvas.height = this._offsetHeight * ratio;
},
/**
- * @param {!number} index
+ * @return {?WebInspector.FlameChart.TimelineData}
*/
- _colorPairForIndex: function(index)
+ _timelineData: function()
{
- return this._colorIndexes[index];
+ return this._dataProvider.timelineData();
},
/**
- * @param {!number} index
- * @param {!number} sat
+ * @param {!number} windowLeft
+ * @param {!number} windowRight
*/
- _createPair: function(index, sat)
+ changeWindow: function(windowLeft, windowRight)
{
- var hue = (index * 7 + 12 * (index % 2)) % 360;
- return {index: index, highlighted: "hsla(" + hue + ", " + sat + "%, 33%, 0.7)", normal: "hsla(" + hue + ", " + sat + "%, 66%, 0.7)"}
- }
-}
+ console.assert(!this._timeBasedWindow);
+ this._windowLeft = windowLeft;
+ this._windowRight = windowRight;
+ this._windowWidth = this._windowRight - this._windowLeft;
-/**
- * @constructor
- * @param {!Object} colorPair
- * @param {!number} depth
- * @param {!number} duration
- * @param {!number} startTime
- * @param {Object} node
- */
-WebInspector.FlameChart.Entry = function(colorPair, depth, duration, startTime, node)
-{
- this.colorPair = colorPair;
- this.depth = depth;
- this.duration = duration;
- this.startTime = startTime;
- this.node = node;
- this.selfTime = 0;
-}
+ this._scheduleUpdate();
+ },
-WebInspector.FlameChart.prototype = {
/**
- * @param {!number} timeLeft
- * @param {!number} timeRight
+ * @param {number} startTime
+ * @param {number} endTime
*/
- selectRange: function(timeLeft, timeRight)
- {
- this._overviewGrid.setWindow(timeLeft / this._totalTime, timeRight / this._totalTime);
- },
-
- _onWindowChanged: function(event)
+ setWindowTimes: function(startTime, endTime)
{
+ console.assert(this._timeBasedWindow);
+ this._timeWindowLeft = startTime;
+ this._timeWindowRight = endTime;
this._scheduleUpdate();
},
+ /**
+ * @param {!MouseEvent} event
+ */
_startCanvasDragging: function(event)
{
- if (!this._timelineData)
+ if (!this._timelineData())
return false;
this._isDragging = true;
- this._wasDragged = false;
- this._dragStartPoint = event.pageX;
- this._dragStartWindowLeft = this._windowLeft;
- this._dragStartWindowRight = this._windowRight;
+ this._maxDragOffset = 0;
+ this._dragStartPointX = event.pageX;
+ this._dragStartPointY = event.pageY;
+ this._dragStartScrollTop = this._vScrollElement.scrollTop;
+ this._dragStartWindowLeft = this._timeWindowLeft;
+ this._dragStartWindowRight = this._timeWindowRight;
+ this._canvas.style.cursor = "";
return true;
},
+ /**
+ * @param {!MouseEvent} event
+ */
_canvasDragging: function(event)
{
- var pixelShift = this._dragStartPoint - event.pageX;
+ var pixelShift = this._dragStartPointX - event.pageX;
+ var pixelScroll = this._dragStartPointY - event.pageY;
+ this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
var windowShift = pixelShift / this._totalPixels;
-
- var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift);
- if (windowLeft === this._windowLeft)
- return;
- windowShift = windowLeft - this._dragStartWindowLeft;
-
- var windowRight = Math.min(1, this._dragStartWindowRight + windowShift);
- if (windowRight === this._windowRight)
- return;
- windowShift = windowRight - this._dragStartWindowRight;
- this._overviewGrid.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift);
- this._wasDragged = true;
+ var windowTime = this._windowWidth * this._totalTime;
+ var timeShift = windowTime * pixelShift / this._pixelWindowWidth;
+ timeShift = Number.constrain(
+ timeShift,
+ this._zeroTime - this._dragStartWindowLeft,
+ this._zeroTime + 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));
},
_endCanvasDragging: function()
this._isDragging = false;
},
- _calculateTimelineData: function()
- {
- if (this._timelineData)
- return this._timelineData;
-
- if (!this._cpuProfileView.profileHead)
- return null;
-
- var samples = this._cpuProfileView.samples;
- var idToNode = this._cpuProfileView._idToNode;
- var gcNode = this._cpuProfileView._gcNode;
- var samplesCount = samples.length;
- var samplingInterval = this._cpuProfileView.samplingIntervalMs;
-
- var index = 0;
- var entries = /** @type {Array.<!WebInspector.FlameChart.Entry>} */ ([]);
-
- var openIntervals = [];
- var stackTrace = [];
- var colorGenerator = WebInspector.FlameChart._colorGenerator;
- var colorEntryIndexes = [];
- var maxDepth = 5; // minimum stack depth for the case when we see no activity.
- var depth = 0;
-
- for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) {
- var node = idToNode[samples[sampleIndex]];
- stackTrace.length = 0;
- while (node) {
- stackTrace.push(node);
- node = node.parent;
- }
- stackTrace.pop(); // Remove (root) node
-
- maxDepth = Math.max(maxDepth, depth);
- depth = 0;
- node = stackTrace.pop();
- var intervalIndex;
-
- // GC samples have no stack, so we just put GC node on top of the last recoreded sample.
- if (node === gcNode) {
- while (depth < openIntervals.length) {
- intervalIndex = openIntervals[depth].index;
- entries[intervalIndex].duration += samplingInterval;
- ++depth;
- }
- // If previous stack is also GC then just continue.
- if (openIntervals.length > 0 && openIntervals.peekLast().node === node) {
- entries[intervalIndex].selfTime += samplingInterval;
- continue;
- }
- }
-
- while (node && depth < openIntervals.length && node === openIntervals[depth].node) {
- intervalIndex = openIntervals[depth].index;
- entries[intervalIndex].duration += samplingInterval;
- node = stackTrace.pop();
- ++depth;
- }
- if (depth < openIntervals.length)
- openIntervals.length = depth;
- if (!node) {
- entries[intervalIndex].selfTime += samplingInterval;
- continue;
- }
-
- while (node) {
- var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
- var indexesForColor = colorEntryIndexes[colorPair.index];
- if (!indexesForColor)
- indexesForColor = colorEntryIndexes[colorPair.index] = [];
-
- var entry = new WebInspector.FlameChart.Entry(colorPair, depth, samplingInterval, sampleIndex * samplingInterval, node);
- indexesForColor.push(entries.length);
- entries.push(entry);
- openIntervals.push({node: node, index: index});
- ++index;
-
- node = stackTrace.pop();
- ++depth;
- }
- entries[entries.length - 1].selfTime += samplingInterval;
- }
-
- this._maxStackDepth = Math.max(maxDepth, depth);
-
- var entryColorIndexes = new Uint16Array(entries.length);
- var entryLevels = new Uint8Array(entries.length);
- var entryTotalTimes = new Float32Array(entries.length);
- var entryOffsets = new Float32Array(entries.length);
- var entryTitles = new Array(entries.length);
- var entryDeoptFlags = new Uint8Array(entries.length);
-
- for (var i = 0; i < entries.length; ++i) {
- var entry = entries[i];
- entryColorIndexes[i] = colorPair.index;
- entryLevels[i] = entry.depth;
- entryTotalTimes[i] = entry.duration;
- entryOffsets[i] = entry.startTime;
- entryTitles[i] = entry.node.functionName;
- var reason = entry.node.deoptReason;
- entryDeoptFlags[i] = (reason && reason !== "no reason");
- }
-
- this._timelineData = {
- entries: entries,
- totalTime: this._cpuProfileView.profileHead.totalTime,
- entryColorIndexes: entryColorIndexes,
- entryLevels: entryLevels,
- entryTotalTimes: entryTotalTimes,
- entryOffsets: entryOffsets,
- colorEntryIndexes: colorEntryIndexes,
- entryTitles: entryTitles,
- entryDeoptFlags: entryDeoptFlags
- };
-
- return this._timelineData;
- },
-
+ /**
+ * @param {?MouseEvent} event
+ */
_onMouseMove: function(event)
{
if (this._isDragging)
return;
-
var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);
if (this._highlightedEntryIndex === entryIndex)
return;
- if (entryIndex === -1 || this._timelineData.entries[entryIndex].node.scriptId === "0")
+ if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
this._canvas.style.cursor = "default";
else
this._canvas.style.cursor = "pointer";
this._highlightedEntryIndex = entryIndex;
- this._scheduleUpdate();
- },
- _millisecondsToString: function(ms)
- {
- if (ms === 0)
- return "0";
- if (ms < 1000)
- return WebInspector.UIString("%.1f\u2009ms", ms);
- return Number.secondsToString(ms / 1000, true);
- },
+ this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
+ this._entryInfo.removeChildren();
- _prepareHighlightedEntryInfo: function()
- {
- if (this._isDragging)
- return null;
- var entry = this._timelineData.entries[this._highlightedEntryIndex];
- if (!entry)
- return null;
- var node = entry.node;
- if (!node)
- return null;
-
- var entryInfo = [];
- function pushEntryInfoRow(title, text)
- {
- var row = {};
- row.title = title;
- row.text = text;
- entryInfo.push(row);
- }
+ if (this._highlightedEntryIndex === -1)
+ return;
- pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName);
- if (this._cpuProfileView.samples) {
- var selfTime = this._millisecondsToString(entry.selfTime);
- var totalTime = this._millisecondsToString(entry.duration);
- pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime);
- pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime);
+ if (!this._isDragging) {
+ var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex);
+ if (entryInfo)
+ this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
}
- if (node.url)
- pushEntryInfoRow(WebInspector.UIString("URL"), node.url + ":" + node.lineNumber);
- pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true));
- pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true));
- if (node.deoptReason && node.deoptReason !== "no reason")
- pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason);
-
- return entryInfo;
},
- _onClick: function(e)
+ _onClick: function()
{
// 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.
- if (this._wasDragged)
+ const clickThreshold = 5;
+ if (this._maxDragOffset > clickThreshold)
return;
if (this._highlightedEntryIndex === -1)
return;
- var node = this._timelineData.entries[this._highlightedEntryIndex].node;
- this.dispatchEventToListeners(WebInspector.FlameChart.Events.SelectedNode, node);
+ this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex);
},
+ /**
+ * @param {?MouseEvent} e
+ */
_onMouseWheel: function(e)
{
- if (e.wheelDeltaY) {
- const zoomFactor = 1.1;
- const mouseWheelZoomSpeed = 1 / 120;
+ var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.zeroTime();
+ var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.zeroTime() + this._dataProvider.totalTime();
- var zoom = Math.pow(zoomFactor, -e.wheelDeltaY * mouseWheelZoomSpeed);
- var overviewReference = (this._pixelWindowLeft + e.offsetX - this._paddingLeft) / this._totalPixels;
- this._overviewGrid.zoom(zoom, overviewReference);
+ if (e.wheelDeltaY) {
+ if (!e.altKey) {
+ const mouseWheelZoomSpeed = 1 / 120;
+ var zoom = Math.pow(1.2, -e.wheelDeltaY * mouseWheelZoomSpeed) - 1;
+ var cursorTime = this._cursorTime(e.offsetX);
+ windowLeft += (windowLeft - cursorTime) * zoom;
+ windowRight += (windowRight - cursorTime) * zoom;
+ } else {
+ this._vScrollElement.scrollTop -= e.wheelDeltaY / 120 * this._offsetHeight / 8;
+ }
} else {
- var shift = Number.constrain(-1 * this._windowWidth / 4 * e.wheelDeltaX / 120, -this._windowLeft, 1 - this._windowRight);
- this._overviewGrid.setWindow(this._windowLeft + shift, this._windowRight + shift);
+ var shift = e.wheelDeltaX * this._pixelToTime;
+ shift = Number.constrain(shift, this._zeroTime - windowLeft, this._totalTime + this._zeroTime - windowRight);
+ windowLeft += shift;
+ windowRight += shift;
}
+ windowLeft = Number.constrain(windowLeft, this._zeroTime, this._totalTime + this._zeroTime);
+ windowRight = Number.constrain(windowRight, this._zeroTime, this._totalTime + this._zeroTime);
+ this._flameChartDelegate.requestWindowTimes(windowLeft, windowRight);
+ },
+
+ /**
+ * @param {number} x
+ * @return {number}
+ */
+ _cursorTime: function(x)
+ {
+ return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._zeroTime;
},
/**
*/
_coordinatesToEntryIndex: function(x, y)
{
- var timelineData = this._timelineData;
+ y += this._scrollTop;
+ var timelineData = this._timelineData();
if (!timelineData)
return -1;
- var timelineEntries = timelineData.entries;
- var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime;
- var cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
-
- for (var i = 0; i < timelineEntries.length; ++i) {
- if (cursorTime < timelineEntries[i].startTime)
+ var cursorTimeOffset = this._cursorTime(x) - this._zeroTime;
+ var cursorLevel = this._isTopDown ? Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight) : Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);
+ var entryOffsets = timelineData.entryOffsets;
+ var entryTotalTimes = timelineData.entryTotalTimes;
+ var entryLevels = timelineData.entryLevels;
+ var length = entryOffsets.length;
+ for (var i = 0; i < length; ++i) {
+ var entryLevel = entryLevels[i];
+ if (cursorLevel !== entryLevel)
+ continue;
+ if (cursorTimeOffset < entryOffsets[i])
return -1;
- if (cursorTime < (timelineEntries[i].startTime + timelineEntries[i].duration)
- && cursorLevel === timelineEntries[i].depth)
+ if (cursorTimeOffset < (entryOffsets[i] + entryTotalTimes[i]))
return i;
}
return -1;
},
- onResize: function()
- {
- this._updateOverviewCanvas = true;
- this._scheduleUpdate();
- },
-
- _drawOverviewCanvas: function(width, height)
- {
- if (!this._timelineData)
- return;
-
- var timelineEntries = this._timelineData.entries;
-
- var drawData = new Uint8Array(width);
- var scaleFactor = width / this._totalTime;
-
- for (var entryIndex = 0; entryIndex < timelineEntries.length; ++entryIndex) {
- var entry = timelineEntries[entryIndex];
- var start = Math.floor(entry.startTime * scaleFactor);
- var finish = Math.floor((entry.startTime + entry.duration) * scaleFactor);
- for (var x = start; x < finish; ++x)
- drawData[x] = Math.max(drawData[x], entry.depth + 1);
- }
-
- var ratio = window.devicePixelRatio;
- var canvasWidth = width * ratio;
- var canvasHeight = height * ratio;
- this._overviewCanvas.width = canvasWidth;
- this._overviewCanvas.height = canvasHeight;
- this._overviewCanvas.style.width = width + "px";
- this._overviewCanvas.style.height = height + "px";
-
- var context = this._overviewCanvas.getContext("2d");
-
- var yScaleFactor = canvasHeight / (this._maxStackDepth * 1.1);
- context.lineWidth = 1;
- context.translate(0.5, 0.5);
- context.strokeStyle = "rgba(20,0,0,0.4)";
- context.fillStyle = "rgba(214,225,254,0.8)";
- context.moveTo(-1, canvasHeight - 1);
- if (drawData)
- context.lineTo(-1, Math.round(height - drawData[0] * yScaleFactor - 1));
- var value;
- for (var x = 0; x < width; ++x) {
- value = Math.round(canvasHeight - drawData[x] * yScaleFactor - 1);
- context.lineTo(x * ratio, value);
- }
- context.lineTo(canvasWidth + 1, value);
- context.lineTo(canvasWidth + 1, canvasHeight - 1);
- context.fill();
- context.stroke();
- context.closePath();
- },
-
/**
* @param {!number} height
* @param {!number} width
*/
draw: function(width, height)
{
- var timelineData = this._calculateTimelineData();
+ var timelineData = this._timelineData();
if (!timelineData)
return;
- var ratio = window.devicePixelRatio;
- this._canvas.width = width * ratio;
- this._canvas.height = height * ratio;
- this._canvas.style.width = width + "px";
- this._canvas.style.height = height + "px";
-
var context = this._canvas.getContext("2d");
+ context.save();
+ var ratio = window.devicePixelRatio;
context.scale(ratio, ratio);
- var timeWindowRight = this._timeWindowRight;
+
+ var timeWindowRight = this._timeWindowRight - this._zeroTime;
+ var timeWindowLeft = this._timeWindowLeft - this._zeroTime;
var timeToPixel = this._timeToPixel;
var pixelWindowLeft = this._pixelWindowLeft;
var paddingLeft = this._paddingLeft;
var minWidth = this._minWidth;
+ var entryTotalTimes = timelineData.entryTotalTimes;
+ var entryOffsets = timelineData.entryOffsets;
+ var entryLevels = timelineData.entryLevels;
- var entryTotalTimes = this._timelineData.entryTotalTimes;
- var entryOffsets = this._timelineData.entryOffsets;
- var entryLevels = this._timelineData.entryLevels;
- var colorEntryIndexes = this._timelineData.colorEntryIndexes;
- var entryTitles = this._timelineData.entryTitles;
- var entryDeoptFlags = this._timelineData.entryDeoptFlags;
-
- var colorGenerator = WebInspector.FlameChart._colorGenerator;
- var titleIndexes = new Uint32Array(this._timelineData.entryTotalTimes);
+ var titleIndexes = new Uint32Array(timelineData.entryTotalTimes);
var lastTitleIndex = 0;
- var dotsWidth = context.measureText("\u2026").width;
- var textPaddingLeft = 2;
- this._minTextWidth = context.measureText("\u2026").width + textPaddingLeft;
+ var textPadding = this._dataProvider.textPadding();
+ this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026");
var minTextWidth = this._minTextWidth;
- var marksField = [];
- for (var i = 0; i < this._maxStackDepth; ++i)
- marksField.push(new Uint16Array(width));
+ var lastDrawOffset = new Int32Array(this._dataProvider.maxStackDepth());
+ for (var i = 0; i < lastDrawOffset.length; ++i)
+ lastDrawOffset[i] = -1;
var barHeight = this._barHeight;
- var barX = 0;
- var barWidth = 0;
- var barRight = 0;
- var barLevel = 0;
- var bHeight = height - barHeight;
- context.strokeStyle = "black";
- var colorPair;
- var entryIndex = 0;
- var entryOffset = 0;
- for (var colorIndex = 0; colorIndex < colorEntryIndexes.length; ++colorIndex) {
- colorPair = colorGenerator._colorPairForIndex(colorIndex);
- context.fillStyle = colorPair.normal;
- var indexes = colorEntryIndexes[colorIndex];
- if (!indexes)
+
+ var offsetToPosition = this._offsetToPosition.bind(this);
+ var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline();
+ var colorBuckets = {};
+ var minVisibleBarLevel = Math.max(0, Math.floor((this._scrollTop - this._baseHeight) / barHeight));
+ var maxVisibleBarLevel = Math.min(this._dataProvider.maxStackDepth(), Math.ceil((height + this._scrollTop) / barHeight));
+ var visibleBarsCount = maxVisibleBarLevel - minVisibleBarLevel + 1;
+
+ context.translate(0, -this._scrollTop);
+ var levelsCompleted = 0;
+ var lastEntryOnLevelPainted = [];
+ for (var i = 0; i < visibleBarsCount; ++i)
+ lastEntryOnLevelPainted[i] = false;
+
+ for (var entryIndex = 0; levelsCompleted < visibleBarsCount && entryIndex < entryOffsets.length; ++entryIndex) {
+ // skip if it is not visible (top/bottom side)
+ var barLevel = entryLevels[entryIndex];
+ if (barLevel < minVisibleBarLevel || barLevel > maxVisibleBarLevel || lastEntryOnLevelPainted[barLevel - minVisibleBarLevel])
+ continue;
+
+ // stop if we reached right border in time (entries were ordered by start time).
+ var entryOffset = entryOffsets[entryIndex];
+ if (entryOffset > timeWindowRight) {
+ lastEntryOnLevelPainted[barLevel - minVisibleBarLevel] = true;
+ levelsCompleted++;
+ continue;
+ }
+
+ // skip if it is not visible (left side).
+ var entryOffsetRight = entryOffset + entryTotalTimes[entryIndex];
+ if (entryOffsetRight < timeWindowLeft)
continue;
+
+ var barRight = this._offsetToPosition(entryOffsetRight);
+ if (barRight <= lastDrawOffset[barLevel])
+ continue;
+ var barX = Math.max(this._offsetToPosition(entryOffset), lastDrawOffset[barLevel]);
+ lastDrawOffset[barLevel] = barRight;
+
+ var barWidth = barRight - barX;
+ var color = this._dataProvider.entryColor(entryIndex);
+ var bucket = colorBuckets[color];
+ if (!bucket) {
+ bucket = [];
+ colorBuckets[color] = bucket;
+ }
+ bucket.push(entryIndex);
+ }
+
+ var colors = Object.keys(colorBuckets);
+ // We don't use for in here because it couldn't be optimized.
+ for (var c = 0; c < colors.length; ++c) {
+ var color = colors[c];
+ context.fillStyle = color;
+ context.strokeStyle = color;
+ var indexes = colorBuckets[color];
+
+ // First fill the boxes.
context.beginPath();
- for (var i = 0; i < indexes.length; ++i) {
- entryIndex = indexes[i];
- entryOffset = entryOffsets[entryIndex];
- if (entryOffset > timeWindowRight)
- break;
- barX = Math.ceil(entryOffset * timeToPixel) - pixelWindowLeft + paddingLeft;
- barRight = Math.floor((entryOffset + entryTotalTimes[entryIndex]) * timeToPixel) - pixelWindowLeft + paddingLeft;
- if (barRight < 0)
- continue;
- barWidth = (barRight - barX) || minWidth;
- barLevel = entryLevels[entryIndex];
- var marksRow = marksField[barLevel];
- if (barWidth <= marksRow[barX])
- continue;
- marksRow[barX] = barWidth;
- if (entryIndex === this._highlightedEntryIndex) {
- context.fill();
- context.beginPath();
- context.fillStyle = colorPair.highlighted;
- }
- context.rect(barX, bHeight - barLevel * barHeight, barWidth, barHeight);
- if (entryIndex === this._highlightedEntryIndex) {
- context.fill();
- context.beginPath();
- context.fillStyle = colorPair.normal;
- }
- if (barWidth > minTextWidth)
+ for (i = 0; i < indexes.length; ++i) {
+ var entryIndex = indexes[i];
+ var entryOffset = entryOffsets[entryIndex];
+ var barX = this._offsetToPosition(entryOffset);
+ var barRight = this._offsetToPosition(entryOffset + entryTotalTimes[entryIndex]);
+ var barWidth = Math.max(barRight - barX, minWidth);
+ var barLevel = entryLevels[entryIndex];
+ var barY = this._levelToHeight(barLevel);
+ context.rect(barX, barY, barWidth, barHeight);
+ if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex))
titleIndexes[lastTitleIndex++] = entryIndex;
}
context.fill();
}
- var font = (barHeight - 4) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family");
- var boldFont = "bold " + font;
- var isBoldFontSelected = false;
- context.font = font;
context.textBaseline = "alphabetic";
- context.fillStyle = "#333";
- this._dotsWidth = context.measureText("\u2026").width;
- var textBaseHeight = bHeight + barHeight - 4;
for (var i = 0; i < lastTitleIndex; ++i) {
- entryIndex = titleIndexes[i];
- if (isBoldFontSelected) {
- if (!entryDeoptFlags[entryIndex]) {
- context.font = font;
- isBoldFontSelected = false;
- }
- } else {
- if (entryDeoptFlags[entryIndex]) {
- context.font = boldFont;
- isBoldFontSelected = true;
- }
- }
+ var entryIndex = titleIndexes[i];
+ var entryOffset = entryOffsets[entryIndex];
+ var barX = this._offsetToPosition(entryOffset);
+ var barRight = this._offsetToPosition(entryOffset + entryTotalTimes[entryIndex]);
+ var barWidth = Math.max(barRight - barX, minWidth);
+ var barLevel = entryLevels[entryIndex];
+ var barY = this._levelToHeight(barLevel);
+ var text = this._dataProvider.entryTitle(entryIndex);
+ if (text && text.length)
+ text = this._prepareText(context, text, barWidth - 2 * textPadding);
+
+ if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, offsetToPosition))
+ continue;
- entryOffset = entryOffsets[entryIndex];
- barX = Math.floor(entryOffset * timeToPixel) - pixelWindowLeft + paddingLeft;
- barRight = Math.ceil((entryOffset + entryTotalTimes[entryIndex]) * timeToPixel) - pixelWindowLeft + paddingLeft;
- barWidth = (barRight - barX) || minWidth;
- var xText = Math.max(0, barX);
- var widthText = barWidth - textPaddingLeft + barX - xText;
- var title = this._prepareText(context, entryTitles[entryIndex], widthText);
- if (title)
- context.fillText(title, xText + textPaddingLeft, textBaseHeight - entryLevels[entryIndex] * barHeight);
+ if (!text || !text.length)
+ continue;
+
+ context.font = this._dataProvider.entryFont(entryIndex);
+ context.fillStyle = this._dataProvider.textColor(entryIndex);
+ context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta);
}
+ context.restore();
- var entryInfo = this._prepareHighlightedEntryInfo();
- this._entryInfo.removeChildren();
- if (entryInfo)
- this._entryInfo.appendChild(this._buildEntryInfo(entryInfo));
+ var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary());
+ if (timelineData.entryOffsets.length)
+ WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets);
+
+ this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex);
+ this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
+ },
+
+ setSelectedEntry: function(entryIndex)
+ {
+ this._selectedEntryIndex = entryIndex;
+ this._updateElementPosition(this._selectedElement, this._selectedEntryIndex);
+ },
+
+ _updateElementPosition: function(element, entryIndex)
+ {
+ if (element.parentElement)
+ element.remove();
+ if (entryIndex === -1)
+ return;
+ var timeRange = this._dataProvider.highlightTimeRange(entryIndex);
+ if (!timeRange)
+ return;
+ var timelineData = this._timelineData();
+ var barX = this._offsetToPosition(timeRange.startTimeOffset);
+ var barRight = this._offsetToPosition(timeRange.endTimeOffset);
+ if (barRight === 0 || barX === this._canvas.width)
+ return;
+ var barWidth = Math.max(barRight - barX, this._minWidth);
+ var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop;
+ var style = element.style;
+ style.left = barX + "px";
+ style.top = barY + "px";
+ style.width = barWidth + "px";
+ style.height = this._barHeight + "px";
+ this.element.appendChild(element);
+ },
+
+ _offsetToPosition: function(offset)
+ {
+ var value = Math.floor(offset * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
+ return Math.min(this._canvas.width, Math.max(0, value));
+ },
+
+ _levelToHeight: function(level)
+ {
+ return this._baseHeight - level * this._barHeightDelta;
},
_buildEntryInfo: function(entryInfo)
_prepareText: function(context, title, maxSize)
{
- if (maxSize < this._dotsWidth)
- return null;
- var titleWidth = context.measureText(title).width;
+ var titleWidth = this._measureWidth(context, title);
if (maxSize > titleWidth)
return title;
- maxSize -= this._dotsWidth;
- var dotRegExp=/[\.\$]/g;
- var match = dotRegExp.exec(title);
- if (!match) {
- var visiblePartSize = maxSize / titleWidth;
- var newTextLength = Math.floor(title.length * visiblePartSize) + 1;
- var minTextLength = 4;
- if (newTextLength < minTextLength)
- return null;
- var substring;
- do {
- --newTextLength;
- substring = title.substring(0, newTextLength);
- } while (context.measureText(substring).width > maxSize);
- return title.substring(0, newTextLength) + "\u2026";
- }
- while (match) {
- var substring = title.substring(match.index + 1);
- var width = context.measureText(substring).width;
- if (maxSize > width)
- return "\u2026" + substring;
- match = dotRegExp.exec(title);
+
+ var l = 3;
+ var r = title.length;
+ while (l < r) {
+ var m = (l + r) >> 1;
+ if (this._measureWidth(context, title.trimMiddle(m)) < maxSize)
+ l = m + 1;
+ else
+ r = m;
}
- var i = 0;
- do {
- ++i;
- } while (context.measureText(title.substring(0, i)).width < maxSize);
- return title.substring(0, i - 1) + "\u2026";
+ title = title.trimMiddle(r - 1);
+ titleWidth = this._measureWidth(context, title);
+ if (titleWidth <= maxSize)
+ return title;
+ if (maxSize > this._measureWidth(context, "\u2026"))
+ return "\u2026";
+ return "";
},
- _scheduleUpdate: function()
+ /**
+ * @param {!CanvasRenderingContext2D} context
+ * @param {string} text
+ * @return {number}
+ */
+ _measureWidth: function(context, text)
{
- if (this._updateTimerId)
- return;
- this._updateTimerId = setTimeout(this.update.bind(this), 10);
+ if (text.length > 20)
+ return context.measureText(text).width;
+
+ var width = this._textWidth[text];
+ if (!width) {
+ width = context.measureText(text).width;
+ this._textWidth[text] = width;
+ }
+ return width;
},
_updateBoundaries: function()
{
- this._windowLeft = this._overviewGrid.windowLeft();
- this._windowRight = this._overviewGrid.windowRight();
- this._windowWidth = this._windowRight - this._windowLeft;
-
- this._totalTime = this._timelineData.totalTime;
- this._timeWindowLeft = this._windowLeft * this._totalTime;
- this._timeWindowRight = this._windowRight * this._totalTime;
+ this._totalTime = this._dataProvider.totalTime();
+ this._zeroTime = this._dataProvider.zeroTime();
+ if (this._timeBasedWindow) {
+ if (this._timeWindowRight !== Infinity) {
+ this._windowLeft = (this._timeWindowLeft - this._zeroTime) / this._totalTime;
+ this._windowRight = (this._timeWindowRight - this._zeroTime) / this._totalTime;
+ this._windowWidth = this._windowRight - this._windowLeft;
+ } else {
+ this._windowLeft = 0;
+ this._windowRight = 1;
+ this._windowWidth = 1;
+ }
+ } else {
+ this._timeWindowLeft = this._windowLeft * this._totalTime;
+ this._timeWindowRight = this._windowRight * this._totalTime;
+ }
- this._pixelWindowWidth = this._chartContainer.clientWidth;
+ this._pixelWindowWidth = this._offsetWidth - this._paddingLeft;
this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
this._timeToPixel = this._totalPixels / this._totalTime;
this._pixelToTime = this._totalTime / this._totalPixels;
this._paddingLeftTime = this._paddingLeft / this._timeToPixel;
+
+ this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight;
+
+ var totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth());
+ this._vScrollContent.style.height = totalHeight + "px";
+ this._scrollTop = this._vScrollElement.scrollTop;
+ },
+
+ onResize: function()
+ {
+ this._offsetWidth = this.element.offsetWidth - this._vScrollElement.offsetWidth;
+ this._offsetHeight = this.element.offsetHeight;
+ this._canvas.style.width = this._offsetWidth + "px";
+ this._canvas.style.height = this._offsetHeight + "px";
+ this._scheduleUpdate();
+ },
+
+ _scheduleUpdate: function()
+ {
+ if (this._updateTimerId)
+ return;
+ this._updateTimerId = requestAnimationFrame(this.update.bind(this));
},
update: function()
{
this._updateTimerId = 0;
- if (!this._timelineData)
- this._calculateTimelineData();
- if (!this._timelineData)
+ if (!this._timelineData())
return;
+ this._resetCanvas();
this._updateBoundaries();
- this.draw(this._chartContainer.clientWidth, this._chartContainer.clientHeight);
this._calculator._updateBoundaries(this);
- this._overviewCalculator._updateBoundaries(this);
- this._timelineGrid.element.style.width = this.element.clientWidth;
- this._timelineGrid.updateDividers(this._calculator);
- this._overviewGrid.updateDividers(this._overviewCalculator);
- if (this._updateOverviewCanvas) {
- this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20);
- this._updateOverviewCanvas = false;
- }
+ this.draw(this._offsetWidth, this._offsetHeight);
},
- __proto__: WebInspector.View.prototype
-};
+ reset: function()
+ {
+ this._highlightedEntryIndex = -1;
+ this._selectedEntryIndex = -1;
+ this._textWidth = {};
+ this.update();
+ },
+
+ __proto__: WebInspector.HBox.prototype
+}