Bug 1121194 - Support vertical panning for the flamegraph in the new performance tool, r=jsantell

This commit is contained in:
Victor Porof 2015-03-11 16:01:19 -04:00
parent b89ba90a7b
commit c6241e074b
6 changed files with 276 additions and 47 deletions

View File

@ -19,6 +19,7 @@ support-files =
[browser_flame-graph-02.js]
[browser_flame-graph-03a.js]
[browser_flame-graph-03b.js]
[browser_flame-graph-03c.js]
[browser_flame-graph-04.js]
[browser_flame-graph-utils-01.js]
[browser_flame-graph-utils-02.js]

View File

@ -24,6 +24,8 @@ function* performTest() {
let graph = new FlameGraph(doc.body, 1);
graph.fixedWidth = TEST_WIDTH;
graph.fixedHeight = TEST_HEIGHT;
graph.horizontalPanThreshold = 0;
graph.verticalPanThreshold = 0;
yield graph.ready();

View File

@ -0,0 +1,139 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that vertical panning in the flame graph widget works properly.
let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
let TEST_BOUNDS = { startTime: 0, endTime: 150 };
let TEST_WIDTH = 200;
let TEST_HEIGHT = 100;
let TEST_DPI_DENSITIY = 2;
let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
add_task(function*() {
yield promiseTab("about:blank");
yield performTest();
gBrowser.removeCurrentTab();
});
function* performTest() {
let [host, win, doc] = yield createHost();
doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
graph.fixedWidth = TEST_WIDTH;
graph.fixedHeight = TEST_HEIGHT;
yield graph.ready();
testGraph(graph);
yield graph.destroy();
host.destroy();
}
function testGraph(graph) {
graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
// Drag up vertically only.
dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (1).");
is(graph.getViewRange().endTime | 0, 150,
"The selection end boundary is correct (1).");
is(graph.getViewRange().verticalOffset | 0, 0,
"The vertical offset is correct (1).");
hover(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 - 50);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (2).");
is(graph.getViewRange().endTime | 0, 150,
"The selection end boundary is correct (2).");
is(graph.getViewRange().verticalOffset | 0, 17,
"The vertical offset is correct (2).");
dragStop(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 - 100);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (3).");
is(graph.getViewRange().endTime | 0, 150,
"The selection end boundary is correct (3).");
is(graph.getViewRange().verticalOffset | 0, 42,
"The vertical offset is correct (3).");
// Drag down strongly vertically and slightly horizontally.
dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (4).");
is(graph.getViewRange().endTime | 0, 150,
"The selection end boundary is correct (4).");
is(graph.getViewRange().verticalOffset | 0, 42,
"The vertical offset is correct (4).");
hover(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 + 50);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (5).");
is(graph.getViewRange().endTime | 0, 150,
"The selection end boundary is correct (5).");
is(graph.getViewRange().verticalOffset | 0, 25,
"The vertical offset is correct (5).");
dragStop(graph, TEST_WIDTH / 2 + 100, TEST_HEIGHT / 2 + 500);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (6).");
is(graph.getViewRange().endTime | 0, 150,
"The selection end boundary is correct (6).");
is(graph.getViewRange().verticalOffset | 0, 0,
"The vertical offset is correct (6).");
// Drag up slightly vertically and strongly horizontally.
dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (7).");
is(graph.getViewRange().endTime | 0, 150,
"The selection end boundary is correct (7).");
is(graph.getViewRange().verticalOffset | 0, 0,
"The vertical offset is correct (7).");
hover(graph, TEST_WIDTH / 2 + 50, TEST_HEIGHT / 2);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (8).");
is(graph.getViewRange().endTime | 0, 116,
"The selection end boundary is correct (8).");
is(graph.getViewRange().verticalOffset | 0, 0,
"The vertical offset is correct (8).");
dragStop(graph, TEST_WIDTH / 2 + 500, TEST_HEIGHT / 2 + 100);
is(graph.getViewRange().startTime | 0, 0,
"The selection start boundary is correct (9).");
is(graph.getViewRange().endTime | 0, 0,
"The selection end boundary is correct (9).");
is(graph.getViewRange().verticalOffset | 0, 0,
"The vertical offset is correct (9).");
}
// EventUtils just doesn't work!
function hover(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ clientX: x, clientY: y });
}
function dragStart(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ clientX: x, clientY: y });
graph._onMouseDown({ clientX: x, clientY: y });
}
function dragStop(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ clientX: x, clientY: y });
graph._onMouseUp({ clientX: x, clientY: y });
}

View File

@ -26,11 +26,15 @@ const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
const GRAPH_MIN_SELECTION_WIDTH = 0.001; // ms
const GRAPH_HORIZONTAL_PAN_THRESHOLD = 10; // px
const GRAPH_VERTICAL_PAN_THRESHOLD = 30; // px
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const TIMELINE_TICKS_MULTIPLE = 5; // ms
const TIMELINE_TICKS_SPACING_MIN = 75; // px
const OVERVIEW_HEADER_HEIGHT = 16; // px
const OVERVIEW_HEADER_BACKGROUND = "rgba(255,255,255,0.7)";
const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
@ -121,9 +125,11 @@ function FlameGraph(parent, sharpness) {
this._height = canvas.height = bounds.height * this._pixelRatio;
this._ctx = canvas.getContext("2d");
this._bounds = new GraphSelection();
this._selection = new GraphSelection();
this._selectionDragger = new GraphSelectionDragger();
this._bounds = new GraphArea();
this._selection = new GraphArea();
this._selectionDragger = new GraphAreaDragger();
this._verticalOffset = 0;
this._verticalOffsetDragger = new GraphAreaDragger(0);
// Calculating text widths is necessary to trim the text inside the blocks
// while the scaling changes (e.g. via scrolling). This is very expensive,
@ -198,6 +204,8 @@ FlameGraph.prototype = {
this._bounds = null;
this._selection = null;
this._selectionDragger = null;
this._verticalOffset = null;
this._verticalOffsetDragger = null;
this._textWidthsCache = null;
this._data = null;
@ -208,6 +216,7 @@ FlameGraph.prototype = {
/**
* Rendering options. Subclasses should override these.
*/
overviewHeaderBackgroundColor: OVERVIEW_HEADER_BACKGROUND,
overviewHeaderTextColor: OVERVIEW_HEADER_TEXT_COLOR,
overviewTimelineStrokes: OVERVIEW_TIMELINE_STROKES,
blockTextColor: FLAME_GRAPH_BLOCK_TEXT_COLOR,
@ -219,6 +228,12 @@ FlameGraph.prototype = {
fixedWidth: null,
fixedHeight: null,
/**
* How much preliminar drag is necessary to determine the panning direction.
*/
horizontalPanThreshold: GRAPH_HORIZONTAL_PAN_THRESHOLD,
verticalPanThreshold: GRAPH_VERTICAL_PAN_THRESHOLD,
/**
* The units used in the overhead ticks. Could be "ms", for example.
* Overwrite this with your own localized format.
@ -278,12 +293,13 @@ FlameGraph.prototype = {
},
/**
* Sets the selection (i.e. the 'view range') bounds.
* Sets the selection and vertical offset (i.e. the 'view range').
* @return number
*/
setViewRange: function({ startTime, endTime }) {
setViewRange: function({ startTime, endTime }, verticalOffset = 0) {
this._selection.start = startTime * this._pixelRatio;
this._selection.end = endTime * this._pixelRatio;
this._verticalOffset = verticalOffset * this._pixelRatio;
this._shouldRedraw = true;
},
@ -299,13 +315,14 @@ FlameGraph.prototype = {
},
/**
* Gets the current selection (i.e. the 'view range').
* Gets the current selection and vertical offset (i.e. the 'view range').
* @return number
*/
getViewRange: function() {
return {
startTime: this._selection.start / this._pixelRatio,
endTime: this._selection.end / this._pixelRatio
endTime: this._selection.end / this._pixelRatio,
verticalOffset: this._verticalOffset / this._pixelRatio
};
},
@ -366,8 +383,8 @@ FlameGraph.prototype = {
let selection = this._selection;
let selectionWidth = selection.end - selection.start;
let selectionScale = canvasWidth / selectionWidth;
this._drawPyramid(this._data, this._verticalOffset, selection.start, selectionScale);
this._drawTicks(selection.start, selectionScale);
this._drawPyramid(this._data, selection.start, selectionScale);
this._shouldRedraw = false;
},
@ -385,6 +402,9 @@ FlameGraph.prototype = {
let canvasHeight = this._height;
let scaledOffset = dataOffset * dataScale;
ctx.fillStyle = this.overviewHeaderBackgroundColor;
ctx.fillRect(0, 0, canvasWidth, OVERVIEW_HEADER_HEIGHT * this._pixelRatio);
let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
@ -415,48 +435,50 @@ FlameGraph.prototype = {
*
* @param object dataSource
* The data source. See the constructor for more information.
* @param number verticalOffset
* Offsets the drawing vertically by the specified amount.
* @param number dataOffset, dataScale
* Offsets and scales the data source by the specified amount.
* This is used for scrolling the visualization.
*/
_drawPyramid: function(dataSource, dataOffset, dataScale) {
_drawPyramid: function(dataSource, verticalOffset, dataOffset, dataScale) {
let ctx = this._ctx;
let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
let visibleBlocks = this._drawPyramidFill(dataSource, dataOffset, dataScale);
let visibleBlocksInfo = this._drawPyramidFill(dataSource, verticalOffset, dataOffset, dataScale);
ctx.textBaseline = "middle";
ctx.font = fontSize + "px " + fontFamily;
ctx.fillStyle = this.blockTextColor;
this._drawPyramidText(visibleBlocks, dataOffset, dataScale);
this._drawPyramidText(visibleBlocksInfo, verticalOffset, dataOffset, dataScale);
},
/**
* Fills all block inside this graph's pyramid.
* @see FlameGraph.prototype._drawPyramid
*/
_drawPyramidFill: function(dataSource, dataOffset, dataScale) {
let visibleBlocksStore = [];
_drawPyramidFill: function(dataSource, verticalOffset, dataOffset, dataScale) {
let visibleBlocksInfoStore = [];
let minVisibleBlockWidth = this._overflowCharWidth;
for (let { color, blocks } of dataSource) {
this._drawBlocksFill(
color, blocks, dataOffset, dataScale,
visibleBlocksStore, minVisibleBlockWidth);
color, blocks, verticalOffset, dataOffset, dataScale,
visibleBlocksInfoStore, minVisibleBlockWidth);
}
return visibleBlocksStore;
return visibleBlocksInfoStore;
},
/**
* Adds the text for all block inside this graph's pyramid.
* @see FlameGraph.prototype._drawPyramid
*/
_drawPyramidText: function(blocks, dataOffset, dataScale) {
for (let block of blocks) {
this._drawBlockText(block, dataOffset, dataScale);
_drawPyramidText: function(blocksInfo, verticalOffset, dataOffset, dataScale) {
for (let { block, rect } of blocksInfo) {
this._drawBlockText(block, rect, verticalOffset, dataOffset, dataScale);
}
},
@ -468,19 +490,22 @@ FlameGraph.prototype = {
* @param array blocks
* A list of { x, y, width, height } objects visually representing
* all the blocks sharing this particular style.
* @param number verticalOffset
* Offsets the drawing vertically by the specified amount.
* @param number dataOffset, dataScale
* Offsets and scales the data source by the specified amount.
* This is used for scrolling the visualization.
* @param array visibleBlocksStore
* An array to store all the visible blocks into, after drawing them.
* @param array visibleBlocksInfoStore
* An array to store all the visible blocks into, along with the
* final baked coordinates and dimensions, after drawing them.
* The provided array will be populated.
* @param number minVisibleBlockWidth
* The minimum width of the blocks that will be added into
* the `visibleBlocksStore`.
* the `visibleBlocksInfoStore`.
*/
_drawBlocksFill: function(
color, blocks, dataOffset, dataScale,
visibleBlocksStore, minVisibleBlockWidth)
color, blocks, verticalOffset, dataOffset, dataScale,
visibleBlocksInfoStore, minVisibleBlockWidth)
{
let ctx = this._ctx;
let canvasWidth = this._width;
@ -493,13 +518,14 @@ FlameGraph.prototype = {
for (let block of blocks) {
let { x, y, width, height } = block;
let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
let rectTop = (y + OVERVIEW_HEADER_HEIGHT) * this._pixelRatio;
let rectTop = (y - verticalOffset + OVERVIEW_HEADER_HEIGHT) * this._pixelRatio;
let rectWidth = width * this._pixelRatio * dataScale;
let rectHeight = height * this._pixelRatio;
if (rectLeft > canvasWidth || // Too far right.
rectLeft < -rectWidth || // Too far left.
rectTop > canvasHeight) { // Too far bottom.
rectTop > canvasHeight || // Too far bottom.
rectTop < -rectHeight) { // Too far top.
continue;
}
@ -524,7 +550,10 @@ FlameGraph.prototype = {
// Populate the visible blocks store with this block if the width
// is longer than a given threshold.
if (rectWidth > minVisibleBlockWidth) {
visibleBlocksStore.push(block);
visibleBlocksInfoStore.push({
block: block,
rect: { rectLeft, rectTop, rectWidth, rectHeight }
});
}
}
@ -537,24 +566,29 @@ FlameGraph.prototype = {
* @param object block
* A single { x, y, width, height, text } object visually representing
* the block containing the text.
* @param object rect
* A single { rectLeft, rectTop, rectWidth, rectHeight } object
* representing the final baked coordinates of the drawn rectangle.
* Think of them as screen-space values, vs. object-space values. These
* differ from the scalars in `block` when the graph is scaled/panned.
* @param number verticalOffset
* Offsets the drawing vertically by the specified amount.
* @param number dataOffset, dataScale
* Offsets and scales the data source by the specified amount.
* This is used for scrolling the visualization.
*/
_drawBlockText: function(block, dataOffset, dataScale) {
_drawBlockText: function(block, rect, verticalOffset, dataOffset, dataScale) {
let ctx = this._ctx;
let scaledOffset = dataOffset * dataScale;
let { x, y, width, height, text } = block;
let { rectLeft, rectTop, rectWidth, rectHeight } = rect;
let paddingTop = FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP * this._pixelRatio;
let paddingLeft = FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT * this._pixelRatio;
let paddingRight = FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT * this._pixelRatio;
let totalHorizontalPadding = paddingLeft + paddingRight;
let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
let rectWidth = width * this._pixelRatio * dataScale;
// Clamp the blocks position to start at 0. Avoid negative X coords,
// to properly place the text inside the blocks.
if (rectLeft < 0) {
@ -563,7 +597,7 @@ FlameGraph.prototype = {
}
let textLeft = rectLeft + paddingLeft;
let textTop = (y + height / 2 + OVERVIEW_HEADER_HEIGHT) * this._pixelRatio + paddingTop;
let textTop = rectTop + rectHeight / 2 + paddingTop;
let textAvailableWidth = rectWidth - totalHorizontalPadding;
// Massage the text to fit inside a given width. This clamps the string
@ -672,6 +706,7 @@ FlameGraph.prototype = {
_onMouseMove: function(e) {
let offset = this._getContainerOffset();
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
let mouseY = (e.clientY - offset.top) * this._pixelRatio;
let canvasWidth = this._width;
let canvasHeight = this._height;
@ -680,14 +715,41 @@ FlameGraph.prototype = {
let selectionWidth = selection.end - selection.start;
let selectionScale = canvasWidth / selectionWidth;
let dragger = this._selectionDragger;
if (dragger.origin != null) {
selection.start = dragger.anchor.start + (dragger.origin - mouseX) / selectionScale;
selection.end = dragger.anchor.end + (dragger.origin - mouseX) / selectionScale;
let horizDrag = this._selectionDragger;
let vertDrag = this._verticalOffsetDragger;
// Avoid dragging both horizontally and vertically at the same time,
// as this doesn't feel natural. Based on a minimum distance, enable either
// one, and remember the drag direction to offset the mouse coords later.
if (!this._horizontalDragEnabled && !this._verticalDragEnabled) {
let horizDiff = Math.abs(horizDrag.origin - mouseX);
if (horizDiff > this.horizontalPanThreshold) {
this._horizontalDragDirection = Math.sign(horizDrag.origin - mouseX);
this._horizontalDragEnabled = true;
}
let vertDiff = Math.abs(vertDrag.origin - mouseY);
if (vertDiff > this.verticalPanThreshold) {
this._verticalDragDirection = Math.sign(vertDrag.origin - mouseY);
this._verticalDragEnabled = true;
}
}
if (horizDrag.origin != null && this._horizontalDragEnabled) {
let relativeX = mouseX + this._horizontalDragDirection * this.horizontalPanThreshold;
selection.start = horizDrag.anchor.start + (horizDrag.origin - relativeX) / selectionScale;
selection.end = horizDrag.anchor.end + (horizDrag.origin - relativeX) / selectionScale;
this._normalizeSelectionBounds();
this._shouldRedraw = true;
this.emit("selecting");
}
if (vertDrag.origin != null && this._verticalDragEnabled) {
let relativeY = mouseY + this._verticalDragDirection * this.verticalPanThreshold;
this._verticalOffset = vertDrag.anchor + (vertDrag.origin - relativeY) / this._pixelRatio;
this._normalizeVerticalOffset();
this._shouldRedraw = true;
this.emit("panning-vertically");
}
},
/**
@ -696,11 +758,19 @@ FlameGraph.prototype = {
_onMouseDown: function(e) {
let offset = this._getContainerOffset();
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
let mouseY = (e.clientY - offset.top) * this._pixelRatio;
this._selectionDragger.origin = mouseX;
this._selectionDragger.anchor.start = this._selection.start;
this._selectionDragger.anchor.end = this._selection.end;
this._canvas.setAttribute("input", "adjusting-selection-boundary");
this._verticalOffsetDragger.origin = mouseY;
this._verticalOffsetDragger.anchor = this._verticalOffset;
this._horizontalDragEnabled = false;
this._verticalDragEnabled = false;
this._canvas.setAttribute("input", "adjusting-view-area");
},
/**
@ -708,6 +778,11 @@ FlameGraph.prototype = {
*/
_onMouseUp: function() {
this._selectionDragger.origin = null;
this._verticalOffsetDragger.origin = null;
this._horizontalDragEnabled = false;
this._horizontalDragDirection = 0;
this._verticalDragEnabled = false;
this._verticalDragDirection = 0;
this._canvas.removeAttribute("input");
},
@ -782,6 +857,14 @@ FlameGraph.prototype = {
this._selection.end = selectionEnd;
},
/**
* Makes sure that the current vertical offset is within the allowed
* panning range.
*/
_normalizeVerticalOffset: function() {
this._verticalOffset = Math.max(this._verticalOffset, 0);
},
/**
*
* Finds the optimal tick interval between time markers in this graph.

View File

@ -12,9 +12,9 @@ const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.
this.EXPORTED_SYMBOLS = [
"GraphCursor",
"GraphSelection",
"GraphSelectionDragger",
"GraphSelectionResizer",
"GraphArea",
"GraphAreaDragger",
"GraphAreaResizer",
"AbstractCanvasGraph",
"LineGraphWidget",
"BarGraphWidget",
@ -101,17 +101,17 @@ this.GraphCursor = function() {
this.y = null;
};
this.GraphSelection = function() {
this.GraphArea = function() {
this.start = null;
this.end = null;
};
this.GraphSelectionDragger = function() {
this.GraphAreaDragger = function(anchor = new GraphArea()) {
this.origin = null;
this.anchor = new GraphSelection();
this.anchor = anchor;
};
this.GraphSelectionResizer = function() {
this.GraphAreaResizer = function() {
this.margin = null;
};
@ -178,9 +178,9 @@ this.AbstractCanvasGraph = function(parent, name, sharpness) {
this._ctx.mozImageSmoothingEnabled = false;
this._cursor = new GraphCursor();
this._selection = new GraphSelection();
this._selectionDragger = new GraphSelectionDragger();
this._selectionResizer = new GraphSelectionResizer();
this._selection = new GraphArea();
this._selectionDragger = new GraphAreaDragger();
this._selectionResizer = new GraphAreaResizer();
this._onAnimationFrame = this._onAnimationFrame.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);

View File

@ -864,6 +864,10 @@
cursor: col-resize;
}
.graph-widget-canvas[input=adjusting-view-area] {
cursor: grabbing;
}
.graph-widget-canvas[input=hovering-selection-contents] {
cursor: grab;
}