TrimMemoryMiddleware.kt
/* 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/. */
package mozilla.components.browser.state.engine.middleware
import android.content.ComponentCallbacks2
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.browser.state.selector.allTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.support.base.log.logger.Logger
// The number of tabs we keep active and do not suspend (in addition to the selected tab)
private const val MIN_ACTIVE_TABS = 3
/**
* [Middleware] responsible for suspending [EngineSession] instances on low memory.
*/
internal class TrimMemoryMiddleware : Middleware<BrowserState, BrowserAction> {
private val logger = Logger("TrimMemoryMiddleware")
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction,
) {
next(action)
if (action is SystemAction.LowMemoryAction) {
trimMemory(context, action)
}
}
private fun trimMemory(
context: MiddlewareContext<BrowserState, BrowserAction>,
action: SystemAction.LowMemoryAction,
) {
if (!shouldCloseEngineSessions(action.level)) {
return
}
val suspendTabs = determineTabsToSuspend(context.state)
logger.info("Trim memory (tabs=${context.state.allTabs.size}, suspending=${suspendTabs.size})")
// This is not the most efficient way of doing this. We are looping over all tabs and then
// dispatching a SuspendEngineSessionAction for each tab that is no longer needed.
suspendTabs.forEach { tab ->
context.dispatch(EngineAction.SuspendEngineSessionAction(tab.id))
}
}
private fun determineTabsToSuspend(
state: BrowserState,
): List<SessionState> {
return state.allTabs.filter { tab ->
// We never suspend the currently selected tab
tab.id != state.selectedTabId
}.filter { tab ->
// Only tabs with an engine session can get suspended
tab.engineState.engineSession != null
}.sortedByDescending { tab ->
if (tab is TabSessionState) {
// We want to suspend the tabs that haven't been accessed for a while first
tab.lastAccess
} else {
// We are more aggressive with custom tabs an always consider them for suspension
0
}
}.drop(MIN_ACTIVE_TABS) // Keep n [MIN_ACTIVE_TABS] most recently accessed tabs.
}
}
@Suppress("DEPRECATION") // Apps are not notified of these levels since API level 34.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1909473
private fun shouldCloseEngineSessions(level: Int): Boolean {
return when (level) {
// Foreground: The device is running extremely low on memory. The app is not yet considered a killable
// process, but the system will begin killing background processes if apps do not release resources.
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> true
// Background: The system is running low on memory and our process is one of the first to be killed
// if the system does not recover memory now.
ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> true
else -> false
}
}