summaryrefslogtreecommitdiffstats
path: root/testing/talos/talos/talos-powers/api.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/talos/talos/talos-powers/api.js465
1 files changed, 465 insertions, 0 deletions
diff --git a/testing/talos/talos/talos-powers/api.js b/testing/talos/talos/talos-powers/api.js
new file mode 100644
index 0000000000..b25cec5018
--- /dev/null
+++ b/testing/talos/talos/talos-powers/api.js
@@ -0,0 +1,465 @@
+/* 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/. */
+
+/* globals ExtensionAPI, Services, XPCOMUtils */
+
+const { ComponentUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ComponentUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AboutHomeStartupCache: "resource:///modules/BrowserGlue.sys.mjs",
+ PerTestCoverageUtils:
+ "resource://testing-common/PerTestCoverageUtils.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "resProto",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsISubstitutingProtocolHandler"
+);
+
+// These are not automagically defined for us because we are an extension.
+//
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["IOUtils", "PathUtils"]);
+
+const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+let frameScriptURL;
+let profilerStartTime;
+
+function TalosPowersService() {
+ this.wrappedJSObject = this;
+
+ this.init();
+}
+
+TalosPowersService.prototype = {
+ factory: ComponentUtils.generateSingletonFactory(TalosPowersService),
+ classDescription: "Talos Powers",
+ classID: Components.ID("{f5d53443-d58d-4a2f-8df0-98525d4f91ad}"),
+ contractID: "@mozilla.org/talos/talos-powers-service;1",
+ QueryInterface: ChromeUtils.generateQI([]),
+
+ register() {
+ Cm.registerFactory(
+ this.classID,
+ this.classDescription,
+ this.contractID,
+ this.factory
+ );
+
+ void Cc[this.contractID].getService();
+ },
+
+ unregister() {
+ Cm.unregisterFactory(this.classID, this.factory);
+ },
+
+ init() {
+ if (!frameScriptURL) {
+ throw new Error("Cannot find frame script url (extension not started?)");
+ }
+ Services.mm.loadFrameScript(frameScriptURL, true);
+ Services.mm.addMessageListener("Talos:ForceQuit", this);
+ Services.mm.addMessageListener("TalosContentProfiler:Command", this);
+ Services.mm.addMessageListener("TalosPowersContent:ForceCCAndGC", this);
+ Services.mm.addMessageListener("TalosPowersContent:GetStartupInfo", this);
+ Services.mm.addMessageListener("TalosPowers:ParentExec:QueryMsg", this);
+ },
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "Talos:ForceQuit": {
+ this.forceQuit(message.data);
+ break;
+ }
+ case "TalosContentProfiler:Command": {
+ this.receiveProfileCommand(message);
+ break;
+ }
+ case "TalosPowersContent:ForceCCAndGC": {
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceShrinkingGC();
+ break;
+ }
+ case "TalosPowersContent:GetStartupInfo": {
+ this.receiveGetStartupInfo(message);
+ break;
+ }
+ case "TalosPowers:ParentExec:QueryMsg": {
+ this.RecieveParentExecCommand(message);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Enable the Gecko 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,
+ data.featuresArray,
+ data.threadsArray
+ );
+
+ 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 profileDir (string)
+ * The name of the directory to write the profile in.
+ * @param profileFile (string)
+ * The name of the file to write to.
+ *
+ * @returns Promise
+ */
+ profilerFinish(profileDir, profileFile) {
+ const profilePath = PathUtils.join(profileDir, profileFile);
+ return new Promise((resolve, reject) => {
+ Services.profiler.Pause();
+ Services.profiler.getProfileDataAsync().then(
+ profile =>
+ IOUtils.writeJSON(profilePath, profile, {
+ tmpPath: `${profilePath}.tmp`,
+ }).then(() => {
+ Services.profiler.StopProfiler();
+ resolve();
+ Services.obs.notifyObservers(null, "talos-profile-gathered");
+ }),
+ error => {
+ console.error("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();
+ }
+ );
+ });
+ },
+
+ /**
+ * 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) {
+ this.addIntervalMarker(marker, profilerStartTime);
+ }
+ 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();
+
+ profilerStartTime = Cu.now();
+
+ if (marker) {
+ this.addInstantMarker(marker);
+ }
+ },
+
+ /**
+ * Adds an instant marker to the Profile in the parent process.
+ *
+ * @param marker (string) A marker to set.
+ *
+ */
+ addInstantMarker(marker) {
+ ChromeUtils.addProfilerMarker("Talos", { category: "Test" }, marker);
+ },
+
+ /**
+ * Adds a marker to the Profile in the parent process.
+ *
+ * @param marker (string)
+ * A marker to set before pausing.
+ *
+ * @param startTime (number)
+ * Start time, used to create an interval profile marker. If
+ * undefined, a single instance marker will be placed.
+ */
+ addIntervalMarker(marker, startTime) {
+ ChromeUtils.addProfilerMarker(
+ "Talos",
+ { startTime, category: "Test" },
+ 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.
+ this.profilerFinish(data.profileDir, data.profileFile).then(() => {
+ mm.sendAsyncMessage(ACK_NAME, { name });
+ });
+ break;
+ }
+
+ case "Profiler:Pause": {
+ this.profilerPause(data.marker, data.startTime);
+ 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, data.startTime);
+ mm.sendAsyncMessage(ACK_NAME, { name });
+ break;
+ }
+ }
+ },
+
+ async forceQuit(messageData) {
+ if (messageData && messageData.waitForStartupFinished) {
+ // We can wait for various startup items here to complete during
+ // the getInfo.html step for Talos so that subsequent runs don't
+ // have to do things like re-request the SafeBrowsing list.
+ let { SafeBrowsing } = ChromeUtils.importESModule(
+ "resource://gre/modules/SafeBrowsing.sys.mjs"
+ );
+
+ // Speed things up in case nobody else called this:
+ SafeBrowsing.init();
+
+ try {
+ await SafeBrowsing.addMozEntriesFinishedPromise;
+ } catch (e) {
+ // We don't care if things go wrong here - let's just shut down.
+ }
+
+ // We wait for the AboutNewTab's TopSitesFeed (and its "Contile"
+ // integration, which shows the sponsored Top Sites) to finish
+ // being enabled here. This is because it's possible for getInfo.html
+ // to run so quickly that the feed will still be initializing, and
+ // that would cause us to write a mostly empty cache to the
+ // about:home startup cache on shutdown, which causes that test
+ // to break periodically.
+ AboutNewTab.onBrowserReady();
+ // There aren't currently any easily observable notifications or
+ // events to let us know when the feed is ready, so we'll just poll
+ // for now.
+ let pollForFeed = async function () {
+ let foundFeed = AboutNewTab.activityStream.store.feeds.get(
+ "feeds.system.topsites"
+ );
+ if (!foundFeed) {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ return pollForFeed();
+ }
+ return foundFeed;
+ };
+ let feed = await pollForFeed();
+ await feed._contile.refresh();
+ await feed.refresh({ broadcast: true });
+ await AboutHomeStartupCache.cacheNow();
+ }
+
+ await SessionStore.promiseAllWindowsRestored;
+
+ // Check to see if the top-most browser window still needs to fire its
+ // idle tasks notification. If so, we'll wait for it before shutting
+ // down, since some caching that can influence future runs in this profile
+ // keys off of that notification.
+ let topWin = BrowserWindowTracker.getTopWindow();
+ if (topWin && topWin.gBrowserInit) {
+ await topWin.gBrowserInit.idleTasksFinishedPromise;
+ }
+
+ for (let domWindow of Services.wm.getEnumerator(null)) {
+ domWindow.close();
+ }
+
+ try {
+ Services.startup.quit(Services.startup.eForceQuit);
+ } catch (e) {
+ dump("Force Quit failed: " + e);
+ }
+ },
+
+ receiveGetStartupInfo(message) {
+ let mm = message.target.messageManager;
+ let startupInfo = Services.startup.getStartupInfo();
+
+ if (!startupInfo.firstPaint) {
+ // It's possible that we were called early enough that
+ // the firstPaint measurement hasn't been set yet. In
+ // that case, we set up an observer for the next time
+ // a window is painted and re-retrieve the startup info.
+ let obs = function (subject, topic) {
+ Services.obs.removeObserver(this, topic);
+ startupInfo = Services.startup.getStartupInfo();
+ mm.sendAsyncMessage(
+ "TalosPowersContent:GetStartupInfo:Result",
+ startupInfo
+ );
+ };
+ Services.obs.addObserver(obs, "widget-first-paint");
+ } else {
+ mm.sendAsyncMessage(
+ "TalosPowersContent:GetStartupInfo:Result",
+ startupInfo
+ );
+ }
+ },
+
+ // These services are exposed to local unprivileged content.
+ // Each service is a function which accepts an argument, a callback for sending
+ // the reply (possibly async), and the parent window as a utility.
+ // arg/reply semantice are service-specific.
+ // To add a service: add a method at ParentExecServices here, then at the content:
+ // <script src="chrome://talos-powers-content/content/TalosPowersContent.js"></script>
+ // and then e.g. TalosPowersParent.exec("sampleParentService", myArg, myCallback)
+ // Sample service:
+ /*
+ // arg: anything. return: sample reply
+ sampleParentService: function(arg, callback, win) {
+ win.setTimeout(function() {
+ callback("sample reply for: " + arg);
+ }, 500);
+ },
+ */
+ ParentExecServices: {
+ ping(arg, callback, win) {
+ callback();
+ },
+
+ // arg: ignored. return: handle (number) for use with stopFrameTimeRecording
+ startFrameTimeRecording(arg, callback, win) {
+ var rv = win.windowUtils.startFrameTimeRecording();
+ callback(rv);
+ },
+
+ // arg: handle from startFrameTimeRecording. return: array with composition intervals
+ stopFrameTimeRecording(arg, callback, win) {
+ var rv = win.windowUtils.stopFrameTimeRecording(arg);
+ callback(rv);
+ },
+
+ requestDumpCoverageCounters(arg, callback, win) {
+ PerTestCoverageUtils.afterTest().then(callback);
+ },
+
+ requestResetCoverageCounters(arg, callback, win) {
+ PerTestCoverageUtils.beforeTest().then(callback);
+ },
+
+ dumpAboutSupport(arg, callback, win) {
+ const { Troubleshoot } = ChromeUtils.importESModule(
+ "resource://gre/modules/Troubleshoot.sys.mjs"
+ );
+ Troubleshoot.snapshot().then(snapshot => {
+ dump("about:support\t" + JSON.stringify(snapshot) + "\n");
+ callback();
+ });
+ },
+ },
+
+ RecieveParentExecCommand(msg) {
+ function sendResult(result) {
+ let mm = msg.target.messageManager;
+ mm.sendAsyncMessage("TalosPowers:ParentExec:ReplyMsg", {
+ id: msg.data.id,
+ result,
+ });
+ }
+
+ let command = msg.data.command;
+ if (!this.ParentExecServices.hasOwnProperty(command.name)) {
+ throw new Error(
+ "TalosPowers:ParentExec: Invalid service '" + command.name + "'"
+ );
+ }
+
+ this.ParentExecServices[command.name](
+ command.data,
+ sendResult,
+ msg.target.ownerGlobal
+ );
+ },
+};
+
+this.talos_powers = class extends ExtensionAPI {
+ onStartup() {
+ let uri = Services.io.newURI("content/", null, this.extension.rootURI);
+ resProto.setSubstitutionWithFlags(
+ "talos-powers",
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+
+ frameScriptURL = this.extension.rootURI.resolve(
+ "chrome/talos-powers-content.js"
+ );
+
+ TalosPowersService.prototype.register();
+ }
+
+ onShutdown() {
+ TalosPowersService.prototype.unregister();
+
+ frameScriptURL = null;
+ resProto.setSubstitution("talos-powers", null);
+ }
+};