Source code

Revision control

Copy as Markdown

Other Tools

/* 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(
);
ChromeUtils.defineESModuleGetters(this, {
AboutHomeStartupCache: "resource:///modules/BrowserGlue.sys.mjs",
PerTestCoverageUtils:
});
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 profilerSubtestStartTime;
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.
*
* @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
);
},
/**
* 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();
}
);
});
},
/**
* Add a parent process marker to the profiler to indicate the subtest duration.
*
* @param marker (string, optional)
* Marker name.
*/
profilerSubtestEnd(marker = null, startTime = undefined) {
if (marker) {
this.addIntervalMarker(marker, startTime ?? profilerSubtestStartTime);
}
},
/**
* * Add a parent process marker to the profiler to indicate the start of the subtest.
*
* @param marker (string, optional)
* Marker name.
*/
profilerSubtestStart(marker = null) {
profilerSubtestStartTime = 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.
*
* @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:SubtestEnd": {
this.profilerSubtestEnd(data.marker, data.startTime);
mm.sendAsyncMessage(ACK_NAME, { name });
break;
}
case "Profiler:SubtestStart": {
this.profilerSubtestStart(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(
);
// 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.firstPaint2) {
// It's possible that we were called early enough that
// the firstPaint2 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:
// 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) {
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) {
PerTestCoverageUtils.afterTest().then(callback);
},
requestResetCoverageCounters(arg, callback) {
PerTestCoverageUtils.beforeTest().then(callback);
},
dumpAboutSupport(arg, callback) {
const { Troubleshoot } = ChromeUtils.importESModule(
);
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);
}
};