1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/MemoryMonitor.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,235 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.db.BrowserContract; 1.12 +import org.mozilla.gecko.db.BrowserDB; 1.13 +import org.mozilla.gecko.favicons.Favicons; 1.14 +import org.mozilla.gecko.util.ThreadUtils; 1.15 + 1.16 +import android.content.BroadcastReceiver; 1.17 +import android.content.ComponentCallbacks2; 1.18 +import android.content.Context; 1.19 +import android.content.Intent; 1.20 +import android.content.IntentFilter; 1.21 +import android.os.Build; 1.22 +import android.util.Log; 1.23 + 1.24 +/** 1.25 + * This is a utility class to keep track of how much memory and disk-space pressure 1.26 + * the system is under. It receives input from GeckoActivity via the onLowMemory() and 1.27 + * onTrimMemory() functions, and also listens for some system intents related to 1.28 + * disk-space notifications. Internally it will track how much memory and disk pressure 1.29 + * the system is under, and perform various actions to help alleviate the pressure. 1.30 + * 1.31 + * Note that since there is no notification for when the system has lots of free memory 1.32 + * again, this class also assumes that, over time, the system will free up memory. This 1.33 + * assumption is implemented using a timer that slowly lowers the internal memory 1.34 + * pressure state if no new low-memory notifications are received. 1.35 + * 1.36 + * Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both 1.37 + * of these classes may be accessed from various threads, and have both been designed to 1.38 + * be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock 1.39 + * is allowed to pick up the MemoryMonitor lock, but not vice-versa. 1.40 + */ 1.41 +class MemoryMonitor extends BroadcastReceiver { 1.42 + private static final String LOGTAG = "GeckoMemoryMonitor"; 1.43 + private static final String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP"; 1.44 + private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE"; 1.45 + 1.46 + // Memory pressue levels, keep in sync with those in AndroidJavaWrappers.h 1.47 + private static final int MEMORY_PRESSURE_NONE = 0; 1.48 + private static final int MEMORY_PRESSURE_CLEANUP = 1; 1.49 + private static final int MEMORY_PRESSURE_LOW = 2; 1.50 + private static final int MEMORY_PRESSURE_MEDIUM = 3; 1.51 + private static final int MEMORY_PRESSURE_HIGH = 4; 1.52 + 1.53 + private static MemoryMonitor sInstance = new MemoryMonitor(); 1.54 + 1.55 + static MemoryMonitor getInstance() { 1.56 + return sInstance; 1.57 + } 1.58 + 1.59 + private final PressureDecrementer mPressureDecrementer; 1.60 + private int mMemoryPressure; 1.61 + private boolean mStoragePressure; 1.62 + private boolean mInited; 1.63 + 1.64 + private MemoryMonitor() { 1.65 + mPressureDecrementer = new PressureDecrementer(); 1.66 + mMemoryPressure = MEMORY_PRESSURE_NONE; 1.67 + mStoragePressure = false; 1.68 + } 1.69 + 1.70 + public void init(final Context context) { 1.71 + if (mInited) { 1.72 + return; 1.73 + } 1.74 + 1.75 + IntentFilter filter = new IntentFilter(); 1.76 + filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); 1.77 + filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); 1.78 + filter.addAction(ACTION_MEMORY_DUMP); 1.79 + filter.addAction(ACTION_FORCE_PRESSURE); 1.80 + context.getApplicationContext().registerReceiver(this, filter); 1.81 + mInited = true; 1.82 + } 1.83 + 1.84 + public void onLowMemory() { 1.85 + Log.d(LOGTAG, "onLowMemory() notification received"); 1.86 + if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) { 1.87 + // We need to wait on Gecko here, because if we haven't reduced 1.88 + // memory usage enough when we return from this, Android will kill us. 1.89 + GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createNoOpEvent()); 1.90 + } 1.91 + } 1.92 + 1.93 + public void onTrimMemory(int level) { 1.94 + Log.d(LOGTAG, "onTrimMemory() notification received with level " + level); 1.95 + if (Build.VERSION.SDK_INT < 14) { 1.96 + // this won't even get called pre-ICS 1.97 + return; 1.98 + } 1.99 + 1.100 + if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { 1.101 + increaseMemoryPressure(MEMORY_PRESSURE_HIGH); 1.102 + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { 1.103 + increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM); 1.104 + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { 1.105 + // includes TRIM_MEMORY_BACKGROUND 1.106 + increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP); 1.107 + } else { 1.108 + // levels down here mean gecko is the foreground process so we 1.109 + // should be less aggressive with wiping memory as it may impact 1.110 + // user experience. 1.111 + increaseMemoryPressure(MEMORY_PRESSURE_LOW); 1.112 + } 1.113 + } 1.114 + 1.115 + @Override 1.116 + public void onReceive(Context context, Intent intent) { 1.117 + if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) { 1.118 + Log.d(LOGTAG, "Device storage is low"); 1.119 + mStoragePressure = true; 1.120 + ThreadUtils.postToBackgroundThread(new StorageReducer(context)); 1.121 + } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) { 1.122 + Log.d(LOGTAG, "Device storage is ok"); 1.123 + mStoragePressure = false; 1.124 + } else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) { 1.125 + String label = intent.getStringExtra("label"); 1.126 + if (label == null) { 1.127 + label = "default"; 1.128 + } 1.129 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Memory:Dump", label)); 1.130 + } else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) { 1.131 + increaseMemoryPressure(MEMORY_PRESSURE_HIGH); 1.132 + } 1.133 + } 1.134 + 1.135 + private boolean increaseMemoryPressure(int level) { 1.136 + int oldLevel; 1.137 + synchronized (this) { 1.138 + // bump up our level if we're not already higher 1.139 + if (mMemoryPressure > level) { 1.140 + return false; 1.141 + } 1.142 + oldLevel = mMemoryPressure; 1.143 + mMemoryPressure = level; 1.144 + } 1.145 + 1.146 + // since we don't get notifications for when memory pressure is off, 1.147 + // we schedule our own timer to slowly back off the memory pressure level. 1.148 + // note that this will reset the time to next decrement if the decrementer 1.149 + // is already running, which is the desired behaviour because we just got 1.150 + // a new low-mem notification. 1.151 + mPressureDecrementer.start(); 1.152 + 1.153 + if (oldLevel == level) { 1.154 + // if we're not going to a higher level we probably don't 1.155 + // need to run another round of the same memory reductions 1.156 + // we did on the last memory pressure increase. 1.157 + return false; 1.158 + } 1.159 + 1.160 + // TODO hook in memory-reduction stuff for different levels here 1.161 + if (level >= MEMORY_PRESSURE_MEDIUM) { 1.162 + //Only send medium or higher events because that's all that is used right now 1.163 + if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { 1.164 + GeckoAppShell.dispatchMemoryPressure(); 1.165 + } 1.166 + 1.167 + Favicons.clearMemCache(); 1.168 + } 1.169 + return true; 1.170 + } 1.171 + 1.172 + private boolean decreaseMemoryPressure() { 1.173 + int newLevel; 1.174 + synchronized (this) { 1.175 + if (mMemoryPressure <= 0) { 1.176 + return false; 1.177 + } 1.178 + 1.179 + newLevel = --mMemoryPressure; 1.180 + } 1.181 + Log.d(LOGTAG, "Decreased memory pressure to " + newLevel); 1.182 + 1.183 + return true; 1.184 + } 1.185 + 1.186 + class PressureDecrementer implements Runnable { 1.187 + private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes 1.188 + 1.189 + private boolean mPosted; 1.190 + 1.191 + synchronized void start() { 1.192 + if (mPosted) { 1.193 + // cancel the old one before scheduling a new one 1.194 + ThreadUtils.getBackgroundHandler().removeCallbacks(this); 1.195 + } 1.196 + ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY); 1.197 + mPosted = true; 1.198 + } 1.199 + 1.200 + @Override 1.201 + public synchronized void run() { 1.202 + if (!decreaseMemoryPressure()) { 1.203 + // done decrementing, bail out 1.204 + mPosted = false; 1.205 + return; 1.206 + } 1.207 + 1.208 + // need to keep decrementing 1.209 + ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY); 1.210 + } 1.211 + } 1.212 + 1.213 + class StorageReducer implements Runnable { 1.214 + private final Context mContext; 1.215 + public StorageReducer(final Context context) { 1.216 + this.mContext = context; 1.217 + } 1.218 + 1.219 + @Override 1.220 + public void run() { 1.221 + // this might get run right on startup, if so wait 10 seconds and try again 1.222 + if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { 1.223 + ThreadUtils.getBackgroundHandler().postDelayed(this, 10000); 1.224 + return; 1.225 + } 1.226 + 1.227 + if (!mStoragePressure) { 1.228 + // pressure is off, so we can abort 1.229 + return; 1.230 + } 1.231 + 1.232 + BrowserDB.expireHistory(mContext.getContentResolver(), 1.233 + BrowserContract.ExpirePriority.AGGRESSIVE); 1.234 + BrowserDB.removeThumbnails(mContext.getContentResolver()); 1.235 + // TODO: drop or shrink disk caches 1.236 + } 1.237 + } 1.238 +}