Bug 1408124 - Properly handle profiler state in the recording panel draft
authorGreg Tatum <gtatum@mozilla.com>
Fri, 27 Oct 2017 09:00:00 -0500
changeset 695083 b8348691e824e597c60f57b2195b7e19154ff1bc
parent 695082 7bdc15947aa804835591facc60cc18a2fa8af206
child 695084 d54258c6b2a666848b414b1ddf5253ebb949830f
push id88335
push usergtatum@mozilla.com
push dateWed, 08 Nov 2017 18:51:46 +0000
bugs1408124
milestone58.0a1
Bug 1408124 - Properly handle profiler state in the recording panel MozReview-Commit-ID: KdrUebBBvNm
devtools/client/jar.mn
devtools/client/performance/new/initializer.js
devtools/client/performance/new/perf.xhtml
devtools/client/themes/perf.css
devtools/server/actors/perf.js
devtools/shared/specs/perf.js
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -164,16 +164,17 @@ devtools.jar:
     skin/webconsole.css (themes/webconsole.css)
     skin/images/webconsole.svg (themes/images/webconsole.svg)
     skin/images/breadcrumbs-scrollbutton.png (themes/images/breadcrumbs-scrollbutton.png)
     skin/images/breadcrumbs-scrollbutton@2x.png (themes/images/breadcrumbs-scrollbutton@2x.png)
     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/performance/new/initializer.js
+++ b/devtools/client/performance/new/initializer.js
@@ -1,25 +1,254 @@
 /* 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 */
 
+const { utils: Cu } = Components;
+const BrowserLoaderModule = {};
+Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+const { require } = BrowserLoaderModule.BrowserLoader({
+  baseURI: "resource://devtools/client/memory/",
+  window
+});
+const { createClass, createElement, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const { render } = require("devtools/client/shared/vendor/react-dom");
+const { div, button } = DOM;
+
 /**
- * Perform a simple initialization on the panel. Hook up event listeners.
- */
-function gInit(perfFront) {
-  document.querySelector("#startRecording").addEventListener("click", () => {
-    perfFront.startProfiler();
-  });
+ * 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";
+
+const Perf = createClass({
+  displayName: "Perf",
+
+  propTypes: {
+    perfFront: PropTypes.object.isRequired,
+  },
+
+  getInitialState() {
+    return {
+      recordingState: NOT_YET_KNOWN,
+      recordingUnexpectedlyStopped: false,
+    };
+  },
+
+  componentDidUpdate() {
+    console.log("!!! recordingState", this.state.recordingState);
+  },
+
+  componentDidMount() {
+    // We don't yet know what state the profile is in, find out.
+    this.props.perfFront.isActive().then(isActive => {
+      if (this.state.recordingState === NOT_YET_KNOWN) {
+        this.setState({
+          recordingState: isActive ? OTHER_IS_RECORDING : AVAILABLE_TO_RECORD
+        });
+      }
+    });
+
+    // Handle when the profiler changes state. It might be us, it might be someone else.
+    this.props.perfFront.on("profiler-started", this.handleProfilerStarting);
+
+    // Handle when the profiler changes state. It might be us, it might be someone else.
+    this.props.perfFront.on("profiler-stopped", this.handleProfilerStopping);
+  },
 
-  document.querySelector("#stopRecording").addEventListener("click", async () => {
-    const profile = await perfFront.stopProfiler();
+  handleProfilerStarting() {
+    switch (this.state.recordingState) {
+      // Eslint doesn't allow comments between case statements:
+      //
+      // case NOT_YET_KNOWN:
+      //   We couldn't have started it yet, so it must have been someone else.
+      // case AVAILABLE_TO_RECORD
+      //   We aren't recording, someone else started it up.
+      // case REQUEST_TO_STOP_PROFILER
+      //   Someone re-started the profiler while we were asking for the completed
+      //   profile.
+      // case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER
+      //   Someone re-started the profiler while we were asking for the completed
+      //   profile.
+      case NOT_YET_KNOWN:
+      case AVAILABLE_TO_RECORD:
+      case REQUEST_TO_STOP_PROFILER:
+      case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
+        this.setState({
+          recordingState: OTHER_IS_RECORDING,
+          recordingUnexpectedlyStopped: false
+        });
+        break;
+
+      // Wait for the profiler to tell us that it has started.
+      case REQUEST_TO_START_RECORDING:
+        this.setState({
+          recordingState: RECORDING,
+          recordingUnexpectedlyStopped: false
+        });
+        break;
+
+      // These don't make sense to happen, and means we have a logical fallacy somewhere.
+      case OTHER_IS_RECORDING:
+      case RECORDING:
+        throw new Error(
+          "The profiler started recording, when it shouldn't have been able to.");
+      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;
+
+      // Highly unlikely, but someone stopped the recorder, this is fine.
+      case REQUEST_TO_START_RECORDING:
+        // Do nothing.
+        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");
+    }
+  },
+
+  startRecording() {
+    this.setState({
+      recordingState: REQUEST_TO_START_RECORDING,
+      // Reset this error state since it's no longer valid.
+      recordingUnexpectedlyStopped: false,
+    });
+    this.props.perfFront.startProfiler();
+  },
+
+  getProfileAndStopProfiler: async function () {
+    this.setState({ recordingState: REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER });
+    const profile = await this.props.perfFront.getProfileAndStopProfiler();
+    this.setState({ recordingState: AVAILABLE_TO_RECORD });
+
+    // 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);
-  });
+  },
+
+  stopProfilerAndDiscardProfile: async function () {
+    this.props.perfFront.stopProfilerAndDiscardProfile();
+  },
+
+  render() {
+    // TODO - L10N all of the messages.
+    switch (this.state.recordingState) {
+      case NOT_YET_KNOWN:
+        return null;
+
+      case AVAILABLE_TO_RECORD:
+        return renderButton({
+          onClick: this.startRecording,
+          label: "Start recording",
+          additionalMessage: this.state.recordingUnexpectedlyStopped
+            ? div({}, "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,
+          disabled: this.state.recordingState === REQUEST_TO_START_RECORDING,
+          additionalMessage: "Another tool is currently recording."
+        });
+
+      default:
+        throw new Error("Unhandled recording state");
+    }
+  }
+});
+
+function renderButton(props) {
+  const { disabled, label, onClick, additionalMessage } = props;
+  const nbsp = "\u00A0";
+
+  return div(
+    { className: "perf" },
+    div({ className: "perf-additional-message" }, additionalMessage || nbsp),
+    div(
+      {},
+      button(
+        {
+          className: "devtools-button perf-button",
+          "data-standalone": true,
+          disabled,
+          onClick
+        },
+        label
+      )
+    )
+  );
 }
+
+/**
+ * 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 };
+  render(createElement(Perf, props), document.querySelector("#root"));
+}
--- a/devtools/client/performance/new/perf.xhtml
+++ b/devtools/client/performance/new/perf.xhtml
@@ -6,26 +6,20 @@
   %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>
-    <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
-    <link rel="stylesheet" href="chrome://devtools/skin/memory.css" type="text/css"/>
-    <link rel="stylesheet" href="chrome://devtools/skin/components-frame.css" type="text/css"/>
-    <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
-  </head>
-  <body class="theme-body">
-    <div>
-      Recording panel
-      <button id='startRecording'>Start Recording</button>
-      <button id='stopRecording'>Stop Recording</button>
-    </div>
   </body>
 </html>
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/perf.js
+++ b/devtools/server/actors/perf.js
@@ -2,29 +2,35 @@
  * 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);
 });
 
 exports.PerfActor = ActorClassWithSpec(perfSpec, {
-  typeName: "cssProperties",
-
   initialize(conn) {
     Actor.prototype.initialize.call(this, conn);
+    this._observer = {
+      observe: this.observe.bind(this)
+    };
+    Services.obs.addObserver(this._observer, "profiler-started");
+    Services.obs.addObserver(this._observer, "profiler-stopped");
   },
 
   destroy() {
+    Services.obs.removeObserver(this._observer, "profiler-started");
+    Services.obs.removeObserver(this._observer, "profiler-stopped");
     Actor.prototype.destroy.call(this);
   },
 
   startProfiler() {
     const settings = {
       entries: 1000000,
       interval: 1,
       features: ["js", "stackwalk", "threads", "leaf"],
@@ -36,17 +42,33 @@ exports.PerfActor = ActorClassWithSpec(p
       settings.interval,
       settings.features,
       settings.features.length,
       settings.threads,
       settings.threads.length
     );
   },
 
-  async stopProfiler() {
+  stopProfilerAndDiscardProfile() {
+    geckoProfiler.StopProfiler();
+  },
+
+  async getProfileAndStopProfiler() {
     const profile = await geckoProfiler.getProfileDataAsync();
     geckoProfiler.StopProfiler();
     if (Object.keys(profile).length === 0) {
       throw new Error("Unable to capture a profile.");
     }
     return profile;
+  },
+
+  isActive() {
+    return geckoProfiler.IsActive();
+  },
+
+  observe(_subject, topic, _data) {
+    switch (topic) {
+      case "profiler-started":
+      case "profiler-stopped":
+        this.emit(topic);
+    }
   }
 });
--- a/devtools/shared/specs/perf.js
+++ b/devtools/shared/specs/perf.js
@@ -3,22 +3,41 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { RetVal, generateActorSpec } = require("devtools/shared/protocol");
 
 const perfSpec = generateActorSpec({
   typeName: "perf",
 
+  events: {
+    "profiler-started": {
+      type: "profiler-started"
+    },
+    "profiler-stopped": {
+      type: "profiler-stopped"
+    }
+  },
+
   methods: {
     startProfiler: {
       request: {},
       response: {}
     },
 
-    stopProfiler: {
+    getProfileAndStopProfiler: {
       request: {},
       response: RetVal("json")
-    }
+    },
+
+    stopProfilerAndDiscardProfile: {
+      request: {},
+      response: {}
+    },
+
+    isActive: {
+      request: {},
+      response: { value: RetVal("boolean") }
+    },
   }
 });
 
 exports.perfSpec = perfSpec;