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