summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java2
-rw-r--r--services/core/java/com/android/server/am/CacheOomRanker.java308
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java19
-rw-r--r--services/core/java/com/android/server/am/ProcessRecord.java18
-rw-r--r--services/tests/mockingservicestests/AndroidManifest.xml2
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java432
6 files changed, 779 insertions, 2 deletions
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index be1d1bcbdfff..c601ce229cb7 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -8302,6 +8302,7 @@ public class ActivityManagerService extends IActivityManager.Stub
synchronized(this) {
mConstants.dump(pw);
mOomAdjuster.dumpCachedAppOptimizerSettings(pw);
+ mOomAdjuster.dumpCacheOomRankerSettings(pw);
pw.println();
if (dumpAll) {
pw.println("-------------------------------------------------------------------------------");
@@ -8722,6 +8723,7 @@ public class ActivityManagerService extends IActivityManager.Stub
synchronized (this) {
mConstants.dump(pw);
mOomAdjuster.dumpCachedAppOptimizerSettings(pw);
+ mOomAdjuster.dumpCacheOomRankerSettings(pw);
}
} else if ("services".equals(cmd) || "s".equals(cmd)) {
if (dumpClient) {
diff --git a/services/core/java/com/android/server/am/CacheOomRanker.java b/services/core/java/com/android/server/am/CacheOomRanker.java
new file mode 100644
index 000000000000..26cfd62d40ad
--- /dev/null
+++ b/services/core/java/com/android/server/am/CacheOomRanker.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2020 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 android.provider.DeviceConfig;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.concurrent.Executor;
+
+/**
+ * Class to re-rank a number of the least recently used processes before they
+ * are assigned oom adjust scores.
+ */
+public class CacheOomRanker {
+ @VisibleForTesting
+ static final String KEY_USE_OOM_RE_RANKING = "use_oom_re_ranking";
+ private static final boolean DEFAULT_USE_OOM_RE_RANKING = false;
+ @VisibleForTesting
+ static final String KEY_OOM_RE_RANKING_NUMBER_TO_RE_RANK = "oom_re_ranking_number_to_re_rank";
+ @VisibleForTesting static final int DEFAULT_OOM_RE_RANKING_NUMBER_TO_RE_RANK = 8;
+ @VisibleForTesting
+ static final String KEY_OOM_RE_RANKING_LRU_WEIGHT = "oom_re_ranking_lru_weight";
+ @VisibleForTesting static final float DEFAULT_OOM_RE_RANKING_LRU_WEIGHT = 0.35f;
+ @VisibleForTesting
+ static final String KEY_OOM_RE_RANKING_USES_WEIGHT = "oom_re_ranking_uses_weight";
+ @VisibleForTesting static final float DEFAULT_OOM_RE_RANKING_USES_WEIGHT = 0.5f;
+ @VisibleForTesting
+ static final String KEY_OOM_RE_RANKING_RSS_WEIGHT = "oom_re_ranking_rss_weight";
+ @VisibleForTesting static final float DEFAULT_OOM_RE_RANKING_RSS_WEIGHT = 0.15f;
+
+ private static final Comparator<RankedProcessRecord> SCORED_PROCESS_RECORD_COMPARATOR =
+ new ScoreComparator();
+ private static final Comparator<RankedProcessRecord> CACHE_USE_COMPARATOR =
+ new CacheUseComparator();
+ private static final Comparator<RankedProcessRecord> LAST_RSS_COMPARATOR =
+ new LastRssComparator();
+ private static final Comparator<RankedProcessRecord> LAST_ACTIVITY_TIME_COMPARATOR =
+ new LastActivityTimeComparator();
+
+ private final Object mPhenotypeFlagLock = new Object();
+
+ @GuardedBy("mPhenotypeFlagLock")
+ private boolean mUseOomReRanking = DEFAULT_USE_OOM_RE_RANKING;
+ // Weight to apply to the LRU ordering.
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting float mLruWeight = DEFAULT_OOM_RE_RANKING_LRU_WEIGHT;
+ // Weight to apply to the ordering by number of times the process has been added to the cache.
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting float mUsesWeight = DEFAULT_OOM_RE_RANKING_USES_WEIGHT;
+ // Weight to apply to the ordering by RSS used by the processes.
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting float mRssWeight = DEFAULT_OOM_RE_RANKING_RSS_WEIGHT;
+
+ // Positions to replace in the lru list.
+ @GuardedBy("mPhenotypeFlagLock")
+ private int[] mLruPositions;
+ // Processes to re-rank
+ @GuardedBy("mPhenotypeFlagLock")
+ private RankedProcessRecord[] mScoredProcessRecords;
+
+ private final DeviceConfig.OnPropertiesChangedListener mOnFlagsChangedListener =
+ new DeviceConfig.OnPropertiesChangedListener() {
+ @Override
+ public void onPropertiesChanged(DeviceConfig.Properties properties) {
+ synchronized (mPhenotypeFlagLock) {
+ for (String name : properties.getKeyset()) {
+ if (KEY_USE_OOM_RE_RANKING.equals(name)) {
+ updateUseOomReranking();
+ } else if (KEY_OOM_RE_RANKING_NUMBER_TO_RE_RANK.equals(name)) {
+ updateNumberToReRank();
+ } else if (KEY_OOM_RE_RANKING_LRU_WEIGHT.equals(name)) {
+ updateLruWeight();
+ } else if (KEY_OOM_RE_RANKING_USES_WEIGHT.equals(name)) {
+ updateUsesWeight();
+ } else if (KEY_OOM_RE_RANKING_RSS_WEIGHT.equals(name)) {
+ updateRssWeight();
+ }
+ }
+ }
+ }
+ };
+
+ /** Load settings from device config and register a listener for changes. */
+ public void init(Executor executor) {
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ executor, mOnFlagsChangedListener);
+ synchronized (mPhenotypeFlagLock) {
+ updateUseOomReranking();
+ updateNumberToReRank();
+ updateLruWeight();
+ updateUsesWeight();
+ updateRssWeight();
+ }
+ }
+
+ /**
+ * Returns whether oom re-ranking is enabled.
+ */
+ public boolean useOomReranking() {
+ synchronized (mPhenotypeFlagLock) {
+ return mUseOomReRanking;
+ }
+ }
+
+ @GuardedBy("mPhenotypeFlagLock")
+ private void updateUseOomReranking() {
+ mUseOomReRanking = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_USE_OOM_RE_RANKING, DEFAULT_USE_OOM_RE_RANKING);
+ }
+
+ @GuardedBy("mPhenotypeFlagLock")
+ private void updateNumberToReRank() {
+ int previousNumberToReRank = getNumberToReRank();
+ int numberToReRank = DeviceConfig.getInt(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_OOM_RE_RANKING_NUMBER_TO_RE_RANK, DEFAULT_OOM_RE_RANKING_NUMBER_TO_RE_RANK);
+ if (previousNumberToReRank != numberToReRank) {
+ mScoredProcessRecords = new RankedProcessRecord[numberToReRank];
+ for (int i = 0; i < mScoredProcessRecords.length; ++i) {
+ mScoredProcessRecords[i] = new RankedProcessRecord();
+ }
+ mLruPositions = new int[numberToReRank];
+ }
+ }
+
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting
+ int getNumberToReRank() {
+ return mScoredProcessRecords == null ? 0 : mScoredProcessRecords.length;
+ }
+
+ @GuardedBy("mPhenotypeFlagLock")
+ private void updateLruWeight() {
+ mLruWeight = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_OOM_RE_RANKING_LRU_WEIGHT, DEFAULT_OOM_RE_RANKING_LRU_WEIGHT);
+ }
+
+ @GuardedBy("mPhenotypeFlagLock")
+ private void updateUsesWeight() {
+ mUsesWeight = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_OOM_RE_RANKING_USES_WEIGHT, DEFAULT_OOM_RE_RANKING_USES_WEIGHT);
+ }
+
+ @GuardedBy("mPhenotypeFlagLock")
+ private void updateRssWeight() {
+ mRssWeight = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_OOM_RE_RANKING_RSS_WEIGHT, DEFAULT_OOM_RE_RANKING_RSS_WEIGHT);
+ }
+
+ /**
+ * Re-rank the cached processes in the lru list with a weighted ordering
+ * of lru, rss size and number of times the process has been put in the cache.
+ */
+ public void reRankLruCachedApps(ProcessList processList) {
+ float lruWeight;
+ float usesWeight;
+ float rssWeight;
+ int[] lruPositions;
+ RankedProcessRecord[] scoredProcessRecords;
+
+ ArrayList<ProcessRecord> lruList = processList.mLruProcesses;
+
+ synchronized (mPhenotypeFlagLock) {
+ lruWeight = mLruWeight;
+ usesWeight = mUsesWeight;
+ rssWeight = mRssWeight;
+ lruPositions = mLruPositions;
+ scoredProcessRecords = mScoredProcessRecords;
+ }
+
+ // Don't re-rank if the class hasn't been initialized with defaults.
+ if (lruPositions == null || scoredProcessRecords == null) {
+ return;
+ }
+
+ // Collect the least recently used processes to re-rank, only rank cached
+ // processes further down the list than mLruProcessServiceStart.
+ int cachedProcessPos = 0;
+ for (int i = 0; i < processList.mLruProcessServiceStart
+ && cachedProcessPos < scoredProcessRecords.length; ++i) {
+ ProcessRecord app = lruList.get(i);
+ // Processes that will be assigned a cached oom adj score.
+ if (!app.killedByAm && app.thread != null && app.curAdj
+ >= ProcessList.UNKNOWN_ADJ) {
+ scoredProcessRecords[cachedProcessPos].proc = app;
+ scoredProcessRecords[cachedProcessPos].score = 0.0f;
+ lruPositions[cachedProcessPos] = i;
+ ++cachedProcessPos;
+ }
+ }
+
+ // TODO maybe ensure a certain number above this in the cache before re-ranking.
+ if (cachedProcessPos < scoredProcessRecords.length) {
+ // Ignore we don't have enough processes to worry about re-ranking.
+ return;
+ }
+
+ // Add scores for each of the weighted features we want to rank based on.
+ if (lruWeight > 0.0f) {
+ // This doesn't use the LRU list ordering as after the first re-ranking
+ // that will no longer be lru.
+ Arrays.sort(scoredProcessRecords, LAST_ACTIVITY_TIME_COMPARATOR);
+ addToScore(scoredProcessRecords, lruWeight);
+ }
+ if (rssWeight > 0.0f) {
+ Arrays.sort(scoredProcessRecords, LAST_RSS_COMPARATOR);
+ addToScore(scoredProcessRecords, rssWeight);
+ }
+ if (usesWeight > 0.0f) {
+ Arrays.sort(scoredProcessRecords, CACHE_USE_COMPARATOR);
+ addToScore(scoredProcessRecords, usesWeight);
+ }
+
+ // Re-rank by the new combined score.
+ Arrays.sort(scoredProcessRecords, SCORED_PROCESS_RECORD_COMPARATOR);
+
+ if (ActivityManagerDebugConfig.DEBUG_OOM_ADJ) {
+ boolean printedHeader = false;
+ for (int i = 0; i < scoredProcessRecords.length; ++i) {
+ if (scoredProcessRecords[i].proc.pid != lruList.get(lruPositions[i]).pid) {
+ if (!printedHeader) {
+ Slog.i(OomAdjuster.TAG, "reRankLruCachedApps");
+ printedHeader = true;
+ }
+ Slog.i(OomAdjuster.TAG, " newPos=" + lruPositions[i] + " "
+ + scoredProcessRecords[i].proc);
+ }
+ }
+ }
+
+ for (int i = 0; i < scoredProcessRecords.length; ++i) {
+ lruList.set(lruPositions[i], scoredProcessRecords[i].proc);
+ scoredProcessRecords[i].proc = null;
+ }
+ }
+
+ private static void addToScore(RankedProcessRecord[] scores, float weight) {
+ for (int i = 1; i < scores.length; ++i) {
+ scores[i].score += i * weight;
+ }
+ }
+
+ void dump(PrintWriter pw) {
+ pw.println("CacheOomRanker settings");
+ synchronized (mPhenotypeFlagLock) {
+ pw.println(" " + KEY_USE_OOM_RE_RANKING + "=" + mUseOomReRanking);
+ pw.println(" " + KEY_OOM_RE_RANKING_NUMBER_TO_RE_RANK + "=" + getNumberToReRank());
+ pw.println(" " + KEY_OOM_RE_RANKING_LRU_WEIGHT + "=" + mLruWeight);
+ pw.println(" " + KEY_OOM_RE_RANKING_USES_WEIGHT + "=" + mUsesWeight);
+ pw.println(" " + KEY_OOM_RE_RANKING_RSS_WEIGHT + "=" + mRssWeight);
+ }
+ }
+
+ private static class ScoreComparator implements Comparator<RankedProcessRecord> {
+ @Override
+ public int compare(RankedProcessRecord o1, RankedProcessRecord o2) {
+ return Float.compare(o1.score, o2.score);
+ }
+ }
+
+ private static class LastActivityTimeComparator implements Comparator<RankedProcessRecord> {
+ @Override
+ public int compare(RankedProcessRecord o1, RankedProcessRecord o2) {
+ return Long.compare(o1.proc.lastActivityTime, o2.proc.lastActivityTime);
+ }
+ }
+
+ private static class CacheUseComparator implements Comparator<RankedProcessRecord> {
+ @Override
+ public int compare(RankedProcessRecord o1, RankedProcessRecord o2) {
+ return Long.compare(o1.proc.getCacheOomRankerUseCount(),
+ o2.proc.getCacheOomRankerUseCount());
+ }
+ }
+
+ private static class LastRssComparator implements Comparator<RankedProcessRecord> {
+ @Override
+ public int compare(RankedProcessRecord o1, RankedProcessRecord o2) {
+ // High RSS first to match least recently used.
+ return Long.compare(o2.proc.mLastRss, o1.proc.mLastRss);
+ }
+ }
+
+ private static class RankedProcessRecord {
+ public ProcessRecord proc;
+ public float score;
+ }
+}
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index e5d57df742aa..d69bf2d75064 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -72,6 +72,7 @@ import static com.android.server.am.AppProfiler.TAG_PSS;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH;
import android.app.ActivityManager;
+import android.app.ActivityThread;
import android.app.ApplicationExitInfo;
import android.app.usage.UsageEvents;
import android.compat.annotation.ChangeId;
@@ -119,7 +120,7 @@ import java.util.Arrays;
* All of the code required to compute proc states and oom_adj values.
*/
public final class OomAdjuster {
- private static final String TAG = "OomAdjuster";
+ static final String TAG = "OomAdjuster";
static final String OOM_ADJ_REASON_METHOD = "updateOomAdj";
static final String OOM_ADJ_REASON_NONE = OOM_ADJ_REASON_METHOD + "_meh";
static final String OOM_ADJ_REASON_ACTIVITY = OOM_ADJ_REASON_METHOD + "_activityChange";
@@ -169,6 +170,12 @@ public final class OomAdjuster {
*/
CachedAppOptimizer mCachedAppOptimizer;
+ /**
+ * Re-rank apps getting a cache oom adjustment from lru to weighted order
+ * based on weighted scores for LRU, PSS and cache use count.
+ */
+ CacheOomRanker mCacheOomRanker;
+
ActivityManagerConstants mConstants;
final long[] mTmpLong = new long[3];
@@ -331,6 +338,7 @@ public final class OomAdjuster {
mLocalPowerManager = LocalServices.getService(PowerManagerInternal.class);
mConstants = mService.mConstants;
mCachedAppOptimizer = new CachedAppOptimizer(mService);
+ mCacheOomRanker = new CacheOomRanker();
mProcessGroupHandler = new Handler(adjusterThread.getLooper(), msg -> {
final int pid = msg.arg1;
@@ -361,6 +369,7 @@ public final class OomAdjuster {
void initSettings() {
mCachedAppOptimizer.init();
+ mCacheOomRanker.init(ActivityThread.currentApplication().getMainExecutor());
if (mService.mConstants.KEEP_WARMING_SERVICES.size() > 0) {
final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
mService.mContext.registerReceiverForAllUsers(new BroadcastReceiver() {
@@ -769,6 +778,9 @@ public final class OomAdjuster {
}
}
+ if (mCacheOomRanker.useOomReranking()) {
+ mCacheOomRanker.reRankLruCachedApps(mProcessList);
+ }
assignCachedAdjIfNecessary(mProcessList.mLruProcesses);
if (computeClients) { // There won't be cycles if we didn't compute clients above.
@@ -2775,6 +2787,11 @@ public final class OomAdjuster {
}
@GuardedBy("mService")
+ void dumpCacheOomRankerSettings(PrintWriter pw) {
+ mCacheOomRanker.dump(pw);
+ }
+
+ @GuardedBy("mService")
void updateAppFreezeStateLocked(ProcessRecord app) {
if (!mCachedAppOptimizer.useFreezer()) {
return;
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index d4d01652e338..520a28bfd889 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -360,6 +360,13 @@ class ProcessRecord implements WindowProcessListener {
int mCachedProcState = ActivityManager.PROCESS_STATE_CACHED_EMPTY;
int mCachedSchedGroup = ProcessList.SCHED_GROUP_BACKGROUND;
+ // Approximates the usage count of the app, used for cache re-ranking by CacheOomRanker.
+ //
+ // Counts the number of times the process is re-added to the cache (i.e. setCached(false);
+ // setCached(true)). This over counts, as setCached is sometimes reset while remaining in the
+ // cache. However, this happens uniformly across processes, so ranking is not affected.
+ private int mCacheOomRankerUseCount;
+
boolean mReachable; // Whether or not this process is reachable from given process
long mKillTime; // The timestamp in uptime when this process was killed.
@@ -828,7 +835,12 @@ class ProcessRecord implements WindowProcessListener {
}
void setCached(boolean cached) {
- mCached = cached;
+ if (mCached != cached) {
+ mCached = cached;
+ if (cached) {
+ ++mCacheOomRankerUseCount;
+ }
+ }
}
@Override
@@ -836,6 +848,10 @@ class ProcessRecord implements WindowProcessListener {
return mCached;
}
+ int getCacheOomRankerUseCount() {
+ return mCacheOomRankerUseCount;
+ }
+
boolean hasActivities() {
return mWindowProcessController.hasActivities();
}
diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml
index fbde1d2cf1cf..e4b650cad055 100644
--- a/services/tests/mockingservicestests/AndroidManifest.xml
+++ b/services/tests/mockingservicestests/AndroidManifest.xml
@@ -21,11 +21,13 @@
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
<uses-permission android:name="android.permission.HARDWARE_TEST"/>
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<uses-permission android:name="android.permission.MANAGE_APPOPS"/>
<uses-permission android:name="android.permission.MONITOR_DEVICE_CONFIG_ACCESS"/>
+ <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/>
<!-- needed by MasterClearReceiverTest to display a system dialog -->
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"/>
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java
new file mode 100644
index 000000000000..37103965de3b
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2020 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.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.provider.DeviceConfig;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.appop.AppOpsService;
+import com.android.server.testables.TestableDeviceConfig;
+import com.android.server.wm.ActivityTaskManagerService;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.File;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for {@link CachedAppOptimizer}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksMockingServicesTests:CacheOomRankerTest
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CacheOomRankerTest {
+
+ @Mock
+ private AppOpsService mAppOpsService;
+ private Handler mHandler;
+ private ActivityManagerService mAms;
+
+ @Mock
+ private PackageManagerInternal mPackageManagerInt;
+
+ @Rule
+ public final TestableDeviceConfig.TestableDeviceConfigRule
+ mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule();
+ @Rule
+ public final ApplicationExitInfoTest.ServiceThreadRule
+ mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
+
+ private int mNextPid = 10000;
+ private int mNextUid = 30000;
+ private int mNextPackageUid = 40000;
+ private int mNextPackageName = 1;
+
+ private TestExecutor mExecutor = new TestExecutor();
+ private CacheOomRanker mCacheOomRanker = new CacheOomRanker();
+
+ @Before
+ public void setUp() {
+ HandlerThread handlerThread = new HandlerThread("");
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+ /* allowIo */
+ ServiceThread thread = new ServiceThread("TestServiceThread",
+ Process.THREAD_PRIORITY_DEFAULT,
+ true /* allowIo */);
+ thread.start();
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ mAms = new ActivityManagerService(
+ new TestInjector(context), mServiceThreadRule.getThread());
+ mAms.mActivityTaskManager = new ActivityTaskManagerService(context);
+ mAms.mActivityTaskManager.initialize(null, null, context.getMainLooper());
+ mAms.mAtmInternal = spy(mAms.mActivityTaskManager.getAtmInternal());
+ mAms.mPackageManagerInt = mPackageManagerInt;
+ doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
+ LocalServices.removeServiceForTest(PackageManagerInternal.class);
+ LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+
+ mCacheOomRanker.init(mExecutor);
+ }
+
+ @Test
+ public void init_listensForConfigChanges() throws InterruptedException {
+ mExecutor.init();
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_USE_OOM_RE_RANKING,
+ Boolean.TRUE.toString(), true);
+ mExecutor.waitForLatch();
+ assertThat(mCacheOomRanker.useOomReranking()).isTrue();
+ mExecutor.init();
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_USE_OOM_RE_RANKING, Boolean.FALSE.toString(), false);
+ mExecutor.waitForLatch();
+ assertThat(mCacheOomRanker.useOomReranking()).isFalse();
+
+ mExecutor.init();
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_OOM_RE_RANKING_NUMBER_TO_RE_RANK,
+ Integer.toString(CacheOomRanker.DEFAULT_OOM_RE_RANKING_NUMBER_TO_RE_RANK + 2),
+ false);
+ mExecutor.waitForLatch();
+ assertThat(mCacheOomRanker.getNumberToReRank())
+ .isEqualTo(CacheOomRanker.DEFAULT_OOM_RE_RANKING_NUMBER_TO_RE_RANK + 2);
+
+ mExecutor.init();
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_OOM_RE_RANKING_LRU_WEIGHT,
+ Float.toString(CacheOomRanker.DEFAULT_OOM_RE_RANKING_LRU_WEIGHT + 0.1f),
+ false);
+ mExecutor.waitForLatch();
+ assertThat(mCacheOomRanker.mLruWeight)
+ .isEqualTo(CacheOomRanker.DEFAULT_OOM_RE_RANKING_LRU_WEIGHT + 0.1f);
+
+ mExecutor.init();
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_OOM_RE_RANKING_RSS_WEIGHT,
+ Float.toString(CacheOomRanker.DEFAULT_OOM_RE_RANKING_RSS_WEIGHT - 0.1f),
+ false);
+ mExecutor.waitForLatch();
+ assertThat(mCacheOomRanker.mRssWeight)
+ .isEqualTo(CacheOomRanker.DEFAULT_OOM_RE_RANKING_RSS_WEIGHT - 0.1f);
+
+ mExecutor.init();
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_OOM_RE_RANKING_USES_WEIGHT,
+ Float.toString(CacheOomRanker.DEFAULT_OOM_RE_RANKING_USES_WEIGHT + 0.2f),
+ false);
+ mExecutor.waitForLatch();
+ assertThat(mCacheOomRanker.mUsesWeight)
+ .isEqualTo(CacheOomRanker.DEFAULT_OOM_RE_RANKING_USES_WEIGHT + 0.2f);
+ }
+
+ @Test
+ public void reRankLruCachedApps_lruImpactsOrdering() throws InterruptedException {
+ setConfig(/* numberToReRank= */ 5,
+ /* usesWeight= */ 0.0f,
+ /* pssWeight= */ 0.0f,
+ /* lruWeight= */1.0f);
+
+ ProcessList list = new ProcessList();
+ ArrayList<ProcessRecord> processList = list.mLruProcesses;
+ ProcessRecord lastUsed40MinutesAgo = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(40).toMillis(), 10 * 1024L, 1000);
+ processList.add(lastUsed40MinutesAgo);
+ ProcessRecord lastUsed42MinutesAgo = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(42).toMillis(), 20 * 1024L, 2000);
+ processList.add(lastUsed42MinutesAgo);
+ ProcessRecord lastUsed60MinutesAgo = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(60).toMillis(), 1024L, 10000);
+ processList.add(lastUsed60MinutesAgo);
+ ProcessRecord lastUsed15MinutesAgo = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(15).toMillis(), 100 * 1024L, 10);
+ processList.add(lastUsed15MinutesAgo);
+ ProcessRecord lastUsed17MinutesAgo = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(17).toMillis(), 1024L, 20);
+ processList.add(lastUsed17MinutesAgo);
+ // Only re-ranking 5 entries so this should stay in most recent position.
+ ProcessRecord lastUsed30MinutesAgo = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(30).toMillis(), 1024L, 20);
+ processList.add(lastUsed30MinutesAgo);
+
+ mCacheOomRanker.reRankLruCachedApps(list);
+
+ // First 5 ordered by least recently used first, then last processes position unchanged.
+ assertThat(processList).containsExactly(lastUsed60MinutesAgo, lastUsed42MinutesAgo,
+ lastUsed40MinutesAgo, lastUsed17MinutesAgo, lastUsed15MinutesAgo,
+ lastUsed30MinutesAgo);
+ }
+
+ @Test
+ public void reRankLruCachedApps_rssImpactsOrdering() throws InterruptedException {
+ setConfig(/* numberToReRank= */ 6,
+ /* usesWeight= */ 0.0f,
+ /* pssWeight= */ 1.0f,
+ /* lruWeight= */ 0.0f);
+
+ ProcessList list = new ProcessList();
+ ArrayList<ProcessRecord> processList = list.mLruProcesses;
+ ProcessRecord rss10k = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(40).toMillis(), 10 * 1024L, 1000);
+ processList.add(rss10k);
+ ProcessRecord rss20k = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(42).toMillis(), 20 * 1024L, 2000);
+ processList.add(rss20k);
+ ProcessRecord rss1k = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(60).toMillis(), 1024L, 10000);
+ processList.add(rss1k);
+ ProcessRecord rss100k = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(15).toMillis(), 100 * 1024L, 10);
+ processList.add(rss100k);
+ ProcessRecord rss2k = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(17).toMillis(), 2 * 1024L, 20);
+ processList.add(rss2k);
+ ProcessRecord rss15k = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(30).toMillis(), 15 * 1024L, 20);
+ processList.add(rss15k);
+ // Only re-ranking 6 entries so this should stay in most recent position.
+ ProcessRecord rss16k = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(30).toMillis(), 16 * 1024L, 20);
+ processList.add(rss16k);
+
+ mCacheOomRanker.reRankLruCachedApps(list);
+
+ // First 6 ordered by largest pss, then last processes position unchanged.
+ assertThat(processList).containsExactly(rss100k, rss20k, rss15k, rss10k, rss2k, rss1k,
+ rss16k);
+ }
+
+ @Test
+ public void reRankLruCachedApps_usesImpactsOrdering() throws InterruptedException {
+ setConfig(/* numberToReRank= */ 4,
+ /* usesWeight= */ 1.0f,
+ /* pssWeight= */ 0.0f,
+ /* lruWeight= */ 0.0f);
+
+ ProcessList list = new ProcessList();
+ list.mLruProcessServiceStart = 1;
+ ArrayList<ProcessRecord> processList = list.mLruProcesses;
+ ProcessRecord used1000 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(40).toMillis(), 10 * 1024L, 1000);
+ processList.add(used1000);
+ ProcessRecord used2000 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(42).toMillis(), 20 * 1024L, 2000);
+ processList.add(used2000);
+ ProcessRecord used10 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(15).toMillis(), 100 * 1024L, 10);
+ processList.add(used10);
+ ProcessRecord used20 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(17).toMillis(), 2 * 1024L, 20);
+ processList.add(used20);
+ // Only re-ranking 6 entries so last two should stay in most recent position.
+ ProcessRecord used500 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(30).toMillis(), 15 * 1024L, 500);
+ processList.add(used500);
+ ProcessRecord used200 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(30).toMillis(), 16 * 1024L, 200);
+ processList.add(used200);
+
+ mCacheOomRanker.reRankLruCachedApps(list);
+
+ // First 4 ordered by uses, then last processes position unchanged.
+ assertThat(processList).containsExactly(used10, used20, used1000, used2000, used500,
+ used200);
+ }
+
+ @Test
+ public void reRankLruCachedApps_notEnoughProcesses() throws InterruptedException {
+ setConfig(/* numberToReRank= */ 4,
+ /* usesWeight= */ 0.5f,
+ /* pssWeight= */ 0.2f,
+ /* lruWeight= */ 0.3f);
+
+ ProcessList list = new ProcessList();
+ ArrayList<ProcessRecord> processList = list.mLruProcesses;
+ ProcessRecord unknownAdj1 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(40).toMillis(), 10 * 1024L, 1000);
+ processList.add(unknownAdj1);
+ ProcessRecord unknownAdj2 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(42).toMillis(), 20 * 1024L, 2000);
+ processList.add(unknownAdj2);
+ ProcessRecord unknownAdj3 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(15).toMillis(), 100 * 1024L, 10);
+ processList.add(unknownAdj3);
+ ProcessRecord foregroundAdj = nextProcessRecord(ProcessList.FOREGROUND_APP_ADJ,
+ Duration.ofMinutes(17).toMillis(), 2 * 1024L, 20);
+ processList.add(foregroundAdj);
+ ProcessRecord serviceAdj = nextProcessRecord(ProcessList.SERVICE_ADJ,
+ Duration.ofMinutes(30).toMillis(), 15 * 1024L, 500);
+ processList.add(serviceAdj);
+ ProcessRecord systemAdj = nextProcessRecord(ProcessList.SYSTEM_ADJ,
+ Duration.ofMinutes(30).toMillis(), 16 * 1024L, 200);
+ processList.add(systemAdj);
+
+ // 6 Processes but only 3 in eligible for cache so no re-ranking.
+ mCacheOomRanker.reRankLruCachedApps(list);
+
+ // All positions unchanged.
+ assertThat(processList).containsExactly(unknownAdj1, unknownAdj2, unknownAdj3,
+ foregroundAdj, serviceAdj, systemAdj);
+ }
+
+ @Test
+ public void reRankLruCachedApps_notEnoughNonServiceProcesses() throws InterruptedException {
+ setConfig(/* numberToReRank= */ 4,
+ /* usesWeight= */ 1.0f,
+ /* pssWeight= */ 0.0f,
+ /* lruWeight= */ 0.0f);
+
+ ProcessList list = new ProcessList();
+ list.mLruProcessServiceStart = 4;
+ ArrayList<ProcessRecord> processList = list.mLruProcesses;
+ ProcessRecord used1000 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(40).toMillis(), 10 * 1024L, 1000);
+ processList.add(used1000);
+ ProcessRecord used2000 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(42).toMillis(), 20 * 1024L, 2000);
+ processList.add(used2000);
+ ProcessRecord used10 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(15).toMillis(), 100 * 1024L, 10);
+ processList.add(used10);
+ ProcessRecord used20 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(17).toMillis(), 2 * 1024L, 20);
+ processList.add(used20);
+ ProcessRecord used500 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(30).toMillis(), 15 * 1024L, 500);
+ processList.add(used500);
+ ProcessRecord used200 = nextProcessRecord(ProcessList.UNKNOWN_ADJ,
+ Duration.ofMinutes(30).toMillis(), 16 * 1024L, 200);
+ processList.add(used200);
+
+ mCacheOomRanker.reRankLruCachedApps(list);
+
+ // All positions unchanged.
+ assertThat(processList).containsExactly(used1000, used2000, used10, used20, used500,
+ used200);
+ }
+
+ private void setConfig(int numberToReRank, float useWeight, float pssWeight, float lruWeight)
+ throws InterruptedException {
+ mExecutor.init(4);
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_OOM_RE_RANKING_NUMBER_TO_RE_RANK,
+ Integer.toString(numberToReRank),
+ false);
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_OOM_RE_RANKING_LRU_WEIGHT,
+ Float.toString(lruWeight),
+ false);
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_OOM_RE_RANKING_RSS_WEIGHT,
+ Float.toString(pssWeight),
+ false);
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ CacheOomRanker.KEY_OOM_RE_RANKING_USES_WEIGHT,
+ Float.toString(useWeight),
+ false);
+ mExecutor.waitForLatch();
+ assertThat(mCacheOomRanker.getNumberToReRank()).isEqualTo(numberToReRank);
+ assertThat(mCacheOomRanker.mRssWeight).isEqualTo(pssWeight);
+ assertThat(mCacheOomRanker.mUsesWeight).isEqualTo(useWeight);
+ assertThat(mCacheOomRanker.mLruWeight).isEqualTo(lruWeight);
+ }
+
+ private ProcessRecord nextProcessRecord(int setAdj, long lastActivityTime, long lastRss,
+ int returnedToCacheCount) {
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.packageName = "a.package.name" + mNextPackageName++;
+ ProcessRecord app = new ProcessRecord(mAms, ai, ai.packageName + ":process", mNextUid++);
+ app.pid = mNextPid++;
+ app.info.uid = mNextPackageUid++;
+ // Exact value does not mater, it can be any state for which compaction is allowed.
+ app.setProcState = PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+ app.setAdj = setAdj;
+ app.lastActivityTime = lastActivityTime;
+ app.mLastRss = lastRss;
+ app.setCached(false);
+ for (int i = 0; i < returnedToCacheCount; ++i) {
+ app.setCached(false);
+ app.setCached(true);
+ }
+ return app;
+ }
+
+ private class TestExecutor implements Executor {
+ private CountDownLatch mLatch;
+
+ private void init(int count) {
+ mLatch = new CountDownLatch(count);
+ }
+
+ private void init() {
+ init(1);
+ }
+
+ private void waitForLatch() throws InterruptedException {
+ mLatch.await(5, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ mLatch.countDown();
+ }
+ }
+
+ private class TestInjector extends ActivityManagerService.Injector {
+ private TestInjector(Context context) {
+ super(context);
+ }
+
+ @Override
+ public AppOpsService getAppOpsService(File file, Handler handler) {
+ return mAppOpsService;
+ }
+
+ @Override
+ public Handler getUiHandler(ActivityManagerService service) {
+ return mHandler;
+ }
+ }
+}