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