summaryrefslogtreecommitdiffstats
path: root/dom/ipc/tests/browser_ProcessPriorityManager.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/ipc/tests/browser_ProcessPriorityManager.js')
-rw-r--r--dom/ipc/tests/browser_ProcessPriorityManager.js894
1 files changed, 894 insertions, 0 deletions
diff --git a/dom/ipc/tests/browser_ProcessPriorityManager.js b/dom/ipc/tests/browser_ProcessPriorityManager.js
new file mode 100644
index 0000000000..f32306e174
--- /dev/null
+++ b/dom/ipc/tests/browser_ProcessPriorityManager.js
@@ -0,0 +1,894 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PRIORITY_SET_TOPIC =
+ "process-priority-manager:TEST-ONLY:process-priority-set";
+
+// Copied from Hal.cpp
+const PROCESS_PRIORITY_FOREGROUND = "FOREGROUND";
+const PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE = "BACKGROUND_PERCEIVABLE";
+const PROCESS_PRIORITY_BACKGROUND = "BACKGROUND";
+
+// This is how many milliseconds we'll wait for a process priority
+// change before we assume that it's just not happening.
+const WAIT_FOR_CHANGE_TIME_MS = 2000;
+
+// A convenience function for getting the child ID from a browsing context.
+function browsingContextChildID(bc) {
+ return bc.currentWindowGlobal?.domProcess.childID;
+}
+
+/**
+ * This class is responsible for watching process priority changes, and
+ * mapping them to tabs in a single window.
+ */
+class TabPriorityWatcher {
+ /**
+ * Constructing a TabPriorityWatcher should happen before any tests
+ * start when there's only a single tab in the window.
+ *
+ * Callers must call `destroy()` on any instance that is constructed
+ * when the test is completed.
+ *
+ * @param tabbrowser (<tabbrowser>)
+ * The tabbrowser (gBrowser) for the window to be tested.
+ */
+ constructor(tabbrowser) {
+ this.tabbrowser = tabbrowser;
+ Assert.equal(
+ tabbrowser.tabs.length,
+ 1,
+ "TabPriorityWatcher must be constructed in a window " +
+ "with a single tab to start."
+ );
+
+ // This maps from childIDs to process priorities.
+ this.priorityMap = new Map();
+
+ // The keys in this map are childIDs we're not expecting to change.
+ // Each value is either null (if no change has been seen) or the
+ // priority that the process changed to.
+ this.noChangeChildIDs = new Map();
+
+ Services.obs.addObserver(this, PRIORITY_SET_TOPIC);
+ }
+
+ /**
+ * Cleans up lingering references for an instance of
+ * TabPriorityWatcher to avoid leaks. This should be called when
+ * finishing the test.
+ */
+ destroy() {
+ Services.obs.removeObserver(this, PRIORITY_SET_TOPIC);
+ }
+
+ /**
+ * This returns a Promise that resolves when the process with
+ * the given childID reaches the given priority.
+ * This will eventually time out if that priority is never reached.
+ *
+ * @param childID
+ * The childID of the process to wait on.
+ * @param expectedPriority (String)
+ * One of the PROCESS_PRIORITY_ constants defined at the
+ * top of this file.
+ * @return Promise
+ * @resolves undefined
+ * Once the browser reaches the expected priority.
+ */
+ async waitForPriorityChange(childID, expectedPriority) {
+ await TestUtils.waitForCondition(() => {
+ let currentPriority = this.priorityMap.get(childID);
+ if (currentPriority == expectedPriority) {
+ Assert.ok(
+ true,
+ `Process with child ID ${childID} reached expected ` +
+ `priority: ${currentPriority}`
+ );
+ return true;
+ }
+ return false;
+ }, `Waiting for process with child ID ${childID} to reach priority ${expectedPriority}`);
+ }
+
+ /**
+ * Returns a Promise that resolves after a duration of
+ * WAIT_FOR_CHANGE_TIME_MS. During that time, if the process
+ * with the passed in child ID changes priority, a test
+ * failure will be registered.
+ *
+ * @param childID
+ * The childID of the process that we expect to not change priority.
+ * @return Promise
+ * @resolves undefined
+ * Once the WAIT_FOR_CHANGE_TIME_MS duration has passed.
+ */
+ async ensureNoPriorityChange(childID) {
+ this.noChangeChildIDs.set(childID, null);
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS));
+ let priority = this.noChangeChildIDs.get(childID);
+ Assert.equal(
+ priority,
+ null,
+ `Should have seen no process priority change for child ID ${childID}`
+ );
+ this.noChangeChildIDs.delete(childID);
+ }
+
+ /**
+ * This returns a Promise that resolves when all of the processes
+ * of the browsing contexts in the browsing context tree
+ * of a particular <browser> have reached a particular priority.
+ * This will eventually time out if that priority is never reached.
+ *
+ * @param browser (<browser>)
+ * The <browser> to get the BC tree from.
+ * @param expectedPriority (String)
+ * One of the PROCESS_PRIORITY_ constants defined at the
+ * top of this file.
+ * @return Promise
+ * @resolves undefined
+ * Once the browser reaches the expected priority.
+ */
+ async waitForBrowserTreePriority(browser, expectedPriority) {
+ let childIDs = new Set(
+ browser.browsingContext
+ .getAllBrowsingContextsInSubtree()
+ .map(browsingContextChildID)
+ );
+ let promises = [];
+ for (let childID of childIDs) {
+ let currentPriority = this.priorityMap.get(childID);
+
+ promises.push(
+ currentPriority == expectedPriority
+ ? this.ensureNoPriorityChange(childID)
+ : this.waitForPriorityChange(childID, expectedPriority)
+ );
+ }
+
+ await Promise.all(promises);
+ }
+
+ /**
+ * Synchronously returns the priority of a particular child ID.
+ *
+ * @param childID
+ * The childID to get the content process priority for.
+ * @return String
+ * The priority of the child ID's process.
+ */
+ currentPriority(childID) {
+ return this.priorityMap.get(childID);
+ }
+
+ /**
+ * A utility function that takes a string passed via the
+ * PRIORITY_SET_TOPIC observer notification and extracts the
+ * childID and priority string.
+ *
+ * @param ppmDataString (String)
+ * The string data passed through the PRIORITY_SET_TOPIC observer
+ * notification.
+ * @return Object
+ * An object with the following properties:
+ *
+ * childID (Number)
+ * The ID of the content process that changed priority.
+ *
+ * priority (String)
+ * The priority that the content process was set to.
+ */
+ parsePPMData(ppmDataString) {
+ let [childIDStr, priority] = ppmDataString.split(":");
+ return {
+ childID: parseInt(childIDStr, 10),
+ priority,
+ };
+ }
+
+ /** nsIObserver **/
+ observe(subject, topic, data) {
+ if (topic != PRIORITY_SET_TOPIC) {
+ Assert.ok(false, "TabPriorityWatcher is observing the wrong topic");
+ return;
+ }
+
+ let { childID, priority } = this.parsePPMData(data);
+ if (this.noChangeChildIDs.has(childID)) {
+ this.noChangeChildIDs.set(childID, priority);
+ }
+ this.priorityMap.set(childID, priority);
+ }
+}
+
+let gTabPriorityWatcher;
+
+add_setup(async function () {
+ // We need to turn on testMode for the process priority manager in
+ // order to receive the observer notifications that this test relies on.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processPriorityManager.testMode", true],
+ ["dom.ipc.processPriorityManager.enabled", true],
+ ],
+ });
+ gTabPriorityWatcher = new TabPriorityWatcher(gBrowser);
+});
+
+registerCleanupFunction(() => {
+ gTabPriorityWatcher.destroy();
+ gTabPriorityWatcher = null;
+});
+
+/**
+ * Utility function that switches the current tabbrowser from one
+ * tab to another, and ensures that the tab that goes into the background
+ * has (or reaches) a particular content process priority.
+ *
+ * It is expected that the fromTab and toTab belong to two separate content
+ * processes.
+ *
+ * @param Object
+ * An object with the following properties:
+ *
+ * fromTab (<tab>)
+ * The tab that will be switched from to the toTab. The fromTab
+ * is the one that will be going into the background.
+ *
+ * toTab (<tab>)
+ * The tab that will be switched to from the fromTab. The toTab
+ * is presumed to start in the background, and will enter the
+ * foreground.
+ *
+ * fromTabExpectedPriority (String)
+ * The priority that the content process for the fromTab is
+ * expected to be (or reach) after the tab goes into the background.
+ * This should be one of the PROCESS_PRIORITY_ strings defined at the
+ * top of the file.
+ *
+ * @return Promise
+ * @resolves undefined
+ * Once the tab switch is complete, and the two content processes for the
+ * tabs have reached the expected priority levels.
+ */
+async function assertPriorityChangeOnBackground({
+ fromTab,
+ toTab,
+ fromTabExpectedPriority,
+}) {
+ let fromBrowser = fromTab.linkedBrowser;
+ let toBrowser = toTab.linkedBrowser;
+
+ // If the tabs aren't running in separate processes, none of the
+ // rest of this is going to work.
+ Assert.notEqual(
+ toBrowser.frameLoader.remoteTab.osPid,
+ fromBrowser.frameLoader.remoteTab.osPid,
+ "Tabs should be running in separate processes."
+ );
+
+ let fromPromise = gTabPriorityWatcher.waitForBrowserTreePriority(
+ fromBrowser,
+ fromTabExpectedPriority
+ );
+ let toPromise = gTabPriorityWatcher.waitForBrowserTreePriority(
+ toBrowser,
+ PROCESS_PRIORITY_FOREGROUND
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, toTab);
+ await Promise.all([fromPromise, toPromise]);
+}
+
+/**
+ * Test that if a normal tab goes into the background,
+ * it has its process priority lowered to PROCESS_PRIORITY_BACKGROUND.
+ * Additionally, test priorityHint flag sets the process priority
+ * appropriately to PROCESS_PRIORITY_BACKGROUND and PROCESS_PRIORITY_FOREGROUND.
+ */
+add_task(async function test_normal_background_tab() {
+ let originalTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/dom/ipc/tests/file_cross_frame.html",
+ async browser => {
+ let tab = gBrowser.getTabForBrowser(browser);
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ let origtabID = browsingContextChildID(
+ originalTab.linkedBrowser.browsingContext
+ );
+
+ Assert.equal(
+ originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint,
+ false,
+ "PriorityHint of the original tab should be false by default"
+ );
+
+ // Changing renderLayers doesn't change priority of the background tab.
+ originalTab.linkedBrowser.preserveLayers(true);
+ originalTab.linkedBrowser.renderLayers = true;
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)
+ );
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(origtabID),
+ PROCESS_PRIORITY_BACKGROUND,
+ "Tab didn't get prioritized only due to renderLayers"
+ );
+
+ // Test when priorityHint is true, the original tab priority
+ // becomes PROCESS_PRIORITY_FOREGROUND.
+ originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint = true;
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(origtabID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Setting priorityHint to true should set the original tab to foreground priority"
+ );
+
+ // Test when priorityHint is false, the original tab priority
+ // becomes PROCESS_PRIORITY_BACKGROUND.
+ originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint = false;
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)
+ );
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(origtabID),
+ PROCESS_PRIORITY_BACKGROUND,
+ "Setting priorityHint to false should set the original tab to background priority"
+ );
+
+ let tabID = browsingContextChildID(tab.linkedBrowser.browsingContext);
+
+ // Test when priorityHint is true, the process priority of the
+ // active tab remains PROCESS_PRIORITY_FOREGROUND.
+ tab.linkedBrowser.frameLoader.remoteTab.priorityHint = true;
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(tabID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Setting priorityHint to true should maintain the new tab priority as foreground"
+ );
+
+ // Test when priorityHint is false, the process priority of the
+ // active tab remains PROCESS_PRIORITY_FOREGROUND.
+ tab.linkedBrowser.frameLoader.remoteTab.priorityHint = false;
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(tabID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Setting priorityHint to false should maintain the new tab priority as foreground"
+ );
+
+ originalTab.linkedBrowser.preserveLayers(false);
+ originalTab.linkedBrowser.renderLayers = false;
+ }
+ );
+});
+
+// Load a simple page on the given host into a new tab.
+async function loadKeepAliveTab(host) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ host + "/browser/dom/ipc/tests/file_dummy.html"
+ );
+ let childID = browsingContextChildID(
+ gBrowser.selectedBrowser.browsingContext
+ );
+
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(childID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a new tab should make it prioritized"
+ );
+
+ if (SpecialPowers.useRemoteSubframes) {
+ // There must be only one process with a remote type for the tab we loaded
+ // to ensure that when we load a new page into the iframe with that host
+ // that it will end up in the same process as the initial tab.
+ let remoteType = gBrowser.selectedBrowser.remoteType;
+ await TestUtils.waitForCondition(() => {
+ return (
+ ChromeUtils.getAllDOMProcesses().filter(
+ process => process.remoteType == remoteType
+ ).length == 1
+ );
+ }, `Waiting for there to be only one process with remote type ${remoteType}`);
+ }
+
+ return { tab, childID };
+}
+
+/**
+ * If an iframe in a foreground tab is navigated to a new page for
+ * a different site, then the process of the new iframe page should
+ * have priority PROCESS_PRIORITY_FOREGROUND. Additionally, if Fission
+ * is enabled, then the old iframe page's process's priority should be
+ * lowered to PROCESS_PRIORITY_BACKGROUND.
+ */
+add_task(async function test_iframe_navigate() {
+ // This test (eventually) loads a page from the host topHost that has an
+ // iframe from iframe1Host. It then navigates the iframe to iframe2Host.
+ let topHost = "https://example.com";
+ let iframe1Host = "https://example.org";
+ let iframe2Host = "https://example.net";
+
+ // Before we load the final test page into a tab, we need to load pages
+ // from both iframe hosts into tabs. This is needed so that we are testing
+ // the "load a new page" part of prioritization and not the "initial
+ // process load" part. Additionally, it ensures that the process for the
+ // initial iframe page doesn't shut down once we navigate away from it,
+ // which will also affect its prioritization.
+ let { tab: iframe1Tab, childID: iframe1TabChildID } = await loadKeepAliveTab(
+ iframe1Host
+ );
+ let { tab: iframe2Tab, childID: iframe2TabChildID } = await loadKeepAliveTab(
+ iframe2Host
+ );
+
+ await BrowserTestUtils.withNewTab(
+ topHost + "/browser/dom/ipc/tests/file_cross_frame.html",
+ async browser => {
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(iframe2TabChildID),
+ PROCESS_PRIORITY_BACKGROUND,
+ "Switching to another new tab should deprioritize the old one"
+ );
+
+ let topChildID = browsingContextChildID(browser.browsingContext);
+ let iframe = browser.browsingContext.children[0];
+ let iframe1ChildID = browsingContextChildID(iframe);
+
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(topChildID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "The top level page in the new tab should be prioritized"
+ );
+
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(iframe1ChildID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "The iframe in the new tab should be prioritized"
+ );
+
+ if (SpecialPowers.useRemoteSubframes) {
+ // Basic process uniqueness checks for the state after all three tabs
+ // are initially loaded.
+ Assert.notEqual(
+ topChildID,
+ iframe1ChildID,
+ "file_cross_frame.html should be loaded into a different process " +
+ "than its initial iframe"
+ );
+
+ Assert.notEqual(
+ topChildID,
+ iframe2TabChildID,
+ "file_cross_frame.html should be loaded into a different process " +
+ "than the tab containing iframe2Host"
+ );
+
+ Assert.notEqual(
+ iframe1ChildID,
+ iframe2TabChildID,
+ "The initial iframe loaded by file_cross_frame.html should be " +
+ "loaded into a different process than the tab containing " +
+ "iframe2Host"
+ );
+
+ // Note: this assertion depends on our process selection logic.
+ // Specifically, that we reuse an existing process for an iframe if
+ // possible.
+ Assert.equal(
+ iframe1TabChildID,
+ iframe1ChildID,
+ "Both pages loaded in iframe1Host should be in the same process"
+ );
+ }
+
+ // Do a cross-origin navigation in the iframe in the foreground tab.
+ let iframe2URI = iframe2Host + "/browser/dom/ipc/tests/file_dummy.html";
+ let loaded = BrowserTestUtils.browserLoaded(browser, true, iframe2URI);
+ await SpecialPowers.spawn(
+ iframe,
+ [iframe2URI],
+ async function (_iframe2URI) {
+ content.location = _iframe2URI;
+ }
+ );
+ await loaded;
+
+ let iframe2ChildID = browsingContextChildID(iframe);
+ let iframe1Priority = gTabPriorityWatcher.currentPriority(iframe1ChildID);
+ let iframe2Priority = gTabPriorityWatcher.currentPriority(iframe2ChildID);
+
+ if (SpecialPowers.useRemoteSubframes) {
+ // Basic process uniqueness check for the state after navigating the
+ // iframe. There's no need to check the top level pages because they
+ // have not navigated.
+ //
+ // iframe1ChildID != iframe2ChildID is implied by:
+ // iframe1ChildID != iframe2TabChildID
+ // iframe2TabChildID == iframe2ChildID
+ //
+ // iframe2ChildID != topChildID is implied by:
+ // topChildID != iframe2TabChildID
+ // iframe2TabChildID == iframe2ChildID
+
+ // Note: this assertion depends on our process selection logic.
+ // Specifically, that we reuse an existing process for an iframe if
+ // possible. If that changes, this test may need to be carefully
+ // rewritten, as the whole point of the test is to check what happens
+ // with the priority manager when an iframe shares a process with
+ // a page in another tab.
+ Assert.equal(
+ iframe2TabChildID,
+ iframe2ChildID,
+ "Both pages loaded in iframe2Host should be in the same process"
+ );
+
+ // Now that we've established the relationship between the various
+ // processes, we can finally check that the priority manager is doing
+ // the right thing.
+ Assert.equal(
+ iframe1Priority,
+ PROCESS_PRIORITY_BACKGROUND,
+ "The old iframe process should have been deprioritized"
+ );
+ } else {
+ Assert.equal(
+ iframe1ChildID,
+ iframe2ChildID,
+ "Navigation should not have switched processes"
+ );
+ }
+
+ Assert.equal(
+ iframe2Priority,
+ PROCESS_PRIORITY_FOREGROUND,
+ "The new iframe process should be prioritized"
+ );
+ }
+ );
+
+ await BrowserTestUtils.removeTab(iframe2Tab);
+ await BrowserTestUtils.removeTab(iframe1Tab);
+});
+
+/**
+ * Test that a cross-group navigation properly preserves the process priority.
+ * The goal of this test is to check that the code related to mPriorityActive in
+ * CanonicalBrowsingContext::ReplacedBy works correctly, but in practice the
+ * prioritization code in SetRenderLayers will also make this test pass, though
+ * that prioritization happens slightly later.
+ */
+add_task(async function test_cross_group_navigate() {
+ // This page is same-site with the page we're going to cross-group navigate to.
+ let coopPage =
+ "https://example.com/browser/dom/tests/browser/file_coop_coep.html";
+
+ // Load it as a top level tab so that we don't accidentally get the initial
+ // load prioritization.
+ let backgroundTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ coopPage
+ );
+ let backgroundTabChildID = browsingContextChildID(
+ gBrowser.selectedBrowser.browsingContext
+ );
+
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(backgroundTabChildID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a new tab should make it prioritized"
+ );
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.org/browser/dom/ipc/tests/file_cross_frame.html",
+ async browser => {
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(backgroundTabChildID),
+ PROCESS_PRIORITY_BACKGROUND,
+ "Switching to a new tab should deprioritize the old one"
+ );
+
+ let dotOrgChildID = browsingContextChildID(browser.browsingContext);
+
+ // Do a cross-group navigation.
+ BrowserTestUtils.loadURIString(browser, coopPage);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let coopChildID = browsingContextChildID(browser.browsingContext);
+ let coopPriority = gTabPriorityWatcher.currentPriority(coopChildID);
+ let dotOrgPriority = gTabPriorityWatcher.currentPriority(dotOrgChildID);
+
+ Assert.equal(
+ backgroundTabChildID,
+ coopChildID,
+ "The same site should get loaded into the same process"
+ );
+ Assert.notEqual(
+ dotOrgChildID,
+ coopChildID,
+ "Navigation should have switched processes"
+ );
+ Assert.equal(
+ dotOrgPriority,
+ PROCESS_PRIORITY_BACKGROUND,
+ "The old page process should have been deprioritized"
+ );
+ Assert.equal(
+ coopPriority,
+ PROCESS_PRIORITY_FOREGROUND,
+ "The new page process should be prioritized"
+ );
+ }
+ );
+
+ await BrowserTestUtils.removeTab(backgroundTab);
+});
+
+/**
+ * Test that if a tab with video goes into the background,
+ * it has its process priority lowered to
+ * PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE if it has no audio,
+ * and that it has its priority remain at
+ * PROCESS_PRIORITY_FOREGROUND if it does have audio.
+ */
+add_task(async function test_video_background_tab() {
+ let originalTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ // Let's load up a video in the tab, but mute it, so that this tab should
+ // reach PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let video = content.document.createElement("video");
+ video.src = "https://example.net/browser/dom/ipc/tests/short.mp4";
+ video.muted = true;
+ content.document.body.appendChild(video);
+ // We'll loop the video to avoid it ending before the test is done.
+ video.loop = true;
+ await video.play();
+ });
+
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // The tab with the muted video should reach
+ // PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Let's unmute the video now.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let video = content.document.querySelector("video");
+ video.muted = false;
+ });
+
+ // The tab with the unmuted video should stay at
+ // PROCESS_PRIORITY_FOREGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+ });
+});
+
+/**
+ * Test that if a tab with a playing <audio> element goes into
+ * the background, the process priority does not change, unless
+ * that audio is muted (in which case, it reaches
+ * PROCESS_PRIORITY_BACKGROUND).
+ */
+add_task(async function test_audio_background_tab() {
+ let originalTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ // Let's load up some audio in the tab, but mute it, so that this tab should
+ // reach PROCESS_PRIORITY_BACKGROUND.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let audio = content.document.createElement("audio");
+ audio.src = "https://example.net/browser/dom/ipc/tests/owl.mp3";
+ audio.muted = true;
+ content.document.body.appendChild(audio);
+ // We'll loop the audio to avoid it ending before the test is done.
+ audio.loop = true;
+ await audio.play();
+ });
+
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // The tab with the muted audio should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Now unmute the audio. Unfortuntely, there's a bit of a race here,
+ // since the wakelock on the audio element is released and then
+ // re-acquired if the audio reaches its end and loops around. This
+ // will cause an unexpected priority change on the content process.
+ //
+ // To avoid this race, we'll seek the audio back to the beginning,
+ // and lower its playback rate to the minimum to increase the
+ // likelihood that the check completes before the audio loops around.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let audio = content.document.querySelector("audio");
+ let seeked = ContentTaskUtils.waitForEvent(audio, "seeked");
+ audio.muted = false;
+ // 0.25 is the minimum playback rate that still keeps the audio audible.
+ audio.playbackRate = 0.25;
+ audio.currentTime = 0;
+ await seeked;
+ });
+
+ // The tab with the unmuted audio should stay at
+ // PROCESS_PRIORITY_FOREGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+ });
+});
+
+/**
+ * Test that if a tab with a WebAudio playing goes into the background,
+ * the process priority does not change, unless that WebAudio context is
+ * suspended.
+ */
+add_task(async function test_web_audio_background_tab() {
+ let originalTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ // Let's synthesize a basic square wave as WebAudio.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let audioCtx = new content.AudioContext();
+ let oscillator = audioCtx.createOscillator();
+ oscillator.type = "square";
+ oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
+ oscillator.connect(audioCtx.destination);
+ oscillator.start();
+ while (audioCtx.state != "running") {
+ info(`wait until AudioContext starts running`);
+ await new Promise(r => (audioCtx.onstatechange = r));
+ }
+ // we'll stash the AudioContext away so that it's easier to access
+ // in the next SpecialPowers.spawn.
+ content.audioCtx = audioCtx;
+ });
+
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // The tab with the WebAudio should stay at
+ // PROCESS_PRIORITY_FOREGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Now suspend the WebAudio. This will cause it to stop
+ // playing.
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.audioCtx.suspend();
+ });
+
+ // The tab with the suspended WebAudio should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+ });
+});
+
+/**
+ * Test that foreground tab's process priority isn't changed when going back to
+ * a bfcached session history entry.
+ */
+add_task(async function test_audio_background_tab() {
+ let page1 = "https://example.com";
+ let page2 = page1 + "/?2";
+
+ await BrowserTestUtils.withNewTab(page1, async browser => {
+ let childID = browsingContextChildID(browser.browsingContext);
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(childID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a new tab should make it prioritized."
+ );
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, page2);
+ BrowserTestUtils.loadURIString(browser, page2);
+ await loaded;
+
+ childID = browsingContextChildID(browser.browsingContext);
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(childID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a new page should keep the tab prioritized."
+ );
+
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ browser.goBack();
+ await pageShowPromise;
+
+ childID = browsingContextChildID(browser.browsingContext);
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(childID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a page from the bfcache should keep the tab prioritized."
+ );
+ });
+});