michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.db.BrowserContract; michael@0: import org.mozilla.gecko.db.BrowserDB; michael@0: import org.mozilla.gecko.favicons.Favicons; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.content.BroadcastReceiver; michael@0: import android.content.ComponentCallbacks2; michael@0: import android.content.Context; michael@0: import android.content.Intent; michael@0: import android.content.IntentFilter; michael@0: import android.os.Build; michael@0: import android.util.Log; michael@0: michael@0: /** michael@0: * This is a utility class to keep track of how much memory and disk-space pressure michael@0: * the system is under. It receives input from GeckoActivity via the onLowMemory() and michael@0: * onTrimMemory() functions, and also listens for some system intents related to michael@0: * disk-space notifications. Internally it will track how much memory and disk pressure michael@0: * the system is under, and perform various actions to help alleviate the pressure. michael@0: * michael@0: * Note that since there is no notification for when the system has lots of free memory michael@0: * again, this class also assumes that, over time, the system will free up memory. This michael@0: * assumption is implemented using a timer that slowly lowers the internal memory michael@0: * pressure state if no new low-memory notifications are received. michael@0: * michael@0: * Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both michael@0: * of these classes may be accessed from various threads, and have both been designed to michael@0: * be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock michael@0: * is allowed to pick up the MemoryMonitor lock, but not vice-versa. michael@0: */ michael@0: class MemoryMonitor extends BroadcastReceiver { michael@0: private static final String LOGTAG = "GeckoMemoryMonitor"; michael@0: private static final String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP"; michael@0: private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE"; michael@0: michael@0: // Memory pressue levels, keep in sync with those in AndroidJavaWrappers.h michael@0: private static final int MEMORY_PRESSURE_NONE = 0; michael@0: private static final int MEMORY_PRESSURE_CLEANUP = 1; michael@0: private static final int MEMORY_PRESSURE_LOW = 2; michael@0: private static final int MEMORY_PRESSURE_MEDIUM = 3; michael@0: private static final int MEMORY_PRESSURE_HIGH = 4; michael@0: michael@0: private static MemoryMonitor sInstance = new MemoryMonitor(); michael@0: michael@0: static MemoryMonitor getInstance() { michael@0: return sInstance; michael@0: } michael@0: michael@0: private final PressureDecrementer mPressureDecrementer; michael@0: private int mMemoryPressure; michael@0: private boolean mStoragePressure; michael@0: private boolean mInited; michael@0: michael@0: private MemoryMonitor() { michael@0: mPressureDecrementer = new PressureDecrementer(); michael@0: mMemoryPressure = MEMORY_PRESSURE_NONE; michael@0: mStoragePressure = false; michael@0: } michael@0: michael@0: public void init(final Context context) { michael@0: if (mInited) { michael@0: return; michael@0: } michael@0: michael@0: IntentFilter filter = new IntentFilter(); michael@0: filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); michael@0: filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); michael@0: filter.addAction(ACTION_MEMORY_DUMP); michael@0: filter.addAction(ACTION_FORCE_PRESSURE); michael@0: context.getApplicationContext().registerReceiver(this, filter); michael@0: mInited = true; michael@0: } michael@0: michael@0: public void onLowMemory() { michael@0: Log.d(LOGTAG, "onLowMemory() notification received"); michael@0: if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) { michael@0: // We need to wait on Gecko here, because if we haven't reduced michael@0: // memory usage enough when we return from this, Android will kill us. michael@0: GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createNoOpEvent()); michael@0: } michael@0: } michael@0: michael@0: public void onTrimMemory(int level) { michael@0: Log.d(LOGTAG, "onTrimMemory() notification received with level " + level); michael@0: if (Build.VERSION.SDK_INT < 14) { michael@0: // this won't even get called pre-ICS michael@0: return; michael@0: } michael@0: michael@0: if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { michael@0: increaseMemoryPressure(MEMORY_PRESSURE_HIGH); michael@0: } else if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { michael@0: increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM); michael@0: } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { michael@0: // includes TRIM_MEMORY_BACKGROUND michael@0: increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP); michael@0: } else { michael@0: // levels down here mean gecko is the foreground process so we michael@0: // should be less aggressive with wiping memory as it may impact michael@0: // user experience. michael@0: increaseMemoryPressure(MEMORY_PRESSURE_LOW); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onReceive(Context context, Intent intent) { michael@0: if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) { michael@0: Log.d(LOGTAG, "Device storage is low"); michael@0: mStoragePressure = true; michael@0: ThreadUtils.postToBackgroundThread(new StorageReducer(context)); michael@0: } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) { michael@0: Log.d(LOGTAG, "Device storage is ok"); michael@0: mStoragePressure = false; michael@0: } else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) { michael@0: String label = intent.getStringExtra("label"); michael@0: if (label == null) { michael@0: label = "default"; michael@0: } michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Memory:Dump", label)); michael@0: } else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) { michael@0: increaseMemoryPressure(MEMORY_PRESSURE_HIGH); michael@0: } michael@0: } michael@0: michael@0: private boolean increaseMemoryPressure(int level) { michael@0: int oldLevel; michael@0: synchronized (this) { michael@0: // bump up our level if we're not already higher michael@0: if (mMemoryPressure > level) { michael@0: return false; michael@0: } michael@0: oldLevel = mMemoryPressure; michael@0: mMemoryPressure = level; michael@0: } michael@0: michael@0: // since we don't get notifications for when memory pressure is off, michael@0: // we schedule our own timer to slowly back off the memory pressure level. michael@0: // note that this will reset the time to next decrement if the decrementer michael@0: // is already running, which is the desired behaviour because we just got michael@0: // a new low-mem notification. michael@0: mPressureDecrementer.start(); michael@0: michael@0: if (oldLevel == level) { michael@0: // if we're not going to a higher level we probably don't michael@0: // need to run another round of the same memory reductions michael@0: // we did on the last memory pressure increase. michael@0: return false; michael@0: } michael@0: michael@0: // TODO hook in memory-reduction stuff for different levels here michael@0: if (level >= MEMORY_PRESSURE_MEDIUM) { michael@0: //Only send medium or higher events because that's all that is used right now michael@0: if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { michael@0: GeckoAppShell.dispatchMemoryPressure(); michael@0: } michael@0: michael@0: Favicons.clearMemCache(); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: private boolean decreaseMemoryPressure() { michael@0: int newLevel; michael@0: synchronized (this) { michael@0: if (mMemoryPressure <= 0) { michael@0: return false; michael@0: } michael@0: michael@0: newLevel = --mMemoryPressure; michael@0: } michael@0: Log.d(LOGTAG, "Decreased memory pressure to " + newLevel); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: class PressureDecrementer implements Runnable { michael@0: private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes michael@0: michael@0: private boolean mPosted; michael@0: michael@0: synchronized void start() { michael@0: if (mPosted) { michael@0: // cancel the old one before scheduling a new one michael@0: ThreadUtils.getBackgroundHandler().removeCallbacks(this); michael@0: } michael@0: ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY); michael@0: mPosted = true; michael@0: } michael@0: michael@0: @Override michael@0: public synchronized void run() { michael@0: if (!decreaseMemoryPressure()) { michael@0: // done decrementing, bail out michael@0: mPosted = false; michael@0: return; michael@0: } michael@0: michael@0: // need to keep decrementing michael@0: ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY); michael@0: } michael@0: } michael@0: michael@0: class StorageReducer implements Runnable { michael@0: private final Context mContext; michael@0: public StorageReducer(final Context context) { michael@0: this.mContext = context; michael@0: } michael@0: michael@0: @Override michael@0: public void run() { michael@0: // this might get run right on startup, if so wait 10 seconds and try again michael@0: if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { michael@0: ThreadUtils.getBackgroundHandler().postDelayed(this, 10000); michael@0: return; michael@0: } michael@0: michael@0: if (!mStoragePressure) { michael@0: // pressure is off, so we can abort michael@0: return; michael@0: } michael@0: michael@0: BrowserDB.expireHistory(mContext.getContentResolver(), michael@0: BrowserContract.ExpirePriority.AGGRESSIVE); michael@0: BrowserDB.removeThumbnails(mContext.getContentResolver()); michael@0: // TODO: drop or shrink disk caches michael@0: } michael@0: } michael@0: }