Bug 1196947 - Performance tools should display a message in private browsing, r=jsantell
authorVictor Porof <vporof@mozilla.com>
Sat, 26 Sep 2015 06:21:09 +0200
changeset 264706 a5156dc31261fc8c1034086e770fc8a2289cbf68
parent 264705 d24fd81248c891451629ff86a024e435ed282bc1
child 264707 a39fdef2430076b3fd399aefc8d01aa309a0cecc
push id65707
push usercbook@mozilla.com
push dateMon, 28 Sep 2015 12:18:34 +0000
treeherdermozilla-inbound@2c0e60a8f736 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1196947
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1196947 - Performance tools should display a message in private browsing, r=jsantell
browser/locales/en-US/chrome/browser/devtools/performance.dtd
devtools/client/performance/events.js
devtools/client/performance/performance-controller.js
devtools/client/performance/performance-view.js
devtools/client/performance/performance.xul
devtools/client/performance/test/browser.ini
devtools/client/performance/test/browser_perf-console-record-01.js
devtools/client/performance/test/browser_perf-console-record-02.js
devtools/client/performance/test/browser_perf-console-record-03.js
devtools/client/performance/test/browser_perf-highlighted.js
devtools/client/performance/test/browser_perf-legacy-front-06.js
devtools/client/performance/test/browser_perf-private-browsing.js
devtools/client/performance/test/head.js
devtools/server/actors/performance.js
devtools/shared/performance/recorder.js
devtools/shared/shared/profiler.js
tools/profiler/gecko/nsIProfiler.idl
tools/profiler/gecko/nsProfiler.cpp
--- a/browser/locales/en-US/chrome/browser/devtools/performance.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/performance.dtd
@@ -30,17 +30,22 @@
   -  when running on a build that can run multiprocess Firefox, but just is not enabled. -->
 <!ENTITY performanceUI.disabledRealTime.disabledE10S "Enable multiprocess Firefox in preferences for rendering recording data in realtime.">
 
 <!-- LOCALIZATION NOTE (performanceUI.bufferStatusFull): This string
   -  is displayed when the profiler's circular buffer has started to overlap. -->
 <!ENTITY performanceUI.bufferStatusFull "The buffer is full. Older samples are now being overwritten.">
 
 <!-- LOCALIZATION NOTE (performanceUI.loadingNotice): This is the label shown
-  -  in the call list view while loading a profile. -->
+  -  in the details view while the profiler is unavailable, for example, while
+  -  in Private Browsing mode. -->
+<!ENTITY performanceUI.unavailableNoticePB "Recording a profile is currently unavailable. Please close all private browsing windows and try again.">
+
+<!-- LOCALIZATION NOTE (performanceUI.loadingNotice): This is the label shown
+  -  in the details view while loading a profile. -->
 <!ENTITY performanceUI.loadingNotice "Loading…">
 
 <!-- LOCALIZATION NOTE (performanceUI.recordButton): This string is displayed
   -  on a button that starts a new profile. -->
 <!ENTITY performanceUI.recordButton.tooltip "Toggle the recording state of a performance recording.">
 
 <!-- LOCALIZATION NOTE (performanceUI.importButton): This string is displayed
   -  on a button that opens a dialog to import a saved profile data file. -->
--- a/devtools/client/performance/events.js
+++ b/devtools/client/performance/events.js
@@ -25,16 +25,19 @@ module.exports = {
   // Emitted by the PerformanceView on import button click
   UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
   // Emitted by the RecordingsView on export button click
   UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
 
   // When a new recording is being tracked in the panel.
   NEW_RECORDING: "Performance:NewRecording",
 
+  // When a new recording can't be successfully created when started.
+  NEW_RECORDING_FAILED: "Performance:NewRecordingFailed",
+
   // When a recording is started or stopped or stopping via the PerformanceController
   RECORDING_STATE_CHANGE: "Performance:RecordingStateChange",
 
   // Emitted by the PerformanceController or RecordingView
   // when a recording model is selected
   RECORDING_SELECTED: "Performance:RecordingSelected",
 
   // When recordings have been cleared out
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -84,24 +84,26 @@ const BRANCH_NAME = "devtools.performanc
 var gToolbox, gTarget, gFront;
 
 /**
  * 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: [],
@@ -126,17 +128,16 @@ var PerformanceController = {
 
     // 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.on("pref-changed", this._onPrefChanged);
 
-    gFront.on("*", this._onFrontEvent);
     ToolbarView.on(EVENTS.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.RECORDING_SELECTED, this._onRecordingSelectFromView);
     DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._pipe);
@@ -146,30 +147,50 @@ var PerformanceController = {
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function() {
     this._telemetry.destroy();
     this._prefs.off("pref-changed", this._onPrefChanged);
 
-    gFront.off("*", this._onFrontEvent);
     ToolbarView.off(EVENTS.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.RECORDING_SELECTED, this._onRecordingSelectFromView);
     DetailsView.off(EVENTS.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
@@ -201,32 +222,59 @@ var PerformanceController = {
    * @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,
       withMemory: this.getOption("enable-memory"),
       withTicks: this.getOption("enable-framerate"),
       withJITOptimizations: this.getOption("enable-jit-optimizations"),
       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")
     };
 
-    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 (!(yield gFront.startRecording(options))) {
+      this.emit(EVENTS.NEW_RECORDING_FAILED);
+      PerformanceView.setState("unavailable");
+    }
   }),
 
   /**
    * Stops recording with the PerformanceFront.
    */
   stopRecording: Task.async(function *() {
     let recording = this.getLatestManualRecording();
     yield gFront.stopRecording(recording);
--- a/devtools/client/performance/performance-view.js
+++ b/devtools/client/performance/performance-view.js
@@ -9,37 +9,40 @@
 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 panes
-  // of the main profiler view. Used in `PerformanceView.setState()`
+  // Mapping of state to selectors for different properties and their values,
+  // from the main profiler view. Used in `PerformanceView.setState()`
   states: {
-    empty: [
-      { deck: "#performance-view", pane: "#empty-notice" }
+    "unavailable": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#unavailable-notice") },
     ],
-    recording: [
-      { deck: "#performance-view", pane: "#performance-view-content" },
-      { deck: "#details-pane-container", pane: "#recording-notice" }
+    "empty": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#empty-notice") }
+    ],
+    "recording": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#performance-view-content") },
+      { sel: "#details-pane-container", opt: "selectedPanel", val: () => $("#recording-notice") }
     ],
     "console-recording": [
-      { deck: "#performance-view", pane: "#performance-view-content" },
-      { deck: "#details-pane-container", pane: "#console-recording-notice" }
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#performance-view-content") },
+      { sel: "#details-pane-container", opt: "selectedPanel", val: () => $("#console-recording-notice") }
     ],
-    recorded: [
-      { deck: "#performance-view", pane: "#performance-view-content" },
-      { deck: "#details-pane-container", pane: "#details-pane" }
+    "recorded": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#performance-view-content") },
+      { sel: "#details-pane-container", opt: "selectedPanel", val: () => $("#details-pane") }
     ],
-    loading: [
-      { deck: "#performance-view", pane: "#performance-view-content" },
-      { deck: "#details-pane-container", pane: "#loading-notice" }
+    "loading": [
+      { sel: "#performance-view", opt: "selectedPanel", val: () => $("#performance-view-content") },
+      { sel: "#details-pane-container", opt: "selectedPanel", val: () => $("#loading-notice") }
     ]
   },
 
   /**
    * Sets up the view with event binding and main subviews.
    */
   initialize: Task.async(function* () {
     this._recordButton = $("#main-record-button");
@@ -47,30 +50,36 @@ var PerformanceView = {
     this._clearButton = $("#clear-button");
 
     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);
 
     for (let button of $$(".record-button")) {
       button.addEventListener("click", this._onRecordButtonClick);
     }
     this._importButton.addEventListener("click", this._onImportButtonClick);
     this._clearButton.addEventListener("click", this._onClearButtonClick);
 
     // Bind to controller events to unlock the record button
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
     PerformanceController.on(EVENTS.PROFILER_STATUS_UPDATED, this._onProfilerStatusUpdated);
     PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
     PerformanceController.on(EVENTS.NEW_RECORDING, this._onRecordingStateChange);
+    PerformanceController.on(EVENTS.NEW_RECORDING_FAILED, this._onNewRecordingFailed);
 
-    this.setState("empty");
+    if (yield 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.
     yield ToolbarView.initialize();
     yield RecordingsView.initialize();
     yield OverviewView.initialize();
     yield DetailsView.initialize();
   }),
@@ -84,41 +93,45 @@ var PerformanceView = {
     }
     this._importButton.removeEventListener("click", this._onImportButtonClick);
     this._clearButton.removeEventListener("click", this._onClearButtonClick);
 
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
     PerformanceController.off(EVENTS.PROFILER_STATUS_UPDATED, this._onProfilerStatusUpdated);
     PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
     PerformanceController.off(EVENTS.NEW_RECORDING, this._onRecordingStateChange);
+    PerformanceController.off(EVENTS.NEW_RECORDING_FAILED, this._onNewRecordingFailed);
 
     yield ToolbarView.destroy();
     yield RecordingsView.destroy();
     yield OverviewView.destroy();
     yield DetailsView.destroy();
   }),
 
   /**
-   * Sets the state of the profiler view. Possible options are "empty",
-   * "recording", "console-recording", "recorded".
+   * Sets the state of the profiler view. Possible options are "unavailable",
+   * "empty", "recording", "console-recording", "recorded".
    */
   setState: function (state) {
     let viewConfig = this.states[state];
     if (!viewConfig) {
       throw new Error(`Invalid state for PerformanceView: ${state}`);
     }
-    for (let { deck, pane } of viewConfig) {
-      $(deck).selectedPanel = $(pane);
+    for (let { sel, opt, val } of viewConfig) {
+      for (let el of $$(sel)) {
+        el[opt] = val();
+      }
     }
 
     this._state = state;
 
     if (state === "console-recording") {
       let recording = PerformanceController.getCurrentRecording();
       let label = recording.getLabel() || "";
+
       // Wrap the label in quotes if it exists for the commands.
       label = label ? `"${label}"` : "";
 
       let startCommand = $(".console-profile-recording-notice .console-profile-command");
       let stopCommand = $(".console-profile-stop-notice .console-profile-command");
 
       startCommand.value = `console.profile(${label})`;
       stopCommand.value = `console.profileEnd(${label})`;
@@ -187,63 +200,71 @@ var PerformanceView = {
   },
 
   /*
    * Toggles the `checked` attribute on the record buttons based
    * on `activate`.
    *
    * @param {boolean} activate
    */
-  _activateRecordButtons: function (activate) {
+  _toggleRecordButtons: function (activate) {
     for (let button of $$(".record-button")) {
       if (activate) {
         button.setAttribute("checked", "true");
       } else {
         button.removeAttribute("checked");
       }
     }
   },
 
   /**
    * When a recording has started.
    */
   _onRecordingStateChange: function () {
     let currentRecording = PerformanceController.getCurrentRecording();
     let recordings = PerformanceController.getRecordings();
 
-    this._activateRecordButtons(recordings.find(r => !r.isConsole() && r.isRecording()));
+    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 (e) {
+    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._recordButton.hasAttribute("checked")) {
       this.emit(EVENTS.UI_STOP_RECORDING);
     } else {
       this._lockRecordButtons(true);
-      this._activateRecordButtons(true);
+      this._toggleRecordButtons(true);
       this.emit(EVENTS.UI_START_RECORDING);
     }
   },
 
   /**
    * Handler for clicking the import button.
    */
   _onImportButtonClick: function(e) {
--- a/devtools/client/performance/performance.xul
+++ b/devtools/client/performance/performance.xul
@@ -152,16 +152,40 @@
                          popup="performance-options-menupopup"
                          tooltiptext="&performanceUI.options.gear.tooltiptext;"/>
         </hbox>
       </toolbar>
 
       <!-- Recording contents and general notice messages -->
       <deck id="performance-view" flex="1">
 
+        <!-- A default notice, shown while initially opening the tool.
+             Keep this element the first child of #performance-view. -->
+        <hbox id="tool-loading-notice"
+              class="notice-container"
+              flex="1">
+        </hbox>
+
+        <!-- "Unavailable" notice, shown when the entire tool is disabled,
+             for example, when in private browsing mode. -->
+        <vbox id="unavailable-notice"
+              class="notice-container"
+              align="center"
+              pack="center"
+              flex="1">
+          <hbox class="devtools-toolbarbutton-group"
+                pack="center">
+            <toolbarbutton class="devtools-toolbarbutton record-button"
+                           label="&performanceUI.startRecording;"
+                           standalone="true"/>
+          </hbox>
+          <label class="tool-disabled-message"
+                 value="&performanceUI.unavailableNoticePB;"/>
+        </vbox>
+
         <!-- "Empty" notice, shown when there's no recordings available -->
         <hbox id="empty-notice"
               class="notice-container"
               align="center"
               pack="center"
               flex="1">
           <hbox class="devtools-toolbarbutton-group"
                 pack="center">
--- a/devtools/client/performance/test/browser.ini
+++ b/devtools/client/performance/test/browser.ini
@@ -3,19 +3,16 @@ tags = devtools
 subsuite = devtools
 support-files =
   doc_allocs.html
   doc_innerHTML.html
   doc_markers.html
   doc_simple-test.html
   head.js
 
-# Commented out tests are profiler tests
-# that need to be moved over to performance tool
-
 [browser_aaa-run-first-leaktest.js]
 [browser_perf-categories-js-calltree.js]
 [browser_perf-clear-01.js]
 [browser_perf-clear-02.js]
 [browser_perf-columns-js-calltree.js]
 [browser_perf-columns-memory-calltree.js]
 [browser_perf-console-record-01.js]
 [browser_perf-console-record-02.js]
@@ -76,16 +73,17 @@ skip-if = os == 'linux' # Bug 1172120
 [browser_perf-overview-render-02.js]
 [browser_perf-overview-render-03.js]
 [browser_perf-overview-render-04.js]
 skip-if = os == 'linux' # bug 1186322
 [browser_perf-overview-selection-01.js]
 [browser_perf-overview-selection-02.js]
 [browser_perf-overview-selection-03.js]
 [browser_perf-overview-time-interval.js]
+[browser_perf-private-browsing.js]
 [browser_perf-states.js]
 skip-if = debug # bug 1203888
 [browser_perf-refresh.js]
 [browser_perf-ui-recording.js]
 skip-if = os == 'linux' # bug 1186322
 [browser_perf-recording-notices-01.js]
 skip-if = os == 'linux' # bug 1186322
 [browser_perf-recording-notices-02.js]
--- a/devtools/client/performance/test/browser_perf-console-record-01.js
+++ b/devtools/client/performance/test/browser_perf-console-record-01.js
@@ -17,17 +17,17 @@ function* spawnTest() {
   yield profileStart;
 
   busyWait(WAIT_TIME);
   let profileEnd = once(front, "recording-stopped");
   console.profileEnd("rust");
   yield profileEnd;
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   let recordings = PerformanceController.getRecordings();
   yield waitUntil(() => PerformanceController.getRecordings().length === 1);
   is(recordings.length, 1, "one recording found in the performance panel.");
   is(recordings[0].isConsole(), true, "recording came from console.profile.");
   is(recordings[0].getLabel(), "rust", "correct label in the recording model.");
 
--- a/devtools/client/performance/test/browser_perf-console-record-02.js
+++ b/devtools/client/performance/test/browser_perf-console-record-02.js
@@ -1,31 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the profiler is populated by in-progress console recordings
  * when it is opened.
  */
 
-var WAIT_TIME = 10;
-
 function* spawnTest() {
   let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
   let front = toolbox.performance;
 
   let profileStart = once(front, "recording-started");
   console.profile("rust");
   yield profileStart;
+
   profileStart = once(front, "recording-started");
   console.profile("rust2");
   yield profileStart;
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   yield waitUntil(() => PerformanceController.getRecordings().length === 2);
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
   is(recordings[0].isRecording(), true, "recording is still recording (1).");
@@ -34,15 +33,16 @@ function* spawnTest() {
   is(recordings[1].isRecording(), true, "recording is still recording (2).");
 
   is(RecordingsView.selectedItem.attachment, recordings[0],
     "The first console recording should be selected.");
 
   let profileEnd = once(front, "recording-stopped");
   console.profileEnd("rust");
   yield profileEnd;
+
   profileEnd = once(front, "recording-stopped");
   console.profileEnd("rust2");
   yield profileEnd;
 
   yield teardown(panel);
   finish();
 }
--- a/devtools/client/performance/test/browser_perf-console-record-03.js
+++ b/devtools/client/performance/test/browser_perf-console-record-03.js
@@ -1,18 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the profiler is populated by in-progress console recordings, and
  * also console recordings that have finished before it was opened.
  */
 
-var WAIT_TIME = 10;
-
 function* spawnTest() {
   let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
   let front = toolbox.performance;
 
   let profileStart = once(front, "recording-started");
   console.profile("rust");
   yield profileStart;
 
@@ -20,17 +18,17 @@ function* spawnTest() {
   console.profileEnd("rust");
   yield profileEnd;
 
   profileStart = once(front, "recording-started");
   console.profile("rust2");
   yield profileStart;
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   yield waitUntil(() => PerformanceController.getRecordings().length === 2);
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
   is(recordings[0].isRecording(), false, "recording is still recording (1).");
--- a/devtools/client/performance/test/browser_perf-highlighted.js
+++ b/devtools/client/performance/test/browser_perf-highlighted.js
@@ -23,17 +23,17 @@ function* spawnTest() {
   let profileEnd = once(front, "recording-stopped");
   console.profileEnd("rust");
   yield profileEnd;
 
   ok(!tab.hasAttribute("highlighted"),
     "performance tab is no longer highlighted when console.profile recording finishes");
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   yield startRecording(panel);
 
   ok(tab.hasAttribute("highlighted"),
     "performance tab is highlighted during recording while in performance tool");
 
   yield stopRecording(panel);
--- a/devtools/client/performance/test/browser_perf-legacy-front-06.js
+++ b/devtools/client/performance/test/browser_perf-legacy-front-06.js
@@ -15,17 +15,17 @@ function* spawnTest() {
   let profileStart = once(front, "recording-started");
   console.profile("rust");
   yield profileStart;
   profileStart = once(front, "recording-started");
   console.profile("rust2");
   yield profileStart;
 
   yield gDevTools.showToolbox(target, "performance");
-  let panel = toolbox.getCurrentPanel();
+  let panel = yield toolbox.getCurrentPanel().open();
   let { panelWin: { PerformanceController, RecordingsView }} = panel;
 
   yield waitUntil(() => PerformanceController.getRecordings().length === 2);
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
   is(recordings[0].isRecording(), true, "recording is still recording (1).");
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-private-browsing.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that disables the frontend when in private browsing mode.
+ */
+
+let gPanelWinTuples = [];
+
+function* spawnTest() {
+  yield testNormalWindow();
+  yield testPrivateWindow();
+  yield testRecordingFailingInWindow(0);
+  yield testRecordingFailingInWindow(1);
+  yield teardownPerfInWindow(1);
+  yield testRecordingSucceedingInWindow(0);
+  yield teardownPerfInWindow(0);
+
+  gPanelWinTuples = null;
+  finish();
+}
+
+function* createPanelInWindow(options) {
+  let win = yield addWindow(options);
+  let tab = yield addTab(SIMPLE_URL, win);
+  let target = TargetFactory.forTab(tab);
+  yield target.makeRemote();
+
+  let toolbox = yield gDevTools.showToolbox(target, "performance");
+  yield toolbox.initPerformance();
+
+  let panel = yield toolbox.getCurrentPanel().open();
+  gPanelWinTuples.push({ panel, win });
+
+  return { panel, win };
+}
+
+function* testNormalWindow() {
+  let { panel } = yield createPanelInWindow({ private: false });
+  let { PerformanceView } = panel.panelWin;
+
+  is(PerformanceView.getState(), "empty",
+    "The initial state of the performance panel view is correct (1).");
+}
+
+function* testPrivateWindow() {
+  let { panel } = yield createPanelInWindow({ private: true });
+  let { PerformanceView } = panel.panelWin;
+
+  is(PerformanceView.getState(), "unavailable",
+    "The initial state of the performance panel view is correct (2).");
+}
+
+function* testRecordingFailingInWindow(index) {
+  let { panel } = gPanelWinTuples[index];
+  let { EVENTS, PerformanceController } = panel.panelWin;
+
+  let onRecordingStarted = () => {
+    ok(false, "Recording should not start while a private window is present.");
+  };
+
+  PerformanceController.on(EVENTS.RECORDING_STARTED, onRecordingStarted);
+
+  let whenFailed = once(PerformanceController, EVENTS.NEW_RECORDING_FAILED);
+  PerformanceController.startRecording();
+  yield whenFailed;
+  ok(true, "Recording has failed.");
+
+  PerformanceController.off(EVENTS.RECORDING_STARTED, onRecordingStarted);
+}
+
+function* testRecordingSucceedingInWindow(index) {
+  let { panel } = gPanelWinTuples[index];
+  let { EVENTS, PerformanceController } = panel.panelWin;
+
+  let onRecordingFailed = () => {
+    ok(false, "Recording should start while now private windows are present.");
+  };
+
+  PerformanceController.on(EVENTS.NEW_RECORDING_FAILED, onRecordingFailed);
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+  ok(true, "Recording has succeeded.");
+
+  PerformanceController.off(EVENTS.RECORDING_STARTED, onRecordingFailed);
+}
+
+function* teardownPerfInWindow(index) {
+  let { panel, win } = gPanelWinTuples[index];
+  yield teardown(panel, win);
+  win.close();
+}
--- a/devtools/client/performance/test/head.js
+++ b/devtools/client/performance/test/head.js
@@ -100,16 +100,39 @@ registerCleanupFunction(() => {
   // Rollback any pref changes
   Object.keys(DEFAULT_PREFS).forEach(pref => {
     Preferences.set(pref, DEFAULT_PREFS[pref]);
   });
 
   Cu.forceGC();
 });
 
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+  Services.obs.addObserver(function observer(aSubject, aTopic) {
+    if (aWindow == aSubject) {
+      Services.obs.removeObserver(observer, aTopic);
+      executeSoon(aCallback);
+    }
+  }, "browser-delayed-startup-finished", false);
+}
+
+function addWindow(windowOptions) {
+  let deferred = Promise.defer();
+  let win = OpenBrowserWindow(windowOptions);
+
+  whenDelayedStartupFinished(win, () => {
+    executeSoon(() => {
+      deferred.resolve(win);
+    });
+  });
+
+  return deferred.promise;
+}
+
 function addTab(aUrl, aWindow) {
   info("Adding tab: " + aUrl);
 
   let deferred = Promise.defer();
   let targetWindow = aWindow || window;
   let targetBrowser = targetWindow.gBrowser;
 
   targetWindow.focus();
@@ -228,16 +251,18 @@ function initPerformance(aUrl, tool="per
     // TEST_PROFILER_FILTER_STATUS = array
     merge(target, targetOps);
 
     let toolbox = yield gDevTools.showToolbox(target, tool);
 
     // Wait for the performance tool to be spun up
     yield toolbox.initPerformance();
 
+    // Panel is already initialized after `showToolbox` and `initPerformance`,
+    // no need to wait for `open` here.
     let panel = toolbox.getCurrentPanel();
     return { target, panel, toolbox };
   });
 }
 
 /**
  * Initializes a webconsole panel. Returns a target, panel and toolbox reference.
  * Also returns a console property that allows calls to `profile` and `profileEnd`.
@@ -271,22 +296,22 @@ function consoleExecute (console, method
         resolve();
         return;
       }
     }
   }
   return promise;
 }
 
-function* teardown(panel) {
+function* teardown(panel, win = window) {
   info("Destroying the performance tool.");
 
   let tab = panel.target.tab;
   yield panel._toolbox.destroy();
-  yield removeTab(tab);
+  yield removeTab(tab, win);
 }
 
 function idleWait(time) {
   return DevToolsUtils.waitForTime(time);
 }
 
 function busyWait(time) {
   let start = Date.now();
--- a/devtools/server/actors/performance.js
+++ b/devtools/server/actors/performance.js
@@ -102,35 +102,48 @@ var PerformanceActor = exports.Performan
   connect: method(function (config) {
     this.bridge.connect({ systemClient: config.systemClient });
     return { traits: this.traits };
   }, {
     request: { options: Arg(0, "nullable:json") },
     response: RetVal("json")
   }),
 
+  canCurrentlyRecord: method(function() {
+    return this.bridge.canCurrentlyRecord();
+  }, {
+    response: { value: RetVal("json") }
+  }),
+
   startRecording: method(Task.async(function *(options={}) {
+    if (!this.bridge.canCurrentlyRecord().success) {
+      return null;
+    }
+
     let normalizedOptions = normalizePerformanceFeatures(options, this.traits.features);
     let recording = yield this.bridge.startRecording(normalizedOptions);
-
     this.manage(recording);
 
     return recording;
   }), {
     request: {
       options: Arg(0, "nullable:json"),
     },
-    response: RetVal("performance-recording"),
+    response: {
+      recording: RetVal("nullable:performance-recording")
+    }
   }),
 
   stopRecording: actorBridge("stopRecording", {
     request: {
       options: Arg(0, "performance-recording"),
     },
-    response: RetVal("performance-recording"),
+    response: {
+      recording: RetVal("performance-recording")
+    }
   }),
 
   isRecording: actorBridge("isRecording", {
     response: { isRecording: RetVal("boolean") }
   }),
 
   getRecordings: actorBridge("getRecordings", {
     response: { recordings: RetVal("array:performance-recording") }
--- a/devtools/shared/performance/recorder.js
+++ b/devtools/shared/performance/recorder.js
@@ -278,16 +278,35 @@ const PerformanceRecorder = exports.Perf
     let activeRecordings = this._recordings.filter(r => r.isRecording());
 
     if (activeRecordings.length) {
       events.emit(this, "timeline-data", eventName, eventData, activeRecordings);
     }
   },
 
   /**
+   * Checks whether or not recording is currently supported. At the moment,
+   * this is only influenced by private browsing mode and the profiler.
+   */
+  canCurrentlyRecord: function() {
+    let success = true;
+    let reasons = [];
+
+    if (!Profiler.canProfile()) {
+      success = false,
+      reasons.push("profiler-unavailable");
+    }
+
+    // Check other factors that will affect the possibility of successfully
+    // starting a recording here.
+
+    return { success, reasons };
+  },
+
+  /**
    * Begins a recording session
    *
    * @param boolean options.withMarkers
    * @param boolean options.withJITOptimizations
    * @param boolean options.withTicks
    * @param boolean options.withMemory
    * @param boolean options.withAllocations
    * @param boolean options.allocationsSampleProbability
--- a/devtools/shared/shared/profiler.js
+++ b/devtools/shared/shared/profiler.js
@@ -50,25 +50,34 @@ const ProfilerManager = (function () {
 
     // How many subscribers there
     _profilerStatusSubscribers: 0,
 
     /**
      * The nsIProfiler is target agnostic and interacts with the whole platform.
      * Therefore, special care needs to be given to make sure different profiler
      * consumers (i.e. "toolboxes") don't interfere with each other. Register
-     * the instance here.
+     * the profiler actor instances here.
+     *
+     * @param Profiler instance
+     *        A profiler actor class.
      */
     addInstance: function (instance) {
       consumers.add(instance);
 
       // Lazily register events
       this.registerEventListeners();
     },
 
+    /**
+     * Remove the profiler actor instances here.
+     *
+     * @param Profiler instance
+     *        A profiler actor class.
+     */
     removeInstance: function (instance) {
       consumers.delete(instance);
 
       if (this.length < 0) {
         let msg = "Somehow the number of started profilers is now negative.";
         DevToolsUtils.reportException("Profiler", msg);
       }
 
@@ -96,35 +105,46 @@ const ProfilerManager = (function () {
         features: options.features || DEFAULT_PROFILER_OPTIONS.features,
         threadFilters: options.threadFilters || DEFAULT_PROFILER_OPTIONS.threadFilters,
       };
 
       // The start time should be before any samples we might be
       // interested in.
       let currentTime = nsIProfilerModule.getElapsedTime();
 
-      nsIProfilerModule.StartProfiler(
-        config.entries,
-        config.interval,
-        config.features,
-        config.features.length,
-        config.threadFilters,
-        config.threadFilters.length
-      );
-      let { position, totalSize, generation } = this.getBufferInfo();
+      try {
+        nsIProfilerModule.StartProfiler(
+          config.entries,
+          config.interval,
+          config.features,
+          config.features.length,
+          config.threadFilters,
+          config.threadFilters.length
+        );
+      } catch (e) {
+        // For some reason, the profiler couldn't be started. This could happen,
+        // for example, when in private browsing mode.
+        Cu.reportError(`Could not start the profiler module: ${e.message}`);
+        return { started: false, reason: e, currentTime };
+      }
 
       this._updateProfilerStatusPolling();
+
+      let { position, totalSize, generation } = this.getBufferInfo();
       return { started: true, position, totalSize, generation, currentTime };
     },
 
+    /**
+     * Attempts to stop the nsIProfiler module.
+     */
     stop: function () {
       // Actually stop the profiler only if the last client has stopped profiling.
-      // Since this is used as a root actor, and the profiler module interacts with the
-      // whole platform, we need to avoid a case in which the profiler is stopped
-      // when there might be other clients still profiling.
+      // Since this is used as a root actor, and the profiler module interacts
+      // with the whole platform, we need to avoid a case in which the profiler
+      // is stopped when there might be other clients still profiling.
       if (this.length <= 1) {
         nsIProfilerModule.StopProfiler();
       }
       this._updateProfilerStatusPolling();
       return { started: false };
     },
 
     /**
@@ -301,17 +321,18 @@ const ProfilerManager = (function () {
       }
     },
 
     /**
      * Unregisters handlers for all system events.
      */
     unregisterEventListeners: function () {
       if (this._eventsRegistered) {
-        PROFILER_SYSTEM_EVENTS.forEach(eventName => Services.obs.removeObserver(this, eventName));
+        PROFILER_SYSTEM_EVENTS.forEach(eventName =>
+          Services.obs.removeObserver(this, eventName));
         this._eventsRegistered = false;
       }
     },
 
     /**
      * Takes an event name and additional data and emits them
      * through each profiler instance that is subscribed to the event.
      *
@@ -478,16 +499,24 @@ var Profiler = exports.Profiler = Class(
         response.push(e);
       }
     });
     return { registered: response };
   },
 });
 
 /**
+ * Checks whether or not the profiler module can currently run.
+ * @return boolean
+ */
+Profiler.canProfile = function() {
+  return nsIProfilerModule.CanProfile();
+};
+
+/**
  * JSON.stringify callback used in Profiler.prototype.observe.
  */
 function cycleBreaker(key, value) {
   if (key == "wrappedJSObject") {
     return undefined;
   }
   return value;
 }
--- a/tools/profiler/gecko/nsIProfiler.idl
+++ b/tools/profiler/gecko/nsIProfiler.idl
@@ -1,25 +1,26 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
- 
+
 #include "nsISupports.idl"
 
 %{C++
 template<class T> class nsTArray;
 class nsCString;
 %}
 
 [ref] native StringArrayRef(const nsTArray<nsCString>);
 
-[scriptable, uuid(921e1223-b1ea-4906-bb26-a846e6b6835b)]
+[scriptable, uuid(ff398a14-df1c-4966-9ab2-772ea6a6da6c)]
 interface nsIProfiler : nsISupports
 {
+  boolean CanProfile();
   void StartProfiler(in uint32_t aEntries, in double aInterval,
                       [array, size_is(aFeatureCount)] in string aFeatures,
                       in uint32_t aFeatureCount,
                       [array, size_is(aFilterCount), optional] in string aThreadNameFilters,
                       [optional] in uint32_t aFilterCount);
   void StopProfiler();
   boolean IsPaused();
   void PauseSampling();
--- a/tools/profiler/gecko/nsProfiler.cpp
+++ b/tools/profiler/gecko/nsProfiler.cpp
@@ -66,16 +66,23 @@ nsProfiler::Observe(nsISupports *aSubjec
   } else if (strcmp(aTopic, "last-pb-context-exited") == 0) {
     mLockedForPrivateBrowsing = false;
     profiler_unlock();
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsProfiler::CanProfile(bool *aCanProfile)
+{
+  *aCanProfile = !mLockedForPrivateBrowsing;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsProfiler::StartProfiler(uint32_t aEntries, double aInterval,
                           const char** aFeatures, uint32_t aFeatureCount,
                           const char** aThreadNameFilters, uint32_t aFilterCount)
 {
   if (mLockedForPrivateBrowsing) {
     return NS_ERROR_NOT_AVAILABLE;
   }