Bug 1408124 - Create a new perf actor and recording panel; r?julienw draft
authorGreg Tatum <gtatum@mozilla.com>
Wed, 08 Nov 2017 10:36:43 -0600
changeset 705214 e70e522e67fbe1dc630e7aa624648b408b1437af
parent 703662 cad9c9573579698c223b4b6cb53ca723cd930ad2
child 742299 0236d78e5906c5c9849b01983abfab2ad37fbf4f
push id91402
push usergtatum@mozilla.com
push dateWed, 29 Nov 2017 17:15:19 +0000
reviewersjulienw
bugs1408124
milestone59.0a1
Bug 1408124 - Create a new perf actor and recording panel; r?julienw This patch adds a new performance recording panel that interfaes with perf.html. It is enabled through the new preferences: "devtools.performance.new-panel-enabled" MozReview-Commit-ID: 1HBLsbREDPk
devtools/client/definitions.js
devtools/client/framework/toolbox-options.js
devtools/client/framework/toolbox-process-window.js
devtools/client/jar.mn
devtools/client/moz.build
devtools/client/performance-new/components/Perf.js
devtools/client/performance-new/components/moz.build
devtools/client/performance-new/frame-script.js
devtools/client/performance-new/initializer.js
devtools/client/performance-new/moz.build
devtools/client/performance-new/panel.js
devtools/client/performance-new/perf.xhtml
devtools/client/performance-new/test/.eslintrc.js
devtools/client/performance-new/test/chrome/chrome.ini
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/preferences/devtools.js
devtools/client/themes/perf.css
devtools/server/actors/moz.build
devtools/server/actors/perf.js
devtools/server/main.js
devtools/server/tests/browser/browser.ini
devtools/server/tests/browser/browser_perf-01.js
devtools/server/tests/browser/browser_perf-02.js
devtools/server/tests/browser/browser_perf-03.js
devtools/server/tests/browser/head.js
devtools/shared/fronts/moz.build
devtools/shared/fronts/perf.js
devtools/shared/specs/index.js
devtools/shared/specs/moz.build
devtools/shared/specs/perf.js
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -14,16 +14,17 @@ loader.lazyGetter(this, "WebConsolePanel
 loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/client/debugger/panel").DebuggerPanel);
 loader.lazyGetter(this, "NewDebuggerPanel", () => require("devtools/client/debugger/new/panel").DebuggerPanel);
 loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/client/styleeditor/styleeditor-panel").StyleEditorPanel);
 loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/client/shadereditor/panel").ShaderEditorPanel);
 loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/client/canvasdebugger/panel").CanvasDebuggerPanel);
 loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/client/webaudioeditor/panel").WebAudioEditorPanel);
 loader.lazyGetter(this, "MemoryPanel", () => require("devtools/client/memory/panel").MemoryPanel);
 loader.lazyGetter(this, "PerformancePanel", () => require("devtools/client/performance/panel").PerformancePanel);
+loader.lazyGetter(this, "NewPerformancePanel", () => require("devtools/client/performance-new/panel").PerformancePanel);
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel);
 loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/scratchpad-panel").ScratchpadPanel);
 loader.lazyGetter(this, "DomPanel", () => require("devtools/client/dom/dom-panel").DomPanel);
 
 // Other dependencies
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "CommandState", "devtools/shared/gcli/command-state", true);
@@ -248,38 +249,58 @@ Tools.canvasDebugger = {
   },
 
   build: function (iframeWindow, toolbox) {
     return new CanvasDebuggerPanel(iframeWindow, toolbox);
   }
 };
 
 Tools.performance = {
-  id: "performance",
-  ordinal: 7,
-  icon: "chrome://devtools/skin/images/tool-profiler.svg",
-  url: "chrome://devtools/content/performance/performance.xul",
-  visibilityswitch: "devtools.performance.enabled",
-  label: l10n("performance.label"),
-  panelLabel: l10n("performance.panelLabel"),
-  get tooltip() {
-    return l10n("performance.tooltip", "Shift+" +
-    functionkey(l10n("performance.commandkey")));
-  },
-  accesskey: l10n("performance.accesskey"),
-  inMenu: true,
+ id: "performance",
+ ordinal: 7,
+ icon: "chrome://devtools/skin/images/tool-profiler.svg",
+ visibilityswitch: "devtools.performance.enabled",
+ label: l10n("performance.label"),
+ panelLabel: l10n("performance.panelLabel"),
+ get tooltip() {
+   return l10n("performance.tooltip", "Shift+" +
+   functionkey(l10n("performance.commandkey")));
+ },
+ accesskey: l10n("performance.accesskey"),
+ inMenu: true,
+};
 
-  isTargetSupported: function (target) {
-    return target.hasActor("performance");
-  },
+function switchPerformancePanel() {
+  if (Services.prefs.getBoolPref("devtools.performance.new-panel-enabled", false)) {
+    Tools.performance.url = "chrome://devtools/content/performance-new/perf.xhtml";
+    Tools.performance.build = function (frame, target) {
+      return new NewPerformancePanel(frame, target);
+    };
+    Tools.performance.isTargetSupported = function (target) {
+     // Root actors are lazily initialized, so we can't check if the target has
+     // the perf actor yet. Also this function is not async, so we can't initialize
+     // the actor yet.
+      return true;
+    };
+  } else {
+    Tools.performance.url = "chrome://devtools/content/performance/performance.xul";
+    Tools.performance.build = function (frame, target) {
+      return new PerformancePanel(frame, target);
+    };
+    Tools.performance.isTargetSupported = function (target) {
+      return target.hasActor("performance");
+    };
+  }
+}
+switchPerformancePanel();
 
-  build: function (frame, target) {
-    return new PerformancePanel(frame, target);
-  }
-};
+Services.prefs.addObserver(
+ "devtools.performance.new-panel-enabled",
+ { observe: switchPerformancePanel }
+);
 
 Tools.memory = {
   id: "memory",
   ordinal: 8,
   icon: "chrome://devtools/skin/images/tool-memory.svg",
   url: "chrome://devtools/content/memory/memory.xhtml",
   visibilityswitch: "devtools.memory.enabled",
   label: l10n("memory.label"),
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -323,16 +323,21 @@ OptionsPanel.prototype = {
       label: L10N.getStr("toolbox.options.enableNewConsole.label"),
       id: "devtools-new-webconsole",
       parentId: "webconsole-options"
     }, {
       pref: "devtools.debugger.new-debugger-frontend",
       label: L10N.getStr("toolbox.options.enableNewDebugger.label"),
       id: "devtools-new-debugger",
       parentId: "debugger-options"
+    }, {
+      pref: "devtools.performance.new-panel-enabled",
+      label: "Enable new performance recorder (then re-open DevTools)",
+      id: "devtools-new-performance",
+      parentId: "context-options"
     }];
 
     let createPreferenceOption = ({pref, label, id}) => {
       let inputLabel = this.panelDoc.createElement("label");
       let checkbox = this.panelDoc.createElement("input");
       checkbox.setAttribute("type", "checkbox");
       if (GetPref(pref)) {
         checkbox.setAttribute("checked", "checked");
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -72,16 +72,17 @@ function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
   Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
   Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
   Services.prefs.setBoolPref("devtools.scratchpad.enabled", true);
   // Bug 1225160 - Using source maps with browser debugging can lead to a crash
   Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
   Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
   Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+  Services.prefs.setBoolPref("devtools.preference.new-panel-enabled", false);
 }
 window.addEventListener("load", function () {
   let cmdClose = document.getElementById("toolbox-cmd-close");
   cmdClose.addEventListener("command", onCloseCommand);
   setPrefDefaults();
   connect().catch(e => {
     let errorMessageContainer = document.getElementById("error-message-container");
     let errorMessage = document.getElementById("error-message");
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -58,16 +58,19 @@ devtools.jar:
     content/webaudioeditor/views/utils.js (webaudioeditor/views/utils.js)
     content/webaudioeditor/views/context.js (webaudioeditor/views/context.js)
     content/webaudioeditor/views/inspector.js (webaudioeditor/views/inspector.js)
     content/webaudioeditor/views/properties.js (webaudioeditor/views/properties.js)
     content/webaudioeditor/views/automation.js (webaudioeditor/views/automation.js)
     content/performance/performance.xul (performance/performance.xul)
     content/performance/performance-controller.js (performance/performance-controller.js)
     content/performance/performance-view.js (performance/performance-view.js)
+    content/performance-new/perf.xhtml (performance-new/perf.xhtml)
+    content/performance-new/frame-script.js (performance-new/frame-script.js)
+    content/performance-new/initializer.js (performance-new/initializer.js)
     content/performance/views/overview.js (performance/views/overview.js)
     content/performance/views/toolbar.js (performance/views/toolbar.js)
     content/performance/views/details.js (performance/views/details.js)
     content/performance/views/details-abstract-subview.js (performance/views/details-abstract-subview.js)
     content/performance/views/details-waterfall.js (performance/views/details-waterfall.js)
     content/performance/views/details-js-call-tree.js (performance/views/details-js-call-tree.js)
     content/performance/views/details-js-flamegraph.js (performance/views/details-js-flamegraph.js)
     content/performance/views/details-memory-call-tree.js (performance/views/details-memory-call-tree.js)
@@ -150,16 +153,17 @@ devtools.jar:
     skin/images/breakpoint.svg (themes/images/breakpoint.svg)
     skin/webconsole.css (themes/webconsole.css)
     skin/images/webconsole.svg (themes/images/webconsole.svg)
     skin/images/breadcrumbs-scrollbutton.svg (themes/images/breadcrumbs-scrollbutton.svg)
     skin/animation.css (themes/animation.css)
     skin/animationinspector.css (themes/animationinspector.css)
     skin/canvasdebugger.css (themes/canvasdebugger.css)
     skin/debugger.css (themes/debugger.css)
+    skin/perf.css (themes/perf.css)
     skin/performance.css (themes/performance.css)
     skin/memory.css (themes/memory.css)
     skin/scratchpad.css (themes/scratchpad.css)
     skin/shadereditor.css (themes/shadereditor.css)
     skin/storage.css (themes/storage.css)
     skin/splitview.css (themes/splitview.css)
     skin/styleeditor.css (themes/styleeditor.css)
     skin/webaudioeditor.css (themes/webaudioeditor.css)
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -15,16 +15,17 @@ DIRS += [
     'dom',
     'framework',
     'inspector',
     'jsonview',
     'locales',
     'memory',
     'netmonitor',
     'performance',
+    'performance-new',
     'preferences',
     'responsive.html',
     'scratchpad',
     'shadereditor',
     'shared',
     'sourceeditor',
     'storage',
     'styleeditor',
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/Perf.js
@@ -0,0 +1,356 @@
+/* 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 } = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * 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";
+
+class Perf extends PureComponent {
+  static get propTypes() {
+    return {
+      perfFront: PropTypes.object.isRequired,
+      receiveProfile: 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);
+  }
+
+  componentDidMount() {
+    const { perfFront } = 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;
+      // 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 });
+    });
+
+    // 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) {
+      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;
+
+      case RECORDING:
+      case REQUEST_TO_START_RECORDING:
+        this.props.perfFront.stopProfilerAndDiscardProfile();
+        break;
+
+      default:
+        throw new Error("Unhandled recording state.");
+    }
+  }
+
+  getRecordingStateForTesting() {
+    return this.state.recordingState;
+  }
+
+  handleProfilerStarting() {
+    switch (this.state.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
+        });
+        break;
+
+      case REQUEST_TO_START_RECORDING:
+        // Wait for the profiler to tell us that it has started.
+        this.setState({
+          recordingState: RECORDING,
+          recordingUnexpectedlyStopped: false
+        });
+        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}"`);
+      default:
+        throw new Error("Unhandled recording state");
+    }
+  }
+
+  handleProfilerStopping() {
+    switch (this.state.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
+        });
+        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
+        });
+        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) {
+      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
+        });
+        break;
+
+      case REQUEST_TO_START_RECORDING:
+      case RECORDING:
+        this.setState({
+          recordingState: LOCKED_BY_PRIVATE_BROWSING,
+          recordingUnexpectedlyStopped: true
+        });
+        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() {
+    this.setState({
+      recordingState: REQUEST_TO_START_RECORDING,
+      // Reset this error state since it's no longer valid.
+      recordingUnexpectedlyStopped: false,
+    });
+    this.props.perfFront.startProfiler();
+  }
+
+  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();
+  }
+
+  render() {
+    const { recordingState, isSupportedPlatform } = this.state;
+
+    // Handle the cases of platform support.
+    switch (isSupportedPlatform) {
+      case null:
+        // We don't know yet if this is a supported platform, wait for a response.
+        return null;
+      case false:
+        return renderButton({
+          label: "Start recording",
+          disabled: true,
+          additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
+                             "supports Tier-1 platforms."
+        });
+      case true:
+        // Continue on and render the panel.
+        break;
+    }
+
+    // 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: "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");
+    }
+  }
+}
+
+module.exports = Perf;
+
+function renderButton(props) {
+  const { disabled, label, onClick, additionalMessage } = props;
+  const nbsp = "\u00A0";
+
+  return div(
+    { className: "perf" },
+    div({ className: "perf-additional-message" }, additionalMessage || nbsp),
+    div(
+      null,
+      button(
+        {
+          className: "devtools-button perf-button",
+          "data-standalone": true,
+          disabled,
+          onClick
+        },
+        label
+      )
+    )
+  );
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/moz.build
@@ -0,0 +1,8 @@
+# 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(
+    'Perf.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/frame-script.js
@@ -0,0 +1,106 @@
+/* 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";
+/* global addMessageListener, addEventListener, content */
+
+/**
+ * This frame script injects itself into perf-html.io and injects the profile
+ * into the page. It is mostly taken from the Gecko Profiler Addon implementation.
+ */
+
+const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
+
+let gProfile = null;
+
+addMessageListener(TRANSFER_EVENT, e => {
+  gProfile = e.data;
+  // Eagerly try and see if the framescript was evaluated after perf loaded its scripts.
+  connectToPage();
+  // If not try again at DOMContentLoaded which should be called after the script
+  // tag was synchronously loaded in.
+  addEventListener("DOMContentLoaded", connectToPage);
+});
+
+function connectToPage() {
+  const unsafeWindow = content.wrappedJSObject;
+  if (unsafeWindow.connectToGeckoProfiler) {
+    unsafeWindow.connectToGeckoProfiler(makeAccessibleToPage({
+      getProfile: () => Promise.resolve(gProfile),
+      getSymbolTable: (debugName, breakpadId) => getSymbolTable(debugName, breakpadId),
+    }, unsafeWindow));
+  }
+}
+
+/**
+ * For now, do not try to symbolicate. Reject any attempt.
+ */
+function getSymbolTable(debugName, breakpadId) {
+  // Errors will not properly clone into the content page as they bring privileged
+  // stacks information into the page. In this case provide a mock object to maintain
+  // the Error type object shape.
+  const error = {
+    message: `The DevTools' "perf" actor does not support symbolication.`
+  };
+  return Promise.reject(error);
+}
+
+// The following functions handle the security of cloning the object into the page.
+// The code was taken from the original Gecko Profiler Add-on to maintain
+// compatibility with the existing profile importing mechanism:
+// See: https://github.com/devtools-html/Gecko-Profiler-Addon/blob/78138190b42565f54ce4022a5b28583406489ed2/data/tab-framescript.js
+
+/**
+ * Create a promise that can be used in the page.
+ */
+function createPromiseInPage(fun, contentGlobal) {
+  function funThatClonesObjects(resolve, reject) {
+    return fun(result => resolve(Components.utils.cloneInto(result, contentGlobal)),
+               error => reject(Components.utils.cloneInto(error, contentGlobal)));
+  }
+  return new contentGlobal.Promise(Components.utils.exportFunction(funThatClonesObjects,
+                                                                   contentGlobal));
+}
+
+/**
+ * Returns a function that calls the original function and tries to make the
+ * return value available to the page.
+ */
+function wrapFunction(fun, contentGlobal) {
+  return function () {
+    let result = fun.apply(this, arguments);
+    if (typeof result === "object") {
+      if (("then" in result) && (typeof result.then === "function")) {
+        // fun returned a promise.
+        return createPromiseInPage((resolve, reject) =>
+          result.then(resolve, reject), contentGlobal);
+      }
+      return Components.utils.cloneInto(result, contentGlobal);
+    }
+    return result;
+  };
+}
+
+/**
+ * Pass a simple object containing values that are objects or functions.
+ * The objects or functions are wrapped in such a way that they can be
+ * consumed by the page.
+ */
+function makeAccessibleToPage(obj, contentGlobal) {
+  let result = Components.utils.createObjectIn(contentGlobal);
+  for (let field in obj) {
+    switch (typeof obj[field]) {
+      case "function":
+        Components.utils.exportFunction(
+          wrapFunction(obj[field], contentGlobal), result, { defineAs: field });
+        break;
+      case "object":
+        Components.utils.cloneInto(obj[field], result, { defineAs: field });
+        break;
+      default:
+        result[field] = obj[field];
+        break;
+    }
+  }
+  return result;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/initializer.js
@@ -0,0 +1,44 @@
+/* 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";
+
+/* exported gInit, gDestroy */
+
+const BrowserLoaderModule = {};
+Components.utils.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 { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+const { createElement } = require("devtools/client/shared/vendor/react");
+
+/**
+ * Perform a simple initialization on the panel. Hook up event listeners.
+ *
+ * @param perfFront - The Perf actor's front. Used to start and stop recordings.
+ */
+function gInit(perfFront) {
+  const props = {
+    perfFront,
+    receiveProfile: profile => {
+      // Open up a new tab and send a message with the profile.
+      const browser = top.gBrowser;
+      const tab = browser.addTab("https://perf-html.io/from-addon");
+      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"));
+}
+
+function gDestroy() {
+  unmountComponentAtNode(document.querySelector("#root"));
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/moz.build
@@ -0,0 +1,17 @@
+# 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',
+]
+
+DevToolsModules(
+    'panel.js',
+)
+
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
+
+with Files('**'):
+    BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/panel.js
@@ -0,0 +1,59 @@
+/* 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 { PerfFront } = require("devtools/shared/fronts/perf");
+
+loader.lazyRequireGetter(this, "EventEmitter",
+  "devtools/shared/old-event-emitter");
+
+class PerformancePanel {
+  constructor(iframeWindow, toolbox) {
+    this.panelWin = iframeWindow;
+    this.toolbox = toolbox;
+
+    EventEmitter.decorate(this);
+  }
+
+  /**
+   * Open is effectively an asynchronous constructor.
+   * @return {Promise} Resolves when the Perf tool completes opening.
+   */
+  open() {
+    if (!this._opening) {
+      this._opening = this._doOpen();
+    }
+    return this._opening;
+  }
+
+  async _doOpen() {
+    this.panelWin.gToolbox = this.toolbox;
+    this.panelWin.gTarget = this.target;
+
+    const rootForm = await this.target.root;
+    const perfFront = new PerfFront(this.target.client, rootForm);
+
+    this.isReady = true;
+    this.emit("ready");
+    this.panelWin.gInit(perfFront);
+    return this;
+  }
+
+  // DevToolPanel API:
+
+  get target() {
+    return this.toolbox.target;
+  }
+
+  async destroy() {
+    // Make sure this panel is not already destroyed.
+    if (this._destroyed) {
+      return;
+    }
+    this.panelWin.gDestroy();
+    this.emit("destroyed");
+    this._destroyed = true;
+  }
+}
+exports.PerformancePanel = PerformancePanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/perf.xhtml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html [
+  <!ENTITY % htmlDTD
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "DTD/xhtml1-strict.dtd">
+  %htmlDTD;
+]>
+
+<!-- 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/. -->
+<html xmlns="http://www.w3.org/1999/xhtml" dir="">
+  <head>
+    <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
+    <link rel="stylesheet" href="chrome://devtools/skin/perf.css" type="text/css"/>
+  </head>
+  <body class="theme-body">
+    <div id="root"></div>
+    <script type="application/javascript" src="initializer.js"></script>
+    <script type="application/javascript"
+            src="chrome://devtools/content/shared/theme-switching.js"
+            defer="true">
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+  // Extend from the shared list of defined globals for mochitests.
+  "extends": "../../../.eslintrc.mochitests.js"
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+  head.js
+
+[test_perf-state-01.html]
+[test_perf-state-02.html]
+[test_perf-state-03.html]
+[test_perf-state-04.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/head.js
@@ -0,0 +1,133 @@
+/* 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";
+
+/* exported addPerfTest, MockPerfFront */
+/* globals URL_ROOT */
+
+const { BrowserLoader } = Components.utils.import("resource://devtools/client/shared/browser-loader.js", {});
+var { require } = BrowserLoader({
+  baseURI: "resource://devtools/client/performance-new/",
+  window
+});
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { perfDescription } = require("devtools/shared/specs/perf");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+
+flags.testing = true;
+let EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+SimpleTest.registerCleanupFunction(function () {
+  if (DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
+    ok(false, "Should have had the expected number of DevToolsUtils.assert() failures." +
+      "Expected " + EXPECTED_DTU_ASSERT_FAILURE_COUNT +
+      ", got " + DevToolsUtils.assertionFailureCount);
+  }
+});
+
+/**
+ * Handle test setup and teardown while catching errors.
+ */
+function addPerfTest(asyncTest) {
+  window.onload = async () => {
+    try {
+      await asyncTest();
+    } catch (e) {
+      ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+    } finally {
+      SimpleTest.finish();
+    }
+  };
+}
+
+/**
+ * The Gecko Profiler is a rather heavy-handed component that uses a lot of resources.
+ * In order to get around that, and have quick component tests we provide a mock of
+ * the performance front. It also has a method called flushAsyncQueue() that will
+ * flush any queued async calls to deterministically run our tests.
+ */
+class MockPerfFront extends EventEmitter {
+  constructor() {
+    super();
+    this._isActive = false;
+    this._asyncQueue = [];
+
+    // Tests can update these two values directly as needed.
+    this.mockIsSupported = true;
+    this.mockIsLocked = false;
+
+    // Wrap all async methods in a flushable queue, so that tests can control
+    // when the responses come back.
+    this.isActive = this._wrapInAsyncQueue(this.isActive);
+    this.startProfiler = this._wrapInAsyncQueue(this.startProfiler);
+    this.stopProfilerAndDiscardProfile = this._wrapInAsyncQueue(
+      this.stopProfilerAndDiscardProfile);
+    this.getProfileAndStopProfiler = this._wrapInAsyncQueue(
+      this.getProfileAndStopProfiler);
+  }
+
+  /**
+   * Provide a flushable queue mechanism for all async work. The work piles up
+   * and then is evaluated at once when _flushPendingQueue is called.
+   */
+  _wrapInAsyncQueue(fn) {
+    if (typeof fn !== "function") {
+      throw new Error("_wrapInAsyncQueue requires a function");
+    }
+    return (...args) => {
+      return new Promise(resolve => {
+        this._asyncQueue.push(() => {
+          resolve(fn.apply(this, args));
+        });
+      });
+    };
+  }
+
+  flushAsyncQueue() {
+    const pending = this._asyncQueue;
+    this._asyncQueue = [];
+    pending.forEach(fn => fn());
+    // Ensure this is async.
+    return new Promise(resolve => setTimeout(resolve, 0));
+  }
+
+  startProfiler() {
+    this._isActive = true;
+    this.emit("profiler-started");
+  }
+
+  getProfileAndStopProfiler() {
+    this._isActive = false;
+    this.emit("profiler-stopped");
+    // Return a fake profile.
+    return {};
+  }
+
+  stopProfilerAndDiscardProfile() {
+    this._isActive = false;
+    this.emit("profiler-stopped");
+  }
+
+  isActive() {
+    return this._isActive;
+  }
+
+  isSupportedPlatform() {
+    return this.mockIsSupported;
+  }
+
+  isLockedForPrivateBrowsing() {
+    return this.mockIsLocked;
+  }
+}
+
+// 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.");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-01.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+  <meta charset="utf-8">
+  <title>Perf component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="container"></div>
+
+  <pre id="test">
+    <script src="head.js" type="application/javascript"></script>
+    <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");
+
+        // Inject a function which will allow us to receive the profile.
+        let profile;
+        function receiveProfile(profileIn) {
+          profile = profileIn;
+        }
+
+        const element = React.createElement(Perf, { perfFront, receiveProfile });
+        const perfComponent = ReactDOM.render(element, container);
+        is(perfComponent.state.recordingState, "not-yet-known",
+          "The component at first is in an unknown state.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "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",
+          "Sent in a request to start recording.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "recording",
+          "The actor has started its recording");
+
+        button.click();
+        is(perfComponent.state.recordingState,
+          "request-to-get-profile-and-stop-profiler",
+          "We have requested to stop the profiler.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "available-to-record",
+          "The profiler is available to record again.");
+        await perfFront.flushAsyncQueue();
+        is(typeof profile, "object", "Got a profile");
+      });
+    </script>
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-02.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+  <meta charset="utf-8">
+  <title>Perf component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="container"></div>
+
+  <pre id="test">
+    <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");
+
+        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",
+          "The component at first is in an unknown state.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "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",
+          "We can request to stop the profiler.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "available-to-record",
+          "The profiler is now available to record.");
+      });
+    </script>
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-03.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+  <meta charset="utf-8">
+  <title>Perf component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="container"></div>
+
+  <pre id="test">
+    <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 receiveProfile = () => {};
+        const element = React.createElement(Perf, { perfFront, receiveProfile });
+        const perfComponent = ReactDOM.render(element, container);
+
+        is(perfComponent.state.recordingState, "not-yet-known",
+          "The component at first is in an unknown state.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "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",
+          "Sent in a request to start recording.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "recording",
+          "The actor has started its recording");
+
+        ok(true, "Simulate a third party stopping the profiler.");
+        perfFront.stopProfilerAndDiscardProfile();
+        await perfFront.flushAsyncQueue();
+
+        ok(perfComponent.state.recordingUnexpectedlyStopped,
+          "The profiler unexpectedly stopped.");
+        is(perfComponent.state.recordingState, "available-to-record",
+          "However, the profiler is available to record again.");
+      });
+    </script>
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-04.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+  <meta charset="utf-8">
+  <title>Perf component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="container"></div>
+
+  <pre id="test">
+    <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");
+
+        perfFront.mockIsLocked = true;
+
+        const receiveProfile = () => {};
+        const element = React.createElement(Perf, { perfFront, receiveProfile });
+        const perfComponent = ReactDOM.render(element, container);
+
+        is(perfComponent.state.recordingState, "not-yet-known",
+          "The component at first is in an unknown state.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "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",
+          "After the profiler is unlocked, it's available to record.");
+
+        document.querySelector("button").click();
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "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",
+          "The recording stops when going into private browsing mode.");
+      });
+    </script>
+  </pre>
+</body>
+</html>
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -300,16 +300,19 @@ pref("devtools.webconsole.timestampMessa
 pref("devtools.webconsole.autoMultiline", true);
 
 // Enable the new webconsole frontend
 pref("devtools.webconsole.new-frontend-enabled", true);
 
 // Enable the webconsole sidebar toggle
 pref("devtools.webconsole.sidebarToggle", false);
 
+// Disable the new performance recording panel by default
+pref("devtools.performance.new-panel-enabled", false);
+
 // Enable client-side mapping service for source maps
 pref("devtools.source-map.client-service.enabled", true);
 
 // The number of lines that are displayed in the web console.
 pref("devtools.hud.loglimit", 10000);
 
 // The number of lines that are displayed in the old web console for the Net,
 // CSS, JS and Web Developer categories. These defaults should be kept in sync
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/perf.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+.perf {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.devtools-button.perf-button {
+  padding: 5px;
+  margin: auto;
+  font-size: 120%;
+}
+
+.perf-additional-message {
+  margin: 10px;
+  margin-top: 65px;
+}
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -37,16 +37,17 @@ DevToolsModules(
     'heap-snapshot-file.js',
     'highlighters.css',
     'highlighters.js',
     'inspector.js',
     'layout.js',
     'memory.js',
     'monitor.js',
     'object.js',
+    'perf.js',
     'performance-recording.js',
     'performance.js',
     'preference.js',
     'pretty-print-worker.js',
     'process.js',
     'promises.js',
     'reflow.js',
     'root.js',
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/perf.js
@@ -0,0 +1,153 @@
+/* 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 protocol = require("devtools/shared/protocol");
+const { ActorClassWithSpec, Actor } = protocol;
+const { perfSpec } = require("devtools/shared/specs/perf");
+const { Cc, Ci } = require("chrome");
+const Services = require("Services");
+
+loader.lazyGetter(this, "geckoProfiler", () => {
+  return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
+});
+
+loader.lazyImporter(this, "PrivateBrowsingUtils",
+  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+// Some platforms are built without the Gecko Profiler.
+const IS_SUPPORTED_PLATFORM = "nsIProfiler" in Ci;
+
+/**
+ * The PerfActor wraps the Gecko Profiler interface
+ */
+exports.PerfActor = ActorClassWithSpec(perfSpec, {
+  initialize(conn) {
+    Actor.prototype.initialize.call(this, conn);
+
+    // Only setup the observers on a supported platform.
+    if (IS_SUPPORTED_PLATFORM) {
+      this._observer = {
+        observe: this._observe.bind(this)
+      };
+      Services.obs.addObserver(this._observer, "profiler-started");
+      Services.obs.addObserver(this._observer, "profiler-stopped");
+      Services.obs.addObserver(this._observer, "chrome-document-global-created");
+      Services.obs.addObserver(this._observer, "last-pb-context-exited");
+    }
+  },
+
+  destroy() {
+    if (!IS_SUPPORTED_PLATFORM) {
+      return;
+    }
+    Services.obs.removeObserver(this._observer, "profiler-started");
+    Services.obs.removeObserver(this._observer, "profiler-stopped");
+    Services.obs.removeObserver(this._observer, "chrome-document-global-created");
+    Services.obs.removeObserver(this._observer, "last-pb-context-exited");
+    Actor.prototype.destroy.call(this);
+  },
+
+  startProfiler() {
+    if (!IS_SUPPORTED_PLATFORM) {
+      return false;
+    }
+
+    // For a quick implementation, decide on some default values. These may need
+    // to be tweaked or made configurable as needed.
+    const settings = {
+      entries: 1000000,
+      interval: 1,
+      features: ["js", "stackwalk", "threads", "leaf"],
+      threads: ["GeckoMain", "Compositor"]
+    };
+
+    try {
+      // This can throw an error if the profiler is in the wrong state.
+      geckoProfiler.StartProfiler(
+        settings.entries,
+        settings.interval,
+        settings.features,
+        settings.features.length,
+        settings.threads,
+        settings.threads.length
+      );
+    } catch (e) {
+      // In case any errors get triggered, bailout with a false.
+      return false;
+    }
+
+    return true;
+  },
+
+  stopProfilerAndDiscardProfile() {
+    if (!IS_SUPPORTED_PLATFORM) {
+      return;
+    }
+    geckoProfiler.StopProfiler();
+  },
+
+  async getProfileAndStopProfiler() {
+    if (!IS_SUPPORTED_PLATFORM) {
+      return null;
+    }
+    let profile;
+    try {
+      // Attempt to pull out the data.
+      profile = await geckoProfiler.getProfileDataAsync();
+
+      // Stop and discard the buffers.
+      geckoProfiler.StopProfiler();
+    } catch (e) {
+      // If there was any kind of error, bailout with no profile.
+      return null;
+    }
+
+    // Gecko Profiler errors can return an empty object, return null for this case
+    // as well.
+    if (Object.keys(profile).length === 0) {
+      return null;
+    }
+    return profile;
+  },
+
+  isActive() {
+    if (!IS_SUPPORTED_PLATFORM) {
+      return false;
+    }
+    return geckoProfiler.IsActive();
+  },
+
+  isSupportedPlatform() {
+    return IS_SUPPORTED_PLATFORM;
+  },
+
+  isLockedForPrivateBrowsing() {
+    if (!IS_SUPPORTED_PLATFORM) {
+      return false;
+    }
+    return !geckoProfiler.CanProfile();
+  },
+
+  /**
+   * Watch for events that happen within the browser. These can affect the current
+   * availability and state of the Gecko Profiler.
+   */
+  _observe(subject, topic, _data) {
+    switch (topic) {
+      case "chrome-document-global-created":
+        if (PrivateBrowsingUtils.isWindowPrivate(subject)) {
+          this.emit("profile-locked-by-private-browsing");
+        }
+        break;
+      case "last-pb-context-exited":
+        this.emit("profile-unlocked-from-private-browsing");
+        break;
+      case "profiler-started":
+      case "profiler-stopped":
+        this.emit(topic);
+        break;
+    }
+  }
+});
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -438,16 +438,25 @@ var DebuggerServer = {
       constructor: "DeviceActor",
       type: { global: true }
     });
     this.registerModule("devtools/server/actors/heap-snapshot-file", {
       prefix: "heapSnapshotFile",
       constructor: "HeapSnapshotFileActor",
       type: { global: true }
     });
+    // Always register this as a global module, even while there is a pref turning
+    // on and off the other performance actor. This actor shouldn't conflict with
+    // the other one. These are also lazily loaded so there shouldn't be a performance
+    // impact.
+    this.registerModule("devtools/server/actors/perf", {
+      prefix: "perf",
+      constructor: "PerfActor",
+      type: { global: true }
+    });
   },
 
   /**
    * Install tab actors.
    */
   _addTabActors() {
     this.registerModule("devtools/server/actors/webconsole", {
       prefix: "console",
@@ -529,17 +538,18 @@ var DebuggerServer = {
       constructor: "MonitorActor",
       type: { tab: true }
     });
     this.registerModule("devtools/server/actors/timeline", {
       prefix: "timeline",
       constructor: "TimelineActor",
       type: { tab: true }
     });
-    if ("nsIProfiler" in Ci) {
+    if ("nsIProfiler" in Ci &&
+        !Services.prefs.getBoolPref("devtools.performance.new-panel-enabled", false)) {
       this.registerModule("devtools/server/actors/performance", {
         prefix: "performance",
         constructor: "PerformanceActor",
         type: { tab: true }
       });
     }
     this.registerModule("devtools/server/actors/animation", {
       prefix: "animations",
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -67,16 +67,19 @@ skip-if = e10s # Bug 1183605 - devtools/
 [browser_markers-docloading-03.js]
 [browser_markers-gc.js]
 [browser_markers-minor-gc.js]
 [browser_markers-parse-html.js]
 [browser_markers-styles.js]
 [browser_markers-timestamp.js]
 [browser_navigateEvents.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
+[browser_perf-01.js]
+[browser_perf-02.js]
+[browser_perf-03.js]
 [browser_perf-allocation-data.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [browser_perf-profiler-01.js]
 [browser_perf-profiler-02.js]
 skip-if = true # Needs to be updated for async actor destruction
 [browser_perf-profiler-03.js]
 skip-if = true # Needs to be updated for async actor destruction
 [browser_perf-realtime-markers.js]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-01.js
@@ -0,0 +1,47 @@
+/* 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";
+
+/**
+ * Run through a series of basic recording actions for the perf actor.
+ */
+add_task(async function () {
+  const {front, client} = await initPerfFront();
+
+  // Assert the initial state.
+  is(await front.isSupportedPlatform(), true,
+    "This test only runs on supported platforms.");
+  is(await front.isLockedForPrivateBrowsing(), false,
+    "The browser is not in private browsing mode.");
+  is(await front.isActive(), false,
+    "The profiler is not active yet.");
+
+  // Start the profiler.
+  const profilerStarted = once(front, "profiler-started");
+  await front.startProfiler();
+  await profilerStarted;
+  is(await front.isActive(), true, "The profiler was started.");
+
+  // Stop the profiler and assert the results.
+  const profilerStopped1 = once(front, "profiler-stopped");
+  const profile = await front.getProfileAndStopProfiler();
+  await profilerStopped1;
+  is(await front.isActive(), false, "The profiler was stopped.");
+  ok("threads" in profile, "The actor was used to record a profile.");
+
+  // Restart the profiler.
+  await front.startProfiler();
+  is(await front.isActive(), true, "The profiler was re-started.");
+
+  // Stop and discard.
+  const profilerStopped2 = once(front, "profiler-stopped");
+  await front.stopProfilerAndDiscardProfile();
+  await profilerStopped2;
+  is(await front.isActive(), false,
+    "The profiler was stopped and the profile discarded.");
+
+  // Clean up.
+  await front.destroy();
+  await client.close();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-02.js
@@ -0,0 +1,30 @@
+/* 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";
+
+/**
+ * Test what happens when other tools control the profiler.
+ */
+add_task(async function () {
+  const {front, client} = await initPerfFront();
+
+  // Simulate other tools by getting an independent handle on the Gecko Profiler.
+  const geckoProfiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
+
+  is(await front.isActive(), false, "The profiler hasn't been started yet.");
+
+  // Start the profiler.
+  await front.startProfiler();
+  is(await front.isActive(), true, "The profiler was started.");
+
+  // Stop the profiler manually through the Gecko Profiler interface.
+  const profilerStopped = once(front, "profiler-stopped");
+  geckoProfiler.StopProfiler();
+  await profilerStopped;
+  is(await front.isActive(), false, "The profiler was stopped by another tool.");
+
+  // Clean up.
+  await front.destroy();
+  await client.close();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-03.js
@@ -0,0 +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";
+
+/**
+ * Test that the profiler emits events when private browsing windows are opened
+ * and closed.
+ */
+add_task(async function () {
+  const {front, client} = await initPerfFront();
+
+  is(await front.isLockedForPrivateBrowsing(), false,
+    "The profiler is not locked for private browsing.");
+
+  // Open up a new private browser window, and assert the correct events are fired.
+  const profilerLocked = once(front, "profile-locked-by-private-browsing");
+  const privateWindow = await BrowserTestUtils.openNewBrowserWindow({private: true});
+  await profilerLocked;
+  is(await front.isLockedForPrivateBrowsing(), true,
+    "The profiler is now locked because of private browsing.");
+
+  // Close the private browser window, and assert the correct events are fired.
+  const profilerUnlocked = once(front, "profile-unlocked-from-private-browsing");
+  await BrowserTestUtils.closeWindow(privateWindow);
+  await profilerUnlocked;
+  is(await front.isLockedForPrivateBrowsing(), false,
+    "The profiler is available again after closing the private browsing window.");
+
+  // Clean up.
+  await front.destroy();
+  await client.close();
+});
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -103,16 +103,51 @@ function initDebuggerServer() {
     DebuggerServer.destroy();
   } catch (e) {
     info(`DebuggerServer destroy error: ${e}\n${e.stack}`);
   }
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
 }
 
+async function initPerfFront() {
+  const {PerfFront} = require("devtools/shared/fronts/perf");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  await waitUntilClientConnected(client);
+  const rootForm = await getRootForm(client);
+  const front = PerfFront(client, rootForm);
+  return {front, client};
+}
+
+/**
+ * Gets the RootActor form from a DebuggerClient.
+ * @param {DebuggerClient} client
+ * @return {RootActor} Resolves when connected.
+ */
+function getRootForm(client) {
+  return new Promise(resolve => {
+    client.listTabs(rootForm => {
+      resolve(rootForm);
+    });
+  });
+}
+
+/**
+ * Wait until a DebuggerClient is connected.
+ * @param {DebuggerClient} client
+ * @return {Promise} Resolves when connected.
+ */
+function waitUntilClientConnected(client) {
+  return new Promise(resolve => {
+    client.addOneTimeListener("connected", resolve);
+  });
+}
+
 /**
  * Connect a debugger client.
  * @param {DebuggerClient}
  * @return {Promise} Resolves to the selected tabActor form when the client is
  * connected.
  */
 function connectDebuggerClient(client) {
   return client.connect()
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -18,16 +18,17 @@ DevToolsModules(
     'eventlooplag.js',
     'framerate.js',
     'gcli.js',
     'highlighters.js',
     'inspector.js',
     'layout.js',
     'memory.js',
     'node.js',
+    'perf.js',
     'performance-recording.js',
     'performance.js',
     'preference.js',
     'promises.js',
     'reflow.js',
     'storage.js',
     'string.js',
     'styles.js',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/perf.js
@@ -0,0 +1,15 @@
+/* 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 { FrontClassWithSpec, Front } = require("devtools/shared/protocol");
+const { perfSpec } = require("devtools/shared/specs/perf");
+
+exports.PerfFront = FrontClassWithSpec(perfSpec, {
+  initialize: function (client, form) {
+    Front.prototype.initialize.call(this, client, form);
+    this.actorID = form.perfActor;
+    this.manage(this);
+  }
+});
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -128,16 +128,21 @@ const Types = exports.__TypesForTests = 
     front: null,
   },
   {
     types: ["domnode", "domnodelist"],
     spec: "devtools/shared/specs/node",
     front: "devtools/shared/fronts/node",
   },
   {
+    types: ["perf"],
+    spec: "devtools/shared/specs/perf",
+    front: "devtools/shared/fronts/perf",
+  },
+  {
     types: ["performance"],
     spec: "devtools/shared/specs/performance",
     front: "devtools/shared/fronts/performance",
   },
   {
     types: ["performance-recording"],
     spec: "devtools/shared/specs/performance-recording",
     front: "devtools/shared/fronts/performance-recording",
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -23,16 +23,17 @@ DevToolsModules(
     'gcli.js',
     'heap-snapshot-file.js',
     'highlighters.js',
     'index.js',
     'inspector.js',
     'layout.js',
     'memory.js',
     'node.js',
+    'perf.js',
     'performance-recording.js',
     'performance.js',
     'preference.js',
     'promises.js',
     'reflow.js',
     'script.js',
     'source.js',
     'storage.js',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/perf.js
@@ -0,0 +1,66 @@
+/* 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 { RetVal, generateActorSpec } = require("devtools/shared/protocol");
+
+const perfDescription = {
+  typeName: "perf",
+
+  events: {
+    "profiler-started": {
+      type: "profiler-started"
+    },
+    "profiler-stopped": {
+      type: "profiler-stopped"
+    },
+    "profile-locked-by-private-browsing": {
+      type: "profile-locked-by-private-browsing"
+    },
+    "profile-unlocked-from-private-browsing": {
+      type: "profile-unlocked-from-private-browsing"
+    }
+  },
+
+  methods: {
+    startProfiler: {
+      request: {},
+      response: { value: RetVal("boolean") }
+    },
+
+    /**
+     * Returns null when unable to return the profile.
+     */
+    getProfileAndStopProfiler: {
+      request: {},
+      response: RetVal("nullable:json")
+    },
+
+    stopProfilerAndDiscardProfile: {
+      request: {},
+      response: {}
+    },
+
+    isActive: {
+      request: {},
+      response: { value: RetVal("boolean") }
+    },
+
+    isSupportedPlatform: {
+      request: {},
+      response: { value: RetVal("boolean") }
+    },
+
+    isLockedForPrivateBrowsing: {
+      request: {},
+      response: { value: RetVal("boolean") }
+    }
+  }
+};
+
+exports.perfDescription = perfDescription;
+
+const perfSpec = generateActorSpec(perfDescription);
+
+exports.perfSpec = perfSpec;