no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / dom / workers / test / browser_serviceworker_fetch_new_process.js
blobae7d71c222cc7bd07d7e3d5db4d8812e317d1809
1 const DIRPATH = getRootDirectory(gTestPath).replace(
2   "chrome://mochitests/content/",
3   ""
4 );
6 /**
7  * We choose blob contents that will roundtrip cleanly through the `textContent`
8  * of our returned HTML page.
9  */
10 const TEST_BLOB_CONTENTS = `I'm a disk-backed test blob! Hooray!`;
12 add_setup(async function () {
13   await SpecialPowers.pushPrefEnv({
14     set: [
15       // Set preferences so that opening a page with the origin "example.org"
16       // will result in a remoteType of "privilegedmozilla" for both the
17       // page and the ServiceWorker.
18       ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true],
19       ["browser.tabs.remote.separatedMozillaDomains", "example.org"],
20       ["dom.ipc.processCount.privilegedmozilla", 1],
21       ["dom.ipc.processPrelaunch.enabled", false],
22       ["dom.serviceWorkers.enabled", true],
23       ["dom.serviceWorkers.testing.enabled", true],
24       // ServiceWorker worker instances should stay alive until explicitly
25       // caused to terminate by dropping these timeouts to 0 in
26       // `waitForWorkerAndProcessShutdown`.
27       ["dom.serviceWorkers.idle_timeout", 299999],
28       ["dom.serviceWorkers.idle_extended_timeout", 299999],
29     ],
30   });
31 });
33 function countRemoteType(remoteType) {
34   return ChromeUtils.getAllDOMProcesses().filter(
35     p => p.remoteType == remoteType
36   ).length;
39 /**
40  * Helper function to get a list of all current processes and their remote
41  * types.  Note that when in used in a templated literal that it is
42  * synchronously invoked when the string is evaluated and captures system state
43  * at that instant.
44  */
45 function debugRemotes() {
46   return ChromeUtils.getAllDOMProcesses()
47     .map(p => p.remoteType || "parent")
48     .join(",");
51 /**
52  * Wait for there to be zero processes of the given remoteType.  This check is
53  * considered successful if there are already no processes of the given type
54  * at this very moment.
55  */
56 async function waitForNoProcessesOfType(remoteType) {
57   info(`waiting for there to be no ${remoteType} procs`);
58   await TestUtils.waitForCondition(
59     () => countRemoteType(remoteType) == 0,
60     "wait for the worker's process to shutdown"
61   );
64 /**
65  * Given a ServiceWorkerRegistrationInfo with an active ServiceWorker that
66  * has no active ExtendableEvents but would otherwise continue running thanks
67  * to the idle keepalive:
68  * - Assert that there is a ServiceWorker instance in the given registration's
69  *   active slot.  (General invariant check.)
70  * - Assert that a single process with the given remoteType currently exists.
71  *   (This doesn't mean the SW is alive in that process, though this test
72  *   verifies that via other checks when appropriate.)
73  * - Induce the worker to shutdown by temporarily dropping the idle timeout to 0
74  *   and causing the idle timer to be reset due to rapid debugger attach/detach.
75  * - Wait for the the single process with the given remoteType to go away.
76  * - Reset the idle timeouts back to their previous high values.
77  */
78 async function waitForWorkerAndProcessShutdown(swRegInfo, remoteType) {
79   info(`terminating worker and waiting for ${remoteType} procs to shut down`);
80   ok(swRegInfo.activeWorker, "worker should be in the active slot");
81   is(
82     countRemoteType(remoteType),
83     1,
84     `should have a single ${remoteType} process but have: ${debugRemotes()}`
85   );
87   // Let's not wait too long for the process to shutdown.
88   await SpecialPowers.pushPrefEnv({
89     set: [
90       ["dom.serviceWorkers.idle_timeout", 0],
91       ["dom.serviceWorkers.idle_extended_timeout", 0],
92     ],
93   });
95   // We need to cause the worker to re-evaluate its idle timeout. The easiest
96   // way to do this I could think of is to attach and then detach the debugger
97   // from the active worker.
98   swRegInfo.activeWorker.attachDebugger();
99   await new Promise(resolve => Cu.dispatch(resolve));
100   swRegInfo.activeWorker.detachDebugger();
102   // Eventually the length will reach 0, meaning we're done!
103   await waitForNoProcessesOfType(remoteType);
105   is(
106     countRemoteType(remoteType),
107     0,
108     `processes with remoteType=${remoteType} type should have shut down`
109   );
111   // Make sure we never kill workers on idle except when this is called.
112   await SpecialPowers.popPrefEnv();
115 async function do_test_sw(host, remoteType, swMode, fileBlob) {
116   info(
117     `### entering test: host=${host}, remoteType=${remoteType}, mode=${swMode}`
118   );
120   const prin = Services.scriptSecurityManager.createContentPrincipal(
121     Services.io.newURI(`https://${host}`),
122     {}
123   );
124   const sw = `https://${host}/${DIRPATH}file_service_worker_fetch_synthetic.js`;
125   const scope = `https://${host}/${DIRPATH}server_fetch_synthetic.sjs`;
127   const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
128     Ci.nsIServiceWorkerManager
129   );
130   const swRegInfo = await swm.registerForTest(prin, scope, sw);
131   swRegInfo.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo);
133   info(
134     `service worker registered: ${JSON.stringify({
135       principal: swRegInfo.principal.spec,
136       scope: swRegInfo.scope,
137     })}`
138   );
140   // Wait for the worker to install & shut down.
141   await TestUtils.waitForCondition(
142     () => swRegInfo.activeWorker,
143     "wait for the worker to become active"
144   );
145   await waitForWorkerAndProcessShutdown(swRegInfo, remoteType);
147   info(
148     `test navigation interception with mode=${swMode} starting from about:blank`
149   );
150   await BrowserTestUtils.withNewTab(
151     {
152       gBrowser,
153       url: "about:blank",
154     },
155     async browser => {
156       // NOTE: We intentionally trigger the navigation from content in order to
157       // make sure frontend doesn't eagerly process-switch for us.
158       SpecialPowers.spawn(
159         browser,
160         [scope, swMode, fileBlob],
161         // eslint-disable-next-line no-shadow
162         async (scope, swMode, fileBlob) => {
163           const pageUrl = `${scope}?mode=${swMode}`;
164           if (!fileBlob) {
165             content.location.href = pageUrl;
166           } else {
167             const doc = content.document;
168             const formElem = doc.createElement("form");
169             doc.body.appendChild(formElem);
171             formElem.action = pageUrl;
172             formElem.method = "POST";
173             formElem.enctype = "multipart/form-data";
175             const fileElem = doc.createElement("input");
176             formElem.appendChild(fileElem);
178             fileElem.type = "file";
179             fileElem.name = "foo";
181             fileElem.mozSetFileArray([fileBlob]);
183             formElem.submit();
184           }
185         }
186       );
188       await BrowserTestUtils.browserLoaded(browser);
190       is(
191         countRemoteType(remoteType),
192         1,
193         `should have spawned a content process with remoteType=${remoteType}`
194       );
196       const { source, blobContents } = await SpecialPowers.spawn(
197         browser,
198         [],
199         () => {
200           return {
201             source: content.document.getElementById("source").textContent,
202             blobContents: content.document.getElementById("blob").textContent,
203           };
204         }
205       );
207       is(
208         source,
209         swMode === "synthetic" ? "ServiceWorker" : "ServerJS",
210         "The page contents should come from the right place."
211       );
213       is(
214         blobContents,
215         fileBlob ? TEST_BLOB_CONTENTS : "",
216         "The request blob contents should be the blob/empty as appropriate."
217       );
219       // Ensure the worker was loaded in this process.
220       const workerDebuggerURLs = await SpecialPowers.spawn(
221         browser,
222         [sw],
223         async url => {
224           if (!content.navigator.serviceWorker.controller) {
225             throw new Error("document not controlled!");
226           }
227           const wdm = Cc[
228             "@mozilla.org/dom/workers/workerdebuggermanager;1"
229           ].getService(Ci.nsIWorkerDebuggerManager);
231           return Array.from(wdm.getWorkerDebuggerEnumerator())
232             .map(wd => {
233               return wd.url;
234             })
235             .filter(swURL => swURL == url);
236         }
237       );
238       if (remoteType.startsWith("webServiceWorker=")) {
239         Assert.notDeepEqual(
240           workerDebuggerURLs,
241           [sw],
242           "Isolated workers should not be running in the content child process"
243         );
244       } else {
245         Assert.deepEqual(
246           workerDebuggerURLs,
247           [sw],
248           "The worker should be running in the correct child process"
249         );
250       }
252       // Unregister the ServiceWorker.  The registration will continue to control
253       // `browser` and therefore continue to exist and its worker to continue
254       // running until the tab is closed.
255       await SpecialPowers.spawn(browser, [], async () => {
256         let registration = await content.navigator.serviceWorker.ready;
257         await registration.unregister();
258       });
259     }
260   );
262   // Now that the controlled tab is closed and the registration has been
263   // removed, the ServiceWorker will be made redundant which will forcibly
264   // terminate it, which will result in the shutdown of the given content
265   // process.  Wait for that to happen both as a verification and so the next
266   // test has a sufficiently clean slate.
267   await waitForNoProcessesOfType(remoteType);
271  * Create a File-backed blob.  This will happen synchronously from the main
272  * thread, which isn't optimal, but the test blocks on this progress anyways.
273  * Bug 1669578 has been filed on improving this idiom and avoiding the sync
274  * writes.
275  */
276 async function makeFileBlob(blobContents) {
277   const tmpFile = Cc["@mozilla.org/file/directory_service;1"]
278     .getService(Ci.nsIDirectoryService)
279     .QueryInterface(Ci.nsIProperties)
280     .get("TmpD", Ci.nsIFile);
281   tmpFile.append("test-file-backed-blob.txt");
282   tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
284   var outStream = Cc[
285     "@mozilla.org/network/file-output-stream;1"
286   ].createInstance(Ci.nsIFileOutputStream);
287   outStream.init(
288     tmpFile,
289     0x02 | 0x08 | 0x20, // write, create, truncate
290     0o666,
291     0
292   );
293   outStream.write(blobContents, blobContents.length);
294   outStream.close();
296   const fileBlob = await File.createFromNsIFile(tmpFile);
297   return fileBlob;
300 function getSWTelemetrySums() {
301   let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
302     Ci.nsITelemetry
303   );
304   let keyedhistograms = telemetry.getSnapshotForKeyedHistograms(
305     "main",
306     false
307   ).parent;
308   let keyedscalars = telemetry.getSnapshotForKeyedScalars("main", false).parent;
309   // We're not looking at the distribution of the histograms, just that they changed
310   return {
311     SERVICE_WORKER_RUNNING_All: keyedhistograms.SERVICE_WORKER_RUNNING
312       ? keyedhistograms.SERVICE_WORKER_RUNNING.All.sum
313       : 0,
314     SERVICE_WORKER_RUNNING_Fetch: keyedhistograms.SERVICE_WORKER_RUNNING
315       ? keyedhistograms.SERVICE_WORKER_RUNNING.Fetch.sum
316       : 0,
317     SERVICEWORKER_RUNNING_MAX_All: keyedscalars["serviceworker.running_max"]
318       ? keyedscalars["serviceworker.running_max"].All
319       : 0,
320     SERVICEWORKER_RUNNING_MAX_Fetch: keyedscalars["serviceworker.running_max"]
321       ? keyedscalars["serviceworker.running_max"].Fetch
322       : 0,
323   };
326 add_task(async function test() {
327   // Can't test telemetry without this since we may not be on the nightly channel
328   let oldCanRecord = Services.telemetry.canRecordExtended;
329   Services.telemetry.canRecordExtended = true;
330   registerCleanupFunction(() => {
331     Services.telemetry.canRecordExtended = oldCanRecord;
332   });
334   let initialSums = getSWTelemetrySums();
336   // ## Isolated Privileged Process
337   // Trigger a straightforward intercepted navigation with no request body that
338   // returns a synthetic response.
339   await do_test_sw("example.org", "privilegedmozilla", "synthetic", null);
341   // Trigger an intercepted navigation with FormData containing an
342   // <input type="file"> which will result in the request body containing a
343   // RemoteLazyInputStream which will be consumed in the content process by the
344   // ServiceWorker while generating the synthetic response.
345   const fileBlob = await makeFileBlob(TEST_BLOB_CONTENTS);
346   await do_test_sw("example.org", "privilegedmozilla", "synthetic", fileBlob);
348   // Trigger an intercepted navigation with FormData containing an
349   // <input type="file"> which will result in the request body containing a
350   // RemoteLazyInputStream which will be relayed back to the parent process
351   // via direct invocation of fetch() on the event.request but without any
352   // cloning.
353   await do_test_sw("example.org", "privilegedmozilla", "fetch", fileBlob);
355   // Same as the above but cloning the request before fetching it.
356   await do_test_sw("example.org", "privilegedmozilla", "clone", fileBlob);
358   // ## Fission Isolation
359   if (Services.appinfo.fissionAutostart) {
360     // ## ServiceWorker isolation
361     const isolateUrl = "example.com";
362     const isolateRemoteType = `webServiceWorker=https://` + isolateUrl;
363     await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", null);
364     await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", fileBlob);
365   }
366   let telemetrySums = getSWTelemetrySums();
367   info(JSON.stringify(telemetrySums));
368   info(
369     "Initial Running All: " +
370       initialSums.SERVICE_WORKER_RUNNING_All +
371       ", Fetch: " +
372       initialSums.SERVICE_WORKER_RUNNING_Fetch
373   );
374   info(
375     "Initial Max Running All: " +
376       initialSums.SERVICEWORKER_RUNNING_MAX_All +
377       ", Fetch: " +
378       initialSums.SERVICEWORKER_RUNNING_MAX_Fetch
379   );
380   info(
381     "Running All: " +
382       telemetrySums.SERVICE_WORKER_RUNNING_All +
383       ", Fetch: " +
384       telemetrySums.SERVICE_WORKER_RUNNING_Fetch
385   );
386   info(
387     "Max Running All: " +
388       telemetrySums.SERVICEWORKER_RUNNING_MAX_All +
389       ", Fetch: " +
390       telemetrySums.SERVICEWORKER_RUNNING_MAX_Fetch
391   );
392   Assert.greater(
393     telemetrySums.SERVICE_WORKER_RUNNING_All,
394     initialSums.SERVICE_WORKER_RUNNING_All,
395     "ServiceWorker running count changed"
396   );
397   Assert.greater(
398     telemetrySums.SERVICE_WORKER_RUNNING_Fetch,
399     initialSums.SERVICE_WORKER_RUNNING_Fetch,
400     "ServiceWorker running count changed"
401   );
402   // We don't use ok()'s for MAX because MAX may have been set before we
403   // set canRecordExtended, and if so we won't record a new value unless
404   // the max increases again.