/* 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;