|
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 package org.mozilla.gecko; |
|
7 |
|
8 import org.mozilla.gecko.db.BrowserContract; |
|
9 import org.mozilla.gecko.db.BrowserDB; |
|
10 import org.mozilla.gecko.favicons.Favicons; |
|
11 import org.mozilla.gecko.util.ThreadUtils; |
|
12 |
|
13 import android.content.BroadcastReceiver; |
|
14 import android.content.ComponentCallbacks2; |
|
15 import android.content.Context; |
|
16 import android.content.Intent; |
|
17 import android.content.IntentFilter; |
|
18 import android.os.Build; |
|
19 import android.util.Log; |
|
20 |
|
21 /** |
|
22 * This is a utility class to keep track of how much memory and disk-space pressure |
|
23 * the system is under. It receives input from GeckoActivity via the onLowMemory() and |
|
24 * onTrimMemory() functions, and also listens for some system intents related to |
|
25 * disk-space notifications. Internally it will track how much memory and disk pressure |
|
26 * the system is under, and perform various actions to help alleviate the pressure. |
|
27 * |
|
28 * Note that since there is no notification for when the system has lots of free memory |
|
29 * again, this class also assumes that, over time, the system will free up memory. This |
|
30 * assumption is implemented using a timer that slowly lowers the internal memory |
|
31 * pressure state if no new low-memory notifications are received. |
|
32 * |
|
33 * Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both |
|
34 * of these classes may be accessed from various threads, and have both been designed to |
|
35 * be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock |
|
36 * is allowed to pick up the MemoryMonitor lock, but not vice-versa. |
|
37 */ |
|
38 class MemoryMonitor extends BroadcastReceiver { |
|
39 private static final String LOGTAG = "GeckoMemoryMonitor"; |
|
40 private static final String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP"; |
|
41 private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE"; |
|
42 |
|
43 // Memory pressue levels, keep in sync with those in AndroidJavaWrappers.h |
|
44 private static final int MEMORY_PRESSURE_NONE = 0; |
|
45 private static final int MEMORY_PRESSURE_CLEANUP = 1; |
|
46 private static final int MEMORY_PRESSURE_LOW = 2; |
|
47 private static final int MEMORY_PRESSURE_MEDIUM = 3; |
|
48 private static final int MEMORY_PRESSURE_HIGH = 4; |
|
49 |
|
50 private static MemoryMonitor sInstance = new MemoryMonitor(); |
|
51 |
|
52 static MemoryMonitor getInstance() { |
|
53 return sInstance; |
|
54 } |
|
55 |
|
56 private final PressureDecrementer mPressureDecrementer; |
|
57 private int mMemoryPressure; |
|
58 private boolean mStoragePressure; |
|
59 private boolean mInited; |
|
60 |
|
61 private MemoryMonitor() { |
|
62 mPressureDecrementer = new PressureDecrementer(); |
|
63 mMemoryPressure = MEMORY_PRESSURE_NONE; |
|
64 mStoragePressure = false; |
|
65 } |
|
66 |
|
67 public void init(final Context context) { |
|
68 if (mInited) { |
|
69 return; |
|
70 } |
|
71 |
|
72 IntentFilter filter = new IntentFilter(); |
|
73 filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); |
|
74 filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); |
|
75 filter.addAction(ACTION_MEMORY_DUMP); |
|
76 filter.addAction(ACTION_FORCE_PRESSURE); |
|
77 context.getApplicationContext().registerReceiver(this, filter); |
|
78 mInited = true; |
|
79 } |
|
80 |
|
81 public void onLowMemory() { |
|
82 Log.d(LOGTAG, "onLowMemory() notification received"); |
|
83 if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) { |
|
84 // We need to wait on Gecko here, because if we haven't reduced |
|
85 // memory usage enough when we return from this, Android will kill us. |
|
86 GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createNoOpEvent()); |
|
87 } |
|
88 } |
|
89 |
|
90 public void onTrimMemory(int level) { |
|
91 Log.d(LOGTAG, "onTrimMemory() notification received with level " + level); |
|
92 if (Build.VERSION.SDK_INT < 14) { |
|
93 // this won't even get called pre-ICS |
|
94 return; |
|
95 } |
|
96 |
|
97 if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { |
|
98 increaseMemoryPressure(MEMORY_PRESSURE_HIGH); |
|
99 } else if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { |
|
100 increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM); |
|
101 } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { |
|
102 // includes TRIM_MEMORY_BACKGROUND |
|
103 increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP); |
|
104 } else { |
|
105 // levels down here mean gecko is the foreground process so we |
|
106 // should be less aggressive with wiping memory as it may impact |
|
107 // user experience. |
|
108 increaseMemoryPressure(MEMORY_PRESSURE_LOW); |
|
109 } |
|
110 } |
|
111 |
|
112 @Override |
|
113 public void onReceive(Context context, Intent intent) { |
|
114 if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) { |
|
115 Log.d(LOGTAG, "Device storage is low"); |
|
116 mStoragePressure = true; |
|
117 ThreadUtils.postToBackgroundThread(new StorageReducer(context)); |
|
118 } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) { |
|
119 Log.d(LOGTAG, "Device storage is ok"); |
|
120 mStoragePressure = false; |
|
121 } else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) { |
|
122 String label = intent.getStringExtra("label"); |
|
123 if (label == null) { |
|
124 label = "default"; |
|
125 } |
|
126 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Memory:Dump", label)); |
|
127 } else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) { |
|
128 increaseMemoryPressure(MEMORY_PRESSURE_HIGH); |
|
129 } |
|
130 } |
|
131 |
|
132 private boolean increaseMemoryPressure(int level) { |
|
133 int oldLevel; |
|
134 synchronized (this) { |
|
135 // bump up our level if we're not already higher |
|
136 if (mMemoryPressure > level) { |
|
137 return false; |
|
138 } |
|
139 oldLevel = mMemoryPressure; |
|
140 mMemoryPressure = level; |
|
141 } |
|
142 |
|
143 // since we don't get notifications for when memory pressure is off, |
|
144 // we schedule our own timer to slowly back off the memory pressure level. |
|
145 // note that this will reset the time to next decrement if the decrementer |
|
146 // is already running, which is the desired behaviour because we just got |
|
147 // a new low-mem notification. |
|
148 mPressureDecrementer.start(); |
|
149 |
|
150 if (oldLevel == level) { |
|
151 // if we're not going to a higher level we probably don't |
|
152 // need to run another round of the same memory reductions |
|
153 // we did on the last memory pressure increase. |
|
154 return false; |
|
155 } |
|
156 |
|
157 // TODO hook in memory-reduction stuff for different levels here |
|
158 if (level >= MEMORY_PRESSURE_MEDIUM) { |
|
159 //Only send medium or higher events because that's all that is used right now |
|
160 if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { |
|
161 GeckoAppShell.dispatchMemoryPressure(); |
|
162 } |
|
163 |
|
164 Favicons.clearMemCache(); |
|
165 } |
|
166 return true; |
|
167 } |
|
168 |
|
169 private boolean decreaseMemoryPressure() { |
|
170 int newLevel; |
|
171 synchronized (this) { |
|
172 if (mMemoryPressure <= 0) { |
|
173 return false; |
|
174 } |
|
175 |
|
176 newLevel = --mMemoryPressure; |
|
177 } |
|
178 Log.d(LOGTAG, "Decreased memory pressure to " + newLevel); |
|
179 |
|
180 return true; |
|
181 } |
|
182 |
|
183 class PressureDecrementer implements Runnable { |
|
184 private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes |
|
185 |
|
186 private boolean mPosted; |
|
187 |
|
188 synchronized void start() { |
|
189 if (mPosted) { |
|
190 // cancel the old one before scheduling a new one |
|
191 ThreadUtils.getBackgroundHandler().removeCallbacks(this); |
|
192 } |
|
193 ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY); |
|
194 mPosted = true; |
|
195 } |
|
196 |
|
197 @Override |
|
198 public synchronized void run() { |
|
199 if (!decreaseMemoryPressure()) { |
|
200 // done decrementing, bail out |
|
201 mPosted = false; |
|
202 return; |
|
203 } |
|
204 |
|
205 // need to keep decrementing |
|
206 ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY); |
|
207 } |
|
208 } |
|
209 |
|
210 class StorageReducer implements Runnable { |
|
211 private final Context mContext; |
|
212 public StorageReducer(final Context context) { |
|
213 this.mContext = context; |
|
214 } |
|
215 |
|
216 @Override |
|
217 public void run() { |
|
218 // this might get run right on startup, if so wait 10 seconds and try again |
|
219 if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { |
|
220 ThreadUtils.getBackgroundHandler().postDelayed(this, 10000); |
|
221 return; |
|
222 } |
|
223 |
|
224 if (!mStoragePressure) { |
|
225 // pressure is off, so we can abort |
|
226 return; |
|
227 } |
|
228 |
|
229 BrowserDB.expireHistory(mContext.getContentResolver(), |
|
230 BrowserContract.ExpirePriority.AGGRESSIVE); |
|
231 BrowserDB.removeThumbnails(mContext.getContentResolver()); |
|
232 // TODO: drop or shrink disk caches |
|
233 } |
|
234 } |
|
235 } |