mobile/android/base/favicons/cache/FaviconCache.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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 }

mercurial