diff --git a/browser/devtools/shared/moz.build b/browser/devtools/shared/moz.build index 363aca73555d..00d09cbed485 100644 --- a/browser/devtools/shared/moz.build +++ b/browser/devtools/shared/moz.build @@ -22,7 +22,6 @@ EXTRA_JS_MODULES.devtools += [ 'widgets/AbstractTreeItem.jsm', 'widgets/BreadcrumbsWidget.jsm', 'widgets/Chart.jsm', - 'widgets/FlameGraph.jsm', 'widgets/Graphs.jsm', 'widgets/GraphsWorker.js', 'widgets/SideMenuWidget.jsm', diff --git a/browser/devtools/shared/test/browser.ini b/browser/devtools/shared/test/browser.ini index 561a1cceaac9..467967004ba0 100644 --- a/browser/devtools/shared/test/browser.ini +++ b/browser/devtools/shared/test/browser.ini @@ -15,10 +15,6 @@ support-files = [browser_cubic-bezier-01.js] [browser_cubic-bezier-02.js] [browser_cubic-bezier-03.js] -[browser_flame-graph-01.js] -[browser_flame-graph-02.js] -[browser_flame-graph-03.js] -[browser_flame-graph-04.js] [browser_graphs-01.js] [browser_graphs-02.js] [browser_graphs-03.js] diff --git a/browser/devtools/shared/test/browser_flame-graph-01.js b/browser/devtools/shared/test/browser_flame-graph-01.js deleted file mode 100644 index b982e45d590e..000000000000 --- a/browser/devtools/shared/test/browser_flame-graph-01.js +++ /dev/null @@ -1,60 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -// Tests that flame graph widget works properly. - -let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {}); -let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {}); -let {Promise} = devtools.require("resource://gre/modules/Promise.jsm"); -let {Hosts} = devtools.require("devtools/framework/toolbox-hosts"); - -let test = Task.async(function*() { - yield promiseTab("about:blank"); - yield performTest(); - gBrowser.removeCurrentTab(); - finish(); -}); - -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); - - let readyEventEmitted; - graph.once("ready", () => readyEventEmitted = true); - - yield graph.ready(); - ok(readyEventEmitted, "The 'ready' event should have been emitted"); - - testGraph(host, graph); - - graph.destroy(); - host.destroy(); -} - -function testGraph(host, graph) { - ok(graph._container.classList.contains("flame-graph-widget-container"), - "The correct graph container was created."); - ok(graph._canvas.classList.contains("flame-graph-widget-canvas"), - "The correct graph container was created."); - - let bounds = host.frame.getBoundingClientRect(); - - is(graph.width, bounds.width * window.devicePixelRatio, - "The graph has the correct width."); - is(graph.height, bounds.height * window.devicePixelRatio, - "The graph has the correct height."); - - ok(graph._selection.start === null, - "The graph's selection start value is initially null."); - ok(graph._selection.end === null, - "The graph's selection end value is initially null."); - - ok(graph._selectionDragger.origin === null, - "The graph's dragger origin value is initially null."); - ok(graph._selectionDragger.anchor.start === null, - "The graph's dragger anchor start value is initially null."); - ok(graph._selectionDragger.anchor.end === null, - "The graph's dragger anchor end value is initially null."); -} diff --git a/browser/devtools/shared/test/browser_flame-graph-02.js b/browser/devtools/shared/test/browser_flame-graph-02.js deleted file mode 100644 index 484e71486e42..000000000000 --- a/browser/devtools/shared/test/browser_flame-graph-02.js +++ /dev/null @@ -1,45 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -// Tests that flame graph widgets may have a fixed width or height. - -let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {}); -let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {}); -let {Promise} = devtools.require("resource://gre/modules/Promise.jsm"); -let {Hosts} = devtools.require("devtools/framework/toolbox-hosts"); - -let test = Task.async(function*() { - yield promiseTab("about:blank"); - yield performTest(); - gBrowser.removeCurrentTab(); - finish(); -}); - -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); - graph.fixedWidth = 200; - graph.fixedHeight = 100; - - yield graph.ready(); - testGraph(host, graph); - - graph.destroy(); - host.destroy(); -} - -function testGraph(host, graph) { - let bounds = host.frame.getBoundingClientRect(); - - isnot(graph.width, bounds.width * window.devicePixelRatio, - "The graph should not span all the parent node's width."); - isnot(graph.height, bounds.height * window.devicePixelRatio, - "The graph should not span all the parent node's height."); - - is(graph.width, graph.fixedWidth * window.devicePixelRatio, - "The graph has the correct width."); - is(graph.height, graph.fixedHeight * window.devicePixelRatio, - "The graph has the correct height."); -} diff --git a/browser/devtools/shared/test/browser_flame-graph-03.js b/browser/devtools/shared/test/browser_flame-graph-03.js deleted file mode 100644 index ae17aff46775..000000000000 --- a/browser/devtools/shared/test/browser_flame-graph-03.js +++ /dev/null @@ -1,122 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -// Tests that selections in the flame graph widget work 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_WIDTH = 200; -let TEST_HEIGHT = 100; - -let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {}); -let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {}); -let {Promise} = devtools.require("resource://gre/modules/Promise.jsm"); -let {Hosts} = devtools.require("devtools/framework/toolbox-hosts"); - -let test = Task.async(function*() { - yield promiseTab("about:blank"); - yield performTest(); - gBrowser.removeCurrentTab(); - finish(); -}); - -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, 1); - graph.fixedWidth = TEST_WIDTH; - graph.fixedHeight = TEST_HEIGHT; - - yield graph.ready(); - - testGraph(graph); - - graph.destroy(); - host.destroy(); -} - -function testGraph(graph) { - graph.setData(TEST_DATA); - - is(graph.getDataWindowStart(), 0, - "The selection start boundary is correct (1)."); - is(graph.getDataWindowEnd(), TEST_WIDTH, - "The selection end boundary is correct (1)."); - - scroll(graph, 200, HORIZONTAL_AXIS, 10); - is(graph.getDataWindowStart() | 0, 100, - "The selection start boundary is correct (2)."); - is(graph.getDataWindowEnd() | 0, 300, - "The selection end boundary is correct (2)."); - - scroll(graph, -200, HORIZONTAL_AXIS, 10); - is(graph.getDataWindowStart() | 0, 0, - "The selection start boundary is correct (3)."); - is(graph.getDataWindowEnd() | 0, 200, - "The selection end boundary is correct (3)."); - - scroll(graph, 200, VERTICAL_AXIS, TEST_WIDTH / 2); - is(graph.getDataWindowStart() | 0, 0, - "The selection start boundary is correct (4)."); - is(graph.getDataWindowEnd() | 0, 207, - "The selection end boundary is correct (4)."); - - scroll(graph, -200, VERTICAL_AXIS, TEST_WIDTH / 2); - is(graph.getDataWindowStart() | 0, 7, - "The selection start boundary is correct (5)."); - is(graph.getDataWindowEnd() | 0, 199, - "The selection end boundary is correct (5)."); - - dragStart(graph, TEST_WIDTH / 2); - is(graph.getDataWindowStart() | 0, 7, - "The selection start boundary is correct (6)."); - is(graph.getDataWindowEnd() | 0, 199, - "The selection end boundary is correct (6)."); - - hover(graph, TEST_WIDTH / 2 - 10); - is(graph.getDataWindowStart() | 0, 16, - "The selection start boundary is correct (7)."); - is(graph.getDataWindowEnd() | 0, 209, - "The selection end boundary is correct (7)."); - - dragStop(graph, 10); - is(graph.getDataWindowStart() | 0, 93, - "The selection start boundary is correct (8)."); - is(graph.getDataWindowEnd() | 0, 286, - "The selection end boundary is correct (8)."); -} - -// 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 }); -} - -let HORIZONTAL_AXIS = 1; -let VERTICAL_AXIS = 2; - -function scroll(graph, wheel, axis, x, y = 1) { - x /= window.devicePixelRatio; - y /= window.devicePixelRatio; - graph._onMouseMove({ clientX: x, clientY: y }); - graph._onMouseWheel({ clientX: x, clientY: y, axis, detail: wheel, axis, - HORIZONTAL_AXIS, - VERTICAL_AXIS - }); -} diff --git a/browser/devtools/shared/test/browser_flame-graph-04.js b/browser/devtools/shared/test/browser_flame-graph-04.js deleted file mode 100644 index 93db05bfbdb1..000000000000 --- a/browser/devtools/shared/test/browser_flame-graph-04.js +++ /dev/null @@ -1,88 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -// Tests that text metrics in the flame graph widget work properly. - -let HTML_NS = "http://www.w3.org/1999/xhtml"; -let FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 9; // px -let FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif"; -let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {}); -let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {}); -let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {}); -let {Promise} = devtools.require("resource://gre/modules/Promise.jsm"); -let {Hosts} = devtools.require("devtools/framework/toolbox-hosts"); - -let L10N = new ViewHelpers.L10N(); - -let test = Task.async(function*() { - yield promiseTab("about:blank"); - yield performTest(); - gBrowser.removeCurrentTab(); - finish(); -}); - -function* performTest() { - let [host, win, doc] = yield createHost(); - let graph = new FlameGraph(doc.body, 1); - yield graph.ready(); - - testGraph(graph); - - graph.destroy(); - host.destroy(); -} - -function testGraph(graph) { - is(graph._averageCharWidth, getAverageCharWidth(), - "The average char width was calculated correctly."); - is(graph._overflowCharWidth, getCharWidth(L10N.ellipsis), - "The ellipsis char width was calculated correctly."); - - is(graph._getTextWidthApprox("This text is maybe overflowing"), - getAverageCharWidth() * 30, - "The approximate width was calculated correctly."); - - is(graph._getFittedText("This text is maybe overflowing", 1000), - "This text is maybe overflowing", - "The fitted text for 1000px width is correct."); - - isnot(graph._getFittedText("This text is maybe overflowing", 100), - "This text is maybe overflowing", - "The fitted text for 100px width is correct (1)."); - - ok(graph._getFittedText("This text is maybe overflowing", 100) - .contains(L10N.ellipsis), - "The fitted text for 100px width is correct (2)."); - - is(graph._getFittedText("This text is maybe overflowing", 10), - L10N.ellipsis, - "The fitted text for 10px width is correct."); - - is(graph._getFittedText("This text is maybe overflowing", 1), - "", - "The fitted text for 1px width is correct."); -} - -function getAverageCharWidth() { - let letterWidthsSum = 0; - let start = 32; // space - let end = 123; // "z" - - for (let i = start; i < end; i++) { - let char = String.fromCharCode(i); - letterWidthsSum += getCharWidth(char); - } - - return letterWidthsSum / (end - start); -} - -function getCharWidth(char) { - let canvas = document.createElementNS(HTML_NS, "canvas"); - let ctx = canvas.getContext("2d"); - - let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE; - let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY; - ctx.font = fontSize + "px " + fontFamily; - - return ctx.measureText(char).width; -} diff --git a/browser/devtools/shared/widgets/FlameGraph.jsm b/browser/devtools/shared/widgets/FlameGraph.jsm deleted file mode 100644 index 3f1d08493d3f..000000000000 --- a/browser/devtools/shared/widgets/FlameGraph.jsm +++ /dev/null @@ -1,755 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -const Cu = Components.utils; - -Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); -Cu.import("resource:///modules/devtools/Graphs.jsm"); -const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; -const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); -const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {}); - -this.EXPORTED_SYMBOLS = [ - "FlameGraph", - "FlameGraphUtils" -]; - -const HTML_NS = "http://www.w3.org/1999/xhtml"; -const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml"; -const L10N = new ViewHelpers.L10N(); - -const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035; -const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5; -const GRAPH_MIN_SELECTION_WIDTH = 10; // ms - -const TIMELINE_TICKS_MULTIPLE = 5; // ms -const TIMELINE_TICKS_SPACING_MIN = 75; // px - -const OVERVIEW_HEADER_HEIGHT = 18; // px -const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px -const OVERVIEW_HEADER_TEXT_COLOR = "#18191a"; -const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px -const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif"; -const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px -const OVERVIEW_HEADER_TEXT_PADDING_TOP = 5; // px -const OVERVIEW_TIMELINE_STROKES = "#ddd"; - -const FLAME_GRAPH_BLOCK_BORDER = 1; // px -const FLAME_GRAPH_BLOCK_TEXT_COLOR = "#000"; -const FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 9; // px -const FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif"; -const FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP = 1; // px -const FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT = 3; // px -const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT = 3; // px - -/** - * A flamegraph visualization. This implementation is responsable only with - * drawing the graph, using a data source consisting of rectangles and - * their corresponding widths. - * - * Example usage: - * let graph = new FlameGraph(node); - * let src = FlameGraphUtils.createFlameGraphDataFromSamples(samples); - * graph.once("ready", () => { - * graph.setData(src); - * }); - * - * Data source format: - * [ - * { - * color: "string", - * blocks: [ - * { - * x: number, - * y: number, - * width: number, - * height: number, - * text: "string" - * }, - * ... - * ] - * }, - * { - * color: "string", - * blocks: [...] - * }, - * ... - * { - * color: "string", - * blocks: [...] - * } - * ] - * - * Use `FlameGraphUtils` to convert profiler data (or any other data source) - * into a drawable format. - * - * @param nsIDOMNode parent - * The parent node holding the graph. - * @param number sharpness [optional] - * Defaults to the current device pixel ratio. - */ -function FlameGraph(parent, sharpness) { - EventEmitter.decorate(this); - - this._parent = parent; - this._ready = promise.defer(); - - AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => { - this._iframe = iframe; - this._window = iframe.contentWindow; - this._document = iframe.contentDocument; - this._pixelRatio = sharpness || this._window.devicePixelRatio; - - let container = this._container = this._document.getElementById("graph-container"); - container.className = "flame-graph-widget-container graph-widget-container"; - - let canvas = this._canvas = this._document.getElementById("graph-canvas"); - canvas.className = "flame-graph-widget-canvas graph-widget-canvas"; - - let bounds = parent.getBoundingClientRect(); - bounds.width = this.fixedWidth || bounds.width; - bounds.height = this.fixedHeight || bounds.height; - iframe.setAttribute("width", bounds.width); - iframe.setAttribute("height", bounds.height); - - this._width = canvas.width = bounds.width * this._pixelRatio; - this._height = canvas.height = bounds.height * this._pixelRatio; - this._ctx = canvas.getContext("2d"); - - this._selection = new GraphSelection(); - this._selectionDragger = new GraphSelectionDragger(); - - // Calculating text widths is necessary to trim the text inside the blocks - // while the scaling changes (e.g. via scrolling). This is very expensive, - // so maintain a cache of string contents to text widths. - this._textWidthsCache = {}; - - let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio; - let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY; - this._ctx.font = fontSize + "px " + fontFamily; - this._averageCharWidth = this._calcAverageCharWidth(); - this._overflowCharWidth = this._getTextWidth(this.overflowChar); - - this._onMouseMove = this._onMouseMove.bind(this); - this._onMouseDown = this._onMouseDown.bind(this); - this._onMouseUp = this._onMouseUp.bind(this); - this._onMouseWheel = this._onMouseWheel.bind(this); - this._onAnimationFrame = this._onAnimationFrame.bind(this); - - container.addEventListener("mousemove", this._onMouseMove); - container.addEventListener("mousedown", this._onMouseDown); - container.addEventListener("mouseup", this._onMouseUp); - container.addEventListener("MozMousePixelScroll", this._onMouseWheel); - - this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame); - - this._ready.resolve(this); - this.emit("ready", this); - }); -} - -FlameGraph.prototype = { - /** - * Read-only width and height of the canvas. - * @return number - */ - get width() { - return this._width; - }, - get height() { - return this._height; - }, - - /** - * Returns a promise resolved once this graph is ready to receive data. - */ - ready: function() { - return this._ready.promise; - }, - - /** - * Destroys this graph. - */ - destroy: function() { - let container = this._container; - container.removeEventListener("mousemove", this._onMouseMove); - container.removeEventListener("mousedown", this._onMouseDown); - container.removeEventListener("mouseup", this._onMouseUp); - container.removeEventListener("MozMousePixelScroll", this._onMouseWheel); - - this._window.cancelAnimationFrame(this._animationId); - this._iframe.remove(); - - this._selection = null; - this._selectionDragger = null; - - this._data = null; - - this.emit("destroyed"); - }, - - /** - * Rendering options. Subclasses should override these. - */ - overviewHeaderTextColor: OVERVIEW_HEADER_TEXT_COLOR, - overviewTimelineStrokes: OVERVIEW_TIMELINE_STROKES, - blockTextColor: FLAME_GRAPH_BLOCK_TEXT_COLOR, - - /** - * Makes sure the canvas graph is of the specified width or height, and - * doesn't flex to fit all the available space. - */ - fixedWidth: null, - fixedHeight: null, - - /** - * The units used in the overhead ticks. Could be "ms", for example. - * Overwrite this with your own localized format. - */ - timelineTickUnits: "", - - /** - * Character used when a block's text is overflowing. - * Defaults to an ellipsis. - */ - overflowChar: L10N.ellipsis, - - /** - * Sets the data source for this graph. - * - * @param object data - * The data source. See the constructor for more information. - */ - setData: function(data) { - this._data = data; - this._selection = { start: 0, end: this._width }; - this._shouldRedraw = true; - }, - - /** - * Same as `setData`, but waits for this graph to finish initializing first. - * - * @param object data - * The data source. See the constructor for more information. - * @return promise - * A promise resolved once the data is set. - */ - setDataWhenReady: Task.async(function*(data) { - yield this.ready(); - this.setData(data); - }), - - /** - * Gets the start or end of this graph's selection, i.e. the 'data window'. - * @return number - */ - getDataWindowStart: function() { - return this._selection.start; - }, - getDataWindowEnd: function() { - return this._selection.end; - }, - - /** - * The contents of this graph are redrawn only when something changed, - * like the data source, or the selection bounds etc. This flag tracks - * if the rendering is "dirty" and needs to be refreshed. - */ - _shouldRedraw: false, - - /** - * Animation frame callback, invoked on each tick of the refresh driver. - */ - _onAnimationFrame: function() { - this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame); - this._drawWidget(); - }, - - /** - * Redraws the widget when necessary. The actual graph is not refreshed - * every time this function is called, only the cliphead, selection etc. - */ - _drawWidget: function() { - if (!this._shouldRedraw) { - return; - } - let ctx = this._ctx; - let canvasWidth = this._width; - let canvasHeight = this._height; - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - - let selection = this._selection; - let selectionWidth = selection.end - selection.start; - let selectionScale = canvasWidth / selectionWidth; - this._drawTicks(selection.start, selectionScale); - this._drawPyramid(this._data, selection.start, selectionScale); - - this._shouldRedraw = false; - }, - - /** - * Draws the overhead ticks in this graph. - * - * @param number dataOffset, dataScale - * Offsets and scales the data source by the specified amount. - * This is used for scrolling the visualization. - */ - _drawTicks: function(dataOffset, dataScale) { - let ctx = this._ctx; - let canvasWidth = this._width; - let canvasHeight = this._height; - let scaledOffset = dataOffset * dataScale; - - let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio; - let availableWidth = canvasWidth - safeBounds; - - 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; - let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio; - let tickInterval = this._findOptimalTickInterval(dataScale); - - ctx.textBaseline = "top"; - ctx.font = fontSize + "px " + fontFamily; - ctx.fillStyle = this.overviewHeaderTextColor; - ctx.strokeStyle = this.overviewTimelineStrokes; - ctx.beginPath(); - - for (let x = 0; x < availableWidth + scaledOffset; x += tickInterval) { - let lineLeft = x - scaledOffset; - let textLeft = lineLeft + textPaddingLeft; - let time = Math.round(x / dataScale / this._pixelRatio); - let label = time + " " + this.timelineTickUnits; - ctx.fillText(label, textLeft, textPaddingTop); - ctx.moveTo(lineLeft, 0); - ctx.lineTo(lineLeft, canvasHeight); - } - - ctx.stroke(); - }, - - /** - * Draws the blocks and text in this graph. - * - * @param object dataSource - * The data source. See the constructor for more information. - * @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) { - 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); - - ctx.textBaseline = "middle"; - ctx.font = fontSize + "px " + fontFamily; - ctx.fillStyle = this.blockTextColor; - - this._drawPyramidText(visibleBlocks, dataOffset, dataScale); - }, - - /** - * Fills all block inside this graph's pyramid. - * @see FlameGraph.prototype._drawPyramid - */ - _drawPyramidFill: function(dataSource, dataOffset, dataScale) { - let visibleBlocksStore = []; - let minVisibleBlockWidth = this._overflowCharWidth; - - for (let { color, blocks } of dataSource) { - this._drawBlocksFill( - color, blocks, dataOffset, dataScale, - visibleBlocksStore, minVisibleBlockWidth); - } - - return visibleBlocksStore; - }, - - /** - * 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); - } - }, - - /** - * Fills a group of blocks sharing the same style. - * - * @param string color - * The color used as the block's background. - * @param array blocks - * A list of { x, y, width, height } objects visually representing - * all the blocks sharing this particular style. - * @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. - * The provided array will be populated. - * @param number minVisibleBlockWidth - * The minimum width of the blocks that will be added into - * the `visibleBlocksStore`. - */ - _drawBlocksFill: function( - color, blocks, dataOffset, dataScale, - visibleBlocksStore, minVisibleBlockWidth) - { - let ctx = this._ctx; - let canvasWidth = this._width; - let canvasHeight = this._height; - let scaledOffset = dataOffset * dataScale; - - ctx.fillStyle = color; - ctx.beginPath(); - - 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 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. - continue; - } - - // Clamp the blocks position to start at 0. Avoid negative X coords, - // to properly place the text inside the blocks. - if (rectLeft < 0) { - rectWidth += rectLeft; - rectLeft = 0; - } - - // Avoid drawing blocks that are too narrow. - if (rectWidth <= FLAME_GRAPH_BLOCK_BORDER || - rectHeight <= FLAME_GRAPH_BLOCK_BORDER) { - continue; - } - - ctx.rect( - rectLeft, rectTop, - rectWidth - FLAME_GRAPH_BLOCK_BORDER, - rectHeight - FLAME_GRAPH_BLOCK_BORDER); - - // Populate the visible blocks store with this block if the width - // is longer than a given threshold. - if (rectWidth > minVisibleBlockWidth) { - visibleBlocksStore.push(block); - } - } - - ctx.fill(); - }, - - /** - * Adds text for a single block. - * - * @param object block - * A single { x, y, width, height, text } object visually representing - * the block containing the text. - * @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) { - let ctx = this._ctx; - let scaledOffset = dataOffset * dataScale; - - let { x, y, width, height, text } = block; - - 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) { - rectWidth += rectLeft; - rectLeft = 0; - } - - let textLeft = rectLeft + paddingLeft; - let textTop = (y + height / 2 + OVERVIEW_HEADER_HEIGHT) * this._pixelRatio + paddingTop; - let textAvailableWidth = rectWidth - totalHorizontalPadding; - - // Massage the text to fit inside a given width. This clamps the string - // at the end to avoid overflowing. - let fittedText = this._getFittedText(text, textAvailableWidth); - if (fittedText.length < 1) { - return; - } - - ctx.fillText(fittedText, textLeft, textTop); - }, - - /** - * Calculating text widths is necessary to trim the text inside the blocks - * while the scaling changes (e.g. via scrolling). This is very expensive, - * so maintain a cache of string contents to text widths. - */ - _textWidthsCache: null, - _overflowCharWidth: null, - _averageCharWidth: null, - - /** - * Gets the width of the specified text, for the current context state - * (font size, family etc.). - * - * @param string text - * The text to analyze. - * @return number - * The text width. - */ - _getTextWidth: function(text) { - let cachedWidth = this._textWidthsCache[text]; - if (cachedWidth) { - return cachedWidth; - } - let metrics = this._ctx.measureText(text); - return (this._textWidthsCache[text] = metrics.width); - }, - - /** - * Gets an approximate width of the specified text. This is much faster - * than `_getTextWidth`, but inexact. - * - * @param string text - * The text to analyze. - * @return number - * The approximate text width. - */ - _getTextWidthApprox: function(text) { - return text.length * this._averageCharWidth; - }, - - /** - * Gets the average letter width in the English alphabet, for the current - * context state (font size, family etc.). This provides a close enough - * value to use in `_getTextWidthApprox`. - * - * @return number - * The average letter width. - */ - _calcAverageCharWidth: function() { - let letterWidthsSum = 0; - let start = 32; // space - let end = 123; // "z" - - for (let i = start; i < end; i++) { - let char = String.fromCharCode(i); - letterWidthsSum += this._getTextWidth(char); - } - - return letterWidthsSum / (end - start); - }, - - /** - * Massage a text to fit inside a given width. This clamps the string - * at the end to avoid overflowing. - * - * @param string text - * The text to fit inside the given width. - * @param number maxWidth - * The available width for the given text. - * @return string - * The fitted text. - */ - _getFittedText: function(text, maxWidth) { - let textWidth = this._getTextWidth(text); - if (textWidth < maxWidth) { - return text; - } - if (this._overflowCharWidth > maxWidth) { - return ""; - } - for (let i = 1, len = text.length; i <= len; i++) { - let trimmedText = text.substring(0, len - i); - let trimmedWidth = this._getTextWidthApprox(trimmedText) + this._overflowCharWidth; - if (trimmedWidth < maxWidth) { - return trimmedText + this.overflowChar; - } - } - return ""; - }, - - /** - * Listener for the "mousemove" event on the graph's container. - */ - _onMouseMove: function(e) { - let offset = this._getContainerOffset(); - let mouseX = (e.clientX - offset.left) * this._pixelRatio; - - let canvasWidth = this._width; - let canvasHeight = this._height; - - let selection = this._selection; - 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; - this._normalizeSelectionBounds(); - this._shouldRedraw = true; - } - }, - - /** - * Listener for the "mousedown" event on the graph's container. - */ - _onMouseDown: function(e) { - let offset = this._getContainerOffset(); - let mouseX = (e.clientX - offset.left) * 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"); - }, - - /** - * Listener for the "mouseup" event on the graph's container. - */ - _onMouseUp: function() { - this._selectionDragger.origin = null; - this._canvas.removeAttribute("input"); - }, - - /** - * Listener for the "wheel" event on the graph's container. - */ - _onMouseWheel: function(e) { - let offset = this._getContainerOffset(); - let mouseX = (e.clientX - offset.left) * this._pixelRatio; - - let canvasWidth = this._width; - let canvasHeight = this._height; - - let selection = this._selection; - let selectionWidth = selection.end - selection.start; - let selectionScale = canvasWidth / selectionWidth; - - switch (e.axis) { - case e.VERTICAL_AXIS: { - let distFromStart = mouseX; - let distFromEnd = canvasWidth - mouseX; - let vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY / selectionScale; - selection.start -= distFromStart * vector; - selection.end += distFromEnd * vector; - break; - } - case e.HORIZONTAL_AXIS: { - let vector = e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY / selectionScale; - selection.start += vector; - selection.end += vector; - break; - } - } - - this._normalizeSelectionBounds(); - this._shouldRedraw = true; - }, - - /** - * Makes sure the start and end points of the current selection - * are withing the graph's visible bounds, and that they form a selection - * wider than the allowed minimum width. - */ - _normalizeSelectionBounds: function() { - let canvasWidth = this._width * 2; - let canvasHeight = this._height; - - let { start, end } = this._selection; - let minSelectionWidth = GRAPH_MIN_SELECTION_WIDTH * this._pixelRatio; - - if (start < 0) { - start = 0; - } - if (end < 0) { - start = 0; - end = minSelectionWidth; - } - if (end > canvasWidth) { - end = canvasWidth; - } - if (start > canvasWidth) { - end = canvasWidth; - start = canvasWidth - minSelectionWidth; - } - if (end - start < minSelectionWidth) { - let midPoint = (start + end) / 2; - start = midPoint - minSelectionWidth / 2; - end = midPoint + minSelectionWidth / 2; - } - - this._selection.start = start; - this._selection.end = end; - }, - - /** - * - * Finds the optimal tick interval between time markers in this graph. - * - * @param number dataScale - * @return number - */ - _findOptimalTickInterval: function(dataScale) { - let timingStep = TIMELINE_TICKS_MULTIPLE; - let spacingMin = TIMELINE_TICKS_SPACING_MIN * this._pixelRatio; - - if (dataScale > spacingMin) { - return dataScale; - } - - while (true) { - let scaledStep = dataScale * timingStep; - if (scaledStep < spacingMin) { - timingStep <<= 1; - continue; - } - return scaledStep; - } - }, - - /** - * Gets the offset of this graph's container relative to the owner window. - * - * @return object - * The { left, top } offset. - */ - _getContainerOffset: function() { - let node = this._canvas; - let x = 0; - let y = 0; - - while ((node = node.offsetParent)) { - x += node.offsetLeft; - y += node.offsetTop; - } - - return { left: x, top: y }; - } -}; - -/** - * A collection of utility functions converting various data sources - * into a format drawable by the FlameGraph. - */ -let FlameGraphUtils = { - // TODO bug 1077459 -}; diff --git a/browser/devtools/shared/widgets/Graphs.jsm b/browser/devtools/shared/widgets/Graphs.jsm index a90fabbd10b1..26b514e756cc 100644 --- a/browser/devtools/shared/widgets/Graphs.jsm +++ b/browser/devtools/shared/widgets/Graphs.jsm @@ -7,14 +7,10 @@ const Cu = Components.utils; Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; -const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {}); +const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); this.EXPORTED_SYMBOLS = [ - "GraphCursor", - "GraphSelection", - "GraphSelectionDragger", - "GraphSelectionResizer", "AbstractCanvasGraph", "LineGraphWidget", "BarGraphWidget", @@ -97,23 +93,28 @@ const BAR_GRAPH_LEGEND_MOUSEOVER_DEBOUNCE = 50; // ms /** * Small data primitives for all graphs. */ -this.GraphCursor = function() { - this.x = null; - this.y = null; +this.GraphCursor = function() {}; +this.GraphSelection = function() {}; +this.GraphSelectionDragger = function() {}; +this.GraphSelectionResizer = function() {}; + +GraphCursor.prototype = { + x: null, + y: null }; -this.GraphSelection = function() { - this.start = null; - this.end = null; +GraphSelection.prototype = { + start: null, + end: null }; -this.GraphSelectionDragger = function() { - this.origin = null; - this.anchor = new GraphSelection(); +GraphSelectionDragger.prototype = { + origin: null, + anchor: new GraphSelection() }; -this.GraphSelectionResizer = function() { - this.margin = null; +GraphSelectionResizer.prototype = { + margin: null }; /** @@ -244,11 +245,6 @@ AbstractCanvasGraph.prototype = { this._window.cancelAnimationFrame(this._animationId); this._iframe.remove(); - this._cursor = null; - this._selection = null; - this._selectionDragger = null; - this._selectionResizer = null; - this._data = null; this._mask = null; this._maskArgs = null; @@ -896,9 +892,6 @@ AbstractCanvasGraph.prototype = { /** * Gets the offset of this graph's container relative to the owner window. - * - * @return object - * The { left, top } offset. */ _getContainerOffset: function() { let node = this._canvas;