Bug 1408124 - Fix and extend tests for the PerfActor draft
authorGreg Tatum <gtatum@mozilla.com>
Mon, 06 Nov 2017 17:11:17 -0600
changeset 695085 57a2cecb27f3fc0274776dd5b2e546cc9a8df71d
parent 695084 d54258c6b2a666848b414b1ddf5253ebb949830f
child 695086 043a247f303b4b557ee7aba22f5d0967ebea4f2d
push id88335
push usergtatum@mozilla.com
push dateWed, 08 Nov 2017 18:51:46 +0000
bugs1408124
milestone58.0a1
Bug 1408124 - Fix and extend tests for the PerfActor MozReview-Commit-ID: C0LWdVqvS3I
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/specs/perf.js
--- a/devtools/server/actors/perf.js
+++ b/devtools/server/actors/perf.js
@@ -8,67 +8,146 @@ const { ActorClassWithSpec, Actor } = pr
 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);
-    this._observer = {
-      observe: this.observe.bind(this)
-    };
-    Services.obs.addObserver(this._observer, "profiler-started");
-    Services.obs.addObserver(this._observer, "profiler-stopped");
+
+    // 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"]
     };
 
-    geckoProfiler.StartProfiler(
-      settings.entries,
-      settings.interval,
-      settings.features,
-      settings.features.length,
-      settings.threads,
-      settings.threads.length
-    );
+    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() {
-    const profile = await geckoProfiler.getProfileDataAsync();
-    geckoProfiler.StopProfiler();
+    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) {
-      throw new Error("Unable to capture a profile.");
+      return null;
     }
     return profile;
   },
 
   isActive() {
+    if (!IS_SUPPORTED_PLATFORM) {
+      return false;
+    }
     return geckoProfiler.IsActive();
   },
 
-  observe(_subject, topic, _data) {
+  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-for-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
@@ -451,24 +451,24 @@ var DebuggerServer = {
       constructor: "DeviceActor",
       type: { global: true }
     });
     this.registerModule("devtools/server/actors/heap-snapshot-file", {
       prefix: "heapSnapshotFile",
       constructor: "HeapSnapshotFileActor",
       type: { global: true }
     });
-    if ("nsIProfiler" in Ci &&
-        Services.prefs.getBoolPref("devtools.performance.new-panel-enabled")) {
-      this.registerModule("devtools/server/actors/perf", {
-        prefix: "perf",
-        constructor: "PerfActor",
-        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.
+    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",
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -67,16 +67,18 @@ 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]
 [browser_perf-01.js]
+[browser_perf-02.js]
+[browser_perf-03.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [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
--- a/devtools/server/tests/browser/browser_perf-01.js
+++ b/devtools/server/tests/browser/browser_perf-01.js
@@ -1,29 +1,47 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* 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";
-/* eslint-disable mozilla/no-arbitrary-setTimeout */
 
-const { PerfFront } = require("devtools/shared/fronts/perf");
-
-function wait(n) {
-  return new Promise(resolve => setTimeout(resolve, n));
-}
+/**
+ * Run through a series of basic recording actions for the perf actor.
+ */
+add_task(async function () {
+  const {front, client} = await initPerfFront();
 
-add_task(function* () {
-  yield addTab(MAIN_DOMAIN + "doc_perf.html");
+  // 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.");
 
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  let front = PerfFront(client, form);
+  // Start the profiler.
+  const profilerStarted = once(front, "profiler-started");
+  await front.startProfiler();
+  await profilerStarted;
+  is(await front.isActive(), true, "The profiler was started.");
 
-  yield front.startProfiler();
-  yield wait(100);
-  const profile = yield front.stopProfiler();
-
+  // 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.");
 
-  yield front.destroy();
-  yield client.close();
-  gBrowser.removeCurrentTab();
+  // 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-for-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
@@ -75,28 +75,16 @@ function* initLayoutFrontForUrl(url) {
   let form = yield connectDebuggerClient(client);
   let inspector = InspectorFront(client, form);
   let walker = yield inspector.getWalker();
   let layout = yield walker.getLayoutInspector();
 
   return {inspector, walker, layout, client};
 }
 
-function* initPerfFrontForUrl(url) {
-  const {PerfFront} = require("devtools/shared/fronts/perf");
-
-  yield addTab(url);
-
-  initDebuggerServer();
-  let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let form = yield connectDebuggerClient(client);
-  const perfFront = PerfFront(client, form);
-  return { client, perfFront };
-}
-
 function* initAccessibilityFrontForUrl(url) {
   const {AccessibilityFront} = require("devtools/shared/fronts/accessibility");
   const {InspectorFront} = require("devtools/shared/fronts/inspector");
 
   yield addTab(url);
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
@@ -115,16 +103,51 @@ function initDebuggerServer() {
     DebuggerServer.destroy();
   } catch (e) {
     info(`DebuggerServer destroy error: ${e}\n${e.stack}`);
   }
   DebuggerServer.init();
   DebuggerServer.addBrowserActors();
 }
 
+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/specs/perf.js
+++ b/devtools/shared/specs/perf.js
@@ -9,35 +9,54 @@ const perfSpec = generateActorSpec({
   typeName: "perf",
 
   events: {
     "profiler-started": {
       type: "profiler-started"
     },
     "profiler-stopped": {
       type: "profiler-stopped"
+    },
+    "profile-locked-for-private-browsing": {
+      type: "profile-locked-for-private-browsing"
+    },
+    "profile-unlocked-from-private-browsing": {
+      type: "profile-unlocked-from-private-browsing"
     }
   },
 
   methods: {
     startProfiler: {
       request: {},
-      response: {}
+      response: { value: RetVal("boolean") }
     },
 
+    /**
+     * Returns null when unable to return the profile.
+     */
     getProfileAndStopProfiler: {
       request: {},
-      response: RetVal("json")
+      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.perfSpec = perfSpec;