diff options
Diffstat (limited to 'dom/workers/test/browser_serviceworker_fetch_new_process.js')
-rw-r--r-- | dom/workers/test/browser_serviceworker_fetch_new_process.js | 405 |
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. +}); |