browser_ProcessPriorityManager.js (36012B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const PRIORITY_SET_TOPIC = 7 "process-priority-manager:TEST-ONLY:process-priority-set"; 8 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"; 13 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; 17 18 // This test has to wait WAIT_FOR_CHANGE_TIME_MS multiple times, 19 // so give it a little longer to complete. 20 requestLongerTimeout(2); 21 22 // A convenience function for getting the child ID from a browsing context. 23 function browsingContextChildID(bc) { 24 return bc.currentWindowGlobal?.domProcess.childID; 25 } 26 27 /** 28 * This class is responsible for watching process priority changes, and 29 * mapping them to tabs in a single window. 30 */ 31 class TabPriorityWatcher { 32 /** 33 * Constructing a TabPriorityWatcher should happen before any tests 34 * start when there's only a single tab in the window. 35 * 36 * Callers must call `destroy()` on any instance that is constructed 37 * when the test is completed. 38 * 39 * @param tabbrowser (<tabbrowser>) 40 * The tabbrowser (gBrowser) for the window to be tested. 41 */ 42 constructor(tabbrowser) { 43 this.tabbrowser = tabbrowser; 44 Assert.equal( 45 tabbrowser.tabs.length, 46 1, 47 "TabPriorityWatcher must be constructed in a window " + 48 "with a single tab to start." 49 ); 50 51 // This maps from childIDs to process priorities. 52 this.priorityMap = new Map(); 53 54 // The keys in this map are childIDs we're not expecting to change. 55 // Each value is an array of priorities we've seen the childID changed 56 // to since it was added to the map. If the array is empty, there 57 // have been no changes. 58 this.noChangeChildIDs = new Map(); 59 60 Services.obs.addObserver(this, PRIORITY_SET_TOPIC); 61 } 62 63 /** 64 * Cleans up lingering references for an instance of 65 * TabPriorityWatcher to avoid leaks. This should be called when 66 * finishing the test. 67 */ 68 destroy() { 69 Services.obs.removeObserver(this, PRIORITY_SET_TOPIC); 70 } 71 72 /** 73 * This returns a Promise that resolves when the process with 74 * the given childID reaches the given priority. 75 * This will eventually time out if that priority is never reached. 76 * 77 * @param childID 78 * The childID of the process to wait on. 79 * @param expectedPriority (String) 80 * One of the PROCESS_PRIORITY_ constants defined at the 81 * top of this file. 82 * @returns {Promise<void>} 83 * Resolves once the browser reaches the expected priority. 84 */ 85 async waitForPriorityChange(childID, expectedPriority) { 86 await TestUtils.waitForCondition(() => { 87 let currentPriority = this.priorityMap.get(childID); 88 if (currentPriority == expectedPriority) { 89 Assert.ok( 90 true, 91 `Process with child ID ${childID} reached expected ` + 92 `priority: ${currentPriority}` 93 ); 94 return true; 95 } 96 return false; 97 }, `Waiting for process with child ID ${childID} to reach priority ${expectedPriority}`); 98 } 99 100 /** 101 * Returns a Promise that resolves after a duration of 102 * WAIT_FOR_CHANGE_TIME_MS. During that time, if the process 103 * with the passed in child ID changes priority, a test 104 * failure will be registered. 105 * 106 * @param childID 107 * The childID of the process that we expect to not change priority. 108 * @returns {Promise<void>} 109 * Resolves once the WAIT_FOR_CHANGE_TIME_MS duration has passed. 110 */ 111 async ensureNoPriorityChange(childID) { 112 this.noChangeChildIDs.set(childID, []); 113 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 114 await new Promise(resolve => setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)); 115 let priorities = this.noChangeChildIDs.get(childID); 116 Assert.deepEqual( 117 priorities, 118 [], 119 `Should have seen no process priority changes for child ID ${childID}` 120 ); 121 this.noChangeChildIDs.delete(childID); 122 } 123 124 /** 125 * This returns a Promise that resolves when all of the processes 126 * of the browsing contexts in the browsing context tree 127 * of a particular <browser> have reached a particular priority. 128 * This will eventually time out if that priority is never reached. 129 * 130 * @param browser (<browser>) 131 * The <browser> to get the BC tree from. 132 * @param expectedPriority (String) 133 * One of the PROCESS_PRIORITY_ constants defined at the 134 * top of this file. 135 * @returns {Promise<void>} 136 * Resolves once the browser reaches the expected priority. 137 */ 138 async waitForBrowserTreePriority(browser, expectedPriority) { 139 let childIDs = new Set( 140 browser.browsingContext 141 .getAllBrowsingContextsInSubtree() 142 .map(browsingContextChildID) 143 ); 144 let promises = []; 145 for (let childID of childIDs) { 146 let currentPriority = this.priorityMap.get(childID); 147 148 promises.push( 149 currentPriority == expectedPriority 150 ? this.ensureNoPriorityChange(childID) 151 : this.waitForPriorityChange(childID, expectedPriority) 152 ); 153 } 154 155 await Promise.all(promises); 156 } 157 158 /** 159 * Synchronously returns the priority of a particular child ID. 160 * 161 * @param childID 162 * The childID to get the content process priority for. 163 * @return String 164 * The priority of the child ID's process. 165 */ 166 currentPriority(childID) { 167 return this.priorityMap.get(childID); 168 } 169 170 /** 171 * A utility function that takes a string passed via the 172 * PRIORITY_SET_TOPIC observer notification and extracts the 173 * childID and priority string. 174 * 175 * @param ppmDataString (String) 176 * The string data passed through the PRIORITY_SET_TOPIC observer 177 * notification. 178 * @return Object 179 * An object with the following properties: 180 * 181 * childID (Number) 182 * The ID of the content process that changed priority. 183 * 184 * priority (String) 185 * The priority that the content process was set to. 186 */ 187 parsePPMData(ppmDataString) { 188 let [childIDStr, priority] = ppmDataString.split(":"); 189 return { 190 childID: parseInt(childIDStr, 10), 191 priority, 192 }; 193 } 194 195 /** nsIObserver */ 196 observe(subject, topic, data) { 197 if (topic != PRIORITY_SET_TOPIC) { 198 Assert.ok(false, "TabPriorityWatcher is observing the wrong topic"); 199 return; 200 } 201 202 let { childID, priority } = this.parsePPMData(data); 203 if (this.noChangeChildIDs.has(childID)) { 204 this.noChangeChildIDs.get(childID).push(priority); 205 } 206 this.priorityMap.set(childID, priority); 207 } 208 } 209 210 let gTabPriorityWatcher; 211 212 add_setup(async function () { 213 // We need to turn on testMode for the process priority manager in 214 // order to receive the observer notifications that this test relies on. 215 await SpecialPowers.pushPrefEnv({ 216 set: [ 217 ["dom.ipc.processPriorityManager.testMode", true], 218 ["dom.ipc.processPriorityManager.enabled", true], 219 ], 220 }); 221 gTabPriorityWatcher = new TabPriorityWatcher(gBrowser); 222 }); 223 224 registerCleanupFunction(() => { 225 gTabPriorityWatcher.destroy(); 226 gTabPriorityWatcher = null; 227 }); 228 229 /** 230 * Utility function that switches the current tabbrowser from one 231 * tab to another, and ensures that the tab that goes into the background 232 * has (or reaches) a particular content process priority. 233 * 234 * It is expected that the fromTab and toTab belong to two separate content 235 * processes. 236 * 237 * @param Object 238 * An object with the following properties: 239 * 240 * fromTab (<tab>) 241 * The tab that will be switched from to the toTab. The fromTab 242 * is the one that will be going into the background. 243 * 244 * toTab (<tab>) 245 * The tab that will be switched to from the fromTab. The toTab 246 * is presumed to start in the background, and will enter the 247 * foreground. 248 * 249 * fromTabExpectedPriority (String) 250 * The priority that the content process for the fromTab is 251 * expected to be (or reach) after the tab goes into the background. 252 * This should be one of the PROCESS_PRIORITY_ strings defined at the 253 * top of the file. 254 * 255 * @returns {Promise<void>} 256 * Resolves once the tab switch is complete, and the two content processes for 257 * the 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; 266 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 ); 274 275 let fromPromise = gTabPriorityWatcher.waitForBrowserTreePriority( 276 fromBrowser, 277 fromTabExpectedPriority 278 ); 279 let toPromise = gTabPriorityWatcher.waitForBrowserTreePriority( 280 toBrowser, 281 PROCESS_PRIORITY_FOREGROUND 282 ); 283 284 await BrowserTestUtils.switchTab(gBrowser, toTab); 285 await Promise.all([fromPromise, toPromise]); 286 } 287 288 /** 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; 296 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 }); 306 307 await assertPriorityChangeOnBackground({ 308 fromTab: originalTab, 309 toTab: tab, 310 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 311 }); 312 313 let origtabID = browsingContextChildID( 314 originalTab.linkedBrowser.browsingContext 315 ); 316 317 Assert.equal( 318 originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint, 319 false, 320 "PriorityHint of the original tab should be false by default" 321 ); 322 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 ); 335 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 ); 344 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 ); 357 358 let tabID = browsingContextChildID(tab.linkedBrowser.browsingContext); 359 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 ); 368 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 ); 377 378 originalTab.linkedBrowser.preserveLayers(false); 379 originalTab.linkedBrowser.renderLayers = false; 380 } 381 ); 382 }); 383 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 ); 393 394 Assert.equal( 395 gTabPriorityWatcher.currentPriority(childID), 396 PROCESS_PRIORITY_FOREGROUND, 397 "Loading a new tab should make it prioritized" 398 ); 399 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 } 413 414 return { tab, childID }; 415 } 416 417 const AUDIO_WAKELOCK_NAME = "audio-playing"; 418 const VIDEO_WAKELOCK_NAME = "video-playing"; 419 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 }); 435 } 436 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 ); 466 } 467 468 /** 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"; 481 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 } = 489 await loadKeepAliveTab(iframe1Host); 490 let { tab: iframe2Tab, childID: iframe2TabChildID } = 491 await loadKeepAliveTab(iframe2Host); 492 493 await BrowserTestUtils.withNewTab( 494 topHost + "/browser/dom/ipc/tests/file_cross_frame.html", 495 async browser => { 496 Assert.equal( 497 gTabPriorityWatcher.currentPriority(iframe2TabChildID), 498 PROCESS_PRIORITY_BACKGROUND, 499 "Switching to another new tab should deprioritize the old one" 500 ); 501 502 let topChildID = browsingContextChildID(browser.browsingContext); 503 let iframe = browser.browsingContext.children[0]; 504 let iframe1ChildID = browsingContextChildID(iframe); 505 506 Assert.equal( 507 gTabPriorityWatcher.currentPriority(topChildID), 508 PROCESS_PRIORITY_FOREGROUND, 509 "The top level page in the new tab should be prioritized" 510 ); 511 512 Assert.equal( 513 gTabPriorityWatcher.currentPriority(iframe1ChildID), 514 PROCESS_PRIORITY_FOREGROUND, 515 "The iframe in the new tab should be prioritized" 516 ); 517 518 if (SpecialPowers.useRemoteSubframes) { 519 // Basic process uniqueness checks for the state after all three tabs 520 // are initially loaded. 521 Assert.notEqual( 522 topChildID, 523 iframe1ChildID, 524 "file_cross_frame.html should be loaded into a different process " + 525 "than its initial iframe" 526 ); 527 528 Assert.notEqual( 529 topChildID, 530 iframe2TabChildID, 531 "file_cross_frame.html should be loaded into a different process " + 532 "than the tab containing iframe2Host" 533 ); 534 535 Assert.notEqual( 536 iframe1ChildID, 537 iframe2TabChildID, 538 "The initial iframe loaded by file_cross_frame.html should be " + 539 "loaded into a different process than the tab containing " + 540 "iframe2Host" 541 ); 542 543 // Note: this assertion depends on our process selection logic. 544 // Specifically, that we reuse an existing process for an iframe if 545 // possible. 546 Assert.equal( 547 iframe1TabChildID, 548 iframe1ChildID, 549 "Both pages loaded in iframe1Host should be in the same process" 550 ); 551 } 552 553 // Do a cross-origin navigation in the iframe in the foreground tab. 554 let iframe2URI = iframe2Host + "/browser/dom/ipc/tests/file_dummy.html"; 555 let loaded = BrowserTestUtils.browserLoaded(browser, true, iframe2URI); 556 await SpecialPowers.spawn( 557 iframe, 558 [iframe2URI], 559 async function (_iframe2URI) { 560 content.location = _iframe2URI; 561 } 562 ); 563 await loaded; 564 565 let iframe2ChildID = browsingContextChildID(iframe); 566 let iframe1Priority = gTabPriorityWatcher.currentPriority(iframe1ChildID); 567 let iframe2Priority = gTabPriorityWatcher.currentPriority(iframe2ChildID); 568 569 if (SpecialPowers.useRemoteSubframes) { 570 // Basic process uniqueness check for the state after navigating the 571 // iframe. There's no need to check the top level pages because they 572 // have not navigated. 573 // 574 // iframe1ChildID != iframe2ChildID is implied by: 575 // iframe1ChildID != iframe2TabChildID 576 // iframe2TabChildID == iframe2ChildID 577 // 578 // iframe2ChildID != topChildID is implied by: 579 // topChildID != iframe2TabChildID 580 // iframe2TabChildID == iframe2ChildID 581 582 // Note: this assertion depends on our process selection logic. 583 // Specifically, that we reuse an existing process for an iframe if 584 // possible. If that changes, this test may need to be carefully 585 // rewritten, as the whole point of the test is to check what happens 586 // with the priority manager when an iframe shares a process with 587 // a page in another tab. 588 Assert.equal( 589 iframe2TabChildID, 590 iframe2ChildID, 591 "Both pages loaded in iframe2Host should be in the same process" 592 ); 593 594 // Now that we've established the relationship between the various 595 // processes, we can finally check that the priority manager is doing 596 // the right thing. 597 Assert.equal( 598 iframe1Priority, 599 PROCESS_PRIORITY_BACKGROUND, 600 "The old iframe process should have been deprioritized" 601 ); 602 } else { 603 Assert.equal( 604 iframe1ChildID, 605 iframe2ChildID, 606 "Navigation should not have switched processes" 607 ); 608 } 609 610 Assert.equal( 611 iframe2Priority, 612 PROCESS_PRIORITY_FOREGROUND, 613 "The new iframe process should be prioritized" 614 ); 615 } 616 ); 617 618 await BrowserTestUtils.removeTab(iframe2Tab); 619 await BrowserTestUtils.removeTab(iframe1Tab); 620 }); 621 622 /** 623 * Test that a cross-group navigation properly preserves the process priority. 624 * The goal of this test is to check that the code related to mPriorityActive in 625 * CanonicalBrowsingContext::ReplacedBy works correctly, but in practice the 626 * prioritization code in SetRenderLayers will also make this test pass, though 627 * that prioritization happens slightly later. 628 */ 629 add_task(async function test_cross_group_navigate() { 630 // This page is same-site with the page we're going to cross-group navigate to. 631 let coopPage = 632 "https://example.com/browser/dom/tests/browser/file_coop_coep.html"; 633 634 // Load it as a top level tab so that we don't accidentally get the initial 635 // load prioritization. 636 let backgroundTab = await BrowserTestUtils.openNewForegroundTab( 637 gBrowser, 638 coopPage 639 ); 640 let backgroundTabChildID = browsingContextChildID( 641 gBrowser.selectedBrowser.browsingContext 642 ); 643 644 Assert.equal( 645 gTabPriorityWatcher.currentPriority(backgroundTabChildID), 646 PROCESS_PRIORITY_FOREGROUND, 647 "Loading a new tab should make it prioritized" 648 ); 649 650 await BrowserTestUtils.withNewTab( 651 "https://example.org/browser/dom/ipc/tests/file_cross_frame.html", 652 async browser => { 653 Assert.equal( 654 gTabPriorityWatcher.currentPriority(backgroundTabChildID), 655 PROCESS_PRIORITY_BACKGROUND, 656 "Switching to a new tab should deprioritize the old one" 657 ); 658 659 let dotOrgChildID = browsingContextChildID(browser.browsingContext); 660 661 // Do a cross-group navigation. 662 BrowserTestUtils.startLoadingURIString(browser, coopPage); 663 await BrowserTestUtils.browserLoaded(browser); 664 665 let coopChildID = browsingContextChildID(browser.browsingContext); 666 let coopPriority = gTabPriorityWatcher.currentPriority(coopChildID); 667 let dotOrgPriority = gTabPriorityWatcher.currentPriority(dotOrgChildID); 668 669 Assert.equal( 670 backgroundTabChildID, 671 coopChildID, 672 "The same site should get loaded into the same process" 673 ); 674 Assert.notEqual( 675 dotOrgChildID, 676 coopChildID, 677 "Navigation should have switched processes" 678 ); 679 Assert.equal( 680 dotOrgPriority, 681 PROCESS_PRIORITY_BACKGROUND, 682 "The old page process should have been deprioritized" 683 ); 684 Assert.equal( 685 coopPriority, 686 PROCESS_PRIORITY_FOREGROUND, 687 "The new page process should be prioritized" 688 ); 689 } 690 ); 691 692 await BrowserTestUtils.removeTab(backgroundTab); 693 }); 694 695 /** 696 * Test that if a tab with video goes into the background, 697 * it has its process priority lowered to 698 * PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE if it has no audio, 699 * and that it has its priority remain at 700 * PROCESS_PRIORITY_FOREGROUND if it does have audio. 701 */ 702 add_task(async function test_video_background_tab() { 703 let originalTab = gBrowser.selectedTab; 704 705 await BrowserTestUtils.withNewTab("https://example.com", async browser => { 706 // Let's load up a video in the tab, but mute it, so that this tab should 707 // reach PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE. We need to wait for the 708 // wakelock changes from the unmuting to get back up to the parent. 709 await SpecialPowers.spawn(browser, [], async () => { 710 let video = content.document.createElement("video"); 711 video.src = "https://example.net/browser/dom/ipc/tests/short.mp4"; 712 video.muted = true; 713 content.document.body.appendChild(video); 714 // We'll loop the video to avoid it ending before the test is done. 715 video.loop = true; 716 await video.play(); 717 }); 718 await Promise.all([ 719 waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { 720 needLock: false, 721 }), 722 waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { 723 needLock: true, 724 isForegroundLock: true, 725 }), 726 ]); 727 728 let tab = gBrowser.getTabForBrowser(browser); 729 730 // The tab with the muted video should reach 731 // PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE when backgrounded. 732 await assertPriorityChangeOnBackground({ 733 fromTab: tab, 734 toTab: originalTab, 735 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE, 736 }); 737 738 // Now switch back. The initial blank tab should reach 739 // PROCESS_PRIORITY_BACKGROUND when backgrounded. 740 await assertPriorityChangeOnBackground({ 741 fromTab: originalTab, 742 toTab: tab, 743 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 744 }); 745 746 // Let's unmute the video now. We need to wait for the wakelock change from 747 // the unmuting to get back up to the parent. 748 await Promise.all([ 749 waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { 750 needLock: true, 751 isForegroundLock: true, 752 }), 753 SpecialPowers.spawn(browser, [], async () => { 754 let video = content.document.querySelector("video"); 755 video.muted = false; 756 }), 757 ]); 758 759 // The tab with the unmuted video should stay at 760 // PROCESS_PRIORITY_FOREGROUND when backgrounded. 761 await assertPriorityChangeOnBackground({ 762 fromTab: tab, 763 toTab: originalTab, 764 fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND, 765 }); 766 767 // Now switch back. The initial blank tab should reach 768 // PROCESS_PRIORITY_BACKGROUND when backgrounded. 769 await assertPriorityChangeOnBackground({ 770 fromTab: originalTab, 771 toTab: tab, 772 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 773 }); 774 }); 775 }); 776 777 /** 778 * Test that if a tab with a playing <audio> element goes into 779 * the background, the process priority does not change, unless 780 * that audio is muted (in which case, it reaches 781 * PROCESS_PRIORITY_BACKGROUND). 782 */ 783 add_task(async function test_audio_background_tab() { 784 let originalTab = gBrowser.selectedTab; 785 786 await BrowserTestUtils.withNewTab("https://example.com", async browser => { 787 // Let's load up some audio in the tab, but mute it, so that this tab should 788 // reach PROCESS_PRIORITY_BACKGROUND. 789 await SpecialPowers.spawn(browser, [], async () => { 790 let audio = content.document.createElement("audio"); 791 audio.src = "https://example.net/browser/dom/ipc/tests/owl.mp3"; 792 audio.muted = true; 793 content.document.body.appendChild(audio); 794 // We'll loop the audio to avoid it ending before the test is done. 795 audio.loop = true; 796 await audio.play(); 797 }); 798 799 let tab = gBrowser.getTabForBrowser(browser); 800 801 // The tab with the muted audio should reach 802 // PROCESS_PRIORITY_BACKGROUND when backgrounded. 803 await assertPriorityChangeOnBackground({ 804 fromTab: tab, 805 toTab: originalTab, 806 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 807 }); 808 809 // Now switch back. The initial blank tab should reach 810 // PROCESS_PRIORITY_BACKGROUND when backgrounded. 811 await assertPriorityChangeOnBackground({ 812 fromTab: originalTab, 813 toTab: tab, 814 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 815 }); 816 817 // Now unmute the audio. Unfortuntely, there's a bit of a race here, 818 // since the wakelock on the audio element is released and then 819 // re-acquired if the audio reaches its end and loops around. This 820 // will cause an unexpected priority change on the content process. 821 // 822 // To avoid this race, we'll seek the audio back to the beginning, 823 // and lower its playback rate to the minimum to increase the 824 // likelihood that the check completes before the audio loops around. 825 await SpecialPowers.spawn(browser, [], async () => { 826 let audio = content.document.querySelector("audio"); 827 let seeked = ContentTaskUtils.waitForEvent(audio, "seeked"); 828 audio.muted = false; 829 // 0.25 is the minimum playback rate that still keeps the audio audible. 830 audio.playbackRate = 0.25; 831 audio.currentTime = 0; 832 await seeked; 833 }); 834 835 // The tab with the unmuted audio should stay at 836 // PROCESS_PRIORITY_FOREGROUND when backgrounded. 837 await assertPriorityChangeOnBackground({ 838 fromTab: tab, 839 toTab: originalTab, 840 fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND, 841 }); 842 843 // Now switch back. The initial blank tab should reach 844 // PROCESS_PRIORITY_BACKGROUND when backgrounded. 845 await assertPriorityChangeOnBackground({ 846 fromTab: originalTab, 847 toTab: tab, 848 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 849 }); 850 }); 851 }); 852 853 /** 854 * Test that if a tab with a WebAudio playing goes into the background, 855 * the process priority does not change, unless that WebAudio context is 856 * suspended. 857 */ 858 add_task(async function test_web_audio_background_tab() { 859 let originalTab = gBrowser.selectedTab; 860 861 await BrowserTestUtils.withNewTab("https://example.com", async browser => { 862 // Let's synthesize a basic square wave as WebAudio. 863 await SpecialPowers.spawn(browser, [], async () => { 864 let audioCtx = new content.AudioContext(); 865 let oscillator = audioCtx.createOscillator(); 866 oscillator.type = "square"; 867 oscillator.frequency.setValueAtTime(440, audioCtx.currentTime); 868 oscillator.connect(audioCtx.destination); 869 oscillator.start(); 870 while (audioCtx.state != "running") { 871 info(`wait until AudioContext starts running`); 872 await new Promise(r => (audioCtx.onstatechange = r)); 873 } 874 // we'll stash the AudioContext away so that it's easier to access 875 // in the next SpecialPowers.spawn. 876 content.audioCtx = audioCtx; 877 }); 878 879 let tab = gBrowser.getTabForBrowser(browser); 880 881 // The tab with the WebAudio should stay at 882 // PROCESS_PRIORITY_FOREGROUND when backgrounded. 883 await assertPriorityChangeOnBackground({ 884 fromTab: tab, 885 toTab: originalTab, 886 fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND, 887 }); 888 889 // Now switch back. The initial blank tab should reach 890 // PROCESS_PRIORITY_BACKGROUND when backgrounded. 891 await assertPriorityChangeOnBackground({ 892 fromTab: originalTab, 893 toTab: tab, 894 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 895 }); 896 897 // Now suspend the WebAudio. This will cause it to stop 898 // playing. 899 await SpecialPowers.spawn(browser, [], async () => { 900 content.audioCtx.suspend(); 901 }); 902 903 // The tab with the suspended WebAudio should reach 904 // PROCESS_PRIORITY_BACKGROUND when backgrounded. 905 await assertPriorityChangeOnBackground({ 906 fromTab: tab, 907 toTab: originalTab, 908 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 909 }); 910 911 // Now switch back. The initial blank tab should reach 912 // PROCESS_PRIORITY_BACKGROUND when backgrounded. 913 await assertPriorityChangeOnBackground({ 914 fromTab: originalTab, 915 toTab: tab, 916 fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND, 917 }); 918 }); 919 }); 920 921 /** 922 * Test that foreground tab's process priority isn't changed when going back to 923 * a bfcached session history entry. 924 */ 925 add_task(async function test_audio_background_tab() { 926 let page1 = "https://example.com"; 927 let page2 = page1 + "/?2"; 928 929 await BrowserTestUtils.withNewTab(page1, async browser => { 930 let childID = browsingContextChildID(browser.browsingContext); 931 Assert.equal( 932 gTabPriorityWatcher.currentPriority(childID), 933 PROCESS_PRIORITY_FOREGROUND, 934 "Loading a new tab should make it prioritized." 935 ); 936 let loaded = BrowserTestUtils.browserLoaded(browser, false, page2); 937 BrowserTestUtils.startLoadingURIString(browser, page2); 938 await loaded; 939 940 childID = browsingContextChildID(browser.browsingContext); 941 Assert.equal( 942 gTabPriorityWatcher.currentPriority(childID), 943 PROCESS_PRIORITY_FOREGROUND, 944 "Loading a new page should keep the tab prioritized." 945 ); 946 947 let pageShowPromise = BrowserTestUtils.waitForContentEvent( 948 browser, 949 "pageshow" 950 ); 951 browser.goBack(); 952 await pageShowPromise; 953 954 childID = browsingContextChildID(browser.browsingContext); 955 Assert.equal( 956 gTabPriorityWatcher.currentPriority(childID), 957 PROCESS_PRIORITY_FOREGROUND, 958 "Loading a page from the bfcache should keep the tab prioritized." 959 ); 960 }); 961 }); 962 963 /** 964 * Test that if a normal tab gets moved to a new window, it gets 965 * PROCESS_PRIORITY_FOREGROUND (since it is the only tab in that window) 966 * See bug 1896172. 967 */ 968 add_task(async function test_tab_moved_to_new_window() { 969 await BrowserTestUtils.withNewTab( 970 "https://example.com/browser/dom/ipc/tests/file_cross_frame.html", 971 async browser => { 972 let tab = gBrowser.getTabForBrowser(browser); 973 let tabID = browsingContextChildID(tab.linkedBrowser.browsingContext); 974 let delayedStartupPromise = BrowserTestUtils.waitForNewWindow(); 975 let win = gBrowser.replaceTabWithWindow(tab); 976 await delayedStartupPromise; 977 // The bug was caused by the closing of the original tab, so wait 978 // for that to happen before checking the priority. 979 await TestUtils.waitForCondition(() => gBrowser.tabs.length === 1); 980 Assert.equal( 981 gTabPriorityWatcher.currentPriority(tabID), 982 PROCESS_PRIORITY_FOREGROUND, 983 "Tab should be in foreground after moving to new window" 984 ); 985 986 await BrowserTestUtils.closeWindow(win); 987 } 988 ); 989 }); 990 991 /** 992 * Test that if a tab is quickly switched away from and back to, it ends up at 993 * PROCESS_PRIORITY_FOREGROUND. 994 * See bug 1927609. 995 */ 996 add_task(async function test_tab_quickly_switched() { 997 let originalTab = gBrowser.selectedTab; 998 let origtabID = browsingContextChildID( 999 originalTab.linkedBrowser.browsingContext 1000 ); 1001 1002 await BrowserTestUtils.withNewTab( 1003 "https://example.com/browser/dom/ipc/tests/file_dummy.html", 1004 async browser => { 1005 let tab = gBrowser.getTabForBrowser(browser); 1006 let tabID = browsingContextChildID(tab.linkedBrowser.browsingContext); 1007 1008 // Don't use BrowserTestUtils.switchTab() because things have settled 1009 // by the time it's done, which doesn't expose this bug. 1010 gBrowser.selectedTab = originalTab; 1011 gBrowser.selectedTab = tab; 1012 await new Promise(resolve => 1013 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 1014 setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS) 1015 ); 1016 Assert.equal( 1017 gTabPriorityWatcher.currentPriority(tabID), 1018 PROCESS_PRIORITY_FOREGROUND, 1019 "Active tab should be foreground priority" 1020 ); 1021 Assert.equal( 1022 gTabPriorityWatcher.currentPriority(origtabID), 1023 PROCESS_PRIORITY_BACKGROUND, 1024 "Inactive tab should be background priority" 1025 ); 1026 } 1027 ); 1028 });