blob: c8135eeb7425a2ff96612059ff12f88cabacfc97 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.memory;
import android.app.ActivityManager;
import android.content.ComponentCallbacks2;
import android.content.res.Configuration;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.base.MemoryPressureLevel;
import org.chromium.base.MemoryPressureListener;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.Supplier;
/**
* This class monitors memory pressure and reports it to the native side.
* Even though there can be other callbacks besides MemoryPressureListener (which reports
* pressure to the native side, and is added implicitly), the class is designed to suite
* needs of native MemoryPressureListeners.
*
* There are two groups of MemoryPressureListeners:
*
* 1. Stateless, i.e. ones that simply free memory (caches, etc.) in response to memory
* pressure. These listeners need to be called periodically (to have effect), but not
* too frequently (to avoid regressing performance too much).
*
* 2. Stateful, i.e. ones that change their behavior based on the last received memory
* pressure (in addition to freeing memory). These listeners need to know when the
* pressure subsides, i.e. they need to be notified about CRITICAL->MODERATE changes.
*
* Android notifies about memory pressure through onTrimMemory() / onLowMemory() callbacks
* from ComponentCallbacks2, but these are unreliable (e.g. called too early, called just
* once, not called when memory pressure subsides, etc., see https://crbug.com/813909 for
* more examples).
*
* There is also ActivityManager.getMyMemoryState() API which returns current pressure for
* the calling process. It has its caveats, for example it can't be called from isolated
* processes (renderers). Plus we don't want to poll getMyMemoryState() unnecessarily, for
* example there is no reason to poll it when Chrome is in the background.
*
* This class implements the following principles:
*
* 1. Throttle pressure signals sent to callbacks.
* Callbacks are called at most once during throttling interval. If same pressure is
* reported several times during the interval, all reports except the first one are
* ignored.
*
* 2. Always report changes in pressure.
* If pressure changes during the interval, the change is not ignored, but delayed
* until the end of the interval.
*
* 3. Poll on CRITICAL memory pressure.
* Once CRITICAL pressure is reported, getMyMemoryState API is used to periodically
* query pressure until it subsides (becomes non-CRITICAL).
*
* Zooming out, the class is used as follows:
*
* 1. Only the browser process / WebView process poll, and it only polls when it makes
* sense to do so (when Chrome is in the foreground / there are WebView instances
* around).
*
* 2. Services (GPU, renderers) don't poll, instead they get additional pressure signals
* from the main process.
*
* NOTE: This class should only be used on UiThread as defined by ThreadUtils (which is
* Android main thread for Chrome, but can be some other thread for WebView).
*/
public class MemoryPressureMonitor {
private static final int DEFAULT_THROTTLING_INTERVAL_MS = 60 * 1000;
private final int mThrottlingIntervalMs;
// Pressure reported to callbacks in the current throttling interval.
private @MemoryPressureLevel int mLastReportedPressure = MemoryPressureLevel.NONE;
// Pressure received (but not reported) during the current throttling interval,
// or null if no pressure was received.
private @MemoryPressureLevel Integer mThrottledPressure;
// Whether we need to throttle pressure signals.
private boolean mIsInsideThrottlingInterval;
private boolean mPollingEnabled;
private Supplier<Integer> mCurrentPressureSupplierForTesting;
private MemoryPressureCallback mReportingCallbackForTesting;
private final Runnable mThrottlingIntervalTask = this::onThrottlingIntervalFinished;
// The only instance.
public static final MemoryPressureMonitor INSTANCE =
new MemoryPressureMonitor(DEFAULT_THROTTLING_INTERVAL_MS);
@VisibleForTesting
protected MemoryPressureMonitor(int throttlingIntervalMs) {
mThrottlingIntervalMs = throttlingIntervalMs;
}
/** Starts listening to ComponentCallbacks2. */
public void registerComponentCallbacks() {
ThreadUtils.assertOnUiThread();
ContextUtils.getApplicationContext()
.registerComponentCallbacks(
new ComponentCallbacks2() {
@Override
public void onTrimMemory(int level) {
Integer pressure = memoryPressureFromTrimLevel(level);
if (pressure != null) {
notifyPressure(pressure);
}
}
@Override
public void onLowMemory() {
notifyPressure(MemoryPressureLevel.CRITICAL);
}
@Override
public void onConfigurationChanged(Configuration configuration) {}
});
}
/**
* Enables memory pressure polling.
* See class comment for specifics. This method also does a single pressure check to get
* the current pressure.
*/
public void enablePolling() {
ThreadUtils.assertOnUiThread();
if (mPollingEnabled) return;
mPollingEnabled = true;
if (!mIsInsideThrottlingInterval) {
reportCurrentPressure();
}
}
/** Disables memory pressure polling. */
public void disablePolling() {
ThreadUtils.assertOnUiThread();
if (!mPollingEnabled) return;
mPollingEnabled = false;
}
/**
* Notifies the class about change in memory pressure.
* Note that |pressure| might get throttled or delayed, i.e. calling this method doesn't
* necessarily call the callbacks. See the class comment.
*/
public void notifyPressure(@MemoryPressureLevel int pressure) {
ThreadUtils.assertOnUiThread();
if (mIsInsideThrottlingInterval) {
// We've already reported during this interval. Save |pressure| and act on
// it later, when the interval finishes.
mThrottledPressure = pressure;
return;
}
reportPressure(pressure);
}
/**
* Last pressure that was reported to MemoryPressureListener.
* Returns MemoryPressureLevel.NONE if nothing was reported yet.
*/
public @MemoryPressureLevel int getLastReportedPressure() {
ThreadUtils.assertOnUiThread();
return mLastReportedPressure;
}
private void reportPressure(@MemoryPressureLevel int pressure) {
assert !mIsInsideThrottlingInterval : "Can't report pressure when throttling.";
startThrottlingInterval();
mLastReportedPressure = pressure;
if (mReportingCallbackForTesting != null) {
mReportingCallbackForTesting.onPressure(pressure);
} else {
MemoryPressureListener.notifyMemoryPressure(pressure);
}
}
private void onThrottlingIntervalFinished() {
mIsInsideThrottlingInterval = false;
// If there was a pressure change during the interval, report it.
if (mThrottledPressure != null && mLastReportedPressure != mThrottledPressure) {
int throttledPressure = mThrottledPressure;
mThrottledPressure = null;
reportPressure(throttledPressure);
return;
}
// The pressure didn't change during the interval. Report current pressure
// (starting a new interval) if we need to.
if (mPollingEnabled && mLastReportedPressure == MemoryPressureLevel.CRITICAL) {
reportCurrentPressure();
}
}
private void reportCurrentPressure() {
Integer pressure =
mCurrentPressureSupplierForTesting != null
? mCurrentPressureSupplierForTesting.get()
: MemoryPressureMonitor.getCurrentMemoryPressure();
if (pressure != null) {
reportPressure(pressure);
}
}
private void startThrottlingInterval() {
ThreadUtils.postOnUiThreadDelayed(mThrottlingIntervalTask, mThrottlingIntervalMs);
mIsInsideThrottlingInterval = true;
}
public void setCurrentPressureSupplierForTesting(Supplier<Integer> supplier) {
mCurrentPressureSupplierForTesting = supplier;
ResettersForTesting.register(() -> mCurrentPressureSupplierForTesting = null);
}
public void setReportingCallbackForTesting(MemoryPressureCallback callback) {
mReportingCallbackForTesting = callback;
ResettersForTesting.register(() -> mReportingCallbackForTesting = null);
}
/**
* Queries current memory pressure.
* Returns null if the pressure couldn't be determined.
*/
private static @MemoryPressureLevel Integer getCurrentMemoryPressure() {
// We used to have a histogram here to measure the duration of each successful
// ActivityManager.getMyMemoryState() call called
// Android.MemoryPressureMonitor.GetMyMemoryState.Succeeded.Time. 50th percentile was 0.8ms.
try {
ActivityManager.RunningAppProcessInfo processInfo =
new ActivityManager.RunningAppProcessInfo();
ActivityManager.getMyMemoryState(processInfo);
return memoryPressureFromTrimLevel(processInfo.lastTrimLevel);
} catch (Exception e) {
// Defensively catch all exceptions, just in case.
return null;
}
}
/**
* Maps ComponentCallbacks2.TRIM_* value to MemoryPressureLevel.
* Returns null if |level| couldn't be mapped and should be ignored.
*/
@VisibleForTesting
public static @MemoryPressureLevel Integer memoryPressureFromTrimLevel(int level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE
|| level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
return MemoryPressureLevel.CRITICAL;
} else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Don't notify on TRIM_MEMORY_UI_HIDDEN, since this class only
// dispatches actionable memory pressure signals to native.
return MemoryPressureLevel.MODERATE;
}
return null;
}
}