mobile/android/base/MemoryMonitor.java

changeset 0
6474c204b198
     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 +}

mercurial