summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Philip P. Moltmann <moltmann@google.com> 2016-12-12 16:44:07 -0800
committer Svetoslav Ganov <svetoslavganov@google.com> 2017-01-31 02:50:24 +0000
commit2fc44947dd4f45a23c56985d4c12f01332027a9f (patch)
tree1924359350fc856df27e7a6f93ae31c0eb531238
parent66d545bfe9cbe07c81fd03a8806a1e300b508c06 (diff)
[DO NOT MERGE] Delay SharedPreferences.apply() by 50 ms
... so that multiple applies can be combined into a single write. Do do this I replaces the executor in the QueuedWork by a more Android-y handler. Test: Ran shared preferences CTS tests. Looked at log and saw a lot of skipped writes Bug: 33385963 Bug: 30662828 Change-Id: I8f33df717be7091532930ccf6ca8c48940e4edd4 (cherry picked from commit 3349644872d9b769f4e2d0ec1b91d368a552ff8d)
-rw-r--r--core/java/android/app/QueuedWork.java209
-rw-r--r--core/java/android/app/SharedPreferencesImpl.java8
-rw-r--r--core/java/android/content/BroadcastReceiver.java6
3 files changed, 167 insertions, 56 deletions
diff --git a/core/java/android/app/QueuedWork.java b/core/java/android/app/QueuedWork.java
index 6ee478059171..0ae85056da56 100644
--- a/core/java/android/app/QueuedWork.java
+++ b/core/java/android/app/QueuedWork.java
@@ -16,86 +16,197 @@
package android.app;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.LinkedList;
/**
- * Internal utility class to keep track of process-global work that's
- * outstanding and hasn't been finished yet.
+ * Internal utility class to keep track of process-global work that's outstanding and hasn't been
+ * finished yet.
+ *
+ * New work will be {@link #queue queued}.
*
- * This was created for writing SharedPreference edits out
- * asynchronously so we'd have a mechanism to wait for the writes in
- * Activity.onPause and similar places, but we may use this mechanism
- * for other things in the future.
+ * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
+ * This is used to make sure the work has been finished.
+ *
+ * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
+ * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
+ * other things in the future.
+ *
+ * The queued asynchronous work is performed on a separate, dedicated thread.
*
* @hide
*/
public class QueuedWork {
+ private static final String LOG_TAG = QueuedWork.class.getSimpleName();
+
+ /** Delay for delayed runnables */
+ private static final long DELAY = 50;
- // The set of Runnables that will finish or wait on any async
- // activities started by the application.
- private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
- new ConcurrentLinkedQueue<Runnable>();
+ /** Lock for this class */
+ private static final Object sLock = new Object();
- private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class
+ /** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
+ @GuardedBy("sLock")
+ private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
+
+ /** {@link #getHandler() Lazily} created handler */
+ @GuardedBy("sLock")
+ private static Handler sHandler = null;
+
+ /** Work queued via {@link #queue} */
+ @GuardedBy("sLock")
+ private static final LinkedList<Runnable> sWork = new LinkedList<>();
+
+ /** If new work can be delayed or not */
+ @GuardedBy("sLock")
+ private static boolean sCanDelay = true;
/**
- * Returns a single-thread Executor shared by the entire process,
- * creating it if necessary.
+ * Lazily create a handler on a separate thread.
+ *
+ * @return the handler
*/
- public static ExecutorService singleThreadExecutor() {
- synchronized (QueuedWork.class) {
- if (sSingleThreadExecutor == null) {
- // TODO: can we give this single thread a thread name?
- sSingleThreadExecutor = Executors.newSingleThreadExecutor();
+ private static Handler getHandler() {
+ synchronized (sLock) {
+ if (sHandler == null) {
+ HandlerThread handlerThread = new HandlerThread("queued-work-looper",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ handlerThread.start();
+
+ sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
- return sSingleThreadExecutor;
+ return sHandler;
}
}
/**
- * Add a runnable to finish (or wait for) a deferred operation
- * started in this context earlier. Typically finished by e.g.
- * an Activity#onPause. Used by SharedPreferences$Editor#startCommit().
+ * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
+ *
+ * Used by SharedPreferences$Editor#startCommit().
*
- * Note that this doesn't actually start it running. This is just
- * a scratch set for callers doing async work to keep updated with
- * what's in-flight. In the common case, caller code
- * (e.g. SharedPreferences) will pretty quickly call remove()
- * after an add(). The only time these Runnables are run is from
- * waitToFinish(), below.
+ * Note that this doesn't actually start it running. This is just a scratch set for callers
+ * doing async work to keep updated with what's in-flight. In the common case, caller code
+ * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
+ * these Runnables are run is from {@link #waitToFinish}.
+ *
+ * @param finisher The runnable to add as finisher
*/
- public static void add(Runnable finisher) {
- sPendingWorkFinishers.add(finisher);
+ public static void addFinisher(Runnable finisher) {
+ synchronized (sLock) {
+ sFinishers.add(finisher);
+ }
}
- public static void remove(Runnable finisher) {
- sPendingWorkFinishers.remove(finisher);
+ /**
+ * Remove a previously {@link #addFinisher added} finisher-runnable.
+ *
+ * @param finisher The runnable to remove.
+ */
+ public static void removeFinisher(Runnable finisher) {
+ synchronized (sLock) {
+ sFinishers.remove(finisher);
+ }
}
/**
- * Finishes or waits for async operations to complete.
- * (e.g. SharedPreferences$Editor#startCommit writes)
+ * Trigger queued work to be processed immediately. The queued work is processed on a separate
+ * thread asynchronous. While doing that run and process all finishers on this thread. The
+ * finishers can be implemented in a way to check weather the queued work is finished.
*
- * Is called from the Activity base class's onPause(), after
- * BroadcastReceiver's onReceive, after Service command handling,
- * etc. (so async work is never lost)
+ * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
+ * after Service command handling, etc. (so async work is never lost)
*/
public static void waitToFinish() {
- Runnable toFinish;
- while ((toFinish = sPendingWorkFinishers.poll()) != null) {
- toFinish.run();
+ Handler handler = getHandler();
+
+ synchronized (sLock) {
+ if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
+ // Force the delayed work to be processed now
+ handler.removeMessages(QueuedWorkHandler.MSG_RUN);
+ handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
+ }
+
+ // We should not delay any work as this might delay the finishers
+ sCanDelay = false;
+ }
+
+ try {
+ while (true) {
+ Runnable finisher;
+
+ synchronized (sLock) {
+ finisher = sFinishers.poll();
+ }
+
+ if (finisher == null) {
+ break;
+ }
+
+ finisher.run();
+ }
+ } finally {
+ sCanDelay = true;
+ }
+ }
+
+ /**
+ * Queue a work-runnable for processing asynchronously.
+ *
+ * @param work The new runnable to process
+ * @param shouldDelay If the message should be delayed
+ */
+ public static void queue(Runnable work, boolean shouldDelay) {
+ Handler handler = getHandler();
+
+ synchronized (sLock) {
+ sWork.add(work);
+
+ if (shouldDelay && sCanDelay) {
+ handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
+ } else {
+ handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
+ }
}
}
-
+
/**
- * Returns true if there is pending work to be done. Note that the
- * result is out of data as soon as you receive it, so be careful how you
- * use it.
+ * @return True iff there is any {@link #queue async work queued}.
*/
public static boolean hasPendingWork() {
- return !sPendingWorkFinishers.isEmpty();
+ synchronized (sLock) {
+ return !sWork.isEmpty();
+ }
+ }
+
+ private static class QueuedWorkHandler extends Handler {
+ static final int MSG_RUN = 1;
+
+ QueuedWorkHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_RUN) {
+ LinkedList<Runnable> work;
+
+ synchronized (sWork) {
+ work = (LinkedList<Runnable>) sWork.clone();
+ sWork.clear();
+
+ // Remove all msg-s as all work will be processed now
+ removeMessages(MSG_RUN);
+ }
+
+ work.forEach(Runnable::run);
+ }
+ }
}
-
}
diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java
index f273cd8670f0..59434331b36a 100644
--- a/core/java/android/app/SharedPreferencesImpl.java
+++ b/core/java/android/app/SharedPreferencesImpl.java
@@ -379,12 +379,12 @@ final class SharedPreferencesImpl implements SharedPreferences {
}
};
- QueuedWork.add(awaitCommit);
+ QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
- QueuedWork.remove(awaitCommit);
+ QueuedWork.removeFinisher(awaitCommit);
}
};
@@ -557,10 +557,10 @@ final class SharedPreferencesImpl implements SharedPreferences {
}
if (DEBUG) {
- Log.d(TAG, "added " + mcr.memoryStateGeneration + " -> " + mFile.getName());
+ Log.d(TAG, "queued " + mcr.memoryStateGeneration + " -> " + mFile.getName());
}
- QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
+ QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
private static FileOutputStream createFileOutputStream(File file) {
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
index a7a86158edee..68c25ef5bd06 100644
--- a/core/java/android/content/BroadcastReceiver.java
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -211,16 +211,16 @@ public abstract class BroadcastReceiver {
// of the list to finish the broadcast, so we don't block this
// thread (which may be the main thread) to have it finished.
//
- // Note that we don't need to use QueuedWork.add() with the
+ // Note that we don't need to use QueuedWork.addFinisher() with the
// runnable, since we know the AM is waiting for us until the
// executor gets to it.
- QueuedWork.singleThreadExecutor().execute( new Runnable() {
+ QueuedWork.queue(new Runnable() {
@Override public void run() {
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing broadcast after work to component " + mToken);
sendFinished(mgr);
}
- });
+ }, false);
} else {
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing broadcast to component " + mToken);