diff options
4 files changed, 184 insertions, 103 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index b86aba687a64..8c08e757e405 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -3034,7 +3034,7 @@ public class JobSchedulerService extends com.android.server.SystemService } void resetExecutionQuota(@NonNull String pkgName, int userId) { - mQuotaController.clearAppStats(pkgName, userId); + mQuotaController.clearAppStats(userId, pkgName); } void resetScheduleQuota() { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index 4393a95af5ba..7256371102f5 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -54,12 +54,14 @@ import android.provider.Settings; import android.util.ArraySet; import android.util.KeyValueListParser; import android.util.Log; +import android.util.Pair; import android.util.Slog; import android.util.SparseArrayMap; import android.util.SparseBooleanArray; import android.util.SparseSetArray; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.IndentingPrintWriter; @@ -74,6 +76,7 @@ import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.PriorityQueue; import java.util.function.Consumer; import java.util.function.Predicate; @@ -301,10 +304,10 @@ public final class QuotaController extends StateController { private final SparseArrayMap<List<TimingSession>> mTimingSessions = new SparseArrayMap<>(); /** - * List of alarm listeners for each package that listen for when each package comes back within - * quota. + * Listener to track and manage when each package comes back within quota. */ - private final SparseArrayMap<QcAlarmListener> mInQuotaAlarmListeners = new SparseArrayMap<>(); + @GuardedBy("mLock") + private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener(); /** Cached calculation results for each app, with the standby buckets as the array indices. */ private final SparseArrayMap<ExecutionStats[]> mExecutionStatsCache = new SparseArrayMap<>(); @@ -579,7 +582,7 @@ public final class QuotaController extends StateController { Slog.wtf(TAG, "Told app removed but given null package name."); return; } - clearAppStats(packageName, UserHandle.getUserId(uid)); + clearAppStats(UserHandle.getUserId(uid), packageName); mForegroundUids.delete(uid); mUidToPackageCache.remove(uid); } @@ -589,13 +592,13 @@ public final class QuotaController extends StateController { mTrackedJobs.delete(userId); mPkgTimers.delete(userId); mTimingSessions.delete(userId); - mInQuotaAlarmListeners.delete(userId); + mInQuotaAlarmListener.removeAlarmsLocked(userId); mExecutionStatsCache.delete(userId); mUidToPackageCache.clear(); } /** Drop all historical stats and stop tracking any active sessions for the specified app. */ - public void clearAppStats(@NonNull String packageName, int userId) { + public void clearAppStats(int userId, @NonNull String packageName) { mTrackedJobs.delete(userId, packageName); Timer timer = mPkgTimers.get(userId, packageName); if (timer != null) { @@ -606,11 +609,7 @@ public final class QuotaController extends StateController { mPkgTimers.delete(userId, packageName); } mTimingSessions.delete(userId, packageName); - QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); - if (alarmListener != null) { - mAlarmManager.cancel(alarmListener); - mInQuotaAlarmListeners.delete(userId, packageName); - } + mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); mExecutionStatsCache.delete(userId, packageName); } @@ -1208,12 +1207,7 @@ public final class QuotaController extends StateController { // exempted. maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket); } else { - QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); - if (alarmListener != null && alarmListener.isWaiting()) { - mAlarmManager.cancel(alarmListener); - // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. - alarmListener.setTriggerTime(0); - } + mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); } return changed; } @@ -1229,12 +1223,7 @@ public final class QuotaController extends StateController { final String packageName = jobStatus.getSourcePackageName(); final int realStandbyBucket = jobStatus.getStandbyBucket(); if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) { - QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); - if (alarmListener != null && alarmListener.isWaiting()) { - mAlarmManager.cancel(alarmListener); - // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. - alarmListener.setTriggerTime(0); - } + mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); } else { mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket); } @@ -1285,7 +1274,6 @@ public final class QuotaController extends StateController { final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats, standbyBucket); - QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs && isUnderJobCountQuota @@ -1297,21 +1285,11 @@ public final class QuotaController extends StateController { + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) + "ms in its quota."); } - if (alarmListener != null) { - // Cancel any pending alarm. - mAlarmManager.cancel(alarmListener); - // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. - alarmListener.setTriggerTime(0); - } + mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); return; } - if (alarmListener == null) { - alarmListener = new QcAlarmListener(userId, packageName); - mInQuotaAlarmListeners.add(userId, packageName, alarmListener); - } - // The time this app will have quota again. long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { @@ -1325,27 +1303,7 @@ public final class QuotaController extends StateController { inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, stats.sessionRateLimitExpirationTimeElapsed); } - // Only schedule the alarm if: - // 1. There isn't one currently scheduled - // 2. The new alarm is significantly earlier than the previous alarm (which could be the - // case if the package moves into a higher standby bucket). If it's earlier but not - // significantly so, then we essentially delay the job a few extra minutes. - // 3. The alarm is after the current alarm by more than the quota buffer. - // TODO: this might be overengineering. Simplify if proven safe. - if (!alarmListener.isWaiting() - || inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS - || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) { - if (DEBUG) { - Slog.d(TAG, "Scheduling start alarm for " + pkgString); - } - // If the next time this app will have quota is at least 3 minutes before the - // alarm is supposed to go off, reschedule the alarm. - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed, - ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler); - alarmListener.setTriggerTime(inQuotaTimeElapsed); - } else if (DEBUG) { - Slog.d(TAG, "No need to schedule start alarm for " + pkgString); - } + mInQuotaAlarmListener.addAlarmLocked(userId, packageName, inQuotaTimeElapsed); } private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, boolean isWithinQuota) { @@ -1875,32 +1833,161 @@ public final class QuotaController extends StateController { } } - private class QcAlarmListener implements AlarmManager.OnAlarmListener { - private final int mUserId; - private final String mPackageName; - private volatile long mTriggerTimeElapsed; + static class AlarmQueue extends PriorityQueue<Pair<Package, Long>> { + AlarmQueue() { + super(1, (o1, o2) -> (int) (o1.second - o2.second)); + } + + /** + * Remove any instances of the Package from the queue. + * + * @return true if an instance was removed, false otherwise. + */ + boolean remove(@NonNull Package pkg) { + boolean removed = false; + Pair[] alarms = toArray(new Pair[size()]); + for (int i = alarms.length - 1; i >= 0; --i) { + if (pkg.equals(alarms[i].first)) { + remove(alarms[i]); + removed = true; + } + } + return removed; + } + } + + /** Track when UPTCs are expected to come back into quota. */ + private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener { + @GuardedBy("mLock") + private final AlarmQueue mAlarmQueue = new AlarmQueue(); + /** The next time the alarm is set to go off, in the elapsed realtime timebase. */ + @GuardedBy("mLock") + private long mTriggerTimeElapsed = 0; + + @GuardedBy("mLock") + void addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed) { + final Package pkg = new Package(userId, pkgName); + mAlarmQueue.remove(pkg); + mAlarmQueue.offer(new Pair<>(pkg, inQuotaTimeElapsed)); + setNextAlarmLocked(); + } - QcAlarmListener(int userId, String packageName) { - mUserId = userId; - mPackageName = packageName; + @GuardedBy("mLock") + void removeAlarmLocked(@NonNull Package pkg) { + if (mAlarmQueue.remove(pkg)) { + setNextAlarmLocked(); + } } - boolean isWaiting() { - return mTriggerTimeElapsed > 0; + @GuardedBy("mLock") + void removeAlarmLocked(int userId, @NonNull String packageName) { + removeAlarmLocked(new Package(userId, packageName)); } - void setTriggerTime(long timeElapsed) { - mTriggerTimeElapsed = timeElapsed; + @GuardedBy("mLock") + void removeAlarmsLocked(int userId) { + boolean removed = false; + Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); + for (int i = alarms.length - 1; i >= 0; --i) { + final Package pkg = (Package) alarms[i].first; + if (userId == pkg.userId) { + mAlarmQueue.remove(alarms[i]); + removed = true; + } + } + if (removed) { + setNextAlarmLocked(); + } } - long getTriggerTimeElapsed() { - return mTriggerTimeElapsed; + @GuardedBy("mLock") + private void setNextAlarmLocked() { + if (mAlarmQueue.size() > 0) { + final long nextTriggerTimeElapsed = mAlarmQueue.peek().second; + // Only schedule the alarm if one of the following is true: + // 1. There isn't one currently scheduled + // 2. The new alarm is significantly earlier than the previous alarm. If it's + // earlier but not significantly so, then we essentially delay the job a few extra + // minutes. + // 3. The alarm is after the current alarm. + if (mTriggerTimeElapsed == 0 + || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS + || mTriggerTimeElapsed < nextTriggerTimeElapsed) { + if (DEBUG) { + Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed); + } + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed, + ALARM_TAG_QUOTA_CHECK, this, mHandler); + mTriggerTimeElapsed = nextTriggerTimeElapsed; + } + } else { + mAlarmManager.cancel(this); + mTriggerTimeElapsed = 0; + } } @Override public void onAlarm() { - mHandler.obtainMessage(MSG_CHECK_PACKAGE, mUserId, 0, mPackageName).sendToTarget(); - mTriggerTimeElapsed = 0; + synchronized (mLock) { + while (mAlarmQueue.size() > 0) { + final Pair<Package, Long> alarm = mAlarmQueue.peek(); + if (alarm.second <= sElapsedRealtimeClock.millis()) { + mHandler.obtainMessage(MSG_CHECK_PACKAGE, alarm.first.userId, 0, + alarm.first.packageName).sendToTarget(); + mAlarmQueue.remove(alarm); + } else { + break; + } + } + setNextAlarmLocked(); + } + } + + @GuardedBy("mLock") + void dumpLocked(IndentingPrintWriter pw) { + pw.println("In quota alarms:"); + pw.increaseIndent(); + + if (mAlarmQueue.size() == 0) { + pw.println("NOT WAITING"); + } else { + Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); + for (int i = 0; i < alarms.length; ++i) { + final Package pkg = (Package) alarms[i].first; + pw.print(pkg); + pw.print(": "); + pw.print(alarms[i].second); + pw.println(); + } + } + + pw.decreaseIndent(); + } + + @GuardedBy("mLock") + void dumpLocked(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write( + StateControllerProto.QuotaController.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED, + mTriggerTimeElapsed); + + Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); + for (int i = 0; i < alarms.length; ++i) { + final long aToken = proto.start( + StateControllerProto.QuotaController.InQuotaAlarmListener.ALARMS); + + final Package pkg = (Package) alarms[i].first; + pkg.dumpDebug(proto, + StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.PKG); + proto.write( + StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED, + (Long) alarms[i].second); + + proto.end(aToken); + } + + proto.end(token); } } @@ -2618,23 +2705,7 @@ public final class QuotaController extends StateController { pw.decreaseIndent(); pw.println(); - pw.println("In quota alarms:"); - pw.increaseIndent(); - for (int u = 0; u < mInQuotaAlarmListeners.numMaps(); ++u) { - final int userId = mInQuotaAlarmListeners.keyAt(u); - for (int p = 0; p < mInQuotaAlarmListeners.numElementsForKey(userId); ++p) { - final String pkgName = mInQuotaAlarmListeners.keyAt(u, p); - QcAlarmListener alarmListener = mInQuotaAlarmListeners.valueAt(u, p); - - pw.print(string(userId, pkgName)); - pw.print(": "); - if (alarmListener.isWaiting()) { - pw.println(alarmListener.getTriggerTimeElapsed()); - } else { - pw.println("NOT WAITING"); - } - } - } + mInQuotaAlarmListener.dumpLocked(pw); pw.decreaseIndent(); } @@ -2768,22 +2839,13 @@ public final class QuotaController extends StateController { } } - QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, pkgName); - if (alarmListener != null) { - final long alToken = proto.start( - StateControllerProto.QuotaController.PackageStats.IN_QUOTA_ALARM_LISTENER); - proto.write(StateControllerProto.QuotaController.AlarmListener.IS_WAITING, - alarmListener.isWaiting()); - proto.write( - StateControllerProto.QuotaController.AlarmListener.TRIGGER_TIME_ELAPSED, - alarmListener.getTriggerTimeElapsed()); - proto.end(alToken); - } - proto.end(psToken); } } + mInQuotaAlarmListener.dumpLocked(proto, + StateControllerProto.QuotaController.IN_QUOTA_ALARM_LISTENER); + proto.end(mToken); proto.end(token); } diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 4bef2e38ad55..bd1bae6d83fb 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -670,7 +670,7 @@ message StateControllerProto { repeated ExecutionStats execution_stats = 4; - optional AlarmListener in_quota_alarm_listener = 5; + reserved 5; // in_quota_alarm_listener } repeated PackageStats package_stats = 5; @@ -683,7 +683,25 @@ message StateControllerProto { } repeated UidPackageMapping uid_to_package_cache = 7; - // Next tag: 8 + message InQuotaAlarmListener { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // The time at which the alarm is set to go off, in the elapsed realtime timebase. + optional int64 trigger_time_elapsed = 1; + + message Alarm { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional Package pkg = 1; + + // The time at which the package will be in quota, in the elapsed realtime timebase. + optional int64 in_quota_time_elapsed = 2; + } + repeated Alarm alarms = 2; + } + optional InQuotaAlarmListener in_quota_alarm_listener = 8; + + // Next tag: 9 } message StorageController { option (.android.msg_privacy).dest = DEST_AUTOMATIC; diff --git a/services/core/java/com/android/server/utils/quota/QuotaTracker.java b/services/core/java/com/android/server/utils/quota/QuotaTracker.java index a8cf9f6c0ec4..115b5c8d4010 100644 --- a/services/core/java/com/android/server/utils/quota/QuotaTracker.java +++ b/services/core/java/com/android/server/utils/quota/QuotaTracker.java @@ -552,6 +552,7 @@ abstract class QuotaTracker { mTriggerTimeElapsed = nextTriggerTimeElapsed; } } else { + cancelAlarm(this); mTriggerTimeElapsed = 0; } } |