Bug 1454061 - Introduce redux to performance recording panel; r=julienw
authorGreg Tatum <gtatum@mozilla.com>
Fri, 13 Apr 2018 15:29:34 -0500
changeset 467667 734ac146303fb18aaae3e6334b9ee19adf351370
parent 467666 7197059eb99c95e33f7b6bf5f102e81a4fe7437d
child 467668 a231670d9c66f178453520089f837f36390d0c55
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjulienw
bugs1454061
milestone61.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 1454061 - Introduce redux to performance recording panel; r=julienw MozReview-Commit-ID: 1jdAAo1Kb21
devtools/client/performance-new/components/Description.js
devtools/client/performance-new/components/Perf.js
devtools/client/performance-new/components/PerfSettings.js
devtools/client/performance-new/components/Range.js
devtools/client/performance-new/components/RecordingButton.js
devtools/client/performance-new/components/Settings.js
devtools/client/performance-new/components/moz.build
devtools/client/performance-new/initializer.js
devtools/client/performance-new/moz.build
devtools/client/performance-new/store/actions.js
devtools/client/performance-new/store/moz.build
devtools/client/performance-new/store/reducers.js
devtools/client/performance-new/store/selectors.js
devtools/client/performance-new/test/chrome/head.js
devtools/client/performance-new/test/chrome/test_perf-state-01.html
devtools/client/performance-new/test/chrome/test_perf-state-02.html
devtools/client/performance-new/test/chrome/test_perf-state-03.html
devtools/client/performance-new/test/chrome/test_perf-state-04.html
devtools/client/performance-new/utils.js
devtools/client/themes/perf.css
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/Description.js
@@ -0,0 +1,78 @@
+/* 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";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const { div, button, p } = require("devtools/client/shared/vendor/react-dom-factories");
+const { openLink } = require("devtools/client/shared/link");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const selectors = require("devtools/client/performance-new/store/selectors");
+
+/**
+ * This component provides a helpful description for what is going on in the component
+ * and provides some external links.
+ */
+class Description extends PureComponent {
+  static get propTypes() {
+    return {
+      // StateProps:
+      toolbox: PropTypes.object.isRequired
+    };
+  }
+
+  constructor(props) {
+    super(props);
+    this.handleLinkClick = this.handleLinkClick.bind(this);
+  }
+
+  handleLinkClick(event) {
+    openLink(event.target.value, this.props.toolbox);
+  }
+
+  /**
+   * Implement links as buttons to avoid any risk of loading the link in the
+   * the panel.
+   */
+  renderLink(href, text) {
+    return button(
+      {
+        className: "perf-external-link",
+        value: href,
+        onClick: this.handleLinkClick
+      },
+      text
+    );
+  }
+
+  render() {
+    return div(
+      { className: "perf-description" },
+      p(null,
+        "This new recording panel is a bit different from the existing " +
+          "performance panel. It records the entire browser, and then opens up " +
+          "and shares the profile with ",
+        this.renderLink(
+          "https://perf-html.io",
+          "perf-html.io"
+        ),
+        ", a Mozilla performance analysis tool."
+      ),
+      p(null,
+        "This is still a prototype. Join along or file bugs at: ",
+        this.renderLink(
+          "https://github.com/devtools-html/perf.html",
+          "github.com/devtools-html/perf.html"
+        ),
+        "."
+      )
+    );
+  }
+}
+
+const mapStateToProps = state => ({
+  toolbox: selectors.getToolbox(state)
+});
+
+module.exports = connect(mapStateToProps)(Description);
--- a/devtools/client/performance-new/components/Perf.js
+++ b/devtools/client/performance-new/components/Perf.js
@@ -1,110 +1,107 @@
 /* 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";
 
 const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
-const { div, button, p, span, img } = require("devtools/client/shared/vendor/react-dom-factories");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { div } = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const PerfSettings = createFactory(require("devtools/client/performance-new/components/PerfSettings.js"));
-const { openLink } = require("devtools/client/shared/link");
+const RecordingButton = createFactory(require("devtools/client/performance-new/components/RecordingButton.js"));
+const Settings = createFactory(require("devtools/client/performance-new/components/Settings.js"));
+const Description = createFactory(require("devtools/client/performance-new/components/Description.js"));
+const actions = require("devtools/client/performance-new/store/actions");
+const { recordingState: {
+  NOT_YET_KNOWN,
+  AVAILABLE_TO_RECORD,
+  REQUEST_TO_START_RECORDING,
+  REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER,
+  REQUEST_TO_STOP_PROFILER,
+  RECORDING,
+  OTHER_IS_RECORDING,
+  LOCKED_BY_PRIVATE_BROWSING,
+}} = require("devtools/client/performance-new/utils");
+const selectors = require("devtools/client/performance-new/store/selectors");
 
 /**
- * The recordingState is one of the following:
- **/
-
-// The initial state before we've queried the PerfActor
-const NOT_YET_KNOWN = "not-yet-known";
-// The profiler is available, we haven't started recording yet.
-const AVAILABLE_TO_RECORD = "available-to-record";
-// An async request has been sent to start the profiler.
-const REQUEST_TO_START_RECORDING = "request-to-start-recording";
-// An async request has been sent to get the profile and stop the profiler.
-const REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER =
-  "request-to-get-profile-and-stop-profiler";
-// An async request has been sent to stop the profiler.
-const REQUEST_TO_STOP_PROFILER = "request-to-stop-profiler";
-// The profiler notified us that our request to start it actually started it.
-const RECORDING = "recording";
-// Some other code with access to the profiler started it.
-const OTHER_IS_RECORDING = "other-is-recording";
-// Profiling is not available when in private browsing mode.
-const LOCKED_BY_PRIVATE_BROWSING = "locked-by-private-browsing";
-
+ * This is the top level component for initializing the performance recording panel.
+ * It has two jobs:
+ *
+ * 1) It manages state changes for the performance recording. e.g. If the profiler
+ * suddenly becomes unavailable, it needs to react to those changes, and update the
+ * recordingState in the store.
+ *
+ * 2) It mounts all of the sub components, but is itself very light on actual
+ * markup for presentation.
+ */
 class Perf extends PureComponent {
   static get propTypes() {
     return {
-      toolbox: PropTypes.object.isRequired,
+      // StateProps:
       perfFront: PropTypes.object.isRequired,
-      receiveProfile: PropTypes.func.isRequired
+      recordingState: PropTypes.string.isRequired,
+      isSupportedPlatform: PropTypes.bool,
+
+      // DispatchProps:
+      changeRecordingState: PropTypes.func.isRequired,
+      reportProfilerReady: PropTypes.func.isRequired
     };
   }
 
   constructor(props) {
     super(props);
-    this.state = {
-      recordingState: NOT_YET_KNOWN,
-      recordingUnexpectedlyStopped: false,
-      // The following is either "null" for unknown, or a boolean value.
-      isSupportedPlatform: null
-    };
-    this.startRecording = this.startRecording.bind(this);
-    this.getProfileAndStopProfiler = this.getProfileAndStopProfiler.bind(this);
-    this.stopProfilerAndDiscardProfile = this.stopProfilerAndDiscardProfile.bind(this);
     this.handleProfilerStarting = this.handleProfilerStarting.bind(this);
     this.handleProfilerStopping = this.handleProfilerStopping.bind(this);
     this.handlePrivateBrowsingStarting = this.handlePrivateBrowsingStarting.bind(this);
     this.handlePrivateBrowsingEnding = this.handlePrivateBrowsingEnding.bind(this);
-    this.settingsComponentCreated = this.settingsComponentCreated.bind(this);
-    this.handleLinkClick = this.handleLinkClick.bind(this);
   }
 
   componentDidMount() {
-    const { perfFront } = this.props;
+    const { perfFront, reportProfilerReady } = this.props;
 
     // Ask for the initial state of the profiler.
     Promise.all([
       perfFront.isActive(),
       perfFront.isSupportedPlatform(),
       perfFront.isLockedForPrivateBrowsing(),
     ]).then((results) => {
       const [
         isActive,
         isSupportedPlatform,
         isLockedForPrivateBrowsing
       ] = results;
 
-      let recordingState = this.state.recordingState;
+      let recordingState = this.props.recordingState;
       // It's theoretically possible we got an event that already let us know about
       // the current state of the profiler.
       if (recordingState === NOT_YET_KNOWN && isSupportedPlatform) {
         if (isLockedForPrivateBrowsing) {
           recordingState = LOCKED_BY_PRIVATE_BROWSING;
         } else {
           recordingState = isActive
             ? OTHER_IS_RECORDING
             : AVAILABLE_TO_RECORD;
         }
       }
-      this.setState({ isSupportedPlatform, recordingState });
+      reportProfilerReady(isSupportedPlatform, recordingState);
     });
 
     // Handle when the profiler changes state. It might be us, it might be someone else.
     this.props.perfFront.on("profiler-started", this.handleProfilerStarting);
     this.props.perfFront.on("profiler-stopped", this.handleProfilerStopping);
     this.props.perfFront.on("profile-locked-by-private-browsing",
       this.handlePrivateBrowsingStarting);
     this.props.perfFront.on("profile-unlocked-from-private-browsing",
       this.handlePrivateBrowsingEnding);
   }
 
   componentWillUnmount() {
-    switch (this.state.recordingState) {
+    switch (this.props.recordingState) {
       case NOT_YET_KNOWN:
       case AVAILABLE_TO_RECORD:
       case REQUEST_TO_STOP_PROFILER:
       case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
       case LOCKED_BY_PRIVATE_BROWSING:
       case OTHER_IS_RECORDING:
         // Do nothing for these states.
         break;
@@ -114,317 +111,144 @@ class Perf extends PureComponent {
         this.props.perfFront.stopProfilerAndDiscardProfile();
         break;
 
       default:
         throw new Error("Unhandled recording state.");
     }
   }
 
-  /**
-   * Store a reference to the settings component. This gives the <Perf> component
-   * access to the `.getRecordingSettings()` method. At this time the recording panel
-   * is not doing much state management, so this avoid the overhead of redux.
-   */
-  settingsComponentCreated(settings) {
-    this.settings = settings;
-  }
-
-  getRecordingStateForTesting() {
-    return this.state.recordingState;
-  }
-
   handleProfilerStarting() {
-    switch (this.state.recordingState) {
+    const { changeRecordingState, recordingState } = this.props;
+    switch (recordingState) {
       case NOT_YET_KNOWN:
         // We couldn't have started it yet, so it must have been someone
         // else. (fallthrough)
       case AVAILABLE_TO_RECORD:
         // We aren't recording, someone else started it up. (fallthrough)
       case REQUEST_TO_STOP_PROFILER:
         // We requested to stop the profiler, but someone else already started
         // it up. (fallthrough)
       case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
         // Someone re-started the profiler while we were asking for the completed
         // profile.
 
-        this.setState({
-          recordingState: OTHER_IS_RECORDING,
-          recordingUnexpectedlyStopped: false
-        });
+        changeRecordingState(OTHER_IS_RECORDING);
         break;
 
       case REQUEST_TO_START_RECORDING:
         // Wait for the profiler to tell us that it has started.
-        this.setState({
-          recordingState: RECORDING,
-          recordingUnexpectedlyStopped: false
-        });
+        changeRecordingState(RECORDING);
         break;
 
       case LOCKED_BY_PRIVATE_BROWSING:
       case OTHER_IS_RECORDING:
       case RECORDING:
         // These state cases don't make sense to happen, and means we have a logical
         // fallacy somewhere.
         throw new Error(
           "The profiler started recording, when it shouldn't have " +
-          `been able to. Current state: "${this.state.recordingState}"`);
+          `been able to. Current state: "${recordingState}"`);
       default:
         throw new Error("Unhandled recording state");
     }
   }
 
   handleProfilerStopping() {
-    switch (this.state.recordingState) {
+    const { changeRecordingState, recordingState } = this.props;
+    switch (recordingState) {
       case NOT_YET_KNOWN:
       case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
       case REQUEST_TO_STOP_PROFILER:
       case OTHER_IS_RECORDING:
-        this.setState({
-          recordingState: AVAILABLE_TO_RECORD,
-          recordingUnexpectedlyStopped: false
-        });
+        changeRecordingState(AVAILABLE_TO_RECORD);
         break;
 
       case REQUEST_TO_START_RECORDING:
         // Highly unlikely, but someone stopped the recorder, this is fine.
         // Do nothing (fallthrough).
       case LOCKED_BY_PRIVATE_BROWSING:
         // The profiler is already locked, so we know about this already.
         break;
 
       case RECORDING:
-        this.setState({
-          recordingState: AVAILABLE_TO_RECORD,
-          recordingUnexpectedlyStopped: true
-        });
+        changeRecordingState(
+          AVAILABLE_TO_RECORD,
+          { didRecordingUnexpectedlyStopped: true }
+        );
         break;
 
       case AVAILABLE_TO_RECORD:
         throw new Error(
           "The profiler stopped recording, when it shouldn't have been able to.");
       default:
         throw new Error("Unhandled recording state");
     }
   }
 
   handlePrivateBrowsingStarting() {
-    switch (this.state.recordingState) {
+    const { recordingState, changeRecordingState } = this.props;
+
+    switch (recordingState) {
       case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
         // This one is a tricky case. Go ahead and act like nothing went wrong, maybe
         // it will resolve correctly? (fallthrough)
       case REQUEST_TO_STOP_PROFILER:
       case AVAILABLE_TO_RECORD:
       case OTHER_IS_RECORDING:
       case NOT_YET_KNOWN:
-        this.setState({
-          recordingState: LOCKED_BY_PRIVATE_BROWSING,
-          recordingUnexpectedlyStopped: false
-        });
+        changeRecordingState(LOCKED_BY_PRIVATE_BROWSING);
         break;
 
       case REQUEST_TO_START_RECORDING:
       case RECORDING:
-        this.setState({
-          recordingState: LOCKED_BY_PRIVATE_BROWSING,
-          recordingUnexpectedlyStopped: true
-        });
+        changeRecordingState(
+          LOCKED_BY_PRIVATE_BROWSING,
+          { didRecordingUnexpectedlyStopped: false }
+        );
         break;
 
       case LOCKED_BY_PRIVATE_BROWSING:
         // Do nothing
         break;
 
       default:
         throw new Error("Unhandled recording state");
     }
   }
 
   handlePrivateBrowsingEnding() {
     // No matter the state, go ahead and set this as ready to record. This should
     // be the only logical state to go into.
-    this.setState({
-      recordingState: AVAILABLE_TO_RECORD,
-      recordingUnexpectedlyStopped: false
-    });
-  }
-
-  startRecording() {
-    const settings = this.settings;
-    if (!settings) {
-      console.error("Expected the PerfSettings panel to be rendered and available.");
-      return;
-    }
-    this.setState({
-      recordingState: REQUEST_TO_START_RECORDING,
-      // Reset this error state since it's no longer valid.
-      recordingUnexpectedlyStopped: false,
-    });
-    this.props.perfFront.startProfiler(
-      // Pull out the recording settings from the child component. This approach avoids
-      // using Redux as a state manager.
-      settings.getRecordingSettings()
-    );
-  }
-
-  async getProfileAndStopProfiler() {
-    this.setState({ recordingState: REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER });
-    const profile = await this.props.perfFront.getProfileAndStopProfiler();
-    this.setState({ recordingState: AVAILABLE_TO_RECORD });
-    console.log("getProfileAndStopProfiler");
-    this.props.receiveProfile(profile);
-  }
-
-  stopProfilerAndDiscardProfile() {
-    this.setState({ recordingState: REQUEST_TO_STOP_PROFILER });
-    this.props.perfFront.stopProfilerAndDiscardProfile();
-  }
-
-  renderButton() {
-    const { recordingState, isSupportedPlatform } = this.state;
-
-    if (!isSupportedPlatform) {
-      return renderButton({
-        label: "Start recording",
-        disabled: true,
-        additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
-                           "supports Tier-1 platforms."
-      });
-    }
-
-    // TODO - L10N all of the messages. Bug 1418056
-    switch (recordingState) {
-      case NOT_YET_KNOWN:
-        return null;
-
-      case AVAILABLE_TO_RECORD:
-        return renderButton({
-          onClick: this.startRecording,
-          label: span(
-            null,
-            img({
-              className: "perf-button-image",
-              src: "chrome://devtools/skin/images/tool-profiler.svg"
-            }),
-            "Start recording",
-          ),
-          additionalMessage: this.state.recordingUnexpectedlyStopped
-            ? div(null, "The recording was stopped by another tool.")
-            : null
-        });
-
-      case REQUEST_TO_STOP_PROFILER:
-        return renderButton({
-          label: "Stopping the recording",
-          disabled: true
-        });
-
-      case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
-        return renderButton({
-          label: "Stopping the recording, and capturing the profile",
-          disabled: true
-        });
-
-      case REQUEST_TO_START_RECORDING:
-      case RECORDING:
-        return renderButton({
-          label: "Stop and grab the recording",
-          onClick: this.getProfileAndStopProfiler,
-          disabled: this.state.recordingState === REQUEST_TO_START_RECORDING
-        });
-
-      case OTHER_IS_RECORDING:
-        return renderButton({
-          label: "Stop and discard the other recording",
-          onClick: this.stopProfilerAndDiscardProfile,
-          additionalMessage: "Another tool is currently recording."
-        });
-
-      case LOCKED_BY_PRIVATE_BROWSING:
-        return renderButton({
-          label: "Start recording",
-          disabled: true,
-          additionalMessage: `The profiler is disabled when Private Browsing is enabled.
-                              Close all Private Windows to re-enable the profiler`
-        });
-
-      default:
-        throw new Error("Unhandled recording state");
-    }
-  }
-
-  handleLinkClick(event) {
-    openLink(event.target.value, this.props.toolbox);
+    this.props.changeRecordingState(AVAILABLE_TO_RECORD);
   }
 
   render() {
-    const { isSupportedPlatform } = this.state;
+    const { isSupportedPlatform } = this.props;
 
     if (isSupportedPlatform === null) {
       // We don't know yet if this is a supported platform, wait for a response.
       return null;
     }
 
     return div(
       { className: "perf" },
-      this.renderButton(),
-      PerfSettings({ ref: this.settingsComponentCreated }),
-      div(
-        { className: "perf-description" },
-        p(null,
-          "This new recording panel is a bit different from the existing " +
-            "performance panel. It records the entire browser, and then opens up " +
-            "and shares the profile with ",
-          button(
-            // Implement links as buttons to avoid any risk of loading the link in the
-            // the panel.
-            {
-              className: "perf-external-link",
-              value: "https://perf-html.io",
-              onClick: this.handleLinkClick
-            },
-            "perf-html.io"
-          ),
-          ", a Mozilla performance analysis tool."
-        ),
-        p(null,
-          "This is still a prototype. Join along or file bugs at: ",
-          button(
-            // Implement links as buttons to avoid any risk of loading the link in the
-            // the panel.
-            {
-              className: "perf-external-link",
-              value: "https://github.com/devtools-html/perf.html",
-              onClick: this.handleLinkClick
-            },
-            "github.com/devtools-html/perf.html"
-          ),
-          "."
-        )
-      ),
+      RecordingButton(),
+      Settings(),
+      Description(),
     );
   }
 }
 
-module.exports = Perf;
-
-function renderButton(props) {
-  const { disabled, label, onClick, additionalMessage } = props;
-  const nbsp = "\u00A0";
+function mapStateToProps(state) {
+  return {
+    perfFront: selectors.getPerfFront(state),
+    recordingState: selectors.getRecordingState(state),
+    isSupportedPlatform: selectors.getIsSupportedPlatform(state),
+  };
+}
 
-  return div(
-    { className: "perf-button-container" },
-    div({ className: "perf-additional-message" }, additionalMessage || nbsp),
-    div(
-      null,
-      button(
-        {
-          className: "devtools-button perf-button",
-          "data-standalone": true,
-          disabled,
-          onClick
-        },
-        label
-      )
-    )
-  );
-}
+const mapDispatchToProps = {
+  changeRecordingState: actions.changeRecordingState,
+  reportProfilerReady: actions.reportProfilerReady,
+};
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(Perf);
--- a/devtools/client/performance-new/components/Range.js
+++ b/devtools/client/performance-new/components/Range.js
@@ -1,16 +1,19 @@
 /* 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";
 const { PureComponent } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { div, input, label } = require("devtools/client/shared/vendor/react-dom-factories");
 
+/**
+ * Provide a numeric range slider UI that works off of custom numeric scales.
+ */
 class Range extends PureComponent {
   static get propTypes() {
     return {
       value: PropTypes.number.isRequired,
       label: PropTypes.string.isRequired,
       id: PropTypes.string.isRequired,
       scale: PropTypes.object.isRequired,
       onChange: PropTypes.func.isRequired,
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/RecordingButton.js
@@ -0,0 +1,167 @@
+/* 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";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const { div, button, span, img } = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const actions = require("devtools/client/performance-new/store/actions");
+const { recordingState: {
+  NOT_YET_KNOWN,
+  AVAILABLE_TO_RECORD,
+  REQUEST_TO_START_RECORDING,
+  REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER,
+  REQUEST_TO_STOP_PROFILER,
+  RECORDING,
+  OTHER_IS_RECORDING,
+  LOCKED_BY_PRIVATE_BROWSING,
+}} = require("devtools/client/performance-new/utils");
+const selectors = require("devtools/client/performance-new/store/selectors");
+
+/**
+ * This component is not responsible for the full life cycle of recording a profile. It
+ * is only responsible for the actual act of stopping and starting recordings. It
+ * also reacts to the changes of the recording state from external changes.
+ */
+class RecordingButton extends PureComponent {
+  static get propTypes() {
+    return {
+      // StateProps
+      recordingState: PropTypes.string.isRequired,
+      isSupportedPlatform: PropTypes.bool,
+      recordingUnexpectedlyStopped: PropTypes.bool.isRequired,
+
+      // DispatchProps
+      startRecording: PropTypes.func.isRequired,
+      getProfileAndStopProfiler: PropTypes.func.isRequired,
+      stopProfilerAndDiscardProfile: PropTypes.func.isRequired,
+    };
+  }
+
+  renderButton(buttonSettings) {
+    const { disabled, label, onClick, additionalMessage } = buttonSettings;
+    const nbsp = "\u00A0";
+
+    return div(
+      { className: "perf-button-container" },
+      div({ className: "perf-additional-message" }, additionalMessage || nbsp),
+      div(
+        null,
+        button(
+          {
+            className: "devtools-button perf-button",
+            "data-standalone": true,
+            disabled,
+            onClick
+          },
+          label
+        )
+      )
+    );
+  }
+
+  render() {
+    const {
+      startRecording,
+      getProfileAndStopProfiler,
+      stopProfilerAndDiscardProfile,
+      recordingState,
+      isSupportedPlatform,
+      recordingUnexpectedlyStopped
+    } = this.props;
+
+    if (!isSupportedPlatform) {
+      return this.renderButton({
+        label: "Start recording",
+        disabled: true,
+        additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
+                           "supports Tier-1 platforms."
+      });
+    }
+
+    // TODO - L10N all of the messages. Bug 1418056
+    switch (recordingState) {
+      case NOT_YET_KNOWN:
+        return null;
+
+      case AVAILABLE_TO_RECORD:
+        return this.renderButton({
+          onClick: startRecording,
+          label: span(
+            null,
+            img({
+              className: "perf-button-image",
+              src: "chrome://devtools/skin/images/tool-profiler.svg"
+            }),
+            "Start recording",
+          ),
+          additionalMessage: recordingUnexpectedlyStopped
+            ? div(null, "The recording was stopped by another tool.")
+            : null
+        });
+
+      case REQUEST_TO_STOP_PROFILER:
+        return this.renderButton({
+          label: "Stopping the recording",
+          disabled: true
+        });
+
+      case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
+        return this.renderButton({
+          label: "Stopping the recording, and capturing the profile",
+          disabled: true
+        });
+
+      case REQUEST_TO_START_RECORDING:
+      case RECORDING:
+        return this.renderButton({
+          label: "Stop and grab the recording",
+          onClick: getProfileAndStopProfiler,
+          disabled: recordingState === REQUEST_TO_START_RECORDING
+        });
+
+      case OTHER_IS_RECORDING:
+        return this.renderButton({
+          label: "Stop and discard the other recording",
+          onClick: stopProfilerAndDiscardProfile,
+          additionalMessage: "Another tool is currently recording."
+        });
+
+      case LOCKED_BY_PRIVATE_BROWSING:
+        return this.renderButton({
+          label: span(
+            null,
+            img({
+              className: "perf-button-image",
+              src: "chrome://devtools/skin/images/tool-profiler.svg"
+            }),
+            "Start recording",
+          ),
+          disabled: true,
+          additionalMessage: `The profiler is disabled when Private Browsing is enabled.
+                              Close all Private Windows to re-enable the profiler`
+        });
+
+      default:
+        throw new Error("Unhandled recording state");
+    }
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    recordingState: selectors.getRecordingState(state),
+    isSupportedPlatform: selectors.getIsSupportedPlatform(state),
+    recordingUnexpectedlyStopped: selectors.getRecordingUnexpectedlyStopped(state),
+  };
+}
+
+const mapDispatchToProps = {
+  startRecording: actions.startRecording,
+  stopProfilerAndDiscardProfile: actions.stopProfilerAndDiscardProfile,
+  getProfileAndStopProfiler: actions.getProfileAndStopProfiler,
+};
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(RecordingButton);
rename from devtools/client/performance-new/components/PerfSettings.js
rename to devtools/client/performance-new/components/Settings.js
--- a/devtools/client/performance-new/components/PerfSettings.js
+++ b/devtools/client/performance-new/components/Settings.js
@@ -1,16 +1,20 @@
 /* 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";
 const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
 const { div, details, summary, label, input, span, h2, section } = require("devtools/client/shared/vendor/react-dom-factories");
 const Range = createFactory(require("devtools/client/performance-new/components/Range"));
 const { makeExponentialScale, formatFileSize, calculateOverhead } = require("devtools/client/performance-new/utils");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const actions = require("devtools/client/performance-new/store/actions");
+const selectors = require("devtools/client/performance-new/store/selectors");
 
 // sizeof(double) + sizeof(char)
 // http://searchfox.org/mozilla-central/rev/e8835f52eff29772a57dca7bcc86a9a312a23729/tools/profiler/core/ProfileEntry.h#73
 const PROFILE_ENTRY_SIZE = 9;
 
 const NOTCHES = Array(22).fill("discrete-level-notch");
 
 const threadColumns = [
@@ -115,70 +119,55 @@ const featureCheckboxes = [
   {
     name: "TaskTracer",
     value: "tasktracer",
     title: "Enable TaskTracer (Experimental, requires custom build.)"
   },
 ];
 
 /**
- * This component manages the settings for recording a performance profile. In addition
- * to rendering the UI, it also manages the state of the settings. In order to not
- * introduce the complexity of adding Redux to a relatively simple UI, this
- * component expects to be accessed via the `ref`, and then calling
- * `settings.getRecordingSettings()` to get out the settings. If the recording panel
- * takes on new responsibilities, then this decision should be revisited.
+ * This component manages the settings for recording a performance profile.
  */
-class PerfSettings extends PureComponent {
+class Settings extends PureComponent {
   static get propTypes() {
-    return {};
+    return {
+      // StateProps
+      interval: PropTypes.number.isRequired,
+      entries: PropTypes.number.isRequired,
+      features: PropTypes.array.isRequired,
+      threads: PropTypes.array.isRequired,
+      threadsString: PropTypes.string.isRequired,
+
+      // DispatchProps
+      changeInterval: PropTypes.func.isRequired,
+      changeEntries: PropTypes.func.isRequired,
+      changeFeatures: PropTypes.func.isRequired,
+      changeThreads: PropTypes.func.isRequired,
+    };
   }
 
   constructor(props) {
     super(props);
-    // Right now the defaults are reset every time the panel is opened. These should
-    // be persisted between sessions. See Bug 1453014.
     this.state = {
-      interval: 1,
-      entries: 10000000, // 90MB
-      features: {
-        js: true,
-        stackwalk: true,
-      },
-      threads: "GeckoMain,Compositor",
-      threadListFocused: false,
+      // Allow the textbox to have a temporary tracked value.
+      temporaryThreadText: null
     };
+
     this._handleThreadCheckboxChange = this._handleThreadCheckboxChange.bind(this);
     this._handleFeaturesCheckboxChange = this._handleFeaturesCheckboxChange.bind(this);
-    this._handleThreadTextChange = this._handleThreadTextChange.bind(this);
+    this._setThreadTextFromInput = this._setThreadTextFromInput.bind(this);
     this._handleThreadTextCleanup = this._handleThreadTextCleanup.bind(this);
     this._renderThreadsColumns = this._renderThreadsColumns.bind(this);
-    this._onChangeInterval = this._onChangeInterval.bind(this);
-    this._onChangeEntries = this._onChangeEntries.bind(this);
+
     this._intervalExponentialScale = makeExponentialScale(0.01, 100);
     this._entriesExponentialScale = makeExponentialScale(100000, 100000000);
   }
 
-  getRecordingSettings() {
-    const features = [];
-    for (const [name, isSet] of Object.entries(this.state.features)) {
-      if (isSet) {
-        features.push(name);
-      }
-    }
-    return {
-      entries: this.state.entries,
-      interval: this.state.interval,
-      features,
-      threads: _threadStringToList(this.state.threads)
-    };
-  }
-
   _renderNotches() {
-    const { interval, entries, features } = this.state;
+    const { interval, entries, features } = this.props;
     const overhead = calculateOverhead(interval, entries, features);
     const notchCount = 22;
     const notches = [];
     for (let i = 0; i < notchCount; i++) {
       const active = i <= Math.round(overhead * (NOTCHES.length - 1))
         ? "active" : "inactive";
 
       let level = "normal";
@@ -195,64 +184,65 @@ class PerfSettings extends PureComponent
             `perf-settings-notch-${active}`
         })
       );
     }
     return notches;
   }
 
   _handleThreadCheckboxChange(event) {
+    const { threads, changeThreads } = this.props;
     const { checked, value }  = event.target;
 
-    this.setState(state => {
-      let threadsList = _threadStringToList(state.threads);
-      if (checked) {
-        if (!threadsList.includes(value)) {
-          threadsList.push(value);
-        }
-      } else {
-        threadsList = threadsList.filter(thread => thread !== value);
+    if (checked) {
+      if (!threads.includes(value)) {
+        changeThreads([...threads, value]);
       }
-      return { threads: threadsList.join(",") };
-    });
+    } else {
+      changeThreads(threads.filter(thread => thread !== value));
+    }
   }
 
   _handleFeaturesCheckboxChange(event) {
+    const { features, changeFeatures } = this.props;
     const { checked, value }  = event.target;
 
-    this.setState(state => ({
-      features: {...state.features, [value]: checked}
-    }));
-  }
-
-  _handleThreadTextChange(event) {
-    this.setState({ threads: event.target.value });
+    if (checked) {
+      if (!features.includes(value)) {
+        changeFeatures([value, ...features]);
+      }
+    } else {
+      changeFeatures(features.filter(feature => feature !== value));
+    }
   }
 
-  _handleThreadTextCleanup() {
-    this.setState(state => {
-      const threadsList = _threadStringToList(state.threads);
-      return { threads: threadsList.join(",") };
-    });
+  _setThreadTextFromInput(event) {
+    this.setState({ temporaryThreadText: event.target.value });
   }
 
-  _renderThreadsColumns(threads, index) {
+  _handleThreadTextCleanup(event) {
+    this.setState({ temporaryThreadText: null });
+    this.props.changeThreads(_threadTextToList(event.target.value));
+  }
+
+  _renderThreadsColumns(threadDisplay, index) {
+    const { threads } = this.props;
     return div(
       { className: "perf-settings-thread-column", key: index },
-      threads.map(({name, title}) => label(
+      threadDisplay.map(({name, title}) => label(
         {
           className: "perf-settings-checkbox-label",
           key: name,
           title
         },
         input({
           className: "perf-settings-checkbox",
           type: "checkbox",
           value: name,
-          checked: this.state.threads.includes(name),
+          checked: threads.includes(name),
           onChange: this._handleThreadCheckboxChange
         }),
         name
       ))
     );
   }
 
   _renderThreads() {
@@ -278,19 +268,22 @@ class PerfSettings extends PureComponent
                   "enable profiling of the threads in the profiler. The name needs to " +
                   "be only a partial match of the thread name to be included. It " +
                   "is whitespace sensitive."
               },
               div({}, "Add custom threads by name:"),
               input({
                 className: "perf-settings-text-input",
                 type: "text",
-                value: this.state.threads,
-                onChange: this._handleThreadTextChange,
+                value: this.state.temporaryThreadText === null
+                  ? this.props.threads
+                  : this.state.temporaryThreadText,
                 onBlur: this._handleThreadTextCleanup,
+                onFocus: this._setThreadTextFromInput,
+                onChange: this._setThreadTextFromInput,
               })
             )
           )
         )
       )
     );
   }
 
@@ -306,17 +299,17 @@ class PerfSettings extends PureComponent
             {
               className: "perf-settings-checkbox-label perf-settings-feature-label",
               key: value,
             },
             input({
               className: "perf-settings-checkbox",
               type: "checkbox",
               value,
-              checked: this.state.features[value],
+              checked: this.props.features.includes(value),
               onChange: this._handleFeaturesCheckboxChange
             }),
             div({ className: "perf-settings-feature-name" }, name),
             div(
               { className: "perf-settings-feature-title" },
               title,
               recommended
                 ? span(
@@ -326,64 +319,56 @@ class PerfSettings extends PureComponent
                 : null
             )
           ))
         )
       )
     );
   }
 
-  _onChangeInterval(interval) {
-    this.setState({ interval });
-  }
-
-  _onChangeEntries(entries) {
-    this.setState({ entries });
-  }
-
   render() {
     return section(
       { className: "perf-settings" },
       h2({ className: "perf-settings-title" }, "Recording Settings"),
       div(
         { className: "perf-settings-row" },
         label({ className: "perf-settings-label" }, "Overhead:"),
         div(
           { className: "perf-settings-value perf-settings-notches" },
           this._renderNotches()
         )
       ),
       Range({
         label: "Sampling interval:",
-        value: this.state.interval,
+        value: this.props.interval,
         id: "perf-range-interval",
         scale: this._intervalExponentialScale,
         display: _intervalTextDisplay,
-        onChange: this._onChangeInterval
+        onChange: this.props.changeInterval
       }),
       Range({
         label: "Buffer size:",
-        value: this.state.entries,
+        value: this.props.entries,
         id: "perf-range-entries",
         scale: this._entriesExponentialScale,
         display: _entriesTextDisplay,
-        onChange: this._onChangeEntries
+        onChange: this.props.changeEntries,
       }),
       this._renderThreads(),
       this._renderFeatures()
     );
   }
 }
 
 /**
  * Clean up the thread list string into a list of values.
  * @param string threads, comma separated values.
  * @return Array list of thread names
  */
-function _threadStringToList(threads) {
+function _threadTextToList(threads) {
   return threads
     // Split on commas
     .split(",")
     // Clean up any extraneous whitespace
     .map(string => string.trim())
     // Filter out any blank strings
     .filter(string => string);
 }
@@ -401,9 +386,26 @@ function _intervalTextDisplay(value) {
  * Format the entries number for display.
  * @param {number} value
  * @return {string}
  */
 function _entriesTextDisplay(value) {
   return formatFileSize(value * PROFILE_ENTRY_SIZE);
 }
 
-module.exports = PerfSettings;
+function mapStateToProps(state) {
+  return {
+    interval: selectors.getInterval(state),
+    entries: selectors.getEntries(state),
+    features: selectors.getFeatures(state),
+    threads: selectors.getThreads(state),
+    threadsString: selectors.getThreadsString(state),
+  };
+}
+
+const mapDispatchToProps = {
+  changeInterval: actions.changeInterval,
+  changeEntries: actions.changeEntries,
+  changeFeatures: actions.changeFeatures,
+  changeThreads: actions.changeThreads,
+};
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings);
--- a/devtools/client/performance-new/components/moz.build
+++ b/devtools/client/performance-new/components/moz.build
@@ -1,10 +1,12 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'Description.js',
     'Perf.js',
-    'PerfSettings.js',
     'Range.js',
+    'RecordingButton.js',
+    'Settings.js',
 )
--- a/devtools/client/performance-new/initializer.js
+++ b/devtools/client/performance-new/initializer.js
@@ -8,29 +8,41 @@
 const BrowserLoaderModule = {};
 ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
 const { require } = BrowserLoaderModule.BrowserLoader({
   baseURI: "resource://devtools/client/memory/",
   window
 });
 const Perf = require("devtools/client/performance-new/components/Perf");
 const Services = require("Services");
-const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
-const { createElement } = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const React = require("devtools/client/shared/vendor/react");
+const createStore = require("devtools/client/shared/redux/create-store")();
+const reducers = require("devtools/client/performance-new/store/reducers");
+const actions = require("devtools/client/performance-new/store/actions");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 /**
- * Perform a simple initialization on the panel. Hook up event listeners.
+ * Initialize the panel by creating a redux store, and render the root component.
  *
  * @param toolbox - The toolbox
  * @param perfFront - The Perf actor's front. Used to start and stop recordings.
  */
 function gInit(toolbox, perfFront) {
-  const props = {
+  const store = createStore(reducers);
+  store.dispatch(actions.initializeStore({
     toolbox,
     perfFront,
+    /**
+     * This function uses privileged APIs in order to take the profile, open up a new
+     * tab, and then inject it into perf.html. In order to provide a clear separation
+     * in the codebase between privileged and non-privileged code, this function is
+     * defined in initializer.js, and injected into the the normal component. All of
+     * the React components and Redux store behave as normal unprivileged web components.
+     */
     receiveProfile: profile => {
       // Open up a new tab and send a message with the profile.
       let browser = top.gBrowser;
       if (!browser) {
         // Current isn't browser window. Looking for the recent browser.
         const win = Services.wm.getMostRecentWindow("navigator:browser");
         if (!win) {
           throw new Error("No browser window");
@@ -42,15 +54,23 @@ function gInit(toolbox, perfFront) {
       browser.selectedTab = tab;
       const mm = tab.linkedBrowser.messageManager;
       mm.loadFrameScript(
         "chrome://devtools/content/performance-new/frame-script.js",
         false
       );
       mm.sendAsyncMessage("devtools:perf-html-transfer-profile", profile);
     }
-  };
-  render(createElement(Perf, props), document.querySelector("#root"));
+  }));
+
+  ReactDOM.render(
+    React.createElement(
+      Provider,
+      { store },
+      React.createElement(Perf)
+    ),
+    document.querySelector("#root")
+  );
 }
 
 function gDestroy() {
-  unmountComponentAtNode(document.querySelector("#root"));
+  ReactDOM.unmountComponentAtNode(document.querySelector("#root"));
 }
--- a/devtools/client/performance-new/moz.build
+++ b/devtools/client/performance-new/moz.build
@@ -1,15 +1,16 @@
 # vim: set filetype=python:
 # 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/.
 
 DIRS += [
     'components',
+    'store',
 ]
 
 DevToolsModules(
     'panel.js',
     'utils.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/store/actions.js
@@ -0,0 +1,118 @@
+/* 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";
+
+const selectors = require("devtools/client/performance-new/store/selectors");
+const { recordingState: {
+  AVAILABLE_TO_RECORD,
+  REQUEST_TO_START_RECORDING,
+  REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER,
+  REQUEST_TO_STOP_PROFILER,
+}} = require("devtools/client/performance-new/utils");
+
+/**
+ * The recording state manages the current state of the recording panel.
+ * @param {string} state - A valid state in `recordingState`.
+ * @param {object} options
+ */
+const changeRecordingState = exports.changeRecordingState =
+  (state, options = { didRecordingUnexpectedlyStopped: false }) => ({
+    type: "CHANGE_RECORDING_STATE",
+    state,
+    didRecordingUnexpectedlyStopped: options.didRecordingUnexpectedlyStopped
+  });
+
+/**
+ * This is the result of the initial questions about the state of the profiler.
+ *
+ * @param {boolean} isSupportedPlatform - This is a supported platform.
+ * @param {string} recordingState - A valid state in `recordingState`.
+ */
+exports.reportProfilerReady = (isSupportedPlatform, recordingState) => ({
+  type: "REPORT_PROFILER_READY",
+  isSupportedPlatform,
+  recordingState,
+});
+
+/**
+ * Updates the recording settings for the interval.
+ * @param {number} interval
+ */
+exports.changeInterval = interval => ({
+  type: "CHANGE_INTERVAL",
+  interval
+});
+
+/**
+ * Updates the recording settings for the entries.
+ * @param {number} entries
+ */
+exports.changeEntries = entries => ({
+  type: "CHANGE_ENTRIES",
+  entries
+});
+
+/**
+ * Updates the recording settings for the features.
+ * @param {object} features
+ */
+exports.changeFeatures = features => ({
+  type: "CHANGE_FEATURES",
+  features
+});
+
+/**
+ * Updates the recording settings for the threads.
+ * @param {array} threads
+ */
+exports.changeThreads = threads => ({
+  type: "CHANGE_THREADS",
+  threads
+});
+
+/**
+ * Receive the values to intialize the store. See the reducer for what values
+ * are expected.
+ * @param {object} threads
+ */
+exports.initializeStore = values => ({
+  type: "INITIALIZE_STORE",
+  values
+});
+
+/**
+ * Start a new recording with the perfFront and update the internal recording state.
+ */
+exports.startRecording = () => {
+  return (dispatch, getState) => {
+    const recordingSettings = selectors.getRecordingSettings(getState());
+    const perfFront = selectors.getPerfFront(getState());
+    perfFront.startProfiler(recordingSettings);
+    dispatch(changeRecordingState(REQUEST_TO_START_RECORDING));
+  };
+};
+
+/**
+ * Stops the profiler, and opens the profile in a new window.
+ */
+exports.getProfileAndStopProfiler = () => {
+  return async (dispatch, getState) => {
+    const perfFront = selectors.getPerfFront(getState());
+    dispatch(changeRecordingState(REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER));
+    const profile = await perfFront.getProfileAndStopProfiler();
+    selectors.getReceiveProfileFn(getState())(profile);
+    dispatch(changeRecordingState(AVAILABLE_TO_RECORD));
+  };
+};
+
+/**
+ * Stops the profiler, but does not try to retrieve the profile.
+ */
+exports.stopProfilerAndDiscardProfile = () => {
+  return async (dispatch, getState) => {
+    const perfFront = selectors.getPerfFront(getState());
+    dispatch(changeRecordingState(REQUEST_TO_STOP_PROFILER));
+    perfFront.stopProfilerAndDiscardProfile();
+  };
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/store/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+    'actions.js',
+    'reducers.js',
+    'selectors.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/store/reducers.js
@@ -0,0 +1,137 @@
+/* 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";
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+
+const { recordingState: {
+  NOT_YET_KNOWN,
+}} = require("devtools/client/performance-new/utils");
+
+/**
+ * The current state of the recording.
+ * @param state - A recordingState key.
+ */
+function recordingState(state = NOT_YET_KNOWN, action) {
+  switch (action.type) {
+    case "CHANGE_RECORDING_STATE":
+      return action.state;
+    case "REPORT_PROFILER_READY":
+      return action.recordingState;
+    default:
+      return state;
+  }
+}
+
+/**
+ * Whether or not the recording state unexpectedly stopped. This allows
+ * the UI to display a helpful message.
+ * @param {boolean} state
+ */
+function recordingUnexpectedlyStopped(state = false, action) {
+  switch (action.type) {
+    case "CHANGE_RECORDING_STATE":
+      return action.didRecordingUnexpectedlyStopped;
+    default:
+      return state;
+  }
+}
+
+/**
+ * The profiler needs to be queried asynchronously on whether or not
+ * it supports the user's platform.
+ * @param {boolean | null} state
+ */
+function isSupportedPlatform(state = null, action) {
+  switch (action.type) {
+    case "REPORT_PROFILER_READY":
+      return action.isSupportedPlatform;
+    default:
+      return state;
+  }
+}
+
+// Right now all recording setting the defaults are reset every time the panel
+// is opened. These should be persisted between sessions. See Bug 1453014.
+
+/**
+ * The setting for the recording interval.
+ * @param {number} state
+ */
+function interval(state = 1, action) {
+  switch (action.type) {
+    case "CHANGE_INTERVAL":
+      return action.interval;
+    default:
+      return state;
+  }
+}
+
+/**
+ * The number of entries in the profiler's circular buffer. Defaults to 90mb.
+ * @param {number} state
+ */
+function entries(state = 10000000, action) {
+  switch (action.type) {
+    case "CHANGE_ENTRIES":
+      return action.entries;
+    default:
+      return state;
+  }
+}
+
+/**
+ * The features that are enabled for the profiler.
+ * @param {array} state
+ */
+function features(state = ["js", "stackwalk"], action) {
+  switch (action.type) {
+    case "CHANGE_FEATURES":
+      return action.features;
+    default:
+      return state;
+  }
+}
+
+/**
+ * The current threads list.
+ * @param {array of strings} state
+ */
+function threads(state = ["GeckoMain", "Compositor"], action) {
+  switch (action.type) {
+    case "CHANGE_THREADS":
+      return action.threads;
+    default:
+      return state;
+  }
+}
+
+/**
+ * These are all the values used to initialize the profiler. They should never change
+ * once added to the store.
+ *
+ * state = {
+ *   toolbox - The current toolbox.
+ *   perfFront - The current Front to the Perf actor.
+ *   receiveProfile - A function to receive the profile and open it into a new window.
+ * }
+ */
+function initializedValues(state = null, action) {
+  switch (action.type) {
+    case "INITIALIZE_STORE":
+      return action.values;
+    default:
+      return state;
+  }
+}
+
+module.exports = combineReducers({
+  recordingState,
+  recordingUnexpectedlyStopped,
+  isSupportedPlatform,
+  interval,
+  entries,
+  features,
+  threads,
+  initializedValues,
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/store/selectors.js
@@ -0,0 +1,50 @@
+/* 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";
+
+const getRecordingState = state => state.recordingState;
+const getRecordingUnexpectedlyStopped = state => state.recordingUnexpectedlyStopped;
+const getIsSupportedPlatform = state => state.isSupportedPlatform;
+const getInterval = state => state.interval;
+const getEntries = state => state.entries;
+const getFeatures = state => state.features;
+const getThreads = state => state.threads;
+const getThreadsString = state => getThreads(state).join(",");
+
+const getRecordingSettings = state => {
+  return {
+    entries: getEntries(state),
+    interval: getInterval(state),
+    features: getFeatures(state),
+    threads: getThreads(state),
+  };
+};
+
+const getInitializedValues = state => {
+  const values = state.initializedValues;
+  if (!values) {
+    throw new Error("The store must be initialized before it can be used.");
+  }
+  return values;
+};
+
+const getPerfFront = state => getInitializedValues(state).perfFront;
+const getToolbox = state => getInitializedValues(state).toolbox;
+const getReceiveProfileFn = state => getInitializedValues(state).receiveProfile;
+
+module.exports = {
+  getRecordingState,
+  getRecordingUnexpectedlyStopped,
+  getIsSupportedPlatform,
+  getInterval,
+  getEntries,
+  getFeatures,
+  getThreads,
+  getThreadsString,
+  getRecordingSettings,
+  getInitializedValues,
+  getPerfFront,
+  getToolbox,
+  getReceiveProfileFn,
+};
--- a/devtools/client/performance-new/test/chrome/head.js
+++ b/devtools/client/performance-new/test/chrome/head.js
@@ -124,8 +124,64 @@ class MockPerfFront extends EventEmitter
 // Do a quick validation to make sure that our Mock has the same methods as a spec.
 const mockKeys = Object.getOwnPropertyNames(MockPerfFront.prototype);
 Object.getOwnPropertyNames(perfDescription.methods).forEach(methodName => {
   if (!mockKeys.includes(methodName)) {
     throw new Error(`The MockPerfFront is missing the method "${methodName}" from the ` +
                     "actor's spec. It should be added to the mock.");
   }
 });
+
+/**
+ * This is a helper function to correctly mount the Perf component, and provide
+ * mocks where needed.
+ */
+function createPerfComponent() {
+  const Perf = require("devtools/client/performance-new/components/Perf");
+  const React = require("devtools/client/shared/vendor/react");
+  const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+  const ReactRedux = require("devtools/client/shared/vendor/react-redux");
+  const createStore = require("devtools/client/shared/redux/create-store")();
+  const reducers = require("devtools/client/performance-new/store/reducers");
+  const actions = require("devtools/client/performance-new/store/actions");
+  const selectors = require("devtools/client/performance-new/store/selectors");
+
+  const perfFront = new MockPerfFront();
+  const toolboxMock = {};
+  const store = createStore(reducers);
+  const container = document.querySelector("#container");
+  const receiveProfileCalls = [];
+
+  function receiveProfileMock(profile) {
+    receiveProfileCalls.push(profile);
+  }
+
+  const mountComponent = () => {
+    store.dispatch(actions.initializeStore({
+      toolbox: toolboxMock,
+      perfFront,
+      receiveProfile: receiveProfileMock,
+    }));
+
+    return ReactDOM.render(
+      React.createElement(
+        ReactRedux.Provider,
+        { store },
+        React.createElement(Perf)
+      ),
+      container
+    );
+  };
+
+  // Provide a list of common values that may be needed during testing.
+  return {
+    receiveProfileCalls,
+    perfFront,
+    mountComponent,
+    selectors,
+    store,
+    container,
+    getState: store.getState,
+    dispatch: store.dispatch,
+    // Provide a common shortcut for this selector.
+    getRecordingState: () => selectors.getRecordingState(store.getState())
+  };
+}
--- a/devtools/client/performance-new/test/chrome/test_perf-state-01.html
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-01.html
@@ -17,54 +17,52 @@
     <script type="application/javascript">
       "use strict";
 
       /**
        * Test the normal workflow of starting and stopping the profiler through the
        * Perf component.
        */
       addPerfTest(async () => {
-        const Perf = require("devtools/client/performance-new/components/Perf");
-        const React = require("devtools/client/shared/vendor/react");
-        const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-        const perfFront = new MockPerfFront();
-        const container = document.querySelector("#container");
+        const {
+          perfFront,
+          getRecordingState,
+          receiveProfileCalls,
+          mountComponent,
+          container
+        } = createPerfComponent();
 
-        // Inject a function which will allow us to receive the profile.
-        let profile;
-        function receiveProfile(profileIn) {
-          profile = profileIn;
-        }
+        mountComponent();
 
-        const element = React.createElement(Perf, { perfFront, receiveProfile });
-        const perfComponent = ReactDOM.render(element, container);
-        is(perfComponent.state.recordingState, "not-yet-known",
+        is(getRecordingState(), "not-yet-known",
           "The component at first is in an unknown state.");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "available-to-record",
+        is(getRecordingState(), "available-to-record",
           "After talking to the actor, we're ready to record.");
 
         const button = container.querySelector("button");
         ok(button, "Selected the button to click.");
         button.click();
-        is(perfComponent.state.recordingState, "request-to-start-recording",
+        is(getRecordingState(), "request-to-start-recording",
           "Sent in a request to start recording.");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "recording",
+        is(getRecordingState(), "recording",
           "The actor has started its recording");
 
         button.click();
-        is(perfComponent.state.recordingState,
+        is(getRecordingState(),
           "request-to-get-profile-and-stop-profiler",
           "We have requested to stop the profiler.");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "available-to-record",
+        is(getRecordingState(), "available-to-record",
           "The profiler is available to record again.");
         await perfFront.flushAsyncQueue();
-        is(typeof profile, "object", "Got a profile");
+        is(receiveProfileCalls.length, 1,
+           "The receiveProfile function was called once");
+        is(typeof receiveProfileCalls[0], "object", "Got a profile");
       });
     </script>
   </pre>
 </body>
 </html>
--- a/devtools/client/performance-new/test/chrome/test_perf-state-02.html
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-02.html
@@ -16,44 +16,44 @@
     <script src="head.js" type="application/javascript"></script>
     <script type="application/javascript">
       "use strict";
 
       /**
        * Test the perf component when the profiler is already started.
        */
       addPerfTest(async () => {
-        const Perf = require("devtools/client/performance-new/components/Perf");
-        const React = require("devtools/client/shared/vendor/react");
-        const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-        const perfFront = new MockPerfFront();
-        const container = document.querySelector("#container");
+        const {
+          perfFront,
+          getRecordingState,
+          mountComponent,
+          container
+        } = createPerfComponent();
 
         ok(true, "Start the profiler before initiliazing the component, to simulate" +
                  "the profiler being controlled by another tool.");
 
         perfFront.startProfiler();
         await perfFront.flushAsyncQueue();
 
-        const receiveProfile = () => {};
-        const element = React.createElement(Perf, { perfFront, receiveProfile });
-        const perfComponent = ReactDOM.render(element, container);
-        is(perfComponent.state.recordingState, "not-yet-known",
+        mountComponent();
+
+        is(getRecordingState(), "not-yet-known",
           "The component at first is in an unknown state.");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "other-is-recording",
+        is(getRecordingState(), "other-is-recording",
           "The profiler is not available to record.");
 
         const button = container.querySelector("button");
         ok(button, "Selected a button on the component");
         button.click();
-        is(perfComponent.state.recordingState, "request-to-stop-profiler",
+        is(getRecordingState(), "request-to-stop-profiler",
           "We can request to stop the profiler.");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "available-to-record",
+        is(getRecordingState(), "available-to-record",
           "The profiler is now available to record.");
       });
     </script>
   </pre>
 </body>
 </html>
--- a/devtools/client/performance-new/test/chrome/test_perf-state-03.html
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-03.html
@@ -16,46 +16,49 @@
     <script src="head.js" type="application/javascript"></script>
     <script type="application/javascript">
       "use strict";
 
       /**
        * Test the perf component for when the profiler is already started.
        */
       addPerfTest(async () => {
-        const Perf = require("devtools/client/performance-new/components/Perf");
-        const React = require("devtools/client/shared/vendor/react");
-        const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-        const perfFront = new MockPerfFront();
-        const container = document.querySelector("#container");
+        const {
+          perfFront,
+          getRecordingState,
+          mountComponent,
+          getState,
+          selectors
+        } = createPerfComponent();
 
-        const receiveProfile = () => {};
-        const element = React.createElement(Perf, { perfFront, receiveProfile });
-        const perfComponent = ReactDOM.render(element, container);
+        mountComponent();
 
-        is(perfComponent.state.recordingState, "not-yet-known",
+        is(getRecordingState(), "not-yet-known",
           "The component at first is in an unknown state.");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "available-to-record",
+        is(getRecordingState(), "available-to-record",
           "After talking to the actor, we're ready to record.");
 
         document.querySelector("button").click();
-        is(perfComponent.state.recordingState, "request-to-start-recording",
+        is(getRecordingState(), "request-to-start-recording",
           "Sent in a request to start recording.");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "recording",
+        is(getRecordingState(), "recording",
           "The actor has started its recording");
 
         ok(true, "Simulate a third party stopping the profiler.");
+        ok(!selectors.getRecordingUnexpectedlyStopped(getState()),
+          "The profiler has not unexpectedly stopped.");
+
         perfFront.stopProfilerAndDiscardProfile();
         await perfFront.flushAsyncQueue();
 
-        ok(perfComponent.state.recordingUnexpectedlyStopped,
+        ok(selectors.getRecordingUnexpectedlyStopped(getState()),
           "The profiler unexpectedly stopped.");
-        is(perfComponent.state.recordingState, "available-to-record",
+        is(getRecordingState(), "available-to-record",
           "However, the profiler is available to record again.");
       });
     </script>
   </pre>
 </body>
 </html>
--- a/devtools/client/performance-new/test/chrome/test_perf-state-04.html
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-04.html
@@ -16,49 +16,47 @@
     <script src="head.js" type="application/javascript"></script>
     <script type="application/javascript">
       "use strict";
 
       /**
        * Test that the profiler gets disabled during private browsing.
        */
       addPerfTest(async () => {
-        const Perf = require("devtools/client/performance-new/components/Perf");
-        const React = require("devtools/client/shared/vendor/react");
-        const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-        const perfFront = new MockPerfFront();
-        const container = document.querySelector("#container");
+        const {
+          perfFront,
+          getRecordingState,
+          mountComponent,
+        } = createPerfComponent();
 
         perfFront.mockIsLocked = true;
 
-        const receiveProfile = () => {};
-        const element = React.createElement(Perf, { perfFront, receiveProfile });
-        const perfComponent = ReactDOM.render(element, container);
+        mountComponent();
 
-        is(perfComponent.state.recordingState, "not-yet-known",
+        is(getRecordingState(), "not-yet-known",
           "The component at first is in an unknown state.");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "locked-by-private-browsing",
+        is(getRecordingState(), "locked-by-private-browsing",
           "After talking to the actor, it's locked for private browsing.");
 
         perfFront.mockIsLocked = false;
         perfFront.emit("profile-unlocked-from-private-browsing");
 
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "available-to-record",
+        is(getRecordingState(), "available-to-record",
           "After the profiler is unlocked, it's available to record.");
 
         document.querySelector("button").click();
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "recording",
+        is(getRecordingState(), "recording",
           "The actor has started its recording");
 
         perfFront.mockIsLocked = true;
         perfFront.emit("profile-locked-by-private-browsing");
         await perfFront.flushAsyncQueue();
-        is(perfComponent.state.recordingState, "locked-by-private-browsing",
+        is(getRecordingState(), "locked-by-private-browsing",
           "The recording stops when going into private browsing mode.");
       });
     </script>
   </pre>
 </body>
 </html>
--- a/devtools/client/performance-new/utils.js
+++ b/devtools/client/performance-new/utils.js
@@ -1,13 +1,33 @@
 /* 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";
 
+const recordingState = {
+  // The initial state before we've queried the PerfActor
+  NOT_YET_KNOWN: "not-yet-known",
+  // The profiler is available, we haven't started recording yet.
+  AVAILABLE_TO_RECORD: "available-to-record",
+  // An async request has been sent to start the profiler.
+  REQUEST_TO_START_RECORDING: "request-to-start-recording",
+  // An async request has been sent to get the profile and stop the profiler.
+  REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
+    "request-to-get-profile-and-stop-profiler",
+  // An async request has been sent to stop the profiler.
+  REQUEST_TO_STOP_PROFILER: "request-to-stop-profiler",
+  // The profiler notified us that our request to start it actually started it.
+  RECORDING: "recording",
+  // Some other code with access to the profiler started it.
+  OTHER_IS_RECORDING: "other-is-recording",
+  // Profiling is not available when in private browsing mode.
+  LOCKED_BY_PRIVATE_BROWSING: "locked-by-private-browsing",
+};
+
 const UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
 
 /**
  * Linearly interpolate between values.
  * https://en.wikipedia.org/wiki/Linear_interpolation
  *
  * @param {number} frac - Value ranged 0 - 1 to interpolate between the range
  *                        start and range end.
@@ -110,17 +130,17 @@ function scaleRangeWithClamping(
   );
   return lerp(frac, destRangeStart, destRangeEnd);
 }
 
 /**
  * Use some heuristics to guess at the overhead of the recording settings.
  * @param {number} interval
  * @param {number} bufferSize
- * @param {object} features - Map of the feature name to a boolean.
+ * @param {array} features - List of the selected features.
  */
 function calculateOverhead(interval, bufferSize, features) {
   const overheadFromSampling =
     scaleRangeWithClamping(
       Math.log(interval),
       Math.log(0.05),
       Math.log(1),
       1,
@@ -135,28 +155,29 @@ function calculateOverhead(interval, buf
     );
   const overheadFromBuffersize = scaleRangeWithClamping(
     Math.log(bufferSize),
     Math.log(10),
     Math.log(1000000),
     0,
     0.1
   );
-  const overheadFromStackwalk = features.stackwalk ? 0.05 : 0;
-  const overheadFromJavaScrpt = features.js ? 0.05 : 0;
-  const overheadFromTaskTracer = features.tasktracer ? 0.05 : 0;
+  const overheadFromStackwalk = features.includes("stackwalk") ? 0.05 : 0;
+  const overheadFromJavaScrpt = features.includes("js") ? 0.05 : 0;
+  const overheadFromTaskTracer = features.includes("tasktracer") ? 0.05 : 0;
   return clamp(
     overheadFromSampling +
       overheadFromBuffersize +
       overheadFromStackwalk +
       overheadFromJavaScrpt +
       overheadFromTaskTracer,
     0,
     1
   );
 }
 
 module.exports = {
   formatFileSize,
   makeExponentialScale,
   scaleRangeWithClamping,
-  calculateOverhead
+  calculateOverhead,
+  recordingState
 };
--- a/devtools/client/themes/perf.css
+++ b/devtools/client/themes/perf.css
@@ -17,16 +17,22 @@
   font-size: 120%;
 }
 
 .perf-button-image {
   vertical-align: text-top;
   padding-inline-end: 4px;
 }
 
+.perf-button-container {
+  display: flex;
+  flex-flow: column;
+  align-items: center;
+}
+
 .perf-additional-message {
   margin: 10px;
   margin-top: 65px;
 }
 
 .perf > * {
   max-width: 440px;
 }