diff options
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; + } + } +} |