diff options
author | Sam Judd <judds@google.com> | 2014-10-30 18:45:59 -0700 |
---|---|---|
committer | Sam Judd <judds@google.com> | 2014-11-02 14:07:31 -0800 |
commit | dd737542dc2f6d58cfb1367929c8d3ebc1eeebf4 (patch) | |
tree | 8049827c020f3f9d59e73e8a8d08aaf4fb94ba86 /library/src/main | |
parent | deef4ae2607fcbd32caffc03e2490cbca9134643 (diff) | |
download | glide-dd737542dc2f6d58cfb1367929c8d3ebc1eeebf4.tar.gz |
Add a back off to bitmap pool pre filling.
Diffstat (limited to 'library/src/main')
3 files changed, 165 insertions, 105 deletions
diff --git a/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillIdleHandler.java b/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillIdleHandler.java deleted file mode 100644 index 3fef55c2..00000000 --- a/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillIdleHandler.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.bumptech.glide.load.engine.prefill; - -import android.graphics.Bitmap; -import android.os.MessageQueue; -import android.os.SystemClock; -import android.util.Log; -import com.bumptech.glide.load.Key; -import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; -import com.bumptech.glide.load.engine.cache.MemoryCache; -import com.bumptech.glide.load.resource.bitmap.BitmapResource; -import com.bumptech.glide.util.Util; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.util.HashSet; -import java.util.Set; - -/** - * A class that allocates {@link android.graphics.Bitmap Bitmaps} when the main thread runs out of messages so that the - * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} is pre-populated. - */ -final class BitmapPreFillIdleHandler implements MessageQueue.IdleHandler { - private static final String TAG = "PreFillIdleHandler"; - // Visisble for testing. - static final long MAX_DURATION_MILLIS = 32; - - private static final Clock DEFAULT_CLOCK = new Clock(); - - private final BitmapPool bitmapPool; - private final MemoryCache memoryCache; - private final PreFillQueue toPrefill; - private final Clock clock; - private final Set<PreFillType> seenAttributes = - new HashSet<PreFillType>(); - - private boolean isCancelled; - - public BitmapPreFillIdleHandler(BitmapPool bitmapPool, MemoryCache memoryCache, - PreFillQueue allocationOrder) { - this(bitmapPool, memoryCache, allocationOrder, DEFAULT_CLOCK); - } - - // Visible for testing. - BitmapPreFillIdleHandler(BitmapPool bitmapPool, MemoryCache memoryCache, - PreFillQueue allocationOrder, Clock clock) { - this.bitmapPool = bitmapPool; - this.memoryCache = memoryCache; - this.toPrefill = allocationOrder; - this.clock = clock; - } - - public void cancel() { - isCancelled = true; - } - - @Override - public boolean queueIdle() { - long start = clock.now(); - while (!toPrefill.isEmpty() && (clock.now() - start) < MAX_DURATION_MILLIS) { - PreFillType toAllocate = toPrefill.remove(); - Bitmap bitmap = Bitmap.createBitmap(toAllocate.getWidth(), toAllocate.getHeight(), - toAllocate.getConfig()); - - // Don't over fill the memory cache to avoid evicting useful resources, but make sure it's not empty so - // we use all available space. - if ((memoryCache.getMaxSize() - memoryCache.getCurrentSize()) >= Util.getBitmapByteSize(bitmap)) { - memoryCache.put(new UniqueKey(), BitmapResource.obtain(bitmap, bitmapPool)); - } else { - if (seenAttributes.add(toAllocate)) { - Bitmap fromPool = bitmapPool.get(toAllocate.getWidth(), toAllocate.getHeight(), - toAllocate.getConfig()); - if (fromPool != null) { - bitmapPool.put(fromPool); - } - } - bitmapPool.put(bitmap); - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "allocated [" + toAllocate.getWidth() + "x" + toAllocate.getHeight() + "] " - + toAllocate.getConfig() + " size: " + Util.getBitmapByteSize(bitmap)); - } - } - - return !isCancelled && !toPrefill.isEmpty(); - } - - private static class UniqueKey implements Key { - - @Override - public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { - // Do nothing. - } - } - - // Visible for testing. - static class Clock { - public long now() { - return SystemClock.currentThreadTimeMillis(); - } - } -} diff --git a/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillRunner.java b/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillRunner.java new file mode 100644 index 00000000..20c71a55 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillRunner.java @@ -0,0 +1,160 @@ +package com.bumptech.glide.load.engine.prefill; + +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; +import com.bumptech.glide.load.Key; +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.engine.cache.MemoryCache; +import com.bumptech.glide.load.resource.bitmap.BitmapResource; +import com.bumptech.glide.util.Util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * A class that allocates {@link android.graphics.Bitmap Bitmaps} to make sure that the + * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} is pre-populated. + * + * <p>By posting to the main thread with backoffs, we try to avoid ANRs when the garbage collector gets into a state + * where a high percentage of {@link Bitmap} allocations trigger a stop the world GC. We try to detect whether or not a + * GC has occurred by only allowing our allocator to run for a limited number of milliseconds. Since the allocations + * themselves very fast, a GC is the most likely reason for a substantial delay. If we detect our allocator has run for + * more than our limit, we assume a GC has occurred, stop the current allocations, and try again after a delay. + */ +final class BitmapPreFillRunner implements Runnable { + private static final String TAG = "PreFillRunner"; + private static final Clock DEFAULT_CLOCK = new Clock(); + + /** + * The maximum number of millis we can run before posting. Set to match and detect the duration of non concurrent + * GCs. + */ + static final long MAX_DURATION_MS = 32; + + /** + * The amount of time in ms we wait before continuing to allocate after the first GC is detected. + */ + static final long INITIAL_BACKOFF_MS = 40; + + /** + * The amount by which the current backoff time is multiplied each time we detect a GC. + */ + static final int BACKOFF_RATIO = 4; + + /** + * The maximum amount of time in ms we wait before continuing to allocate. + */ + static final long MAX_BACKOFF_MS = TimeUnit.SECONDS.toMillis(1); + + private final BitmapPool bitmapPool; + private final MemoryCache memoryCache; + private final PreFillQueue toPrefill; + private final Clock clock; + private final Set<PreFillType> seenTypes = new HashSet<PreFillType>(); + private final Handler handler; + + private long currentDelay = INITIAL_BACKOFF_MS; + private boolean isCancelled; + + public BitmapPreFillRunner(BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder) { + this(bitmapPool, memoryCache, allocationOrder, DEFAULT_CLOCK, new Handler(Looper.getMainLooper())); + } + + // Visible for testing. + BitmapPreFillRunner(BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder, Clock clock, + Handler handler) { + this.bitmapPool = bitmapPool; + this.memoryCache = memoryCache; + this.toPrefill = allocationOrder; + this.clock = clock; + this.handler = handler; + } + + public void cancel() { + isCancelled = true; + } + + /** + * Attempts to allocate {@link android.graphics.Bitmap}s and returns {@code true} if there are more + * {@link android.graphics.Bitmap}s to allocate and {@code false} otherwise. + */ + private boolean allocate() { + long start = clock.now(); + while (!toPrefill.isEmpty() && !isGcDetected(start)) { + PreFillType toAllocate = toPrefill.remove(); + Bitmap bitmap = Bitmap.createBitmap(toAllocate.getWidth(), toAllocate.getHeight(), + toAllocate.getConfig()); + + // Don't over fill the memory cache to avoid evicting useful resources, but make sure it's not empty so + // we use all available space. + if (getFreeMemoryCacheBytes() >= Util.getBitmapByteSize(bitmap)) { + memoryCache.put(new UniqueKey(), BitmapResource.obtain(bitmap, bitmapPool)); + } else { + addToBitmapPool(toAllocate, bitmap); + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "allocated [" + toAllocate.getWidth() + "x" + toAllocate.getHeight() + "] " + + toAllocate.getConfig() + " size: " + Util.getBitmapByteSize(bitmap)); + } + } + + return !isCancelled && !toPrefill.isEmpty(); + } + + private boolean isGcDetected(long startTimeMs) { + return clock.now() - startTimeMs >= MAX_DURATION_MS; + } + + private int getFreeMemoryCacheBytes() { + return memoryCache.getMaxSize() - memoryCache.getCurrentSize(); + } + + private void addToBitmapPool(PreFillType toAllocate, Bitmap bitmap) { + // 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 + // is at the front of the queue so that the Bitmap we're adding won't be evicted immediately. + if (seenTypes.add(toAllocate)) { + Bitmap fromPool = bitmapPool.get(toAllocate.getWidth(), toAllocate.getHeight(), + toAllocate.getConfig()); + if (fromPool != null) { + bitmapPool.put(fromPool); + } + } + + bitmapPool.put(bitmap); + } + + @Override + public void run() { + if (allocate()) { + handler.postDelayed(this, getNextDelay()); + } + } + + private long getNextDelay() { + long result = currentDelay; + currentDelay = Math.min(currentDelay * BACKOFF_RATIO, MAX_BACKOFF_MS); + return result; + } + + private static class UniqueKey implements Key { + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { + // Do nothing. + } + } + + // Visible for testing. + static class Clock { + public long now() { + return SystemClock.currentThreadTimeMillis(); + } + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFiller.java b/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFiller.java index 4671a275..e4ca67df 100644 --- a/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFiller.java +++ b/library/src/main/java/com/bumptech/glide/load/engine/prefill/BitmapPreFiller.java @@ -1,6 +1,7 @@ package com.bumptech.glide.load.engine.prefill; import android.graphics.Bitmap; +import android.os.Handler; import android.os.Looper; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; @@ -19,8 +20,9 @@ public final class BitmapPreFiller { private final MemoryCache memoryCache; private final BitmapPool bitmapPool; private final DecodeFormat defaultFormat; + private final Handler handler = new Handler(Looper.getMainLooper()); - private BitmapPreFillIdleHandler current; + private BitmapPreFillRunner current; public BitmapPreFiller(MemoryCache memoryCache, BitmapPool bitmapPool, DecodeFormat defaultFormat) { this.memoryCache = memoryCache; @@ -44,8 +46,8 @@ public final class BitmapPreFiller { } PreFillQueue allocationOrder = generateAllocationOrder(bitmapAttributes); - current = new BitmapPreFillIdleHandler(bitmapPool, memoryCache, allocationOrder); - Looper.myQueue().addIdleHandler(current); + current = new BitmapPreFillRunner(bitmapPool, memoryCache, allocationOrder); + handler.post(current); } // Visible for testing. |