diff options
| author | 2016-12-12 16:44:07 -0800 | |
|---|---|---|
| committer | 2017-01-31 02:50:24 +0000 | |
| commit | 2fc44947dd4f45a23c56985d4c12f01332027a9f (patch) | |
| tree | 1924359350fc856df27e7a6f93ae31c0eb531238 | |
| parent | 66d545bfe9cbe07c81fd03a8806a1e300b508c06 (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.java | 209 | ||||
| -rw-r--r-- | core/java/android/app/SharedPreferencesImpl.java | 8 | ||||
| -rw-r--r-- | core/java/android/content/BroadcastReceiver.java | 6 |
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); |