Home | History | Annotate | Download | only in prefill
      1 package com.bumptech.glide.load.engine.prefill;
      2 
      3 import android.graphics.Bitmap;
      4 import android.os.Handler;
      5 import android.os.Looper;
      6 import android.os.SystemClock;
      7 import android.util.Log;
      8 
      9 import com.bumptech.glide.load.Key;
     10 import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
     11 import com.bumptech.glide.load.engine.cache.MemoryCache;
     12 import com.bumptech.glide.load.resource.bitmap.BitmapResource;
     13 import com.bumptech.glide.util.Util;
     14 
     15 import java.io.UnsupportedEncodingException;
     16 import java.security.MessageDigest;
     17 import java.util.HashSet;
     18 import java.util.Set;
     19 import java.util.concurrent.TimeUnit;
     20 
     21 /**
     22  * A class that allocates {@link android.graphics.Bitmap Bitmaps} to make sure that the
     23  * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} is pre-populated.
     24  *
     25  * <p>By posting to the main thread with backoffs, we try to avoid ANRs when the garbage collector gets into a state
     26  * where a high percentage of {@link Bitmap} allocations trigger a stop the world GC. We try to detect whether or not a
     27  * GC has occurred by only allowing our allocator to run for a limited number of milliseconds. Since the allocations
     28  * themselves very fast, a GC is the most likely reason for a substantial delay. If we detect our allocator has run for
     29  * more than our limit, we assume a GC has occurred, stop the current allocations, and try again after a delay.
     30  */
     31 final class BitmapPreFillRunner implements Runnable {
     32     private static final String TAG = "PreFillRunner";
     33     private static final Clock DEFAULT_CLOCK = new Clock();
     34 
     35     /**
     36      * The maximum number of millis we can run before posting. Set to match and detect the duration of non concurrent
     37      * GCs.
     38      */
     39     static final long MAX_DURATION_MS = 32;
     40 
     41     /**
     42      * The amount of time in ms we wait before continuing to allocate after the first GC is detected.
     43      */
     44     static final long INITIAL_BACKOFF_MS = 40;
     45 
     46     /**
     47      * The amount by which the current backoff time is multiplied each time we detect a GC.
     48      */
     49     static final int BACKOFF_RATIO = 4;
     50 
     51     /**
     52      * The maximum amount of time in ms we wait before continuing to allocate.
     53      */
     54     static final long MAX_BACKOFF_MS = TimeUnit.SECONDS.toMillis(1);
     55 
     56     private final BitmapPool bitmapPool;
     57     private final MemoryCache memoryCache;
     58     private final PreFillQueue toPrefill;
     59     private final Clock clock;
     60     private final Set<PreFillType> seenTypes = new HashSet<PreFillType>();
     61     private final Handler handler;
     62 
     63     private long currentDelay = INITIAL_BACKOFF_MS;
     64     private boolean isCancelled;
     65 
     66     public BitmapPreFillRunner(BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder) {
     67         this(bitmapPool, memoryCache, allocationOrder, DEFAULT_CLOCK, new Handler(Looper.getMainLooper()));
     68     }
     69 
     70     // Visible for testing.
     71     BitmapPreFillRunner(BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder, Clock clock,
     72             Handler handler) {
     73         this.bitmapPool = bitmapPool;
     74         this.memoryCache = memoryCache;
     75         this.toPrefill = allocationOrder;
     76         this.clock = clock;
     77         this.handler = handler;
     78     }
     79 
     80     public void cancel() {
     81         isCancelled = true;
     82     }
     83 
     84     /**
     85      * Attempts to allocate {@link android.graphics.Bitmap}s and returns {@code true} if there are more
     86      * {@link android.graphics.Bitmap}s to allocate and {@code false} otherwise.
     87      */
     88     private boolean allocate() {
     89         long start = clock.now();
     90         while (!toPrefill.isEmpty() && !isGcDetected(start)) {
     91             PreFillType toAllocate = toPrefill.remove();
     92             Bitmap bitmap = Bitmap.createBitmap(toAllocate.getWidth(), toAllocate.getHeight(),
     93                     toAllocate.getConfig());
     94 
     95             // Don't over fill the memory cache to avoid evicting useful resources, but make sure it's not empty so
     96             // we use all available space.
     97             if (getFreeMemoryCacheBytes() >= Util.getBitmapByteSize(bitmap)) {
     98                 memoryCache.put(new UniqueKey(), BitmapResource.obtain(bitmap, bitmapPool));
     99             } else {
    100                 addToBitmapPool(toAllocate, bitmap);
    101             }
    102 
    103             if (Log.isLoggable(TAG, Log.DEBUG)) {
    104                 Log.d(TAG, "allocated [" + toAllocate.getWidth() + "x" + toAllocate.getHeight() + "] "
    105                         + toAllocate.getConfig() + " size: " + Util.getBitmapByteSize(bitmap));
    106             }
    107         }
    108 
    109         return !isCancelled && !toPrefill.isEmpty();
    110     }
    111 
    112     private boolean isGcDetected(long startTimeMs) {
    113         return clock.now() - startTimeMs >= MAX_DURATION_MS;
    114     }
    115 
    116     private int getFreeMemoryCacheBytes() {
    117         return memoryCache.getMaxSize() - memoryCache.getCurrentSize();
    118     }
    119 
    120     private void addToBitmapPool(PreFillType toAllocate, Bitmap bitmap) {
    121         // The pool may not move sizes to the front of the LRU on put. Do a get here to make sure the size we're adding
    122         // is at the front of the queue so that the Bitmap we're adding won't be evicted immediately.
    123         if (seenTypes.add(toAllocate)) {
    124           Bitmap fromPool = bitmapPool.get(toAllocate.getWidth(), toAllocate.getHeight(),
    125               toAllocate.getConfig());
    126             if (fromPool != null) {
    127                 bitmapPool.put(fromPool);
    128             }
    129         }
    130 
    131         bitmapPool.put(bitmap);
    132     }
    133 
    134     @Override
    135     public void run() {
    136         if (allocate()) {
    137             handler.postDelayed(this, getNextDelay());
    138         }
    139     }
    140 
    141     private long getNextDelay() {
    142         long result = currentDelay;
    143         currentDelay = Math.min(currentDelay * BACKOFF_RATIO, MAX_BACKOFF_MS);
    144         return result;
    145     }
    146 
    147     private static class UniqueKey implements Key {
    148 
    149         @Override
    150         public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException {
    151             // Do nothing.
    152         }
    153     }
    154 
    155     // Visible for testing.
    156     static class Clock {
    157         public long now() {
    158             return SystemClock.currentThreadTimeMillis();
    159         }
    160     }
    161 }
    162