Bug 675539 - Unload tabs in low-memory scenarios r=mconley
authorGabriele Svelto <gsvelto@mozilla.com>
Wed, 27 Feb 2019 22:24:27 +0000
changeset 519427 a836b30ac0705082c7156307a3e54248507851f9
parent 519426 60a25e3f7b506d87e9d6dcd9b171de8e558bdb1e
child 519428 2f27f2c7a443b3c798c0722379328e6ba6c3ef5d
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs675539
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 675539 - Unload tabs in low-memory scenarios r=mconley This adds a mechanism that discards tabs when the browser detects a low-memory scenario. Tabs are discarded in LRU order prioritizing regular tabs over pinned ones, pinned ones over tabs playing audio and all of the previous over pinned tabs playing audio. Differential Revision: https://phabricator.services.mozilla.com/D20476
browser/app/profile/firefox.js
browser/components/BrowserGlue.jsm
browser/modules/TabUnloader.jsm
browser/modules/moz.build
browser/modules/test/browser/browser.ini
browser/modules/test/browser/browser_TabUnloader.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -475,16 +475,23 @@ pref("browser.tabs.newanimations", false
 // Pref to control whether we use separate privileged content processes.
 #if defined(NIGHTLY_BUILD) && !defined(MOZ_ASAN)
 pref("browser.tabs.remote.separatePrivilegedContentProcess", true);
 #endif
 
 // Turn on HTTP response process selection.
 pref("browser.tabs.remote.useHTTPResponseProcessSelection", true);
 
+// Unload tabs on low-memory on nightly.
+#ifdef RELEASE_OR_BETA
+pref("browser.tabs.unloadOnLowMemory", false);
+#else
+pref("browser.tabs.unloadOnLowMemory", true);
+#endif
+
 pref("browser.ctrlTab.recentlyUsedOrder", true);
 
 // By default, do not export HTML at shutdown.
 // If true, at shutdown the bookmarks in your menu and toolbar will
 // be exported as HTML to the bookmarks.html file.
 pref("browser.bookmarks.autoExportHTML",          false);
 
 // The maximum number of daily bookmark backups to
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -428,16 +428,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SaveToPocket: "chrome://pocket/content/SaveToPocket.jsm",
   SearchTelemetry: "resource:///modules/SearchTelemetry.jsm",
   SessionStartup: "resource:///modules/sessionstore/SessionStartup.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   ShellService: "resource:///modules/ShellService.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
+  TabUnloader: "resource:///modules/TabUnloader.jsm",
   UIState: "resource://services-sync/UIState.jsm",
   UITour: "resource:///modules/UITour.jsm",
   WebChannel: "resource://gre/modules/WebChannel.jsm",
   WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
 });
 
 // eslint-disable-next-line no-unused-vars
 XPCOMUtils.defineLazyModuleGetters(this, {
@@ -1709,16 +1710,20 @@ BrowserGlue.prototype = {
       Blocklist.loadBlocklistAsync();
     });
 
     if (Services.prefs.getIntPref("browser.livebookmarks.migrationAttemptsLeft", 0) > 0) {
       Services.tm.idleDispatchToMainThread(() => {
         LiveBookmarkMigrator.migrate().catch(Cu.reportError);
       });
     }
+
+    Services.tm.idleDispatchToMainThread(() => {
+      TabUnloader.init();
+    });
   },
 
   /**
    * Use this function as an entry point to schedule tasks that need
    * to run once per session, at any arbitrary point in time.
    * This function will be called from an idle observer. Check the value of
    * LATE_TASKS_IDLE_TIME_SEC to see the current value for this idle
    * observer.
new file mode 100644
--- /dev/null
+++ b/browser/modules/TabUnloader.jsm
@@ -0,0 +1,76 @@
+/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TabUnloader"];
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * This module is responsible for detecting low-memory scenarios and unloading
+ * tabs in response to them.
+ */
+
+var TabUnloader = {
+  /**
+   * Initialize low-memory detection and tab auto-unloading.
+   */
+  init() {
+    if (Services.prefs.getBoolPref("browser.tabs.unloadOnLowMemory", true)) {
+      Services.obs.addObserver(this, "memory-pressure", /* ownsWeak */ true);
+    }
+  },
+
+  observe(subject, topic, data) {
+    if (topic == "memory-pressure" && data != "heap-minimize") {
+      unloadLeastRecentlyUsedTab();
+    }
+  },
+
+  QueryInterface: ChromeUtils.generateQI([
+    Ci.nsIObserver,
+    Ci.nsISupportsWeakReference,
+  ]),
+};
+
+function unloadLeastRecentlyUsedTab() {
+  let bgTabBrowsers = getSortedBackgroundTabBrowsers();
+
+  for (let tb of bgTabBrowsers) {
+    if (tb.browser.discardBrowser(tb.tab)) {
+      return;
+    }
+  }
+}
+
+/* Sort tabs in the order we use for unloading, first non-pinned, non-audible
+ * tabs in LRU order, then non-audible tabs in LRU order, then non-pinned
+ * audible tabs in LRU order and finally pinned, audible tabs in LRU order. */
+function sortTabs(a, b) {
+  if (a.tab.soundPlaying != b.tab.soundPlaying) {
+    return a.tab.soundPlaying - b.tab.soundPlaying;
+  }
+
+  if (a.tab.pinned != b.tab.pinned) {
+    return a.tab.pinned - b.tab.pinned;
+  }
+
+  return a.tab.lastAccessed - b.tab.lastAccessed;
+}
+
+function getSortedBackgroundTabBrowsers() {
+  let bgTabBrowsers = [];
+
+  for (let win of Services.wm.getEnumerator("navigator:browser")) {
+    for (let tab of win.gBrowser.tabs) {
+      if (!tab.selected && tab.linkedBrowser.isConnected) {
+        bgTabBrowsers.push({ tab, browser: win.gBrowser });
+      }
+    }
+  }
+
+  return bgTabBrowsers.sort(sortTabs);
+}
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -146,16 +146,17 @@ EXTRA_JS_MODULES += [
     'ProcessHangMonitor.jsm',
     'ReaderParent.jsm',
     'RemotePrompt.jsm',
     'Sanitizer.jsm',
     'SelectionChangedMenulist.jsm',
     'SiteDataManager.jsm',
     'SitePermissions.jsm',
     'TabsList.jsm',
+    'TabUnloader.jsm',
     'ThemeVariableMap.jsm',
     'TransientPrefs.jsm',
     'webrtcUI.jsm',
     'ZoomUI.jsm',
 ]
 
 if CONFIG['MOZ_ASAN_REPORTER']:
     EXTRA_JS_MODULES += [
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -19,16 +19,21 @@ support-files =
 [browser_PermissionUI.js]
 [browser_PermissionUI_prompts.js]
 [browser_ProcessHangNotifications.js]
 skip-if = !e10s
 [browser_SitePermissions.js]
 [browser_SitePermissions_combinations.js]
 [browser_SitePermissions_expiry.js]
 [browser_SitePermissions_tab_urls.js]
+[browser_TabUnloader.js]
+support-files =
+  ../../../base/content/test/tabs/dummy_page.html
+  ../../../base/content/test/tabs/file_mediaPlayback.html
+  ../../../base/content/test/general/audio.ogg
 [browser_taskbar_preview.js]
 skip-if = os != win || (os == win && bits == 64) # bug 1456807
 [browser_UnsubmittedCrashHandler.js]
 run-if = crashreporter
 [browser_urlBar_zoom.js]
 [browser_UsageTelemetry.js]
 [browser_UsageTelemetry_domains.js]
 [browser_UsageTelemetry_private_and_restore.js]
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/browser_TabUnloader.js
@@ -0,0 +1,106 @@
+/* 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 = "http://example.com/browser/browser/modules/test/browser/";
+
+async function play(tab) {
+  let browser = tab.linkedBrowser;
+  await ContentTask.spawn(browser, {}, async function() {
+    let audio = content.document.querySelector("audio");
+    await audio.play();
+  });
+}
+
+async function addTab() {
+  return BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    url: BASE_URL + "dummy_page.html",
+    waitForLoad: true,
+  });
+}
+
+async function addAudioTab() {
+  let tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    url: BASE_URL + "file_mediaPlayback.html",
+    waitForLoad: true,
+    waitForStateStop: true,
+  });
+
+  await play(tab);
+  return tab;
+}
+
+add_task(async function test() {
+  // 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);
+
+  // Pretend we've visited the tabs
+  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");
+
+  // Check that the tabs are present
+  ok(tab1.linkedPanel && tab2.linkedPanel && pinnedTab.linkedPanel &&
+     soundTab.linkedPanel && pinnedSoundTab.linkedPanel, "tabs are present");
+
+  // Check that heap-minimize memory-pressure events do not unload tabs
+  TabUnloader.observe(null, "memory-pressure", "heap-minimize");
+  ok(tab1.linkedPanel && tab2.linkedPanel && pinnedTab.linkedPanel &&
+     soundTab.linkedPanel && pinnedSoundTab.linkedPanel,
+     "heap-minimize memory-pressure notification did not unload a tab");
+
+  // Check that low-memory memory-pressure events unload tabs
+  TabUnloader.observe(null, "memory-pressure", "low-memory");
+  ok(!tab1.linkedPanel,
+     "low-memory memory-pressure notification unloaded the LRU tab");
+
+  // If no normal tab is available unload pinned tabs
+  TabUnloader.observe(null, "memory-pressure", "low-memory");
+  ok(!tab2.linkedPanel, "unloaded a second tab in LRU order");
+  TabUnloader.observe(null, "memory-pressure", "low-memory");
+  ok(!pinnedTab.linkedPanel, "unloaded a pinned tab");
+
+  // If no pinned tab is available unload tabs playing sound
+  TabUnloader.observe(null, "memory-pressure", "low-memory");
+  ok(!soundTab.linkedPanel, "unloaded a tab playing sound");
+
+  // If no pinned tab or tab playing sound is available unload tabs that are
+  // both pinned and playing sound
+  TabUnloader.observe(null, "memory-pressure", "low-memory");
+  ok(!pinnedSoundTab.linkedPanel, "unloaded a pinned tab playing sound");
+
+  // Check low-memory-ongoing events
+  await BrowserTestUtils.switchTab(gBrowser, tab1);
+  await BrowserTestUtils.switchTab(gBrowser, tab0);
+  TabUnloader.observe(null, "memory-pressure", "low-memory-ongoing");
+  ok(!tab1.linkedPanel,
+     "low-memory memory-pressure notification unloaded the LRU tab");
+
+  // Cleanup
+  BrowserTestUtils.removeTab(tab1);
+  BrowserTestUtils.removeTab(tab2);
+  BrowserTestUtils.removeTab(pinnedTab);
+  BrowserTestUtils.removeTab(soundTab);
+  BrowserTestUtils.removeTab(pinnedSoundTab);
+});
+