Bug 713032 - Add a MemoryMonitor class that will be the brains behind low-memory condition handling. r=snorp
authorKartikaya Gupta <kgupta@mozilla.com>
Wed, 19 Sep 2012 15:21:19 -0400
changeset 107646 fdc9029194ed3e22c875978826230aa859b9135d
parent 107645 3073e4bb03ffdee7a987714ca3da4ef768f19912
child 107648 58f286147a9a2e35c497a6d9736595f6f53350e9
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewerssnorp
bugs713032
milestone18.0a1
Bug 713032 - Add a MemoryMonitor class that will be the brains behind low-memory condition handling. r=snorp
mobile/android/base/GeckoActivity.java.in
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoApplication.java
mobile/android/base/Makefile.in
mobile/android/base/MemoryMonitor.java
--- a/mobile/android/base/GeckoActivity.java.in
+++ b/mobile/android/base/GeckoActivity.java.in
@@ -22,10 +22,22 @@ public class GeckoActivity extends Activ
 
         if (getApplication() instanceof GeckoApplication) {
             ((GeckoApplication) getApplication()).onActivityResume(this);
         }
     }
 
     public boolean isApplicationInBackground() {
         return ((GeckoApplication) getApplication()).isApplicationInBackground();
-    } 
+    }
+
+    @Override
+    public void onLowMemory() {
+        MemoryMonitor.getInstance().onLowMemory();
+        super.onLowMemory();
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        MemoryMonitor.getInstance().onTrimMemory(level);
+        super.onTrimMemory(level);
+    }
 }
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -2181,26 +2181,16 @@ abstract public class GeckoApp
             if (mFormAssistPopup != null)
                 mFormAssistPopup.hide();
             SiteIdentityPopup.getInstance().dismiss();
             refreshChrome();
         }
     }
 
     @Override
-    public void onLowMemory()
-    {
-        Log.e(LOGTAG, "low memory");
-        if (checkLaunchState(LaunchState.GeckoRunning))
-            GeckoAppShell.onLowMemory();
-        super.onLowMemory();
-        GeckoAppShell.geckoEventSync();
-    }
-
-    @Override
     public Object onRetainNonConfigurationInstance() {
         // Send a non-null value so that we can restart the application, 
         // when activity restarts due to configuration change.
         return new Boolean(true);
     } 
 
     abstract public String getPackageName();
     abstract public String getContentProcessName();
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -20,16 +20,17 @@ public class GeckoApplication extends Ap
         } catch (ClassNotFoundException e) {}
 
         super.onCreate();
 
         GeckoConnectivityReceiver.getInstance().init(getApplicationContext());
         GeckoBatteryManager.getInstance().init(getApplicationContext());
         GeckoBatteryManager.getInstance().start();
         GeckoNetworkManager.getInstance().init(getApplicationContext());
+        MemoryMonitor.getInstance().init(getApplicationContext());
     }
 
     protected void onActivityPause(GeckoActivity activity) {
         mInBackground = true;
 
         GeckoAppShell.sendEventToGecko(GeckoEvent.createPauseEvent(true));
         GeckoConnectivityReceiver.getInstance().stop();
         GeckoNetworkManager.getInstance().stop();
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -85,16 +85,17 @@ FENNEC_JAVA_FILES = \
   GeckoPreferences.java \
   GeckoProfile.java \
   GeckoThread.java \
   GlobalHistory.java \
   GeckoViewsFactory.java \
   InputMethods.java \
   LinkPreference.java \
   LinkTextView.java \
+  MemoryMonitor.java \
   MenuButton.java \
   MenuItemActionBar.java \
   MenuItemDefault.java \
   MultiChoicePreference.java \
   NSSBridge.java \
   CustomEditText.java \
   OnInterceptTouchListener.java \
   PrefsHelper.java \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/MemoryMonitor.java
@@ -0,0 +1,182 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Debug;
+import android.util.Log;
+
+/**
+  * This is a utility class to keep track of how much memory and disk-space pressure
+  * the system is under. It receives input from GeckoActivity via the onLowMemory() and
+  * onTrimMemory() functions, and also listens for some system intents related to
+  * disk-space notifications. Internally it will track how much memory and disk pressure
+  * the system is under, and perform various actions to help alleviate the pressure.
+  *
+  * Note that since there is no notification for when the system has lots of free memory
+  * again, this class also assumes that, over time, the system will free up memory. This
+  * assumption is implemented using a timer that slowly lowers the internal memory
+  * pressure state if no new low-memory notifications are received.
+  *
+  * Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both
+  * of these classes may be accessed from various threads, and have both been designed to
+  * be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock
+  * is allowed to pick up the MemoryMonitor lock, but not vice-versa.
+  */
+class MemoryMonitor extends BroadcastReceiver {
+    private static final String LOGTAG = "GeckoMemoryMonitor";
+
+    private static final int MEMORY_PRESSURE_NONE = 0;
+    private static final int MEMORY_PRESSURE_CLEANUP = 1;
+    private static final int MEMORY_PRESSURE_LOW = 2;
+    private static final int MEMORY_PRESSURE_MEDIUM = 3;
+    private static final int MEMORY_PRESSURE_HIGH = 4;
+
+    private static MemoryMonitor sInstance = new MemoryMonitor();
+
+    static MemoryMonitor getInstance() {
+        return sInstance;
+    }
+
+    private final PressureDecrementer mPressureDecrementer;
+    private int mMemoryPressure;
+    private boolean mStoragePressure;
+
+    private MemoryMonitor() {
+        mPressureDecrementer = new PressureDecrementer();
+        mMemoryPressure = MEMORY_PRESSURE_NONE;
+        mStoragePressure = false;
+    }
+
+    public void init(Context context) {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
+        filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+        context.getApplicationContext().registerReceiver(this, filter);
+    }
+
+    public void onLowMemory() {
+        Log.d(LOGTAG, "onLowMemory() notification received");
+        increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
+    }
+
+    public void onTrimMemory(int level) {
+        Log.d(LOGTAG, "onTrimMemory() notification received with level " + level);
+        if (Build.VERSION.SDK_INT < 14) {
+            // this won't even get called pre-ICS
+            return;
+        }
+
+        if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
+            increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
+        } else if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+            increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM);
+        } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
+            // includes TRIM_MEMORY_BACKGROUND
+            increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP);
+        } else {
+            if (Build.VERSION.SDK_INT < 16) {
+                // in SDK 14 and 15 we don't have these extra fine-grained levels so
+                // just default to low (we already know it's < TRIM_MEMORY_UI_HIDDEN)
+                increaseMemoryPressure(MEMORY_PRESSURE_LOW);
+            } else if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
+                increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
+            } else if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
+                increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM);
+            } else {
+                increaseMemoryPressure(MEMORY_PRESSURE_LOW);
+            }
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) {
+            Log.d(LOGTAG, "Device storage is low");
+            mStoragePressure = true;
+            // TODO: drop or shrink disk caches
+            // TODO: drop stuff from browser.db
+        } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
+            Log.d(LOGTAG, "Device storage is ok");
+            mStoragePressure = false;
+        }
+    }
+
+    private void increaseMemoryPressure(int level) {
+        int oldLevel;
+        synchronized (this) {
+            // bump up our level if we're not already higher
+            if (mMemoryPressure > level) {
+                return;
+            }
+            oldLevel = mMemoryPressure;
+            mMemoryPressure = level;
+        }
+
+        // since we don't get notifications for when memory pressure is off,
+        // we schedule our own timer to slowly back off the memory pressure level.
+        // note that this will reset the time to next decrement if the decrementer
+        // is already running, which is the desired behaviour because we just got
+        // a new low-mem notification.
+        mPressureDecrementer.start();
+
+        if (oldLevel == level) {
+            // if we're not going to a higher level we probably don't
+            // need to run another round of the same memory reductions
+            // we did on the last memory pressure increase.
+            return;
+        }
+
+        // TODO hook in memory-reduction stuff for different levels here
+        if (level >= MEMORY_PRESSURE_MEDIUM) {
+            if (GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning)) {
+                GeckoAppShell.onLowMemory();
+            }
+            GeckoAppShell.geckoEventSync();
+        }
+    }
+
+    private synchronized boolean decreaseMemoryPressure() {
+        if (mMemoryPressure > 0) {
+            mMemoryPressure--;
+            Log.d(LOGTAG, "Decreased memory pressure to " + mMemoryPressure);
+            return true;
+        }
+        return false;
+    }
+
+    class PressureDecrementer implements Runnable {
+        private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes
+
+        private boolean mPosted;
+
+        synchronized void start() {
+            if (mPosted) {
+                // cancel the old one before scheduling a new one
+                GeckoAppShell.getHandler().removeCallbacks(this);
+            }
+            GeckoAppShell.getHandler().postDelayed(this, DECREMENT_DELAY);
+            mPosted = true;
+        }
+
+        @Override
+        public synchronized void run() {
+            if (!decreaseMemoryPressure()) {
+                // done decrementing, bail out
+                mPosted = false;
+                return;
+            }
+
+            // need to keep decrementing
+            GeckoAppShell.getHandler().postDelayed(this, DECREMENT_DELAY);
+        }
+    }
+}