forked from mirrors/gecko-dev
		
	 2f419065da
			
		
	
	
		2f419065da
		
	
	
	
	
		
			
			This is a start, there are more modules and scripts we can/should move, as well as tests. Differential Revision: https://phabricator.services.mozilla.com/D210676
		
			
				
	
	
		
			515 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			515 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- 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/. */
 | |
| 
 | |
| /*
 | |
|  * TabUnloader is used to discard tabs when memory or resource constraints
 | |
|  * are reached. The discarded tabs are determined using a heuristic that
 | |
|  * accounts for when the tab was last used, how many resources the tab uses,
 | |
|  * and whether the tab is likely to affect the user if it is closed.
 | |
|  */
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
|   webrtcUI: "resource:///modules/webrtcUI.sys.mjs",
 | |
| });
 | |
| 
 | |
| // If there are only this many or fewer tabs open, just sort by weight, and close
 | |
| // the lowest tab. Otherwise, do a more intensive compuation that determines the
 | |
| // tabs to close based on memory and process use.
 | |
| const MIN_TABS_COUNT = 10;
 | |
| 
 | |
| // Weight for non-discardable tabs.
 | |
| const NEVER_DISCARD = 100000;
 | |
| 
 | |
| // Default minimum inactive duration.  Tabs that were accessed in the last
 | |
| // period of this duration are not unloaded.
 | |
| const kMinInactiveDurationInMs = Services.prefs.getIntPref(
 | |
|   "browser.tabs.min_inactive_duration_before_unload"
 | |
| );
 | |
| 
 | |
| let criteriaTypes = [
 | |
|   ["isNonDiscardable", NEVER_DISCARD],
 | |
|   ["isLoading", 8],
 | |
|   ["usingPictureInPicture", NEVER_DISCARD],
 | |
|   ["playingMedia", NEVER_DISCARD],
 | |
|   ["usingWebRTC", NEVER_DISCARD],
 | |
|   ["isPinned", 2],
 | |
|   ["isPrivate", NEVER_DISCARD],
 | |
| ];
 | |
| 
 | |
| // Indicies into the criteriaTypes lists.
 | |
| let CRITERIA_METHOD = 0;
 | |
| let CRITERIA_WEIGHT = 1;
 | |
| 
 | |
| /**
 | |
|  * This is an object that supplies methods that determine details about
 | |
|  * each tab. This default object is used if another one is not passed
 | |
|  * to the tab unloader functions. This allows tests to override the methods
 | |
|  * with tab specific data rather than creating test tabs.
 | |
|  */
 | |
| let DefaultTabUnloaderMethods = {
 | |
|   isNonDiscardable(tab, weight) {
 | |
|     if (tab.undiscardable || tab.selected) {
 | |
|       return weight;
 | |
|     }
 | |
| 
 | |
|     return !tab.linkedBrowser.isConnected ? -1 : 0;
 | |
|   },
 | |
| 
 | |
|   isPinned(tab, weight) {
 | |
|     return tab.pinned ? weight : 0;
 | |
|   },
 | |
| 
 | |
|   isLoading() {
 | |
|     return 0;
 | |
|   },
 | |
| 
 | |
|   usingPictureInPicture(tab, weight) {
 | |
|     // This has higher weight even when paused.
 | |
|     return tab.pictureinpicture ? weight : 0;
 | |
|   },
 | |
| 
 | |
|   playingMedia(tab, weight) {
 | |
|     return tab.soundPlaying ? weight : 0;
 | |
|   },
 | |
| 
 | |
|   usingWebRTC(tab, weight) {
 | |
|     const browser = tab.linkedBrowser;
 | |
|     if (!browser) {
 | |
|       return 0;
 | |
|     }
 | |
| 
 | |
|     // No need to iterate browser contexts for hasActivePeerConnection
 | |
|     // because hasActivePeerConnection is set only in the top window.
 | |
|     return lazy.webrtcUI.browserHasStreams(browser) ||
 | |
|       browser.browsingContext?.currentWindowGlobal?.hasActivePeerConnections()
 | |
|       ? weight
 | |
|       : 0;
 | |
|   },
 | |
| 
 | |
|   isPrivate(tab, weight) {
 | |
|     return lazy.PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser)
 | |
|       ? weight
 | |
|       : 0;
 | |
|   },
 | |
| 
 | |
|   getMinTabCount() {
 | |
|     return MIN_TABS_COUNT;
 | |
|   },
 | |
| 
 | |
|   getNow() {
 | |
|     return Date.now();
 | |
|   },
 | |
| 
 | |
|   *iterateTabs() {
 | |
|     for (let win of Services.wm.getEnumerator("navigator:browser")) {
 | |
|       for (let tab of win.gBrowser.tabs) {
 | |
|         yield { tab, gBrowser: win.gBrowser };
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   *iterateBrowsingContexts(bc) {
 | |
|     yield bc;
 | |
|     for (let childBC of bc.children) {
 | |
|       yield* this.iterateBrowsingContexts(childBC);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   *iterateProcesses(tab) {
 | |
|     let bc = tab?.linkedBrowser?.browsingContext;
 | |
|     if (!bc) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const iter = this.iterateBrowsingContexts(bc);
 | |
|     for (let childBC of iter) {
 | |
|       if (childBC?.currentWindowGlobal) {
 | |
|         yield childBC.currentWindowGlobal.osPid;
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add the amount of memory used by each process to the process map.
 | |
|    *
 | |
|    * @param tabs array of tabs, used only by unit tests
 | |
|    * @param map of processes returned by getAllProcesses.
 | |
|    */
 | |
|   async calculateMemoryUsage(processMap) {
 | |
|     let parentProcessInfo = await ChromeUtils.requestProcInfo();
 | |
|     let childProcessInfoList = parentProcessInfo.children;
 | |
|     for (let childProcInfo of childProcessInfoList) {
 | |
|       let processInfo = processMap.get(childProcInfo.pid);
 | |
|       if (!processInfo) {
 | |
|         processInfo = { count: 0, topCount: 0, tabSet: new Set() };
 | |
|         processMap.set(childProcInfo.pid, processInfo);
 | |
|       }
 | |
|       processInfo.memory = childProcInfo.memory;
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * This module is responsible for detecting low-memory scenarios and unloading
 | |
|  * tabs in response to them.
 | |
|  */
 | |
| 
 | |
| export var TabUnloader = {
 | |
|   /**
 | |
|    * Initialize low-memory detection and tab auto-unloading.
 | |
|    */
 | |
|   init() {
 | |
|     const watcher = Cc["@mozilla.org/xpcom/memory-watcher;1"].getService(
 | |
|       Ci.nsIAvailableMemoryWatcherBase
 | |
|     );
 | |
|     watcher.registerTabUnloader(this);
 | |
|   },
 | |
| 
 | |
|   isDiscardable(tab) {
 | |
|     if (!("weight" in tab)) {
 | |
|       return false;
 | |
|     }
 | |
|     return tab.weight < NEVER_DISCARD;
 | |
|   },
 | |
| 
 | |
|   // This method is exposed on nsITabUnloader
 | |
|   async unloadTabAsync(minInactiveDuration = kMinInactiveDurationInMs) {
 | |
|     const watcher = Cc["@mozilla.org/xpcom/memory-watcher;1"].getService(
 | |
|       Ci.nsIAvailableMemoryWatcherBase
 | |
|     );
 | |
| 
 | |
|     if (!Services.prefs.getBoolPref("browser.tabs.unloadOnLowMemory", true)) {
 | |
|       watcher.onUnloadAttemptCompleted(Cr.NS_ERROR_NOT_AVAILABLE);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (this._isUnloading) {
 | |
|       // Don't post multiple unloading requests.  The situation may be solved
 | |
|       // when the active unloading task is completed.
 | |
|       Services.console.logStringMessage("Unloading a tab is in progress.");
 | |
|       watcher.onUnloadAttemptCompleted(Cr.NS_ERROR_ABORT);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._isUnloading = true;
 | |
|     const isTabUnloaded = await this.unloadLeastRecentlyUsedTab(
 | |
|       minInactiveDuration
 | |
|     );
 | |
|     this._isUnloading = false;
 | |
| 
 | |
|     watcher.onUnloadAttemptCompleted(
 | |
|       isTabUnloaded ? Cr.NS_OK : Cr.NS_ERROR_NOT_AVAILABLE
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get a list of tabs that can be discarded. This list includes all tabs in
 | |
|    * all windows and is sorted based on a weighting described below.
 | |
|    *
 | |
|    * @param minInactiveDuration If this value is a number, tabs that were accessed
 | |
|    *        in the last |minInactiveDuration| msec are not unloaded even if they
 | |
|    *        are least-recently-used.
 | |
|    *
 | |
|    * @param tabMethods an helper object with methods called by this algorithm.
 | |
|    *
 | |
|    * The algorithm used is:
 | |
|    *   1. Sort all of the tabs by a base weight. Tabs with a higher weight, such as
 | |
|    *      those that are pinned or playing audio, will appear at the end. When two
 | |
|    *      tabs have the same weight, sort by the order in which they were last.
 | |
|    *      recently accessed Tabs that have a weight of NEVER_DISCARD are included in
 | |
|    *       the list, but will not be discarded.
 | |
|    *   2. Exclude the last X tabs, where X is the value returned by getMinTabCount().
 | |
|    *      These tabs are considered to have been recently accessed and are not further
 | |
|    *      reweighted. This also saves time when there are less than X tabs open.
 | |
|    *   3. Calculate the amount of processes that are used only by each tab, as the
 | |
|    *      resources used by these proceses can be freed up if the tab is closed. Sort
 | |
|    *      the tabs by the number of unique processes used and add a reweighting factor
 | |
|    *      based on this.
 | |
|    *   4. Futher reweight based on an approximation of the amount of memory that each
 | |
|    *      tab uses.
 | |
|    *   5. Combine these weights to produce a final tab discard order, and discard the
 | |
|    *      first tab. If this fails, then discard the next tab in the list until no more
 | |
|    *      non-discardable tabs are found.
 | |
|    *
 | |
|    * The tabMethods are used so that unit tests can use false tab objects and
 | |
|    * override their behaviour.
 | |
|    */
 | |
|   async getSortedTabs(
 | |
|     minInactiveDuration = kMinInactiveDurationInMs,
 | |
|     tabMethods = DefaultTabUnloaderMethods
 | |
|   ) {
 | |
|     let tabs = [];
 | |
| 
 | |
|     const now = tabMethods.getNow();
 | |
| 
 | |
|     let lowestWeight = 1000;
 | |
|     for (let tab of tabMethods.iterateTabs()) {
 | |
|       if (
 | |
|         typeof minInactiveDuration == "number" &&
 | |
|         now - tab.tab.lastAccessed < minInactiveDuration
 | |
|       ) {
 | |
|         // Skip "fresh" tabs, which were accessed within the specified duration.
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       let weight = determineTabBaseWeight(tab, tabMethods);
 | |
| 
 | |
|       // Don't add tabs that have a weight of -1.
 | |
|       if (weight != -1) {
 | |
|         tab.weight = weight;
 | |
|         tabs.push(tab);
 | |
|         if (weight < lowestWeight) {
 | |
|           lowestWeight = weight;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     tabs = tabs.sort((a, b) => {
 | |
|       if (a.weight != b.weight) {
 | |
|         return a.weight - b.weight;
 | |
|       }
 | |
| 
 | |
|       return a.tab.lastAccessed - b.tab.lastAccessed;
 | |
|     });
 | |
| 
 | |
|     // If the lowest priority tab is not discardable, no need to continue.
 | |
|     if (!tabs.length || !this.isDiscardable(tabs[0])) {
 | |
|       return tabs;
 | |
|     }
 | |
| 
 | |
|     // Determine the lowest weight that the tabs have. The tabs with the
 | |
|     // lowest weight (should be most non-selected tabs) will be additionally
 | |
|     // weighted by the number of processes and memory that they use.
 | |
|     let higherWeightedCount = 0;
 | |
|     for (let idx = 0; idx < tabs.length; idx++) {
 | |
|       if (tabs[idx].weight != lowestWeight) {
 | |
|         higherWeightedCount = tabs.length - idx;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Don't continue to reweight the last few tabs, the number of which is
 | |
|     // determined by getMinTabCount. This prevents extra work when there are
 | |
|     // only a few tabs, or for the last few tabs that have likely been used
 | |
|     // recently.
 | |
|     let minCount = tabMethods.getMinTabCount();
 | |
|     if (higherWeightedCount < minCount) {
 | |
|       higherWeightedCount = minCount;
 | |
|     }
 | |
| 
 | |
|     // If |lowestWeightedCount| is 1, no benefit from calculating
 | |
|     // the tab's memory and additional weight.
 | |
|     const lowestWeightedCount = tabs.length - higherWeightedCount;
 | |
|     if (lowestWeightedCount > 1) {
 | |
|       let processMap = getAllProcesses(tabs, tabMethods);
 | |
| 
 | |
|       let higherWeightedTabs = tabs.splice(-higherWeightedCount);
 | |
| 
 | |
|       await adjustForResourceUse(tabs, processMap, tabMethods);
 | |
|       tabs = tabs.concat(higherWeightedTabs);
 | |
|     }
 | |
| 
 | |
|     return tabs;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Select and discard one tab.
 | |
|    * @returns true if a tab was unloaded, otherwise false.
 | |
|    */
 | |
|   async unloadLeastRecentlyUsedTab(
 | |
|     minInactiveDuration = kMinInactiveDurationInMs
 | |
|   ) {
 | |
|     const sortedTabs = await this.getSortedTabs(minInactiveDuration);
 | |
| 
 | |
|     for (let tabInfo of sortedTabs) {
 | |
|       if (!this.isDiscardable(tabInfo)) {
 | |
|         // Since |sortedTabs| is sorted, once we see an undiscardable tab
 | |
|         // no need to continue the loop.
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       const remoteType = tabInfo.tab?.linkedBrowser?.remoteType;
 | |
|       if (tabInfo.gBrowser.discardBrowser(tabInfo.tab)) {
 | |
|         Services.console.logStringMessage(
 | |
|           `TabUnloader discarded <${remoteType}>`
 | |
|         );
 | |
|         tabInfo.tab.updateLastUnloadedByTabUnloader();
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI([
 | |
|     "nsIObserver",
 | |
|     "nsISupportsWeakReference",
 | |
|   ]),
 | |
| };
 | |
| 
 | |
| /** Determine the base weight of the tab without accounting for
 | |
|  *  resource use
 | |
|  * @param tab tab to use
 | |
|  * @returns the tab's base weight
 | |
|  */
 | |
| function determineTabBaseWeight(tab, tabMethods) {
 | |
|   let totalWeight = 0;
 | |
| 
 | |
|   for (let criteriaType of criteriaTypes) {
 | |
|     let weight = tabMethods[criteriaType[CRITERIA_METHOD]](
 | |
|       tab.tab,
 | |
|       criteriaType[CRITERIA_WEIGHT]
 | |
|     );
 | |
| 
 | |
|     // If a criteria returns -1, then never discard this tab.
 | |
|     if (weight == -1) {
 | |
|       return -1;
 | |
|     }
 | |
| 
 | |
|     totalWeight += weight;
 | |
|   }
 | |
| 
 | |
|   return totalWeight;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Constuct a map of the processes that are used by the supplied tabs.
 | |
|  * The map will map process ids to an object with two properties:
 | |
|  *   count - the number of tabs or subframes that use this process
 | |
|  *   topCount - the number of top-level tabs that use this process
 | |
|  *   tabSet - the indices of the tabs hosted by this process
 | |
|  *
 | |
|  * @param tabs array of tabs
 | |
|  * @param tabMethods an helper object with methods called by this algorithm.
 | |
|  * @returns process map
 | |
|  */
 | |
| function getAllProcesses(tabs, tabMethods) {
 | |
|   // Determine the number of tabs that reference each process. This
 | |
|   // is stored in the map 'processMap' where the key is the process
 | |
|   // and the value is that number of browsing contexts that use that
 | |
|   // process.
 | |
|   // XXXndeakin this should be unique processes per tab, in the case multiple
 | |
|   // subframes use the same process?
 | |
| 
 | |
|   let processMap = new Map();
 | |
| 
 | |
|   for (let tabIndex = 0; tabIndex < tabs.length; ++tabIndex) {
 | |
|     const tab = tabs[tabIndex];
 | |
| 
 | |
|     // The per-tab map will map process ids to an object with three properties:
 | |
|     //   isTopLevel - whether the process hosts the tab's top-level frame or not
 | |
|     //   frameCount - the number of frames hosted by the process
 | |
|     //                (a top frame contributes 2 and a sub frame contributes 1)
 | |
|     //   entryToProcessMap - the reference to the object in |processMap|
 | |
|     tab.processes = new Map();
 | |
| 
 | |
|     let topLevel = true;
 | |
|     for (let pid of tabMethods.iterateProcesses(tab.tab)) {
 | |
|       let processInfo = processMap.get(pid);
 | |
|       if (processInfo) {
 | |
|         processInfo.count++;
 | |
|         processInfo.tabSet.add(tabIndex);
 | |
|       } else {
 | |
|         processInfo = { count: 1, topCount: 0, tabSet: new Set([tabIndex]) };
 | |
|         processMap.set(pid, processInfo);
 | |
|       }
 | |
| 
 | |
|       let tabProcessEntry = tab.processes.get(pid);
 | |
|       if (tabProcessEntry) {
 | |
|         ++tabProcessEntry.frameCount;
 | |
|       } else {
 | |
|         tabProcessEntry = {
 | |
|           isTopLevel: topLevel,
 | |
|           frameCount: 1,
 | |
|           entryToProcessMap: processInfo,
 | |
|         };
 | |
|         tab.processes.set(pid, tabProcessEntry);
 | |
|       }
 | |
| 
 | |
|       if (topLevel) {
 | |
|         topLevel = false;
 | |
|         processInfo.topCount = processInfo.topCount
 | |
|           ? processInfo.topCount + 1
 | |
|           : 1;
 | |
|         // top-level frame contributes two frame counts
 | |
|         ++tabProcessEntry.frameCount;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return processMap;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Adjust the tab info and reweight the tabs based on the process and memory
 | |
|  * use that is used, as described by getSortedTabs
 | |
| 
 | |
|  * @param tabs array of tabs
 | |
|  * @param processMap map of processes returned by getAllProcesses
 | |
|  * @param tabMethods an helper object with methods called by this algorithm.
 | |
|  */
 | |
| async function adjustForResourceUse(tabs, processMap, tabMethods) {
 | |
|   // The second argument is needed for testing.
 | |
|   await tabMethods.calculateMemoryUsage(processMap, tabs);
 | |
| 
 | |
|   let sortWeight = 0;
 | |
|   for (let tab of tabs) {
 | |
|     tab.sortWeight = ++sortWeight;
 | |
| 
 | |
|     let uniqueCount = 0;
 | |
|     let totalMemory = 0;
 | |
|     for (const procEntry of tab.processes.values()) {
 | |
|       const processInfo = procEntry.entryToProcessMap;
 | |
|       if (processInfo.tabSet.size == 1) {
 | |
|         uniqueCount++;
 | |
|       }
 | |
| 
 | |
|       // Guess how much memory the frame might be using using by dividing
 | |
|       // the total memory used by a process by the number of tabs and
 | |
|       // frames that are using that process. Assume that any subframes take up
 | |
|       // only half as much memory as a process loaded in a top level tab.
 | |
|       // So for example, if a process is used in four top level tabs and two
 | |
|       // subframes, the top level tabs share 80% of the memory and the subframes
 | |
|       // use 20% of the memory.
 | |
|       const perFrameMemory =
 | |
|         processInfo.memory /
 | |
|         (processInfo.topCount * 2 + (processInfo.count - processInfo.topCount));
 | |
|       totalMemory += perFrameMemory * procEntry.frameCount;
 | |
|     }
 | |
| 
 | |
|     tab.uniqueCount = uniqueCount;
 | |
|     tab.memory = totalMemory;
 | |
|   }
 | |
| 
 | |
|   tabs.sort((a, b) => {
 | |
|     return b.uniqueCount - a.uniqueCount;
 | |
|   });
 | |
|   sortWeight = 0;
 | |
|   for (let tab of tabs) {
 | |
|     tab.sortWeight += ++sortWeight;
 | |
|     if (tab.uniqueCount > 1) {
 | |
|       // If the tab has a number of processes that are only used by this tab,
 | |
|       // subtract off an additional amount to the sorting weight value. That
 | |
|       // way, tabs that use lots of processes are more likely to be discarded.
 | |
|       tab.sortWeight -= tab.uniqueCount - 1;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   tabs.sort((a, b) => {
 | |
|     return b.memory - a.memory;
 | |
|   });
 | |
|   sortWeight = 0;
 | |
|   for (let tab of tabs) {
 | |
|     tab.sortWeight += ++sortWeight;
 | |
|   }
 | |
| 
 | |
|   tabs.sort((a, b) => {
 | |
|     if (a.sortWeight != b.sortWeight) {
 | |
|       return a.sortWeight - b.sortWeight;
 | |
|     }
 | |
|     return a.tab.lastAccessed - b.tab.lastAccessed;
 | |
|   });
 | |
| }
 |