OSDN Git Service

Migrate AppCompaction settings to DeviceConfig APIs.
authorBen Murdoch <benm@google.com>
Wed, 16 Jan 2019 10:05:58 +0000 (10:05 +0000)
committerBen Murdoch <benm@google.com>
Thu, 24 Jan 2019 14:01:06 +0000 (14:01 +0000)
To support app compaction P/H experiments, migrate it's
settings away from using the key/value pair string the
rest of ActivityManagerConstants uses, and use the
DeviceConfig APIs instead.

Test: boots, works, atest FrameworksServicesTests:AppCompactorTest

Bug: 122879579

Change-Id: I9d1ae17d9671d0443b728ecdeca7102ba34d9c2f

api/system-current.txt
core/java/android/provider/DeviceConfig.java
services/core/java/com/android/server/am/ActivityManagerConstants.java
services/core/java/com/android/server/am/ActivityManagerService.java
services/core/java/com/android/server/am/AppCompactor.java
services/core/java/com/android/server/am/OomAdjuster.java
services/tests/servicestests/src/com/android/server/am/AppCompactorTest.java [new file with mode: 0644]

index 315a941..7e78ee9 100644 (file)
@@ -5626,6 +5626,18 @@ package android.provider {
     field public static final String NAMESPACE_NOTIFICATION_ASSISTANT = "notification_assistant";
   }
 
+  public static interface DeviceConfig.ActivityManager {
+    field public static final String KEY_COMPACT_ACTION_1 = "compact_action_1";
+    field public static final String KEY_COMPACT_ACTION_2 = "compact_action_2";
+    field public static final String KEY_COMPACT_THROTTLE_1 = "compact_throttle_1";
+    field public static final String KEY_COMPACT_THROTTLE_2 = "compact_throttle_2";
+    field public static final String KEY_COMPACT_THROTTLE_3 = "compact_throttle_3";
+    field public static final String KEY_COMPACT_THROTTLE_4 = "compact_throttle_4";
+    field public static final String KEY_MAX_CACHED_PROCESSES = "max_cached_processes";
+    field public static final String KEY_USE_COMPACTION = "use_compaction";
+    field public static final String NAMESPACE = "activity_manager";
+  }
+
   public static interface DeviceConfig.FsiBoot {
     field public static final String NAMESPACE = "fsi_boot";
     field public static final String OOB_ENABLED = "oob_enabled";
index 6ccd296..cd823a9 100644 (file)
@@ -153,6 +153,34 @@ public final class DeviceConfig {
         String OOB_WHITELIST = "oob_whitelist";
     }
 
+    /**
+     * Namespace for activity manager related features. These features will be applied
+     * immediately upon change.
+     *
+     * @hide
+     */
+    @SystemApi
+    public interface ActivityManager {
+        String NAMESPACE = "activity_manager";
+
+        /**
+         * App compaction flags. See {@link com.android.server.am.AppCompactor}.
+         */
+        String KEY_USE_COMPACTION = "use_compaction";
+        String KEY_COMPACT_ACTION_1 = "compact_action_1";
+        String KEY_COMPACT_ACTION_2 = "compact_action_2";
+        String KEY_COMPACT_THROTTLE_1 = "compact_throttle_1";
+        String KEY_COMPACT_THROTTLE_2 = "compact_throttle_2";
+        String KEY_COMPACT_THROTTLE_3 = "compact_throttle_3";
+        String KEY_COMPACT_THROTTLE_4 = "compact_throttle_4";
+
+        /**
+         * Maximum number of cached processes. See
+         * {@link com.android.server.am.ActivityManagerConstants}.
+         */
+        String KEY_MAX_CACHED_PROCESSES = "max_cached_processes";
+    }
+
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
     private static Map<OnPropertyChangedListener, Pair<String, Executor>> sListeners =
index dd2b33a..d025d73 100644 (file)
 
 package com.android.server.am;
 
+import static android.provider.DeviceConfig.ActivityManager.KEY_MAX_CACHED_PROCESSES;
+
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_POWER_QUICK;
 
+import android.app.ActivityThread;
 import android.content.ContentResolver;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.OnPropertyChangedListener;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.KeyValueListParser;
 import android.util.Slog;
 
@@ -34,7 +40,6 @@ import java.io.PrintWriter;
 final class ActivityManagerConstants extends ContentObserver {
 
     // Key names stored in the settings value.
-    private static final String KEY_MAX_CACHED_PROCESSES = "max_cached_processes";
     private static final String KEY_BACKGROUND_SETTLE_TIME = "background_settle_time";
     private static final String KEY_FGSERVICE_MIN_SHOWN_TIME
             = "fgservice_min_shown_time";
@@ -69,13 +74,6 @@ final class ActivityManagerConstants extends ContentObserver {
     static final String KEY_PROCESS_START_ASYNC = "process_start_async";
     static final String KEY_MEMORY_INFO_THROTTLE_TIME = "memory_info_throttle_time";
     static final String KEY_TOP_TO_FGS_GRACE_DURATION = "top_to_fgs_grace_duration";
-    static final String KEY_USE_COMPACTION = "use_compaction";
-    static final String KEY_COMPACT_ACTION_1 = "compact_action_1";
-    static final String KEY_COMPACT_ACTION_2 = "compact_action_2";
-    static final String KEY_COMPACT_THROTTLE_1 = "compact_throttle_1";
-    static final String KEY_COMPACT_THROTTLE_2 = "compact_throttle_2";
-    static final String KEY_COMPACT_THROTTLE_3 = "compact_throttle_3";
-    static final String KEY_COMPACT_THROTTLE_4 = "compact_throttle_4";
 
     private static final int DEFAULT_MAX_CACHED_PROCESSES = 32;
     private static final long DEFAULT_BACKGROUND_SETTLE_TIME = 60*1000;
@@ -106,13 +104,6 @@ final class ActivityManagerConstants extends ContentObserver {
     private static final boolean DEFAULT_PROCESS_START_ASYNC = true;
     private static final long DEFAULT_MEMORY_INFO_THROTTLE_TIME = 5*60*1000;
     private static final long DEFAULT_TOP_TO_FGS_GRACE_DURATION = 15 * 1000;
-    private static final boolean DEFAULT_USE_COMPACTION = false;
-    public static final int DEFAULT_COMPACT_ACTION_1 = 1;
-    public static final int DEFAULT_COMPACT_ACTION_2 = 3;
-    public static final long DEFAULT_COMPACT_THROTTLE_1 = 5000;
-    public static final long DEFAULT_COMPACT_THROTTLE_2 = 10000;
-    public static final long DEFAULT_COMPACT_THROTTLE_3 = 500;
-    public static final long DEFAULT_COMPACT_THROTTLE_4 = 10000;
 
     // Maximum number of cached processes we will allow.
     public int MAX_CACHED_PROCESSES = DEFAULT_MAX_CACHED_PROCESSES;
@@ -232,23 +223,6 @@ final class ActivityManagerConstants extends ContentObserver {
     // this long.
     public long TOP_TO_FGS_GRACE_DURATION = DEFAULT_TOP_TO_FGS_GRACE_DURATION;
 
-    // Use compaction for background apps.
-    public boolean USE_COMPACTION = DEFAULT_USE_COMPACTION;
-
-    // Action for compactAppSome.
-    public int COMPACT_ACTION_1 = DEFAULT_COMPACT_ACTION_1;
-    // Action for compactAppFull;
-    public int COMPACT_ACTION_2 = DEFAULT_COMPACT_ACTION_2;
-
-    // How long we'll skip second compactAppSome after first compactAppSome
-    public long COMPACT_THROTTLE_1 = DEFAULT_COMPACT_THROTTLE_1;
-    // How long we'll skip compactAppSome after compactAppFull
-    public long COMPACT_THROTTLE_2 = DEFAULT_COMPACT_THROTTLE_2;
-    // How long we'll skip compactAppFull after compactAppSome
-    public long COMPACT_THROTTLE_3 = DEFAULT_COMPACT_THROTTLE_3;
-    // How long we'll skip second compactAppFull after first compactAppFull
-    public long COMPACT_THROTTLE_4 = DEFAULT_COMPACT_THROTTLE_4;
-
     // Indicates whether the activity starts logging is enabled.
     // Controlled by Settings.Global.ACTIVITY_STARTS_LOGGING_ENABLED
     volatile boolean mFlagActivityStartsLoggingEnabled;
@@ -295,10 +269,19 @@ final class ActivityManagerConstants extends ContentObserver {
                 Settings.Global.getUriFor(
                         Settings.Global.BACKGROUND_ACTIVITY_STARTS_ENABLED);
 
+    private final OnPropertyChangedListener mOnDeviceConfigChangedListener =
+            new OnPropertyChangedListener() {
+                @Override
+                public void onPropertyChanged(String namespace, String name, String value) {
+                    if (KEY_MAX_CACHED_PROCESSES.equals(name)) {
+                        updateMaxCachedProcesses();
+                    }
+                }
+            };
+
     public ActivityManagerConstants(ActivityManagerService service, Handler handler) {
         super(handler);
         mService = service;
-        updateMaxCachedProcesses();
     }
 
     public void start(ContentResolver resolver) {
@@ -309,6 +292,11 @@ final class ActivityManagerConstants extends ContentObserver {
         updateConstants();
         updateActivityStartsLoggingEnabled();
         updateBackgroundActivityStartsEnabled();
+        DeviceConfig.addOnPropertyChangedListener(DeviceConfig.ActivityManager.NAMESPACE,
+                ActivityThread.currentApplication().getMainExecutor(),
+                mOnDeviceConfigChangedListener);
+        updateMaxCachedProcesses();
+
     }
 
     public void setOverrideMaxCachedProcesses(int value) {
@@ -347,8 +335,6 @@ final class ActivityManagerConstants extends ContentObserver {
                 // with defaults.
                 Slog.e("ActivityManagerConstants", "Bad activity manager config settings", e);
             }
-            MAX_CACHED_PROCESSES = mParser.getInt(KEY_MAX_CACHED_PROCESSES,
-                    DEFAULT_MAX_CACHED_PROCESSES);
             BACKGROUND_SETTLE_TIME = mParser.getLong(KEY_BACKGROUND_SETTLE_TIME,
                     DEFAULT_BACKGROUND_SETTLE_TIME);
             FGSERVICE_MIN_SHOWN_TIME = mParser.getLong(KEY_FGSERVICE_MIN_SHOWN_TIME,
@@ -406,13 +392,9 @@ final class ActivityManagerConstants extends ContentObserver {
                     DEFAULT_MEMORY_INFO_THROTTLE_TIME);
             TOP_TO_FGS_GRACE_DURATION = mParser.getDurationMillis(KEY_TOP_TO_FGS_GRACE_DURATION,
                     DEFAULT_TOP_TO_FGS_GRACE_DURATION);
-            USE_COMPACTION = mParser.getBoolean(KEY_USE_COMPACTION, DEFAULT_USE_COMPACTION);
-            COMPACT_ACTION_1 = mParser.getInt(KEY_COMPACT_ACTION_1, DEFAULT_COMPACT_ACTION_1);
-            COMPACT_ACTION_2 = mParser.getInt(KEY_COMPACT_ACTION_2, DEFAULT_COMPACT_ACTION_2);
-            COMPACT_THROTTLE_1 = mParser.getLong(KEY_COMPACT_THROTTLE_1, DEFAULT_COMPACT_THROTTLE_1);
-            COMPACT_THROTTLE_2 = mParser.getLong(KEY_COMPACT_THROTTLE_2, DEFAULT_COMPACT_THROTTLE_2);
-            COMPACT_THROTTLE_3 = mParser.getLong(KEY_COMPACT_THROTTLE_3, DEFAULT_COMPACT_THROTTLE_3);
-            COMPACT_THROTTLE_4 = mParser.getLong(KEY_COMPACT_THROTTLE_4, DEFAULT_COMPACT_THROTTLE_4);
+
+            // For new flags that are intended for server-side experiments, please use the new
+            // DeviceConfig package.
 
             updateMaxCachedProcesses();
         }
@@ -429,8 +411,19 @@ final class ActivityManagerConstants extends ContentObserver {
     }
 
     private void updateMaxCachedProcesses() {
-        CUR_MAX_CACHED_PROCESSES = mOverrideMaxCachedProcesses < 0
-                ? MAX_CACHED_PROCESSES : mOverrideMaxCachedProcesses;
+        String maxCachedProcessesFlag = DeviceConfig.getProperty(
+                DeviceConfig.ActivityManager.NAMESPACE, KEY_MAX_CACHED_PROCESSES);
+        try {
+            CUR_MAX_CACHED_PROCESSES = mOverrideMaxCachedProcesses < 0
+                    ? (TextUtils.isEmpty(maxCachedProcessesFlag)
+                    ? DEFAULT_MAX_CACHED_PROCESSES : Integer.parseInt(maxCachedProcessesFlag))
+                    : mOverrideMaxCachedProcesses;
+        } catch (NumberFormatException e) {
+            // Bad flag value from Phenotype, revert to default.
+            Slog.e("ActivityManagerConstants",
+                    "Unable to parse flag for max_cached_processes: " + maxCachedProcessesFlag, e);
+            CUR_MAX_CACHED_PROCESSES = DEFAULT_MAX_CACHED_PROCESSES;
+        }
         CUR_MAX_EMPTY_PROCESSES = computeEmptyProcessLimit(CUR_MAX_CACHED_PROCESSES);
 
         // Note the trim levels do NOT depend on the override process limit, we want
@@ -503,8 +496,6 @@ final class ActivityManagerConstants extends ContentObserver {
         pw.println(MEMORY_INFO_THROTTLE_TIME);
         pw.print("  "); pw.print(KEY_TOP_TO_FGS_GRACE_DURATION); pw.print("=");
         pw.println(TOP_TO_FGS_GRACE_DURATION);
-        pw.print("  "); pw.print(KEY_USE_COMPACTION); pw.print("=");
-        pw.println(USE_COMPACTION);
 
         pw.println();
         if (mOverrideMaxCachedProcesses >= 0) {
index b24290f..c203c79 100644 (file)
@@ -7228,6 +7228,7 @@ public class ActivityManagerService extends IActivityManager.Stub
         mActivityTaskManager.installSystemProviders();
         mDevelopmentSettingsObserver = new DevelopmentSettingsObserver();
         SettingsToPropertiesMapper.start(mContext.getContentResolver());
+        mOomAdjuster.initSettings();
 
         // Now that the settings provider is published we can consider sending
         // in a rescue party.
@@ -9340,6 +9341,7 @@ public class ActivityManagerService extends IActivityManager.Stub
 
         synchronized(this) {
             mConstants.dump(pw);
+            mOomAdjuster.dumpAppCompactorSettings(pw);
             pw.println();
             if (dumpAll) {
                 pw.println("-------------------------------------------------------------------------------");
@@ -9739,6 +9741,7 @@ public class ActivityManagerService extends IActivityManager.Stub
             } else if ("settings".equals(cmd)) {
                 synchronized (this) {
                     mConstants.dump(pw);
+                    mOomAdjuster.dumpAppCompactorSettings(pw);
                 }
             } else if ("services".equals(cmd) || "s".equals(cmd)) {
                 if (dumpClient) {
index fd402cc..bb55ec3 100644 (file)
 
 package com.android.server.am;
 
-import com.android.internal.annotations.GuardedBy;
+import static android.os.Process.THREAD_PRIORITY_FOREGROUND;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_ACTION_1;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_ACTION_2;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_THROTTLE_1;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_THROTTLE_2;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_THROTTLE_3;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_THROTTLE_4;
+import static android.provider.DeviceConfig.ActivityManager.KEY_USE_COMPACTION;
 
 import android.app.ActivityManager;
-
+import android.app.ActivityThread;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.Trace;
-
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.OnPropertyChangedListener;
+import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.StatsLog;
 
-import static android.os.Process.THREAD_PRIORITY_FOREGROUND;
-
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.ServiceThread;
 
 import java.io.FileOutputStream;
-import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 
 public final class AppCompactor {
-    /**
-     * Processes to compact.
-     */
-    final ArrayList<ProcessRecord> mPendingCompactionProcesses = new ArrayList<ProcessRecord>();
 
+    // Phenotype sends int configurations and we map them to the strings we'll use on device,
+    // preventing a weird string value entering the kernel.
+    private static final int COMPACT_ACTION_FILE_FLAG = 1;
+    private static final int COMPACT_ACTION_ANON_FLAG = 2;
+    private static final int COMPACT_ACTION_FULL_FLAG = 3;
+    private static final String COMPACT_ACTION_FILE = "file";
+    private static final String COMPACT_ACTION_ANON = "anon";
+    private static final String COMPACT_ACTION_FULL = "all";
+
+    // Defaults for phenotype flags.
+    @VisibleForTesting static final Boolean DEFAULT_USE_COMPACTION = false;
+    @VisibleForTesting static final int DEFAULT_COMPACT_ACTION_1 = COMPACT_ACTION_FILE_FLAG;
+    @VisibleForTesting static final int DEFAULT_COMPACT_ACTION_2 = COMPACT_ACTION_FULL_FLAG;
+    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_1 = 5_000;
+    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_2 = 10_000;
+    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_3 = 500;
+    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_4 = 10_000;
+
+    @VisibleForTesting
+    interface PropertyChangedCallbackForTest {
+        void onPropertyChanged();
+    }
+    private PropertyChangedCallbackForTest mTestCallback;
+
+    // Handler constants.
     static final int COMPACT_PROCESS_SOME = 1;
     static final int COMPACT_PROCESS_FULL = 2;
     static final int COMPACT_PROCESS_MSG = 1;
@@ -57,70 +86,106 @@ public final class AppCompactor {
      */
     final ServiceThread mCompactionThread;
 
-    final private Handler mCompactionHandler;
-
-    final private ActivityManagerService mAm;
-    final private ActivityManagerConstants mConstants;
-
-    final private String COMPACT_ACTION_FILE = "file";
-    final private String COMPACT_ACTION_ANON = "anon";
-    final private String COMPACT_ACTION_FULL = "all";
-
-    final private String compactActionSome;
-    final private String compactActionFull;
-
-    final private long throttleSomeSome;
-    final private long throttleSomeFull;
-    final private long throttleFullSome;
-    final private long throttleFullFull;
+    private final ArrayList<ProcessRecord> mPendingCompactionProcesses =
+            new ArrayList<ProcessRecord>();
+    private final ActivityManagerService mAm;
+    private final OnPropertyChangedListener mOnFlagsChangedListener =
+            new OnPropertyChangedListener() {
+                @Override
+                public void onPropertyChanged(String namespace, String name, String value) {
+                    synchronized (mPhenotypeFlagLock) {
+                        if (KEY_USE_COMPACTION.equals(name)) {
+                            updateUseCompaction();
+                        } else if (KEY_COMPACT_ACTION_1.equals(name)
+                                || KEY_COMPACT_ACTION_2.equals(name)) {
+                            updateCompactionActions();
+                        } else if (KEY_COMPACT_THROTTLE_1.equals(name)
+                                || KEY_COMPACT_THROTTLE_2.equals(name)
+                                || KEY_COMPACT_THROTTLE_3.equals(name)
+                                || KEY_COMPACT_THROTTLE_4.equals(name)) {
+                            updateCompactionThrottles();
+                        }
+                    }
+                    if (mTestCallback != null) {
+                        mTestCallback.onPropertyChanged();
+                    }
+                }
+            };
+
+    private final Object mPhenotypeFlagLock = new Object();
+
+    // Configured by phenotype. Updates from the server take effect immediately.
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting String mCompactActionSome =
+            compactActionIntToString(DEFAULT_COMPACT_ACTION_1);
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting String mCompactActionFull =
+            compactActionIntToString(DEFAULT_COMPACT_ACTION_2);
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting long mCompactThrottleSomeSome = DEFAULT_COMPACT_THROTTLE_1;
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting long mCompactThrottleSomeFull = DEFAULT_COMPACT_THROTTLE_2;
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting long mCompactThrottleFullSome = DEFAULT_COMPACT_THROTTLE_3;
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting long mCompactThrottleFullFull = DEFAULT_COMPACT_THROTTLE_4;
+    @GuardedBy("mPhenotypeFlagLock")
+    private boolean mUseCompaction = DEFAULT_USE_COMPACTION;
+
+    // Handler on which compaction runs.
+    private Handler mCompactionHandler;
 
     public AppCompactor(ActivityManagerService am) {
         mAm = am;
-        mConstants = am.mConstants;
-
         mCompactionThread = new ServiceThread("CompactionThread",
                 THREAD_PRIORITY_FOREGROUND, true);
-        mCompactionThread.start();
-        mCompactionHandler = new MemCompactionHandler(this);
-
-        switch(mConstants.COMPACT_ACTION_1) {
-            case 1:
-                compactActionSome = COMPACT_ACTION_FILE;
-                break;
-            case 2:
-                compactActionSome = COMPACT_ACTION_ANON;
-                break;
-            case 3:
-                compactActionSome = COMPACT_ACTION_FULL;
-                break;
-            default:
-                compactActionSome = COMPACT_ACTION_FILE;
-                break;
+    }
+
+    @VisibleForTesting
+    AppCompactor(ActivityManagerService am, PropertyChangedCallbackForTest callback) {
+        this(am);
+        mTestCallback = callback;
+    }
+
+    /**
+     * Reads phenotype config to determine whether app compaction is enabled or not and
+     * starts the background thread if necessary.
+     */
+    public void init() {
+        DeviceConfig.addOnPropertyChangedListener(DeviceConfig.ActivityManager.NAMESPACE,
+                ActivityThread.currentApplication().getMainExecutor(), mOnFlagsChangedListener);
+        synchronized (mPhenotypeFlagLock) {
+            updateUseCompaction();
+            updateCompactionActions();
+            updateCompactionThrottles();
         }
+    }
 
-        switch(mConstants.COMPACT_ACTION_2) {
-            case 1:
-                compactActionFull = COMPACT_ACTION_FILE;
-                break;
-            case 2:
-                compactActionFull = COMPACT_ACTION_ANON;
-                break;
-            case 3:
-                compactActionFull = COMPACT_ACTION_FULL;
-                break;
-            default:
-                compactActionFull = COMPACT_ACTION_FULL;
-                break;
+    /**
+     * Returns whether compaction is enabled.
+     */
+    public boolean useCompaction() {
+        synchronized (mPhenotypeFlagLock) {
+            return mUseCompaction;
         }
+    }
 
-        throttleSomeSome = mConstants.COMPACT_THROTTLE_1;
-        throttleSomeFull = mConstants.COMPACT_THROTTLE_2;
-        throttleFullSome = mConstants.COMPACT_THROTTLE_3;
-        throttleFullFull = mConstants.COMPACT_THROTTLE_4;
+    @GuardedBy("mAm")
+    void dump(PrintWriter pw) {
+        pw.println("AppCompactor settings");
+        synchronized (mPhenotypeFlagLock) {
+            pw.println("  " + KEY_USE_COMPACTION + "=" + mUseCompaction);
+            pw.println("  " + KEY_COMPACT_ACTION_1 + "=" + mCompactActionSome);
+            pw.println("  " + KEY_COMPACT_ACTION_2 + "=" + mCompactActionFull);
+            pw.println("  " + KEY_COMPACT_THROTTLE_1 + "=" + mCompactThrottleSomeSome);
+            pw.println("  " + KEY_COMPACT_THROTTLE_2 + "=" + mCompactThrottleSomeFull);
+            pw.println("  " + KEY_COMPACT_THROTTLE_3 + "=" + mCompactThrottleFullSome);
+            pw.println("  " + KEY_COMPACT_THROTTLE_4 + "=" + mCompactThrottleFullFull);
+        }
     }
 
-    // Must be called while holding AMS lock.
-    final void compactAppSome(ProcessRecord app) {
+    @GuardedBy("mAm")
+    void compactAppSome(ProcessRecord app) {
         app.reqCompactAction = COMPACT_PROCESS_SOME;
         mPendingCompactionProcesses.add(app);
         mCompactionHandler.sendMessage(
@@ -128,8 +193,8 @@ public final class AppCompactor {
                 COMPACT_PROCESS_MSG, app.curAdj, app.setProcState));
     }
 
-    // Must be called while holding AMS lock.
-    final void compactAppFull(ProcessRecord app) {
+    @GuardedBy("mAm")
+    void compactAppFull(ProcessRecord app) {
         app.reqCompactAction = COMPACT_PROCESS_FULL;
         mPendingCompactionProcesses.add(app);
         mCompactionHandler.sendMessage(
@@ -137,97 +202,205 @@ public final class AppCompactor {
                 COMPACT_PROCESS_MSG, app.curAdj, app.setProcState));
 
     }
-    final class MemCompactionHandler extends Handler {
-        AppCompactor mAc;
 
-        private MemCompactionHandler(AppCompactor ac) {
-            super(ac.mCompactionThread.getLooper());
-            mAc = ac;
+    /**
+     * Reads the flag value from DeviceConfig to determine whether app compaction
+     * should be enabled, and starts/stops the compaction thread as needed.
+     */
+    @GuardedBy("mPhenotypeFlagLock")
+    private void updateUseCompaction() {
+        String useCompactionFlag =
+                DeviceConfig.getProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                    KEY_USE_COMPACTION);
+        mUseCompaction = TextUtils.isEmpty(useCompactionFlag)
+                ? DEFAULT_USE_COMPACTION : Boolean.parseBoolean(useCompactionFlag);
+        if (mUseCompaction && !mCompactionThread.isAlive()) {
+            mCompactionThread.start();
+            mCompactionHandler = new MemCompactionHandler();
+        }
+    }
+
+    @GuardedBy("mPhenotypeFlagLock")
+    private void updateCompactionActions() {
+        String compactAction1Flag =
+                DeviceConfig.getProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                        KEY_COMPACT_ACTION_1);
+        String compactAction2Flag =
+                DeviceConfig.getProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                        KEY_COMPACT_ACTION_2);
+
+        int compactAction1 = DEFAULT_COMPACT_ACTION_1;
+        try {
+            compactAction1 = TextUtils.isEmpty(compactAction1Flag)
+                    ? DEFAULT_COMPACT_ACTION_1 : Integer.parseInt(compactAction1Flag);
+        } catch (NumberFormatException e) {
+          // Do nothing, leave default.
+        }
+
+        int compactAction2 = DEFAULT_COMPACT_ACTION_2;
+        try {
+            compactAction2 = TextUtils.isEmpty(compactAction2Flag)
+                    ? DEFAULT_COMPACT_ACTION_2 : Integer.parseInt(compactAction2Flag);
+        } catch (NumberFormatException e) {
+            // Do nothing, leave default.
+        }
+
+        mCompactActionSome = compactActionIntToString(compactAction1);
+        mCompactActionFull = compactActionIntToString(compactAction2);
+    }
+
+    @GuardedBy("mPhenotypeFlagLock")
+    private void updateCompactionThrottles() {
+        boolean useThrottleDefaults = false;
+        String throttleSomeSomeFlag =
+                DeviceConfig.getProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                    KEY_COMPACT_THROTTLE_1);
+        String throttleSomeFullFlag =
+                DeviceConfig.getProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                    KEY_COMPACT_THROTTLE_2);
+        String throttleFullSomeFlag =
+                DeviceConfig.getProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                    KEY_COMPACT_THROTTLE_3);
+        String throttleFullFullFlag =
+                DeviceConfig.getProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                    KEY_COMPACT_THROTTLE_4);
+
+        if (TextUtils.isEmpty(throttleSomeSomeFlag) || TextUtils.isEmpty(throttleSomeFullFlag)
+                || TextUtils.isEmpty(throttleFullSomeFlag)
+                || TextUtils.isEmpty(throttleFullFullFlag)) {
+            // Set defaults for all if any are not set.
+            useThrottleDefaults = true;
+        } else {
+            try {
+                mCompactThrottleSomeSome = Integer.parseInt(throttleSomeSomeFlag);
+                mCompactThrottleSomeFull = Integer.parseInt(throttleSomeFullFlag);
+                mCompactThrottleFullSome = Integer.parseInt(throttleFullSomeFlag);
+                mCompactThrottleFullFull = Integer.parseInt(throttleFullFullFlag);
+            } catch (NumberFormatException e) {
+                useThrottleDefaults = true;
+            }
+        }
+
+        if (useThrottleDefaults) {
+            mCompactThrottleSomeSome = DEFAULT_COMPACT_THROTTLE_1;
+            mCompactThrottleSomeFull = DEFAULT_COMPACT_THROTTLE_2;
+            mCompactThrottleFullSome = DEFAULT_COMPACT_THROTTLE_3;
+            mCompactThrottleFullFull = DEFAULT_COMPACT_THROTTLE_4;
+        }
+    }
+
+    @VisibleForTesting
+    static String compactActionIntToString(int action) {
+        switch(action) {
+            case COMPACT_ACTION_FILE_FLAG:
+                return COMPACT_ACTION_FILE;
+            case COMPACT_ACTION_ANON_FLAG:
+                return COMPACT_ACTION_ANON;
+            case COMPACT_ACTION_FULL_FLAG:
+                return COMPACT_ACTION_FULL;
+            default:
+                return COMPACT_ACTION_FILE;
+        }
+    }
+
+    private final class MemCompactionHandler extends Handler {
+        private MemCompactionHandler() {
+            super(mCompactionThread.getLooper());
         }
 
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-            case COMPACT_PROCESS_MSG: {
-                long start = SystemClock.uptimeMillis();
-                ProcessRecord proc;
-                int pid;
-                String action;
-                final String name;
-                int pendingAction, lastCompactAction;
-                long lastCompactTime;
-                synchronized(mAc.mAm) {
-                    proc = mAc.mPendingCompactionProcesses.remove(0);
-
-                    // don't compact if the process has returned to perceptible
-                    if (proc.setAdj <= ProcessList.PERCEPTIBLE_APP_ADJ) {
-                        return;
+                case COMPACT_PROCESS_MSG: {
+                    long start = SystemClock.uptimeMillis();
+                    ProcessRecord proc;
+                    int pid;
+                    String action;
+                    final String name;
+                    int pendingAction, lastCompactAction;
+                    long lastCompactTime;
+                    synchronized (mAm) {
+                        proc = mPendingCompactionProcesses.remove(0);
+
+                        // don't compact if the process has returned to perceptible
+                        if (proc.setAdj <= ProcessList.PERCEPTIBLE_APP_ADJ) {
+                            return;
+                        }
+
+                        pid = proc.pid;
+                        name = proc.processName;
+                        pendingAction = proc.reqCompactAction;
+                        lastCompactAction = proc.lastCompactAction;
+                        lastCompactTime = proc.lastCompactTime;
                     }
 
-                    pid = proc.pid;
-                    name = proc.processName;
-                    pendingAction = proc.reqCompactAction;
-                    lastCompactAction = proc.lastCompactAction;
-                    lastCompactTime = proc.lastCompactTime;
-                }
-                if (pid == 0) {
-                    // not a real process, either one being launched or one being killed
-                    return;
-                }
-
-                // basic throttling
-                // use the ActivityManagerConstants knobs to determine whether current/prevous
-                // compaction combo should be throtted or not
-                if (pendingAction == COMPACT_PROCESS_SOME) {
-                    if ((lastCompactAction == COMPACT_PROCESS_SOME && (start - lastCompactTime < throttleSomeSome)) ||
-                        (lastCompactAction == COMPACT_PROCESS_FULL && (start - lastCompactTime < throttleSomeFull))) {
+                    if (pid == 0) {
+                        // not a real process, either one being launched or one being killed
                         return;
                     }
-                } else {
-                    if ((lastCompactAction == COMPACT_PROCESS_SOME && (start - lastCompactTime < throttleFullSome)) ||
-                        (lastCompactAction == COMPACT_PROCESS_FULL && (start - lastCompactTime < throttleFullFull))) {
-                        return;
+
+                    // basic throttling
+                    // use the Phenotype flag knobs to determine whether current/prevous
+                    // compaction combo should be throtted or not
+
+                    // Note that we explicitly don't take mPhenotypeFlagLock here as the flags
+                    // should very seldom change, and taking the risk of using the wrong action is
+                    // preferable to taking the lock for every single compaction action.
+                    if (pendingAction == COMPACT_PROCESS_SOME) {
+                        if ((lastCompactAction == COMPACT_PROCESS_SOME
+                                  && (start - lastCompactTime < mCompactThrottleSomeSome))
+                                  || (lastCompactAction == COMPACT_PROCESS_FULL
+                                      && (start - lastCompactTime
+                                          < mCompactThrottleSomeFull))) {
+                            return;
+                        }
+                    } else {
+                        if ((lastCompactAction == COMPACT_PROCESS_SOME
+                                  && (start - lastCompactTime < mCompactThrottleFullSome))
+                                  || (lastCompactAction == COMPACT_PROCESS_FULL
+                                      && (start - lastCompactTime
+                                          < mCompactThrottleFullFull))) {
+                            return;
+                        }
                     }
-                }
 
-                try {
-                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Compact " +
-                            ((pendingAction == COMPACT_PROCESS_SOME) ? "some" : "full") +
-                            ": " + name);
-                    long[] rssBefore = Process.getRss(pid);
-                    FileOutputStream fos = new FileOutputStream("/proc/" + pid + "/reclaim");
                     if (pendingAction == COMPACT_PROCESS_SOME) {
-                        action = compactActionSome;
+                        action = mCompactActionSome;
                     } else {
-                        action = compactActionFull;
+                        action = mCompactActionFull;
                     }
-                    fos.write(action.getBytes());
-                    fos.close();
-                    long[] rssAfter = Process.getRss(pid);
-                    long end = SystemClock.uptimeMillis();
-                    long time = end - start;
-                    EventLog.writeEvent(EventLogTags.AM_COMPACT, pid, name, action,
-                            rssBefore[0], rssBefore[1], rssBefore[2], rssBefore[3],
-                            rssAfter[0], rssAfter[1], rssAfter[2], rssAfter[3], time,
-                            lastCompactAction, lastCompactTime, msg.arg1, msg.arg2);
-                    StatsLog.write(StatsLog.APP_COMPACTED, pid, name, pendingAction,
-                            rssBefore[0], rssBefore[1], rssBefore[2], rssBefore[3],
-                            rssAfter[0], rssAfter[1], rssAfter[2], rssAfter[3], time,
-                            lastCompactAction, lastCompactTime, msg.arg1,
-                            ActivityManager.processStateAmToProto(msg.arg2));
-                    synchronized(mAc.mAm) {
-                        proc.lastCompactTime = end;
-                        proc.lastCompactAction = pendingAction;
+
+                    try {
+                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Compact "
+                                + ((pendingAction == COMPACT_PROCESS_SOME) ? "some" : "full")
+                                + ": " + name);
+                        long[] rssBefore = Process.getRss(pid);
+                        FileOutputStream fos = new FileOutputStream("/proc/" + pid + "/reclaim");
+                        fos.write(action.getBytes());
+                        fos.close();
+                        long[] rssAfter = Process.getRss(pid);
+                        long end = SystemClock.uptimeMillis();
+                        long time = end - start;
+                        EventLog.writeEvent(EventLogTags.AM_COMPACT, pid, name, action,
+                                rssBefore[0], rssBefore[1], rssBefore[2], rssBefore[3],
+                                rssAfter[0], rssAfter[1], rssAfter[2], rssAfter[3], time,
+                                lastCompactAction, lastCompactTime, msg.arg1, msg.arg2);
+                        StatsLog.write(StatsLog.APP_COMPACTED, pid, name, pendingAction,
+                                rssBefore[0], rssBefore[1], rssBefore[2], rssBefore[3],
+                                rssAfter[0], rssAfter[1], rssAfter[2], rssAfter[3], time,
+                                lastCompactAction, lastCompactTime, msg.arg1,
+                                ActivityManager.processStateAmToProto(msg.arg2));
+                        synchronized (mAm) {
+                            proc.lastCompactTime = end;
+                            proc.lastCompactAction = pendingAction;
+                        }
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    } catch (Exception e) {
+                        // nothing to do, presumably the process died
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                     }
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
-                } catch (Exception e) {
-                    // nothing to do, presumably the process died
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                 }
             }
-            }
         }
     }
-
-
 }
index be910d4..1e03f6c 100644 (file)
@@ -134,10 +134,11 @@ public final class OomAdjuster {
 
         mLocalPowerManager = LocalServices.getService(PowerManagerInternal.class);
         mConstants = mService.mConstants;
-        // mConstants can be null under test, which causes AppCompactor to crash
-        if (mConstants != null) {
-            mAppCompact = new AppCompactor(mService);
-        }
+        mAppCompact = new AppCompactor(mService);
+    }
+
+    void initSettings() {
+        mAppCompact.init();
     }
 
     /**
@@ -1679,7 +1680,7 @@ public final class OomAdjuster {
 
         if (app.curAdj != app.setAdj) {
             // don't compact during bootup
-            if (mConstants.USE_COMPACTION && mService.mBooted) {
+            if (mAppCompact.useCompaction() && mService.mBooted) {
                 // Perform a minor compaction when a perceptible app becomes the prev/home app
                 // Perform a major compaction when any app enters cached
                 // reminder: here, setAdj is previous state, curAdj is upcoming state
@@ -2104,4 +2105,8 @@ public final class OomAdjuster {
                 + " mNewNumServiceProcs=" + mNewNumServiceProcs);
     }
 
+    @GuardedBy("mService")
+    void dumpAppCompactorSettings(PrintWriter pw) {
+        mAppCompact.dump(pw);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/am/AppCompactorTest.java b/services/tests/servicestests/src/com/android/server/am/AppCompactorTest.java
new file mode 100644 (file)
index 0000000..1a231cf
--- /dev/null
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_ACTION_1;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_ACTION_2;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_THROTTLE_1;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_THROTTLE_2;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_THROTTLE_3;
+import static android.provider.DeviceConfig.ActivityManager.KEY_COMPACT_THROTTLE_4;
+import static android.provider.DeviceConfig.ActivityManager.KEY_USE_COMPACTION;
+
+import static com.android.server.am.ActivityManagerService.Injector;
+import static com.android.server.am.AppCompactor.compactActionIntToString;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.provider.DeviceConfig;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.appop.AppOpsService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for {@link AppCompactor}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:AppCompactorTest
+ */
+@RunWith(AndroidJUnit4.class)
+public final class AppCompactorTest {
+
+    @Mock private AppOpsService mAppOpsService;
+    private AppCompactor mCompactorUnderTest;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private CountDownLatch mCountDown;
+
+    private static void clearDeviceConfig() throws IOException  {
+        UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        uiDevice.executeShellCommand(
+                "device_config delete activity_manager " + KEY_USE_COMPACTION);
+        uiDevice.executeShellCommand(
+                "device_config delete activity_manager " + KEY_COMPACT_ACTION_1);
+        uiDevice.executeShellCommand(
+                "device_config delete activity_manager " + KEY_COMPACT_ACTION_2);
+        uiDevice.executeShellCommand(
+                "device_config delete activity_manager " + KEY_COMPACT_THROTTLE_1);
+        uiDevice.executeShellCommand(
+                "device_config delete activity_manager " + KEY_COMPACT_THROTTLE_2);
+        uiDevice.executeShellCommand(
+                "device_config delete activity_manager " + KEY_COMPACT_THROTTLE_3);
+        uiDevice.executeShellCommand(
+                "device_config delete activity_manager " + KEY_COMPACT_THROTTLE_4);
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        MockitoAnnotations.initMocks(this);
+        clearDeviceConfig();
+        mHandlerThread = new HandlerThread("");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        ActivityManagerService ams = new ActivityManagerService(new TestInjector());
+        mCompactorUnderTest = new AppCompactor(ams,
+                new AppCompactor.PropertyChangedCallbackForTest() {
+                    @Override
+                    public void onPropertyChanged() {
+                        if (mCountDown != null) {
+                            mCountDown.countDown();
+                        }
+                    }
+                });
+    }
+
+    @After
+    public void tearDown() throws IOException {
+        mHandlerThread.quit();
+        mCountDown = null;
+        clearDeviceConfig();
+    }
+
+    @Test
+    public void init_setsDefaults() {
+        mCompactorUnderTest.init();
+        assertThat(mCompactorUnderTest.useCompaction(),
+                is(mCompactorUnderTest.DEFAULT_USE_COMPACTION));
+        assertThat(mCompactorUnderTest.mCompactActionSome, is(
+                compactActionIntToString(mCompactorUnderTest.DEFAULT_COMPACT_ACTION_1)));
+        assertThat(mCompactorUnderTest.mCompactActionFull, is(
+                compactActionIntToString(mCompactorUnderTest.DEFAULT_COMPACT_ACTION_2)));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_1));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_2));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_3));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_4));
+    }
+
+    @Test
+    public void init_withDeviceConfigSetsParameters() {
+        // When the DeviceConfig already has a flag value stored (note this test will need to
+        // change if the default value changes from false).
+        assertThat(mCompactorUnderTest.DEFAULT_USE_COMPACTION, is(false));
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_USE_COMPACTION, "true", false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_ACTION_1,
+                Integer.toString((AppCompactor.DEFAULT_COMPACT_ACTION_1 + 1 % 3) + 1), false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_ACTION_2,
+                Integer.toString((AppCompactor.DEFAULT_COMPACT_ACTION_2 + 1 % 3) + 1), false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_1,
+                Long.toString(AppCompactor.DEFAULT_COMPACT_THROTTLE_1 + 1), false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_2,
+                Long.toString(AppCompactor.DEFAULT_COMPACT_THROTTLE_2 + 1), false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_3,
+                Long.toString(AppCompactor.DEFAULT_COMPACT_THROTTLE_3 + 1), false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_4,
+                Long.toString(AppCompactor.DEFAULT_COMPACT_THROTTLE_4 + 1), false);
+
+        // Then calling init will read and set that flag.
+        mCompactorUnderTest.init();
+        assertThat(mCompactorUnderTest.useCompaction(), is(true));
+        assertThat(mCompactorUnderTest.mCompactionThread.isAlive(), is(true));
+
+        assertThat(mCompactorUnderTest.mCompactActionSome,
+                is(compactActionIntToString((AppCompactor.DEFAULT_COMPACT_ACTION_1 + 1 % 3) + 1)));
+        assertThat(mCompactorUnderTest.mCompactActionFull,
+                is(compactActionIntToString((AppCompactor.DEFAULT_COMPACT_ACTION_2 + 1 % 3) + 1)));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_1 + 1));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_2 + 1));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_3 + 1));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_4 + 1));
+    }
+
+    @Test
+    public void useCompaction_listensToDeviceConfigChanges() throws InterruptedException {
+        assertThat(mCompactorUnderTest.useCompaction(),
+                is(mCompactorUnderTest.DEFAULT_USE_COMPACTION));
+        // When we call init and change some the flag value...
+        mCompactorUnderTest.init();
+        mCountDown = new CountDownLatch(1);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_USE_COMPACTION, "true", false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+
+        // Then that new flag value is updated in the implementation.
+        assertThat(mCompactorUnderTest.useCompaction(), is(true));
+        assertThat(mCompactorUnderTest.mCompactionThread.isAlive(), is(true));
+
+        // And again, setting the flag the other way.
+        mCountDown = new CountDownLatch(1);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_USE_COMPACTION, "false", false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+        assertThat(mCompactorUnderTest.useCompaction(), is(false));
+    }
+
+    @Test
+    public void useCompaction_listensToDeviceConfigChangesBadValues() throws InterruptedException {
+        assertThat(mCompactorUnderTest.useCompaction(),
+                is(mCompactorUnderTest.DEFAULT_USE_COMPACTION));
+        mCompactorUnderTest.init();
+
+        // When we push an invalid flag value...
+        mCountDown = new CountDownLatch(1);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_USE_COMPACTION, "foobar", false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+
+        // Then we set the default.
+        assertThat(mCompactorUnderTest.useCompaction(), is(AppCompactor.DEFAULT_USE_COMPACTION));
+    }
+
+    @Test
+    public void compactAction_listensToDeviceConfigChanges() throws InterruptedException {
+        mCompactorUnderTest.init();
+
+        // When we override new values for the compaction action with reasonable values...
+
+        // There are three possible values for compactAction[Some|Full].
+        for (int i = 1; i < 4; i++) {
+            mCountDown = new CountDownLatch(2);
+            int expectedSome = (mCompactorUnderTest.DEFAULT_COMPACT_ACTION_1 + i) % 3 + 1;
+            DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                    KEY_COMPACT_ACTION_1, Integer.toString(expectedSome), false);
+            int expectedFull = (mCompactorUnderTest.DEFAULT_COMPACT_ACTION_2 + i) % 3 + 1;
+            DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                    KEY_COMPACT_ACTION_2, Integer.toString(expectedFull), false);
+            assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+
+            // Then the updates are reflected in the flags.
+            assertThat(mCompactorUnderTest.mCompactActionSome,
+                    is(compactActionIntToString(expectedSome)));
+            assertThat(mCompactorUnderTest.mCompactActionFull,
+                    is(compactActionIntToString(expectedFull)));
+        }
+    }
+
+    @Test
+    public void compactAction_listensToDeviceConfigChangesBadValues() throws InterruptedException {
+        mCompactorUnderTest.init();
+
+        // When we override new values for the compaction action with bad values ...
+        mCountDown = new CountDownLatch(2);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_ACTION_1, "foo", false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_ACTION_2, "foo", false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+
+        // Then the default values are reflected in the flag
+        assertThat(mCompactorUnderTest.mCompactActionSome,
+                is(compactActionIntToString(AppCompactor.DEFAULT_COMPACT_ACTION_1)));
+        assertThat(mCompactorUnderTest.mCompactActionFull,
+                is(compactActionIntToString(AppCompactor.DEFAULT_COMPACT_ACTION_2)));
+
+        mCountDown = new CountDownLatch(2);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_ACTION_1, "", false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_ACTION_2, "", false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+
+        assertThat(mCompactorUnderTest.mCompactActionSome,
+                is(compactActionIntToString(AppCompactor.DEFAULT_COMPACT_ACTION_1)));
+        assertThat(mCompactorUnderTest.mCompactActionFull,
+                is(compactActionIntToString(AppCompactor.DEFAULT_COMPACT_ACTION_2)));
+    }
+
+    @Test
+    public void compactThrottle_listensToDeviceConfigChanges() throws InterruptedException {
+        mCompactorUnderTest.init();
+
+        // When we override new reasonable throttle values after init...
+        mCountDown = new CountDownLatch(4);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_1,
+                Long.toString(AppCompactor.DEFAULT_COMPACT_THROTTLE_1 + 1), false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_2,
+                Long.toString(AppCompactor.DEFAULT_COMPACT_THROTTLE_2 + 1), false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_3,
+                Long.toString(AppCompactor.DEFAULT_COMPACT_THROTTLE_3 + 1), false);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_4,
+                Long.toString(AppCompactor.DEFAULT_COMPACT_THROTTLE_4 + 1), false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+
+        // Then those flags values are reflected in the compactor.
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_1 + 1));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_2 + 1));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_3 + 1));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_4 + 1));
+    }
+
+    @Test
+    public void compactThrottle_listensToDeviceConfigChangesBadValues()
+            throws IOException, InterruptedException {
+        mCompactorUnderTest.init();
+
+        // When one of the throttles is overridden with a bad value...
+        mCountDown = new CountDownLatch(1);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_1, "foo", false);
+        // Then all the throttles have the defaults set.
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_1));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_2));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_3));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_4));
+        clearDeviceConfig();
+
+        // Repeat for each of the throttle keys.
+        mCountDown = new CountDownLatch(1);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_2, "foo", false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_1));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_2));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_3));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_4));
+        clearDeviceConfig();
+
+        mCountDown = new CountDownLatch(1);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_3, "foo", false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_1));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_2));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_3));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_4));
+        clearDeviceConfig();
+
+        mCountDown = new CountDownLatch(1);
+        DeviceConfig.setProperty(DeviceConfig.ActivityManager.NAMESPACE,
+                KEY_COMPACT_THROTTLE_4, "foo", false);
+        assertThat(mCountDown.await(5, TimeUnit.SECONDS), is(true));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_1));
+        assertThat(mCompactorUnderTest.mCompactThrottleSomeFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_2));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullSome,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_3));
+        assertThat(mCompactorUnderTest.mCompactThrottleFullFull,
+                is(AppCompactor.DEFAULT_COMPACT_THROTTLE_4));
+    }
+
+    private class TestInjector extends Injector {
+        @Override
+        public AppOpsService getAppOpsService(File file, Handler handler) {
+            return mAppOpsService;
+        }
+
+        @Override
+        public Handler getUiHandler(ActivityManagerService service) {
+            return mHandler;
+        }
+    }
+}