blob: 04a02405e810e47bcc4d3b26abb49a5807c5d2f4 [file] [log] [blame]
/*
* Copyright (C) 2014 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;
import com.android.camera.one.v2.camera2proxy.ImageProxy;
import com.android.camera.processing.TaskImageContainer.ProcessingPriority;
import com.android.camera.debug.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* This ImageBackend is created for the purpose of creating a task-running infrastructure that has
* two-level of priority and doing the book-keeping to keep track of tasks that use Android Images.
* Android.media.images are critical system resources that MUST be properly managed in order to
* maintain camera application performance. Android.media.images are merely Java handles to regions
* of physically contiguous memory used by the camera hardware as a destination for imaging data. In
* general, this physically contiguous memory is not counted as an application resource, but as a
* system resources held by the application and does NOT count against the limits of application
* memory. The performance pressures of both computing and memory resources must often be
* prioritized in releasing Android.media.images in a timely manner. In order to properly balance
* these concerns, most image processing requested should be routed through this object. This object
* is also responsible for releasing Android.media image as soon as possible, so as not to stall the
* camera hardware subsystem. Image that reserve these images are a subclass of the basic Java
* Runnable with a few conditions placed upon their run() implementation:
* <ol>
* <li>The task will try to release the image as early as possible by calling the
* releaseSemaphoreReference as soon as a reference to the original image is no longer required.</li>
* <li>A set of tasks that require ImageData must only happen on the first receiveImage call.
* receiveImage must only be called once per image.</li>
* <li>However, the submitted tasks may spawn new tasks via the appendTask with any image that have
* had a task submitted, but NOT released via releaseSemaphoreReference.</li>
* <li>Computation that is dependent on multiple images should be written into this task framework
* in a distributed manner where image task can be computed independently and join their results to
* a common shared object.This style of implementation allows for the earliest release of Android
* Images while honoring the resources priorities set by this class. See the Lucky shot
* implementation for a concrete example for this shared object and its respective task
* {@link TaskLuckyShotSession} {@link LuckyShotSession}</li>
* </ol>
*/
public class ImageBackend implements ImageConsumer {
protected static final int FAST_THREAD_PRIORITY = Thread.MAX_PRIORITY;
protected static final int SLOW_THREAD_PRIORITY = Thread.NORM_PRIORITY;
protected static final int NUM_THREADS_FAST = 2;
protected static final int NUM_THREADS_SLOW = 2;
protected final SimpleCache mSimpleCache;
protected final Map<ImageProxy, ImageReleaseProtocol> mImageSemaphoreMap;
protected final ExecutorService mThreadPoolFast;
protected final ExecutorService mThreadPoolSlow;
private final static Log.Tag TAG = new Log.Tag("ImageBackend");
// Some invariants to know that we're keeping track of everything
// that reflect the state of mImageSemaphoreMap
private int mOutstandingImageRefs = 0;
private int mOutstandingImageOpened = 0;
private int mOutstandingImageClosed = 0;
// Objects that may be registered to this objects events.
private ImageProcessorProxyListener mProxyListener = null;
// Default constructor, values are conservatively targeted to the Nexus 6
public ImageBackend() {
mThreadPoolFast = Executors.newFixedThreadPool(NUM_THREADS_FAST, new FastThreadFactory());
mThreadPoolSlow = Executors.newFixedThreadPool(NUM_THREADS_SLOW, new SlowThreadFactory());
mProxyListener = new ImageProcessorProxyListener();
mImageSemaphoreMap = new HashMap<ImageProxy, ImageReleaseProtocol>();
mSimpleCache = new SimpleCache(NUM_THREADS_SLOW);
}
/**
* Direct Injection Constructor for Testing purposes.
*
* @param fastService Service where Tasks of FAST Priority are placed.
* @param slowService Service where Tasks of SLOW Priority are placed.
* @param imageProcessorProxyListener iamge proxy listener to be used
*/
public ImageBackend(ExecutorService fastService, ExecutorService slowService,
ImageProcessorProxyListener imageProcessorProxyListener) {
mThreadPoolFast = fastService;
mThreadPoolSlow = slowService;
mProxyListener = imageProcessorProxyListener;
mImageSemaphoreMap = new HashMap<ImageProxy, ImageReleaseProtocol>();
mSimpleCache = new SimpleCache(NUM_THREADS_SLOW);
}
/**
* Simple getter for the simple cache functionality associated with this instantiation. Needs to
* be accessed by the tasks in order to get/return memory. TODO: Replace with something better.
*
* @return cache object that implements a simple memory pool for this object.
*/
public SimpleCache getCache() {
return mSimpleCache;
}
/**
* Simple getting for the associated listener object associated with this instantiation that
* handles registration of events listeners.
*
* @return listener proxy that handles events messaging for this object.
*/
public ImageProcessorProxyListener getProxyListener() {
return mProxyListener;
}
public void setCameraImageProcessorListener(ImageProcessorListener listener) {
this.mProxyListener.registerListener(listener, null);
}
/**
* Wrapper function for all log messages created by this object. Default implementation is to
* send messages to the Android logger. For test purposes, this method can be overridden to
* avoid "Stub!" Runtime exceptions in Unit Tests.
*/
public void logWrapper(String message) {
Log.e(TAG, message);
}
/**
* @return Number of Image references currently held by this instance
*/
@Override
public int numberOfReservedOpenImages() {
synchronized (mImageSemaphoreMap) {
// since mOutstandingImageOpened, mOutstandingImageClosed reflect
// the historical state of mImageSemaphoreMap, we need to lock on
// before we return a value.
return mOutstandingImageOpened - mOutstandingImageClosed;
}
}
/**
* Signals the ImageBackend that a tasks has released a reference to the image. Imagebackend
* determines whether all references have been released and applies its specified release
* protocol of closing image and/or unblocking the caller. Should ONLY be called by the tasks
* running on this class.
*
* @param img the image to be released by the task.
* @param executor the executor on which the image close is run. if null, image close is run by
* the calling thread (usually the main task thread).
*/
public void releaseSemaphoreReference(final ImageProxy img, Executor executor) {
synchronized (mImageSemaphoreMap) {
ImageReleaseProtocol protocol = mImageSemaphoreMap.get(img);
if (protocol == null || protocol.getCount() <= 0) {
// That means task implementation has allowed an unbalanced
// semaphore release.
throw new RuntimeException(
"ERROR: Task implementation did NOT balance its release.");
}
// Normal operation from here.
protocol.addCount(-1);
mOutstandingImageRefs--;
logWrapper("Ref release. Total refs = " + mOutstandingImageRefs);
if (protocol.getCount() == 0) {
// Image is ready to be released
// Remove the image from the map so that it may be submitted
// again.
mImageSemaphoreMap.remove(img);
// Conditionally close the image, specified by initial
// receiveImage call
if (protocol.closeOnRelease) {
closeImageExecutorSafe(img, executor);
}
// Conditionally signal the blocking thread to go.
if (protocol.blockUntilRelease) {
protocol.signal();
}
} else {
// Image is still being held by other tasks.
// Otherwise, update the semaphore
mImageSemaphoreMap.put(img, protocol);
}
}
}
/**
* Spawns dependent tasks from internal implementation of a task. If a dependent task does NOT
* require the image reference, it should be passed a null pointer as an image reference. In
* general, this method should be called after the task has completed its own computations, but
* before it has released its own image reference (via the releaseSemaphoreReference call).
*
* @param tasks The set of tasks to be run
* @return whether tasks are successfully submitted.
*/
public boolean appendTasks(ImageProxy img, Set<TaskImageContainer> tasks) {
// Make sure that referred images are all the same, if it exists.
// And count how image references need to be kept track of.
int countImageRefs = numPropagatedImageReferences(img, tasks);
if (img != null) {
// If you're still holding onto the reference, make sure you keep
// count
incrementSemaphoreReferenceCount(img, countImageRefs);
}
scheduleTasks(tasks);
return true;
}
/**
* Spawns a single dependent task from internal implementation of a task.
*
* @param task The task to be run
* @return whether tasks are successfully submitted.
*/
public boolean appendTasks(ImageProxy img, TaskImageContainer task) {
Set<TaskImageContainer> tasks = new HashSet<TaskImageContainer>(1);
tasks.add(task);
return appendTasks(img, tasks);
}
/**
* Implements that top-level image single task submission that is defined by the ImageConsumer
* interface.
*
* @param img Image required by the task
* @param task Task to be run
* @param blockUntilImageRelease If true, call blocks until the object img is no longer referred
* by any task. If false, call is non-blocking
* @param closeOnImageRelease If true, images is closed when the object img is is no longer
* referred by any task. If false, After an image is submitted, it should never be
* submitted again to the interface until all tasks and their spawned tasks are
* finished.
*/
@Override
public boolean receiveImage(ImageProxy img, TaskImageContainer task,
boolean blockUntilImageRelease, boolean closeOnImageRelease)
throws InterruptedException {
Set<TaskImageContainer> passTasks = new HashSet<TaskImageContainer>(1);
passTasks.add(task);
return receiveImage(img, passTasks, blockUntilImageRelease, closeOnImageRelease);
}
/**
* Implements that top-level image single task submission that is defined by the ImageConsumer
* interface.
*
* @param img Image required by the task
* @param tasks A set of Tasks to be run
* @param blockUntilImageRelease If true, call blocks until the object img is no longer referred
* by any task. If false, call is non-blocking
* @param closeOnImageRelease If true, images is closed when the object img is is no longer
* referred by any task. If false, After an image is submitted, it should never be
* submitted again to the interface until all tasks and their spawned tasks are
* finished.
* @return whether the blocking completed properly
*/
@Override
public boolean receiveImage(ImageProxy img, Set<TaskImageContainer> tasks,
boolean blockUntilImageRelease, boolean closeOnImageRelease)
throws InterruptedException {
// Short circuit if no tasks submitted.
if (tasks == null || tasks.size() <= 0) {
return false;
}
if (img == null) {
// TODO: Determine whether you need to be so strict at the top level
throw new RuntimeException("ERROR: Initial call must reference valid Image!");
}
// Make sure that referred images are all the same, if it exists.
// And count how image references need to be kept track of.
int countImageRefs = numPropagatedImageReferences(img, tasks);
// Set the semaphore, given that the number of tasks that need to be
// scheduled
// and the boolean flags for imaging closing and thread blocking
ImageReleaseProtocol protocol = setSemaphoreReferenceCount(img, countImageRefs,
blockUntilImageRelease, closeOnImageRelease);
// Put the tasks on their respective queues.
scheduleTasks(tasks);
// Implement blocking if required
if (protocol.blockUntilRelease) {
protocol.block();
}
return true;
}
/**
* Implements that top-level image task submission short-cut that is defined by the
* ImageConsumer interface.
*
* @param img Image required by the task
* @param executor Executor to run events and image closes, in case of control leakage
* @param processingFlags Magical bit vector that specifies jobs to be run After an image is
* submitted, it should never be submitted again to the interface until all tasks and
* their spawned tasks are finished.
*/
@Override
public boolean receiveImage(ImageProxy img, Executor executor,
Set<ImageTaskFlags> processingFlags) throws InterruptedException {
Set<TaskImageContainer> tasksToExecute = new HashSet<TaskImageContainer>();
if (img == null) {
// No data to process, just pure message.
return true;
}
// Now add the pre-mixed versions of the tasks.
if (processingFlags.contains(ImageTaskFlags.COMPRESS_IMAGE_TO_JPEG)
|| processingFlags.contains(ImageTaskFlags.WRITE_IMAGE_TO_DISK)) {
// Add this type of task to the appropriate queue.
tasksToExecute.add(new TaskCompressImageToJpeg(img, executor, this));
}
if (processingFlags.contains(ImageTaskFlags.CONVERT_IMAGE_TO_RGB_PREVIEW)) {
// Add this type of task to the appropriate queue.
tasksToExecute.add(new TaskConvertImageToRGBPreview(img, executor, this, 160, 100));
}
if (processingFlags.contains(ImageTaskFlags.WRITE_IMAGE_TO_DISK)) {
// Add this type of task to the appropriate queue.
// Has a dependency as well on the result JPEG_COMPRESSION
// TODO: Put disk writing implementation within the framework.
}
receiveImage(img, tasksToExecute,
processingFlags.contains(ImageTaskFlags.BLOCK_UNTIL_IMAGE_RELEASE),
processingFlags.contains(ImageTaskFlags.CLOSE_IMAGE_ON_RELEASE));
return true;
}
/**
* Factory functions, in case, you want some shake and bake functionality.
*/
public TaskConvertImageToRGBPreview createTaskConvertImageToRGBPreview(ImageProxy imageProxy,
Executor executor, ImageBackend imageBackend, int targetWidth, int targetHeight) {
return new TaskConvertImageToRGBPreview(imageProxy, executor, imageBackend, targetWidth,
targetHeight);
}
public TaskCompressImageToJpeg createTaskCompressImageToJpeg(ImageProxy imageProxy,
Executor executor, ImageBackend imageBackend) {
return new TaskCompressImageToJpeg(imageProxy, executor, imageBackend);
}
/**
* Blocks and waits for all tasks to complete.
*/
@Override
public void shutdown() {
mThreadPoolSlow.shutdown();
mThreadPoolFast.shutdown();
}
/**
* Puts the tasks on the specified queue. May be more complicated in the future.
*
* @param tasks The set of tasks to be run
*/
protected void scheduleTasks(Set<TaskImageContainer> tasks) {
for (TaskImageContainer task : tasks) {
if (task.getProcessingPriority() == ProcessingPriority.FAST) {
mThreadPoolFast.execute(task);
} else {
mThreadPoolSlow.execute(task);
}
}
}
/**
* Initializes the semaphore count for the image
*
* @return The protocol object that keeps tracks of the image reference count and actions to be
* taken on release.
*/
protected ImageReleaseProtocol setSemaphoreReferenceCount(ImageProxy img, int count,
boolean blockUntilRelease, boolean closeOnRelease) throws RuntimeException {
synchronized (mImageSemaphoreMap) {
if (mImageSemaphoreMap.get(img) != null) {
throw new RuntimeException(
"ERROR: Rewriting of Semaphore Lock. Image references may not freed properly");
}
// Create the new booking-keeping object.
ImageReleaseProtocol protocol = new ImageReleaseProtocol(blockUntilRelease,
closeOnRelease);
protocol.count = count;
mImageSemaphoreMap.put(img, protocol);
mOutstandingImageRefs += count;
mOutstandingImageOpened++;
logWrapper("Received an opened image: " + mOutstandingImageOpened + "/"
+ mOutstandingImageClosed);
logWrapper("Setting an image reference count of " + count + " Total refs = "
+ mOutstandingImageRefs);
return protocol;
}
}
/**
* Increments the semaphore count for the image. Should ONLY be internally via appendTasks by
* internal tasks. Otherwise, image references could get out of whack.
*
* @return Number of Image references currently held by this instance
*/
protected void incrementSemaphoreReferenceCount(ImageProxy img, int count)
throws RuntimeException {
synchronized (mImageSemaphoreMap) {
ImageReleaseProtocol protocol = mImageSemaphoreMap.get(img);
if (mImageSemaphoreMap.get(img) == null) {
throw new RuntimeException(
"Image Reference has already been released or has never been held.");
}
protocol.addCount(count);
mImageSemaphoreMap.put(img, protocol);
mOutstandingImageRefs += count;
}
}
/**
* Close an Image with a executor if it's available and does the proper booking keeping on the
* object.
*
* @param img Image to be closed
* @param executor Executor to be used, if executor is null, the close is run on the task thread
*/
private void closeImageExecutorSafe(final ImageProxy img, Executor executor) {
Runnable closeTask = new Runnable() {
@Override
public void run() {
img.close();
mOutstandingImageClosed++;
logWrapper("Release of image occurred. Good fun. " + "Total Images Open/Closed = "
+ mOutstandingImageOpened + "/" + mOutstandingImageClosed);
}
};
if (executor == null) {
// Just run it on the main thread.
closeTask.run();
} else {
executor.execute(closeTask);
}
}
/**
* Calculates the number of new Image references in a set of dependent tasks. Checks to make
* sure no new image references are being introduced.
*
* @param tasks The set of dependent tasks to be run
*/
private int numPropagatedImageReferences(ImageProxy img, Set<TaskImageContainer> tasks)
throws RuntimeException {
int countImageRefs = 0;
for (TaskImageContainer task : tasks) {
if (task.mImageProxy != null && task.mImageProxy != img) {
throw new RuntimeException("ERROR: Spawned tasks cannot reference new images!");
}
if (task.mImageProxy != null) {
countImageRefs++;
}
}
return countImageRefs;
}
/**
* A simple tuple class to keep track of image reference, and whether to block and/or close on
* final image release. Instantiated on every task submission call.
*/
static private class ImageReleaseProtocol {
public final boolean blockUntilRelease;
public final boolean closeOnRelease;
private int count;
private final ReentrantLock mLock = new ReentrantLock();
private Condition mSignal;
// TODO: Backport to Reentrant lock
public void setCount(int value) {
mLock.lock();
count = value;
mLock.unlock();
}
public int getCount() {
int value;
mLock.lock();
value = count;
mLock.unlock();
return value;
}
public void addCount(int value) {
mLock.lock();
count += value;
mLock.unlock();
}
ImageReleaseProtocol(boolean block, boolean close) {
blockUntilRelease = block;
closeOnRelease = close;
count = 0;
mSignal = mLock.newCondition();
}
public void block() throws InterruptedException {
mLock.lock();
try {
while (count != 0) {
// Spin to deal with spurious signals.
mSignal.await();
}
mLock.unlock();
} catch (InterruptedException e) {
// TODO: on interruption, figure out what to do.
throw (e);
}
}
public void signal() {
mLock.lock();
mSignal.signal();
mLock.unlock();
}
}
// Thread factories for a default constructor
private class FastThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setPriority(FAST_THREAD_PRIORITY);
return t;
}
}
private class SlowThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setPriority(SLOW_THREAD_PRIORITY);
return t;
}
}
// TODO: Remove with a better implementation. Just to avoid
// the GC not getting rid of elements. Should be hooked up to properly
// implemented memory pool.
public class SimpleCache extends ArrayList<byte[]> {
public SimpleCache(int numEntries) {
super(numEntries);
}
public synchronized void cacheSave(byte[] mem) {
add(mem);
}
public synchronized byte[] cacheGet() {
if (size() < 1) {
return null;
} else {
return mSimpleCache.remove(0);
}
}
}
}