summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/performance-view.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/performance-view.js')
-rw-r--r--devtools/client/performance/performance-view.js490
1 files changed, 490 insertions, 0 deletions
diff --git a/devtools/client/performance/performance-view.js b/devtools/client/performance/performance-view.js
new file mode 100644
index 0000000000..98775327b6
--- /dev/null
+++ b/devtools/client/performance/performance-view.js
@@ -0,0 +1,490 @@
+/* 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/. */
+/* globals $, $$, PerformanceController */
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const RecordingControls = React.createFactory(
+ require("devtools/client/performance/components/RecordingControls")
+);
+const RecordingButton = React.createFactory(
+ require("devtools/client/performance/components/RecordingButton")
+);
+
+const EVENTS = require("devtools/client/performance/events");
+const PerformanceUtils = require("devtools/client/performance/modules/utils");
+const { DetailsView } = require("devtools/client/performance/views/details");
+const { OverviewView } = require("devtools/client/performance/views/overview");
+const {
+ RecordingsView,
+} = require("devtools/client/performance/views/recordings");
+const { ToolbarView } = require("devtools/client/performance/views/toolbar");
+
+const { L10N } = require("devtools/client/performance/modules/global");
+/**
+ * Master view handler for the performance tool.
+ */
+var PerformanceView = {
+ _state: null,
+
+ // Set to true if the front emits a "buffer-status" event, indicating
+ // that the server has support for determining buffer status.
+ _bufferStatusSupported: false,
+
+ // Mapping of state to selectors for different properties and their values,
+ // from the main profiler view. Used in `PerformanceView.setState()`
+ states: {
+ unavailable: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#unavailable-notice"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => true,
+ },
+ ],
+ empty: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#empty-notice"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => true,
+ },
+ ],
+ recording: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false,
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#recording-notice"),
+ },
+ ],
+ "console-recording": [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false,
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#console-recording-notice"),
+ },
+ ],
+ recorded: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false,
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#details-pane"),
+ },
+ ],
+ loading: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false,
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#loading-notice"),
+ },
+ ],
+ },
+
+ /**
+ * Sets up the view with event binding and main subviews.
+ */
+ async initialize() {
+ this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
+ this._onImportButtonClick = this._onImportButtonClick.bind(this);
+ this._onClearButtonClick = this._onClearButtonClick.bind(this);
+ this._onRecordingSelected = this._onRecordingSelected.bind(this);
+ this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this);
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onNewRecordingFailed = this._onNewRecordingFailed.bind(this);
+
+ // Bind to controller events to unlock the record button
+ PerformanceController.on(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingSelected
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
+ this._onProfilerStatusUpdated
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_ADDED,
+ this._onRecordingStateChange
+ );
+ PerformanceController.on(
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ this._onNewRecordingFailed
+ );
+
+ if (await PerformanceController.canCurrentlyRecord()) {
+ this.setState("empty");
+ } else {
+ this.setState("unavailable");
+ }
+
+ // Initialize the ToolbarView first, because other views may need access
+ // to the OptionsView via the controller, to read prefs.
+ await ToolbarView.initialize();
+ await RecordingsView.initialize();
+ await OverviewView.initialize();
+ await DetailsView.initialize();
+
+ // DE-XUL: Begin migrating the toolbar to React. Temporarily hold state here.
+ this._recordingControlsState = {
+ onRecordButtonClick: this._onRecordButtonClick,
+ onImportButtonClick: this._onImportButtonClick,
+ onClearButtonClick: this._onClearButtonClick,
+ isRecording: false,
+ isDisabled: false,
+ };
+ // Mount to an HTML element.
+ const { createHtmlMount } = PerformanceUtils;
+ this._recordingControlsMount = createHtmlMount(
+ $("#recording-controls-mount")
+ );
+ this._recordingButtonsMounts = Array.from(
+ $$(".recording-button-mount")
+ ).map(createHtmlMount);
+
+ this._renderRecordingControls();
+ },
+
+ /**
+ * DE-XUL: Render the recording controls and buttons using React.
+ */
+ _renderRecordingControls: function() {
+ ReactDOM.render(
+ RecordingControls(this._recordingControlsState),
+ this._recordingControlsMount
+ );
+ for (const button of this._recordingButtonsMounts) {
+ ReactDOM.render(RecordingButton(this._recordingControlsState), button);
+ }
+ },
+
+ /**
+ * Unbinds events and destroys subviews.
+ */
+ async destroy() {
+ PerformanceController.off(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingSelected
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
+ this._onProfilerStatusUpdated
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_ADDED,
+ this._onRecordingStateChange
+ );
+ PerformanceController.off(
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ this._onNewRecordingFailed
+ );
+
+ await ToolbarView.destroy();
+ await RecordingsView.destroy();
+ await OverviewView.destroy();
+ await DetailsView.destroy();
+ },
+
+ /**
+ * Sets the state of the profiler view. Possible options are "unavailable",
+ * "empty", "recording", "console-recording", "recorded".
+ */
+ setState: function(state) {
+ // Make sure that the focus isn't captured on a hidden iframe. This fixes a
+ // XUL bug where shortcuts stop working.
+ const iframes = window.document.querySelectorAll("iframe");
+ for (const iframe of iframes) {
+ iframe.blur();
+ }
+ window.focus();
+
+ const viewConfig = this.states[state];
+ if (!viewConfig) {
+ throw new Error(`Invalid state for PerformanceView: ${state}`);
+ }
+ for (const { sel, opt, val } of viewConfig) {
+ for (const el of $$(sel)) {
+ el[opt] = val();
+ }
+ }
+
+ this._state = state;
+
+ if (state === "console-recording") {
+ const recording = PerformanceController.getCurrentRecording();
+ let label = recording.getLabel() || "";
+
+ // Wrap the label in quotes if it exists for the commands.
+ label = label ? `"${label}"` : "";
+
+ const startCommand = $(
+ ".console-profile-recording-notice .console-profile-command"
+ );
+ const stopCommand = $(
+ ".console-profile-stop-notice .console-profile-command"
+ );
+
+ startCommand.value = `console.profile(${label})`;
+ stopCommand.value = `console.profileEnd(${label})`;
+ }
+
+ this.updateBufferStatus();
+ this.emit(EVENTS.UI_STATE_CHANGED, state);
+ },
+
+ /**
+ * Returns the state of the PerformanceView.
+ */
+ getState: function() {
+ return this._state;
+ },
+
+ /**
+ * Reset the displayed buffer status.
+ * Called for every target-switching.
+ */
+ resetBufferStatus() {
+ this._bufferStatusSupported = false;
+ $("#details-pane-container").removeAttribute("buffer-status");
+ },
+
+ /**
+ * Updates the displayed buffer status.
+ */
+ updateBufferStatus: function() {
+ // If we've never seen a "buffer-status" event from the front, ignore
+ // and keep the buffer elements hidden.
+ if (!this._bufferStatusSupported) {
+ return;
+ }
+
+ const recording = PerformanceController.getCurrentRecording();
+ if (!recording || !recording.isRecording()) {
+ return;
+ }
+
+ const bufferUsage =
+ PerformanceController.getBufferUsageForRecording(recording) || 0;
+
+ // Normalize to a percentage value
+ const percent = Math.floor(bufferUsage * 100);
+
+ const $container = $("#details-pane-container");
+ const $bufferLabel = $(".buffer-status-message", $container.selectedPanel);
+
+ // Be a little flexible on the buffer status, although not sure how
+ // this could happen, as RecordingModel clamps.
+ if (percent >= 99) {
+ $container.setAttribute("buffer-status", "full");
+ } else {
+ $container.setAttribute("buffer-status", "in-progress");
+ }
+
+ $bufferLabel.value = L10N.getFormatStr("profiler.bufferFull", percent);
+ this.emit(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED, percent);
+ },
+
+ /**
+ * Toggles the `locked` attribute on the record buttons based
+ * on `lock`.
+ *
+ * @param {boolean} lock
+ */
+ _lockRecordButtons: function(lock) {
+ this._recordingControlsState.isLocked = lock;
+ this._renderRecordingControls();
+ },
+
+ /*
+ * Toggles the `checked` attribute on the record buttons based
+ * on `activate`.
+ *
+ * @param {boolean} activate
+ */
+ _toggleRecordButtons: function(activate) {
+ this._recordingControlsState.isRecording = activate;
+ this._renderRecordingControls();
+ },
+
+ /**
+ * When a recording has started.
+ */
+ _onRecordingStateChange: function() {
+ const currentRecording = PerformanceController.getCurrentRecording();
+ const recordings = PerformanceController.getRecordings();
+
+ this._toggleRecordButtons(
+ !!recordings.find(r => !r.isConsole() && r.isRecording())
+ );
+ this._lockRecordButtons(
+ !!recordings.find(r => !r.isConsole() && r.isFinalizing())
+ );
+
+ if (currentRecording && currentRecording.isFinalizing()) {
+ this.setState("loading");
+ }
+ if (currentRecording && currentRecording.isCompleted()) {
+ this.setState("recorded");
+ }
+ if (currentRecording && currentRecording.isRecording()) {
+ this.updateBufferStatus();
+ }
+ },
+
+ /**
+ * When starting a recording has failed.
+ */
+ _onNewRecordingFailed: function() {
+ this._lockRecordButtons(false);
+ this._toggleRecordButtons(false);
+ },
+
+ /**
+ * Handler for clicking the clear button.
+ */
+ _onClearButtonClick: function(e) {
+ this.emit(EVENTS.UI_CLEAR_RECORDINGS);
+ },
+
+ /**
+ * Handler for clicking the record button.
+ */
+ _onRecordButtonClick: function(e) {
+ if (this._recordingControlsState.isRecording) {
+ this.emit(EVENTS.UI_STOP_RECORDING);
+ } else {
+ this._lockRecordButtons(true);
+ this._toggleRecordButtons(true);
+ this.emit(EVENTS.UI_START_RECORDING);
+ }
+ },
+
+ /**
+ * Handler for clicking the import button.
+ */
+ _onImportButtonClick: function(e) {
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(
+ window,
+ L10N.getStr("recordingsList.importDialogTitle"),
+ Ci.nsIFilePicker.modeOpen
+ );
+ fp.appendFilter(
+ L10N.getStr("recordingsList.saveDialogJSONFilter"),
+ "*.json"
+ );
+ fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
+
+ fp.open(rv => {
+ if (rv == Ci.nsIFilePicker.returnOK) {
+ this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file);
+ }
+ });
+ },
+
+ /**
+ * Fired when a recording is selected. Used to toggle the profiler view state.
+ */
+ _onRecordingSelected: function(recording) {
+ if (!recording) {
+ this.setState("empty");
+ } else if (recording.isRecording() && recording.isConsole()) {
+ this.setState("console-recording");
+ } else if (recording.isRecording()) {
+ this.setState("recording");
+ } else {
+ this.setState("recorded");
+ }
+ },
+
+ /**
+ * Fired when the controller has updated information on the buffer's status.
+ * Update the buffer status display if shown.
+ */
+ _onProfilerStatusUpdated: function(profilerStatus) {
+ // We only care about buffer status here, so check to see
+ // if it has position.
+ if (!profilerStatus || profilerStatus.position === void 0) {
+ return;
+ }
+ // If this is our first buffer event, set the status and add a class
+ if (!this._bufferStatusSupported) {
+ this._bufferStatusSupported = true;
+ $("#details-pane-container").setAttribute("buffer-status", "in-progress");
+ }
+
+ if (!this.getState("recording") && !this.getState("console-recording")) {
+ return;
+ }
+
+ this.updateBufferStatus();
+ },
+
+ toString: () => "[object PerformanceView]",
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(PerformanceView);
+
+exports.PerformanceView = PerformanceView;