/* 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 () * 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 have reached a particular priority. * This will eventually time out if that priority is never reached. * * @param browser () * The 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 () * The tab that will be switched from to the toTab. The fromTab * is the one that will be going into the background. * * toTab () * 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