summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/performance-controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/performance-controller.js')
-rw-r--r--devtools/client/performance/performance-controller.js595
1 files changed, 595 insertions, 0 deletions
diff --git a/devtools/client/performance/performance-controller.js b/devtools/client/performance/performance-controller.js
new file mode 100644
index 000000000..e47a0c401
--- /dev/null
+++ b/devtools/client/performance/performance-controller.js
@@ -0,0 +1,595 @@
+/* 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";
+
+/* globals window, document, PerformanceView, ToolbarView, RecordingsView, DetailsView */
+
+/* exported Cc, Ci, Cu, Cr, loader */
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+var BrowserLoaderModule = {};
+Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+var { loader, require } = BrowserLoaderModule.BrowserLoader({
+ baseURI: "resource://devtools/client/performance/",
+ window
+});
+var { Task } = require("devtools/shared/task");
+/* exported Heritage, ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout */
+var { Heritage, ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+var { gDevTools } = require("devtools/client/framework/devtools");
+
+// Events emitted by various objects in the panel.
+var EVENTS = require("devtools/client/performance/events");
+Object.defineProperty(this, "EVENTS", {
+ value: EVENTS,
+ enumerable: true,
+ writable: false
+});
+
+/* exported React, ReactDOM, JITOptimizationsView, RecordingControls, RecordingButton,
+ RecordingList, RecordingListItem, Services, Waterfall, promise, EventEmitter,
+ DevToolsUtils, system */
+var React = require("devtools/client/shared/vendor/react");
+var ReactDOM = require("devtools/client/shared/vendor/react-dom");
+var Waterfall = React.createFactory(require("devtools/client/performance/components/waterfall"));
+var JITOptimizationsView = React.createFactory(require("devtools/client/performance/components/jit-optimizations"));
+var RecordingControls = React.createFactory(require("devtools/client/performance/components/recording-controls"));
+var RecordingButton = React.createFactory(require("devtools/client/performance/components/recording-button"));
+var RecordingList = React.createFactory(require("devtools/client/performance/components/recording-list"));
+var RecordingListItem = React.createFactory(require("devtools/client/performance/components/recording-list-item"));
+
+var Services = require("Services");
+var promise = require("promise");
+var EventEmitter = require("devtools/shared/event-emitter");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var system = require("devtools/shared/system");
+
+// Logic modules
+/* exported L10N, PerformanceTelemetry, TIMELINE_BLUEPRINT, RecordingUtils,
+ PerformanceUtils, OptimizationsGraph, GraphsController,
+ MarkerDetails, MarkerBlueprintUtils, WaterfallUtils, FrameUtils, CallView, ThreadNode,
+ FrameNode */
+var { L10N } = require("devtools/client/performance/modules/global");
+var { PerformanceTelemetry } = require("devtools/client/performance/modules/logic/telemetry");
+var { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+var RecordingUtils = require("devtools/shared/performance/recording-utils");
+var PerformanceUtils = require("devtools/client/performance/modules/utils");
+var { OptimizationsGraph, GraphsController } = require("devtools/client/performance/modules/widgets/graphs");
+var { MarkerDetails } = require("devtools/client/performance/modules/widgets/marker-details");
+var { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+var WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+var FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+var { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
+var { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+var { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+// Widgets modules
+
+/* exported OptionsView, FlameGraph, FlameGraphUtils, TreeWidget, SideMenuWidget */
+var { OptionsView } = require("devtools/client/shared/options-view");
+var { FlameGraph, FlameGraphUtils } = require("devtools/client/shared/widgets/FlameGraph");
+var { TreeWidget } = require("devtools/client/shared/widgets/TreeWidget");
+var { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+
+/* exported BRANCH_NAME */
+var BRANCH_NAME = "devtools.performance.ui.";
+
+/**
+ * The current target, toolbox and PerformanceFront, set by this tool's host.
+ */
+/* exported gToolbox, gTarget, gFront */
+var gToolbox, gTarget, gFront;
+
+/* exported startupPerformance, shutdownPerformance, PerformanceController */
+
+/**
+ * Initializes the profiler controller and views.
+ */
+var startupPerformance = Task.async(function* () {
+ yield PerformanceController.initialize();
+ yield PerformanceView.initialize();
+ PerformanceController.enableFrontEventListeners();
+});
+
+/**
+ * Destroys the profiler controller and views.
+ */
+var shutdownPerformance = Task.async(function* () {
+ yield PerformanceController.destroy();
+ yield PerformanceView.destroy();
+ PerformanceController.disableFrontEventListeners();
+});
+
+/**
+ * Functions handling target-related lifetime events and
+ * UI interaction.
+ */
+var PerformanceController = {
+ _recordings: [],
+ _currentRecording: null,
+
+ /**
+ * Listen for events emitted by the current tab target and
+ * main UI events.
+ */
+ initialize: Task.async(function* () {
+ this._telemetry = new PerformanceTelemetry(this);
+ this.startRecording = this.startRecording.bind(this);
+ this.stopRecording = this.stopRecording.bind(this);
+ this.importRecording = this.importRecording.bind(this);
+ this.exportRecording = this.exportRecording.bind(this);
+ this.clearRecordings = this.clearRecordings.bind(this);
+ this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+ this._onFrontEvent = this._onFrontEvent.bind(this);
+ this._pipe = this._pipe.bind(this);
+
+ // Store data regarding if e10s is enabled.
+ this._e10s = Services.appinfo.browserTabsRemoteAutostart;
+ this._setMultiprocessAttributes();
+
+ this._prefs = require("devtools/client/performance/modules/global").PREFS;
+ this._prefs.registerObserver();
+ this._prefs.on("pref-changed", this._onPrefChanged);
+
+ ToolbarView.on(EVENTS.UI_PREF_CHANGED, this._onPrefChanged);
+ PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
+ PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+ PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
+ PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
+ RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
+ RecordingsView.on(EVENTS.UI_RECORDING_SELECTED, this._onRecordingSelectFromView);
+ DetailsView.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this._pipe);
+
+ gDevTools.on("pref-changed", this._onThemeChanged);
+ }),
+
+ /**
+ * Remove events handled by the PerformanceController
+ */
+ destroy: function () {
+ this._telemetry.destroy();
+ this._prefs.off("pref-changed", this._onPrefChanged);
+ this._prefs.unregisterObserver();
+
+ ToolbarView.off(EVENTS.UI_PREF_CHANGED, this._onPrefChanged);
+ PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
+ PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+ PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
+ PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
+ RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
+ RecordingsView.off(EVENTS.UI_RECORDING_SELECTED, this._onRecordingSelectFromView);
+ DetailsView.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this._pipe);
+
+ gDevTools.off("pref-changed", this._onThemeChanged);
+ },
+
+ /**
+ * Enables front event listeners.
+ *
+ * The rationale behind this is given by the async intialization of all the
+ * frontend components. Even though the panel is considered "open" only after
+ * both the controller and the view are created, and even though their
+ * initialization is sequential (controller, then view), the controller might
+ * start handling backend events before the view finishes if the event
+ * listeners are added too soon.
+ */
+ enableFrontEventListeners: function () {
+ gFront.on("*", this._onFrontEvent);
+ },
+
+ /**
+ * Disables front event listeners.
+ */
+ disableFrontEventListeners: function () {
+ gFront.off("*", this._onFrontEvent);
+ },
+
+ /**
+ * Returns the current devtools theme.
+ */
+ getTheme: function () {
+ return Services.prefs.getCharPref("devtools.theme");
+ },
+
+ /**
+ * Get a boolean preference setting from `prefName` via the underlying
+ * OptionsView in the ToolbarView. This preference is guaranteed to be
+ * displayed in the UI.
+ *
+ * @param string prefName
+ * @return boolean
+ */
+ getOption: function (prefName) {
+ return ToolbarView.optionsView.getPref(prefName);
+ },
+
+ /**
+ * Get a preference setting from `prefName`. This preference can be of
+ * any type and might not be displayed in the UI.
+ *
+ * @param string prefName
+ * @return any
+ */
+ getPref: function (prefName) {
+ return this._prefs[prefName];
+ },
+
+ /**
+ * Set a preference setting from `prefName`. This preference can be of
+ * any type and might not be displayed in the UI.
+ *
+ * @param string prefName
+ * @param any prefValue
+ */
+ setPref: function (prefName, prefValue) {
+ this._prefs[prefName] = prefValue;
+ },
+
+ /**
+ * Checks whether or not a new recording is supported by the PerformanceFront.
+ * @return Promise:boolean
+ */
+ canCurrentlyRecord: Task.async(function* () {
+ // If we're testing the legacy front, the performance actor will exist,
+ // with `canCurrentlyRecord` method; this ensures we test the legacy path.
+ if (gFront.LEGACY_FRONT) {
+ return true;
+ }
+ let hasActor = yield gTarget.hasActor("performance");
+ if (!hasActor) {
+ return true;
+ }
+ let actorCanCheck = yield gTarget.actorHasMethod("performance", "canCurrentlyRecord");
+ if (!actorCanCheck) {
+ return true;
+ }
+ return (yield gFront.canCurrentlyRecord()).success;
+ }),
+
+ /**
+ * Starts recording with the PerformanceFront.
+ */
+ startRecording: Task.async(function* () {
+ let options = {
+ withMarkers: true,
+ withTicks: this.getOption("enable-framerate"),
+ withMemory: this.getOption("enable-memory"),
+ withFrames: true,
+ withGCEvents: true,
+ withAllocations: this.getOption("enable-allocations"),
+ allocationsSampleProbability: this.getPref("memory-sample-probability"),
+ allocationsMaxLogLength: this.getPref("memory-max-log-length"),
+ bufferSize: this.getPref("profiler-buffer-size"),
+ sampleFrequency: this.getPref("profiler-sample-frequency")
+ };
+
+ let recordingStarted = yield gFront.startRecording(options);
+
+ // In some cases, like when the target has a private browsing tab,
+ // recording is not currently supported because of the profiler module.
+ // Present a notification in this case alerting the user of this issue.
+ if (!recordingStarted) {
+ this.emit(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START);
+ PerformanceView.setState("unavailable");
+ } else {
+ this.emit(EVENTS.BACKEND_READY_AFTER_RECORDING_START);
+ }
+ }),
+
+ /**
+ * Stops recording with the PerformanceFront.
+ */
+ stopRecording: Task.async(function* () {
+ let recording = this.getLatestManualRecording();
+ yield gFront.stopRecording(recording);
+ this.emit(EVENTS.BACKEND_READY_AFTER_RECORDING_STOP);
+ }),
+
+ /**
+ * Saves the given recording to a file. Emits `EVENTS.RECORDING_EXPORTED`
+ * when the file was saved.
+ *
+ * @param PerformanceRecording recording
+ * The model that holds the recording data.
+ * @param nsILocalFile file
+ * The file to stream the data into.
+ */
+ exportRecording: Task.async(function* (_, recording, file) {
+ yield recording.exportRecording(file);
+ this.emit(EVENTS.RECORDING_EXPORTED, recording, file);
+ }),
+
+ /**
+ * Clears all completed recordings from the list as well as the current non-console
+ * recording. Emits `EVENTS.RECORDING_DELETED` when complete so other components can
+ * clean up.
+ */
+ clearRecordings: Task.async(function* () {
+ for (let i = this._recordings.length - 1; i >= 0; i--) {
+ let model = this._recordings[i];
+ if (!model.isConsole() && model.isRecording()) {
+ yield this.stopRecording();
+ }
+ // If last recording is not recording, but finalizing itself,
+ // wait for that to finish
+ if (!model.isRecording() && !model.isCompleted()) {
+ yield this.waitForStateChangeOnRecording(model, "recording-stopped");
+ }
+ // If recording is completed,
+ // clean it up from UI and remove it from the _recordings array.
+ if (model.isCompleted()) {
+ this.emit(EVENTS.RECORDING_DELETED, model);
+ this._recordings.splice(i, 1);
+ }
+ }
+ if (this._recordings.length > 0) {
+ if (!this._recordings.includes(this.getCurrentRecording())) {
+ this.setCurrentRecording(this._recordings[0]);
+ }
+ } else {
+ this.setCurrentRecording(null);
+ }
+ }),
+
+ /**
+ * Loads a recording from a file, adding it to the recordings list. Emits
+ * `EVENTS.RECORDING_IMPORTED` when the file was loaded.
+ *
+ * @param nsILocalFile file
+ * The file to import the data from.
+ */
+ importRecording: Task.async(function* (_, file) {
+ let recording = yield gFront.importRecording(file);
+ this._addRecordingIfUnknown(recording);
+
+ this.emit(EVENTS.RECORDING_IMPORTED, recording);
+ }),
+
+ /**
+ * Sets the currently active PerformanceRecording. Should rarely be called directly,
+ * as RecordingsView handles this when manually selected a recording item. Exceptions
+ * are when clearing the view.
+ * @param PerformanceRecording recording
+ */
+ setCurrentRecording: function (recording) {
+ if (this._currentRecording !== recording) {
+ this._currentRecording = recording;
+ this.emit(EVENTS.RECORDING_SELECTED, recording);
+ }
+ },
+
+ /**
+ * Gets the currently active PerformanceRecording.
+ * @return PerformanceRecording
+ */
+ getCurrentRecording: function () {
+ return this._currentRecording;
+ },
+
+ /**
+ * Get most recently added recording that was triggered manually (via UI).
+ * @return PerformanceRecording
+ */
+ getLatestManualRecording: function () {
+ for (let i = this._recordings.length - 1; i >= 0; i--) {
+ let model = this._recordings[i];
+ if (!model.isConsole() && !model.isImported()) {
+ return this._recordings[i];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Fired from RecordingsView, we listen on the PerformanceController so we can
+ * set it here and re-emit on the controller, where all views can listen.
+ */
+ _onRecordingSelectFromView: function (_, recording) {
+ this.setCurrentRecording(recording);
+ },
+
+ /**
+ * Fired when the ToolbarView fires a PREF_CHANGED event.
+ * with the value.
+ */
+ _onPrefChanged: function (_, prefName, prefValue) {
+ this.emit(EVENTS.PREF_CHANGED, prefName, prefValue);
+ },
+
+ /*
+ * Called when the developer tools theme changes.
+ */
+ _onThemeChanged: function (_, data) {
+ // Right now, gDevTools only emits `pref-changed` for the theme,
+ // but this could change in the future.
+ if (data.pref !== "devtools.theme") {
+ return;
+ }
+
+ this.emit(EVENTS.THEME_CHANGED, data.newValue);
+ },
+
+ /**
+ * Fired from the front on any event. Propagates to other handlers from here.
+ */
+ _onFrontEvent: function (eventName, ...data) {
+ switch (eventName) {
+ case "profiler-status":
+ let [profilerStatus] = data;
+ this.emit(EVENTS.RECORDING_PROFILER_STATUS_UPDATE, profilerStatus);
+ break;
+ case "recording-started":
+ case "recording-stopping":
+ case "recording-stopped":
+ let [recordingModel] = data;
+ this._addRecordingIfUnknown(recordingModel);
+ this.emit(EVENTS.RECORDING_STATE_CHANGE, eventName, recordingModel);
+ break;
+ }
+ },
+
+ /**
+ * Stores a recording internally.
+ *
+ * @param {PerformanceRecordingFront} recording
+ */
+ _addRecordingIfUnknown: function (recording) {
+ if (this._recordings.indexOf(recording) === -1) {
+ this._recordings.push(recording);
+ this.emit(EVENTS.RECORDING_ADDED, recording);
+ }
+ },
+
+ /**
+ * Takes a recording and returns a value between 0 and 1 indicating how much
+ * of the buffer is used.
+ */
+ getBufferUsageForRecording: function (recording) {
+ return gFront.getBufferUsageForRecording(recording);
+ },
+
+ /**
+ * Returns a boolean indicating if any recordings are currently in progress or not.
+ */
+ isRecording: function () {
+ return this._recordings.some(r => r.isRecording());
+ },
+
+ /**
+ * Returns the internal store of recording models.
+ */
+ getRecordings: function () {
+ return this._recordings;
+ },
+
+ /**
+ * Returns traits from the front.
+ */
+ getTraits: function () {
+ return gFront.traits;
+ },
+
+ /**
+ * Utility method taking a string or an array of strings of feature names (like
+ * "withAllocations" or "withMarkers"), and returns whether or not the current
+ * recording supports that feature, based off of UI preferences and server support.
+ *
+ * @option {Array<string>|string} features
+ * A string or array of strings indicating what configuration is needed on the
+ * recording model, like `withTicks`, or `withMemory`.
+ *
+ * @return boolean
+ */
+ isFeatureSupported: function (features) {
+ if (!features) {
+ return true;
+ }
+
+ let recording = this.getCurrentRecording();
+ if (!recording) {
+ return false;
+ }
+
+ let config = recording.getConfiguration();
+ return [].concat(features).every(f => config[f]);
+ },
+
+ /**
+ * Takes an array of PerformanceRecordingFronts and adds them to the internal
+ * store of the UI. Used by the toolbox to lazily seed recordings that
+ * were observed before the panel was loaded in the scenario where `console.profile()`
+ * is used before the tool is loaded.
+ *
+ * @param {Array<PerformanceRecordingFront>} recordings
+ */
+ populateWithRecordings: function (recordings = []) {
+ for (let recording of recordings) {
+ PerformanceController._addRecordingIfUnknown(recording);
+ }
+ this.emit(EVENTS.RECORDINGS_SEEDED);
+ },
+
+ /**
+ * Returns an object with `supported` and `enabled` properties indicating
+ * whether or not the platform is capable of turning on e10s and whether or not
+ * it's already enabled, respectively.
+ *
+ * @return {object}
+ */
+ getMultiprocessStatus: function () {
+ // If testing, set both supported and enabled to true so we
+ // have realtime rendering tests in non-e10s. This function is
+ // overridden wholesale in tests when we want to test multiprocess support
+ // specifically.
+ if (flags.testing) {
+ return { supported: true, enabled: true };
+ }
+ let supported = system.constants.E10S_TESTING_ONLY;
+ // This is only checked on tool startup -- requires a restart if
+ // e10s subsequently enabled.
+ let enabled = this._e10s;
+ return { supported, enabled };
+ },
+
+ /**
+ * Takes a PerformanceRecording and a state, and waits for
+ * the event to be emitted from the front for that recording.
+ *
+ * @param {PerformanceRecordingFront} recording
+ * @param {string} expectedState
+ * @return {Promise}
+ */
+ waitForStateChangeOnRecording: Task.async(function* (recording, expectedState) {
+ let deferred = promise.defer();
+ this.on(EVENTS.RECORDING_STATE_CHANGE, function handler(state, model) {
+ if (state === expectedState && model === recording) {
+ this.off(EVENTS.RECORDING_STATE_CHANGE, handler);
+ deferred.resolve();
+ }
+ });
+ yield deferred.promise;
+ }),
+
+ /**
+ * Called on init, sets an `e10s` attribute on the main view container with
+ * "disabled" if e10s is possible on the platform and just not on, or "unsupported"
+ * if e10s is not possible on the platform. If e10s is on, no attribute is set.
+ */
+ _setMultiprocessAttributes: function () {
+ let { enabled, supported } = this.getMultiprocessStatus();
+ if (!enabled && supported) {
+ $("#performance-view").setAttribute("e10s", "disabled");
+ } else if (!enabled && !supported) {
+ // Could be a chance where the directive goes away yet e10s is still on
+ $("#performance-view").setAttribute("e10s", "unsupported");
+ }
+ },
+
+ /**
+ * Pipes an event from some source to the PerformanceController.
+ */
+ _pipe: function (eventName, ...data) {
+ this.emit(eventName, ...data);
+ },
+
+ toString: () => "[object PerformanceController]"
+};
+
+/**
+ * Convenient way of emitting events from the controller.
+ */
+EventEmitter.decorate(PerformanceController);
+
+/**
+ * DOM query helpers.
+ */
+/* exported $, $$ */
+function $(selector, target = document) {
+ return target.querySelector(selector);
+}
+function $$(selector, target = document) {
+ return target.querySelectorAll(selector);
+}