michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.favicons.cache; michael@0: michael@0: import android.graphics.Bitmap; michael@0: import android.util.Log; michael@0: import org.mozilla.gecko.favicons.Favicons; michael@0: michael@0: import java.util.Iterator; michael@0: import java.util.LinkedList; michael@0: import java.util.concurrent.ConcurrentHashMap; michael@0: import java.util.concurrent.Semaphore; michael@0: import java.util.concurrent.atomic.AtomicInteger; michael@0: michael@0: /** michael@0: * Implements a Least-Recently-Used cache for Favicons, keyed by Favicon URL. michael@0: * michael@0: * When a favicon at a particular URL is decoded, it will yield one or more bitmaps. michael@0: * While in memory, these bitmaps are stored in a list, sorted in ascending order of size, in a michael@0: * FaviconsForURL object. michael@0: * The collection of FaviconsForURL objects currently in the cache is stored in backingMap, keyed michael@0: * by favicon URL. michael@0: * michael@0: * A second map exists for permanent cache entries -- ones that are never expired. These entries michael@0: * are assumed to be disjoint from those in the normal cache, and this map is checked first. michael@0: * michael@0: * FaviconsForURL provides a method for obtaining the smallest icon larger than a given size - the michael@0: * most appropriate icon for a particular size. michael@0: * It also distinguishes between "primary" favicons (Ones that have merely been extracted from a michael@0: * file downloaded from the website) and "secondary" favicons (Ones that have been computed locally michael@0: * as resized versions of primary favicons.). michael@0: * michael@0: * FaviconsForURL is also responsible for storing URL-specific, as opposed to favicon-specific, michael@0: * information. For the purposes of this cache, the simplifying assumption that the dominant colour michael@0: * for all favicons served from a particular favicon URL shall be the same is made. (To violate this michael@0: * would mandate serving an ICO or similar file with multiple radically different images in it - an michael@0: * ill-advised and extremely uncommon use-case, for sure.) michael@0: * The dominant colour information is updated as the element is being added to the cache - typically michael@0: * on the background thread. michael@0: * Also present here are the download timestamp and isFailed flag. Upon failure, the flag is set. michael@0: * A constant exists in this file to specify the maximum time permitted between failures before michael@0: * a retry is again permitted. michael@0: * michael@0: * TODO: Expiry of Favicons from the favicon database cache is not implemented. (Bug 914296) michael@0: * michael@0: * A typical request to the cache will consist of a Favicon URL and a target size. The FaviconsForURL michael@0: * object for that URL will be obtained, queried for a favicon matching exactly the needed size, and michael@0: * if successful, the result is returned. michael@0: * If unsuccessful, the object is asked to return the smallest available primary favicon larger than michael@0: * the target size. If this step works, the result is downscaled to create a new secondary favicon, michael@0: * which is then stored (So subsequent requests will succeed at the first step) and returned. michael@0: * If that step fails, the object finally walks backwards through its sequence of favicons until it michael@0: * finds the largest primary favicon smaller than the target. This is then upscaled by a maximum of michael@0: * 2x towards the target size, and the result cached and returned as above. michael@0: * michael@0: * The bitmaps themselves are encapsulated inside FaviconCacheElement objects. These objects contain, michael@0: * as well as the bitmap, a pointer to the encapsulating FaviconsForURL object (Used by the LRU michael@0: * culler), the size of the encapsulated image, a flag indicating if this is a primary favicon, and michael@0: * a flag indicating if the entry is invalid. michael@0: * All FaviconCacheElement objects are tracked in the ordering LinkedList. This is used to record michael@0: * LRU information about FaviconCacheElements. In particular, the most recently used FaviconCacheElement michael@0: * will be at the start of the list, the least recently used at the end of the list. michael@0: * michael@0: * When the cache runs out of space, it removes FaviconCacheElements starting from the end of the list michael@0: * until a sufficient amount of space has been freed. michael@0: * When a secondary favicon is removed in this way, it is simply deleted from its parent FaviconsForURLs michael@0: * object's list of available favicons. michael@0: * The backpointer field on the FaviconCacheElement is used to remove the element from the encapsulating michael@0: * FaviconsForURL object, when this is required. michael@0: * When a primary favicon is removed, its invalid flag is set to true and its bitmap payload is set michael@0: * to null (So it is available for freeing by the garbage collector). This reduces the memory footprint michael@0: * of the icon to essentially zero, but keeps track of which primary favicons exist for this favicon michael@0: * URL. michael@0: * If a subsequent request comes in for that favicon URL, it is then known that a primary of those michael@0: * dimensions is available, just that it is not in the cache. The system is then able to load the michael@0: * primary back into the cache from the database (Where the entirety of the initially encapsulating michael@0: * container-formatted image file is stored). michael@0: * If this were not done, then when processing requests after the culling of primary favicons it would michael@0: * be impossible to distinguish between the nonexistence of a primary and the nonexistence of a primary michael@0: * in the cache without querying the database. michael@0: * michael@0: * The implementation is safe to use from multiple threads and, while is it not entirely strongly michael@0: * consistent all of the time, you almost certainly don't care. michael@0: * The thread-safety implementation used is approximately MRSW with semaphores. An extra semaphore michael@0: * is used to grant mutual exclusion over reordering operations from reader threads (Who thus gain michael@0: * a quasi-writer status to do such limited mutation as is necessary). michael@0: * michael@0: * Reads which race with writes are liable to not see the ongoing write. The cache may return a michael@0: * stale or now-removed value to the caller. Returned values are never invalid, even in the face michael@0: * of concurrent reading and culling. michael@0: */ michael@0: public class FaviconCache { michael@0: private static final String LOGTAG = "FaviconCache"; michael@0: michael@0: // The number of spaces to allocate for favicons in each node. michael@0: private static final int NUM_FAVICON_SIZES = 4; michael@0: michael@0: // Dimensions of the largest favicon to store in the cache. Everything is downscaled to this. michael@0: public final int maxCachedWidth; michael@0: michael@0: // Retry failed favicons after 20 minutes. michael@0: public static final long FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 20; michael@0: michael@0: // Map relating Favicon URLs with objects representing decoded favicons. michael@0: // Since favicons may be container formats holding multiple icons, the underlying type holds a michael@0: // sorted list of bitmap payloads in ascending order of size. The underlying type may be queried michael@0: // for the least larger payload currently present. michael@0: private final ConcurrentHashMap backingMap = new ConcurrentHashMap(); michael@0: michael@0: // And the same, but never evicted. michael@0: private final ConcurrentHashMap permanentBackingMap = new ConcurrentHashMap(); michael@0: michael@0: // A linked list used to implement a queue, defining the LRU properties of the cache. Elements michael@0: // contained within the various FaviconsForURL objects are held here, the least recently used michael@0: // of which at the end of the list. When space needs to be reclaimed, the appropriate bitmap is michael@0: // culled. michael@0: private final LinkedList ordering = new LinkedList(); michael@0: michael@0: // The above structures, if used correctly, enable this cache to exhibit LRU semantics across all michael@0: // favicon payloads in the system, as well as enabling the dynamic selection from the cache of michael@0: // the primary bitmap most suited to the requested size (in cases where multiple primary bitmaps michael@0: // are provided by the underlying file format). michael@0: michael@0: // Current size, in bytes, of the bitmap data present in the LRU cache. michael@0: private final AtomicInteger currentSize = new AtomicInteger(0); michael@0: michael@0: // The maximum quantity, in bytes, of bitmap data which may be stored in the cache. michael@0: private final int maxSizeBytes; michael@0: michael@0: // Tracks the number of ongoing read operations. Enables the first one in to lock writers out and michael@0: // the last one out to let them in. michael@0: private final AtomicInteger ongoingReads = new AtomicInteger(0); michael@0: michael@0: // Used to ensure transaction fairness - each txn acquires and releases this as the first operation. michael@0: // The effect is an orderly, inexpensive ordering enforced on txns to prevent writer starvation. michael@0: private final Semaphore turnSemaphore = new Semaphore(1); michael@0: michael@0: // A deviation from the usual MRSW solution - this semaphore is used to guard modification to the michael@0: // ordering map. This allows for read transactions to update the most-recently-used value without michael@0: // needing to take out the write lock. michael@0: private final Semaphore reorderingSemaphore = new Semaphore(1); michael@0: michael@0: // The semaphore one must acquire in order to perform a write. michael@0: private final Semaphore writeLock = new Semaphore(1); michael@0: michael@0: /** michael@0: * Called by txns performing only reads as they start. Prevents writer starvation with a turn michael@0: * semaphore and locks writers out if this is the first concurrent reader txn starting up. michael@0: */ michael@0: private void startRead() { michael@0: turnSemaphore.acquireUninterruptibly(); michael@0: turnSemaphore.release(); michael@0: michael@0: if (ongoingReads.incrementAndGet() == 1) { michael@0: // First one in. Wait for writers to finish and lock them out. michael@0: writeLock.acquireUninterruptibly(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Called by transactions performing only reads as they finish. Ensures that if this is the last michael@0: * concluding read transaction then then writers are subsequently allowed in. michael@0: */ michael@0: private void finishRead() { michael@0: if (ongoingReads.decrementAndGet() == 0) { michael@0: writeLock.release(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Called by writer transactions upon start. Ensures fairness and then obtains the write lock. michael@0: * Upon return, no other txns will be executing concurrently. michael@0: */ michael@0: private void startWrite() { michael@0: turnSemaphore.acquireUninterruptibly(); michael@0: writeLock.acquireUninterruptibly(); michael@0: } michael@0: michael@0: /** michael@0: * Called by a concluding write transaction - unlocks the structure. michael@0: */ michael@0: private void finishWrite() { michael@0: turnSemaphore.release(); michael@0: writeLock.release(); michael@0: } michael@0: michael@0: public FaviconCache(int maxSize, int maxWidthToCache) { michael@0: maxSizeBytes = maxSize; michael@0: maxCachedWidth = maxWidthToCache; michael@0: } michael@0: michael@0: /** michael@0: * Determine if the provided favicon URL is marked as a failure (Has failed to load before - michael@0: * such icons get blacklisted for a time to prevent us endlessly retrying.) michael@0: * michael@0: * @param faviconURL Favicon URL to check if failed in memcache. michael@0: * @return true if this favicon is blacklisted, false otherwise. michael@0: */ michael@0: public boolean isFailedFavicon(String faviconURL) { michael@0: if (faviconURL == null) { michael@0: return true; michael@0: } michael@0: michael@0: startRead(); michael@0: michael@0: try { michael@0: // If we don't have it in the cache, it certainly isn't a known failure. michael@0: // Non-evictable favicons are never failed, so we don't need to michael@0: // check permanentBackingMap. michael@0: if (!backingMap.containsKey(faviconURL)) { michael@0: return false; michael@0: } michael@0: michael@0: FaviconsForURL container = backingMap.get(faviconURL); michael@0: michael@0: // If the has failed flag is not set, it's certainly not a known failure. michael@0: if (!container.hasFailed) { michael@0: return false; michael@0: } michael@0: michael@0: final long failureTimestamp = container.downloadTimestamp; michael@0: michael@0: // Calculate elapsed time since the failing download. michael@0: final long failureDiff = System.currentTimeMillis() - failureTimestamp; michael@0: michael@0: // If the expiry is still in effect, return. Otherwise, continue and unmark the failure. michael@0: if (failureDiff < FAILURE_RETRY_MILLISECONDS) { michael@0: return true; michael@0: } michael@0: } catch (Exception unhandled) { michael@0: Log.e(LOGTAG, "FaviconCache exception!", unhandled); michael@0: return true; michael@0: } finally { michael@0: finishRead(); michael@0: } michael@0: michael@0: startWrite(); michael@0: michael@0: // If the entry is no longer failed, remove the record of it from the cache. michael@0: try { michael@0: recordRemoved(backingMap.remove(faviconURL)); michael@0: return false; michael@0: } finally { michael@0: finishWrite(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Mark the indicated page URL as a failed Favicon until the provided time. michael@0: * michael@0: * @param faviconURL Page URL for which a Favicon load has failed. michael@0: */ michael@0: public void putFailed(String faviconURL) { michael@0: startWrite(); michael@0: michael@0: try { michael@0: FaviconsForURL container = new FaviconsForURL(0, true); michael@0: recordRemoved(backingMap.put(faviconURL, container)); michael@0: } finally { michael@0: finishWrite(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Fetch a Favicon for the given URL as close as possible to the size provided. michael@0: * If an icon of the given size is already in the cache, it is returned. michael@0: * If an icon of the given size is not in the cache but a larger unscaled image does exist in michael@0: * the cache, we downscale the larger image to the target size and cache the result. michael@0: * If there is no image of the required size, null is returned. michael@0: * michael@0: * @param faviconURL The URL for which a Favicon is desired. Must not be null. michael@0: * @param targetSize The size of the desired favicon. michael@0: * @return A favicon of the requested size for the requested URL, or null if none cached. michael@0: */ michael@0: public Bitmap getFaviconForDimensions(String faviconURL, int targetSize) { michael@0: if (faviconURL == null) { michael@0: Log.e(LOGTAG, "You passed a null faviconURL to getFaviconForDimensions. Don't."); michael@0: return null; michael@0: } michael@0: michael@0: boolean shouldComputeColour = false; michael@0: boolean wasPermanent = false; michael@0: FaviconsForURL container; michael@0: final Bitmap newBitmap; michael@0: michael@0: startRead(); michael@0: michael@0: try { michael@0: container = permanentBackingMap.get(faviconURL); michael@0: if (container == null) { michael@0: container = backingMap.get(faviconURL); michael@0: if (container == null) { michael@0: // We don't have it! michael@0: return null; michael@0: } michael@0: } else { michael@0: wasPermanent = true; michael@0: } michael@0: michael@0: FaviconCacheElement cacheElement; michael@0: michael@0: // If targetSize is -1, it means we want the largest possible icon. michael@0: int cacheElementIndex = (targetSize == -1) ? -1 : container.getNextHighestIndex(targetSize); michael@0: michael@0: // cacheElementIndex now holds either the index of the next least largest bitmap from michael@0: // targetSize, or -1 if targetSize > all bitmaps. michael@0: if (cacheElementIndex != -1) { michael@0: // If cacheElementIndex is not the sentinel value, then it is a valid index into favicons. michael@0: cacheElement = container.favicons.get(cacheElementIndex); michael@0: michael@0: if (cacheElement.invalidated) { michael@0: return null; michael@0: } michael@0: michael@0: // If we found exactly what we wanted - we're done. michael@0: if (cacheElement.imageSize == targetSize) { michael@0: setMostRecentlyUsedWithinRead(cacheElement); michael@0: return cacheElement.faviconPayload; michael@0: } michael@0: } else { michael@0: // We requested an image larger than all primaries. Set the element to start the search michael@0: // from to the element beyond the end of the array, so the search runs backwards. michael@0: cacheElementIndex = container.favicons.size(); michael@0: } michael@0: michael@0: // We did not find exactly what we wanted, but now have set cacheElementIndex to the index michael@0: // where what we want should live in the list. We now request the next least larger primary michael@0: // from the cache. We will downscale this to our target size. michael@0: michael@0: // If there is no such primary, we'll upscale the next least smaller one instead. michael@0: cacheElement = container.getNextPrimary(cacheElementIndex); michael@0: michael@0: if (cacheElement == null) { michael@0: // The primary has been invalidated! Fail! Need to get it back from the database. michael@0: return null; michael@0: } michael@0: michael@0: if (targetSize == -1) { michael@0: // We got the biggest primary, so that's what we'll return. michael@0: return cacheElement.faviconPayload; michael@0: } michael@0: michael@0: // Scaling logic... michael@0: Bitmap largestElementBitmap = cacheElement.faviconPayload; michael@0: int largestSize = cacheElement.imageSize; michael@0: michael@0: if (largestSize >= targetSize) { michael@0: // The largest we have is larger than the target - downsize to target. michael@0: newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, targetSize, targetSize, true); michael@0: } else { michael@0: // Our largest primary is smaller than the desired size. Upscale by a maximum of 2x. michael@0: // largestSize now reflects the maximum size we can upscale to. michael@0: largestSize *= 2; michael@0: michael@0: if (largestSize >= targetSize) { michael@0: // Perfect! We can upscale by less than 2x and reach the needed size. Do it. michael@0: newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, targetSize, targetSize, true); michael@0: } else { michael@0: // We don't have enough information to make the target size look nonterrible. Best effort: michael@0: newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, largestSize, largestSize, true); michael@0: michael@0: shouldComputeColour = true; michael@0: } michael@0: } michael@0: } catch (Exception unhandled) { michael@0: // Handle any exception thrown and return the locks to a sensible state. michael@0: michael@0: // Flag to prevent finally from doubly-unlocking. michael@0: Log.e(LOGTAG, "FaviconCache exception!", unhandled); michael@0: return null; michael@0: } finally { michael@0: finishRead(); michael@0: } michael@0: michael@0: startWrite(); michael@0: try { michael@0: if (shouldComputeColour) { michael@0: // And since we failed, we'll need the dominant colour. michael@0: container.ensureDominantColor(); michael@0: } michael@0: michael@0: // While the image might not actually BE that size, we set the size field to the target michael@0: // because this is the best image you can get for a request of that size using the Favicon michael@0: // information provided by this website. michael@0: // This way, subsequent requests hit straight away. michael@0: FaviconCacheElement newElement = container.addSecondary(newBitmap, targetSize); michael@0: michael@0: if (!wasPermanent) { michael@0: if (setMostRecentlyUsedWithinWrite(newElement)) { michael@0: currentSize.addAndGet(newElement.sizeOf()); michael@0: } michael@0: } michael@0: } finally { michael@0: finishWrite(); michael@0: } michael@0: michael@0: return newBitmap; michael@0: } michael@0: michael@0: /** michael@0: * Query the cache for the dominant colour stored for the Favicon URL provided, if any. michael@0: * michael@0: * @param key The URL of the Favicon for which a dominant colour is desired. michael@0: * @return The cached dominant colour, or null if none is cached. michael@0: */ michael@0: public int getDominantColor(String key) { michael@0: startRead(); michael@0: michael@0: try { michael@0: FaviconsForURL element = permanentBackingMap.get(key); michael@0: if (element == null) { michael@0: element = backingMap.get(key); michael@0: } michael@0: michael@0: if (element == null) { michael@0: Log.w(LOGTAG, "Cannot compute dominant color of non-cached favicon. Cache fullness " + michael@0: currentSize.get() + '/' + maxSizeBytes); michael@0: return 0xFFFFFF; michael@0: } michael@0: michael@0: return element.ensureDominantColor(); michael@0: } finally { michael@0: finishRead(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Remove all payloads stored in the given container from the LRU cache. michael@0: * Must be called while holding the write lock. michael@0: * michael@0: * @param wasRemoved The container to purge from the cache. michael@0: */ michael@0: private void recordRemoved(FaviconsForURL wasRemoved) { michael@0: // If there was an existing value, strip it from the insertion-order cache. michael@0: if (wasRemoved == null) { michael@0: return; michael@0: } michael@0: michael@0: int sizeRemoved = 0; michael@0: michael@0: for (FaviconCacheElement e : wasRemoved.favicons) { michael@0: sizeRemoved += e.sizeOf(); michael@0: ordering.remove(e); michael@0: } michael@0: michael@0: currentSize.addAndGet(-sizeRemoved); michael@0: } michael@0: michael@0: private Bitmap produceCacheableBitmap(Bitmap favicon) { michael@0: // Never cache the default Favicon, or the null Favicon. michael@0: if (favicon == Favicons.defaultFavicon || favicon == null) { michael@0: return null; michael@0: } michael@0: michael@0: // Some sites serve up insanely huge Favicons (Seen 512x512 ones...) michael@0: // While we want to cache nice big icons, we apply a limit based on screen density for the michael@0: // sake of space. michael@0: if (favicon.getWidth() > maxCachedWidth) { michael@0: return Bitmap.createScaledBitmap(favicon, maxCachedWidth, maxCachedWidth, true); michael@0: } michael@0: michael@0: return favicon; michael@0: } michael@0: michael@0: /** michael@0: * Set an existing element as the most recently used element. Intended for use from read transactions. While michael@0: * write transactions may safely use this method, it will perform slightly worse than its unsafe counterpart below. michael@0: * michael@0: * @param element The element that is to become the most recently used one. michael@0: * @return true if this element already existed in the list, false otherwise. (Useful for preventing multiple-insertion.) michael@0: */ michael@0: private boolean setMostRecentlyUsedWithinRead(FaviconCacheElement element) { michael@0: reorderingSemaphore.acquireUninterruptibly(); michael@0: try { michael@0: boolean contained = ordering.remove(element); michael@0: ordering.offer(element); michael@0: return contained; michael@0: } finally { michael@0: reorderingSemaphore.release(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Functionally equivalent to setMostRecentlyUsedWithinRead, but operates without taking the reordering semaphore. michael@0: * Only safe for use when called from a write transaction, or there is a risk of concurrent modification. michael@0: * michael@0: * @param element The element that is to become the most recently used one. michael@0: * @return true if this element already existed in the list, false otherwise. (Useful for preventing multiple-insertion.) michael@0: */ michael@0: private boolean setMostRecentlyUsedWithinWrite(FaviconCacheElement element) { michael@0: boolean contained = ordering.remove(element); michael@0: ordering.offer(element); michael@0: return contained; michael@0: } michael@0: michael@0: /** michael@0: * Add the provided bitmap to the cache as the only available primary for this URL. michael@0: * Should never be called with scaled Favicons. The input is assumed to be an unscaled Favicon. michael@0: * michael@0: * @param faviconURL The URL of the Favicon being stored. michael@0: * @param aFavicon The Favicon to store. michael@0: */ michael@0: public void putSingleFavicon(String faviconURL, Bitmap aFavicon) { michael@0: Bitmap favicon = produceCacheableBitmap(aFavicon); michael@0: if (favicon == null) { michael@0: return; michael@0: } michael@0: michael@0: // Create a fresh container for the favicons associated with this URL. Allocate extra slots michael@0: // in the underlying ArrayList in case multiple secondary favicons are later created. michael@0: // Currently set to the number of favicon sizes used in the UI, plus 1, at time of writing. michael@0: // Ought to be tuned as things change for maximal performance. michael@0: FaviconsForURL toInsert = new FaviconsForURL(NUM_FAVICON_SIZES); michael@0: michael@0: // Create the cache element for the single element we are inserting, and configure it. michael@0: FaviconCacheElement newElement = toInsert.addPrimary(favicon); michael@0: michael@0: startWrite(); michael@0: try { michael@0: // Set the new element as the most recently used one. michael@0: setMostRecentlyUsedWithinWrite(newElement); michael@0: michael@0: currentSize.addAndGet(newElement.sizeOf()); michael@0: michael@0: // Update the value in the LruCache... michael@0: FaviconsForURL wasRemoved; michael@0: wasRemoved = backingMap.put(faviconURL, toInsert); michael@0: michael@0: recordRemoved(wasRemoved); michael@0: } finally { michael@0: finishWrite(); michael@0: } michael@0: michael@0: cullIfRequired(); michael@0: } michael@0: michael@0: /** michael@0: * Set the collection of primary favicons for the given URL to the provided collection of bitmaps. michael@0: * michael@0: * @param faviconURL The URL from which the favicons originate. michael@0: * @param favicons A List of favicons decoded from this URL. michael@0: * @param permanently If true, the added favicons are never subject to eviction. michael@0: */ michael@0: public void putFavicons(String faviconURL, Iterator favicons, boolean permanently) { michael@0: // We don't know how many icons we'll have - let's just take a guess. michael@0: FaviconsForURL toInsert = new FaviconsForURL(5 * NUM_FAVICON_SIZES); michael@0: int sizeGained = 0; michael@0: michael@0: while (favicons.hasNext()) { michael@0: Bitmap favicon = produceCacheableBitmap(favicons.next()); michael@0: if (favicon == null) { michael@0: continue; michael@0: } michael@0: michael@0: FaviconCacheElement newElement = toInsert.addPrimary(favicon); michael@0: sizeGained += newElement.sizeOf(); michael@0: } michael@0: michael@0: startWrite(); michael@0: try { michael@0: if (permanently) { michael@0: permanentBackingMap.put(faviconURL, toInsert); michael@0: return; michael@0: } michael@0: michael@0: for (FaviconCacheElement newElement : toInsert.favicons) { michael@0: setMostRecentlyUsedWithinWrite(newElement); michael@0: } michael@0: michael@0: // In the event this insertion is being made to a key that already held a value, the subsequent recordRemoved michael@0: // call will subtract the size of the old value, preventing double-counting. michael@0: currentSize.addAndGet(sizeGained); michael@0: michael@0: // Update the value in the LruCache... michael@0: recordRemoved(backingMap.put(faviconURL, toInsert)); michael@0: } finally { michael@0: finishWrite(); michael@0: } michael@0: michael@0: cullIfRequired(); michael@0: } michael@0: michael@0: /** michael@0: * If cache too large, drop stuff from the cache to get the size back into the acceptable range. michael@0: * Otherwise, do nothing. michael@0: */ michael@0: private void cullIfRequired() { michael@0: Log.d(LOGTAG, "Favicon cache fullness: " + currentSize.get() + '/' + maxSizeBytes); michael@0: michael@0: if (currentSize.get() <= maxSizeBytes) { michael@0: return; michael@0: } michael@0: michael@0: startWrite(); michael@0: try { michael@0: while (currentSize.get() > maxSizeBytes) { michael@0: // Cull the least recently used element. michael@0: michael@0: FaviconCacheElement victim; michael@0: victim = ordering.poll(); michael@0: michael@0: currentSize.addAndGet(-victim.sizeOf()); michael@0: victim.onEvictedFromCache(); michael@0: michael@0: Log.d(LOGTAG, "After cull: " + currentSize.get() + '/' + maxSizeBytes); michael@0: } michael@0: } finally { michael@0: finishWrite(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Purge all elements from the FaviconCache. Handy if you want to reclaim some memory. michael@0: */ michael@0: public void evictAll() { michael@0: startWrite(); michael@0: michael@0: // Note that we neither clear, nor track the size of, the permanent map. michael@0: try { michael@0: currentSize.set(0); michael@0: backingMap.clear(); michael@0: ordering.clear(); michael@0: michael@0: } finally { michael@0: finishWrite(); michael@0: } michael@0: } michael@0: }