summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/modules/widgets/graphs.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/modules/widgets/graphs.js')
-rw-r--r--devtools/client/performance/modules/widgets/graphs.js514
1 files changed, 514 insertions, 0 deletions
diff --git a/devtools/client/performance/modules/widgets/graphs.js b/devtools/client/performance/modules/widgets/graphs.js
new file mode 100644
index 000000000..9d9262027
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/graphs.js
@@ -0,0 +1,514 @@
+/* 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";
+
+/**
+ * This file contains the base line graph that all Performance line graphs use.
+ */
+
+const { Task } = require("devtools/shared/task");
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
+const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
+
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const { colorUtils } = require("devtools/shared/css/color");
+const { getColor } = require("devtools/client/shared/theme");
+const ProfilerGlobal = require("devtools/client/performance/modules/global");
+const { MarkersOverview } = require("devtools/client/performance/modules/widgets/markers-overview");
+const { createTierGraphDataFromFrameNode } = require("devtools/client/performance/modules/logic/jit");
+
+/**
+ * For line graphs
+ */
+// px
+const HEIGHT = 35;
+// px
+const STROKE_WIDTH = 1;
+const DAMPEN_VALUES = 0.95;
+const CLIPHEAD_LINE_COLOR = "#666";
+const SELECTION_LINE_COLOR = "#555";
+const SELECTION_BACKGROUND_COLOR_NAME = "graphs-blue";
+const FRAMERATE_GRAPH_COLOR_NAME = "graphs-green";
+const MEMORY_GRAPH_COLOR_NAME = "graphs-blue";
+
+/**
+ * For timeline overview
+ */
+// px
+const MARKERS_GRAPH_HEADER_HEIGHT = 14;
+// px
+const MARKERS_GRAPH_ROW_HEIGHT = 10;
+// px
+const MARKERS_GROUP_VERTICAL_PADDING = 4;
+
+/**
+ * For optimization graph
+ */
+const OPTIMIZATIONS_GRAPH_RESOLUTION = 100;
+
+/**
+ * A base class for performance graphs to inherit from.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ * @param string metric
+ * The unit of measurement for this graph.
+ */
+function PerformanceGraph(parent, metric) {
+ LineGraphWidget.call(this, parent, { metric });
+ this.setTheme();
+}
+
+PerformanceGraph.prototype = Heritage.extend(LineGraphWidget.prototype, {
+ strokeWidth: STROKE_WIDTH,
+ dampenValuesFactor: DAMPEN_VALUES,
+ fixedHeight: HEIGHT,
+ clipheadLineColor: CLIPHEAD_LINE_COLOR,
+ selectionLineColor: SELECTION_LINE_COLOR,
+ withTooltipArrows: false,
+ withFixedTooltipPositions: true,
+
+ /**
+ * Disables selection and empties this graph.
+ */
+ clearView: function () {
+ this.selectionEnabled = false;
+ this.dropSelection();
+ this.setData([]);
+ },
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function (theme) {
+ theme = theme || "light";
+ let mainColor = getColor(this.mainColor || "graphs-blue", theme);
+ this.backgroundColor = getColor("body-background", theme);
+ this.strokeColor = mainColor;
+ this.backgroundGradientStart = colorUtils.setAlpha(mainColor, 0.2);
+ this.backgroundGradientEnd = colorUtils.setAlpha(mainColor, 0.2);
+ this.selectionBackgroundColor = colorUtils.setAlpha(
+ getColor(SELECTION_BACKGROUND_COLOR_NAME, theme), 0.25);
+ this.selectionStripesColor = "rgba(255, 255, 255, 0.1)";
+ this.maximumLineColor = colorUtils.setAlpha(mainColor, 0.4);
+ this.averageLineColor = colorUtils.setAlpha(mainColor, 0.7);
+ this.minimumLineColor = colorUtils.setAlpha(mainColor, 0.9);
+ }
+});
+
+/**
+ * Constructor for the framerate graph. Inherits from PerformanceGraph.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ */
+function FramerateGraph(parent) {
+ PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.fps"));
+}
+
+FramerateGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
+ mainColor: FRAMERATE_GRAPH_COLOR_NAME,
+ setPerformanceData: function ({ duration, ticks }, resolution) {
+ this.dataDuration = duration;
+ return this.setDataFromTimestamps(ticks, resolution, duration);
+ }
+});
+
+/**
+ * Constructor for the memory graph. Inherits from PerformanceGraph.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ */
+function MemoryGraph(parent) {
+ PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.memory"));
+}
+
+MemoryGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
+ mainColor: MEMORY_GRAPH_COLOR_NAME,
+ setPerformanceData: function ({ duration, memory }) {
+ this.dataDuration = duration;
+ return this.setData(memory);
+ }
+});
+
+function TimelineGraph(parent, filter) {
+ MarkersOverview.call(this, parent, filter);
+}
+
+TimelineGraph.prototype = Heritage.extend(MarkersOverview.prototype, {
+ headerHeight: MARKERS_GRAPH_HEADER_HEIGHT,
+ rowHeight: MARKERS_GRAPH_ROW_HEIGHT,
+ groupPadding: MARKERS_GROUP_VERTICAL_PADDING,
+ setPerformanceData: MarkersOverview.prototype.setData
+});
+
+/**
+ * Definitions file for GraphsController, indicating the constructor,
+ * selector and other meta for each of the graphs controller by
+ * GraphsController.
+ */
+const GRAPH_DEFINITIONS = {
+ memory: {
+ constructor: MemoryGraph,
+ selector: "#memory-overview",
+ },
+ framerate: {
+ constructor: FramerateGraph,
+ selector: "#time-framerate",
+ },
+ timeline: {
+ constructor: TimelineGraph,
+ selector: "#markers-overview",
+ primaryLink: true
+ }
+};
+
+/**
+ * A controller for orchestrating the performance's tool overview graphs. Constructs,
+ * syncs, toggles displays and defines the memory, framerate and timeline view.
+ *
+ * @param {object} definition
+ * @param {DOMElement} root
+ * @param {function} getFilter
+ * @param {function} getTheme
+ */
+function GraphsController({ definition, root, getFilter, getTheme }) {
+ this._graphs = {};
+ this._enabled = new Set();
+ this._definition = definition || GRAPH_DEFINITIONS;
+ this._root = root;
+ this._getFilter = getFilter;
+ this._getTheme = getTheme;
+ this._primaryLink = Object.keys(this._definition)
+ .filter(name => this._definition[name].primaryLink)[0];
+ this.$ = root.ownerDocument.querySelector.bind(root.ownerDocument);
+
+ EventEmitter.decorate(this);
+ this._onSelecting = this._onSelecting.bind(this);
+}
+
+GraphsController.prototype = {
+
+ /**
+ * Returns the corresponding graph by `graphName`.
+ */
+ get: function (graphName) {
+ return this._graphs[graphName];
+ },
+
+ /**
+ * Iterates through all graphs and renders the data
+ * from a RecordingModel. Takes a resolution value used in
+ * some graphs.
+ * Saves rendering progress as a promise to be consumed by `destroy`,
+ * to wait for cleaning up rendering during destruction.
+ */
+ render: Task.async(function* (recordingData, resolution) {
+ // Get the previous render promise so we don't start rendering
+ // until the previous render cycle completes, which can occur
+ // especially when a recording is finished, and triggers a
+ // fresh rendering at a higher rate
+ yield (this._rendering && this._rendering.promise);
+
+ // Check after yielding to ensure we're not tearing down,
+ // as this can create a race condition in tests
+ if (this._destroyed) {
+ return;
+ }
+
+ this._rendering = promise.defer();
+ for (let graph of (yield this._getEnabled())) {
+ yield graph.setPerformanceData(recordingData, resolution);
+ this.emit("rendered", graph.graphName);
+ }
+ this._rendering.resolve();
+ }),
+
+ /**
+ * Destroys the underlying graphs.
+ */
+ destroy: Task.async(function* () {
+ let primary = this._getPrimaryLink();
+
+ this._destroyed = true;
+
+ if (primary) {
+ primary.off("selecting", this._onSelecting);
+ }
+
+ // If there was rendering, wait until the most recent render cycle
+ // has finished
+ if (this._rendering) {
+ yield this._rendering.promise;
+ }
+
+ for (let graph of this.getWidgets()) {
+ yield graph.destroy();
+ }
+ }),
+
+ /**
+ * Applies the theme to the underlying graphs. Optionally takes
+ * a `redraw` boolean in the options to force redraw.
+ */
+ setTheme: function (options = {}) {
+ let theme = options.theme || this._getTheme();
+ for (let graph of this.getWidgets()) {
+ graph.setTheme(theme);
+ graph.refresh({ force: options.redraw });
+ }
+ },
+
+ /**
+ * Sets up the graph, if needed. Returns a promise resolving
+ * to the graph if it is enabled once it's ready, or otherwise returns
+ * null if disabled.
+ */
+ isAvailable: Task.async(function* (graphName) {
+ if (!this._enabled.has(graphName)) {
+ return null;
+ }
+
+ let graph = this.get(graphName);
+
+ if (!graph) {
+ graph = yield this._construct(graphName);
+ }
+
+ yield graph.ready();
+ return graph;
+ }),
+
+ /**
+ * Enable or disable a subgraph controlled by GraphsController.
+ * This determines what graphs are visible and get rendered.
+ */
+ enable: function (graphName, isEnabled) {
+ let el = this.$(this._definition[graphName].selector);
+ el.classList[isEnabled ? "remove" : "add"]("hidden");
+
+ // If no status change, just return
+ if (this._enabled.has(graphName) === isEnabled) {
+ return;
+ }
+ if (isEnabled) {
+ this._enabled.add(graphName);
+ } else {
+ this._enabled.delete(graphName);
+ }
+
+ // Invalidate our cache of ready-to-go graphs
+ this._enabledGraphs = null;
+ },
+
+ /**
+ * Disables all graphs controller by the GraphsController, and
+ * also hides the root element. This is a one way switch, and used
+ * when older platforms do not have any timeline data.
+ */
+ disableAll: function () {
+ this._root.classList.add("hidden");
+ // Hide all the subelements
+ Object.keys(this._definition).forEach(graphName => this.enable(graphName, false));
+ },
+
+ /**
+ * Sets a mapped selection on the graph that is the main controller
+ * for keeping the graphs' selections in sync.
+ */
+ setMappedSelection: function (selection, { mapStart, mapEnd }) {
+ return this._getPrimaryLink().setMappedSelection(selection, { mapStart, mapEnd });
+ },
+
+ /**
+ * Fetches the currently mapped selection. If graphs are not yet rendered,
+ * (which throws in Graphs.js), return null.
+ */
+ getMappedSelection: function ({ mapStart, mapEnd }) {
+ let primary = this._getPrimaryLink();
+ if (primary && primary.hasData()) {
+ return primary.getMappedSelection({ mapStart, mapEnd });
+ }
+ return null;
+ },
+
+ /**
+ * Returns an array of graphs that have been created, not necessarily
+ * enabled currently.
+ */
+ getWidgets: function () {
+ return Object.keys(this._graphs).map(name => this._graphs[name]);
+ },
+
+ /**
+ * Drops the selection.
+ */
+ dropSelection: function () {
+ if (this._getPrimaryLink()) {
+ return this._getPrimaryLink().dropSelection();
+ }
+ return null;
+ },
+
+ /**
+ * Makes sure the selection is enabled or disabled in all the graphs.
+ */
+ selectionEnabled: Task.async(function* (enabled) {
+ for (let graph of (yield this._getEnabled())) {
+ graph.selectionEnabled = enabled;
+ }
+ }),
+
+ /**
+ * Creates the graph `graphName` and initializes it.
+ */
+ _construct: Task.async(function* (graphName) {
+ let def = this._definition[graphName];
+ let el = this.$(def.selector);
+ let filter = this._getFilter();
+ let graph = this._graphs[graphName] = new def.constructor(el, filter);
+ graph.graphName = graphName;
+
+ yield graph.ready();
+
+ // Sync the graphs' animations and selections together
+ if (def.primaryLink) {
+ graph.on("selecting", this._onSelecting);
+ } else {
+ CanvasGraphUtils.linkAnimation(this._getPrimaryLink(), graph);
+ CanvasGraphUtils.linkSelection(this._getPrimaryLink(), graph);
+ }
+
+ // Sets the container element's visibility based off of enabled status
+ el.classList[this._enabled.has(graphName) ? "remove" : "add"]("hidden");
+
+ this.setTheme();
+ return graph;
+ }),
+
+ /**
+ * Returns the main graph for this collection, that all graphs
+ * are bound to for syncing and selection.
+ */
+ _getPrimaryLink: function () {
+ return this.get(this._primaryLink);
+ },
+
+ /**
+ * Emitted when a selection occurs.
+ */
+ _onSelecting: function () {
+ this.emit("selecting");
+ },
+
+ /**
+ * Resolves to an array with all graphs that are enabled, and
+ * creates them if needed. Different than just iterating over `this._graphs`,
+ * as those could be enabled. Uses caching, as rendering happens many times per second,
+ * compared to how often which graphs/features are changed (rarely).
+ */
+ _getEnabled: Task.async(function* () {
+ if (this._enabledGraphs) {
+ return this._enabledGraphs;
+ }
+ let enabled = [];
+ for (let graphName of this._enabled) {
+ let graph = yield this.isAvailable(graphName);
+ if (graph) {
+ enabled.push(graph);
+ }
+ }
+ this._enabledGraphs = enabled;
+ return this._enabledGraphs;
+ }),
+};
+
+/**
+ * A base class for performance graphs to inherit from.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the overview.
+ * @param string metric
+ * The unit of measurement for this graph.
+ */
+function OptimizationsGraph(parent) {
+ MountainGraphWidget.call(this, parent);
+ this.setTheme();
+}
+
+OptimizationsGraph.prototype = Heritage.extend(MountainGraphWidget.prototype, {
+
+ render: Task.async(function* (threadNode, frameNode) {
+ // Regardless if we draw or clear the graph, wait
+ // until it's ready.
+ yield this.ready();
+
+ if (!threadNode || !frameNode) {
+ this.setData([]);
+ return;
+ }
+
+ let { sampleTimes } = threadNode;
+
+ if (!sampleTimes.length) {
+ this.setData([]);
+ return;
+ }
+
+ // Take startTime/endTime from samples recorded, rather than
+ // using duration directly from threadNode, as the first sample that
+ // equals the startTime does not get recorded.
+ let startTime = sampleTimes[0];
+ let endTime = sampleTimes[sampleTimes.length - 1];
+
+ let bucketSize = (endTime - startTime) / OPTIMIZATIONS_GRAPH_RESOLUTION;
+ let data = createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize);
+
+ // If for some reason we don't have data (like the frameNode doesn't
+ // have optimizations, but it shouldn't be at this point if it doesn't),
+ // log an error.
+ if (!data) {
+ console.error(
+ `FrameNode#${frameNode.location} does not have optimizations data to render.`);
+ return;
+ }
+
+ this.dataOffsetX = startTime;
+ yield this.setData(data);
+ }),
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function (theme) {
+ theme = theme || "light";
+
+ let interpreterColor = getColor("graphs-red", theme);
+ let baselineColor = getColor("graphs-blue", theme);
+ let ionColor = getColor("graphs-green", theme);
+
+ this.format = [
+ { color: interpreterColor },
+ { color: baselineColor },
+ { color: ionColor },
+ ];
+
+ this.backgroundColor = getColor("sidebar-background", theme);
+ }
+});
+
+exports.OptimizationsGraph = OptimizationsGraph;
+exports.FramerateGraph = FramerateGraph;
+exports.MemoryGraph = MemoryGraph;
+exports.TimelineGraph = TimelineGraph;
+exports.GraphsController = GraphsController;