1package com.bumptech.glide.load.engine.prefill;
2
3import android.graphics.Bitmap;
4import android.os.Handler;
5import android.os.Looper;
6import android.os.SystemClock;
7import android.util.Log;
8
9import com.bumptech.glide.load.Key;
10import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
11import com.bumptech.glide.load.engine.cache.MemoryCache;
12import com.bumptech.glide.load.resource.bitmap.BitmapResource;
13import com.bumptech.glide.util.Util;
14
15import java.io.UnsupportedEncodingException;
16import java.security.MessageDigest;
17import java.util.HashSet;
18import java.util.Set;
19import 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 */
31final 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