summaryrefslogtreecommitdiffstats
path: root/dom/workers/test/browser_serviceworker_fetch_new_process.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/workers/test/browser_serviceworker_fetch_new_process.js')
-rw-r--r--dom/workers/test/browser_serviceworker_fetch_new_process.js405
1 files changed, 405 insertions, 0 deletions
diff --git a/dom/workers/test/browser_serviceworker_fetch_new_process.js b/dom/workers/test/browser_serviceworker_fetch_new_process.js
new file mode 100644
index 0000000000..801c505387
--- /dev/null
+++ b/dom/workers/test/browser_serviceworker_fetch_new_process.js
@@ -0,0 +1,405 @@
+const DIRPATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ ""
+);
+
+/**
+ * We choose blob contents that will roundtrip cleanly through the `textContent`
+ * of our returned HTML page.
+ */
+const TEST_BLOB_CONTENTS = `I'm a disk-backed test blob! Hooray!`;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set preferences so that opening a page with the origin "example.org"
+ // will result in a remoteType of "privilegedmozilla" for both the
+ // page and the ServiceWorker.
+ ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true],
+ ["browser.tabs.remote.separatedMozillaDomains", "example.org"],
+ ["dom.ipc.processCount.privilegedmozilla", 1],
+ ["dom.ipc.processPrelaunch.enabled", false],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // ServiceWorker worker instances should stay alive until explicitly
+ // caused to terminate by dropping these timeouts to 0 in
+ // `waitForWorkerAndProcessShutdown`.
+ ["dom.serviceWorkers.idle_timeout", 299999],
+ ["dom.serviceWorkers.idle_extended_timeout", 299999],
+ ],
+ });
+});
+
+function countRemoteType(remoteType) {
+ return ChromeUtils.getAllDOMProcesses().filter(
+ p => p.remoteType == remoteType
+ ).length;
+}
+
+/**
+ * Helper function to get a list of all current processes and their remote
+ * types. Note that when in used in a templated literal that it is
+ * synchronously invoked when the string is evaluated and captures system state
+ * at that instant.
+ */
+function debugRemotes() {
+ return ChromeUtils.getAllDOMProcesses()
+ .map(p => p.remoteType || "parent")
+ .join(",");
+}
+
+/**
+ * Wait for there to be zero processes of the given remoteType. This check is
+ * considered successful if there are already no processes of the given type
+ * at this very moment.
+ */
+async function waitForNoProcessesOfType(remoteType) {
+ info(`waiting for there to be no ${remoteType} procs`);
+ await TestUtils.waitForCondition(
+ () => countRemoteType(remoteType) == 0,
+ "wait for the worker's process to shutdown"
+ );
+}
+
+/**
+ * Given a ServiceWorkerRegistrationInfo with an active ServiceWorker that
+ * has no active ExtendableEvents but would otherwise continue running thanks
+ * to the idle keepalive:
+ * - Assert that there is a ServiceWorker instance in the given registration's
+ * active slot. (General invariant check.)
+ * - Assert that a single process with the given remoteType currently exists.
+ * (This doesn't mean the SW is alive in that process, though this test
+ * verifies that via other checks when appropriate.)
+ * - Induce the worker to shutdown by temporarily dropping the idle timeout to 0
+ * and causing the idle timer to be reset due to rapid debugger attach/detach.
+ * - Wait for the the single process with the given remoteType to go away.
+ * - Reset the idle timeouts back to their previous high values.
+ */
+async function waitForWorkerAndProcessShutdown(swRegInfo, remoteType) {
+ info(`terminating worker and waiting for ${remoteType} procs to shut down`);
+ ok(swRegInfo.activeWorker, "worker should be in the active slot");
+ is(
+ countRemoteType(remoteType),
+ 1,
+ `should have a single ${remoteType} process but have: ${debugRemotes()}`
+ );
+
+ // Let's not wait too long for the process to shutdown.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 0],
+ ],
+ });
+
+ // We need to cause the worker to re-evaluate its idle timeout. The easiest
+ // way to do this I could think of is to attach and then detach the debugger
+ // from the active worker.
+ swRegInfo.activeWorker.attachDebugger();
+ await new Promise(resolve => Cu.dispatch(resolve));
+ swRegInfo.activeWorker.detachDebugger();
+
+ // Eventually the length will reach 0, meaning we're done!
+ await waitForNoProcessesOfType(remoteType);
+
+ is(
+ countRemoteType(remoteType),
+ 0,
+ `processes with remoteType=${remoteType} type should have shut down`
+ );
+
+ // Make sure we never kill workers on idle except when this is called.
+ await SpecialPowers.popPrefEnv();
+}
+
+async function do_test_sw(host, remoteType, swMode, fileBlob) {
+ info(
+ `### entering test: host=${host}, remoteType=${remoteType}, mode=${swMode}`
+ );
+
+ const prin = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`https://${host}`),
+ {}
+ );
+ const sw = `https://${host}/${DIRPATH}file_service_worker_fetch_synthetic.js`;
+ const scope = `https://${host}/${DIRPATH}server_fetch_synthetic.sjs`;
+
+ const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+ );
+ const swRegInfo = await swm.registerForTest(prin, scope, sw);
+ swRegInfo.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo);
+
+ info(
+ `service worker registered: ${JSON.stringify({
+ principal: swRegInfo.principal.spec,
+ scope: swRegInfo.scope,
+ })}`
+ );
+
+ // Wait for the worker to install & shut down.
+ await TestUtils.waitForCondition(
+ () => swRegInfo.activeWorker,
+ "wait for the worker to become active"
+ );
+ await waitForWorkerAndProcessShutdown(swRegInfo, remoteType);
+
+ info(
+ `test navigation interception with mode=${swMode} starting from about:blank`
+ );
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async browser => {
+ // NOTE: We intentionally trigger the navigation from content in order to
+ // make sure frontend doesn't eagerly process-switch for us.
+ SpecialPowers.spawn(
+ browser,
+ [scope, swMode, fileBlob],
+ // eslint-disable-next-line no-shadow
+ async (scope, swMode, fileBlob) => {
+ const pageUrl = `${scope}?mode=${swMode}`;
+ if (!fileBlob) {
+ content.location.href = pageUrl;
+ } else {
+ const doc = content.document;
+ const formElem = doc.createElement("form");
+ doc.body.appendChild(formElem);
+
+ formElem.action = pageUrl;
+ formElem.method = "POST";
+ formElem.enctype = "multipart/form-data";
+
+ const fileElem = doc.createElement("input");
+ formElem.appendChild(fileElem);
+
+ fileElem.type = "file";
+ fileElem.name = "foo";
+
+ fileElem.mozSetFileArray([fileBlob]);
+
+ formElem.submit();
+ }
+ }
+ );
+
+ await BrowserTestUtils.browserLoaded(browser);
+
+ is(
+ countRemoteType(remoteType),
+ 1,
+ `should have spawned a content process with remoteType=${remoteType}`
+ );
+
+ const { source, blobContents } = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return {
+ source: content.document.getElementById("source").textContent,
+ blobContents: content.document.getElementById("blob").textContent,
+ };
+ }
+ );
+
+ is(
+ source,
+ swMode === "synthetic" ? "ServiceWorker" : "ServerJS",
+ "The page contents should come from the right place."
+ );
+
+ is(
+ blobContents,
+ fileBlob ? TEST_BLOB_CONTENTS : "",
+ "The request blob contents should be the blob/empty as appropriate."
+ );
+
+ // Ensure the worker was loaded in this process.
+ const workerDebuggerURLs = await SpecialPowers.spawn(
+ browser,
+ [sw],
+ async url => {
+ if (!content.navigator.serviceWorker.controller) {
+ throw new Error("document not controlled!");
+ }
+ const wdm = Cc[
+ "@mozilla.org/dom/workers/workerdebuggermanager;1"
+ ].getService(Ci.nsIWorkerDebuggerManager);
+
+ return Array.from(wdm.getWorkerDebuggerEnumerator())
+ .map(wd => {
+ return wd.url;
+ })
+ .filter(swURL => swURL == url);
+ }
+ );
+ if (remoteType.startsWith("webServiceWorker=")) {
+ Assert.notDeepEqual(
+ workerDebuggerURLs,
+ [sw],
+ "Isolated workers should not be running in the content child process"
+ );
+ } else {
+ Assert.deepEqual(
+ workerDebuggerURLs,
+ [sw],
+ "The worker should be running in the correct child process"
+ );
+ }
+
+ // Unregister the ServiceWorker. The registration will continue to control
+ // `browser` and therefore continue to exist and its worker to continue
+ // running until the tab is closed.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let registration = await content.navigator.serviceWorker.ready;
+ await registration.unregister();
+ });
+ }
+ );
+
+ // Now that the controlled tab is closed and the registration has been
+ // removed, the ServiceWorker will be made redundant which will forcibly
+ // terminate it, which will result in the shutdown of the given content
+ // process. Wait for that to happen both as a verification and so the next
+ // test has a sufficiently clean slate.
+ await waitForNoProcessesOfType(remoteType);
+}
+
+/**
+ * Create a File-backed blob. This will happen synchronously from the main
+ * thread, which isn't optimal, but the test blocks on this progress anyways.
+ * Bug 1669578 has been filed on improving this idiom and avoiding the sync
+ * writes.
+ */
+async function makeFileBlob(blobContents) {
+ const tmpFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIDirectoryService)
+ .QueryInterface(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ tmpFile.append("test-file-backed-blob.txt");
+ tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ var outStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ outStream.init(
+ tmpFile,
+ 0x02 | 0x08 | 0x20, // write, create, truncate
+ 0o666,
+ 0
+ );
+ outStream.write(blobContents, blobContents.length);
+ outStream.close();
+
+ const fileBlob = await File.createFromNsIFile(tmpFile);
+ return fileBlob;
+}
+
+function getSWTelemetrySums() {
+ let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
+ Ci.nsITelemetry
+ );
+ let keyedhistograms = telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false
+ ).parent;
+ let keyedscalars = telemetry.getSnapshotForKeyedScalars("main", false).parent;
+ // We're not looking at the distribution of the histograms, just that they changed
+ return {
+ SERVICE_WORKER_RUNNING_All: keyedhistograms.SERVICE_WORKER_RUNNING
+ ? keyedhistograms.SERVICE_WORKER_RUNNING.All.sum
+ : 0,
+ SERVICE_WORKER_RUNNING_Fetch: keyedhistograms.SERVICE_WORKER_RUNNING
+ ? keyedhistograms.SERVICE_WORKER_RUNNING.Fetch.sum
+ : 0,
+ SERVICEWORKER_RUNNING_MAX_All: keyedscalars["serviceworker.running_max"]
+ ? keyedscalars["serviceworker.running_max"].All
+ : 0,
+ SERVICEWORKER_RUNNING_MAX_Fetch: keyedscalars["serviceworker.running_max"]
+ ? keyedscalars["serviceworker.running_max"].Fetch
+ : 0,
+ };
+}
+
+add_task(async function test() {
+ // Can't test telemetry without this since we may not be on the nightly channel
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ registerCleanupFunction(() => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+
+ let initialSums = getSWTelemetrySums();
+
+ // ## Isolated Privileged Process
+ // Trigger a straightforward intercepted navigation with no request body that
+ // returns a synthetic response.
+ await do_test_sw("example.org", "privilegedmozilla", "synthetic", null);
+
+ // Trigger an intercepted navigation with FormData containing an
+ // <input type="file"> which will result in the request body containing a
+ // RemoteLazyInputStream which will be consumed in the content process by the
+ // ServiceWorker while generating the synthetic response.
+ const fileBlob = await makeFileBlob(TEST_BLOB_CONTENTS);
+ await do_test_sw("example.org", "privilegedmozilla", "synthetic", fileBlob);
+
+ // Trigger an intercepted navigation with FormData containing an
+ // <input type="file"> which will result in the request body containing a
+ // RemoteLazyInputStream which will be relayed back to the parent process
+ // via direct invocation of fetch() on the event.request but without any
+ // cloning.
+ await do_test_sw("example.org", "privilegedmozilla", "fetch", fileBlob);
+
+ // Same as the above but cloning the request before fetching it.
+ await do_test_sw("example.org", "privilegedmozilla", "clone", fileBlob);
+
+ // ## Fission Isolation
+ if (Services.appinfo.fissionAutostart) {
+ // ## ServiceWorker isolation
+ const isolateUrl = "example.com";
+ const isolateRemoteType = `webServiceWorker=https://` + isolateUrl;
+ await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", null);
+ await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", fileBlob);
+ }
+ let telemetrySums = getSWTelemetrySums();
+ info(JSON.stringify(telemetrySums));
+ info(
+ "Initial Running All: " +
+ initialSums.SERVICE_WORKER_RUNNING_All +
+ ", Fetch: " +
+ initialSums.SERVICE_WORKER_RUNNING_Fetch
+ );
+ info(
+ "Initial Max Running All: " +
+ initialSums.SERVICEWORKER_RUNNING_MAX_All +
+ ", Fetch: " +
+ initialSums.SERVICEWORKER_RUNNING_MAX_Fetch
+ );
+ info(
+ "Running All: " +
+ telemetrySums.SERVICE_WORKER_RUNNING_All +
+ ", Fetch: " +
+ telemetrySums.SERVICE_WORKER_RUNNING_Fetch
+ );
+ info(
+ "Max Running All: " +
+ telemetrySums.SERVICEWORKER_RUNNING_MAX_All +
+ ", Fetch: " +
+ telemetrySums.SERVICEWORKER_RUNNING_MAX_Fetch
+ );
+ ok(
+ telemetrySums.SERVICE_WORKER_RUNNING_All >
+ initialSums.SERVICE_WORKER_RUNNING_All,
+ "ServiceWorker running count changed"
+ );
+ ok(
+ telemetrySums.SERVICE_WORKER_RUNNING_Fetch >
+ initialSums.SERVICE_WORKER_RUNNING_Fetch,
+ "ServiceWorker running count changed"
+ );
+ // We don't use ok()'s for MAX because MAX may have been set before we
+ // set canRecordExtended, and if so we won't record a new value unless
+ // the max increases again.
+});