Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
5 package org.mozilla.gecko.favicons.cache;
7 import android.graphics.Bitmap;
8 import android.util.Log;
9 import org.mozilla.gecko.favicons.Favicons;
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;
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";
97 // The number of spaces to allocate for favicons in each node.
98 private static final int NUM_FAVICON_SIZES = 4;
100 // Dimensions of the largest favicon to store in the cache. Everything is downscaled to this.
101 public final int maxCachedWidth;
103 // Retry failed favicons after 20 minutes.
104 public static final long FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 20;
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>();
112 // And the same, but never evicted.
113 private final ConcurrentHashMap<String, FaviconsForURL> permanentBackingMap = new ConcurrentHashMap<String, FaviconsForURL>();
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>();
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).
126 // Current size, in bytes, of the bitmap data present in the LRU cache.
127 private final AtomicInteger currentSize = new AtomicInteger(0);
129 // The maximum quantity, in bytes, of bitmap data which may be stored in the cache.
130 private final int maxSizeBytes;
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);
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);
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);
145 // The semaphore one must acquire in order to perform a write.
146 private final Semaphore writeLock = new Semaphore(1);
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();
156 if (ongoingReads.incrementAndGet() == 1) {
157 // First one in. Wait for writers to finish and lock them out.
158 writeLock.acquireUninterruptibly();
159 }
160 }
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 }
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 }
181 /**
182 * Called by a concluding write transaction - unlocks the structure.
183 */
184 private void finishWrite() {
185 turnSemaphore.release();
186 writeLock.release();
187 }
189 public FaviconCache(int maxSize, int maxWidthToCache) {
190 maxSizeBytes = maxSize;
191 maxCachedWidth = maxWidthToCache;
192 }
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 }
206 startRead();
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 }
216 FaviconsForURL container = backingMap.get(faviconURL);
218 // If the has failed flag is not set, it's certainly not a known failure.
219 if (!container.hasFailed) {
220 return false;
221 }
223 final long failureTimestamp = container.downloadTimestamp;
225 // Calculate elapsed time since the failing download.
226 final long failureDiff = System.currentTimeMillis() - failureTimestamp;
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 }
239 startWrite();
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 }
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();
258 try {
259 FaviconsForURL container = new FaviconsForURL(0, true);
260 recordRemoved(backingMap.put(faviconURL, container));
261 } finally {
262 finishWrite();
263 }
264 }
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 }
283 boolean shouldComputeColour = false;
284 boolean wasPermanent = false;
285 FaviconsForURL container;
286 final Bitmap newBitmap;
288 startRead();
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 }
302 FaviconCacheElement cacheElement;
304 // If targetSize is -1, it means we want the largest possible icon.
305 int cacheElementIndex = (targetSize == -1) ? -1 : container.getNextHighestIndex(targetSize);
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);
313 if (cacheElement.invalidated) {
314 return null;
315 }
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 }
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.
332 // If there is no such primary, we'll upscale the next least smaller one instead.
333 cacheElement = container.getNextPrimary(cacheElementIndex);
335 if (cacheElement == null) {
336 // The primary has been invalidated! Fail! Need to get it back from the database.
337 return null;
338 }
340 if (targetSize == -1) {
341 // We got the biggest primary, so that's what we'll return.
342 return cacheElement.faviconPayload;
343 }
345 // Scaling logic...
346 Bitmap largestElementBitmap = cacheElement.faviconPayload;
347 int largestSize = cacheElement.imageSize;
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;
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);
364 shouldComputeColour = true;
365 }
366 }
367 } catch (Exception unhandled) {
368 // Handle any exception thrown and return the locks to a sensible state.
370 // Flag to prevent finally from doubly-unlocking.
371 Log.e(LOGTAG, "FaviconCache exception!", unhandled);
372 return null;
373 } finally {
374 finishRead();
375 }
377 startWrite();
378 try {
379 if (shouldComputeColour) {
380 // And since we failed, we'll need the dominant colour.
381 container.ensureDominantColor();
382 }
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);
390 if (!wasPermanent) {
391 if (setMostRecentlyUsedWithinWrite(newElement)) {
392 currentSize.addAndGet(newElement.sizeOf());
393 }
394 }
395 } finally {
396 finishWrite();
397 }
399 return newBitmap;
400 }
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();
411 try {
412 FaviconsForURL element = permanentBackingMap.get(key);
413 if (element == null) {
414 element = backingMap.get(key);
415 }
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 }
423 return element.ensureDominantColor();
424 } finally {
425 finishRead();
426 }
427 }
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 }
441 int sizeRemoved = 0;
443 for (FaviconCacheElement e : wasRemoved.favicons) {
444 sizeRemoved += e.sizeOf();
445 ordering.remove(e);
446 }
448 currentSize.addAndGet(-sizeRemoved);
449 }
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 }
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 }
464 return favicon;
465 }
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 }
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 }
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 }
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);
517 // Create the cache element for the single element we are inserting, and configure it.
518 FaviconCacheElement newElement = toInsert.addPrimary(favicon);
520 startWrite();
521 try {
522 // Set the new element as the most recently used one.
523 setMostRecentlyUsedWithinWrite(newElement);
525 currentSize.addAndGet(newElement.sizeOf());
527 // Update the value in the LruCache...
528 FaviconsForURL wasRemoved;
529 wasRemoved = backingMap.put(faviconURL, toInsert);
531 recordRemoved(wasRemoved);
532 } finally {
533 finishWrite();
534 }
536 cullIfRequired();
537 }
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;
551 while (favicons.hasNext()) {
552 Bitmap favicon = produceCacheableBitmap(favicons.next());
553 if (favicon == null) {
554 continue;
555 }
557 FaviconCacheElement newElement = toInsert.addPrimary(favicon);
558 sizeGained += newElement.sizeOf();
559 }
561 startWrite();
562 try {
563 if (permanently) {
564 permanentBackingMap.put(faviconURL, toInsert);
565 return;
566 }
568 for (FaviconCacheElement newElement : toInsert.favicons) {
569 setMostRecentlyUsedWithinWrite(newElement);
570 }
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);
576 // Update the value in the LruCache...
577 recordRemoved(backingMap.put(faviconURL, toInsert));
578 } finally {
579 finishWrite();
580 }
582 cullIfRequired();
583 }
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);
592 if (currentSize.get() <= maxSizeBytes) {
593 return;
594 }
596 startWrite();
597 try {
598 while (currentSize.get() > maxSizeBytes) {
599 // Cull the least recently used element.
601 FaviconCacheElement victim;
602 victim = ordering.poll();
604 currentSize.addAndGet(-victim.sizeOf());
605 victim.onEvictedFromCache();
607 Log.d(LOGTAG, "After cull: " + currentSize.get() + '/' + maxSizeBytes);
608 }
609 } finally {
610 finishWrite();
611 }
612 }
614 /**
615 * Purge all elements from the FaviconCache. Handy if you want to reclaim some memory.
616 */
617 public void evictAll() {
618 startWrite();
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();
626 } finally {
627 finishWrite();
628 }
629 }
630 }