1 /* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 * TabUnloader is used to discard tabs when memory or resource constraints
8 * are reached. The discarded tabs are determined using a heuristic that
9 * accounts for when the tab was last used, how many resources the tab uses,
10 * and whether the tab is likely to affect the user if it is closed.
14 ChromeUtils.defineESModuleGetters(lazy, {
15 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
16 webrtcUI: "resource:///modules/webrtcUI.sys.mjs",
19 // If there are only this many or fewer tabs open, just sort by weight, and close
20 // the lowest tab. Otherwise, do a more intensive compuation that determines the
21 // tabs to close based on memory and process use.
22 const MIN_TABS_COUNT = 10;
24 // Weight for non-discardable tabs.
25 const NEVER_DISCARD = 100000;
27 // Default minimum inactive duration. Tabs that were accessed in the last
28 // period of this duration are not unloaded.
29 const kMinInactiveDurationInMs = Services.prefs.getIntPref(
30 "browser.tabs.min_inactive_duration_before_unload"
34 ["isNonDiscardable", NEVER_DISCARD],
36 ["usingPictureInPicture", NEVER_DISCARD],
37 ["playingMedia", NEVER_DISCARD],
38 ["usingWebRTC", NEVER_DISCARD],
40 ["isPrivate", NEVER_DISCARD],
43 // Indicies into the criteriaTypes lists.
44 let CRITERIA_METHOD = 0;
45 let CRITERIA_WEIGHT = 1;
48 * This is an object that supplies methods that determine details about
49 * each tab. This default object is used if another one is not passed
50 * to the tab unloader functions. This allows tests to override the methods
51 * with tab specific data rather than creating test tabs.
53 let DefaultTabUnloaderMethods = {
54 isNonDiscardable(tab, weight) {
55 if (tab.undiscardable || tab.selected) {
59 return !tab.linkedBrowser.isConnected ? -1 : 0;
62 isPinned(tab, weight) {
63 return tab.pinned ? weight : 0;
70 usingPictureInPicture(tab, weight) {
71 // This has higher weight even when paused.
72 return tab.pictureinpicture ? weight : 0;
75 playingMedia(tab, weight) {
76 return tab.soundPlaying ? weight : 0;
79 usingWebRTC(tab, weight) {
80 const browser = tab.linkedBrowser;
85 // No need to iterate browser contexts for hasActivePeerConnection
86 // because hasActivePeerConnection is set only in the top window.
87 return lazy.webrtcUI.browserHasStreams(browser) ||
88 browser.browsingContext?.currentWindowGlobal?.hasActivePeerConnections()
93 isPrivate(tab, weight) {
94 return lazy.PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser)
100 return MIN_TABS_COUNT;
108 for (let win of Services.wm.getEnumerator("navigator:browser")) {
109 for (let tab of win.gBrowser.tabs) {
110 yield { tab, gBrowser: win.gBrowser };
115 *iterateBrowsingContexts(bc) {
117 for (let childBC of bc.children) {
118 yield* this.iterateBrowsingContexts(childBC);
122 *iterateProcesses(tab) {
123 let bc = tab?.linkedBrowser?.browsingContext;
128 const iter = this.iterateBrowsingContexts(bc);
129 for (let childBC of iter) {
130 if (childBC?.currentWindowGlobal) {
131 yield childBC.currentWindowGlobal.osPid;
137 * Add the amount of memory used by each process to the process map.
139 * @param tabs array of tabs, used only by unit tests
140 * @param map of processes returned by getAllProcesses.
142 async calculateMemoryUsage(processMap) {
143 let parentProcessInfo = await ChromeUtils.requestProcInfo();
144 let childProcessInfoList = parentProcessInfo.children;
145 for (let childProcInfo of childProcessInfoList) {
146 let processInfo = processMap.get(childProcInfo.pid);
148 processInfo = { count: 0, topCount: 0, tabSet: new Set() };
149 processMap.set(childProcInfo.pid, processInfo);
151 processInfo.memory = childProcInfo.memory;
157 * This module is responsible for detecting low-memory scenarios and unloading
158 * tabs in response to them.
161 export var TabUnloader = {
163 * Initialize low-memory detection and tab auto-unloading.
166 const watcher = Cc["@mozilla.org/xpcom/memory-watcher;1"].getService(
167 Ci.nsIAvailableMemoryWatcherBase
169 watcher.registerTabUnloader(this);
173 if (!("weight" in tab)) {
176 return tab.weight < NEVER_DISCARD;
179 // This method is exposed on nsITabUnloader
180 async unloadTabAsync(minInactiveDuration = kMinInactiveDurationInMs) {
181 const watcher = Cc["@mozilla.org/xpcom/memory-watcher;1"].getService(
182 Ci.nsIAvailableMemoryWatcherBase
185 if (!Services.prefs.getBoolPref("browser.tabs.unloadOnLowMemory", true)) {
186 watcher.onUnloadAttemptCompleted(Cr.NS_ERROR_NOT_AVAILABLE);
190 if (this._isUnloading) {
191 // Don't post multiple unloading requests. The situation may be solved
192 // when the active unloading task is completed.
193 Services.console.logStringMessage("Unloading a tab is in progress.");
194 watcher.onUnloadAttemptCompleted(Cr.NS_ERROR_ABORT);
198 this._isUnloading = true;
199 const isTabUnloaded = await this.unloadLeastRecentlyUsedTab(
202 this._isUnloading = false;
204 watcher.onUnloadAttemptCompleted(
205 isTabUnloaded ? Cr.NS_OK : Cr.NS_ERROR_NOT_AVAILABLE
210 * Get a list of tabs that can be discarded. This list includes all tabs in
211 * all windows and is sorted based on a weighting described below.
213 * @param minInactiveDuration If this value is a number, tabs that were accessed
214 * in the last |minInactiveDuration| msec are not unloaded even if they
215 * are least-recently-used.
217 * @param tabMethods an helper object with methods called by this algorithm.
219 * The algorithm used is:
220 * 1. Sort all of the tabs by a base weight. Tabs with a higher weight, such as
221 * those that are pinned or playing audio, will appear at the end. When two
222 * tabs have the same weight, sort by the order in which they were last.
223 * recently accessed Tabs that have a weight of NEVER_DISCARD are included in
224 * the list, but will not be discarded.
225 * 2. Exclude the last X tabs, where X is the value returned by getMinTabCount().
226 * These tabs are considered to have been recently accessed and are not further
227 * reweighted. This also saves time when there are less than X tabs open.
228 * 3. Calculate the amount of processes that are used only by each tab, as the
229 * resources used by these proceses can be freed up if the tab is closed. Sort
230 * the tabs by the number of unique processes used and add a reweighting factor
232 * 4. Futher reweight based on an approximation of the amount of memory that each
234 * 5. Combine these weights to produce a final tab discard order, and discard the
235 * first tab. If this fails, then discard the next tab in the list until no more
236 * non-discardable tabs are found.
238 * The tabMethods are used so that unit tests can use false tab objects and
239 * override their behaviour.
242 minInactiveDuration = kMinInactiveDurationInMs,
243 tabMethods = DefaultTabUnloaderMethods
247 const now = tabMethods.getNow();
249 let lowestWeight = 1000;
250 for (let tab of tabMethods.iterateTabs()) {
252 typeof minInactiveDuration == "number" &&
253 now - tab.tab.lastAccessed < minInactiveDuration
255 // Skip "fresh" tabs, which were accessed within the specified duration.
259 let weight = determineTabBaseWeight(tab, tabMethods);
261 // Don't add tabs that have a weight of -1.
265 if (weight < lowestWeight) {
266 lowestWeight = weight;
271 tabs = tabs.sort((a, b) => {
272 if (a.weight != b.weight) {
273 return a.weight - b.weight;
276 return a.tab.lastAccessed - b.tab.lastAccessed;
279 // If the lowest priority tab is not discardable, no need to continue.
280 if (!tabs.length || !this.isDiscardable(tabs[0])) {
284 // Determine the lowest weight that the tabs have. The tabs with the
285 // lowest weight (should be most non-selected tabs) will be additionally
286 // weighted by the number of processes and memory that they use.
287 let higherWeightedCount = 0;
288 for (let idx = 0; idx < tabs.length; idx++) {
289 if (tabs[idx].weight != lowestWeight) {
290 higherWeightedCount = tabs.length - idx;
295 // Don't continue to reweight the last few tabs, the number of which is
296 // determined by getMinTabCount. This prevents extra work when there are
297 // only a few tabs, or for the last few tabs that have likely been used
299 let minCount = tabMethods.getMinTabCount();
300 if (higherWeightedCount < minCount) {
301 higherWeightedCount = minCount;
304 // If |lowestWeightedCount| is 1, no benefit from calculating
305 // the tab's memory and additional weight.
306 const lowestWeightedCount = tabs.length - higherWeightedCount;
307 if (lowestWeightedCount > 1) {
308 let processMap = getAllProcesses(tabs, tabMethods);
310 let higherWeightedTabs = tabs.splice(-higherWeightedCount);
312 await adjustForResourceUse(tabs, processMap, tabMethods);
313 tabs = tabs.concat(higherWeightedTabs);
320 * Select and discard one tab.
321 * @returns true if a tab was unloaded, otherwise false.
323 async unloadLeastRecentlyUsedTab(
324 minInactiveDuration = kMinInactiveDurationInMs
326 const sortedTabs = await this.getSortedTabs(minInactiveDuration);
328 for (let tabInfo of sortedTabs) {
329 if (!this.isDiscardable(tabInfo)) {
330 // Since |sortedTabs| is sorted, once we see an undiscardable tab
331 // no need to continue the loop.
335 const remoteType = tabInfo.tab?.linkedBrowser?.remoteType;
336 if (tabInfo.gBrowser.discardBrowser(tabInfo.tab)) {
337 Services.console.logStringMessage(
338 `TabUnloader discarded <${remoteType}>`
340 tabInfo.tab.updateLastUnloadedByTabUnloader();
347 QueryInterface: ChromeUtils.generateQI([
349 "nsISupportsWeakReference",
353 /** Determine the base weight of the tab without accounting for
355 * @param tab tab to use
356 * @returns the tab's base weight
358 function determineTabBaseWeight(tab, tabMethods) {
361 for (let criteriaType of criteriaTypes) {
362 let weight = tabMethods[criteriaType[CRITERIA_METHOD]](
364 criteriaType[CRITERIA_WEIGHT]
367 // If a criteria returns -1, then never discard this tab.
372 totalWeight += weight;
379 * Constuct a map of the processes that are used by the supplied tabs.
380 * The map will map process ids to an object with two properties:
381 * count - the number of tabs or subframes that use this process
382 * topCount - the number of top-level tabs that use this process
383 * tabSet - the indices of the tabs hosted by this process
385 * @param tabs array of tabs
386 * @param tabMethods an helper object with methods called by this algorithm.
387 * @returns process map
389 function getAllProcesses(tabs, tabMethods) {
390 // Determine the number of tabs that reference each process. This
391 // is stored in the map 'processMap' where the key is the process
392 // and the value is that number of browsing contexts that use that
394 // XXXndeakin this should be unique processes per tab, in the case multiple
395 // subframes use the same process?
397 let processMap = new Map();
399 for (let tabIndex = 0; tabIndex < tabs.length; ++tabIndex) {
400 const tab = tabs[tabIndex];
402 // The per-tab map will map process ids to an object with three properties:
403 // isTopLevel - whether the process hosts the tab's top-level frame or not
404 // frameCount - the number of frames hosted by the process
405 // (a top frame contributes 2 and a sub frame contributes 1)
406 // entryToProcessMap - the reference to the object in |processMap|
407 tab.processes = new Map();
410 for (let pid of tabMethods.iterateProcesses(tab.tab)) {
411 let processInfo = processMap.get(pid);
414 processInfo.tabSet.add(tabIndex);
416 processInfo = { count: 1, topCount: 0, tabSet: new Set([tabIndex]) };
417 processMap.set(pid, processInfo);
420 let tabProcessEntry = tab.processes.get(pid);
421 if (tabProcessEntry) {
422 ++tabProcessEntry.frameCount;
425 isTopLevel: topLevel,
427 entryToProcessMap: processInfo,
429 tab.processes.set(pid, tabProcessEntry);
434 processInfo.topCount = processInfo.topCount
435 ? processInfo.topCount + 1
437 // top-level frame contributes two frame counts
438 ++tabProcessEntry.frameCount;
447 * Adjust the tab info and reweight the tabs based on the process and memory
448 * use that is used, as described by getSortedTabs
450 * @param tabs array of tabs
451 * @param processMap map of processes returned by getAllProcesses
452 * @param tabMethods an helper object with methods called by this algorithm.
454 async function adjustForResourceUse(tabs, processMap, tabMethods) {
455 // The second argument is needed for testing.
456 await tabMethods.calculateMemoryUsage(processMap, tabs);
459 for (let tab of tabs) {
460 tab.sortWeight = ++sortWeight;
464 for (const procEntry of tab.processes.values()) {
465 const processInfo = procEntry.entryToProcessMap;
466 if (processInfo.tabSet.size == 1) {
470 // Guess how much memory the frame might be using using by dividing
471 // the total memory used by a process by the number of tabs and
472 // frames that are using that process. Assume that any subframes take up
473 // only half as much memory as a process loaded in a top level tab.
474 // So for example, if a process is used in four top level tabs and two
475 // subframes, the top level tabs share 80% of the memory and the subframes
476 // use 20% of the memory.
477 const perFrameMemory =
479 (processInfo.topCount * 2 + (processInfo.count - processInfo.topCount));
480 totalMemory += perFrameMemory * procEntry.frameCount;
483 tab.uniqueCount = uniqueCount;
484 tab.memory = totalMemory;
487 tabs.sort((a, b) => {
488 return b.uniqueCount - a.uniqueCount;
491 for (let tab of tabs) {
492 tab.sortWeight += ++sortWeight;
493 if (tab.uniqueCount > 1) {
494 // If the tab has a number of processes that are only used by this tab,
495 // subtract off an additional amount to the sorting weight value. That
496 // way, tabs that use lots of processes are more likely to be discarded.
497 tab.sortWeight -= tab.uniqueCount - 1;
501 tabs.sort((a, b) => {
502 return b.memory - a.memory;
505 for (let tab of tabs) {
506 tab.sortWeight += ++sortWeight;
509 tabs.sort((a, b) => {
510 if (a.sortWeight != b.sortWeight) {
511 return a.sortWeight - b.sortWeight;
513 return a.tab.lastAccessed - b.tab.lastAccessed;