summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/views/overview.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/views/overview.js')
-rw-r--r--devtools/client/performance/views/overview.js423
1 files changed, 423 insertions, 0 deletions
diff --git a/devtools/client/performance/views/overview.js b/devtools/client/performance/views/overview.js
new file mode 100644
index 000000000..f45a6d844
--- /dev/null
+++ b/devtools/client/performance/views/overview.js
@@ -0,0 +1,423 @@
+/* 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/. */
+/* import-globals-from ../performance-controller.js */
+/* import-globals-from ../performance-view.js */
+"use strict";
+
+// No sense updating the overview more often than receiving data from the
+// backend. Make sure this isn't lower than DEFAULT_TIMELINE_DATA_PULL_TIMEOUT
+// in devtools/server/actors/timeline.js
+
+// The following units are in milliseconds.
+const OVERVIEW_UPDATE_INTERVAL = 200;
+const FRAMERATE_GRAPH_LOW_RES_INTERVAL = 100;
+const FRAMERATE_GRAPH_HIGH_RES_INTERVAL = 16;
+const GRAPH_REQUIREMENTS = {
+ timeline: {
+ features: ["withMarkers"]
+ },
+ framerate: {
+ features: ["withTicks"]
+ },
+ memory: {
+ features: ["withMemory"]
+ },
+};
+
+/**
+ * View handler for the overview panel's time view, displaying
+ * framerate, timeline and memory over time.
+ */
+var OverviewView = {
+
+ /**
+ * How frequently we attempt to render the graphs. Overridden
+ * in tests.
+ */
+ OVERVIEW_UPDATE_INTERVAL: OVERVIEW_UPDATE_INTERVAL,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ this.graphs = new GraphsController({
+ root: $("#overview-pane"),
+ getFilter: () => PerformanceController.getPref("hidden-markers"),
+ getTheme: () => PerformanceController.getTheme(),
+ });
+
+ // If no timeline support, shut it all down.
+ if (!PerformanceController.getTraits().features.withMarkers) {
+ this.disable();
+ return;
+ }
+
+ // Store info on multiprocess support.
+ this._multiprocessData = PerformanceController.getMultiprocessStatus();
+
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onRecordingSelected = this._onRecordingSelected.bind(this);
+ this._onRecordingTick = this._onRecordingTick.bind(this);
+ this._onGraphSelecting = this._onGraphSelecting.bind(this);
+ this._onGraphRendered = this._onGraphRendered.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ // Toggle the initial visibility of memory and framerate graph containers
+ // based off of prefs.
+ PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
+ PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+ this.graphs.on("selecting", this._onGraphSelecting);
+ this.graphs.on("rendered", this._onGraphRendered);
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: Task.async(function* () {
+ PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange);
+ PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+ this.graphs.off("selecting", this._onGraphSelecting);
+ this.graphs.off("rendered", this._onGraphRendered);
+ yield this.graphs.destroy();
+ }),
+
+ /**
+ * Returns true if any of the overview graphs have mouse dragging active,
+ * false otherwise.
+ */
+ get isMouseActive() {
+ // Fetch all graphs currently stored in the GraphsController.
+ // These graphs are not necessarily active, but will not have
+ // an active mouse, in that case.
+ return !!this.graphs.getWidgets().some(e => e.isMouseActive);
+ },
+
+ /**
+ * Disabled in the event we're using a Timeline mock, so we'll have no
+ * timeline, ticks or memory data to show, so just block rendering and hide
+ * the panel.
+ */
+ disable: function () {
+ this._disabled = true;
+ this.graphs.disableAll();
+ },
+
+ /**
+ * Returns the disabled status.
+ *
+ * @return boolean
+ */
+ isDisabled: function () {
+ return this._disabled;
+ },
+
+ /**
+ * Sets the time interval selection for all graphs in this overview.
+ *
+ * @param object interval
+ * The { startTime, endTime }, in milliseconds.
+ */
+ setTimeInterval: function (interval, options = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording == null) {
+ throw new Error("A recording should be available in order to set the selection.");
+ }
+ if (this.isDisabled()) {
+ return;
+ }
+ let mapStart = () => 0;
+ let mapEnd = () => recording.getDuration();
+ let selection = { start: interval.startTime, end: interval.endTime };
+ this._stopSelectionChangeEventPropagation = options.stopPropagation;
+ this.graphs.setMappedSelection(selection, { mapStart, mapEnd });
+ this._stopSelectionChangeEventPropagation = false;
+ },
+
+ /**
+ * Gets the time interval selection for all graphs in this overview.
+ *
+ * @return object
+ * The { startTime, endTime }, in milliseconds.
+ */
+ getTimeInterval: function () {
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording == null) {
+ throw new Error("A recording should be available in order to get the selection.");
+ }
+ if (this.isDisabled()) {
+ return { startTime: 0, endTime: recording.getDuration() };
+ }
+ let mapStart = () => 0;
+ let mapEnd = () => recording.getDuration();
+ let selection = this.graphs.getMappedSelection({ mapStart, mapEnd });
+ // If no selection returned, this means the overview graphs have not been rendered
+ // yet, so act as if we have no selection (the full recording). Also
+ // if the selection range distance is tiny, assume the range was cleared or just
+ // clicked, and we do not have a range.
+ if (!selection || (selection.max - selection.min) < 1) {
+ return { startTime: 0, endTime: recording.getDuration() };
+ }
+ return { startTime: selection.min, endTime: selection.max };
+ },
+
+ /**
+ * Method for handling all the set up for rendering the overview graphs.
+ *
+ * @param number resolution
+ * The fps graph resolution. @see Graphs.js
+ */
+ render: Task.async(function* (resolution) {
+ if (this.isDisabled()) {
+ return;
+ }
+
+ let recording = PerformanceController.getCurrentRecording();
+ yield this.graphs.render(recording.getAllData(), resolution);
+
+ // Finished rendering all graphs in this overview.
+ this.emit(EVENTS.UI_OVERVIEW_RENDERED, resolution);
+ }),
+
+ /**
+ * Called at most every OVERVIEW_UPDATE_INTERVAL milliseconds
+ * and uses data fetched from the controller to render
+ * data into all the corresponding overview graphs.
+ */
+ _onRecordingTick: Task.async(function* () {
+ yield this.render(FRAMERATE_GRAPH_LOW_RES_INTERVAL);
+ this._prepareNextTick();
+ }),
+
+ /**
+ * Called to refresh the timer to keep firing _onRecordingTick.
+ */
+ _prepareNextTick: function () {
+ // Check here to see if there's still a _timeoutId, incase
+ // `stop` was called before the _prepareNextTick call was executed.
+ if (this.isRendering()) {
+ this._timeoutId = setTimeout(this._onRecordingTick, this.OVERVIEW_UPDATE_INTERVAL);
+ }
+ },
+
+ /**
+ * Called when recording state changes.
+ */
+ _onRecordingStateChange: OverviewViewOnStateChange(Task.async(
+ function* (_, state, recording) {
+ if (state !== "recording-stopped") {
+ return;
+ }
+ // Check to see if the recording that just stopped is the current recording.
+ // If it is, render the high-res graphs. For manual recordings, it will also
+ // be the current recording, but profiles generated by `console.profile` can stop
+ // while having another profile selected -- in this case, OverviewView should keep
+ // rendering the current recording.
+ if (recording !== PerformanceController.getCurrentRecording()) {
+ return;
+ }
+ this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+ yield this._checkSelection(recording);
+ })),
+
+ /**
+ * Called when a new recording is selected.
+ */
+ _onRecordingSelected: OverviewViewOnStateChange(Task.async(function* (_, recording) {
+ this._setGraphVisibilityFromRecordingFeatures(recording);
+
+ // If this recording is complete, render the high res graph
+ if (recording.isCompleted()) {
+ yield this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+ }
+ yield this._checkSelection(recording);
+ this.graphs.dropSelection();
+ })),
+
+ /**
+ * Start the polling for rendering the overview graph.
+ */
+ _startPolling: function () {
+ this._timeoutId = setTimeout(this._onRecordingTick, this.OVERVIEW_UPDATE_INTERVAL);
+ },
+
+ /**
+ * Stop the polling for rendering the overview graph.
+ */
+ _stopPolling: function () {
+ clearTimeout(this._timeoutId);
+ this._timeoutId = null;
+ },
+
+ /**
+ * Whether or not the overview view is in a state of polling rendering.
+ */
+ isRendering: function () {
+ return !!this._timeoutId;
+ },
+
+ /**
+ * Makes sure the selection is enabled or disabled in all the graphs,
+ * based on whether a recording currently exists and is not in progress.
+ */
+ _checkSelection: Task.async(function* (recording) {
+ let isEnabled = recording ? recording.isCompleted() : false;
+ yield this.graphs.selectionEnabled(isEnabled);
+ }),
+
+ /**
+ * Fired when the graph selection has changed. Called by
+ * mouseup and scroll events.
+ */
+ _onGraphSelecting: function () {
+ if (this._stopSelectionChangeEventPropagation) {
+ return;
+ }
+
+ this.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this.getTimeInterval());
+ },
+
+ _onGraphRendered: function (_, graphName) {
+ switch (graphName) {
+ case "timeline":
+ this.emit(EVENTS.UI_MARKERS_GRAPH_RENDERED);
+ break;
+ case "memory":
+ this.emit(EVENTS.UI_MEMORY_GRAPH_RENDERED);
+ break;
+ case "framerate":
+ this.emit(EVENTS.UI_FRAMERATE_GRAPH_RENDERED);
+ break;
+ }
+ },
+
+ /**
+ * Called whenever a preference in `devtools.performance.ui.` changes.
+ * Does not care about the enabling of memory/framerate graphs,
+ * because those will set values on a recording model, and
+ * the graphs will render based on the existence.
+ */
+ _onPrefChanged: Task.async(function* (_, prefName, prefValue) {
+ switch (prefName) {
+ case "hidden-markers": {
+ let graph = yield this.graphs.isAvailable("timeline");
+ if (graph) {
+ let filter = PerformanceController.getPref("hidden-markers");
+ graph.setFilter(filter);
+ graph.refresh({ force: true });
+ }
+ break;
+ }
+ }
+ }),
+
+ _setGraphVisibilityFromRecordingFeatures: function (recording) {
+ for (let [graphName, requirements] of Object.entries(GRAPH_REQUIREMENTS)) {
+ this.graphs.enable(graphName,
+ PerformanceController.isFeatureSupported(requirements.features));
+ }
+ },
+
+ /**
+ * Fetch the multiprocess status and if e10s is not currently on, disable
+ * realtime rendering.
+ *
+ * @return {boolean}
+ */
+ isRealtimeRenderingEnabled: function () {
+ return this._multiprocessData.enabled;
+ },
+
+ /**
+ * Show the graphs overview panel when a recording is finished
+ * when non-realtime graphs are enabled. Also set the graph visibility
+ * so the performance graphs know which graphs to render.
+ *
+ * @param {RecordingModel} recording
+ */
+ _showGraphsPanel: function (recording) {
+ this._setGraphVisibilityFromRecordingFeatures(recording);
+ $("#overview-pane").classList.remove("hidden");
+ },
+
+ /**
+ * Hide the graphs container completely.
+ */
+ _hideGraphsPanel: function () {
+ $("#overview-pane").classList.add("hidden");
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function (_, theme) {
+ this.graphs.setTheme({ theme, redraw: true });
+ },
+
+ toString: () => "[object OverviewView]"
+};
+
+/**
+ * Utility that can wrap a method of OverviewView that
+ * handles a recording state change like when a recording is starting,
+ * stopping, or about to start/stop, and determines whether or not
+ * the polling for rendering the overview graphs needs to start or stop.
+ * Must be called with the OverviewView context.
+ *
+ * @param {function?} fn
+ * @return {function}
+ */
+function OverviewViewOnStateChange(fn) {
+ return function _onRecordingStateChange(eventName, recording) {
+ // Normalize arguments for the RECORDING_STATE_CHANGE event,
+ // as it also has a `state` argument.
+ if (typeof recording === "string") {
+ recording = arguments[2];
+ }
+
+ let currentRecording = PerformanceController.getCurrentRecording();
+
+ // All these methods require a recording to exist selected and
+ // from the event name, since there is a delay between starting
+ // a recording and changing the selection.
+ if (!currentRecording || !recording) {
+ // If no recording (this can occur when having a console.profile recording, and
+ // we do not stop it from the backend), and we are still rendering updates,
+ // stop that.
+ if (this.isRendering()) {
+ this._stopPolling();
+ }
+ return;
+ }
+
+ // If realtime rendering is not enabed (e10s not on), then
+ // show the disabled message, or the full graphs if the recording is completed
+ if (!this.isRealtimeRenderingEnabled()) {
+ if (recording.isRecording()) {
+ this._hideGraphsPanel();
+ // Abort, as we do not want to change polling status.
+ return;
+ }
+ this._showGraphsPanel(recording);
+ }
+
+ if (this.isRendering() && !currentRecording.isRecording()) {
+ this._stopPolling();
+ } else if (currentRecording.isRecording() && !this.isRendering()) {
+ this._startPolling();
+ }
+
+ if (fn) {
+ fn.apply(this, arguments);
+ }
+ };
+}
+
+// Decorates the OverviewView as an EventEmitter
+EventEmitter.decorate(OverviewView);