diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java index 035c9d27f73ed..b2e95a58f0ec5 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java @@ -16,8 +16,6 @@ package com.android.server.tare; -import static android.text.format.DateUtils.HOUR_IN_MILLIS; - import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME; import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT; import static com.android.server.tare.EconomicPolicy.REGULATION_WEALTH_RECLAMATION; @@ -69,12 +67,6 @@ class Agent { private static final boolean DEBUG = InternalResourceService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); - /** - * The minimum amount of time an app must not have been used by the user before we start - * regularly reclaiming ARCs from it. - */ - private static final long MIN_UNUSED_TIME_MS = 3 * 24 * HOUR_IN_MILLIS; - private static final String ALARM_TAG_AFFORDABILITY_CHECK = "*tare.affordability_check*"; private final Object mLock; @@ -550,41 +542,62 @@ class Agent { /** * Reclaim a percentage of unused ARCs from every app that hasn't been used recently. The - * reclamation will not reduce an app's balance below its minimum balance as dictated by the - * EconomicPolicy. + * reclamation will not reduce an app's balance below its minimum balance as dictated by + * {@code scaleMinBalance}. * - * @param percentage A value between 0 and 1 to indicate how much of the unused balance should - * be reclaimed. + * @param percentage A value between 0 and 1 to indicate how much of the unused balance + * should be reclaimed. + * @param minUnusedTimeMs The minimum amount of time (in milliseconds) that must have + * transpired since the last user usage event before we will consider + * reclaiming ARCs from the app. + * @param scaleMinBalance Whether or not to used the scaled minimum app balance. If false, + * this will use the constant min balance floor given by + * {@link EconomicPolicy#getMinSatiatedBalance(int, String)}. If true, + * this will use the scaled balance given by + * {@link InternalResourceService#getMinBalanceLocked(int, String)}. */ @GuardedBy("mLock") - void reclaimUnusedAssetsLocked(double percentage) { + void reclaimUnusedAssetsLocked(double percentage, long minUnusedTimeMs, + boolean scaleMinBalance) { final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); - final List pkgs = mIrs.getInstalledPackages(); + final SparseArrayMap ledgers = mScribe.getLedgersLocked(); final long now = getCurrentTimeMillis(); - for (int i = 0; i < pkgs.size(); ++i) { - final int userId = UserHandle.getUserId(pkgs.get(i).applicationInfo.uid); - final String pkgName = pkgs.get(i).packageName; - final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); - // AppStandby only counts elapsed time for things like this - // TODO: should we use clock time instead? - final long timeSinceLastUsedMs = - mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId); - if (timeSinceLastUsedMs >= MIN_UNUSED_TIME_MS) { - // Use a constant floor instead of the scaled floor from the IRS. - final long minBalance = economicPolicy.getMinSatiatedBalance(userId, pkgName); + for (int u = 0; u < ledgers.numMaps(); ++u) { + final int userId = ledgers.keyAt(u); + for (int p = 0; p < ledgers.numElementsForKey(userId); ++p) { + final Ledger ledger = ledgers.valueAt(u, p); final long curBalance = ledger.getCurrentBalance(); - long toReclaim = (long) (curBalance * percentage); - if (curBalance - toReclaim < minBalance) { - toReclaim = curBalance - minBalance; + if (curBalance <= 0) { + continue; } - if (toReclaim > 0) { - Slog.i(TAG, "Reclaiming unused wealth! Taking " + toReclaim - + " from " + appToString(userId, pkgName)); + final String pkgName = ledgers.keyAt(u, p); + // AppStandby only counts elapsed time for things like this + // TODO: should we use clock time instead? + final long timeSinceLastUsedMs = + mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId); + if (timeSinceLastUsedMs >= minUnusedTimeMs) { + final long minBalance; + if (!scaleMinBalance) { + // Use a constant floor instead of the scaled floor from the IRS. + minBalance = economicPolicy.getMinSatiatedBalance(userId, pkgName); + } else { + minBalance = mIrs.getMinBalanceLocked(userId, pkgName); + } + long toReclaim = (long) (curBalance * percentage); + if (curBalance - toReclaim < minBalance) { + toReclaim = curBalance - minBalance; + } + if (toReclaim > 0) { + if (DEBUG) { + Slog.i(TAG, "Reclaiming unused wealth! Taking " + toReclaim + + " from " + appToString(userId, pkgName)); + } - recordTransactionLocked(userId, pkgName, ledger, - new Ledger.Transaction( - now, now, REGULATION_WEALTH_RECLAMATION, null, -toReclaim), - true); + recordTransactionLocked(userId, pkgName, ledger, + new Ledger.Transaction( + now, now, REGULATION_WEALTH_RECLAMATION, null, -toReclaim), + true); + } } } } diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java index a39fd474c8ac2..1ad7407407aff 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java @@ -18,6 +18,7 @@ package com.android.server.tare; import static android.provider.Settings.Global.TARE_ALARM_MANAGER_CONSTANTS; import static android.provider.Settings.Global.TARE_JOB_SCHEDULER_CONSTANTS; +import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; @@ -85,6 +86,11 @@ public class InternalResourceService extends SystemService { static final long UNUSED_RECLAMATION_PERIOD_MS = 24 * HOUR_IN_MILLIS; /** How much of an app's unused wealth should be reclaimed periodically. */ private static final float DEFAULT_UNUSED_RECLAMATION_PERCENTAGE = .1f; + /** + * The minimum amount of time an app must not have been used by the user before we start + * periodically reclaiming ARCs from it. + */ + private static final long MIN_UNUSED_TIME_MS = 3 * DAY_IN_MILLIS; /** The amount of time to delay reclamation by after boot. */ private static final long RECLAMATION_STARTUP_DELAY_MS = 30_000L; private static final int PACKAGE_QUERY_FLAGS = @@ -107,6 +113,44 @@ public class InternalResourceService extends SystemService { @GuardedBy("mLock") private CompleteEconomicPolicy mCompleteEconomicPolicy; + private static final class ReclamationConfig { + /** + * ARC circulation threshold (% circulating vs scaled maximum) above which this config + * should come into play. + */ + public final double circulationPercentageThreshold; + /** @see Agent#reclaimUnusedAssetsLocked(double, long, boolean) */ + public final double reclamationPercentage; + /** @see Agent#reclaimUnusedAssetsLocked(double, long, boolean) */ + public final long minUsedTimeMs; + /** @see Agent#reclaimUnusedAssetsLocked(double, long, boolean) */ + public final boolean scaleMinBalance; + + ReclamationConfig(double circulationPercentageThreshold, double reclamationPercentage, + long minUsedTimeMs, boolean scaleMinBalance) { + this.circulationPercentageThreshold = circulationPercentageThreshold; + this.reclamationPercentage = reclamationPercentage; + this.minUsedTimeMs = minUsedTimeMs; + this.scaleMinBalance = scaleMinBalance; + } + } + + /** + * Sorted list of reclamation configs used to determine how many credits to force reclaim when + * the circulation percentage is too high. The list should *always* be sorted in descending + * order of {@link ReclamationConfig#circulationPercentageThreshold}. + */ + @GuardedBy("mLock") + private final List mReclamationConfigs = List.of( + new ReclamationConfig(2, .75, 12 * HOUR_IN_MILLIS, true), + new ReclamationConfig(1.6, .5, DAY_IN_MILLIS, true), + new ReclamationConfig(1.4, .25, DAY_IN_MILLIS, true), + new ReclamationConfig(1.2, .25, 2 * DAY_IN_MILLIS, true), + new ReclamationConfig(1, .25, MIN_UNUSED_TIME_MS, false), + new ReclamationConfig( + .9, DEFAULT_UNUSED_RECLAMATION_PERCENTAGE, MIN_UNUSED_TIME_MS, false) + ); + @NonNull @GuardedBy("mLock") private final List mPkgCache = new ArrayList<>(); @@ -190,7 +234,8 @@ public class InternalResourceService extends SystemService { @Override public void onAlarm() { synchronized (mLock) { - mAgent.reclaimUnusedAssetsLocked(DEFAULT_UNUSED_RECLAMATION_PERCENTAGE); + mAgent.reclaimUnusedAssetsLocked( + DEFAULT_UNUSED_RECLAMATION_PERCENTAGE, MIN_UNUSED_TIME_MS, false); mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis()); scheduleUnusedWealthReclamationLocked(); } @@ -200,6 +245,7 @@ public class InternalResourceService extends SystemService { private static final int MSG_NOTIFY_AFFORDABILITY_CHANGE_LISTENER = 0; private static final int MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT = 1; private static final int MSG_PROCESS_USAGE_EVENT = 2; + private static final int MSG_MAYBE_FOCE_RECLAIM = 3; private static final String ALARM_TAG_WEALTH_RECLAMATION = "*tare.reclamation*"; private static final String KEY_PKG = "pkg"; @@ -317,6 +363,8 @@ public class InternalResourceService extends SystemService { final int newBatteryLevel = getCurrentBatteryLevel(); if (newBatteryLevel > mCurrentBatteryLevel) { mAgent.distributeBasicIncomeLocked(newBatteryLevel); + } else if (newBatteryLevel < mCurrentBatteryLevel) { + mHandler.obtainMessage(MSG_MAYBE_FOCE_RECLAIM).sendToTarget(); } mCurrentBatteryLevel = newBatteryLevel; } @@ -511,6 +559,48 @@ public class InternalResourceService extends SystemService { } } + /** + * Reclaim unused ARCs above apps' minimum balances if there are too many credits currently + * in circulation. + */ + @GuardedBy("mLock") + private void maybeForceReclaimLocked() { + final long maxCirculation = getMaxCirculationLocked(); + if (maxCirculation == 0) { + Slog.wtf(TAG, "Max scaled circulation is 0..."); + mAgent.reclaimUnusedAssetsLocked(1, HOUR_IN_MILLIS, true); + mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis()); + scheduleUnusedWealthReclamationLocked(); + return; + } + final long curCirculation = mScribe.getNarcsInCirculationLocked(); + final double circulationPerc = 1.0 * curCirculation / maxCirculation; + if (DEBUG) { + Slog.d(TAG, "Circulation %: " + circulationPerc); + } + final int numConfigs = mReclamationConfigs.size(); + if (numConfigs == 0) { + return; + } + // The configs are sorted in descending order of circulationPercentageThreshold, so we can + // short-circuit if the current circulation is lower than the lowest threshold. + if (circulationPerc + < mReclamationConfigs.get(numConfigs - 1).circulationPercentageThreshold) { + return; + } + // TODO: maybe exclude apps we think will be launched in the next few hours + for (int i = 0; i < numConfigs; ++i) { + final ReclamationConfig config = mReclamationConfigs.get(i); + if (circulationPerc >= config.circulationPercentageThreshold) { + mAgent.reclaimUnusedAssetsLocked( + config.reclamationPercentage, config.minUsedTimeMs, config.scaleMinBalance); + mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis()); + scheduleUnusedWealthReclamationLocked(); + break; + } + } + } + private void registerListeners() { final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_BATTERY_LEVEL_CHANGED); @@ -594,6 +684,14 @@ public class InternalResourceService extends SystemService { @Override public void handleMessage(Message msg) { switch (msg.what) { + case MSG_MAYBE_FOCE_RECLAIM: { + removeMessages(MSG_MAYBE_FOCE_RECLAIM); + synchronized (mLock) { + maybeForceReclaimLocked(); + } + } + break; + case MSG_NOTIFY_AFFORDABILITY_CHANGE_LISTENER: { Bundle data = msg.getData(); final int userId = msg.arg1; diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java index d4c6c8c269482..42f3d9a1ea7b2 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java @@ -147,6 +147,12 @@ public class Scribe { return ledger; } + @GuardedBy("mIrs.getLock()") + @NonNull + SparseArrayMap getLedgersLocked() { + return mLedgers; + } + /** Returns the total amount of narcs currently allocated to apps. */ @GuardedBy("mIrs.getLock()") long getNarcsInCirculationLocked() {