|
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
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 file, |
|
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 #include "nsComponentManagerUtils.h" |
|
7 #include "nsITimer.h" |
|
8 #include "RasterImage.h" |
|
9 #include "DiscardTracker.h" |
|
10 #include "mozilla/Preferences.h" |
|
11 |
|
12 namespace mozilla { |
|
13 namespace image { |
|
14 |
|
15 static const char* sDiscardTimeoutPref = "image.mem.min_discard_timeout_ms"; |
|
16 |
|
17 /* static */ LinkedList<DiscardTracker::Node> DiscardTracker::sDiscardableImages; |
|
18 /* static */ nsCOMPtr<nsITimer> DiscardTracker::sTimer; |
|
19 /* static */ bool DiscardTracker::sInitialized = false; |
|
20 /* static */ bool DiscardTracker::sTimerOn = false; |
|
21 /* static */ Atomic<bool> DiscardTracker::sDiscardRunnablePending(false); |
|
22 /* static */ uint64_t DiscardTracker::sCurrentDecodedImageBytes = 0; |
|
23 /* static */ uint32_t DiscardTracker::sMinDiscardTimeoutMs = 10000; |
|
24 /* static */ uint32_t DiscardTracker::sMaxDecodedImageKB = 42 * 1024; |
|
25 /* static */ uint32_t DiscardTracker::sHardLimitDecodedImageKB = 0; |
|
26 /* static */ PRLock * DiscardTracker::sAllocationLock = nullptr; |
|
27 /* static */ mozilla::Mutex* DiscardTracker::sNodeListMutex = nullptr; |
|
28 /* static */ Atomic<bool> DiscardTracker::sShutdown(false); |
|
29 |
|
30 /* |
|
31 * When we notice we're using too much memory for decoded images, we enqueue a |
|
32 * DiscardRunnable, which runs this code. |
|
33 */ |
|
34 NS_IMETHODIMP |
|
35 DiscardTracker::DiscardRunnable::Run() |
|
36 { |
|
37 sDiscardRunnablePending = false; |
|
38 |
|
39 DiscardTracker::DiscardNow(); |
|
40 return NS_OK; |
|
41 } |
|
42 |
|
43 void |
|
44 DiscardTimeoutChangedCallback(const char* aPref, void *aClosure) |
|
45 { |
|
46 DiscardTracker::ReloadTimeout(); |
|
47 } |
|
48 |
|
49 nsresult |
|
50 DiscardTracker::Reset(Node *node) |
|
51 { |
|
52 // We shouldn't call Reset() with a null |img| pointer, on images which can't |
|
53 // be discarded, or on animated images (which should be marked as |
|
54 // non-discardable, anyway). |
|
55 MutexAutoLock lock(*sNodeListMutex); |
|
56 MOZ_ASSERT(sInitialized); |
|
57 MOZ_ASSERT(node->img); |
|
58 MOZ_ASSERT(node->img->CanDiscard()); |
|
59 MOZ_ASSERT(!node->img->mAnim); |
|
60 |
|
61 // Insert the node at the front of the list and note when it was inserted. |
|
62 bool wasInList = node->isInList(); |
|
63 if (wasInList) { |
|
64 node->remove(); |
|
65 } |
|
66 node->timestamp = TimeStamp::Now(); |
|
67 sDiscardableImages.insertFront(node); |
|
68 |
|
69 // If the node wasn't already in the list of discardable images, then we may |
|
70 // need to discard some images to stay under the sMaxDecodedImageKB limit. |
|
71 // Call MaybeDiscardSoon to do this check. |
|
72 if (!wasInList) { |
|
73 MaybeDiscardSoon(); |
|
74 } |
|
75 |
|
76 // Make sure the timer is running. |
|
77 nsresult rv = EnableTimer(); |
|
78 NS_ENSURE_SUCCESS(rv,rv); |
|
79 |
|
80 return NS_OK; |
|
81 } |
|
82 |
|
83 void |
|
84 DiscardTracker::Remove(Node *node) |
|
85 { |
|
86 if (sShutdown) { |
|
87 // Already shutdown. List should be empty, so just return. |
|
88 return; |
|
89 } |
|
90 MutexAutoLock lock(*sNodeListMutex); |
|
91 |
|
92 if (node->isInList()) |
|
93 node->remove(); |
|
94 |
|
95 if (sDiscardableImages.isEmpty()) |
|
96 DisableTimer(); |
|
97 } |
|
98 |
|
99 /** |
|
100 * Shut down the tracker, deallocating the timer. |
|
101 */ |
|
102 void |
|
103 DiscardTracker::Shutdown() |
|
104 { |
|
105 sShutdown = true; |
|
106 |
|
107 if (sTimer) { |
|
108 sTimer->Cancel(); |
|
109 sTimer = nullptr; |
|
110 } |
|
111 |
|
112 // Clear the sDiscardableImages linked list so that its destructor |
|
113 // (LinkedList.h) finds an empty array, which is required after bug 803688. |
|
114 DiscardAll(); |
|
115 |
|
116 delete sNodeListMutex; |
|
117 sNodeListMutex = nullptr; |
|
118 } |
|
119 |
|
120 /* |
|
121 * Discard all the images we're tracking. |
|
122 */ |
|
123 void |
|
124 DiscardTracker::DiscardAll() |
|
125 { |
|
126 MutexAutoLock lock(*sNodeListMutex); |
|
127 |
|
128 if (!sInitialized) |
|
129 return; |
|
130 |
|
131 // Be careful: Calling Discard() on an image might cause it to be removed |
|
132 // from the list! |
|
133 Node *n; |
|
134 while ((n = sDiscardableImages.popFirst())) { |
|
135 n->img->Discard(); |
|
136 } |
|
137 |
|
138 // The list is empty, so there's no need to leave the timer on. |
|
139 DisableTimer(); |
|
140 } |
|
141 |
|
142 /* static */ bool |
|
143 DiscardTracker::TryAllocation(uint64_t aBytes) |
|
144 { |
|
145 MOZ_ASSERT(sInitialized); |
|
146 |
|
147 PR_Lock(sAllocationLock); |
|
148 bool enoughSpace = |
|
149 !sHardLimitDecodedImageKB || |
|
150 (sHardLimitDecodedImageKB * 1024) - sCurrentDecodedImageBytes >= aBytes; |
|
151 |
|
152 if (enoughSpace) { |
|
153 sCurrentDecodedImageBytes += aBytes; |
|
154 } |
|
155 PR_Unlock(sAllocationLock); |
|
156 |
|
157 // If we're using too much memory for decoded images, MaybeDiscardSoon will |
|
158 // enqueue a callback to discard some images. |
|
159 MaybeDiscardSoon(); |
|
160 |
|
161 return enoughSpace; |
|
162 } |
|
163 |
|
164 /* static */ void |
|
165 DiscardTracker::InformDeallocation(uint64_t aBytes) |
|
166 { |
|
167 // This function is called back e.g. from RasterImage::Discard(); be careful! |
|
168 |
|
169 MOZ_ASSERT(sInitialized); |
|
170 |
|
171 PR_Lock(sAllocationLock); |
|
172 MOZ_ASSERT(aBytes <= sCurrentDecodedImageBytes); |
|
173 sCurrentDecodedImageBytes -= aBytes; |
|
174 PR_Unlock(sAllocationLock); |
|
175 } |
|
176 |
|
177 /** |
|
178 * Initialize the tracker. |
|
179 */ |
|
180 nsresult |
|
181 DiscardTracker::Initialize() |
|
182 { |
|
183 // Watch the timeout pref for changes. |
|
184 Preferences::RegisterCallback(DiscardTimeoutChangedCallback, |
|
185 sDiscardTimeoutPref); |
|
186 |
|
187 Preferences::AddUintVarCache(&sMaxDecodedImageKB, |
|
188 "image.mem.max_decoded_image_kb", |
|
189 50 * 1024); |
|
190 |
|
191 Preferences::AddUintVarCache(&sHardLimitDecodedImageKB, |
|
192 "image.mem.hard_limit_decoded_image_kb", |
|
193 0); |
|
194 // Create the timer. |
|
195 sTimer = do_CreateInstance("@mozilla.org/timer;1"); |
|
196 |
|
197 // Create a lock for safegarding the 64-bit sCurrentDecodedImageBytes |
|
198 sAllocationLock = PR_NewLock(); |
|
199 |
|
200 // Create a lock for the node list. |
|
201 MOZ_ASSERT(!sNodeListMutex); |
|
202 sNodeListMutex = new Mutex("image::DiscardTracker"); |
|
203 |
|
204 // Mark us as initialized |
|
205 sInitialized = true; |
|
206 |
|
207 // Read the timeout pref and start the timer. |
|
208 ReloadTimeout(); |
|
209 |
|
210 return NS_OK; |
|
211 } |
|
212 |
|
213 /** |
|
214 * Read the discard timeout from about:config. |
|
215 */ |
|
216 void |
|
217 DiscardTracker::ReloadTimeout() |
|
218 { |
|
219 // Read the timeout pref. |
|
220 int32_t discardTimeout; |
|
221 nsresult rv = Preferences::GetInt(sDiscardTimeoutPref, &discardTimeout); |
|
222 |
|
223 // If we got something bogus, return. |
|
224 if (!NS_SUCCEEDED(rv) || discardTimeout <= 0) |
|
225 return; |
|
226 |
|
227 // If the value didn't change, return. |
|
228 if ((uint32_t) discardTimeout == sMinDiscardTimeoutMs) |
|
229 return; |
|
230 |
|
231 // Update the value. |
|
232 sMinDiscardTimeoutMs = (uint32_t) discardTimeout; |
|
233 |
|
234 // Restart the timer so the new timeout takes effect. |
|
235 DisableTimer(); |
|
236 EnableTimer(); |
|
237 } |
|
238 |
|
239 /** |
|
240 * Enables the timer. No-op if the timer is already running. |
|
241 */ |
|
242 nsresult |
|
243 DiscardTracker::EnableTimer() |
|
244 { |
|
245 // Nothing to do if the timer's already on or we haven't yet been |
|
246 // initialized. !sTimer probably means we've shut down, so just ignore that, |
|
247 // too. |
|
248 if (sTimerOn || !sInitialized || !sTimer) |
|
249 return NS_OK; |
|
250 |
|
251 sTimerOn = true; |
|
252 |
|
253 // Activate the timer. Have it call us back in (sMinDiscardTimeoutMs / 2) |
|
254 // ms, so that an image is discarded between sMinDiscardTimeoutMs and |
|
255 // (3/2 * sMinDiscardTimeoutMs) ms after it's unlocked. |
|
256 return sTimer->InitWithFuncCallback(TimerCallback, |
|
257 nullptr, |
|
258 sMinDiscardTimeoutMs / 2, |
|
259 nsITimer::TYPE_REPEATING_SLACK); |
|
260 } |
|
261 |
|
262 /* |
|
263 * Disables the timer. No-op if the timer isn't running. |
|
264 */ |
|
265 void |
|
266 DiscardTracker::DisableTimer() |
|
267 { |
|
268 // Nothing to do if the timer's already off. |
|
269 if (!sTimerOn || !sTimer) |
|
270 return; |
|
271 sTimerOn = false; |
|
272 |
|
273 // Deactivate |
|
274 sTimer->Cancel(); |
|
275 } |
|
276 |
|
277 /** |
|
278 * Routine activated when the timer fires. This discards everything that's |
|
279 * older than sMinDiscardTimeoutMs, and tries to discard enough images so that |
|
280 * we go under sMaxDecodedImageKB. |
|
281 */ |
|
282 void |
|
283 DiscardTracker::TimerCallback(nsITimer *aTimer, void *aClosure) |
|
284 { |
|
285 DiscardNow(); |
|
286 } |
|
287 |
|
288 void |
|
289 DiscardTracker::DiscardNow() |
|
290 { |
|
291 // Assuming the list is ordered with oldest discard tracker nodes at the back |
|
292 // and newest ones at the front, iterate from back to front discarding nodes |
|
293 // until we encounter one which is new enough to keep and until we go under |
|
294 // our sMaxDecodedImageKB limit. |
|
295 |
|
296 TimeStamp now = TimeStamp::Now(); |
|
297 Node* node; |
|
298 while ((node = sDiscardableImages.getLast())) { |
|
299 if ((now - node->timestamp).ToMilliseconds() > sMinDiscardTimeoutMs || |
|
300 sCurrentDecodedImageBytes > sMaxDecodedImageKB * 1024) { |
|
301 |
|
302 // Discarding the image should cause sCurrentDecodedImageBytes to |
|
303 // decrease via a call to InformDeallocation(). |
|
304 node->img->Discard(); |
|
305 |
|
306 // Careful: Discarding may have caused the node to have been removed |
|
307 // from the list. |
|
308 Remove(node); |
|
309 } |
|
310 else { |
|
311 break; |
|
312 } |
|
313 } |
|
314 |
|
315 // If the list is empty, disable the timer. |
|
316 if (sDiscardableImages.isEmpty()) |
|
317 DisableTimer(); |
|
318 } |
|
319 |
|
320 void |
|
321 DiscardTracker::MaybeDiscardSoon() |
|
322 { |
|
323 // Are we carrying around too much decoded image data? If so, enqueue an |
|
324 // event to try to get us down under our limit. |
|
325 if (sCurrentDecodedImageBytes > sMaxDecodedImageKB * 1024 && |
|
326 !sDiscardableImages.isEmpty()) { |
|
327 // Check if the value of sDiscardRunnablePending used to be false |
|
328 if (!sDiscardRunnablePending.exchange(true)) { |
|
329 nsRefPtr<DiscardRunnable> runnable = new DiscardRunnable(); |
|
330 NS_DispatchToMainThread(runnable); |
|
331 } |
|
332 } |
|
333 } |
|
334 |
|
335 } // namespace image |
|
336 } // namespace mozilla |