OSDN Git Service

Add a memory pool for reusing direct bytebuffers.
authorPaul Rohde <codelogic@google.com>
Thu, 9 Apr 2015 16:16:29 +0000 (09:16 -0700)
committerPaul Rohde <codelogic@google.com>
Tue, 14 Apr 2015 16:05:46 +0000 (09:05 -0700)
This should reduce GC's caused by releasing and reusing direct
bytebuffers for subsequent image captures and jpg encoding.

Bug: 20129797
Change-Id: I5dd2538bd3dbc77be1446564f537e7ec0fc56ec4

src/com/android/camera/processing/imagebackend/ImageBackend.java
src/com/android/camera/processing/imagebackend/TaskCompressImageToJpeg.java
src/com/android/camera/processing/imagebackend/TaskJpegEncode.java
src/com/android/camera/processing/imagebackend/TaskPreviewChainedJpeg.java
src/com/android/camera/processing/memory/ByteBufferDirectPool.java [new file with mode: 0644]
src/com/android/camera/processing/memory/LruPool.java [new file with mode: 0644]
src/com/android/camera/processing/memory/LruResourcePool.java [new file with mode: 0644]
src/com/android/camera/processing/memory/SimpleLruResourcePool.java [new file with mode: 0644]

index 98422a9..3297ff5 100644 (file)
@@ -18,11 +18,13 @@ package com.android.camera.processing.imagebackend;
 
 import com.android.camera.debug.Log;
 import com.android.camera.processing.ProcessingTaskConsumer;
+import com.android.camera.processing.memory.ByteBufferDirectPool;
+import com.android.camera.processing.memory.LruResourcePool;
 import com.android.camera.session.CaptureSession;
 import com.android.camera.util.Size;
-
 import com.google.common.base.Optional;
 
+import java.nio.ByteBuffer;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -82,6 +84,7 @@ import java.util.concurrent.locks.ReentrantLock;
  * already been completed should return immediately on its process call.
  */
 public class ImageBackend implements ImageConsumer, ImageTaskManager {
+    private final static Log.Tag TAG = new Log.Tag("ImageBackend");
 
     protected static final int FAST_THREAD_PRIORITY = Thread.MAX_PRIORITY;
     protected static final int AVERAGE_THREAD_PRIORITY = Thread.NORM_PRIORITY - 1;
@@ -91,6 +94,8 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager {
     protected static final int NUM_THREADS_AVERAGE = 2;
     protected static final int NUM_THREADS_SLOW = 2;
 
+    private static final int IMAGE_BACKEND_HARD_REF_POOL_SIZE = 2;
+
     protected final ProcessingTaskConsumer mProcessingTaskConsumer;
 
     /**
@@ -108,7 +113,7 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager {
     protected final ExecutorService mThreadPoolAverage;
     protected final ExecutorService mThreadPoolSlow;
 
-    private final static Log.Tag TAG = new Log.Tag("ImageBackend");
+    private final LruResourcePool<Integer, ByteBuffer> mByteBufferDirectPool;
 
     /**
      * Approximate viewable size (in pixels) for the fast thumbnail in the
@@ -143,6 +148,7 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager {
         mThreadPoolAverage = Executors.newFixedThreadPool(NUM_THREADS_AVERAGE,
                 new AverageThreadFactory());
         mThreadPoolSlow = Executors.newFixedThreadPool(NUM_THREADS_SLOW, new SlowThreadFactory());
+        mByteBufferDirectPool = new ByteBufferDirectPool(IMAGE_BACKEND_HARD_REF_POOL_SIZE);
         mProxyListener = new ImageProcessorProxyListener();
         mImageSemaphoreMap = new HashMap<>();
         mShadowTaskMap = new HashMap<>();
@@ -158,13 +164,17 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager {
      * @param slowService Service where Tasks of SLOW Priority are placed.
      * @param imageProcessorProxyListener iamge proxy listener to be used
      */
-    public ImageBackend(ExecutorService fastService, ExecutorService averageService,
+    public ImageBackend(ExecutorService fastService,
+            ExecutorService averageService,
             ExecutorService slowService,
+            LruResourcePool<Integer, ByteBuffer> byteBufferDirectPool,
             ImageProcessorProxyListener imageProcessorProxyListener,
-            ProcessingTaskConsumer processingTaskConsumer, int tinyThumbnailSize) {
+            ProcessingTaskConsumer processingTaskConsumer,
+            int tinyThumbnailSize) {
         mThreadPoolFast = fastService;
         mThreadPoolAverage = averageService;
         mThreadPoolSlow = slowService;
+        mByteBufferDirectPool = byteBufferDirectPool;
         mProxyListener = imageProcessorProxyListener;
         mImageSemaphoreMap = new HashMap<>();
         mShadowTaskMap = new HashMap<>();
@@ -487,11 +497,12 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager {
                 // JPEG compression of the YUV Image, and writes the result to
                 // disk
                 tasksToExecute.add(new TaskPreviewChainedJpeg(img, executor, this, session,
-                        FILMSTRIP_THUMBNAIL_TARGET_SIZE));
+                        FILMSTRIP_THUMBNAIL_TARGET_SIZE, mByteBufferDirectPool));
             } else {
                 // Request job that only does JPEG compression and writes the
                 // result to disk
-                tasksToExecute.add(new TaskCompressImageToJpeg(img, executor, this, session));
+                tasksToExecute.add(new TaskCompressImageToJpeg(img, executor, this, session,
+                      mByteBufferDirectPool));
             }
         }
 
@@ -546,7 +557,8 @@ public class ImageBackend implements ImageConsumer, ImageTaskManager {
 
     public TaskCompressImageToJpeg createTaskCompressImageToJpeg(ImageToProcess image,
             Executor executor, ImageBackend imageBackend, CaptureSession session) {
-        return new TaskCompressImageToJpeg(image, executor, imageBackend, session);
+        return new TaskCompressImageToJpeg(image, executor, imageBackend, session,
+              mByteBufferDirectPool);
     }
 
     /**
index b5120e5..118cfed 100644 (file)
@@ -29,6 +29,8 @@ import com.android.camera.exif.ExifInterface;
 import com.android.camera.one.v2.camera2proxy.CaptureResultProxy;
 import com.android.camera.one.v2.camera2proxy.ImageProxy;
 import com.android.camera.one.v2.camera2proxy.TotalCaptureResultProxy;
+import com.android.camera.processing.memory.LruResourcePool;
+import com.android.camera.processing.memory.LruResourcePool.Resource;
 import com.android.camera.session.CaptureSession;
 import com.android.camera.util.ExifUtil;
 import com.android.camera.util.JpegUtilNative;
@@ -51,6 +53,15 @@ import java.util.concurrent.Executor;
  * that the JPEG is already encoded in the proper orientation.
  */
 public class TaskCompressImageToJpeg extends TaskJpegEncode {
+
+    /**
+     *  Loss-less JPEG compression  is usually about a factor of 5,
+     *  and is a safe lower bound for this value to use to reduce the memory
+     *  footprint for encoding the final jpg.
+     */
+    private static final int MINIMUM_EXPECTED_JPG_COMPRESSION_FACTOR = 5;
+    private final LruResourcePool<Integer, ByteBuffer> mByteBufferDirectPool;
+
     /**
      * Constructor
      *
@@ -61,8 +72,10 @@ public class TaskCompressImageToJpeg extends TaskJpegEncode {
      */
     TaskCompressImageToJpeg(ImageToProcess image, Executor executor,
             ImageTaskManager imageTaskManager,
-            CaptureSession captureSession) {
+            CaptureSession captureSession,
+            LruResourcePool<Integer, ByteBuffer> byteBufferResourcePool) {
         super(image, executor, imageTaskManager, ProcessingPriority.SLOW, captureSession);
+        mByteBufferDirectPool = byteBufferResourcePool;
     }
 
     /**
@@ -105,6 +118,7 @@ public class TaskCompressImageToJpeg extends TaskJpegEncode {
         int numBytes;
         ByteBuffer compressedData;
         ExifInterface exifData = null;
+        Resource<ByteBuffer> byteBufferResource = null;
 
         switch (img.proxy.getFormat()) {
             case ImageFormat.JPEG:
@@ -240,13 +254,27 @@ public class TaskCompressImageToJpeg extends TaskJpegEncode {
 
                     onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE);
 
-                    compressedData = ByteBuffer.allocateDirect(3 * resultImage.width
-                            * resultImage.height);
+                    // WARNING:
+                    // This reduces the size of the buffer that is created
+                    // to hold the final jpg. It is reduced by the "Minimum expected
+                    // jpg compression factor" to reduce memory allocation consumption.
+                    // If the final jpg is more than this size the image will be
+                    // corrupted. The maximum size of an image is width * height *
+                    // number_of_channels. We artificially reduce this number based on
+                    // what we expect the compression ratio to be to reduce the
+                    // amount of memory we are required to allocate.
+                    int maxPossibleJpgSize = 3 * resultImage.width * resultImage.height;
+                    int jpgBufferSize = maxPossibleJpgSize /
+                          MINIMUM_EXPECTED_JPG_COMPRESSION_FACTOR;
+
+                    byteBufferResource = mByteBufferDirectPool.acquire(jpgBufferSize);
+                    compressedData = byteBufferResource.get();
 
                     // On memory allocation failure, fail gracefully.
                     if (compressedData == null) {
                         // TODO: Put memory allocation failure code here.
                         mSession.finishWithFailure(-1, true);
+                        byteBufferResource.close();
                         return;
                     }
 
@@ -255,7 +283,28 @@ public class TaskCompressImageToJpeg extends TaskJpegEncode {
                             img.proxy, compressedData, getJpegCompressionQuality(),
                             img.crop, inputImage.orientation.getDegrees());
 
+                    // If the compression overflows the size of the buffer, the
+                    // actual number of bytes will be returned.
+                    if (numBytes > jpgBufferSize) {
+                        byteBufferResource.close();
+                        mByteBufferDirectPool.acquire(maxPossibleJpgSize);
+                        compressedData = byteBufferResource.get();
+
+                        // On memory allocation failure, fail gracefully.
+                        if (compressedData == null) {
+                            // TODO: Put memory allocation failure code here.
+                            mSession.finishWithFailure(-1, true);
+                            byteBufferResource.close();
+                            return;
+                        }
+
+                        numBytes = compressJpegFromYUV420Image(
+                              img.proxy, compressedData, getJpegCompressionQuality(),
+                              img.crop, inputImage.orientation.getDegrees());
+                    }
+
                     if (numBytes < 0) {
+                        byteBufferResource.close();
                         throw new RuntimeException("Error compressing jpeg.");
                     }
                     compressedData.limit(numBytes);
@@ -275,6 +324,10 @@ public class TaskCompressImageToJpeg extends TaskJpegEncode {
         compressedData.get(writeOut);
         compressedData.rewind();
 
+        if (byteBufferResource != null) {
+            byteBufferResource.close();
+        }
+
         onJpegEncodeDone(mId, inputImage, resultImage, writeOut,
                 TaskInfo.Destination.FINAL_IMAGE);
 
index 333c693..c818401 100644 (file)
@@ -23,11 +23,9 @@ import android.graphics.Rect;
 import android.graphics.YuvImage;
 import android.net.Uri;
 
-import com.android.camera.app.OrientationManager;
 import com.android.camera.debug.Log;
 import com.android.camera.one.v2.camera2proxy.ImageProxy;
 import com.android.camera.session.CaptureSession;
-import com.android.camera.util.Size;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
index 3fb57b2..b20b764 100644 (file)
 package com.android.camera.processing.imagebackend;
 
 import android.graphics.Rect;
-import com.android.camera.debug.Log;
+
+import com.android.camera.processing.memory.LruResourcePool;
 import com.android.camera.session.CaptureSession;
 import com.android.camera.util.Size;
 
+import java.nio.ByteBuffer;
 import java.util.concurrent.Executor;
 
 /**
@@ -28,6 +30,8 @@ import java.util.concurrent.Executor;
  * inscribed in a circle.
  */
 public class TaskPreviewChainedJpeg extends TaskConvertImageToRGBPreview {
+    private final LruResourcePool<Integer, ByteBuffer> mByteBufferDirectPool;
+
     /**
      * Constructor
      *
@@ -38,10 +42,15 @@ public class TaskPreviewChainedJpeg extends TaskConvertImageToRGBPreview {
      * @param captureSession Capture session that bound to this image
      * @param targetSize Approximate viewable pixel demensions of the desired
      *            preview Image     */
-    TaskPreviewChainedJpeg(ImageToProcess image, Executor executor,
-            ImageTaskManager imageTaskManager, CaptureSession captureSession, Size targetSize) {
+    TaskPreviewChainedJpeg(ImageToProcess image,
+            Executor executor,
+            ImageTaskManager imageTaskManager,
+            CaptureSession captureSession,
+            Size targetSize,
+            LruResourcePool<Integer, ByteBuffer> byteBufferResourcePool) {
         super(image, executor, imageTaskManager, ProcessingPriority.SLOW, captureSession,
                 targetSize , ThumbnailShape.MAINTAIN_ASPECT_NO_INSET);
+        mByteBufferDirectPool = byteBufferResourcePool;
     }
 
     public void logWrapper(String message) {
@@ -72,7 +81,7 @@ public class TaskPreviewChainedJpeg extends TaskConvertImageToRGBPreview {
 
             // Chain JPEG task
             TaskImageContainer jpegTask = new TaskCompressImageToJpeg(img, mExecutor,
-                    mImageTaskManager, mSession);
+                    mImageTaskManager, mSession, mByteBufferDirectPool);
             mImageTaskManager.appendTasks(img, jpegTask);
         } finally {
             // Signal backend that reference has been released
diff --git a/src/com/android/camera/processing/memory/ByteBufferDirectPool.java b/src/com/android/camera/processing/memory/ByteBufferDirectPool.java
new file mode 100644 (file)
index 0000000..f87e589
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.processing.memory;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Resource pool for large, directly allocated byte buffers. The integer key
+ * represents the size of the bytebuffer.
+ */
+public final class ByteBufferDirectPool extends SimpleLruResourcePool<Integer, ByteBuffer> {
+    public ByteBufferDirectPool(int lruSize) {
+        super(lruSize);
+    }
+
+    @Override
+    protected ByteBuffer create(Integer bytes) {
+        return ByteBuffer.allocateDirect(bytes);
+    }
+
+    @Override
+    protected ByteBuffer recycle(Integer integer, ByteBuffer byteBuffer) {
+        // Reset byte buffer location and limits
+        byteBuffer.rewind();
+        byteBuffer.limit(byteBuffer.capacity());
+        return byteBuffer;
+    }
+}
diff --git a/src/com/android/camera/processing/memory/LruPool.java b/src/com/android/camera/processing/memory/LruPool.java
new file mode 100644 (file)
index 0000000..93a1cdf
--- /dev/null
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.processing.memory;
+
+import com.google.common.base.Preconditions;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Queue;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * LruPool that will evict items from the pool after reaching maximum size in
+ * order of Least Recently Used (LRU). This code is based on the Android
+ * Lru implementation but removes the hard requirement that keys must only
+ * exist once. Different values may be returned for the same key, and there is
+ * no guarantee that adding and then immediately acquiring the same key will
+ * return the same value instance.
+ *
+ * The size of the pool is generally equal to the number of items, but can be
+ * reconfigured by a subclass to be proportional to some other computed value.
+ *
+ * This class has multiple moving parts and should only be use to track and
+ * reuse objects that are expensive to create or re-create.
+ *
+ * WARNING:
+ * {@link #acquire(TKey)} is currently linear time, pending a better
+ * implementation.
+ *
+ * TODO: Build a constant time acquire(TKey) method implementation.
+ *
+ */
+public class LruPool<TKey, TValue> {
+    public static class Configuration<TKey, TValue> {
+        /**
+         * Called for entries that have been evicted or removed. This method is
+         * invoked when a value is evicted to make space, but NOT when an item is
+         * removed via {@link #acquire}. The default
+         * implementation does nothing.
+         *
+         * <p>The method is called without synchronization: other threads may
+         * access the cache while this method is executing.
+         */
+        void entryEvicted(TKey key, TValue value) { }
+
+        /**
+         * Called after a cache miss to compute a value for the corresponding key.
+         * Returns the computed value or null if no value can be computed. The
+         * default implementation returns null.
+         *
+         * <p>The method is called without synchronization: other threads may
+         * access the cache while this method is executing.
+         */
+        TValue create(TKey key) {
+            return null;
+        }
+
+        /**
+         * Returns the size of the entry for {@code key} and {@code value} in
+         * user-defined units.  The default implementation returns 1 so that size
+         * is the number of entries and max size is the maximum number of entries.
+         *
+         * <p>An entry's size must not change while it is in the cache.
+         */
+        int sizeOf(TKey key, TValue value) {
+            return 1;
+        }
+    }
+
+    private final Object mLock;
+
+    /**
+     * Maintains an ordered list of keys by "most recently added". Duplicate
+     * keys can exist in the list.
+     */
+    @GuardedBy("mLock")
+    private final LinkedList<TKey> mLruKeyList;
+
+    /**
+     * Maintains individual pools for each distinct key type.
+     */
+    @GuardedBy("mLock")
+    private final HashMap<TKey, Queue<TValue>> mValuePool;
+    private final Configuration<TKey, TValue> mConfiguration;
+
+    private final int mMaxSize;
+
+    /**
+     * Size may be configured to represent quantities other than the number of
+     * items in the pool. By default, it represents the number of items
+     * in the pool.
+     */
+    @GuardedBy("mLock")
+    private int mSize;
+
+    /**
+     * Creates and sets the size of the Lru Pool
+     *
+     * @param maxSize Sets the size of the Lru Pool.
+     */
+    public LruPool(int maxSize) {
+        this(maxSize, new Configuration<TKey, TValue>());
+    }
+
+    public LruPool(int maxSize, Configuration<TKey, TValue> configuration) {
+        Preconditions.checkArgument(maxSize > 0, "maxSize must be > 0.");
+
+        mMaxSize = maxSize;
+        mConfiguration = configuration;
+
+        mLock = new Object();
+        mLruKeyList = new LinkedList<>();
+        mValuePool = new HashMap<>();
+    }
+
+    /**
+     * Acquire a value from the pool, or attempt to create a new one if the create
+     * method is overridden. If an item cannot be retrieved or created, this method
+     * will return null.
+     *
+     * WARNING:
+     * This implementation is currently linear time, pending a better
+     * implementation.
+     *
+     * TODO: Build a constant time acquire(TKey) method implementation.
+     *
+     * @param key the type of object to retrieve from the pool.
+     * @return a value or null if none exists or can be created.
+     */
+    public final TValue acquire(TKey key) {
+        Preconditions.checkNotNull(key);
+
+        // We must remove the item we acquire from the list
+        TValue value;
+
+        synchronized (mLock) {
+            if (mLruKeyList.removeLastOccurrence(key)) {
+                value = mValuePool.get(key).remove();
+                mSize -= checkedSizeOf(key, value);
+            } else {
+                value = mConfiguration.create(key);
+            }
+        }
+
+        return value;
+    }
+
+    /**
+     * Add a new or previously existing value to the pool. The most recently added
+     * item will be placed at the top of the Lru list, and will trim existing items
+     * off the list, if the list exceeds the maximum size.
+     *
+     * @param key the type of object to add to the pool.
+     * @param value the object to add into the pool.
+     */
+    public final void add(TKey key, TValue value) {
+        Preconditions.checkNotNull(key);
+        Preconditions.checkNotNull(value);
+
+        synchronized (mLock) {
+            final Queue<TValue> pool;
+
+            mLruKeyList.push(key);
+            if (!mValuePool.containsKey(key)) {
+                pool = new LinkedList<>();
+                mValuePool.put(key, pool);
+            } else {
+                pool = mValuePool.get(key);
+            }
+            pool.add(value);
+            mSize += checkedSizeOf(key, value);
+
+            unsafeTrimToSize(mMaxSize);
+        }
+    }
+
+    /**
+     * Remove the oldest entries until the total of remaining entries is at or
+     * below the configured size.
+     *
+     * @param trimToSize the maximum size of the cache before returning. May
+     *                   be -1 to evict even 0-sized elements.
+     */
+    public final void trimToSize(int trimToSize) {
+        synchronized (mLock) {
+            unsafeTrimToSize(trimToSize);
+        }
+    }
+
+    /**
+     * For pools that do not override {@link Configuration#sizeOf}, this
+     * returns the number of items in the pool. For custom sizes, this returns
+     * the sum of the sizes of the entries in this pool.
+     */
+    public final int getSize() {
+        synchronized (mLock) {
+            return mSize;
+        }
+    }
+
+    /**
+     * For pools that do not override {@link Configuration#sizeOf}, this
+     * returns the maximum number of entries in the pool. For all other pools,
+     * this returns the maximum sum of the sizes of the entries in this pool.
+     */
+    public final int getMaxSize() {
+        return mMaxSize;
+    }
+
+    @GuardedBy("mLock")
+    private void unsafeTrimToSize(int trimToSize) {
+        while (mSize > trimToSize && !mLruKeyList.isEmpty()) {
+            TKey key = mLruKeyList.removeLast();
+            if (key == null) {
+                break;
+            }
+
+            Queue<TValue> pool = mValuePool.get(key);
+            TValue value = pool.remove();
+
+            if (pool.size() <= 0) {
+                mValuePool.remove(key);
+            }
+
+            mSize = mSize - checkedSizeOf(key, value);
+            mConfiguration.entryEvicted(key, value);
+        }
+
+        if (mSize < 0 || (mLruKeyList.isEmpty() && mSize != 0)) {
+            throw new IllegalStateException("LruPool.sizeOf() is reporting "
+                  + "inconsistent results!");
+        }
+    }
+
+    private int checkedSizeOf(TKey key, TValue value) {
+        int result = mConfiguration.sizeOf(key, value);
+        Preconditions.checkArgument(result >= 0, "Size was < 0.");
+        return result;
+    }
+}
diff --git a/src/com/android/camera/processing/memory/LruResourcePool.java b/src/com/android/camera/processing/memory/LruResourcePool.java
new file mode 100644 (file)
index 0000000..6488951
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.processing.memory;
+
+import com.android.camera.async.SafeCloseable;
+
+import javax.annotation.Nullable;
+
+/**
+ * Pool for and/or creating or reusing expensive resources.
+ */
+public interface LruResourcePool<TKey, TValue> {
+    /**
+     * Returns a wrapped reference to a resource.
+     *
+     * @param key size of memory.
+     * @return T object representing a reusable n-bytes of memory.
+     */
+    public Resource<TValue> acquire(TKey key);
+
+    /**
+     * Closeable resource object that will release the underlying object back to the
+     * source resource pool. A resource may be released or closed multiple times,
+     * and calls to get will return null if the resource has already been released.
+     */
+    public interface Resource<T> extends SafeCloseable {
+        /**
+         * Get the underlying resource. This will return null if the resource has been
+         * closed and returned to the pool.
+         *
+         * @return the resource represented by this instance.
+         */
+        @Nullable
+        public T get();
+    }
+}
diff --git a/src/com/android/camera/processing/memory/SimpleLruResourcePool.java b/src/com/android/camera/processing/memory/SimpleLruResourcePool.java
new file mode 100644 (file)
index 0000000..9cb5c01
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.processing.memory;
+
+import com.google.common.base.Preconditions;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Simple resource based memory pool that can automatically return
+ * items back to the memory pool when closed.
+ */
+@ThreadSafe
+public abstract class SimpleLruResourcePool<TKey, TValue> implements LruResourcePool<TKey, TValue> {
+    @GuardedBy("mLock")
+    private final LruPool<TKey, TValue> mLruPool;
+
+    private final Object mLock;
+
+    public SimpleLruResourcePool(int lruSize) {
+        Preconditions.checkArgument(lruSize > 0);
+
+        mLock = new Object();
+        mLruPool = new LruPool<>(lruSize);
+    }
+
+    @Override
+    public Resource<TValue> acquire(TKey key) {
+        TValue value;
+        synchronized (mLock) {
+            value = mLruPool.acquire(key);
+        }
+
+        // We may not reach a point where we have have a value to reuse,
+        // create a new one.
+        if(value == null) {
+            value = create(key);
+        }
+
+        return new SynchronizedResource<>(this, key, value);
+    }
+
+    /**
+     * Create a new value for a given key.
+     */
+    protected abstract TValue create(TKey key);
+
+    /**
+     * Recycle or reset a given value before it is added back to the pool,
+     * by default, this does nothing.
+     */
+    protected TValue recycle(TKey key, TValue value) {
+        return value;
+    }
+
+    /**
+     * Returns an item to the LruPool.
+     */
+    private void release(TKey key, TValue value) {
+        mLruPool.add(key, recycle(key, value));
+    }
+
+    /**
+     * This is a closable resource that returns the underlying value to the pool
+     * when the object is closed.
+     */
+    @ThreadSafe
+    private static final class SynchronizedResource<TKey, TValue> implements Resource<TValue> {
+        private final Object mLock;
+        private final SimpleLruResourcePool<TKey, TValue> mPool;
+
+        @GuardedBy("mLock")
+        private TKey mKey;
+
+        @GuardedBy("mLock")
+        private TValue mValue;
+
+        public SynchronizedResource(SimpleLruResourcePool<TKey, TValue> pool,
+              TKey key, TValue value) {
+            mPool = pool;
+            mKey = key;
+            mValue = value;
+
+            mLock = new Object();
+        }
+
+        @Nullable
+        @Override
+        public TValue get() {
+            synchronized (mLock) {
+                if (mValue != null) {
+                    return mValue;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public void close() {
+            synchronized (mLock) {
+                if (mValue != null) {
+                    mPool.release(mKey, mValue);
+                    mValue = null;
+                    mKey = null;
+                }
+            }
+        }
+    }
+}