|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.favicons.cache; |
|
6 |
|
7 import android.graphics.Bitmap; |
|
8 import android.util.Log; |
|
9 import org.mozilla.gecko.favicons.Favicons; |
|
10 |
|
11 import java.util.Iterator; |
|
12 import java.util.LinkedList; |
|
13 import java.util.concurrent.ConcurrentHashMap; |
|
14 import java.util.concurrent.Semaphore; |
|
15 import java.util.concurrent.atomic.AtomicInteger; |
|
16 |
|
17 /** |
|
18 * Implements a Least-Recently-Used cache for Favicons, keyed by Favicon URL. |
|
19 * |
|
20 * When a favicon at a particular URL is decoded, it will yield one or more bitmaps. |
|
21 * While in memory, these bitmaps are stored in a list, sorted in ascending order of size, in a |
|
22 * FaviconsForURL object. |
|
23 * The collection of FaviconsForURL objects currently in the cache is stored in backingMap, keyed |
|
24 * by favicon URL. |
|
25 * |
|
26 * A second map exists for permanent cache entries -- ones that are never expired. These entries |
|
27 * are assumed to be disjoint from those in the normal cache, and this map is checked first. |
|
28 * |
|
29 * FaviconsForURL provides a method for obtaining the smallest icon larger than a given size - the |
|
30 * most appropriate icon for a particular size. |
|
31 * It also distinguishes between "primary" favicons (Ones that have merely been extracted from a |
|
32 * file downloaded from the website) and "secondary" favicons (Ones that have been computed locally |
|
33 * as resized versions of primary favicons.). |
|
34 * |
|
35 * FaviconsForURL is also responsible for storing URL-specific, as opposed to favicon-specific, |
|
36 * information. For the purposes of this cache, the simplifying assumption that the dominant colour |
|
37 * for all favicons served from a particular favicon URL shall be the same is made. (To violate this |
|
38 * would mandate serving an ICO or similar file with multiple radically different images in it - an |
|
39 * ill-advised and extremely uncommon use-case, for sure.) |
|
40 * The dominant colour information is updated as the element is being added to the cache - typically |
|
41 * on the background thread. |
|
42 * Also present here are the download timestamp and isFailed flag. Upon failure, the flag is set. |
|
43 * A constant exists in this file to specify the maximum time permitted between failures before |
|
44 * a retry is again permitted. |
|
45 * |
|
46 * TODO: Expiry of Favicons from the favicon database cache is not implemented. (Bug 914296) |
|
47 * |
|
48 * A typical request to the cache will consist of a Favicon URL and a target size. The FaviconsForURL |
|
49 * object for that URL will be obtained, queried for a favicon matching exactly the needed size, and |
|
50 * if successful, the result is returned. |
|
51 * If unsuccessful, the object is asked to return the smallest available primary favicon larger than |
|
52 * the target size. If this step works, the result is downscaled to create a new secondary favicon, |
|
53 * which is then stored (So subsequent requests will succeed at the first step) and returned. |
|
54 * If that step fails, the object finally walks backwards through its sequence of favicons until it |
|
55 * finds the largest primary favicon smaller than the target. This is then upscaled by a maximum of |
|
56 * 2x towards the target size, and the result cached and returned as above. |
|
57 * |
|
58 * The bitmaps themselves are encapsulated inside FaviconCacheElement objects. These objects contain, |
|
59 * as well as the bitmap, a pointer to the encapsulating FaviconsForURL object (Used by the LRU |
|
60 * culler), the size of the encapsulated image, a flag indicating if this is a primary favicon, and |
|
61 * a flag indicating if the entry is invalid. |
|
62 * All FaviconCacheElement objects are tracked in the ordering LinkedList. This is used to record |
|
63 * LRU information about FaviconCacheElements. In particular, the most recently used FaviconCacheElement |
|
64 * will be at the start of the list, the least recently used at the end of the list. |
|
65 * |
|
66 * When the cache runs out of space, it removes FaviconCacheElements starting from the end of the list |
|
67 * until a sufficient amount of space has been freed. |
|
68 * When a secondary favicon is removed in this way, it is simply deleted from its parent FaviconsForURLs |
|
69 * object's list of available favicons. |
|
70 * The backpointer field on the FaviconCacheElement is used to remove the element from the encapsulating |
|
71 * FaviconsForURL object, when this is required. |
|
72 * When a primary favicon is removed, its invalid flag is set to true and its bitmap payload is set |
|
73 * to null (So it is available for freeing by the garbage collector). This reduces the memory footprint |
|
74 * of the icon to essentially zero, but keeps track of which primary favicons exist for this favicon |
|
75 * URL. |
|
76 * If a subsequent request comes in for that favicon URL, it is then known that a primary of those |
|
77 * dimensions is available, just that it is not in the cache. The system is then able to load the |
|
78 * primary back into the cache from the database (Where the entirety of the initially encapsulating |
|
79 * container-formatted image file is stored). |
|
80 * If this were not done, then when processing requests after the culling of primary favicons it would |
|
81 * be impossible to distinguish between the nonexistence of a primary and the nonexistence of a primary |
|
82 * in the cache without querying the database. |
|
83 * |
|
84 * The implementation is safe to use from multiple threads and, while is it not entirely strongly |
|
85 * consistent all of the time, you almost certainly don't care. |
|
86 * The thread-safety implementation used is approximately MRSW with semaphores. An extra semaphore |
|
87 * is used to grant mutual exclusion over reordering operations from reader threads (Who thus gain |
|
88 * a quasi-writer status to do such limited mutation as is necessary). |
|
89 * |
|
90 * Reads which race with writes are liable to not see the ongoing write. The cache may return a |
|
91 * stale or now-removed value to the caller. Returned values are never invalid, even in the face |
|
92 * of concurrent reading and culling. |
|
93 */ |
|
94 public class FaviconCache { |
|
95 private static final String LOGTAG = "FaviconCache"; |
|
96 |
|
97 // The number of spaces to allocate for favicons in each node. |
|
98 private static final int NUM_FAVICON_SIZES = 4; |
|
99 |
|
100 // Dimensions of the largest favicon to store in the cache. Everything is downscaled to this. |
|
101 public final int maxCachedWidth; |
|
102 |
|
103 // Retry failed favicons after 20 minutes. |
|
104 public static final long FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 20; |
|
105 |
|
106 // Map relating Favicon URLs with objects representing decoded favicons. |
|
107 // Since favicons may be container formats holding multiple icons, the underlying type holds a |
|
108 // sorted list of bitmap payloads in ascending order of size. The underlying type may be queried |
|
109 // for the least larger payload currently present. |
|
110 private final ConcurrentHashMap<String, FaviconsForURL> backingMap = new ConcurrentHashMap<String, FaviconsForURL>(); |
|
111 |
|
112 // And the same, but never evicted. |
|
113 private final ConcurrentHashMap<String, FaviconsForURL> permanentBackingMap = new ConcurrentHashMap<String, FaviconsForURL>(); |
|
114 |
|
115 // A linked list used to implement a queue, defining the LRU properties of the cache. Elements |
|
116 // contained within the various FaviconsForURL objects are held here, the least recently used |
|
117 // of which at the end of the list. When space needs to be reclaimed, the appropriate bitmap is |
|
118 // culled. |
|
119 private final LinkedList<FaviconCacheElement> ordering = new LinkedList<FaviconCacheElement>(); |
|
120 |
|
121 // The above structures, if used correctly, enable this cache to exhibit LRU semantics across all |
|
122 // favicon payloads in the system, as well as enabling the dynamic selection from the cache of |
|
123 // the primary bitmap most suited to the requested size (in cases where multiple primary bitmaps |
|
124 // are provided by the underlying file format). |
|
125 |
|
126 // Current size, in bytes, of the bitmap data present in the LRU cache. |
|
127 private final AtomicInteger currentSize = new AtomicInteger(0); |
|
128 |
|
129 // The maximum quantity, in bytes, of bitmap data which may be stored in the cache. |
|
130 private final int maxSizeBytes; |
|
131 |
|
132 // Tracks the number of ongoing read operations. Enables the first one in to lock writers out and |
|
133 // the last one out to let them in. |
|
134 private final AtomicInteger ongoingReads = new AtomicInteger(0); |
|
135 |
|
136 // Used to ensure transaction fairness - each txn acquires and releases this as the first operation. |
|
137 // The effect is an orderly, inexpensive ordering enforced on txns to prevent writer starvation. |
|
138 private final Semaphore turnSemaphore = new Semaphore(1); |
|
139 |
|
140 // A deviation from the usual MRSW solution - this semaphore is used to guard modification to the |
|
141 // ordering map. This allows for read transactions to update the most-recently-used value without |
|
142 // needing to take out the write lock. |
|
143 private final Semaphore reorderingSemaphore = new Semaphore(1); |
|
144 |
|
145 // The semaphore one must acquire in order to perform a write. |
|
146 private final Semaphore writeLock = new Semaphore(1); |
|
147 |
|
148 /** |
|
149 * Called by txns performing only reads as they start. Prevents writer starvation with a turn |
|
150 * semaphore and locks writers out if this is the first concurrent reader txn starting up. |
|
151 */ |
|
152 private void startRead() { |
|
153 turnSemaphore.acquireUninterruptibly(); |
|
154 turnSemaphore.release(); |
|
155 |
|
156 if (ongoingReads.incrementAndGet() == 1) { |
|
157 // First one in. Wait for writers to finish and lock them out. |
|
158 writeLock.acquireUninterruptibly(); |
|
159 } |
|
160 } |
|
161 |
|
162 /** |
|
163 * Called by transactions performing only reads as they finish. Ensures that if this is the last |
|
164 * concluding read transaction then then writers are subsequently allowed in. |
|
165 */ |
|
166 private void finishRead() { |
|
167 if (ongoingReads.decrementAndGet() == 0) { |
|
168 writeLock.release(); |
|
169 } |
|
170 } |
|
171 |
|
172 /** |
|
173 * Called by writer transactions upon start. Ensures fairness and then obtains the write lock. |
|
174 * Upon return, no other txns will be executing concurrently. |
|
175 */ |
|
176 private void startWrite() { |
|
177 turnSemaphore.acquireUninterruptibly(); |
|
178 writeLock.acquireUninterruptibly(); |
|
179 } |
|
180 |
|
181 /** |
|
182 * Called by a concluding write transaction - unlocks the structure. |
|
183 */ |
|
184 private void finishWrite() { |
|
185 turnSemaphore.release(); |
|
186 writeLock.release(); |
|
187 } |
|
188 |
|
189 public FaviconCache(int maxSize, int maxWidthToCache) { |
|
190 maxSizeBytes = maxSize; |
|
191 maxCachedWidth = maxWidthToCache; |
|
192 } |
|
193 |
|
194 /** |
|
195 * Determine if the provided favicon URL is marked as a failure (Has failed to load before - |
|
196 * such icons get blacklisted for a time to prevent us endlessly retrying.) |
|
197 * |
|
198 * @param faviconURL Favicon URL to check if failed in memcache. |
|
199 * @return true if this favicon is blacklisted, false otherwise. |
|
200 */ |
|
201 public boolean isFailedFavicon(String faviconURL) { |
|
202 if (faviconURL == null) { |
|
203 return true; |
|
204 } |
|
205 |
|
206 startRead(); |
|
207 |
|
208 try { |
|
209 // If we don't have it in the cache, it certainly isn't a known failure. |
|
210 // Non-evictable favicons are never failed, so we don't need to |
|
211 // check permanentBackingMap. |
|
212 if (!backingMap.containsKey(faviconURL)) { |
|
213 return false; |
|
214 } |
|
215 |
|
216 FaviconsForURL container = backingMap.get(faviconURL); |
|
217 |
|
218 // If the has failed flag is not set, it's certainly not a known failure. |
|
219 if (!container.hasFailed) { |
|
220 return false; |
|
221 } |
|
222 |
|
223 final long failureTimestamp = container.downloadTimestamp; |
|
224 |
|
225 // Calculate elapsed time since the failing download. |
|
226 final long failureDiff = System.currentTimeMillis() - failureTimestamp; |
|
227 |
|
228 // If the expiry is still in effect, return. Otherwise, continue and unmark the failure. |
|
229 if (failureDiff < FAILURE_RETRY_MILLISECONDS) { |
|
230 return true; |
|
231 } |
|
232 } catch (Exception unhandled) { |
|
233 Log.e(LOGTAG, "FaviconCache exception!", unhandled); |
|
234 return true; |
|
235 } finally { |
|
236 finishRead(); |
|
237 } |
|
238 |
|
239 startWrite(); |
|
240 |
|
241 // If the entry is no longer failed, remove the record of it from the cache. |
|
242 try { |
|
243 recordRemoved(backingMap.remove(faviconURL)); |
|
244 return false; |
|
245 } finally { |
|
246 finishWrite(); |
|
247 } |
|
248 } |
|
249 |
|
250 /** |
|
251 * Mark the indicated page URL as a failed Favicon until the provided time. |
|
252 * |
|
253 * @param faviconURL Page URL for which a Favicon load has failed. |
|
254 */ |
|
255 public void putFailed(String faviconURL) { |
|
256 startWrite(); |
|
257 |
|
258 try { |
|
259 FaviconsForURL container = new FaviconsForURL(0, true); |
|
260 recordRemoved(backingMap.put(faviconURL, container)); |
|
261 } finally { |
|
262 finishWrite(); |
|
263 } |
|
264 } |
|
265 |
|
266 /** |
|
267 * Fetch a Favicon for the given URL as close as possible to the size provided. |
|
268 * If an icon of the given size is already in the cache, it is returned. |
|
269 * If an icon of the given size is not in the cache but a larger unscaled image does exist in |
|
270 * the cache, we downscale the larger image to the target size and cache the result. |
|
271 * If there is no image of the required size, null is returned. |
|
272 * |
|
273 * @param faviconURL The URL for which a Favicon is desired. Must not be null. |
|
274 * @param targetSize The size of the desired favicon. |
|
275 * @return A favicon of the requested size for the requested URL, or null if none cached. |
|
276 */ |
|
277 public Bitmap getFaviconForDimensions(String faviconURL, int targetSize) { |
|
278 if (faviconURL == null) { |
|
279 Log.e(LOGTAG, "You passed a null faviconURL to getFaviconForDimensions. Don't."); |
|
280 return null; |
|
281 } |
|
282 |
|
283 boolean shouldComputeColour = false; |
|
284 boolean wasPermanent = false; |
|
285 FaviconsForURL container; |
|
286 final Bitmap newBitmap; |
|
287 |
|
288 startRead(); |
|
289 |
|
290 try { |
|
291 container = permanentBackingMap.get(faviconURL); |
|
292 if (container == null) { |
|
293 container = backingMap.get(faviconURL); |
|
294 if (container == null) { |
|
295 // We don't have it! |
|
296 return null; |
|
297 } |
|
298 } else { |
|
299 wasPermanent = true; |
|
300 } |
|
301 |
|
302 FaviconCacheElement cacheElement; |
|
303 |
|
304 // If targetSize is -1, it means we want the largest possible icon. |
|
305 int cacheElementIndex = (targetSize == -1) ? -1 : container.getNextHighestIndex(targetSize); |
|
306 |
|
307 // cacheElementIndex now holds either the index of the next least largest bitmap from |
|
308 // targetSize, or -1 if targetSize > all bitmaps. |
|
309 if (cacheElementIndex != -1) { |
|
310 // If cacheElementIndex is not the sentinel value, then it is a valid index into favicons. |
|
311 cacheElement = container.favicons.get(cacheElementIndex); |
|
312 |
|
313 if (cacheElement.invalidated) { |
|
314 return null; |
|
315 } |
|
316 |
|
317 // If we found exactly what we wanted - we're done. |
|
318 if (cacheElement.imageSize == targetSize) { |
|
319 setMostRecentlyUsedWithinRead(cacheElement); |
|
320 return cacheElement.faviconPayload; |
|
321 } |
|
322 } else { |
|
323 // We requested an image larger than all primaries. Set the element to start the search |
|
324 // from to the element beyond the end of the array, so the search runs backwards. |
|
325 cacheElementIndex = container.favicons.size(); |
|
326 } |
|
327 |
|
328 // We did not find exactly what we wanted, but now have set cacheElementIndex to the index |
|
329 // where what we want should live in the list. We now request the next least larger primary |
|
330 // from the cache. We will downscale this to our target size. |
|
331 |
|
332 // If there is no such primary, we'll upscale the next least smaller one instead. |
|
333 cacheElement = container.getNextPrimary(cacheElementIndex); |
|
334 |
|
335 if (cacheElement == null) { |
|
336 // The primary has been invalidated! Fail! Need to get it back from the database. |
|
337 return null; |
|
338 } |
|
339 |
|
340 if (targetSize == -1) { |
|
341 // We got the biggest primary, so that's what we'll return. |
|
342 return cacheElement.faviconPayload; |
|
343 } |
|
344 |
|
345 // Scaling logic... |
|
346 Bitmap largestElementBitmap = cacheElement.faviconPayload; |
|
347 int largestSize = cacheElement.imageSize; |
|
348 |
|
349 if (largestSize >= targetSize) { |
|
350 // The largest we have is larger than the target - downsize to target. |
|
351 newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, targetSize, targetSize, true); |
|
352 } else { |
|
353 // Our largest primary is smaller than the desired size. Upscale by a maximum of 2x. |
|
354 // largestSize now reflects the maximum size we can upscale to. |
|
355 largestSize *= 2; |
|
356 |
|
357 if (largestSize >= targetSize) { |
|
358 // Perfect! We can upscale by less than 2x and reach the needed size. Do it. |
|
359 newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, targetSize, targetSize, true); |
|
360 } else { |
|
361 // We don't have enough information to make the target size look nonterrible. Best effort: |
|
362 newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, largestSize, largestSize, true); |
|
363 |
|
364 shouldComputeColour = true; |
|
365 } |
|
366 } |
|
367 } catch (Exception unhandled) { |
|
368 // Handle any exception thrown and return the locks to a sensible state. |
|
369 |
|
370 // Flag to prevent finally from doubly-unlocking. |
|
371 Log.e(LOGTAG, "FaviconCache exception!", unhandled); |
|
372 return null; |
|
373 } finally { |
|
374 finishRead(); |
|
375 } |
|
376 |
|
377 startWrite(); |
|
378 try { |
|
379 if (shouldComputeColour) { |
|
380 // And since we failed, we'll need the dominant colour. |
|
381 container.ensureDominantColor(); |
|
382 } |
|
383 |
|
384 // While the image might not actually BE that size, we set the size field to the target |
|
385 // because this is the best image you can get for a request of that size using the Favicon |
|
386 // information provided by this website. |
|
387 // This way, subsequent requests hit straight away. |
|
388 FaviconCacheElement newElement = container.addSecondary(newBitmap, targetSize); |
|
389 |
|
390 if (!wasPermanent) { |
|
391 if (setMostRecentlyUsedWithinWrite(newElement)) { |
|
392 currentSize.addAndGet(newElement.sizeOf()); |
|
393 } |
|
394 } |
|
395 } finally { |
|
396 finishWrite(); |
|
397 } |
|
398 |
|
399 return newBitmap; |
|
400 } |
|
401 |
|
402 /** |
|
403 * Query the cache for the dominant colour stored for the Favicon URL provided, if any. |
|
404 * |
|
405 * @param key The URL of the Favicon for which a dominant colour is desired. |
|
406 * @return The cached dominant colour, or null if none is cached. |
|
407 */ |
|
408 public int getDominantColor(String key) { |
|
409 startRead(); |
|
410 |
|
411 try { |
|
412 FaviconsForURL element = permanentBackingMap.get(key); |
|
413 if (element == null) { |
|
414 element = backingMap.get(key); |
|
415 } |
|
416 |
|
417 if (element == null) { |
|
418 Log.w(LOGTAG, "Cannot compute dominant color of non-cached favicon. Cache fullness " + |
|
419 currentSize.get() + '/' + maxSizeBytes); |
|
420 return 0xFFFFFF; |
|
421 } |
|
422 |
|
423 return element.ensureDominantColor(); |
|
424 } finally { |
|
425 finishRead(); |
|
426 } |
|
427 } |
|
428 |
|
429 /** |
|
430 * Remove all payloads stored in the given container from the LRU cache. |
|
431 * Must be called while holding the write lock. |
|
432 * |
|
433 * @param wasRemoved The container to purge from the cache. |
|
434 */ |
|
435 private void recordRemoved(FaviconsForURL wasRemoved) { |
|
436 // If there was an existing value, strip it from the insertion-order cache. |
|
437 if (wasRemoved == null) { |
|
438 return; |
|
439 } |
|
440 |
|
441 int sizeRemoved = 0; |
|
442 |
|
443 for (FaviconCacheElement e : wasRemoved.favicons) { |
|
444 sizeRemoved += e.sizeOf(); |
|
445 ordering.remove(e); |
|
446 } |
|
447 |
|
448 currentSize.addAndGet(-sizeRemoved); |
|
449 } |
|
450 |
|
451 private Bitmap produceCacheableBitmap(Bitmap favicon) { |
|
452 // Never cache the default Favicon, or the null Favicon. |
|
453 if (favicon == Favicons.defaultFavicon || favicon == null) { |
|
454 return null; |
|
455 } |
|
456 |
|
457 // Some sites serve up insanely huge Favicons (Seen 512x512 ones...) |
|
458 // While we want to cache nice big icons, we apply a limit based on screen density for the |
|
459 // sake of space. |
|
460 if (favicon.getWidth() > maxCachedWidth) { |
|
461 return Bitmap.createScaledBitmap(favicon, maxCachedWidth, maxCachedWidth, true); |
|
462 } |
|
463 |
|
464 return favicon; |
|
465 } |
|
466 |
|
467 /** |
|
468 * Set an existing element as the most recently used element. Intended for use from read transactions. While |
|
469 * write transactions may safely use this method, it will perform slightly worse than its unsafe counterpart below. |
|
470 * |
|
471 * @param element The element that is to become the most recently used one. |
|
472 * @return true if this element already existed in the list, false otherwise. (Useful for preventing multiple-insertion.) |
|
473 */ |
|
474 private boolean setMostRecentlyUsedWithinRead(FaviconCacheElement element) { |
|
475 reorderingSemaphore.acquireUninterruptibly(); |
|
476 try { |
|
477 boolean contained = ordering.remove(element); |
|
478 ordering.offer(element); |
|
479 return contained; |
|
480 } finally { |
|
481 reorderingSemaphore.release(); |
|
482 } |
|
483 } |
|
484 |
|
485 /** |
|
486 * Functionally equivalent to setMostRecentlyUsedWithinRead, but operates without taking the reordering semaphore. |
|
487 * Only safe for use when called from a write transaction, or there is a risk of concurrent modification. |
|
488 * |
|
489 * @param element The element that is to become the most recently used one. |
|
490 * @return true if this element already existed in the list, false otherwise. (Useful for preventing multiple-insertion.) |
|
491 */ |
|
492 private boolean setMostRecentlyUsedWithinWrite(FaviconCacheElement element) { |
|
493 boolean contained = ordering.remove(element); |
|
494 ordering.offer(element); |
|
495 return contained; |
|
496 } |
|
497 |
|
498 /** |
|
499 * Add the provided bitmap to the cache as the only available primary for this URL. |
|
500 * Should never be called with scaled Favicons. The input is assumed to be an unscaled Favicon. |
|
501 * |
|
502 * @param faviconURL The URL of the Favicon being stored. |
|
503 * @param aFavicon The Favicon to store. |
|
504 */ |
|
505 public void putSingleFavicon(String faviconURL, Bitmap aFavicon) { |
|
506 Bitmap favicon = produceCacheableBitmap(aFavicon); |
|
507 if (favicon == null) { |
|
508 return; |
|
509 } |
|
510 |
|
511 // Create a fresh container for the favicons associated with this URL. Allocate extra slots |
|
512 // in the underlying ArrayList in case multiple secondary favicons are later created. |
|
513 // Currently set to the number of favicon sizes used in the UI, plus 1, at time of writing. |
|
514 // Ought to be tuned as things change for maximal performance. |
|
515 FaviconsForURL toInsert = new FaviconsForURL(NUM_FAVICON_SIZES); |
|
516 |
|
517 // Create the cache element for the single element we are inserting, and configure it. |
|
518 FaviconCacheElement newElement = toInsert.addPrimary(favicon); |
|
519 |
|
520 startWrite(); |
|
521 try { |
|
522 // Set the new element as the most recently used one. |
|
523 setMostRecentlyUsedWithinWrite(newElement); |
|
524 |
|
525 currentSize.addAndGet(newElement.sizeOf()); |
|
526 |
|
527 // Update the value in the LruCache... |
|
528 FaviconsForURL wasRemoved; |
|
529 wasRemoved = backingMap.put(faviconURL, toInsert); |
|
530 |
|
531 recordRemoved(wasRemoved); |
|
532 } finally { |
|
533 finishWrite(); |
|
534 } |
|
535 |
|
536 cullIfRequired(); |
|
537 } |
|
538 |
|
539 /** |
|
540 * Set the collection of primary favicons for the given URL to the provided collection of bitmaps. |
|
541 * |
|
542 * @param faviconURL The URL from which the favicons originate. |
|
543 * @param favicons A List of favicons decoded from this URL. |
|
544 * @param permanently If true, the added favicons are never subject to eviction. |
|
545 */ |
|
546 public void putFavicons(String faviconURL, Iterator<Bitmap> favicons, boolean permanently) { |
|
547 // We don't know how many icons we'll have - let's just take a guess. |
|
548 FaviconsForURL toInsert = new FaviconsForURL(5 * NUM_FAVICON_SIZES); |
|
549 int sizeGained = 0; |
|
550 |
|
551 while (favicons.hasNext()) { |
|
552 Bitmap favicon = produceCacheableBitmap(favicons.next()); |
|
553 if (favicon == null) { |
|
554 continue; |
|
555 } |
|
556 |
|
557 FaviconCacheElement newElement = toInsert.addPrimary(favicon); |
|
558 sizeGained += newElement.sizeOf(); |
|
559 } |
|
560 |
|
561 startWrite(); |
|
562 try { |
|
563 if (permanently) { |
|
564 permanentBackingMap.put(faviconURL, toInsert); |
|
565 return; |
|
566 } |
|
567 |
|
568 for (FaviconCacheElement newElement : toInsert.favicons) { |
|
569 setMostRecentlyUsedWithinWrite(newElement); |
|
570 } |
|
571 |
|
572 // In the event this insertion is being made to a key that already held a value, the subsequent recordRemoved |
|
573 // call will subtract the size of the old value, preventing double-counting. |
|
574 currentSize.addAndGet(sizeGained); |
|
575 |
|
576 // Update the value in the LruCache... |
|
577 recordRemoved(backingMap.put(faviconURL, toInsert)); |
|
578 } finally { |
|
579 finishWrite(); |
|
580 } |
|
581 |
|
582 cullIfRequired(); |
|
583 } |
|
584 |
|
585 /** |
|
586 * If cache too large, drop stuff from the cache to get the size back into the acceptable range. |
|
587 * Otherwise, do nothing. |
|
588 */ |
|
589 private void cullIfRequired() { |
|
590 Log.d(LOGTAG, "Favicon cache fullness: " + currentSize.get() + '/' + maxSizeBytes); |
|
591 |
|
592 if (currentSize.get() <= maxSizeBytes) { |
|
593 return; |
|
594 } |
|
595 |
|
596 startWrite(); |
|
597 try { |
|
598 while (currentSize.get() > maxSizeBytes) { |
|
599 // Cull the least recently used element. |
|
600 |
|
601 FaviconCacheElement victim; |
|
602 victim = ordering.poll(); |
|
603 |
|
604 currentSize.addAndGet(-victim.sizeOf()); |
|
605 victim.onEvictedFromCache(); |
|
606 |
|
607 Log.d(LOGTAG, "After cull: " + currentSize.get() + '/' + maxSizeBytes); |
|
608 } |
|
609 } finally { |
|
610 finishWrite(); |
|
611 } |
|
612 } |
|
613 |
|
614 /** |
|
615 * Purge all elements from the FaviconCache. Handy if you want to reclaim some memory. |
|
616 */ |
|
617 public void evictAll() { |
|
618 startWrite(); |
|
619 |
|
620 // Note that we neither clear, nor track the size of, the permanent map. |
|
621 try { |
|
622 currentSize.set(0); |
|
623 backingMap.clear(); |
|
624 ordering.clear(); |
|
625 |
|
626 } finally { |
|
627 finishWrite(); |
|
628 } |
|
629 } |
|
630 } |