[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:
committed by
Svetoslav Ganov
parent
66d545bfe9
commit
2fc44947dd
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user