Keep warming services out of cached adj

Add a pre-configured list that will help to prevent those services
which will keep critical code path of the host processes warm
from falling into cached adj; in case of multi-user, such service
running in the background user won't get this capability, unless it's
singleton.

The list will be empty by default and subjected to be overlaid
by device configurations.

Bug: 158685897
Test: atest MockingOomAdjusterTests
Test: Manual - Add test service into the pre-configured list
      start it and verify it won't get into cached state
Change-Id: I062f5ec3947ad3acebcc2ffe79e420beb23443a2
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 7a05b90..f60766a 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4386,4 +4386,6 @@
         0.333
     </item>
 
+    <!-- Component names of the services which will keep critical code path warm -->
+    <string-array name="config_keep_warming_services" translatable="false" />
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 77f7dcd..5bb784a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4058,4 +4058,5 @@
   <java-symbol type="integer" name="config_defaultBinderHeavyHitterAutoSamplerBatchSize" />
   <java-symbol type="dimen" name="config_defaultBinderHeavyHitterAutoSamplerThreshold" />
 
+  <java-symbol type="array" name="config_keep_warming_services" />
 </resources>
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index e8f0eaa..09ed16e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -19,6 +19,7 @@
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_POWER_QUICK;
 
 import android.app.ActivityThread;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
@@ -336,6 +337,11 @@
      */
     public int PENDINGINTENT_WARNING_THRESHOLD =  DEFAULT_PENDINGINTENT_WARNING_THRESHOLD;
 
+    /**
+     * Component names of the services which will keep critical code path of the host warm
+     */
+    public final ArraySet<ComponentName> KEEP_WARMING_SERVICES = new ArraySet<ComponentName>();
+
     private List<String> mDefaultImperceptibleKillExemptPackages;
     private List<Integer> mDefaultImperceptibleKillExemptProcStates;
 
@@ -496,6 +502,10 @@
         BINDER_HEAVY_HITTER_AUTO_SAMPLER_BATCHSIZE = mDefaultBinderHeavyHitterAutoSamplerBatchSize;
         BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD = mDefaultBinderHeavyHitterAutoSamplerThreshold;
         service.scheduleUpdateBinderHeavyHitterWatcherConfig();
+        KEEP_WARMING_SERVICES.addAll(Arrays.stream(
+                context.getResources().getStringArray(
+                        com.android.internal.R.array.config_keep_warming_services))
+                .map(ComponentName::unflattenFromString).collect(Collectors.toSet()));
     }
 
     public void start(ContentResolver resolver) {
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index da5f489..f0343e1 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -76,7 +76,11 @@
 import android.app.usage.UsageEvents;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ServiceInfo;
 import android.os.Debug;
 import android.os.Handler;
@@ -265,6 +269,43 @@
 
     void initSettings() {
         mCachedAppOptimizer.init();
+        if (mService.mConstants.KEEP_WARMING_SERVICES.size() > 0) {
+            final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
+            mService.mContext.registerReceiverForAllUsers(new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    synchronized (mService) {
+                        handleUserSwitchedLocked();
+                    }
+                }
+            }, filter, null, mService.mHandler);
+        }
+    }
+
+    /**
+     * Update the keep-warming service flags upon user switches
+     */
+    @VisibleForTesting
+    @GuardedBy("mService")
+    void handleUserSwitchedLocked() {
+        final ArraySet<ComponentName> warmServices = mService.mConstants.KEEP_WARMING_SERVICES;
+        final ArrayList<ProcessRecord> processes = mProcessList.mLruProcesses;
+        for (int i = processes.size() - 1; i >= 0; i--) {
+            final ProcessRecord app = processes.get(i);
+            boolean includeWarmPkg = false;
+            for (int j = warmServices.size() - 1; j >= 0; j--) {
+                if (app.pkgList.containsKey(warmServices.valueAt(j).getPackageName())) {
+                    includeWarmPkg = true;
+                    break;
+                }
+            }
+            if (!includeWarmPkg) {
+                continue;
+            }
+            for (int j = app.numberOfRunningServices() - 1; j >= 0; j--) {
+                app.getRunningServiceAt(j).updateKeepWarmLocked();
+            }
+        }
     }
 
     /**
@@ -1470,7 +1511,7 @@
                                 "Raise procstate to started service: " + app);
                     }
                 }
-                if (app.hasShownUi && !app.getCachedIsHomeProcess()) {
+                if (!s.mKeepWarming && app.hasShownUi && !app.getCachedIsHomeProcess()) {
                     // If this process has shown some UI, let it immediately
                     // go to the LRU list because it may be pretty heavy with
                     // UI stuff.  We'll tag it with a label just to help
@@ -1479,7 +1520,8 @@
                         app.adjType = "cch-started-ui-services";
                     }
                 } else {
-                    if (now < (s.lastActivity + mConstants.MAX_SERVICE_INACTIVITY)) {
+                    if (s.mKeepWarming
+                            || now < (s.lastActivity + mConstants.MAX_SERVICE_INACTIVITY)) {
                         // This service has seen some activity within
                         // recent memory, so we will keep its process ahead
                         // of the background processes.
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 749a990..828ac71 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -43,6 +43,7 @@
 import android.util.proto.ProtoOutputStream;
 import android.util.proto.ProtoUtils;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.procstats.ServiceState;
 import com.android.internal.os.BatteryStatsImpl;
 import com.android.server.LocalServices;
@@ -149,6 +150,8 @@
 
     private int lastStartId;    // identifier of most recent start request.
 
+    boolean mKeepWarming; // Whether or not it'll keep critical code path of the host warm
+
     static class StartItem {
         final ServiceRecord sr;
         final boolean taskRemoved;
@@ -517,6 +520,7 @@
         lastActivity = SystemClock.uptimeMillis();
         userId = UserHandle.getUserId(appInfo.uid);
         createdFromFg = callerIsFg;
+        updateKeepWarmLocked();
     }
 
     public ServiceState getTracker() {
@@ -735,6 +739,14 @@
         }
     }
 
+    @GuardedBy("ams")
+    void updateKeepWarmLocked() {
+        mKeepWarming = ams.mConstants.KEEP_WARMING_SERVICES.contains(name)
+                && (ams.mUserController.getCurrentUserId() == userId
+                || ams.isSingleton(processName, appInfo, instanceName.getClassName(),
+                        serviceInfo.flags));
+    }
+
     public AppBindRecord retrieveAppBindingLocked(Intent intent,
             ProcessRecord app) {
         Intent.FilterComparison filter = new Intent.FilterComparison(intent);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index fde40aa..2a267c4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -84,6 +84,7 @@
 import android.os.IBinder;
 import android.os.PowerManagerInternal;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -130,6 +131,7 @@
     private static final int MOCKAPP5_UID = MOCKAPP_UID + 4;
     private static final String MOCKAPP5_PROCESSNAME = "test #5";
     private static final String MOCKAPP5_PACKAGENAME = "com.android.test.test5";
+    private static final int MOCKAPP2_UID_OTHER = MOCKAPP2_UID + UserHandle.PER_USER_RANGE;
     private static Context sContext;
     private static PackageManagerInternal sPackageManagerInternal;
     private static ActivityManagerService sService;
@@ -168,6 +170,8 @@
                 mock(SparseArray.class));
         setFieldValue(ActivityManagerService.class, sService, "mOomAdjProfiler",
                 mock(OomAdjProfiler.class));
+        setFieldValue(ActivityManagerService.class, sService, "mUserController",
+                mock(UserController.class));
         doReturn(new ActivityManagerService.ProcessChangeItem()).when(sService)
                 .enqueueProcessChangeItemLocked(anyInt(), anyInt());
         sService.mOomAdjuster = new OomAdjuster(sService, sService.mProcessList,
@@ -1621,6 +1625,117 @@
         assertEquals(SERVICE_ADJ, app.setAdj);
     }
 
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testUpdateOomAdj_DoAll_Service_KeepWarmingList() {
+        final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+                MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+        final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID_OTHER,
+                MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+        final int userOwner = 0;
+        final int userOther = 1;
+        final int cachedAdj1 = CACHED_APP_MIN_ADJ + ProcessList.CACHED_APP_IMPORTANCE_LEVELS;
+        final int cachedAdj2 = cachedAdj1 + ProcessList.CACHED_APP_IMPORTANCE_LEVELS * 2;
+        doReturn(userOwner).when(sService.mUserController).getCurrentUserId();
+
+        final ArrayList<ProcessRecord> lru = sService.mProcessList.mLruProcesses;
+        lru.clear();
+        lru.add(app2);
+        lru.add(app);
+
+        final ComponentName cn = ComponentName.unflattenFromString(
+                MOCKAPP_PACKAGENAME + "/.TestService");
+        final ComponentName cn2 = ComponentName.unflattenFromString(
+                MOCKAPP2_PACKAGENAME + "/.TestService");
+        final long now = SystemClock.uptimeMillis();
+
+        sService.mConstants.KEEP_WARMING_SERVICES.clear();
+        final ServiceInfo si = mock(ServiceInfo.class);
+        si.applicationInfo = mock(ApplicationInfo.class);
+        ServiceRecord s = spy(new ServiceRecord(sService, null, cn, cn, null, 0, null,
+                si, false, null));
+        doReturn(new ArrayMap<IBinder, ArrayList<ConnectionRecord>>()).when(s).getConnections();
+        s.startRequested = true;
+        s.lastActivity = now;
+
+        app.setCached(false);
+        app.startService(s);
+        app.hasShownUi = true;
+
+        final ServiceInfo si2 = mock(ServiceInfo.class);
+        si2.applicationInfo = mock(ApplicationInfo.class);
+        si2.applicationInfo.uid = MOCKAPP2_UID_OTHER;
+        ServiceRecord s2 = spy(new ServiceRecord(sService, null, cn2, cn2, null, 0, null,
+                si2, false, null));
+        doReturn(new ArrayMap<IBinder, ArrayList<ConnectionRecord>>()).when(s2).getConnections();
+        s2.startRequested = true;
+        s2.lastActivity = now - sService.mConstants.MAX_SERVICE_INACTIVITY - 1;
+
+        app2.setCached(false);
+        app2.startService(s2);
+        app2.hasShownUi = false;
+
+        sService.mWakefulness = PowerManagerInternal.WAKEFULNESS_AWAKE;
+        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+
+        assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-ui-services");
+        assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj2, "cch-started-services");
+
+        app.setProcState = PROCESS_STATE_NONEXISTENT;
+        app.adjType = null;
+        app.setAdj = UNKNOWN_ADJ;
+        app.hasShownUi = false;
+        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+
+        assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services");
+
+        app.setCached(false);
+        app.setProcState = PROCESS_STATE_NONEXISTENT;
+        app.adjType = null;
+        app.setAdj = UNKNOWN_ADJ;
+        s.lastActivity = now - sService.mConstants.MAX_SERVICE_INACTIVITY - 1;
+        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+
+        assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services");
+
+        app.stopService(s);
+        app.setProcState = PROCESS_STATE_NONEXISTENT;
+        app.adjType = null;
+        app.setAdj = UNKNOWN_ADJ;
+        app.hasShownUi = true;
+        sService.mConstants.KEEP_WARMING_SERVICES.add(cn);
+        sService.mConstants.KEEP_WARMING_SERVICES.add(cn2);
+        s = spy(new ServiceRecord(sService, null, cn, cn, null, 0, null,
+                si, false, null));
+        doReturn(new ArrayMap<IBinder, ArrayList<ConnectionRecord>>()).when(s).getConnections();
+        s.startRequested = true;
+        s.lastActivity = now;
+
+        app.startService(s);
+        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+
+        assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services");
+        assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services");
+
+        app.setCached(true);
+        app.setProcState = PROCESS_STATE_NONEXISTENT;
+        app.adjType = null;
+        app.setAdj = UNKNOWN_ADJ;
+        app.hasShownUi = false;
+        s.lastActivity = now - sService.mConstants.MAX_SERVICE_INACTIVITY - 1;
+        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+
+        assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services");
+        assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services");
+
+        doReturn(userOther).when(sService.mUserController).getCurrentUserId();
+        sService.mOomAdjuster.handleUserSwitchedLocked();
+
+        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+        assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services");
+        assertProcStates(app2, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services");
+    }
+
     private ProcessRecord makeDefaultProcessRecord(int pid, int uid, String processName,
             String packageName, boolean hasShownUi) {
         long now = SystemClock.uptimeMillis();
@@ -1747,4 +1862,12 @@
         assertEquals(expectedAdj, app.setAdj);
         assertEquals(expectedSchedGroup, app.setSchedGroup);
     }
+
+    private void assertProcStates(ProcessRecord app, boolean expectedCached,
+            int expectedProcState, int expectedAdj, String expectedAdjType) {
+        assertEquals(expectedCached, app.isCached());
+        assertEquals(expectedProcState, app.setProcState);
+        assertEquals(expectedAdj, app.setAdj);
+        assertEquals(expectedAdjType, app.adjType);
+    }
 }