no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / dom / ipc / tests / browser_ProcessPriorityManager.js
blobe1532a53ca65945c1d52b1b12b8b3223628bdeab
1 /* Any copyright is dedicated to the Public Domain.
2    http://creativecommons.org/publicdomain/zero/1.0/ */
4 "use strict";
6 const PRIORITY_SET_TOPIC =
7   "process-priority-manager:TEST-ONLY:process-priority-set";
9 // Copied from Hal.cpp
10 const PROCESS_PRIORITY_FOREGROUND = "FOREGROUND";
11 const PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE = "BACKGROUND_PERCEIVABLE";
12 const PROCESS_PRIORITY_BACKGROUND = "BACKGROUND";
14 // This is how many milliseconds we'll wait for a process priority
15 // change before we assume that it's just not happening.
16 const WAIT_FOR_CHANGE_TIME_MS = 2000;
18 // A convenience function for getting the child ID from a browsing context.
19 function browsingContextChildID(bc) {
20   return bc.currentWindowGlobal?.domProcess.childID;
23 /**
24  * This class is responsible for watching process priority changes, and
25  * mapping them to tabs in a single window.
26  */
27 class TabPriorityWatcher {
28   /**
29    * Constructing a TabPriorityWatcher should happen before any tests
30    * start when there's only a single tab in the window.
31    *
32    * Callers must call `destroy()` on any instance that is constructed
33    * when the test is completed.
34    *
35    * @param tabbrowser (<tabbrowser>)
36    *   The tabbrowser (gBrowser) for the window to be tested.
37    */
38   constructor(tabbrowser) {
39     this.tabbrowser = tabbrowser;
40     Assert.equal(
41       tabbrowser.tabs.length,
42       1,
43       "TabPriorityWatcher must be constructed in a window " +
44         "with a single tab to start."
45     );
47     // This maps from childIDs to process priorities.
48     this.priorityMap = new Map();
50     // The keys in this map are childIDs we're not expecting to change.
51     // Each value is an array of priorities we've seen the childID changed
52     // to since it was added to the map. If the array is empty, there
53     // have been no changes.
54     this.noChangeChildIDs = new Map();
56     Services.obs.addObserver(this, PRIORITY_SET_TOPIC);
57   }
59   /**
60    * Cleans up lingering references for an instance of
61    * TabPriorityWatcher to avoid leaks. This should be called when
62    * finishing the test.
63    */
64   destroy() {
65     Services.obs.removeObserver(this, PRIORITY_SET_TOPIC);
66   }
68   /**
69    * This returns a Promise that resolves when the process with
70    * the given childID reaches the given priority.
71    * This will eventually time out if that priority is never reached.
72    *
73    * @param childID
74    *   The childID of the process to wait on.
75    * @param expectedPriority (String)
76    *   One of the PROCESS_PRIORITY_ constants defined at the
77    *   top of this file.
78    * @return Promise
79    * @resolves undefined
80    *   Once the browser reaches the expected priority.
81    */
82   async waitForPriorityChange(childID, expectedPriority) {
83     await TestUtils.waitForCondition(() => {
84       let currentPriority = this.priorityMap.get(childID);
85       if (currentPriority == expectedPriority) {
86         Assert.ok(
87           true,
88           `Process with child ID ${childID} reached expected ` +
89             `priority: ${currentPriority}`
90         );
91         return true;
92       }
93       return false;
94     }, `Waiting for process with child ID ${childID} to reach priority ${expectedPriority}`);
95   }
97   /**
98    * Returns a Promise that resolves after a duration of
99    * WAIT_FOR_CHANGE_TIME_MS. During that time, if the process
100    * with the passed in child ID changes priority, a test
101    * failure will be registered.
102    *
103    * @param childID
104    *   The childID of the process that we expect to not change priority.
105    * @return Promise
106    * @resolves undefined
107    *   Once the WAIT_FOR_CHANGE_TIME_MS duration has passed.
108    */
109   async ensureNoPriorityChange(childID) {
110     this.noChangeChildIDs.set(childID, []);
111     // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
112     await new Promise(resolve => setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS));
113     let priorities = this.noChangeChildIDs.get(childID);
114     Assert.deepEqual(
115       priorities,
116       [],
117       `Should have seen no process priority changes for child ID ${childID}`
118     );
119     this.noChangeChildIDs.delete(childID);
120   }
122   /**
123    * This returns a Promise that resolves when all of the processes
124    * of the browsing contexts in the browsing context tree
125    * of a particular <browser> have reached a particular priority.
126    * This will eventually time out if that priority is never reached.
127    *
128    * @param browser (<browser>)
129    *   The <browser> to get the BC tree from.
130    * @param expectedPriority (String)
131    *   One of the PROCESS_PRIORITY_ constants defined at the
132    *   top of this file.
133    * @return Promise
134    * @resolves undefined
135    *   Once the browser reaches the expected priority.
136    */
137   async waitForBrowserTreePriority(browser, expectedPriority) {
138     let childIDs = new Set(
139       browser.browsingContext
140         .getAllBrowsingContextsInSubtree()
141         .map(browsingContextChildID)
142     );
143     let promises = [];
144     for (let childID of childIDs) {
145       let currentPriority = this.priorityMap.get(childID);
147       promises.push(
148         currentPriority == expectedPriority
149           ? this.ensureNoPriorityChange(childID)
150           : this.waitForPriorityChange(childID, expectedPriority)
151       );
152     }
154     await Promise.all(promises);
155   }
157   /**
158    * Synchronously returns the priority of a particular child ID.
159    *
160    * @param childID
161    *   The childID to get the content process priority for.
162    * @return String
163    *   The priority of the child ID's process.
164    */
165   currentPriority(childID) {
166     return this.priorityMap.get(childID);
167   }
169   /**
170    * A utility function that takes a string passed via the
171    * PRIORITY_SET_TOPIC observer notification and extracts the
172    * childID and priority string.
173    *
174    * @param ppmDataString (String)
175    *   The string data passed through the PRIORITY_SET_TOPIC observer
176    *   notification.
177    * @return Object
178    *   An object with the following properties:
179    *
180    *   childID (Number)
181    *     The ID of the content process that changed priority.
182    *
183    *   priority (String)
184    *     The priority that the content process was set to.
185    */
186   parsePPMData(ppmDataString) {
187     let [childIDStr, priority] = ppmDataString.split(":");
188     return {
189       childID: parseInt(childIDStr, 10),
190       priority,
191     };
192   }
194   /** nsIObserver **/
195   observe(subject, topic, data) {
196     if (topic != PRIORITY_SET_TOPIC) {
197       Assert.ok(false, "TabPriorityWatcher is observing the wrong topic");
198       return;
199     }
201     let { childID, priority } = this.parsePPMData(data);
202     if (this.noChangeChildIDs.has(childID)) {
203       this.noChangeChildIDs.get(childID).push(priority);
204     }
205     this.priorityMap.set(childID, priority);
206   }
209 let gTabPriorityWatcher;
211 add_setup(async function () {
212   // We need to turn on testMode for the process priority manager in
213   // order to receive the observer notifications that this test relies on.
214   await SpecialPowers.pushPrefEnv({
215     set: [
216       ["dom.ipc.processPriorityManager.testMode", true],
217       ["dom.ipc.processPriorityManager.enabled", true],
218     ],
219   });
220   gTabPriorityWatcher = new TabPriorityWatcher(gBrowser);
223 registerCleanupFunction(() => {
224   gTabPriorityWatcher.destroy();
225   gTabPriorityWatcher = null;
229  * Utility function that switches the current tabbrowser from one
230  * tab to another, and ensures that the tab that goes into the background
231  * has (or reaches) a particular content process priority.
233  * It is expected that the fromTab and toTab belong to two separate content
234  * processes.
236  * @param Object
237  *   An object with the following properties:
239  *   fromTab (<tab>)
240  *     The tab that will be switched from to the toTab. The fromTab
241  *     is the one that will be going into the background.
243  *   toTab (<tab>)
244  *     The tab that will be switched to from the fromTab. The toTab
245  *     is presumed to start in the background, and will enter the
246  *     foreground.
248  *   fromTabExpectedPriority (String)
249  *     The priority that the content process for the fromTab is
250  *     expected to be (or reach) after the tab goes into the background.
251  *     This should be one of the PROCESS_PRIORITY_ strings defined at the
252  *     top of the file.
254  * @return Promise
255  * @resolves undefined
256  *   Once the tab switch is complete, and the two content processes for the
257  *   tabs have reached the expected priority levels.
258  */
259 async function assertPriorityChangeOnBackground({
260   fromTab,
261   toTab,
262   fromTabExpectedPriority,
263 }) {
264   let fromBrowser = fromTab.linkedBrowser;
265   let toBrowser = toTab.linkedBrowser;
267   // If the tabs aren't running in separate processes, none of the
268   // rest of this is going to work.
269   Assert.notEqual(
270     toBrowser.frameLoader.remoteTab.osPid,
271     fromBrowser.frameLoader.remoteTab.osPid,
272     "Tabs should be running in separate processes."
273   );
275   let fromPromise = gTabPriorityWatcher.waitForBrowserTreePriority(
276     fromBrowser,
277     fromTabExpectedPriority
278   );
279   let toPromise = gTabPriorityWatcher.waitForBrowserTreePriority(
280     toBrowser,
281     PROCESS_PRIORITY_FOREGROUND
282   );
284   await BrowserTestUtils.switchTab(gBrowser, toTab);
285   await Promise.all([fromPromise, toPromise]);
289  * Test that if a normal tab goes into the background,
290  * it has its process priority lowered to PROCESS_PRIORITY_BACKGROUND.
291  * Additionally, test priorityHint flag sets the process priority
292  * appropriately to PROCESS_PRIORITY_BACKGROUND and PROCESS_PRIORITY_FOREGROUND.
293  */
294 add_task(async function test_normal_background_tab() {
295   let originalTab = gBrowser.selectedTab;
297   await BrowserTestUtils.withNewTab(
298     "https://example.com/browser/dom/ipc/tests/file_cross_frame.html",
299     async browser => {
300       let tab = gBrowser.getTabForBrowser(browser);
301       await assertPriorityChangeOnBackground({
302         fromTab: tab,
303         toTab: originalTab,
304         fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
305       });
307       await assertPriorityChangeOnBackground({
308         fromTab: originalTab,
309         toTab: tab,
310         fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
311       });
313       let origtabID = browsingContextChildID(
314         originalTab.linkedBrowser.browsingContext
315       );
317       Assert.equal(
318         originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint,
319         false,
320         "PriorityHint of the original tab should be false by default"
321       );
323       // Changing renderLayers doesn't change priority of the background tab.
324       originalTab.linkedBrowser.preserveLayers(true);
325       originalTab.linkedBrowser.renderLayers = true;
326       await new Promise(resolve =>
327         // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
328         setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)
329       );
330       Assert.equal(
331         gTabPriorityWatcher.currentPriority(origtabID),
332         PROCESS_PRIORITY_BACKGROUND,
333         "Tab didn't get prioritized only due to renderLayers"
334       );
336       // Test when priorityHint is true, the original tab priority
337       // becomes PROCESS_PRIORITY_FOREGROUND.
338       originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint = true;
339       Assert.equal(
340         gTabPriorityWatcher.currentPriority(origtabID),
341         PROCESS_PRIORITY_FOREGROUND,
342         "Setting priorityHint to true should set the original tab to foreground priority"
343       );
345       // Test when priorityHint is false, the original tab priority
346       // becomes PROCESS_PRIORITY_BACKGROUND.
347       originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint = false;
348       await new Promise(resolve =>
349         // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
350         setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)
351       );
352       Assert.equal(
353         gTabPriorityWatcher.currentPriority(origtabID),
354         PROCESS_PRIORITY_BACKGROUND,
355         "Setting priorityHint to false should set the original tab to background priority"
356       );
358       let tabID = browsingContextChildID(tab.linkedBrowser.browsingContext);
360       // Test when priorityHint is true, the process priority of the
361       // active tab remains PROCESS_PRIORITY_FOREGROUND.
362       tab.linkedBrowser.frameLoader.remoteTab.priorityHint = true;
363       Assert.equal(
364         gTabPriorityWatcher.currentPriority(tabID),
365         PROCESS_PRIORITY_FOREGROUND,
366         "Setting priorityHint to true should maintain the new tab priority as foreground"
367       );
369       // Test when priorityHint is false, the process priority of the
370       // active tab remains PROCESS_PRIORITY_FOREGROUND.
371       tab.linkedBrowser.frameLoader.remoteTab.priorityHint = false;
372       Assert.equal(
373         gTabPriorityWatcher.currentPriority(tabID),
374         PROCESS_PRIORITY_FOREGROUND,
375         "Setting priorityHint to false should maintain the new tab priority as foreground"
376       );
378       originalTab.linkedBrowser.preserveLayers(false);
379       originalTab.linkedBrowser.renderLayers = false;
380     }
381   );
384 // Load a simple page on the given host into a new tab.
385 async function loadKeepAliveTab(host) {
386   let tab = await BrowserTestUtils.openNewForegroundTab(
387     gBrowser,
388     host + "/browser/dom/ipc/tests/file_dummy.html"
389   );
390   let childID = browsingContextChildID(
391     gBrowser.selectedBrowser.browsingContext
392   );
394   Assert.equal(
395     gTabPriorityWatcher.currentPriority(childID),
396     PROCESS_PRIORITY_FOREGROUND,
397     "Loading a new tab should make it prioritized"
398   );
400   if (SpecialPowers.useRemoteSubframes) {
401     // There must be only one process with a remote type for the tab we loaded
402     // to ensure that when we load a new page into the iframe with that host
403     // that it will end up in the same process as the initial tab.
404     let remoteType = gBrowser.selectedBrowser.remoteType;
405     await TestUtils.waitForCondition(() => {
406       return (
407         ChromeUtils.getAllDOMProcesses().filter(
408           process => process.remoteType == remoteType
409         ).length == 1
410       );
411     }, `Waiting for there to be only one process with remote type ${remoteType}`);
412   }
414   return { tab, childID };
417 const AUDIO_WAKELOCK_NAME = "audio-playing";
418 const VIDEO_WAKELOCK_NAME = "video-playing";
420 // This function was copied from toolkit/content/tests/browser/head.js
421 function wakeLockObserved(powerManager, observeTopic, checkFn) {
422   return new Promise(resolve => {
423     function wakeLockListener() {}
424     wakeLockListener.prototype = {
425       QueryInterface: ChromeUtils.generateQI(["nsIDOMMozWakeLockListener"]),
426       callback(topic, state) {
427         if (topic == observeTopic && checkFn(state)) {
428           powerManager.removeWakeLockListener(wakeLockListener.prototype);
429           resolve();
430         }
431       },
432     };
433     powerManager.addWakeLockListener(wakeLockListener.prototype);
434   });
437 // This function was copied from toolkit/content/tests/browser/head.js
438 async function waitForExpectedWakeLockState(
439   topic,
440   { needLock, isForegroundLock }
441 ) {
442   const powerManagerService = Cc["@mozilla.org/power/powermanagerservice;1"];
443   const powerManager = powerManagerService.getService(
444     Ci.nsIPowerManagerService
445   );
446   const wakelockState = powerManager.getWakeLockState(topic);
447   let expectedLockState = "unlocked";
448   if (needLock) {
449     expectedLockState = isForegroundLock
450       ? "locked-foreground"
451       : "locked-background";
452   }
453   if (wakelockState != expectedLockState) {
454     info(`wait until wakelock becomes ${expectedLockState}`);
455     await wakeLockObserved(
456       powerManager,
457       topic,
458       state => state == expectedLockState
459     );
460   }
461   is(
462     powerManager.getWakeLockState(topic),
463     expectedLockState,
464     `the wakelock state for '${topic}' is equal to '${expectedLockState}'`
465   );
469  * If an iframe in a foreground tab is navigated to a new page for
470  * a different site, then the process of the new iframe page should
471  * have priority PROCESS_PRIORITY_FOREGROUND. Additionally, if Fission
472  * is enabled, then the old iframe page's process's priority should be
473  * lowered to PROCESS_PRIORITY_BACKGROUND.
474  */
475 add_task(async function test_iframe_navigate() {
476   // This test (eventually) loads a page from the host topHost that has an
477   // iframe from iframe1Host. It then navigates the iframe to iframe2Host.
478   let topHost = "https://example.com";
479   let iframe1Host = "https://example.org";
480   let iframe2Host = "https://example.net";
482   // Before we load the final test page into a tab, we need to load pages
483   // from both iframe hosts into tabs. This is needed so that we are testing
484   // the "load a new page" part of prioritization and not the "initial
485   // process load" part. Additionally, it ensures that the process for the
486   // initial iframe page doesn't shut down once we navigate away from it,
487   // which will also affect its prioritization.
488   let { tab: iframe1Tab, childID: iframe1TabChildID } = await loadKeepAliveTab(
489     iframe1Host
490   );
491   let { tab: iframe2Tab, childID: iframe2TabChildID } = await loadKeepAliveTab(
492     iframe2Host
493   );
495   await BrowserTestUtils.withNewTab(
496     topHost + "/browser/dom/ipc/tests/file_cross_frame.html",
497     async browser => {
498       Assert.equal(
499         gTabPriorityWatcher.currentPriority(iframe2TabChildID),
500         PROCESS_PRIORITY_BACKGROUND,
501         "Switching to another new tab should deprioritize the old one"
502       );
504       let topChildID = browsingContextChildID(browser.browsingContext);
505       let iframe = browser.browsingContext.children[0];
506       let iframe1ChildID = browsingContextChildID(iframe);
508       Assert.equal(
509         gTabPriorityWatcher.currentPriority(topChildID),
510         PROCESS_PRIORITY_FOREGROUND,
511         "The top level page in the new tab should be prioritized"
512       );
514       Assert.equal(
515         gTabPriorityWatcher.currentPriority(iframe1ChildID),
516         PROCESS_PRIORITY_FOREGROUND,
517         "The iframe in the new tab should be prioritized"
518       );
520       if (SpecialPowers.useRemoteSubframes) {
521         // Basic process uniqueness checks for the state after all three tabs
522         // are initially loaded.
523         Assert.notEqual(
524           topChildID,
525           iframe1ChildID,
526           "file_cross_frame.html should be loaded into a different process " +
527             "than its initial iframe"
528         );
530         Assert.notEqual(
531           topChildID,
532           iframe2TabChildID,
533           "file_cross_frame.html should be loaded into a different process " +
534             "than the tab containing iframe2Host"
535         );
537         Assert.notEqual(
538           iframe1ChildID,
539           iframe2TabChildID,
540           "The initial iframe loaded by file_cross_frame.html should be " +
541             "loaded into a different process than the tab containing " +
542             "iframe2Host"
543         );
545         // Note: this assertion depends on our process selection logic.
546         // Specifically, that we reuse an existing process for an iframe if
547         // possible.
548         Assert.equal(
549           iframe1TabChildID,
550           iframe1ChildID,
551           "Both pages loaded in iframe1Host should be in the same process"
552         );
553       }
555       // Do a cross-origin navigation in the iframe in the foreground tab.
556       let iframe2URI = iframe2Host + "/browser/dom/ipc/tests/file_dummy.html";
557       let loaded = BrowserTestUtils.browserLoaded(browser, true, iframe2URI);
558       await SpecialPowers.spawn(
559         iframe,
560         [iframe2URI],
561         async function (_iframe2URI) {
562           content.location = _iframe2URI;
563         }
564       );
565       await loaded;
567       let iframe2ChildID = browsingContextChildID(iframe);
568       let iframe1Priority = gTabPriorityWatcher.currentPriority(iframe1ChildID);
569       let iframe2Priority = gTabPriorityWatcher.currentPriority(iframe2ChildID);
571       if (SpecialPowers.useRemoteSubframes) {
572         // Basic process uniqueness check for the state after navigating the
573         // iframe. There's no need to check the top level pages because they
574         // have not navigated.
575         //
576         // iframe1ChildID != iframe2ChildID is implied by:
577         //   iframe1ChildID != iframe2TabChildID
578         //   iframe2TabChildID == iframe2ChildID
579         //
580         // iframe2ChildID != topChildID is implied by:
581         //   topChildID != iframe2TabChildID
582         //   iframe2TabChildID == iframe2ChildID
584         // Note: this assertion depends on our process selection logic.
585         // Specifically, that we reuse an existing process for an iframe if
586         // possible. If that changes, this test may need to be carefully
587         // rewritten, as the whole point of the test is to check what happens
588         // with the priority manager when an iframe shares a process with
589         // a page in another tab.
590         Assert.equal(
591           iframe2TabChildID,
592           iframe2ChildID,
593           "Both pages loaded in iframe2Host should be in the same process"
594         );
596         // Now that we've established the relationship between the various
597         // processes, we can finally check that the priority manager is doing
598         // the right thing.
599         Assert.equal(
600           iframe1Priority,
601           PROCESS_PRIORITY_BACKGROUND,
602           "The old iframe process should have been deprioritized"
603         );
604       } else {
605         Assert.equal(
606           iframe1ChildID,
607           iframe2ChildID,
608           "Navigation should not have switched processes"
609         );
610       }
612       Assert.equal(
613         iframe2Priority,
614         PROCESS_PRIORITY_FOREGROUND,
615         "The new iframe process should be prioritized"
616       );
617     }
618   );
620   await BrowserTestUtils.removeTab(iframe2Tab);
621   await BrowserTestUtils.removeTab(iframe1Tab);
625  * Test that a cross-group navigation properly preserves the process priority.
626  * The goal of this test is to check that the code related to mPriorityActive in
627  * CanonicalBrowsingContext::ReplacedBy works correctly, but in practice the
628  * prioritization code in SetRenderLayers will also make this test pass, though
629  * that prioritization happens slightly later.
630  */
631 add_task(async function test_cross_group_navigate() {
632   // This page is same-site with the page we're going to cross-group navigate to.
633   let coopPage =
634     "https://example.com/browser/dom/tests/browser/file_coop_coep.html";
636   // Load it as a top level tab so that we don't accidentally get the initial
637   // load prioritization.
638   let backgroundTab = await BrowserTestUtils.openNewForegroundTab(
639     gBrowser,
640     coopPage
641   );
642   let backgroundTabChildID = browsingContextChildID(
643     gBrowser.selectedBrowser.browsingContext
644   );
646   Assert.equal(
647     gTabPriorityWatcher.currentPriority(backgroundTabChildID),
648     PROCESS_PRIORITY_FOREGROUND,
649     "Loading a new tab should make it prioritized"
650   );
652   await BrowserTestUtils.withNewTab(
653     "https://example.org/browser/dom/ipc/tests/file_cross_frame.html",
654     async browser => {
655       Assert.equal(
656         gTabPriorityWatcher.currentPriority(backgroundTabChildID),
657         PROCESS_PRIORITY_BACKGROUND,
658         "Switching to a new tab should deprioritize the old one"
659       );
661       let dotOrgChildID = browsingContextChildID(browser.browsingContext);
663       // Do a cross-group navigation.
664       BrowserTestUtils.startLoadingURIString(browser, coopPage);
665       await BrowserTestUtils.browserLoaded(browser);
667       let coopChildID = browsingContextChildID(browser.browsingContext);
668       let coopPriority = gTabPriorityWatcher.currentPriority(coopChildID);
669       let dotOrgPriority = gTabPriorityWatcher.currentPriority(dotOrgChildID);
671       Assert.equal(
672         backgroundTabChildID,
673         coopChildID,
674         "The same site should get loaded into the same process"
675       );
676       Assert.notEqual(
677         dotOrgChildID,
678         coopChildID,
679         "Navigation should have switched processes"
680       );
681       Assert.equal(
682         dotOrgPriority,
683         PROCESS_PRIORITY_BACKGROUND,
684         "The old page process should have been deprioritized"
685       );
686       Assert.equal(
687         coopPriority,
688         PROCESS_PRIORITY_FOREGROUND,
689         "The new page process should be prioritized"
690       );
691     }
692   );
694   await BrowserTestUtils.removeTab(backgroundTab);
698  * Test that if a tab with video goes into the background,
699  * it has its process priority lowered to
700  * PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE if it has no audio,
701  * and that it has its priority remain at
702  * PROCESS_PRIORITY_FOREGROUND if it does have audio.
703  */
704 add_task(async function test_video_background_tab() {
705   let originalTab = gBrowser.selectedTab;
707   await BrowserTestUtils.withNewTab("https://example.com", async browser => {
708     // Let's load up a video in the tab, but mute it, so that this tab should
709     // reach PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE. We need to wait for the
710     // wakelock changes from the unmuting to get back up to the parent.
711     await SpecialPowers.spawn(browser, [], async () => {
712       let video = content.document.createElement("video");
713       video.src = "https://example.net/browser/dom/ipc/tests/short.mp4";
714       video.muted = true;
715       content.document.body.appendChild(video);
716       // We'll loop the video to avoid it ending before the test is done.
717       video.loop = true;
718       await video.play();
719     });
720     await Promise.all([
721       waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
722         needLock: false,
723       }),
724       waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
725         needLock: true,
726         isForegroundLock: true,
727       }),
728     ]);
730     let tab = gBrowser.getTabForBrowser(browser);
732     // The tab with the muted video should reach
733     // PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE when backgrounded.
734     await assertPriorityChangeOnBackground({
735       fromTab: tab,
736       toTab: originalTab,
737       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE,
738     });
740     // Now switch back. The initial blank tab should reach
741     // PROCESS_PRIORITY_BACKGROUND when backgrounded.
742     await assertPriorityChangeOnBackground({
743       fromTab: originalTab,
744       toTab: tab,
745       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
746     });
748     // Let's unmute the video now. We need to wait for the wakelock change from
749     // the unmuting to get back up to the parent.
750     await Promise.all([
751       waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
752         needLock: true,
753         isForegroundLock: true,
754       }),
755       SpecialPowers.spawn(browser, [], async () => {
756         let video = content.document.querySelector("video");
757         video.muted = false;
758       }),
759     ]);
761     // The tab with the unmuted video should stay at
762     // PROCESS_PRIORITY_FOREGROUND when backgrounded.
763     await assertPriorityChangeOnBackground({
764       fromTab: tab,
765       toTab: originalTab,
766       fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
767     });
769     // Now switch back. The initial blank tab should reach
770     // PROCESS_PRIORITY_BACKGROUND when backgrounded.
771     await assertPriorityChangeOnBackground({
772       fromTab: originalTab,
773       toTab: tab,
774       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
775     });
776   });
780  * Test that if a tab with a playing <audio> element goes into
781  * the background, the process priority does not change, unless
782  * that audio is muted (in which case, it reaches
783  * PROCESS_PRIORITY_BACKGROUND).
784  */
785 add_task(async function test_audio_background_tab() {
786   let originalTab = gBrowser.selectedTab;
788   await BrowserTestUtils.withNewTab("https://example.com", async browser => {
789     // Let's load up some audio in the tab, but mute it, so that this tab should
790     // reach PROCESS_PRIORITY_BACKGROUND.
791     await SpecialPowers.spawn(browser, [], async () => {
792       let audio = content.document.createElement("audio");
793       audio.src = "https://example.net/browser/dom/ipc/tests/owl.mp3";
794       audio.muted = true;
795       content.document.body.appendChild(audio);
796       // We'll loop the audio to avoid it ending before the test is done.
797       audio.loop = true;
798       await audio.play();
799     });
801     let tab = gBrowser.getTabForBrowser(browser);
803     // The tab with the muted audio should reach
804     // PROCESS_PRIORITY_BACKGROUND when backgrounded.
805     await assertPriorityChangeOnBackground({
806       fromTab: tab,
807       toTab: originalTab,
808       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
809     });
811     // Now switch back. The initial blank tab should reach
812     // PROCESS_PRIORITY_BACKGROUND when backgrounded.
813     await assertPriorityChangeOnBackground({
814       fromTab: originalTab,
815       toTab: tab,
816       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
817     });
819     // Now unmute the audio. Unfortuntely, there's a bit of a race here,
820     // since the wakelock on the audio element is released and then
821     // re-acquired if the audio reaches its end and loops around. This
822     // will cause an unexpected priority change on the content process.
823     //
824     // To avoid this race, we'll seek the audio back to the beginning,
825     // and lower its playback rate to the minimum to increase the
826     // likelihood that the check completes before the audio loops around.
827     await SpecialPowers.spawn(browser, [], async () => {
828       let audio = content.document.querySelector("audio");
829       let seeked = ContentTaskUtils.waitForEvent(audio, "seeked");
830       audio.muted = false;
831       // 0.25 is the minimum playback rate that still keeps the audio audible.
832       audio.playbackRate = 0.25;
833       audio.currentTime = 0;
834       await seeked;
835     });
837     // The tab with the unmuted audio should stay at
838     // PROCESS_PRIORITY_FOREGROUND when backgrounded.
839     await assertPriorityChangeOnBackground({
840       fromTab: tab,
841       toTab: originalTab,
842       fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
843     });
845     // Now switch back. The initial blank tab should reach
846     // PROCESS_PRIORITY_BACKGROUND when backgrounded.
847     await assertPriorityChangeOnBackground({
848       fromTab: originalTab,
849       toTab: tab,
850       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
851     });
852   });
856  * Test that if a tab with a WebAudio playing goes into the background,
857  * the process priority does not change, unless that WebAudio context is
858  * suspended.
859  */
860 add_task(async function test_web_audio_background_tab() {
861   let originalTab = gBrowser.selectedTab;
863   await BrowserTestUtils.withNewTab("https://example.com", async browser => {
864     // Let's synthesize a basic square wave as WebAudio.
865     await SpecialPowers.spawn(browser, [], async () => {
866       let audioCtx = new content.AudioContext();
867       let oscillator = audioCtx.createOscillator();
868       oscillator.type = "square";
869       oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
870       oscillator.connect(audioCtx.destination);
871       oscillator.start();
872       while (audioCtx.state != "running") {
873         info(`wait until AudioContext starts running`);
874         await new Promise(r => (audioCtx.onstatechange = r));
875       }
876       // we'll stash the AudioContext away so that it's easier to access
877       // in the next SpecialPowers.spawn.
878       content.audioCtx = audioCtx;
879     });
881     let tab = gBrowser.getTabForBrowser(browser);
883     // The tab with the WebAudio should stay at
884     // PROCESS_PRIORITY_FOREGROUND when backgrounded.
885     await assertPriorityChangeOnBackground({
886       fromTab: tab,
887       toTab: originalTab,
888       fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
889     });
891     // Now switch back. The initial blank tab should reach
892     // PROCESS_PRIORITY_BACKGROUND when backgrounded.
893     await assertPriorityChangeOnBackground({
894       fromTab: originalTab,
895       toTab: tab,
896       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
897     });
899     // Now suspend the WebAudio. This will cause it to stop
900     // playing.
901     await SpecialPowers.spawn(browser, [], async () => {
902       content.audioCtx.suspend();
903     });
905     // The tab with the suspended WebAudio should reach
906     // PROCESS_PRIORITY_BACKGROUND when backgrounded.
907     await assertPriorityChangeOnBackground({
908       fromTab: tab,
909       toTab: originalTab,
910       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
911     });
913     // Now switch back. The initial blank tab should reach
914     // PROCESS_PRIORITY_BACKGROUND when backgrounded.
915     await assertPriorityChangeOnBackground({
916       fromTab: originalTab,
917       toTab: tab,
918       fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
919     });
920   });
924  * Test that foreground tab's process priority isn't changed when going back to
925  * a bfcached session history entry.
926  */
927 add_task(async function test_audio_background_tab() {
928   let page1 = "https://example.com";
929   let page2 = page1 + "/?2";
931   await BrowserTestUtils.withNewTab(page1, async browser => {
932     let childID = browsingContextChildID(browser.browsingContext);
933     Assert.equal(
934       gTabPriorityWatcher.currentPriority(childID),
935       PROCESS_PRIORITY_FOREGROUND,
936       "Loading a new tab should make it prioritized."
937     );
938     let loaded = BrowserTestUtils.browserLoaded(browser, false, page2);
939     BrowserTestUtils.startLoadingURIString(browser, page2);
940     await loaded;
942     childID = browsingContextChildID(browser.browsingContext);
943     Assert.equal(
944       gTabPriorityWatcher.currentPriority(childID),
945       PROCESS_PRIORITY_FOREGROUND,
946       "Loading a new page should keep the tab prioritized."
947     );
949     let pageShowPromise = BrowserTestUtils.waitForContentEvent(
950       browser,
951       "pageshow"
952     );
953     browser.goBack();
954     await pageShowPromise;
956     childID = browsingContextChildID(browser.browsingContext);
957     Assert.equal(
958       gTabPriorityWatcher.currentPriority(childID),
959       PROCESS_PRIORITY_FOREGROUND,
960       "Loading a page from the bfcache should keep the tab prioritized."
961     );
962   });