summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
blob: 94ca761b96aefc9aa55698f9f090aed69bace832 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/* -*- 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 org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserProvider;
import org.mozilla.gecko.home.ImageLoader;
import org.mozilla.gecko.icons.storage.MemoryStorage;
import org.mozilla.gecko.util.ThreadUtils;

import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks2;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.content.LocalBroadcastManager;
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 String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP";
    private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE";

    // Memory pressure levels. Keep these in sync with those in AndroidJavaWrappers.h
    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 final MemoryMonitor sInstance = new MemoryMonitor();

    static MemoryMonitor getInstance() {
        return sInstance;
    }

    private Context mAppContext;
    private final PressureDecrementer mPressureDecrementer;
    private int mMemoryPressure;                  // Synchronized access only.
    private volatile boolean mStoragePressure;    // Accessed via UI thread intent, background runnables.
    private boolean mInited;

    private MemoryMonitor() {
        mPressureDecrementer = new PressureDecrementer();
        mMemoryPressure = MEMORY_PRESSURE_NONE;
    }

    public void init(final Context context) {
        if (mInited) {
            return;
        }

        mAppContext = context.getApplicationContext();
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
        filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
        filter.addAction(ACTION_MEMORY_DUMP);
        filter.addAction(ACTION_FORCE_PRESSURE);
        mAppContext.registerReceiver(this, filter);
        mInited = true;
    }

    public void onLowMemory() {
        Log.d(LOGTAG, "onLowMemory() notification received");
        if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) {
            // We need to wait on Gecko here, because if we haven't reduced
            // memory usage enough when we return from this, Android will kill us.
            if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
                GeckoThread.waitOnGecko();
            }
        }
    }

    public void onTrimMemory(int level) {
        Log.d(LOGTAG, "onTrimMemory() notification received with level " + level);
        if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
            // We seem to get this just by entering the task switcher or hitting the home button.
            // Seems bogus, because we are the foreground app, or at least not at the end of the LRU list.
            // Just ignore it, and if there is a real memory pressure event (CRITICAL, MODERATE, etc),
            // we'll respond appropriately.
            return;
        }

        switch (level) {
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                // TRIM_MEMORY_MODERATE is the highest level we'll respond to while backgrounded
                increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
                break;
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
                increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM);
                break;
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
                increaseMemoryPressure(MEMORY_PRESSURE_LOW);
                break;
            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
                increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP);
                break;
            default:
                Log.d(LOGTAG, "Unhandled onTrimMemory() level " + level);
                break;
        }
    }

    @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;
            ThreadUtils.postToBackgroundThread(new StorageReducer(context));
        } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
            Log.d(LOGTAG, "Device storage is ok");
            mStoragePressure = false;
        } else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) {
            String label = intent.getStringExtra("label");
            if (label == null) {
                label = "default";
            }
            GeckoAppShell.notifyObservers("Memory:Dump", label);
        } else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) {
            increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
        }
    }

    @WrapForJNI(calledFrom = "ui")
    private static native void dispatchMemoryPressure();

    private boolean increaseMemoryPressure(int level) {
        int oldLevel;
        synchronized (this) {
            // bump up our level if we're not already higher
            if (mMemoryPressure > level) {
                return false;
            }
            oldLevel = mMemoryPressure;
            mMemoryPressure = level;
        }

        Log.d(LOGTAG, "increasing memory pressure to " + 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 false;
        }

        // TODO hook in memory-reduction stuff for different levels here
        if (level >= MEMORY_PRESSURE_MEDIUM) {
            //Only send medium or higher events because that's all that is used right now
            if (GeckoThread.isRunning()) {
                dispatchMemoryPressure();
            }

            MemoryStorage.get().evictAll();
            ImageLoader.clearLruCache();
            LocalBroadcastManager.getInstance(mAppContext)
                    .sendBroadcast(new Intent(BrowserProvider.ACTION_SHRINK_MEMORY));
        }
        return true;
    }

    /**
     * Thread-safe due to mStoragePressure's volatility.
     */
    boolean isUnderStoragePressure() {
        return mStoragePressure;
    }

    private boolean decreaseMemoryPressure() {
        int newLevel;
        synchronized (this) {
            if (mMemoryPressure <= 0) {
                return false;
            }

            newLevel = --mMemoryPressure;
        }
        Log.d(LOGTAG, "Decreased memory pressure to " + newLevel);

        return true;
    }

    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
                ThreadUtils.getBackgroundHandler().removeCallbacks(this);
            }
            ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
            mPosted = true;
        }

        @Override
        public synchronized void run() {
            if (!decreaseMemoryPressure()) {
                // done decrementing, bail out
                mPosted = false;
                return;
            }

            // need to keep decrementing
            ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
        }
    }

    private static class StorageReducer implements Runnable {
        private final Context mContext;
        private final BrowserDB mDB;

        public StorageReducer(final Context context) {
            this.mContext = context;
            // Since this may be called while Fennec is in the background, we don't want to risk accidentally
            // using the wrong context. If the profile we get is a guest profile, use the default profile instead.
            GeckoProfile profile = GeckoProfile.get(mContext);
            if (profile.inGuestMode()) {
                // If it was the guest profile, switch to the default one.
                profile = GeckoProfile.get(mContext, GeckoProfile.DEFAULT_PROFILE);
            }

            mDB = BrowserDB.from(profile);
        }

        @Override
        public void run() {
            // this might get run right on startup, if so wait 10 seconds and try again
            if (!GeckoThread.isRunning()) {
                ThreadUtils.getBackgroundHandler().postDelayed(this, 10000);
                return;
            }

            if (!MemoryMonitor.getInstance().isUnderStoragePressure()) {
                // Pressure is off, so we can abort.
                return;
            }

            final ContentResolver cr = mContext.getContentResolver();
            mDB.expireHistory(cr, BrowserContract.ExpirePriority.AGGRESSIVE);
            mDB.removeThumbnails(cr);

            // TODO: drop or shrink disk caches
        }
    }
}