Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a9db0fae authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Delay SharedPreferences.apply() by 50 ms"

parents d047116f 33496448
Loading
Loading
Loading
Loading
+160 −49
Original line number Diff line number Diff line
@@ -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}.
 *
 * 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.
 * 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;

    /** Lock for this class */
    private static final Object sLock = new Object();

    // 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>();
    /** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
    @GuardedBy("sLock")
    private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

    private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class
    /** {@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 {@link #waitToFinish}.
     *
     * 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.
     * @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);
            }
        }
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -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) {
+3 −3
Original line number Diff line number Diff line
@@ -375,16 +375,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);