1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera.processing.memory;
18
19import com.google.common.base.Preconditions;
20
21import java.util.HashMap;
22import java.util.LinkedList;
23import java.util.Queue;
24
25import javax.annotation.concurrent.GuardedBy;
26
27/**
28 * LruPool that will evict items from the pool after reaching maximum size in
29 * order of Least Recently Used (LRU). This code is based on the Android
30 * Lru implementation but removes the hard requirement that keys must only
31 * exist once. Different values may be returned for the same key, and there is
32 * no guarantee that adding and then immediately acquiring the same key will
33 * return the same value instance.
34 *
35 * The size of the pool is generally equal to the number of items, but can be
36 * reconfigured by a subclass to be proportional to some other computed value.
37 *
38 * This class has multiple moving parts and should only be use to track and
39 * reuse objects that are expensive to create or re-create.
40 *
41 * WARNING:
42 * {@link #acquire(TKey)} is currently linear time, pending a better
43 * implementation.
44 *
45 * TODO: Build a constant time acquire(TKey) method implementation.
46 *
47 */
48public class LruPool<TKey, TValue> {
49    public static class Configuration<TKey, TValue> {
50        /**
51         * Called for entries that have been evicted or removed. This method is
52         * invoked when a value is evicted to make space, but NOT when an item is
53         * removed via {@link #acquire}. The default
54         * implementation does nothing.
55         *
56         * <p>The method is called without synchronization: other threads may
57         * access the cache while this method is executing.
58         */
59        void entryEvicted(TKey key, TValue value) { }
60
61        /**
62         * Called after a cache miss to compute a value for the corresponding key.
63         * Returns the computed value or null if no value can be computed. The
64         * default implementation returns null.
65         *
66         * <p>The method is called without synchronization: other threads may
67         * access the cache while this method is executing.
68         */
69        TValue create(TKey key) {
70            return null;
71        }
72
73        /**
74         * Returns the size of the entry for {@code key} and {@code value} in
75         * user-defined units.  The default implementation returns 1 so that size
76         * is the number of entries and max size is the maximum number of entries.
77         *
78         * <p>An entry's size must not change while it is in the cache.
79         */
80        int sizeOf(TKey key, TValue value) {
81            return 1;
82        }
83    }
84
85    private final Object mLock;
86
87    /**
88     * Maintains an ordered list of keys by "most recently added". Duplicate
89     * keys can exist in the list.
90     */
91    @GuardedBy("mLock")
92    private final LinkedList<TKey> mLruKeyList;
93
94    /**
95     * Maintains individual pools for each distinct key type.
96     */
97    @GuardedBy("mLock")
98    private final HashMap<TKey, Queue<TValue>> mValuePool;
99    private final Configuration<TKey, TValue> mConfiguration;
100
101    private final int mMaxSize;
102
103    /**
104     * Size may be configured to represent quantities other than the number of
105     * items in the pool. By default, it represents the number of items
106     * in the pool.
107     */
108    @GuardedBy("mLock")
109    private int mSize;
110
111    /**
112     * Creates and sets the size of the Lru Pool
113     *
114     * @param maxSize Sets the size of the Lru Pool.
115     */
116    public LruPool(int maxSize) {
117        this(maxSize, new Configuration<TKey, TValue>());
118    }
119
120    public LruPool(int maxSize, Configuration<TKey, TValue> configuration) {
121        Preconditions.checkArgument(maxSize > 0, "maxSize must be > 0.");
122
123        mMaxSize = maxSize;
124        mConfiguration = configuration;
125
126        mLock = new Object();
127        mLruKeyList = new LinkedList<>();
128        mValuePool = new HashMap<>();
129    }
130
131    /**
132     * Acquire a value from the pool, or attempt to create a new one if the create
133     * method is overridden. If an item cannot be retrieved or created, this method
134     * will return null.
135     *
136     * WARNING:
137     * This implementation is currently linear time, pending a better
138     * implementation.
139     *
140     * TODO: Build a constant time acquire(TKey) method implementation.
141     *
142     * @param key the type of object to retrieve from the pool.
143     * @return a value or null if none exists or can be created.
144     */
145    public final TValue acquire(TKey key) {
146        Preconditions.checkNotNull(key);
147
148        // We must remove the item we acquire from the list
149        TValue value;
150
151        synchronized (mLock) {
152            if (mLruKeyList.removeLastOccurrence(key)) {
153                value = mValuePool.get(key).remove();
154                mSize -= checkedSizeOf(key, value);
155            } else {
156                value = mConfiguration.create(key);
157            }
158        }
159
160        return value;
161    }
162
163    /**
164     * Add a new or previously existing value to the pool. The most recently added
165     * item will be placed at the top of the Lru list, and will trim existing items
166     * off the list, if the list exceeds the maximum size.
167     *
168     * @param key the type of object to add to the pool.
169     * @param value the object to add into the pool.
170     */
171    public final void add(TKey key, TValue value) {
172        Preconditions.checkNotNull(key);
173        Preconditions.checkNotNull(value);
174
175        synchronized (mLock) {
176            final Queue<TValue> pool;
177
178            mLruKeyList.push(key);
179            if (!mValuePool.containsKey(key)) {
180                pool = new LinkedList<>();
181                mValuePool.put(key, pool);
182            } else {
183                pool = mValuePool.get(key);
184            }
185            pool.add(value);
186            mSize += checkedSizeOf(key, value);
187
188            unsafeTrimToSize(mMaxSize);
189        }
190    }
191
192    /**
193     * Remove the oldest entries until the total of remaining entries is at or
194     * below the configured size.
195     *
196     * @param trimToSize the maximum size of the cache before returning. May
197     *                   be -1 to evict even 0-sized elements.
198     */
199    public final void trimToSize(int trimToSize) {
200        synchronized (mLock) {
201            unsafeTrimToSize(trimToSize);
202        }
203    }
204
205    /**
206     * For pools that do not override {@link Configuration#sizeOf}, this
207     * returns the number of items in the pool. For custom sizes, this returns
208     * the sum of the sizes of the entries in this pool.
209     */
210    public final int getSize() {
211        synchronized (mLock) {
212            return mSize;
213        }
214    }
215
216    /**
217     * For pools that do not override {@link Configuration#sizeOf}, this
218     * returns the maximum number of entries in the pool. For all other pools,
219     * this returns the maximum sum of the sizes of the entries in this pool.
220     */
221    public final int getMaxSize() {
222        return mMaxSize;
223    }
224
225    @GuardedBy("mLock")
226    private void unsafeTrimToSize(int trimToSize) {
227        while (mSize > trimToSize && !mLruKeyList.isEmpty()) {
228            TKey key = mLruKeyList.removeLast();
229            if (key == null) {
230                break;
231            }
232
233            Queue<TValue> pool = mValuePool.get(key);
234            TValue value = pool.remove();
235
236            if (pool.size() <= 0) {
237                mValuePool.remove(key);
238            }
239
240            mSize = mSize - checkedSizeOf(key, value);
241            mConfiguration.entryEvicted(key, value);
242        }
243
244        if (mSize < 0 || (mLruKeyList.isEmpty() && mSize != 0)) {
245            throw new IllegalStateException("LruPool.sizeOf() is reporting "
246                  + "inconsistent results!");
247        }
248    }
249
250    private int checkedSizeOf(TKey key, TValue value) {
251        int result = mConfiguration.sizeOf(key, value);
252        Preconditions.checkArgument(result >= 0, "Size was < 0.");
253        return result;
254    }
255}
256