Bug 1182595 - Add common, e10s-friendly SPS Profiler scripts that Talos tests can use. r?jmaher
This introduces TalosContentProfiler.js, which can be used within content, and
TalosParentProfiler.js, which can be used inside the parent process.
MozReview-Commit-ID: 4L7rRuNALOy
--- a/testing/talos/talos/talos-powers/chrome.manifest
+++ b/testing/talos/talos/talos-powers/chrome.manifest
@@ -1,4 +1,5 @@
content talos-powers chrome/
+content talos-powers-content content/ contentaccessible=yes
component {f5d53443-d58d-4a2f-8df0-98525d4f91ad} components/TalosPowersService.js
contract @mozilla.org/talos/talos-powers-service;1 {f5d53443-d58d-4a2f-8df0-98525d4f91ad}
category profile-after-change TalosPowersService @mozilla.org/talos/talos-powers-service;1
--- a/testing/talos/talos/talos-powers/chrome/talos-powers-content.js
+++ b/testing/talos/talos/talos-powers/chrome/talos-powers-content.js
@@ -1,13 +1,13 @@
/* 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/. */
-var { interfaces: Ci } = Components;
+var { interfaces: Ci, utils: Cu } = Components;
/**
* Content that wants to quit the whole session should
* fire the TalosQuitApplication custom event. This will
* attempt to force-quit the browser.
*/
addEventListener("TalosQuitApplication", event => {
// If we're loaded in a low-priority background process, like
@@ -17,8 +17,29 @@ addEventListener("TalosQuitApplication",
let priority = docShell.QueryInterface(Ci.nsIDocumentLoader)
.loadGroup
.QueryInterface(Ci.nsISupportsPriority)
.priority;
if (priority != Ci.nsISupportsPriority.PRIORITY_LOWEST) {
sendAsyncMessage("Talos:ForceQuit", event.detail);
}
});
+
+addEventListener("TalosContentProfilerCommand", (e) => {
+ let name = e.detail.name;
+ let data = e.detail.data;
+ sendAsyncMessage("TalosContentProfiler:Command", { name, data });
+});
+
+addMessageListener("TalosContentProfiler:Response", (msg) => {
+ let name = msg.data.name;
+ let data = msg.data.data;
+
+ let event = Cu.cloneInto({
+ bubbles: true,
+ detail: {
+ name: name,
+ data: data,
+ },
+ }, content);
+ content.dispatchEvent(
+ new content.CustomEvent("TalosContentProfilerResponse", event));
+});
\ No newline at end of file
--- a/testing/talos/talos/talos-powers/components/TalosPowersService.js
+++ b/testing/talos/talos/talos-powers/components/TalosPowersService.js
@@ -2,63 +2,204 @@
* 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/. */
const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
const FRAME_SCRIPT = "chrome://talos-powers/content/talos-powers-content.js";
-function TalosPowersService() {};
+function TalosPowersService() {
+ this.wrappedJSObject = this;
+};
TalosPowersService.prototype = {
classDescription: "Talos Powers",
classID: Components.ID("{f5d53443-d58d-4a2f-8df0-98525d4f91ad}"),
contractID: "@mozilla.org/talos/talos-powers-service;1",
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
observe(subject, topic, data) {
switch(topic) {
case "profile-after-change":
// Note that this observation is registered in the chrome.manifest
// for this add-on.
this.init();
break;
- case "sessionstore-windows-restored":
- this.inject();
- break;
case "xpcom-shutdown":
this.uninit();
break;
}
},
init() {
- // We want to defer any kind of work until after sessionstore has
- // finished in order not to skew sessionstore test numbers.
- Services.obs.addObserver(this, "sessionstore-windows-restored", false);
+ Services.mm.loadFrameScript(FRAME_SCRIPT, true);
+ Services.mm.addMessageListener("Talos:ForceQuit", this);
+ Services.mm.addMessageListener("TalosContentProfiler:Command", this);
Services.obs.addObserver(this, "xpcom-shutdown", false);
},
uninit() {
- Services.obs.removeObserver(this, "sessionstore-windows-restored", false);
Services.obs.removeObserver(this, "xpcom-shutdown", false);
},
- inject() {
- Services.mm.loadFrameScript(FRAME_SCRIPT, true);
- Services.mm.addMessageListener("Talos:ForceQuit", this);
+ receiveMessage(message) {
+ switch(message.name) {
+ case "Talos:ForceQuit": {
+ this.forceQuit(message.data);
+ break;
+ }
+ case "TalosContentProfiler:Command": {
+ this.receiveProfileCommand(message);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Enable the SPS profiler with some settings and then pause
+ * immediately.
+ *
+ * @param data (object)
+ * A JavaScript object with the following properties:
+ *
+ * entries (int):
+ * The sampling buffer size in bytes.
+ *
+ * interval (int):
+ * The sampling interval in milliseconds.
+ *
+ * threadsArray (array of strings):
+ * The thread names to sample.
+ */
+ profilerBegin(data) {
+ Services.profiler
+ .StartProfiler(data.entries, data.interval,
+ ["js", "leaf", "stackwalk", "threads"], 4,
+ data.threadsArray, data.threadsArray.length);
+
+ Services.profiler.PauseSampling();
+ },
+
+ /**
+ * Assuming the Profiler is running, dumps the Profile from all sampled
+ * processes and threads to the disk. The Profiler will be stopped once
+ * the profiles have been dumped. This method returns a Promise that
+ * will resolve once this has occurred.
+ *
+ * @param profileFile (string)
+ * The name of the file to write to.
+ *
+ * @returns Promise
+ */
+ profilerFinish(profileFile) {
+ return new Promise((resolve, reject) => {
+ Services.profiler.getProfileDataAsync().then((profile) => {
+ let encoder = new TextEncoder();
+ let array = encoder.encode(JSON.stringify(profile));
+
+ OS.File.writeAtomic(profileFile, array, {
+ tmpPath: profileFile + ".tmp",
+ }).then(() => {
+ Services.profiler.StopProfiler();
+ resolve();
+ });
+ }, (error) => {
+ Cu.reportError("Failed to gather profile: " + error);
+ // FIXME: We should probably send a message down to the
+ // child which causes it to reject the waiting Promise.
+ reject();
+ });
+ });
},
- receiveMessage(message) {
- if (message.name == "Talos:ForceQuit") {
- this.forceQuit(message.data);
+ /**
+ * Pauses the Profiler, optionally setting a parent process marker before
+ * doing so.
+ *
+ * @param marker (string, optional)
+ * A marker to set before pausing.
+ */
+ profilerPause(marker=null) {
+ if (marker) {
+ Services.profiler.AddMarker(marker);
+ }
+
+ Services.profiler.PauseSampling();
+ },
+
+ /**
+ * Resumes a pausedProfiler, optionally setting a parent process marker
+ * after doing so.
+ *
+ * @param marker (string, optional)
+ * A marker to set after resuming.
+ */
+ profilerResume(marker=null) {
+ Services.profiler.ResumeSampling();
+
+ if (marker) {
+ Services.profiler.AddMarker(marker);
+ }
+ },
+
+ /**
+ * Adds a marker to the Profile in the parent process.
+ */
+ profilerMarker(marker) {
+ Services.profiler.AddMarker(marker);
+ },
+
+ receiveProfileCommand(message) {
+ const ACK_NAME = "TalosContentProfiler:Response";
+ let mm = message.target.messageManager;
+ let name = message.data.name;
+ let data = message.data.data;
+
+ switch(name) {
+ case "Profiler:Begin": {
+ this.profilerBegin(data);
+ // profilerBegin will cause the parent to send an async message to any
+ // child processes to start profiling. Because messages are serviced
+ // in order, we know that by the time that the child services the
+ // ACK message, that the profiler has started in its process.
+ mm.sendAsyncMessage(ACK_NAME, { name });
+ break;
+ }
+
+ case "Profiler:Finish": {
+ // The test is done. Dump the profile.
+ let profileFile = data.profileFile;
+ this.profilerFinish(data.profileFile).then(() => {
+ mm.sendAsyncMessage(ACK_NAME, { name });
+ });
+ break;
+ }
+
+ case "Profiler:Pause": {
+ this.profilerPause(data.marker);
+ mm.sendAsyncMessage(ACK_NAME, { name });
+ break;
+ }
+
+ case "Profiler:Resume": {
+ this.profilerResume(data.marker);
+ mm.sendAsyncMessage(ACK_NAME, { name });
+ break;
+ }
+
+ case "Profiler:Marker": {
+ this.profilerMarker(data.marker);
+ mm.sendAsyncMessage(ACK_NAME, { name });
+ }
}
},
forceQuit(messageData) {
if (messageData && messageData.waitForSafeBrowsing) {
let SafeBrowsing = Cu.import("resource://gre/modules/SafeBrowsing.jsm", {}).SafeBrowsing;
let whenDone = () => {
new file mode 100644
--- /dev/null
+++ b/testing/talos/talos/talos-powers/content/TalosContentProfiler.js
@@ -0,0 +1,242 @@
+/* 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/. */
+
+/**
+ * This utility script is for instrumenting your Talos test for
+ * performance profiles while running within content. If your test
+ * is running in the parent process, you should use
+ * TalosParentProfiler.js instead to avoid the messaging overhead.
+ */
+
+var TalosContentProfiler;
+
+(function() {
+
+ // Whether or not this TalosContentProfiler object has had initFromObject
+ // or initFromURLQueryParams called on it. Any functions that will send
+ // events to the parent to change the behaviour of the SPS Profiler
+ // should only be called after calling either initFromObject or
+ // initFromURLQueryParams.
+ var initted = false;
+
+ // The subtest name that beginTest() was called with.
+ var currentTest = "unknown";
+
+ // Profiler settings.
+ var interval, entries, threadsArray, profileDir;
+
+ try {
+ // Outside of talos, this throws a security exception which no-op this file.
+ // (It's not required nor allowed for addons since Firefox 17)
+ // It's used inside talos from non-privileged pages (like during tscroll),
+ // and it works because talos disables all/most security measures.
+ netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+ } catch (e) {}
+
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ /**
+ * Emits a TalosContentProfiler prefixed event and then returns a Promise
+ * that resolves once a corresponding acknowledgement event is
+ * dispatched on our document.
+ *
+ * @param name
+ * The name of the event that will be TalosContentProfiler prefixed and
+ * eventually sent to the parent.
+ * @param data (optional)
+ * The data that will be sent to the parent.
+ * @returns Promise
+ * Resolves when a corresponding acknowledgement event is dispatched
+ * on this document.
+ */
+ function sendEventAndWait(name, data={}) {
+ return new Promise((resolve) => {
+ var event = new CustomEvent("TalosContentProfilerCommand", {
+ bubbles: true,
+ detail: {
+ name: name,
+ data: data,
+ }
+ });
+ document.dispatchEvent(event);
+
+ addEventListener("TalosContentProfilerResponse", function onResponse(event) {
+ if (event.detail.name != name) {
+ return;
+ }
+
+ removeEventListener("TalosContentProfilerResponse", onResponse);
+
+ resolve(event.detail.data);
+ });
+ });
+ }
+
+ /**
+ * Parses an url query string into a JS object.
+ *
+ * @param locationSearch (string)
+ * The location string to parse.
+ * @returns Object
+ * The GET params from the location string as
+ * key-value pairs in the Object.
+ */
+ function searchToObject(locationSearch) {
+ var pairs = locationSearch.substring(1).split("&");
+ var result = {};
+
+ for (var i in pairs) {
+ if (pairs[i] !== "") {
+ var pair = pairs[i].split("=");
+ result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
+ }
+ }
+
+ return result;
+ }
+
+ TalosContentProfiler = {
+ /**
+ * Initialize the profiler using profiler settings supplied in a JS object.
+ *
+ * @param obj (object)
+ * The following properties on the object are respected:
+ * sps_profile_interval (int)
+ * sps_profile_entries (int)
+ * sps_profile_threads (string, comma separated list of threads to filter with)
+ * sps_profile_dir (string)
+ */
+ initFromObject(obj={}) {
+ if (!initted) {
+ if (("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
+ ("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
+ ("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
+ ("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
+ interval = obj.sps_profile_interval;
+ entries = obj.sps_profile_entries;
+ threadsArray = obj.sps_profile_threads.split(",");
+ profileDir = obj.sps_profile_dir;
+ initted = true;
+ } else {
+ console.error("Profiler could not init with object: " + JSON.stringify(obj));
+ }
+ }
+ },
+
+ /**
+ * Initialize the profiler using a string from a location string.
+ *
+ * @param locationSearch (string)
+ * The location string to initialize with.
+ */
+ initFromURLQueryParams(locationSearch) {
+ this.initFromObject(searchToObject(locationSearch));
+ },
+
+ /**
+ * A Talos test is about to start. This will return a Promise that
+ * resolves once the Profiler has been initialized. Note that the
+ * SPS profiler will be paused immediately after starting and that
+ * resume() should be called in order to collect samples.
+ *
+ * @param testName (string)
+ * The name of the test to use in Profiler markers.
+ * @returns Promise
+ * Resolves once the SPS profiler has been initialized and paused.
+ */
+ beginTest(testName) {
+ if (initted) {
+ currentTest = testName;
+ return sendEventAndWait("Profiler:Begin", {
+ interval,
+ entries,
+ threadsArray,
+ });
+ } else {
+ var msg = "You should not call beginTest without having first " +
+ "initted the Profiler"
+ console.error(msg);
+ return Promise.reject(msg);
+ }
+ },
+
+ /**
+ * A Talos test has finished. This will stop the SPS profiler from sampling,
+ * and return a Promise that resolves once the Profiler has finished dumping
+ * the multi-process profile to disk.
+ *
+ * @returns Promise
+ * Resolves once the profile has been dumped to disk. The test should
+ * not try to quit the browser until this has resolved.
+ */
+ finishTest() {
+ if (initted) {
+ let profileFile = profileDir + "/" + currentTest + ".sps";
+ return sendEventAndWait("Profiler:Finish", { profileFile });
+ } else {
+ var msg = "You should not call finishTest without having first " +
+ "initted the Profiler";
+ console.error(msg);
+ return Promise.reject(msg);
+ }
+ },
+
+ /**
+ * A start-up test has finished. Callers don't need to run beginTest or
+ * finishTest, but should pause the sampler as soon as possible, and call
+ * this function to dump the profile.
+ *
+ * @returns Promise
+ * Resolves once the profile has been dumped to disk. The test should
+ * not try to quit the browser until this has resolved.
+ */
+ finishStartupProfiling() {
+ let profileFile = profileDir + "/startup.sps";
+ return sendEventAndWait("Profiler:Finish", { profileFile });
+ },
+
+ /**
+ * Resumes the SPS profiler sampler. Can also simultaneously set a marker.
+ *
+ * @returns Promise
+ * Resolves once the SPS profiler has resumed.
+ */
+ resume(marker="") {
+ return sendEventAndWait("Profiler:Resume", { marker });
+ },
+
+ /**
+ * Pauses the SPS profiler sampler. Can also simultaneously set a marker.
+ *
+ * @returns Promise
+ * Resolves once the SPS profiler has paused.
+ */
+ pause(marker="") {
+ return sendEventAndWait("Profiler:Pause", { marker });
+ },
+
+ /**
+ * Adds a marker to the profile.
+ *
+ * @returns Promise
+ * Resolves once the marker has been set.
+ */
+ mark(marker) {
+ // If marker is omitted, just use the test name
+ if (!marker) {
+ marker = currentTest;
+ }
+
+ return sendEventAndWait("Profiler:Marker", { marker });
+ },
+
+ /**
+ * Add a marker to the profile on the content process samples.
+ * This occurs synchronously.
+ */
+ contentMarker(marker) {
+ Services.profiler.AddMarker(marker);
+ },
+ };
+})();
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/talos/talos/talos-powers/content/TalosParentProfiler.js
@@ -0,0 +1,193 @@
+/* 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/. */
+
+/**
+ * This utility script is for instrumenting your Talos test for
+ * performance profiles while running within the parent process.
+ * Almost all of the functions that this script exposes to the
+ * SPS Profiler are synchronous, except for finishTest, since that
+ * involves requesting the profiles from any content processes and
+ * then writing to disk.
+ *
+ * If your test is running in the content process, you should use
+ * TalosContentProfiler.js instead.
+ */
+
+var TalosParentProfiler;
+
+(function() {
+
+ // Whether or not this TalosContentProfiler object has had initFromObject
+ // or initFromURLQueryParams called on it. Any functions that change the
+ // state of the SPS Profiler should only be called after calling either
+ // initFromObject or initFromURLQueryParams.
+ let initted = false;
+
+ // The subtest name that beginTest() was called with.
+ let currentTest = "unknown";
+
+ // Profiler settings.
+ let interval, entries, threadsArray, profileDir;
+
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://gre/modules/Console.jsm");
+
+ // Use a bit of XPCOM hackery to get at the Talos Powers service
+ // implementation...
+ let TalosPowers =
+ Components.classes["@mozilla.org/talos/talos-powers-service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+
+ /**
+ * Parses an url query string into a JS object.
+ *
+ * @param locationSearch (string)
+ * The location string to parse.
+ * @returns Object
+ * The GET params from the location string as
+ * key-value pairs in the Object.
+ */
+ function searchToObject(locationSearch) {
+ let pairs = locationSearch.substring(1).split("&");
+ let result = {};
+
+ for (let i in pairs) {
+ if (pairs[i] !== "") {
+ let pair = pairs[i].split("=");
+ result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
+ }
+ }
+
+ return result;
+ }
+
+ TalosParentProfiler = {
+ /**
+ * Initialize the profiler using profiler settings supplied in a JS object.
+ *
+ * @param obj (object)
+ * The following properties on the object are respected:
+ * sps_profile_interval (int)
+ * sps_profile_entries (int)
+ * sps_profile_threads (string, comma separated list of threads to filter with)
+ * sps_profile_dir (string)
+ */
+ initFromObject(obj={}) {
+ if (!initted) {
+ if (("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
+ ("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
+ ("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
+ ("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
+ interval = obj.sps_profile_interval;
+ entries = obj.sps_profile_entries;
+ threadsArray = obj.sps_profile_threads.split(",");
+ profileDir = obj.sps_profile_dir;
+ initted = true;
+ } else {
+ console.error("Profiler could not init with object: " + JSON.stringify(obj));
+ }
+ }
+ },
+
+ /**
+ * Initialize the profiler using a string from a location string.
+ *
+ * @param locationSearch (string)
+ * The location string to initialize with.
+ */
+ initFromURLQueryParams(locationSearch) {
+ this.initFromObject(searchToObject(locationSearch));
+ },
+
+ /**
+ * A Talos test is about to start. Note that the SPS profiler will be
+ * paused immediately after starting and that resume() should be called
+ * in order to collect samples.
+ *
+ * @param testName (string)
+ * The name of the test to use in Profiler markers.
+ */
+ beginTest(testName) {
+ if (initted) {
+ currentTest = testName;
+ TalosPowers.profilerBegin({ entries, interval, threadsArray });
+ } else {
+ let msg = "You should not call beginTest without having first " +
+ "initted the Profiler"
+ console.error(msg);
+ }
+ },
+
+ /**
+ * A Talos test has finished. This will stop the SPS profiler from sampling,
+ * and return a Promise that resolves once the Profiler has finished dumping
+ * the multi-process profile to disk.
+ *
+ * @returns Promise
+ * Resolves once the profile has been dumped to disk. The test should
+ * not try to quit the browser until this has resolved.
+ */
+ finishTest() {
+ if (initted) {
+ let profileFile = profileDir + "/" + currentTest + ".sps";
+ return TalosPowers.profilerFinish(profileFile);
+ } else {
+ let msg = "You should not call finishTest without having first " +
+ "initted the Profiler";
+ console.error(msg);
+ return Promise.reject(msg);
+ }
+ },
+
+ /**
+ * A start-up test has finished. Callers don't need to run beginTest or
+ * finishTest, but should pause the sampler as soon as possible, and call
+ * this function to dump the profile.
+ *
+ * @returns Promise
+ * Resolves once the profile has been dumped to disk. The test should
+ * not try to quit the browser until this has resolved.
+ */
+ finishStartupProfiling() {
+ let profileFile = profileDir + "/startup.sps";
+ return TalosPowers.profilerFinish(profileFile);
+ },
+
+ /**
+ * Resumes the SPS profiler sampler. Can also simultaneously set a marker.
+ *
+ * @returns Promise
+ * Resolves once the SPS profiler has resumed.
+ */
+ resume(marker="") {
+ TalosPowers.profilerResume(marker);
+ },
+
+ /**
+ * Pauses the SPS profiler sampler. Can also simultaneously set a marker.
+ *
+ * @returns Promise
+ * Resolves once the SPS profiler has paused.
+ */
+ pause(marker="") {
+ TalosPowers.profilerPause(marker);
+ },
+
+ /**
+ * Adds a marker to the profile.
+ *
+ * @returns Promise
+ * Resolves once the marker has been set.
+ */
+ mark(marker) {
+ // If marker is omitted, just use the test name
+ if (!marker) {
+ marker = currentTest;
+ }
+
+ TalosPowers.profilerMarker(marker);
+ },
+ };
+})();
\ No newline at end of file