Bug 1408124 - Fix and extend tests for the PerfActor
MozReview-Commit-ID: C0LWdVqvS3I
--- 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;