1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base.memory;
6 
7 import android.app.ActivityManager;
8 import android.content.ComponentCallbacks2;
9 import android.content.res.Configuration;
10 import android.os.Build;
11 import android.os.SystemClock;
12 
13 import androidx.annotation.VisibleForTesting;
14 
15 import org.chromium.base.ContextUtils;
16 import org.chromium.base.MemoryPressureLevel;
17 import org.chromium.base.MemoryPressureListener;
18 import org.chromium.base.ThreadUtils;
19 import org.chromium.base.annotations.MainDex;
20 import org.chromium.base.metrics.RecordHistogram;
21 import org.chromium.base.supplier.Supplier;
22 
23 import java.util.concurrent.TimeUnit;
24 
25 /**
26  * This class monitors memory pressure and reports it to the native side.
27  * Even though there can be other callbacks besides MemoryPressureListener (which reports
28  * pressure to the native side, and is added implicitly), the class is designed to suite
29  * needs of native MemoryPressureListeners.
30  *
31  * There are two groups of MemoryPressureListeners:
32  *
33  * 1. Stateless, i.e. ones that simply free memory (caches, etc.) in response to memory
34  *    pressure. These listeners need to be called periodically (to have effect), but not
35  *    too frequently (to avoid regressing performance too much).
36  *
37  * 2. Stateful, i.e. ones that change their behavior based on the last received memory
38  *    pressure (in addition to freeing memory). These listeners need to know when the
39  *    pressure subsides, i.e. they need to be notified about CRITICAL->MODERATE changes.
40  *
41  * Android notifies about memory pressure through onTrimMemory() / onLowMemory() callbacks
42  * from ComponentCallbacks2, but these are unreliable (e.g. called too early, called just
43  * once, not called when memory pressure subsides, etc., see https://crbug.com/813909 for
44  * more examples).
45  *
46  * There is also ActivityManager.getMyMemoryState() API which returns current pressure for
47  * the calling process. It has its caveats, for example it can't be called from isolated
48  * processes (renderers). Plus we don't want to poll getMyMemoryState() unnecessarily, for
49  * example there is no reason to poll it when Chrome is in the background.
50  *
51  * This class implements the following principles:
52  *
53  * 1. Throttle pressure signals sent to callbacks.
54  *    Callbacks are called at most once during throttling interval. If same pressure is
55  *    reported several times during the interval, all reports except the first one are
56  *    ignored.
57  *
58  * 2. Always report changes in pressure.
59  *    If pressure changes during the interval, the change is not ignored, but delayed
60  *    until the end of the interval.
61  *
62  * 3. Poll on CRITICAL memory pressure.
63  *    Once CRITICAL pressure is reported, getMyMemoryState API is used to periodically
64  *    query pressure until it subsides (becomes non-CRITICAL).
65  *
66  * Zooming out, the class is used as follows:
67  *
68  * 1. Only the browser process / WebView process poll, and it only polls when it makes
69  *    sense to do so (when Chrome is in the foreground / there are WebView instances
70  *    around).
71  *
72  * 2. Services (GPU, renderers) don't poll, instead they get additional pressure signals
73  *    from the main process.
74  *
75  * NOTE: This class should only be used on UiThread as defined by ThreadUtils (which is
76  *       Android main thread for Chrome, but can be some other thread for WebView).
77  */
78 @MainDex
79 public class MemoryPressureMonitor {
80     private static final int DEFAULT_THROTTLING_INTERVAL_MS = 60 * 1000;
81 
82     private final int mThrottlingIntervalMs;
83 
84     // Pressure reported to callbacks in the current throttling interval.
85     private @MemoryPressureLevel int mLastReportedPressure = MemoryPressureLevel.NONE;
86 
87     // Pressure received (but not reported) during the current throttling interval,
88     // or null if no pressure was received.
89     private @MemoryPressureLevel Integer mThrottledPressure;
90 
91     // Whether we need to throttle pressure signals.
92     private boolean mIsInsideThrottlingInterval;
93 
94     private boolean mPollingEnabled;
95 
96     // Changed by tests.
97     private Supplier<Integer> mCurrentPressureSupplier =
98             MemoryPressureMonitor::getCurrentMemoryPressure;
99 
100     // Changed by tests.
101     private MemoryPressureCallback mReportingCallback =
102             MemoryPressureListener::notifyMemoryPressure;
103 
104     private final Runnable mThrottlingIntervalTask = this ::onThrottlingIntervalFinished;
105 
106     // The only instance.
107     public static final MemoryPressureMonitor INSTANCE =
108             new MemoryPressureMonitor(DEFAULT_THROTTLING_INTERVAL_MS);
109 
110     @VisibleForTesting
MemoryPressureMonitor(int throttlingIntervalMs)111     protected MemoryPressureMonitor(int throttlingIntervalMs) {
112         mThrottlingIntervalMs = throttlingIntervalMs;
113     }
114 
115     /**
116      * Starts listening to ComponentCallbacks2.
117      */
registerComponentCallbacks()118     public void registerComponentCallbacks() {
119         ThreadUtils.assertOnUiThread();
120 
121         ContextUtils.getApplicationContext().registerComponentCallbacks(new ComponentCallbacks2() {
122             @Override
123             public void onTrimMemory(int level) {
124                 Integer pressure = memoryPressureFromTrimLevel(level);
125                 if (pressure != null) {
126                     notifyPressure(pressure);
127                 }
128             }
129 
130             @Override
131             public void onLowMemory() {
132                 notifyPressure(MemoryPressureLevel.CRITICAL);
133             }
134 
135             @Override
136             public void onConfigurationChanged(Configuration configuration) {}
137         });
138     }
139 
140     /**
141      * Enables memory pressure polling.
142      * See class comment for specifics. This method also does a single pressure check to get
143      * the current pressure.
144      */
enablePolling()145     public void enablePolling() {
146         ThreadUtils.assertOnUiThread();
147         if (mPollingEnabled) return;
148 
149         mPollingEnabled = true;
150         if (!mIsInsideThrottlingInterval) {
151             reportCurrentPressure();
152         }
153     }
154 
155     /**
156      * Disables memory pressure polling.
157      */
disablePolling()158     public void disablePolling() {
159         ThreadUtils.assertOnUiThread();
160         if (!mPollingEnabled) return;
161 
162         mPollingEnabled = false;
163     }
164 
165     /**
166      * Notifies the class about change in memory pressure.
167      * Note that |pressure| might get throttled or delayed, i.e. calling this method doesn't
168      * necessarily call the callbacks. See the class comment.
169      */
notifyPressure(@emoryPressureLevel int pressure)170     public void notifyPressure(@MemoryPressureLevel int pressure) {
171         ThreadUtils.assertOnUiThread();
172 
173         if (mIsInsideThrottlingInterval) {
174             // We've already reported during this interval. Save |pressure| and act on
175             // it later, when the interval finishes.
176             mThrottledPressure = pressure;
177             return;
178         }
179 
180         reportPressure(pressure);
181     }
182 
183     /**
184      * Last pressure that was reported to MemoryPressureListener.
185      * Returns MemoryPressureLevel.NONE if nothing was reported yet.
186      */
getLastReportedPressure()187     public @MemoryPressureLevel int getLastReportedPressure() {
188         ThreadUtils.assertOnUiThread();
189         return mLastReportedPressure;
190     }
191 
reportPressure(@emoryPressureLevel int pressure)192     private void reportPressure(@MemoryPressureLevel int pressure) {
193         assert !mIsInsideThrottlingInterval : "Can't report pressure when throttling.";
194 
195         startThrottlingInterval();
196 
197         mLastReportedPressure = pressure;
198         mReportingCallback.onPressure(pressure);
199     }
200 
onThrottlingIntervalFinished()201     private void onThrottlingIntervalFinished() {
202         mIsInsideThrottlingInterval = false;
203 
204         // If there was a pressure change during the interval, report it.
205         if (mThrottledPressure != null && mLastReportedPressure != mThrottledPressure) {
206             int throttledPressure = mThrottledPressure;
207             mThrottledPressure = null;
208             reportPressure(throttledPressure);
209             return;
210         }
211 
212         // The pressure didn't change during the interval. Report current pressure
213         // (starting a new interval) if we need to.
214         if (mPollingEnabled && mLastReportedPressure == MemoryPressureLevel.CRITICAL) {
215             reportCurrentPressure();
216         }
217     }
218 
reportCurrentPressure()219     private void reportCurrentPressure() {
220         Integer pressure = mCurrentPressureSupplier.get();
221         if (pressure != null) {
222             reportPressure(pressure);
223         }
224     }
225 
startThrottlingInterval()226     private void startThrottlingInterval() {
227         ThreadUtils.postOnUiThreadDelayed(mThrottlingIntervalTask, mThrottlingIntervalMs);
228         mIsInsideThrottlingInterval = true;
229     }
230 
231     @VisibleForTesting
setCurrentPressureSupplierForTesting(Supplier<Integer> supplier)232     public void setCurrentPressureSupplierForTesting(Supplier<Integer> supplier) {
233         mCurrentPressureSupplier = supplier;
234     }
235 
236     @VisibleForTesting
setReportingCallbackForTesting(MemoryPressureCallback callback)237     public void setReportingCallbackForTesting(MemoryPressureCallback callback) {
238         mReportingCallback = callback;
239     }
240 
241     /**
242      * Queries current memory pressure.
243      * Returns null if the pressure couldn't be determined.
244      */
getCurrentMemoryPressure()245     private static @MemoryPressureLevel Integer getCurrentMemoryPressure() {
246         long startNanos = elapsedRealtimeNanos();
247         try {
248             ActivityManager.RunningAppProcessInfo processInfo =
249                     new ActivityManager.RunningAppProcessInfo();
250             ActivityManager.getMyMemoryState(processInfo);
251             // ActivityManager.getMyMemoryState() time histograms, recorded by
252             // getCurrentMemoryPressure(). Using recordCustomCountHistogram because
253             // recordTimesHistogram doesn't support microsecond precision.
254             RecordHistogram.recordCustomCountHistogram(
255                     "Android.MemoryPressureMonitor.GetMyMemoryState.Succeeded.Time",
256                     elapsedDurationSample(startNanos), 1, 1_000_000, 50);
257             return memoryPressureFromTrimLevel(processInfo.lastTrimLevel);
258         } catch (Exception e) {
259             // Defensively catch all exceptions, just in case.
260             RecordHistogram.recordCustomCountHistogram(
261                     "Android.MemoryPressureMonitor.GetMyMemoryState.Failed.Time",
262                     elapsedDurationSample(startNanos), 1, 1_000_000, 50);
263             return null;
264         }
265     }
266 
elapsedDurationSample(long startNanos)267     private static int elapsedDurationSample(long startNanos) {
268         // We're using Count1MHistogram, so we need to calculate duration in microseconds
269         long durationUs = TimeUnit.NANOSECONDS.toMicros(elapsedRealtimeNanos() - startNanos);
270         // record() takes int, so we need to clamp.
271         return (int) Math.min(durationUs, Integer.MAX_VALUE);
272     }
273 
elapsedRealtimeNanos()274     private static long elapsedRealtimeNanos() {
275         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
276             return SystemClock.elapsedRealtimeNanos();
277         } else {
278             return SystemClock.elapsedRealtime() * 1000000;
279         }
280     }
281 
282     /**
283      * Maps ComponentCallbacks2.TRIM_* value to MemoryPressureLevel.
284      * Returns null if |level| couldn't be mapped and should be ignored.
285      */
286     @VisibleForTesting
memoryPressureFromTrimLevel(int level)287     public static @MemoryPressureLevel Integer memoryPressureFromTrimLevel(int level) {
288         if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE
289                 || level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
290             return MemoryPressureLevel.CRITICAL;
291         } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
292             // Don't notify on TRIM_MEMORY_UI_HIDDEN, since this class only
293             // dispatches actionable memory pressure signals to native.
294             return MemoryPressureLevel.MODERATE;
295         }
296         return null;
297     }
298 }
299