/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const { TabUnloader } = ChromeUtils.import( "resource:///modules/TabUnloader.jsm" ); const BASE_URL = "https://example.com/browser/browser/modules/test/browser/"; async function play(tab) { let browser = tab.linkedBrowser; let waitForAudioPromise = BrowserTestUtils.waitForEvent( tab, "TabAttrModified", false, event => { return ( event.detail.changed.includes("soundplaying") && tab.hasAttribute("soundplaying") ); } ); await SpecialPowers.spawn(browser, [], async function () { let audio = content.document.querySelector("audio"); await audio.play(); }); await waitForAudioPromise; } async function addTab(win = window) { return BrowserTestUtils.openNewForegroundTab({ gBrowser: win.gBrowser, url: BASE_URL + "dummy_page.html", waitForLoad: true, }); } async function addPrivTab(win = window) { const tab = BrowserTestUtils.addTab( win.gBrowser, BASE_URL + "dummy_page.html" ); const browser = win.gBrowser.getBrowserForTab(tab); await BrowserTestUtils.browserLoaded(browser); return tab; } async function addAudioTab(win = window) { let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser: win.gBrowser, url: BASE_URL + "file_mediaPlayback.html", waitForLoad: true, waitForStateStop: true, }); await play(tab); return tab; } async function addWebRTCTab(win = window) { let popupPromise = new Promise(resolve => { win.PopupNotifications.panel.addEventListener( "popupshown", function () { executeSoon(resolve); }, { once: true } ); }); let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser: win.gBrowser, url: BASE_URL + "file_webrtc.html", waitForLoad: true, waitForStateStop: true, }); await popupPromise; let recordingPromise = BrowserTestUtils.contentTopicObserved( tab.linkedBrowser.browsingContext, "recording-device-events" ); win.PopupNotifications.panel.firstElementChild.button.click(); await recordingPromise; return tab; } async function pressure() { let tabDiscarded = BrowserTestUtils.waitForEvent( document, "TabBrowserDiscarded", true ); TabUnloader.unloadTabAsync(null); return tabDiscarded; } function pressureAndObserve(aExpectedTopic) { const promise = new Promise(resolve => { const observer = { QueryInterface: ChromeUtils.generateQI([ "nsIObserver", "nsISupportsWeakReference", ]), observe(aSubject, aTopicInner, aData) { if (aTopicInner == aExpectedTopic) { Services.obs.removeObserver(observer, aTopicInner); resolve(aData); } }, }; Services.obs.addObserver(observer, aExpectedTopic); }); TabUnloader.unloadTabAsync(null); return promise; } async function compareTabOrder(expectedOrder) { let tabInfo = await TabUnloader.getSortedTabs(null); is( tabInfo.length, expectedOrder.length, "right number of tabs in discard sort list" ); for (let idx = 0; idx < expectedOrder.length; idx++) { is(tabInfo[idx].tab, expectedOrder[idx], "index " + idx + " is correct"); } } const PREF_PERMISSION_FAKE = "media.navigator.permission.fake"; const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev"; const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev"; const PREF_FAKE_STREAMS = "media.navigator.streams.fake"; const PREF_ENABLE_UNLOADER = "browser.tabs.unloadOnLowMemory"; const PREF_MAC_LOW_MEM_RESPONSE = "browser.lowMemoryResponseMask"; add_task(async function test() { registerCleanupFunction(() => { Services.prefs.clearUserPref(PREF_ENABLE_UNLOADER); if (AppConstants.platform == "macosx") { Services.prefs.clearUserPref(PREF_MAC_LOW_MEM_RESPONSE); } }); Services.prefs.setBoolPref(PREF_ENABLE_UNLOADER, true); // On Mac, tab unloading and memory pressure notifications are limited // to Nightly so force them on for this test for non-Nightly builds. i.e., // tests on Release and Beta builds. Mac tab unloading and memory pressure // notifications require this pref to be set. if (AppConstants.platform == "macosx") { Services.prefs.setIntPref(PREF_MAC_LOW_MEM_RESPONSE, 3); } TabUnloader.init(); // Set some WebRTC simulation preferences. let prefs = [ [PREF_PERMISSION_FAKE, true], [PREF_AUDIO_LOOPBACK, ""], [PREF_VIDEO_LOOPBACK, ""], [PREF_FAKE_STREAMS, true], ]; await SpecialPowers.pushPrefEnv({ set: prefs }); // Set up 6 tabs, three normal ones, one pinned, one playing sound and one // pinned playing sound let tab0 = gBrowser.tabs[0]; let tab1 = await addTab(); let tab2 = await addTab(); let pinnedTab = await addTab(); gBrowser.pinTab(pinnedTab); let soundTab = await addAudioTab(); let pinnedSoundTab = await addAudioTab(); gBrowser.pinTab(pinnedSoundTab); // Open a new private window and add a tab const windowPriv = await BrowserTestUtils.openNewBrowserWindow({ private: true, }); const tabPriv0 = windowPriv.gBrowser.tabs[0]; const tabPriv1 = await addPrivTab(windowPriv); // Move the original window to the foreground to pass the tests gBrowser.selectedTab = tab0; tab0.ownerGlobal.focus(); // Pretend we've visited the tabs await BrowserTestUtils.switchTab(windowPriv.gBrowser, tabPriv1); await BrowserTestUtils.switchTab(windowPriv.gBrowser, tabPriv0); await BrowserTestUtils.switchTab(gBrowser, tab1); await BrowserTestUtils.switchTab(gBrowser, tab2); await BrowserTestUtils.switchTab(gBrowser, pinnedTab); await BrowserTestUtils.switchTab(gBrowser, soundTab); await BrowserTestUtils.switchTab(gBrowser, pinnedSoundTab); await BrowserTestUtils.switchTab(gBrowser, tab0); // Checks the tabs are in the state we expect them to be ok(pinnedTab.pinned, "tab is pinned"); ok(pinnedSoundTab.soundPlaying, "tab is playing sound"); ok( pinnedSoundTab.pinned && pinnedSoundTab.soundPlaying, "tab is pinned and playing sound" ); await compareTabOrder([ tab1, tab2, pinnedTab, tabPriv1, soundTab, tab0, pinnedSoundTab, tabPriv0, ]); // Check that the tabs are present ok( tab1.linkedPanel && tab2.linkedPanel && pinnedTab.linkedPanel && soundTab.linkedPanel && pinnedSoundTab.linkedPanel && tabPriv0.linkedPanel && tabPriv1.linkedPanel, "tabs are present" ); // Check that low-memory memory-pressure events unload tabs await pressure(); ok( !tab1.linkedPanel, "low-memory memory-pressure notification unloaded the LRU tab" ); await compareTabOrder([ tab2, pinnedTab, tabPriv1, soundTab, tab0, pinnedSoundTab, tabPriv0, ]); // If no normal tab is available unload pinned tabs await pressure(); ok(!tab2.linkedPanel, "unloaded a second tab in LRU order"); await compareTabOrder([ pinnedTab, tabPriv1, soundTab, tab0, pinnedSoundTab, tabPriv0, ]); ok(soundTab.soundPlaying, "tab is still playing sound"); await pressure(); ok(!pinnedTab.linkedPanel, "unloaded a pinned tab"); await compareTabOrder([tabPriv1, soundTab, tab0, pinnedSoundTab, tabPriv0]); ok(pinnedSoundTab.soundPlaying, "tab is still playing sound"); // There are no unloadable tabs. TabUnloader.unloadTabAsync(null); ok(tabPriv1.linkedPanel, "a tab in a private window is never unloaded"); const histogram = TelemetryTestUtils.getAndClearHistogram( "TAB_UNLOAD_TO_RELOAD" ); // It's possible that we're already in the memory-pressure state // and we may receive the "ongoing" message. const message = await pressureAndObserve("memory-pressure"); Assert.ok( message == "low-memory" || message == "low-memory-ongoing", "observed the memory-pressure notification because of no discardable tab" ); // Add a WebRTC tab and another sound tab. let webrtcTab = await addWebRTCTab(); let anotherSoundTab = await addAudioTab(); await BrowserTestUtils.switchTab(gBrowser, tab1); await BrowserTestUtils.switchTab(gBrowser, pinnedTab); const hist = histogram.snapshot(); const numEvents = Object.values(hist.values).reduce((a, b) => a + b); Assert.equal(numEvents, 2, "two tabs have been reloaded."); // tab0 has never been unloaded. No data is added to the histogram. await BrowserTestUtils.switchTab(gBrowser, tab0); await compareTabOrder([ tab1, pinnedTab, tabPriv1, soundTab, webrtcTab, anotherSoundTab, tab0, pinnedSoundTab, tabPriv0, ]); await BrowserTestUtils.closeWindow(windowPriv); let window2 = await BrowserTestUtils.openNewBrowserWindow(); let win2tab1 = window2.gBrowser.selectedTab; let win2tab2 = await addTab(window2); let win2winrtcTab = await addWebRTCTab(window2); let win2tab3 = await addTab(window2); await compareTabOrder([ tab1, win2tab1, win2tab2, pinnedTab, soundTab, webrtcTab, anotherSoundTab, win2winrtcTab, tab0, win2tab3, pinnedSoundTab, ]); await BrowserTestUtils.closeWindow(window2); await compareTabOrder([ tab1, pinnedTab, soundTab, webrtcTab, anotherSoundTab, tab0, pinnedSoundTab, ]); // Cleanup BrowserTestUtils.removeTab(tab1); BrowserTestUtils.removeTab(tab2); BrowserTestUtils.removeTab(pinnedTab); BrowserTestUtils.removeTab(soundTab); BrowserTestUtils.removeTab(pinnedSoundTab); BrowserTestUtils.removeTab(webrtcTab); BrowserTestUtils.removeTab(anotherSoundTab); await awaitWebRTCClose(); }); // Wait for the WebRTC indicator window to close. function awaitWebRTCClose() { if ( Services.prefs.getBoolPref("privacy.webrtc.legacyGlobalIndicator", false) || AppConstants.platform == "macosx" ) { return null; } let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator"); if (!win) { return null; } return new Promise(resolve => { win.addEventListener("unload", function listener(e) { if (e.target == win.document) { win.removeEventListener("unload", listener); executeSoon(resolve); } }); }); }