[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 3349644872)
This commit is contained in:
Philip P. Moltmann
2016-12-12 16:44:07 -08:00
committed by Svetoslav Ganov
parent 66d545bfe9
commit 2fc44947dd
3 changed files with 169 additions and 58 deletions

View File

@@ -16,86 +16,197 @@
package android.app; package android.app;
import java.util.concurrent.ConcurrentLinkedQueue; import android.os.Handler;
import java.util.concurrent.ExecutorService; import android.os.HandlerThread;
import java.util.concurrent.Executors; 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 * Internal utility class to keep track of process-global work that's outstanding and hasn't been
* outstanding and hasn't been finished yet. * finished yet.
* *
* This was created for writing SharedPreference edits out * New work will be {@link #queue queued}.
* asynchronously so we'd have a mechanism to wait for the writes in *
* Activity.onPause and similar places, but we may use this mechanism * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
* for other things in the future. * 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 * @hide
*/ */
public class QueuedWork { public class QueuedWork {
private static final String LOG_TAG = QueuedWork.class.getSimpleName();
// The set of Runnables that will finish or wait on any async /** Delay for delayed runnables */
// activities started by the application. private static final long DELAY = 50;
private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
new ConcurrentLinkedQueue<Runnable>();
private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class /** Lock for this class */
private static final Object sLock = new Object();
/** 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, * Lazily create a handler on a separate thread.
* creating it if necessary. *
* @return the handler
*/ */
public static ExecutorService singleThreadExecutor() { private static Handler getHandler() {
synchronized (QueuedWork.class) { synchronized (sLock) {
if (sSingleThreadExecutor == null) { if (sHandler == null) {
// TODO: can we give this single thread a thread name? HandlerThread handlerThread = new HandlerThread("queued-work-looper",
sSingleThreadExecutor = Executors.newSingleThreadExecutor(); 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 * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
* started in this context earlier. Typically finished by e.g.
* an Activity#onPause. Used by SharedPreferences$Editor#startCommit().
* *
* Note that this doesn't actually start it running. This is just * Used by SharedPreferences$Editor#startCommit().
* a scratch set for callers doing async work to keep updated with *
* what's in-flight. In the common case, caller code * Note that this doesn't actually start it running. This is just a scratch set for callers
* (e.g. SharedPreferences) will pretty quickly call remove() * doing async work to keep updated with what's in-flight. In the common case, caller code
* after an add(). The only time these Runnables are run is from * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
* waitToFinish(), below. * these Runnables are run is from {@link #waitToFinish}.
*
* @param finisher The runnable to add as finisher
*/ */
public static void add(Runnable finisher) { public static void addFinisher(Runnable finisher) {
sPendingWorkFinishers.add(finisher); synchronized (sLock) {
} sFinishers.add(finisher);
}
public static void remove(Runnable finisher) {
sPendingWorkFinishers.remove(finisher);
} }
/** /**
* Finishes or waits for async operations to complete. * Remove a previously {@link #addFinisher added} finisher-runnable.
* (e.g. SharedPreferences$Editor#startCommit writes)
* *
* Is called from the Activity base class's onPause(), after * @param finisher The runnable to remove.
* BroadcastReceiver's onReceive, after Service command handling, */
* etc. (so async work is never lost) public static void removeFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.remove(finisher);
}
}
/**
* 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)
*/ */
public static void waitToFinish() { public static void waitToFinish() {
Runnable toFinish; Handler handler = getHandler();
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run(); 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;
} }
} }
/** /**
* Returns true if there is pending work to be done. Note that the * Queue a work-runnable for processing asynchronously.
* result is out of data as soon as you receive it, so be careful how you *
* use it. * @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);
}
}
}
/**
* @return True iff there is any {@link #queue async work queued}.
*/ */
public static boolean hasPendingWork() { 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);
}
}
} }
} }

View File

@@ -379,12 +379,12 @@ final class SharedPreferencesImpl implements SharedPreferences {
} }
}; };
QueuedWork.add(awaitCommit); QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() { Runnable postWriteRunnable = new Runnable() {
public void run() { public void run() {
awaitCommit.run(); awaitCommit.run();
QueuedWork.remove(awaitCommit); QueuedWork.removeFinisher(awaitCommit);
} }
}; };
@@ -557,10 +557,10 @@ final class SharedPreferencesImpl implements SharedPreferences {
} }
if (DEBUG) { 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) { private static FileOutputStream createFileOutputStream(File file) {

View File

@@ -211,16 +211,16 @@ public abstract class BroadcastReceiver {
// of the list to finish the broadcast, so we don't block this // of the list to finish the broadcast, so we don't block this
// thread (which may be the main thread) to have it finished. // 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 // runnable, since we know the AM is waiting for us until the
// executor gets to it. // executor gets to it.
QueuedWork.singleThreadExecutor().execute( new Runnable() { QueuedWork.queue(new Runnable() {
@Override public void run() { @Override public void run() {
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG, if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing broadcast after work to component " + mToken); "Finishing broadcast after work to component " + mToken);
sendFinished(mgr); sendFinished(mgr);
} }
}); }, false);
} else { } else {
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG, if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing broadcast to component " + mToken); "Finishing broadcast to component " + mToken);