diff options
author | Sam Blitzstein <sblitz@google.com> | 2013-10-09 14:11:27 -0700 |
---|---|---|
committer | Sam Blitzstein <sblitz@google.com> | 2013-10-15 17:34:58 -0700 |
commit | 93a35b93dc582e38ff8ee5979754a16b4bf4da0c (patch) | |
tree | 9034ab3155e8781b0cd77fb70882f911080f6f89 /src/com/android/bitmap/ContiguousFIFOAggregator.java | |
parent | ce2b0fdc1e9c9d083faab75b6bdfbea27bf574e2 (diff) | |
download | bitmap-93a35b93dc582e38ff8ee5979754a16b4bf4da0c.tar.gz |
Initial commit from Gmail's Cache system.
Change-Id: I14168ab3bc02b77399a1812f62bd77ac797232c5
Diffstat (limited to 'src/com/android/bitmap/ContiguousFIFOAggregator.java')
-rw-r--r-- | src/com/android/bitmap/ContiguousFIFOAggregator.java | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/src/com/android/bitmap/ContiguousFIFOAggregator.java b/src/com/android/bitmap/ContiguousFIFOAggregator.java new file mode 100644 index 0000000..9716fb8 --- /dev/null +++ b/src/com/android/bitmap/ContiguousFIFOAggregator.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2013 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.bitmap; + +import android.util.Log; +import android.util.SparseArray; + +import com.android.bitmap.util.Trace; + +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Queue; + +/** + * An implementation of a task aggregator that executes tasks in the order that they are expected + * . All tasks that is given to {@link #execute(Object, Runnable)} must correspond to a key. This + * key must have been previously declared with {@link #expect(Object, Callback)}. + * The task will be scheduled to run when its corresponding key becomes the first expected key + * amongst the other keys in this aggregator. + * <p/> + * If on {@link #execute(Object, Runnable)} the key is not expected, the task will be executed + * immediately as an edge case. + * <p/> + * A characteristic scenario is as follows: + * <ol> + * <li>Expect keys <b>A</b>, <b>B</b>, <b>C</b>, and <b>Z</b>, in that order. Key <b>A</b> is now + * the first expected key.</li> + * <li>Execute task <b>2</b> for key <b>B</b>. The first expected key is <b>A</b>, + * which has no task associated with it, so we store task <b>2</b>. </li> + * <li>Execute task <b>4</b> for key <b>Z</b>. We store task <b>4</b>. </li> + * <li>Execute task <b>1</b> for key <b>A</b>. We run task <b>1</b> because its key <b>A</b> is + * the first expected, then we remove key <b>A</b> from the list of keys that we expect.</li> + * <li>This causes key <b>B</b> to be the first expected key, and we see that we have previously + * stored task <b>2</b> for that key. We run task <b>2</b> and remove key <b>B</b>. </li> + * <li>Key <b>C</b> is now the first expected key, but it has no task, + * so nothing happens. Task <b>4</b>, which was previously stored, + * cannot run until its corresponding key <b>Z</b> becomes the first expected key. This can + * happen if a task comes in for key <b>C</b> or if forget is called on key <b>C</b>.</li> + * </ol> + * <p/> + * ContiguousFIFOAggregator is not thread safe. + */ +public class ContiguousFIFOAggregator<T> { + private final Queue<T> mExpected; + private final SparseArray<Value> mTasks; + + private static final String TAG = ContiguousFIFOAggregator.class.getSimpleName(); + private static final boolean DEBUG = false; + + /** + * Create a new ContiguousFIFOAggregator. + * <p/> + * The nature of the prioritization means that the number of stored keys and tasks may grow + * unbounded. This may happen, for instance, if the top priority key is never given a task to + * {@link #execute(Object, Runnable)}. However, in practice, if you are generating tasks in + * response to UI elements appearing on the screen, you will only have a bounded set of keys. + * UI elements that scroll off the screen will call {@link #forget(Object)} while new elements + * will call {@link #expect(Object, Callback)}. This means that the expected + * number of keys and tasks is + * the maximum number of UI elements that you expect to show on the screen at any time. + */ + public ContiguousFIFOAggregator() { + mExpected = new ArrayDeque<T>(); + mTasks = new SparseArray<Value>(); + } + + /** + * Declare a key to be expected in the future. The order in which you expect keys is very + * important. Keys that are declared first are guaranteed to have their tasks run first. You + * must call either {@link #forget(Object)} or {@link #execute(Object, Runnable)} + * with this key in the future, or you will leak the key. + * + * If you call expect with a previously expected key, the key will be placed at the back of + * the expected queue and its callback will be replaced with this one. + * + * @param key the key to expect a task for. Use the same key when setting the task later + * with {@link #execute (Object, Runnable)}. + * @param callback the callback to notify when the key becomes the first expected key, or null. + */ + public void expect(final T key, final Callback<T> callback) { + if (key == null) { + throw new IllegalArgumentException("Do not use null keys."); + } + + Trace.beginSection("pool expect"); + final int hash = key.hashCode(); + if (contains(key)) { + mExpected.remove(key); + mTasks.remove(hash); + } + final boolean isFirst = mExpected.isEmpty(); + mExpected.offer(key); + mTasks.put(hash, new Value(callback, null)); + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator >> tasks: %s", prettyPrint())); + } + + if (isFirst) { + onFirstExpectedChanged(key); + } + Trace.endSection(); + } + + /** + * Remove a previously declared key that we no longer expect to execute a task for. This + * potentially allows another key to now become the first expected key, + * and so this may trigger one or more tasks to be executed. + * + * @param key the key previously declared in {@link #expect(Object, Callback)}. + * + */ + public void forget(final T key) { + if (key == null) { + throw new IllegalArgumentException("Do not use null keys."); + } + + if (!contains(key)) { + return; + } + + Trace.beginSection("pool forget"); + final boolean removedFirst = key.equals(mExpected.peek()); + mExpected.remove(key); + mTasks.delete(key.hashCode()); + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator < tasks: %s", prettyPrint())); + } + + final T second; + if (removedFirst && (second = mExpected.peek()) != null) { + // We removed the first key. The second key is now first. + onFirstExpectedChanged(second); + } + + maybeExecuteNow(); + Trace.endSection(); + } + + /** + * Attempt to execute a task corresponding to a previously declared key. If the key is the + * first expected key, the task will be executed immediately. Otherwise, the task is stored + * until its key becomes the first expected key. Execution of a task results in the removal + * of that key, which potentially allows another key to now become the first expected key, + * and may cause one or more other tasks to be executed. + * <p/> + * If the key is not expected, the task will be executed immediately as an edge case. + * + * @param key the key previously declared in {@link #expect(Object, Callback)}. + * @param task the task to execute or store, depending on its corresponding key. + */ + public void execute(final T key, final Runnable task) { + Trace.beginSection("pool execute"); + final int hash = key.hashCode(); + final Value value = mTasks.get(hash); + if (value == null || task == null) { + if (task != null) { + task.run(); + } + Trace.endSection(); + return; + } + value.task = task; + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator ++ tasks: %s", prettyPrint())); + } + maybeExecuteNow(); + Trace.endSection(); + } + + /** + * Triggered by {@link #execute(Object, Runnable)} and {@link #forget(Object)}. The keys or + * tasks have changed, which may cause one or more tasks to be executed. This method will + * continue to execute tasks associated with the first expected key to the last expected key, + * stopping when it finds that the first expected key has not yet been assigned a task. + */ + private void maybeExecuteNow() { + T first; + int count = 0; + while (!mExpected.isEmpty()) { + Trace.beginSection("pool maybeExecuteNow loop"); + first = mExpected.peek(); + if (count > 0) { + // When count == 0, the key is already first. + onFirstExpectedChanged(first); + } + + final int hash = first.hashCode(); + final Value value = mTasks.get(hash); + if (value.task == null) { + Trace.endSection(); + break; + } + + mExpected.poll(); + mTasks.delete(hash); + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator - tasks: %s", prettyPrint())); + } + value.task.run(); + count++; + Trace.endSection(); + } + } + + /** + * This method should only be called once for any key. + * @param key The key that has become the new first expected key. + */ + private void onFirstExpectedChanged(final T key) { + final int hash = key.hashCode(); + final Value value = mTasks.get(hash); + if (value == null) { + return; + } + final Callback<T> callback = value.callback; + if (callback == null) { + return; + } + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator first: %d", hash)); + } + callback.onBecomeFirstExpected(key); + } + + private boolean contains(final T key) { + return mTasks.get(key.hashCode()) != null; + } + + /** + * Get a pretty string representing the internal data. + * @return a String for the internal data. + */ + private String prettyPrint() { + if (mExpected.isEmpty()) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mExpected.size() * 28); + buffer.append('{'); + Iterator<T> it = mExpected.iterator(); + while (it.hasNext()) { + final T key = it.next(); + final int hash = key.hashCode(); + buffer.append(hash); + buffer.append('='); + final Value value = mTasks.get(hash); + buffer.append(value); + if (it.hasNext()) { + buffer.append(", "); + } + } + buffer.append('}'); + if (mExpected.size() != mTasks.size()) { + buffer.append(" error"); + } + return buffer.toString(); + } + + /** + * Implement this interface if you want to be notified when the key becomes the first + * expected key. + * @param <T> The type of the key used for the aggregator. + */ + public interface Callback<T> { + + /** + * The key you declared as expected has become the first expected key in this aggregator. + * + * We don't need a noLongerFirstExpected() method because this aggregator strictly adds + * additional to the end of the queue. For a first expected key to no longer be the first + * expected, it would either have to be forgotten with {@link #forget(Object)} or a task + * assigned and executed with {@link #execute(Object, Runnable)}. + * + * @param key The key that became first. We provide the key so the callback can either not + * keep state, or it can keep state which may have changed so the callback can do + * a comparison. + */ + void onBecomeFirstExpected(final T key); + } + + /** + * Holds the callback and task for when a key later becomes the first expected key. + */ + private class Value { + + final Callback<T> callback; + Runnable task; + + Value(final Callback<T> callback, final Runnable task) { + this.callback = callback; + this.task = task; + } + + @Override + public String toString() { + return String.valueOf(task); + } + } +} |