From 6765e85033ab823270cdd9573a7437123059680d Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Thu, 20 Jan 2022 19:48:03 +0000 Subject: [PATCH] Deferring low and min priority jobs when quota is low. Make sure there is some minimum amount of quota available for higher priority jobs before we start running lower priority jobs. Bug: 205714315 Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job Test: atest CtsJobSchedulerTestCases Change-Id: I87919e08684fe1012bfc66d7fae928d04f575637 --- .../job/controllers/QuotaController.java | 393 +++++++++--- .../job/controllers/QuotaControllerTest.java | 561 +++++++++++++++--- 2 files changed, 774 insertions(+), 180 deletions(-) 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 65e1d49d15104..dd5246aebbb47 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 @@ -36,6 +36,7 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AlarmManager; import android.app.IUidObserver; +import android.app.job.JobInfo; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManagerInternal; import android.app.usage.UsageStatsManagerInternal.UsageEventListener; @@ -160,6 +161,28 @@ public final class QuotaController extends StateController { */ public long inQuotaTimeElapsed; + /** + * The time after which the app will be under the bucket quota and can start running + * low priority jobs again. This is only valid if + * {@link #executionTimeInWindowMs} >= + * {@link #mAllowedTimePerPeriodMs} * (1 - {@link #mAllowedTimeSurplusPriorityLow}), + * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs}, + * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or + * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}. + */ + public long inQuotaTimeLowElapsed; + + /** + * The time after which the app will be under the bucket quota and can start running + * min priority jobs again. This is only valid if + * {@link #executionTimeInWindowMs} >= + * {@link #mAllowedTimePerPeriodMs} * (1 - {@link #mAllowedTimeSurplusPriorityMin}), + * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs}, + * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or + * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}. + */ + public long inQuotaTimeMinElapsed; + /** * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid, * in the elapsed realtime timebase. @@ -199,6 +222,8 @@ public final class QuotaController extends StateController { + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", " + "sessionCountInWindow=" + sessionCountInWindow + ", " + "inQuotaTime=" + inQuotaTimeElapsed + ", " + + "inQuotaTimeLow=" + inQuotaTimeLowElapsed + ", " + + "inQuotaTimeMin=" + inQuotaTimeMinElapsed + ", " + "rateLimitJobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", " + "rateLimitJobCountWindow=" + jobCountInRateLimitingWindow + ", " + "rateLimitSessionCountExpirationTime=" @@ -351,6 +376,24 @@ public final class QuotaController extends StateController { */ private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + /** + * The percentage of {@link #mAllowedTimePerPeriodMs} that should not be used by + * {@link JobInfo#PRIORITY_LOW low priority} jobs. In other words, there must be a minimum + * surplus of this amount of remaining allowed time before we start running low priority + * jobs. + */ + private float mAllowedTimeSurplusPriorityLow = + QcConstants.DEFAULT_ALLOWED_TIME_SURPLUS_PRIORITY_LOW; + + /** + * The percentage of {@link #mAllowedTimePerPeriodMs} that should not be used by + * {@link JobInfo#PRIORITY_MIN min priority} jobs. In other words, there must be a minimum + * surplus of this amount of remaining allowed time before we start running low priority + * jobs. + */ + private float mAllowedTimeSurplusPriorityMin = + QcConstants.DEFAULT_ALLOWED_TIME_SURPLUS_PRIORITY_MIN; + /** The period of time used to rate limit recently run jobs. */ private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS; @@ -653,10 +696,11 @@ public final class QuotaController extends StateController { boolean forUpdate) { if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) { unprepareFromExecutionLocked(jobStatus); - ArraySet jobs = mTrackedJobs.get(jobStatus.getSourceUserId(), - jobStatus.getSourcePackageName()); - if (jobs != null) { - jobs.remove(jobStatus); + final int userId = jobStatus.getSourceUserId(); + final String pkgName = jobStatus.getSourcePackageName(); + ArraySet jobs = mTrackedJobs.get(userId, pkgName); + if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) { + mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, pkgName)); } } } @@ -771,7 +815,8 @@ public final class QuotaController extends StateController { return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; } return getTimeUntilQuotaConsumedLocked( - jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), + jobStatus.getEffectivePriority()); } // Expedited job. @@ -856,7 +901,8 @@ public final class QuotaController extends StateController { return isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid()) || isWithinQuotaLocked( - jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket, + jobStatus.getEffectivePriority()); } @GuardedBy("mLock") @@ -873,7 +919,7 @@ public final class QuotaController extends StateController { @VisibleForTesting @GuardedBy("mLock") boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName, - final int standbyBucket) { + final int standbyBucket, final int priority) { if (!mIsEnabled) { return true; } @@ -881,9 +927,16 @@ public final class QuotaController extends StateController { if (isQuotaFreeLocked(standbyBucket)) return true; + final long minSurplus; + if (priority <= JobInfo.PRIORITY_MIN) { + minSurplus = (long) (mAllowedTimePerPeriodMs * mAllowedTimeSurplusPriorityMin); + } else if (priority <= JobInfo.PRIORITY_LOW) { + minSurplus = (long) (mAllowedTimePerPeriodMs * mAllowedTimeSurplusPriorityLow); + } else { + minSurplus = 0; + } ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); - // TODO: use a higher minimum remaining time for jobs with MINIMUM priority - return getRemainingExecutionTimeLocked(stats) > 0 + return getRemainingExecutionTimeLocked(stats) > minSurplus && isUnderJobCountQuotaLocked(stats, standbyBucket) && isUnderSessionCountQuotaLocked(stats, standbyBucket); } @@ -1001,7 +1054,8 @@ public final class QuotaController extends StateController { * job is running. */ @VisibleForTesting - long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) { + long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName, + @JobInfo.Priority int jobPriority) { final long nowElapsed = sElapsedRealtimeClock.millis(); final int standbyBucket = JobSchedulerService.standbyBucketForPackage( packageName, userId, nowElapsed); @@ -1022,10 +1076,15 @@ public final class QuotaController extends StateController { final long startWindowElapsed = nowElapsed - stats.windowSizeMs; final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; - final long allowedTimeRemainingMs = mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs; + final long allowedTimePerPeriodMs = getAllowedTimePerPeriodMs(jobPriority); + final long allowedTimeRemainingMs = allowedTimePerPeriodMs - stats.executionTimeInWindowMs; final long maxExecutionTimeRemainingMs = mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs; + if (allowedTimeRemainingMs <= 0 || maxExecutionTimeRemainingMs <= 0) { + return 0; + } + // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can // essentially run until they reach the maximum limit. if (stats.windowSizeMs == mAllowedTimePerPeriodMs) { @@ -1044,6 +1103,16 @@ public final class QuotaController extends StateController { sessions, startWindowElapsed, allowedTimeRemainingMs)); } + private long getAllowedTimePerPeriodMs(@JobInfo.Priority int jobPriority) { + if (jobPriority <= JobInfo.PRIORITY_MIN) { + return (long) (mAllowedTimePerPeriodMs * (1 - mAllowedTimeSurplusPriorityMin)); + } + if (jobPriority <= JobInfo.PRIORITY_LOW) { + return (long) (mAllowedTimePerPeriodMs * (1 - mAllowedTimeSurplusPriorityLow)); + } + return mAllowedTimePerPeriodMs; + } + /** * Calculates how much time it will take, in milliseconds, until the quota is fully consumed. * @@ -1198,10 +1267,13 @@ public final class QuotaController extends StateController { stats.sessionCountInWindow = 0; if (stats.jobCountLimit == 0 || stats.sessionCountLimit == 0) { // App won't be in quota until configuration changes. - stats.inQuotaTimeElapsed = Long.MAX_VALUE; + stats.inQuotaTimeElapsed = stats.inQuotaTimeLowElapsed = stats.inQuotaTimeMinElapsed = + Long.MAX_VALUE; } else { stats.inQuotaTimeElapsed = 0; } + final long allowedTimeMinMs = getAllowedTimePerPeriodMs(JobInfo.PRIORITY_MIN); + final long allowedTimeLowMs = getAllowedTimePerPeriodMs(JobInfo.PRIORITY_LOW); Timer timer = mPkgTimers.get(userId, packageName); final long nowElapsed = sElapsedRealtimeClock.millis(); @@ -1219,13 +1291,25 @@ public final class QuotaController extends StateController { stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs); } + if (stats.executionTimeInWindowMs >= allowedTimeLowMs) { + stats.inQuotaTimeLowElapsed = Math.max(stats.inQuotaTimeLowElapsed, + nowElapsed - allowedTimeLowMs + stats.windowSizeMs); + } + if (stats.executionTimeInWindowMs >= allowedTimeMinMs) { + stats.inQuotaTimeMinElapsed = Math.max(stats.inQuotaTimeMinElapsed, + nowElapsed - allowedTimeMinMs + stats.windowSizeMs); + } if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { - stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, - nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); + final long inQuotaTime = nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS; + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime); + stats.inQuotaTimeLowElapsed = Math.max(stats.inQuotaTimeLowElapsed, inQuotaTime); + stats.inQuotaTimeMinElapsed = Math.max(stats.inQuotaTimeMinElapsed, inQuotaTime); } if (stats.bgJobCountInWindow >= stats.jobCountLimit) { - stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, - nowElapsed + stats.windowSizeMs); + final long inQuotaTime = nowElapsed + stats.windowSizeMs; + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime); + stats.inQuotaTimeLowElapsed = Math.max(stats.inQuotaTimeLowElapsed, inQuotaTime); + stats.inQuotaTimeMinElapsed = Math.max(stats.inQuotaTimeMinElapsed, inQuotaTime); } } @@ -1267,9 +1351,23 @@ public final class QuotaController extends StateController { start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs + stats.windowSizeMs); } + if (stats.executionTimeInWindowMs >= allowedTimeLowMs) { + stats.inQuotaTimeLowElapsed = Math.max(stats.inQuotaTimeLowElapsed, + start + stats.executionTimeInWindowMs - allowedTimeLowMs + + stats.windowSizeMs); + } + if (stats.executionTimeInWindowMs >= allowedTimeMinMs) { + stats.inQuotaTimeMinElapsed = Math.max(stats.inQuotaTimeMinElapsed, + start + stats.executionTimeInWindowMs - allowedTimeMinMs + + stats.windowSizeMs); + } if (stats.bgJobCountInWindow >= stats.jobCountLimit) { - stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, - session.endTimeElapsed + stats.windowSizeMs); + final long inQuotaTime = session.endTimeElapsed + stats.windowSizeMs; + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime); + stats.inQuotaTimeLowElapsed = Math.max(stats.inQuotaTimeLowElapsed, + inQuotaTime); + stats.inQuotaTimeMinElapsed = Math.max(stats.inQuotaTimeMinElapsed, + inQuotaTime); } if (i == loopStart || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) @@ -1278,8 +1376,12 @@ public final class QuotaController extends StateController { sessionCountInWindow++; if (sessionCountInWindow >= stats.sessionCountLimit) { - stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, - session.endTimeElapsed + stats.windowSizeMs); + final long inQuotaTime = session.endTimeElapsed + stats.windowSizeMs; + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime); + stats.inQuotaTimeLowElapsed = Math.max(stats.inQuotaTimeLowElapsed, + inQuotaTime); + stats.inQuotaTimeMinElapsed = Math.max(stats.inQuotaTimeMinElapsed, + inQuotaTime); } } } @@ -1425,10 +1527,9 @@ public final class QuotaController extends StateController { synchronized (mLock) { final long nowElapsed = sElapsedRealtimeClock.millis(); final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName); - if (transactQuotaLocked(userId, packageName, nowElapsed, quota, credit) - && maybeUpdateConstraintForPkgLocked(nowElapsed, userId, packageName)) { - mStateChangedListener - .onControllerStateChanged(mTrackedJobs.get(userId, packageName)); + if (transactQuotaLocked(userId, packageName, nowElapsed, quota, credit)) { + mStateChangedListener.onControllerStateChanged( + maybeUpdateConstraintForPkgLocked(nowElapsed, userId, packageName)); } } } @@ -1558,9 +1659,8 @@ public final class QuotaController extends StateController { final int userId = mTrackedJobs.keyAt(u); for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) { final String packageName = mTrackedJobs.keyAt(u, p); - if (maybeUpdateConstraintForPkgLocked(nowElapsed, userId, packageName)) { - changedJobs.addAll(mTrackedJobs.valueAt(u, p)); - } + changedJobs.addAll( + maybeUpdateConstraintForPkgLocked(nowElapsed, userId, packageName)); } } if (changedJobs.size() > 0) { @@ -1573,18 +1673,20 @@ public final class QuotaController extends StateController { * * @return true if at least one job had its bit changed */ - private boolean maybeUpdateConstraintForPkgLocked(final long nowElapsed, final int userId, - @NonNull final String packageName) { + @NonNull + private ArraySet maybeUpdateConstraintForPkgLocked(final long nowElapsed, + final int userId, @NonNull final String packageName) { ArraySet jobs = mTrackedJobs.get(userId, packageName); + final ArraySet changedJobs = new ArraySet<>(); if (jobs == null || jobs.size() == 0) { - return false; + return changedJobs; } // Quota is the same for all jobs within a package. final int realStandbyBucket = jobs.valueAt(0).getStandbyBucket(); - final boolean realInQuota = isWithinQuotaLocked(userId, packageName, realStandbyBucket); + final boolean realInQuota = isWithinQuotaLocked( + userId, packageName, realStandbyBucket, JobInfo.PRIORITY_DEFAULT); boolean outOfEJQuota = false; - boolean changed = false; for (int i = jobs.size() - 1; i >= 0; --i) { final JobStatus js = jobs.valueAt(i); final boolean isWithinEJQuota = @@ -1592,21 +1694,30 @@ public final class QuotaController extends StateController { if (isTopStartedJobLocked(js)) { // Job was started while the app was in the TOP state so we should allow it to // finish. - changed |= js.setQuotaConstraintSatisfied(nowElapsed, true); + if (js.setQuotaConstraintSatisfied(nowElapsed, true)) { + changedJobs.add(js); + } } else if (realStandbyBucket != ACTIVE_INDEX - && realStandbyBucket == js.getEffectiveStandbyBucket()) { + && realStandbyBucket == js.getEffectiveStandbyBucket() + && js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT) { // An app in the ACTIVE bucket may be out of quota while the job could be in quota // for some reason. Therefore, avoid setting the real value here and check each job // individually. - changed |= setConstraintSatisfied(js, nowElapsed, isWithinEJQuota || realInQuota); + if (setConstraintSatisfied(js, nowElapsed, isWithinEJQuota || realInQuota)) { + changedJobs.add(js); + } } else { // This job is somehow exempted. Need to determine its own quota status. - changed |= setConstraintSatisfied(js, nowElapsed, - isWithinEJQuota || isWithinQuotaLocked(js)); + if (setConstraintSatisfied(js, nowElapsed, + isWithinEJQuota || isWithinQuotaLocked(js))) { + changedJobs.add(js); + } } if (js.isRequestedExpeditedJob()) { - changed |= setExpeditedQuotaApproved(js, nowElapsed, isWithinEJQuota); + if (setExpeditedQuotaApproved(js, nowElapsed, isWithinEJQuota)) { + changedJobs.add(js); + } outOfEJQuota |= !isWithinEJQuota; } } @@ -1618,7 +1729,7 @@ public final class QuotaController extends StateController { } else { mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName)); } - return changed; + return changedJobs; } private class UidConstraintUpdater implements Consumer { @@ -1651,9 +1762,9 @@ public final class QuotaController extends StateController { final int userId = jobStatus.getSourceUserId(); final String packageName = jobStatus.getSourcePackageName(); final int realStandbyBucket = jobStatus.getStandbyBucket(); - if (isWithinQuotaLocked(userId, packageName, realStandbyBucket) && isWithinEJQuota) { - // TODO(141645789): we probably shouldn't cancel the alarm until we've verified - // that all jobs for the userId-package are within quota. + if (isWithinEJQuota + && isWithinQuotaLocked(userId, packageName, realStandbyBucket, + JobInfo.PRIORITY_MIN)) { mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName)); } else { mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket); @@ -1700,16 +1811,41 @@ public final class QuotaController extends StateController { return; } + ArraySet jobs = mTrackedJobs.get(userId, packageName); + if (jobs == null || jobs.size() == 0) { + Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + + packageToString(userId, packageName) + " that has no jobs"); + mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName)); + return; + } + ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket); final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats, standbyBucket); final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName); - final boolean inRegularQuota = stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs - && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs - && isUnderJobCountQuota - && isUnderTimingSessionCountQuota; + int minPriority = JobInfo.PRIORITY_MAX; + boolean hasDefPlus = false, hasLow = false, hasMin = false; + for (int i = jobs.size() - 1; i >= 0; --i) { + final int priority = jobs.valueAt(i).getEffectivePriority(); + minPriority = Math.min(minPriority, priority); + if (priority <= JobInfo.PRIORITY_MIN) { + hasMin = true; + } else if (priority <= JobInfo.PRIORITY_LOW) { + hasLow = true; + } else { + hasDefPlus = true; + } + if (hasMin && hasLow && hasDefPlus) { + break; + } + } + final boolean inRegularQuota = + stats.executionTimeInWindowMs < getAllowedTimePerPeriodMs(minPriority) + && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs + && isUnderJobCountQuota + && isUnderTimingSessionCountQuota; if (inRegularQuota && remainingEJQuota > 0) { // Already in quota. Why was this method called? if (DEBUG) { @@ -1728,7 +1864,24 @@ public final class QuotaController extends StateController { long inEJQuotaTimeElapsed = Long.MAX_VALUE; if (!inRegularQuota) { // The time this app will have quota again. - long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; + long executionInQuotaTime = Long.MAX_VALUE; + boolean hasExecutionInQuotaTime = false; + if (hasMin && stats.inQuotaTimeMinElapsed > 0) { + executionInQuotaTime = Math.min(executionInQuotaTime, stats.inQuotaTimeMinElapsed); + hasExecutionInQuotaTime = true; + } + if (hasLow && stats.inQuotaTimeLowElapsed > 0) { + executionInQuotaTime = Math.min(executionInQuotaTime, stats.inQuotaTimeLowElapsed); + hasExecutionInQuotaTime = true; + } + if (hasDefPlus && stats.inQuotaTimeElapsed > 0) { + executionInQuotaTime = Math.min(executionInQuotaTime, stats.inQuotaTimeElapsed); + hasExecutionInQuotaTime = true; + } + long inQuotaTimeElapsed = 0; + if (hasExecutionInQuotaTime) { + inQuotaTimeElapsed = executionInQuotaTime; + } if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { // App hit the rate limit. inQuotaTimeElapsed = @@ -1941,6 +2094,7 @@ public final class QuotaController extends StateController { private final ArraySet mRunningBgJobs = new ArraySet<>(); private long mStartTimeElapsed; private int mBgJobCount; + private int mLowestPriority = JobInfo.PRIORITY_MAX; private long mDebitAdjustment; Timer(int uid, int userId, String packageName, boolean regularJobTimer) { @@ -1963,6 +2117,7 @@ public final class QuotaController extends StateController { Slog.v(TAG, "Starting to track " + jobStatus.toShortString()); } // Always maintain list of running jobs, even when quota is free. + mLowestPriority = Math.min(mLowestPriority, jobStatus.getEffectivePriority()); if (mRunningBgJobs.add(jobStatus) && shouldTrackLocked()) { mBgJobCount++; if (mRegularJobTimer) { @@ -2002,6 +2157,13 @@ public final class QuotaController extends StateController { && !isQuotaFreeLocked(standbyBucket)) { emitSessionLocked(nowElapsed); cancelCutoff(); + mLowestPriority = JobInfo.PRIORITY_MAX; + } else if (mLowestPriority == jobStatus.getEffectivePriority()) { + mLowestPriority = JobInfo.PRIORITY_MAX; + for (int i = mRunningBgJobs.size() - 1; i >= 0; --i) { + mLowestPriority = Math.min(mLowestPriority, + mRunningBgJobs.valueAt(i).getEffectivePriority()); + } } } } @@ -2128,9 +2290,14 @@ public final class QuotaController extends StateController { } Message msg = mHandler.obtainMessage( mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg); - final long timeRemainingMs = mRegularJobTimer - ? getTimeUntilQuotaConsumedLocked(mPkg.userId, mPkg.packageName) - : getTimeUntilEJQuotaConsumedLocked(mPkg.userId, mPkg.packageName); + final long timeRemainingMs; + if (mRegularJobTimer) { + timeRemainingMs = getTimeUntilQuotaConsumedLocked( + mPkg.userId, mPkg.packageName, mLowestPriority); + } else { + timeRemainingMs = + getTimeUntilEJQuotaConsumedLocked(mPkg.userId, mPkg.packageName); + } if (DEBUG) { Slog.i(TAG, (mRegularJobTimer ? "Regular job" : "EJ") + " for " + mPkg + " has " @@ -2250,11 +2417,10 @@ public final class QuotaController extends StateController { final ShrinkableDebits debits = getEJDebitsLocked(mPkg.userId, mPkg.packageName); if (transactQuotaLocked(mPkg.userId, mPkg.packageName, - nowElapsed, debits, pendingReward) - && maybeUpdateConstraintForPkgLocked(nowElapsed, - mPkg.userId, mPkg.packageName)) { + nowElapsed, debits, pendingReward)) { mStateChangedListener.onControllerStateChanged( - mTrackedJobs.get(mPkg.userId, mPkg.packageName)); + maybeUpdateConstraintForPkgLocked(nowElapsed, + mPkg.userId, mPkg.packageName)); } } break; @@ -2356,11 +2522,9 @@ public final class QuotaController extends StateController { if (timer != null && timer.isActive()) { timer.rescheduleCutoff(); } - if (maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(), - userId, packageName)) { - mStateChangedListener - .onControllerStateChanged(mTrackedJobs.get(userId, packageName)); - } + mStateChangedListener.onControllerStateChanged( + maybeUpdateConstraintForPkgLocked( + sElapsedRealtimeClock.millis(), userId, packageName)); } if (restrictedChanges.size() > 0) { mStateChangedListener.onRestrictedBucketChanged(restrictedChanges); @@ -2486,27 +2650,19 @@ public final class QuotaController extends StateController { Slog.d(TAG, "Checking if " + pkg + " has reached its quota."); } - long timeRemainingMs = getRemainingExecutionTimeLocked(pkg.userId, - pkg.packageName); - if (timeRemainingMs <= 50) { - // Less than 50 milliseconds left. Start process of shutting down jobs. + final ArraySet changedJobs = maybeUpdateConstraintForPkgLocked( + sElapsedRealtimeClock.millis(), pkg.userId, pkg.packageName); + if (changedJobs.size() > 0) { if (DEBUG) Slog.d(TAG, pkg + " has reached its quota."); - if (maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(), - pkg.userId, pkg.packageName)) { - mStateChangedListener.onControllerStateChanged( - mTrackedJobs.get(pkg.userId, pkg.packageName)); - } + mStateChangedListener.onControllerStateChanged(changedJobs); } else { // This could potentially happen if an old session phases out while a // job is currently running. // Reschedule message - Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg); - timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId, - pkg.packageName); if (DEBUG) { - Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left."); + Slog.d(TAG, pkg + " had early REACHED_QUOTA message"); } - sendMessageDelayed(rescheduleMsg, timeRemainingMs); + mPkgTimers.get(pkg.userId, pkg.packageName).scheduleCutoff(); } break; } @@ -2516,26 +2672,19 @@ public final class QuotaController extends StateController { Slog.d(TAG, "Checking if " + pkg + " has reached its EJ quota."); } - long timeRemainingMs = getRemainingEJExecutionTimeLocked( - pkg.userId, pkg.packageName); - if (timeRemainingMs <= 0) { + final ArraySet changedJobs = maybeUpdateConstraintForPkgLocked( + sElapsedRealtimeClock.millis(), pkg.userId, pkg.packageName); + if (changedJobs.size() > 0) { if (DEBUG) Slog.d(TAG, pkg + " has reached its EJ quota."); - if (maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(), - pkg.userId, pkg.packageName)) { - mStateChangedListener.onControllerStateChanged( - mTrackedJobs.get(pkg.userId, pkg.packageName)); - } + mStateChangedListener.onControllerStateChanged(changedJobs); } else { // This could potentially happen if an old session phases out while a // job is currently running. // Reschedule message - Message rescheduleMsg = obtainMessage(MSG_REACHED_EJ_QUOTA, pkg); - timeRemainingMs = getTimeUntilEJQuotaConsumedLocked( - pkg.userId, pkg.packageName); if (DEBUG) { - Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left for EJ"); + Slog.d(TAG, pkg + " had early REACHED_EJ_QUOTA message"); } - sendMessageDelayed(rescheduleMsg, timeRemainingMs); + mEJPkgTimers.get(pkg.userId, pkg.packageName).scheduleCutoff(); } break; } @@ -2553,11 +2702,9 @@ public final class QuotaController extends StateController { if (DEBUG) { Slog.d(TAG, "Checking pkg " + packageToString(userId, packageName)); } - if (maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(), - userId, packageName)) { - mStateChangedListener.onControllerStateChanged( - mTrackedJobs.get(userId, packageName)); - } + mStateChangedListener.onControllerStateChanged( + maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(), + userId, packageName)); break; } case MSG_UID_PROCESS_STATE_CHANGED: { @@ -2781,6 +2928,12 @@ public final class QuotaController extends StateController { static final String KEY_IN_QUOTA_BUFFER_MS = QC_CONSTANT_PREFIX + "in_quota_buffer_ms"; @VisibleForTesting + static final String KEY_ALLOWED_TIME_SURPLUS_PRIORITY_LOW = + QC_CONSTANT_PREFIX + "allowed_time_surplus_priority_low"; + @VisibleForTesting + static final String KEY_ALLOWED_TIME_SURPLUS_PRIORITY_MIN = + QC_CONSTANT_PREFIX + "allowed_time_surplus_priority_min"; + @VisibleForTesting static final String KEY_WINDOW_SIZE_ACTIVE_MS = QC_CONSTANT_PREFIX + "window_size_active_ms"; @VisibleForTesting @@ -2890,6 +3043,8 @@ public final class QuotaController extends StateController { 10 * 60 * 1000L; // 10 minutes private static final long DEFAULT_IN_QUOTA_BUFFER_MS = 30 * 1000L; // 30 seconds + private static final float DEFAULT_ALLOWED_TIME_SURPLUS_PRIORITY_LOW = .25f; + private static final float DEFAULT_ALLOWED_TIME_SURPLUS_PRIORITY_MIN = .5f; private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time private static final long DEFAULT_WINDOW_SIZE_WORKING_MS = @@ -2950,6 +3105,22 @@ public final class QuotaController extends StateController { */ public long IN_QUOTA_BUFFER_MS = DEFAULT_IN_QUOTA_BUFFER_MS; + /** + * The percentage of {@link #ALLOWED_TIME_PER_PERIOD_MS} that should not be used by + * {@link JobInfo#PRIORITY_LOW low priority} jobs. In other words, there must be a minimum + * surplus of this amount of remaining allowed time before we start running low priority + * jobs. + */ + public float ALLOWED_TIME_SURPLUS_PRIORITY_LOW = DEFAULT_ALLOWED_TIME_SURPLUS_PRIORITY_LOW; + + /** + * The percentage of {@link #ALLOWED_TIME_PER_PERIOD_MS} that should not be used by + * {@link JobInfo#PRIORITY_MIN low priority} jobs. In other words, there must be a minimum + * surplus of this amount of remaining allowed time before we start running min priority + * jobs. + */ + public float ALLOWED_TIME_SURPLUS_PRIORITY_MIN = DEFAULT_ALLOWED_TIME_SURPLUS_PRIORITY_MIN; + /** * The quota window size of the particular standby bucket. Apps in this standby bucket are * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past @@ -3188,6 +3359,8 @@ public final class QuotaController extends StateController { @NonNull String key) { switch (key) { case KEY_ALLOWED_TIME_PER_PERIOD_MS: + case KEY_ALLOWED_TIME_SURPLUS_PRIORITY_LOW: + case KEY_ALLOWED_TIME_SURPLUS_PRIORITY_MIN: case KEY_IN_QUOTA_BUFFER_MS: case KEY_MAX_EXECUTION_TIME_MS: case KEY_WINDOW_SIZE_ACTIVE_MS: @@ -3407,6 +3580,7 @@ public final class QuotaController extends StateController { final DeviceConfig.Properties properties = DeviceConfig.getProperties( DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_ALLOWED_TIME_PER_PERIOD_MS, KEY_IN_QUOTA_BUFFER_MS, + KEY_ALLOWED_TIME_SURPLUS_PRIORITY_LOW, KEY_ALLOWED_TIME_SURPLUS_PRIORITY_MIN, KEY_MAX_EXECUTION_TIME_MS, KEY_WINDOW_SIZE_ACTIVE_MS, KEY_WINDOW_SIZE_WORKING_MS, KEY_WINDOW_SIZE_FREQUENT_MS, KEY_WINDOW_SIZE_RARE_MS, @@ -3414,6 +3588,12 @@ public final class QuotaController extends StateController { ALLOWED_TIME_PER_PERIOD_MS = properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_MS, DEFAULT_ALLOWED_TIME_PER_PERIOD_MS); + ALLOWED_TIME_SURPLUS_PRIORITY_LOW = + properties.getFloat(KEY_ALLOWED_TIME_SURPLUS_PRIORITY_LOW, + DEFAULT_ALLOWED_TIME_SURPLUS_PRIORITY_LOW); + ALLOWED_TIME_SURPLUS_PRIORITY_MIN = + properties.getFloat(KEY_ALLOWED_TIME_SURPLUS_PRIORITY_MIN, + DEFAULT_ALLOWED_TIME_SURPLUS_PRIORITY_MIN); IN_QUOTA_BUFFER_MS = properties.getLong(KEY_IN_QUOTA_BUFFER_MS, DEFAULT_IN_QUOTA_BUFFER_MS); MAX_EXECUTION_TIME_MS = properties.getLong(KEY_MAX_EXECUTION_TIME_MS, @@ -3455,6 +3635,23 @@ public final class QuotaController extends StateController { mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; mShouldReevaluateConstraints = true; } + // Low priority surplus should be in the range [0, .9]. A value of 1 would essentially + // mean never run low priority jobs. + float newAllowedTimeSurplusPriorityLow = + Math.max(0f, Math.min(.9f, ALLOWED_TIME_SURPLUS_PRIORITY_LOW)); + if (Float.compare( + mAllowedTimeSurplusPriorityLow, newAllowedTimeSurplusPriorityLow) != 0) { + mAllowedTimeSurplusPriorityLow = newAllowedTimeSurplusPriorityLow; + mShouldReevaluateConstraints = true; + } + // Min priority surplus should be in the range [0, mAllowedTimeSurplusPriorityLow]. + float newAllowedTimeSurplusPriorityMin = Math.max(0f, + Math.min(mAllowedTimeSurplusPriorityLow, ALLOWED_TIME_SURPLUS_PRIORITY_MIN)); + if (Float.compare( + mAllowedTimeSurplusPriorityMin, newAllowedTimeSurplusPriorityMin) != 0) { + mAllowedTimeSurplusPriorityMin = newAllowedTimeSurplusPriorityMin; + mShouldReevaluateConstraints = true; + } long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs, Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS)); if (mBucketPeriodsMs[ACTIVE_INDEX] != newActivePeriodMs) { @@ -3627,6 +3824,10 @@ public final class QuotaController extends StateController { pw.println("QuotaController:"); pw.increaseIndent(); pw.print(KEY_ALLOWED_TIME_PER_PERIOD_MS, ALLOWED_TIME_PER_PERIOD_MS).println(); + pw.print(KEY_ALLOWED_TIME_SURPLUS_PRIORITY_LOW, ALLOWED_TIME_SURPLUS_PRIORITY_LOW) + .println(); + pw.print(KEY_ALLOWED_TIME_SURPLUS_PRIORITY_MIN, ALLOWED_TIME_SURPLUS_PRIORITY_MIN) + .println(); pw.print(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println(); pw.print(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println(); pw.print(KEY_WINDOW_SIZE_WORKING_MS, WINDOW_SIZE_WORKING_MS).println(); @@ -3749,6 +3950,16 @@ public final class QuotaController extends StateController { return mAllowedTimePerPeriodMs; } + @VisibleForTesting + float getAllowedTimeSurplusPriorityLow() { + return mAllowedTimeSurplusPriorityLow; + } + + @VisibleForTesting + float getAllowedTimeSurplusPriorityMin() { + return mAllowedTimeSurplusPriorityMin; + } + @VisibleForTesting @NonNull int[] getBucketMaxJobCounts() { diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index cfae9a3d586a3..153ce17ec9dd4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -16,6 +16,11 @@ package com.android.server.job.controllers; +import static android.app.job.JobInfo.PRIORITY_DEFAULT; +import static android.app.job.JobInfo.PRIORITY_HIGH; +import static android.app.job.JobInfo.PRIORITY_LOW; +import static android.app.job.JobInfo.PRIORITY_MIN; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -269,14 +274,14 @@ public class QuotaControllerTest { } private void setCharging() { - doReturn(true).when(mJobSchedulerService).isBatteryCharging(); + when(mJobSchedulerService.isBatteryCharging()).thenReturn(true); synchronized (mQuotaController.mLock) { mQuotaController.onBatteryStateChangedLocked(); } } private void setDischarging() { - doReturn(false).when(mJobSchedulerService).isBatteryCharging(); + when(mJobSchedulerService.isBatteryCharging()).thenReturn(false); synchronized (mQuotaController.mLock) { mQuotaController.onBatteryStateChangedLocked(); } @@ -407,6 +412,14 @@ public class QuotaControllerTest { } } + private void setDeviceConfigFloat(String key, float val) { + mDeviceConfigPropertiesBuilder.setFloat(key, val); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForUpdatedConstantsLocked(); + mQcConstants.processConstantLocked(mDeviceConfigPropertiesBuilder.build(), key); + } + } + private void waitForNonDelayedMessagesProcessed() { mQuotaController.getHandler().runWithScissors(() -> {}, 15_000); } @@ -839,7 +852,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, inputStats); assertEquals(expectedStats, inputStats); assertTrue(mQuotaController.isWithinQuotaLocked( - SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX)); + SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX, PRIORITY_DEFAULT)); } assertTrue("Job not ready: " + jobStatus, jobStatus.isReady()); } @@ -863,7 +876,7 @@ public class QuotaControllerTest { assertEquals(expectedStats, inputStats); assertFalse( mQuotaController.isWithinQuotaLocked( - SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX)); + SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX, PRIORITY_DEFAULT)); } // Quota should be exceeded due to activity in active timer. @@ -888,7 +901,7 @@ public class QuotaControllerTest { assertEquals(expectedStats, inputStats); assertFalse( mQuotaController.isWithinQuotaLocked( - SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX)); + SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX, PRIORITY_DEFAULT)); assertFalse("Job unexpectedly ready: " + jobStatus, jobStatus.isReady()); } } @@ -1484,7 +1497,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE)); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( - SOURCE_USER_ID, SOURCE_PACKAGE)); + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); } setStandbyBucket(FREQUENT_INDEX); @@ -1494,7 +1507,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE)); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( - SOURCE_USER_ID, SOURCE_PACKAGE)); + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); } setStandbyBucket(WORKING_INDEX); @@ -1504,7 +1517,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE)); assertEquals(7 * MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( - SOURCE_USER_ID, SOURCE_PACKAGE)); + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); } // ACTIVE window = allowed time, so jobs can essentially run non-stop until they reach the @@ -1516,7 +1529,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE)); assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 9 * MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( - SOURCE_USER_ID, SOURCE_PACKAGE)); + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); } } @@ -1540,7 +1553,7 @@ public class QuotaControllerTest { // Max time will phase out, so should use bucket limit. assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( - SOURCE_USER_ID, SOURCE_PACKAGE)); + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); } mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear(); @@ -1556,7 +1569,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE)); assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( - SOURCE_USER_ID, SOURCE_PACKAGE)); + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); } mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear(); @@ -1573,7 +1586,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE)); assertEquals(3 * MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( - SOURCE_USER_ID, SOURCE_PACKAGE)); + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); } } @@ -1606,7 +1619,7 @@ public class QuotaControllerTest { // window time. assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( - SOURCE_USER_ID, SOURCE_PACKAGE)); + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); } mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear(); @@ -1633,15 +1646,115 @@ public class QuotaControllerTest { // Max time only has one minute phase out. Bucket time has 2 minute phase out. assertEquals(9 * MINUTE_IN_MILLIS, mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); + } + } + + /** + * Test getTimeUntilQuotaConsumedLocked when the determination is based on the job's priority. + */ + @Test + public void testGetTimeUntilQuotaConsumedLocked_Priority() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + // Close to RARE boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (24 * HOUR_IN_MILLIS - 30 * SECOND_IN_MILLIS), + 150 * SECOND_IN_MILLIS, 5), false); + // Far away from FREQUENT boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (7 * HOUR_IN_MILLIS), 2 * MINUTE_IN_MILLIS, 5), false); + // Overlap WORKING_SET boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), + 2 * MINUTE_IN_MILLIS, 5), false); + // Close to ACTIVE boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + + setStandbyBucket(RARE_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(30 * SECOND_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(3 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_HIGH)); + assertEquals(3 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); + assertEquals(0, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_LOW)); + assertEquals(0, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_MIN)); + } + + setStandbyBucket(FREQUENT_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(3 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(3 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_HIGH)); + assertEquals(3 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); + assertEquals(30 * SECOND_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_LOW)); + assertEquals(0, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_MIN)); + } + + setStandbyBucket(WORKING_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(6 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(7 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_HIGH)); + assertEquals(7 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); + assertEquals(4 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_LOW)); + assertEquals(2 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_MIN)); + } + + // ACTIVE window = allowed time, so jobs can essentially run non-stop until they reach the + // max execution time. + setStandbyBucket(ACTIVE_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(7 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 7 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_HIGH)); + assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 7 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_DEFAULT)); + assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 7 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_LOW)); + assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 7 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, PRIORITY_MIN)); } } @Test public void testIsWithinQuotaLocked_NeverApp() { synchronized (mQuotaController.mLock) { - assertFalse( - mQuotaController.isWithinQuotaLocked(0, "com.android.test.never", NEVER_INDEX)); + assertFalse(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test.never", NEVER_INDEX, PRIORITY_DEFAULT)); } } @@ -1649,7 +1762,8 @@ public class QuotaControllerTest { public void testIsWithinQuotaLocked_Charging() { setCharging(); synchronized (mQuotaController.mLock) { - assertTrue(mQuotaController.isWithinQuotaLocked(0, "com.android.test", RARE_INDEX)); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", RARE_INDEX, PRIORITY_DEFAULT)); } } @@ -1663,7 +1777,8 @@ public class QuotaControllerTest { createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); synchronized (mQuotaController.mLock) { mQuotaController.incrementJobCountLocked(0, "com.android.test", 5); - assertTrue(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX)); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", WORKING_INDEX, PRIORITY_DEFAULT)); } } @@ -1680,7 +1795,7 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { mQuotaController.incrementJobCountLocked(0, "com.android.test.spam", jobCount); assertFalse(mQuotaController.isWithinQuotaLocked( - 0, "com.android.test.spam", WORKING_INDEX)); + 0, "com.android.test.spam", WORKING_INDEX, PRIORITY_DEFAULT)); } mQuotaController.saveTimingSession(0, "com.android.test.frequent", @@ -1690,7 +1805,7 @@ public class QuotaControllerTest { createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 500), false); synchronized (mQuotaController.mLock) { assertFalse(mQuotaController.isWithinQuotaLocked( - 0, "com.android.test.frequent", FREQUENT_INDEX)); + 0, "com.android.test.frequent", FREQUENT_INDEX, PRIORITY_DEFAULT)); } } @@ -1706,7 +1821,8 @@ public class QuotaControllerTest { createTimingSession(now - (5 * MINUTE_IN_MILLIS), 4 * MINUTE_IN_MILLIS, 5), false); synchronized (mQuotaController.mLock) { mQuotaController.incrementJobCountLocked(0, "com.android.test", 5); - assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX)); + assertFalse(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", WORKING_INDEX, PRIORITY_DEFAULT)); } } @@ -1722,7 +1838,8 @@ public class QuotaControllerTest { false); synchronized (mQuotaController.mLock) { mQuotaController.incrementJobCountLocked(0, "com.android.test", jobCount); - assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX)); + assertFalse(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", WORKING_INDEX, PRIORITY_DEFAULT)); } } @@ -1875,21 +1992,65 @@ public class QuotaControllerTest { assertEquals("Rare has incorrect quota status with " + (i + 1) + " sessions", i < 2, - mQuotaController.isWithinQuotaLocked(0, "com.android.test", RARE_INDEX)); + mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", RARE_INDEX, PRIORITY_DEFAULT)); assertEquals("Frequent has incorrect quota status with " + (i + 1) + " sessions", i < 3, mQuotaController.isWithinQuotaLocked( - 0, "com.android.test", FREQUENT_INDEX)); + 0, "com.android.test", FREQUENT_INDEX, PRIORITY_DEFAULT)); assertEquals("Working has incorrect quota status with " + (i + 1) + " sessions", i < 4, - mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX)); + mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", WORKING_INDEX, PRIORITY_DEFAULT)); assertEquals("Active has incorrect quota status with " + (i + 1) + " sessions", i < 5, - mQuotaController.isWithinQuotaLocked(0, "com.android.test", ACTIVE_INDEX)); + mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", ACTIVE_INDEX, PRIORITY_DEFAULT)); } } } + @Test + public void testIsWithinQuotaLocked_Priority() { + setDischarging(); + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + synchronized (mQuotaController.mLock) { + mQuotaController.incrementJobCountLocked(0, "com.android.test", 5); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", FREQUENT_INDEX, PRIORITY_HIGH)); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", FREQUENT_INDEX, PRIORITY_DEFAULT)); + assertFalse(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", FREQUENT_INDEX, PRIORITY_LOW)); + assertFalse(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", FREQUENT_INDEX, PRIORITY_MIN)); + + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", WORKING_INDEX, PRIORITY_HIGH)); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", WORKING_INDEX, PRIORITY_DEFAULT)); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", WORKING_INDEX, PRIORITY_LOW)); + assertFalse(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", WORKING_INDEX, PRIORITY_MIN)); + + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", ACTIVE_INDEX, PRIORITY_HIGH)); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", ACTIVE_INDEX, PRIORITY_DEFAULT)); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", ACTIVE_INDEX, PRIORITY_LOW)); + assertTrue(mQuotaController.isWithinQuotaLocked( + 0, "com.android.test", ACTIVE_INDEX, PRIORITY_MIN)); + } + } + @Test public void testIsWithinEJQuotaLocked_NeverApp() { JobStatus js = createExpeditedJobStatus("testIsWithinEJQuotaLocked_NeverApp", 1); @@ -2116,6 +2277,12 @@ public class QuotaControllerTest { final int standbyBucket = ACTIVE_INDEX; setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND); + JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1); + setStandbyBucket(standbyBucket, jobStatus); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + } + // No sessions saved yet. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( @@ -2150,10 +2317,7 @@ public class QuotaControllerTest { verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); - JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1); - setStandbyBucket(standbyBucket, jobStatus); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); mQuotaController.prepareForExecutionLocked(jobStatus); } advanceElapsedClock(5 * MINUTE_IN_MILLIS); @@ -2179,19 +2343,24 @@ public class QuotaControllerTest { // Working set window size is 2 hours. final int standbyBucket = WORKING_INDEX; + JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_WorkingSet", 1); + setStandbyBucket(standbyBucket, jobStatus); synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); // No sessions saved yet. - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); @@ -2201,37 +2370,41 @@ public class QuotaControllerTest { // Counting backwards, the quota will come back one minute before the end. final long expectedAlarmTime = end - MINUTE_IN_MILLIS + 2 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, new TimingSession(now - 2 * HOUR_IN_MILLIS, end, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false); - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - (50 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 30 * MINUTE_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); @@ -2244,22 +2417,29 @@ public class QuotaControllerTest { spyOn(mQuotaController); doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked( + createJobStatus("testMaybeScheduleStartAlarmLocked_Frequent", 1), null); + } + // Frequent window size is 8 hours. final int standbyBucket = FREQUENT_INDEX; // No sessions saved yet. synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); @@ -2267,37 +2447,41 @@ public class QuotaControllerTest { // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false); - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); @@ -2314,6 +2498,11 @@ public class QuotaControllerTest { spyOn(mQuotaController); doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked( + createJobStatus("testMaybeScheduleStartAlarmLocked_Never", 1), null); + } + // The app is really in the NEVER bucket but is elevated somehow (eg via uidActive). setStandbyBucket(NEVER_INDEX); final int effectiveStandbyBucket = FREQUENT_INDEX; @@ -2390,22 +2579,30 @@ public class QuotaControllerTest { // Rare window size is 24 hours. final int standbyBucket = RARE_INDEX; + JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Rare", 1); + setStandbyBucket(standbyBucket, jobStatus); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + } + // Prevent timing session throttling from affecting the test. setDeviceConfigInt(QcConstants.KEY_MAX_SESSION_COUNT_RARE, 50); // No sessions saved yet. synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 25 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); @@ -2417,42 +2614,168 @@ public class QuotaControllerTest { final long expectedAlarmTime = start + MINUTE_IN_MILLIS + 24 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false); - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, 2 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } + @Test + public void testMaybeScheduleStartAlarmLocked_Priority() { + // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests + // because it schedules an alarm too. Prevent it from doing so. + spyOn(mQuotaController); + doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + + setDeviceConfigInt(QcConstants.KEY_MAX_SESSION_COUNT_RARE, 5); + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (24 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 1), false); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1), false); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1), false); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1), false); + + InOrder inOrder = inOrder(mAlarmManager); + + JobStatus jobDef = createJobStatus("testMaybeScheduleStartAlarmLocked_Priority", + SOURCE_PACKAGE, CALLING_UID, + new JobInfo.Builder(1, new ComponentName(mContext, "TestQuotaJobService")) + .setPriority(PRIORITY_DEFAULT) + .build()); + JobStatus jobLow = createJobStatus("testMaybeScheduleStartAlarmLocked_Priority", + SOURCE_PACKAGE, CALLING_UID, + new JobInfo.Builder(2, new ComponentName(mContext, "TestQuotaJobService")) + .setPriority(PRIORITY_LOW) + .build()); + JobStatus jobMin = createJobStatus("testMaybeScheduleStartAlarmLocked_Priority", + SOURCE_PACKAGE, CALLING_UID, + new JobInfo.Builder(3, new ComponentName(mContext, "TestQuotaJobService")) + .setPriority(PRIORITY_MIN) + .build()); + + setStandbyBucket(RARE_INDEX, jobDef, jobLow, jobMin); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(jobMin, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX); + // Min job requires 5 mins of surplus. + long expectedAlarmTime = now + 23 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.maybeStartTrackingJobLocked(jobLow, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX); + // Low job requires 2.5 mins of surplus. + expectedAlarmTime = now + 17 * HOUR_IN_MILLIS + 90 * SECOND_IN_MILLIS; + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.maybeStartTrackingJobLocked(jobDef, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX); + // Default+ jobs require IN_QUOTA_BUFFER_MS. + expectedAlarmTime = now + mQcConstants.IN_QUOTA_BUFFER_MS; + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.maybeStopTrackingJobLocked(jobMin, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobLow, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobDef, null, false); + + setStandbyBucket(FREQUENT_INDEX, jobDef, jobLow, jobMin); + + mQuotaController.maybeStartTrackingJobLocked(jobMin, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX); + // Min job requires 5 mins of surplus. + expectedAlarmTime = now + 7 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.maybeStartTrackingJobLocked(jobLow, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX); + // Low job requires 2.5 mins of surplus. + expectedAlarmTime = now + HOUR_IN_MILLIS + 90 * SECOND_IN_MILLIS; + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.maybeStartTrackingJobLocked(jobDef, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX); + // Default+ jobs already have enough quota. + inOrder.verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.maybeStopTrackingJobLocked(jobMin, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobLow, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobDef, null, false); + + setStandbyBucket(WORKING_INDEX, jobDef, jobLow, jobMin); + + mQuotaController.maybeStartTrackingJobLocked(jobMin, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); + // Min job requires 5 mins of surplus. + expectedAlarmTime = now + HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.maybeStartTrackingJobLocked(jobLow, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); + // Low job has enough surplus. + inOrder.verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.maybeStartTrackingJobLocked(jobDef, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); + // Default+ jobs already have enough quota. + inOrder.verify(mAlarmManager, timeout(1000).times(0)).setWindow( + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + } + } + /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */ @Test public void testMaybeScheduleStartAlarmLocked_BucketChange() { @@ -2464,24 +2787,29 @@ public class QuotaControllerTest { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); // Affects rare bucket - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 12 * HOUR_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3), false); // Affects frequent and rare buckets - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 4 * HOUR_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3), false); // Affects working, frequent, and rare buckets final long outOfQuotaTime = now - HOUR_IN_MILLIS; - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(outOfQuotaTime, 7 * MINUTE_IN_MILLIS, 10), false); // Affects all buckets - mQuotaController.saveTimingSession(0, "com.android.test", + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 5 * MINUTE_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 3), false); InOrder inOrder = inOrder(mAlarmManager); + JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_BucketChange", 1); + // Start in ACTIVE bucket. + setStandbyBucket(ACTIVE_INDEX, jobStatus); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX); + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); @@ -2492,8 +2820,10 @@ public class QuotaControllerTest { final long expectedWorkingAlarmTime = outOfQuotaTime + (2 * HOUR_IN_MILLIS) + mQcConstants.IN_QUOTA_BUFFER_MS; + setStandbyBucket(WORKING_INDEX, jobStatus); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), @@ -2502,8 +2832,10 @@ public class QuotaControllerTest { final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS) + mQcConstants.IN_QUOTA_BUFFER_MS; + setStandbyBucket(FREQUENT_INDEX, jobStatus); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedFrequentAlarmTime), anyLong(), @@ -2512,29 +2844,37 @@ public class QuotaControllerTest { final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS) + mQcConstants.IN_QUOTA_BUFFER_MS; + setStandbyBucket(RARE_INDEX, jobStatus); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", RARE_INDEX); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // And back up again. + setStandbyBucket(FREQUENT_INDEX, jobStatus); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedFrequentAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + setStandbyBucket(WORKING_INDEX, jobStatus); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + setStandbyBucket(ACTIVE_INDEX, jobStatus); synchronized (mQuotaController.mLock) { - mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX); + mQuotaController.maybeScheduleStartAlarmLocked( + SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); @@ -2551,6 +2891,13 @@ public class QuotaControllerTest { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); final int standbyBucket = WORKING_INDEX; + + JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked", 1); + setStandbyBucket(standbyBucket, jobStatus); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + } + ExecutionStats stats; synchronized (mQuotaController.mLock) { stats = mQuotaController.getExecutionStatsLocked( @@ -2646,6 +2993,11 @@ public class QuotaControllerTest { spyOn(mQuotaController); doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked( + createJobStatus("testMaybeScheduleStartAlarmLocked", 1), null); + } + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); // Working set window size is 2 hours. final int standbyBucket = WORKING_INDEX; @@ -2671,13 +3023,17 @@ public class QuotaControllerTest { anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); } - private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck() { // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests // because it schedules an alarm too. Prevent it from doing so. spyOn(mQuotaController); doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked( + createJobStatus("testMaybeScheduleStartAlarmLocked", 1), null); + } + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); // Working set window size is 2 hours. final int standbyBucket = WORKING_INDEX; @@ -2708,6 +3064,8 @@ public class QuotaControllerTest { public void testConstantsUpdating_ValidValues() { setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_MS, 5 * MINUTE_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_IN_QUOTA_BUFFER_MS, 2 * MINUTE_IN_MILLIS); + setDeviceConfigFloat(QcConstants.KEY_ALLOWED_TIME_SURPLUS_PRIORITY_LOW, .7f); + setDeviceConfigFloat(QcConstants.KEY_ALLOWED_TIME_SURPLUS_PRIORITY_MIN, .2f); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_ACTIVE_MS, 15 * MINUTE_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_WORKING_MS, 30 * MINUTE_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_FREQUENT_MS, 45 * MINUTE_IN_MILLIS); @@ -2748,6 +3106,8 @@ public class QuotaControllerTest { assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs()); assertEquals(2 * MINUTE_IN_MILLIS, mQuotaController.getInQuotaBufferMs()); + assertEquals(.7f, mQuotaController.getAllowedTimeSurplusPriorityLow(), 1e-6); + assertEquals(.2f, mQuotaController.getAllowedTimeSurplusPriorityMin(), 1e-6); assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[ACTIVE_INDEX]); assertEquals(30 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]); assertEquals(45 * MINUTE_IN_MILLIS, @@ -2793,6 +3153,8 @@ public class QuotaControllerTest { // Test negatives/too low. setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_MS, -MINUTE_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_IN_QUOTA_BUFFER_MS, -MINUTE_IN_MILLIS); + setDeviceConfigFloat(QcConstants.KEY_ALLOWED_TIME_SURPLUS_PRIORITY_LOW, -.1f); + setDeviceConfigFloat(QcConstants.KEY_ALLOWED_TIME_SURPLUS_PRIORITY_MIN, -.01f); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_ACTIVE_MS, -MINUTE_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_WORKING_MS, -MINUTE_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_FREQUENT_MS, -MINUTE_IN_MILLIS); @@ -2831,6 +3193,8 @@ public class QuotaControllerTest { assertEquals(MINUTE_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs()); assertEquals(0, mQuotaController.getInQuotaBufferMs()); + assertEquals(0f, mQuotaController.getAllowedTimeSurplusPriorityLow(), 1e-6); + assertEquals(0f, mQuotaController.getAllowedTimeSurplusPriorityMin(), 1e-6); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[ACTIVE_INDEX]); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]); @@ -2878,6 +3242,8 @@ public class QuotaControllerTest { // Test larger than a day. Controller should cap at one day. setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_MS, 25 * HOUR_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_IN_QUOTA_BUFFER_MS, 25 * HOUR_IN_MILLIS); + setDeviceConfigFloat(QcConstants.KEY_ALLOWED_TIME_SURPLUS_PRIORITY_LOW, 1f); + setDeviceConfigFloat(QcConstants.KEY_ALLOWED_TIME_SURPLUS_PRIORITY_MIN, .95f); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_ACTIVE_MS, 25 * HOUR_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_WORKING_MS, 25 * HOUR_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_FREQUENT_MS, 25 * HOUR_IN_MILLIS); @@ -2905,6 +3271,8 @@ public class QuotaControllerTest { assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs()); assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getInQuotaBufferMs()); + assertEquals(.9f, mQuotaController.getAllowedTimeSurplusPriorityLow(), 1e-6); + assertEquals(.9f, mQuotaController.getAllowedTimeSurplusPriorityMin(), 1e-6); assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[ACTIVE_INDEX]); assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]); assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]); @@ -3669,8 +4037,8 @@ public class QuotaControllerTest { // Wait for some extra time to allow for job processing. inOrder.verify(mJobSchedulerService, - timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) - .onControllerStateChanged(any()); + timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) + .onControllerStateChanged(argThat(jobs -> jobs.size() > 0)); synchronized (mQuotaController.mLock) { assertEquals(remainingTimeMs / 2, mQuotaController.getRemainingExecutionTimeLocked(jobBg)); @@ -3681,8 +4049,8 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); advanceElapsedClock(remainingTimeMs / 2 + 1); inOrder.verify(mJobSchedulerService, - timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1)) - .onControllerStateChanged(any()); + timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 1)); // Top job should still be allowed to run. assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); @@ -3696,7 +4064,7 @@ public class QuotaControllerTest { advanceElapsedClock(20 * SECOND_IN_MILLIS); setProcessState(ActivityManager.PROCESS_STATE_TOP); inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) - .onControllerStateChanged(any()); + .onControllerStateChanged(argThat(jobs -> jobs.size() == 1)); trackJobs(jobFg, jobTop); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobTop); @@ -3715,7 +4083,7 @@ public class QuotaControllerTest { advanceElapsedClock(20 * SECOND_IN_MILLIS); setProcessState(ActivityManager.PROCESS_STATE_SERVICE); inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) - .onControllerStateChanged(any()); + .onControllerStateChanged(argThat(jobs -> jobs.size() == 2)); // App is now in background and out of quota. Fg should now change to out of quota since it // wasn't started. Top should remain in quota since it started when the app was in TOP. assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); @@ -3753,7 +4121,7 @@ public class QuotaControllerTest { // Wait for some extra time to allow for job processing. verify(mJobSchedulerService, timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(1)) - .onControllerStateChanged(any()); + .onControllerStateChanged(argThat(jobs -> jobs.size() == 1)); assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); assertEquals(JobSchedulerService.sElapsedRealtimeClock.millis(), jobStatus.getWhenStandbyDeferred()); @@ -3797,7 +4165,7 @@ public class QuotaControllerTest { // Wait for some extra time to allow for job processing. verify(mJobSchedulerService, timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) - .onControllerStateChanged(any()); + .onControllerStateChanged(argThat(jobs -> jobs.size() > 0)); assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); // The job used up the remaining quota, but in that time, the same amount of time in the // old TimingSession also fell out of the quota window, so it should still have the same @@ -3819,7 +4187,7 @@ public class QuotaControllerTest { // Wait for some extra time to allow for job processing. verify(mJobSchedulerService, timeout(12 * SECOND_IN_MILLIS).times(1)) - .onControllerStateChanged(any()); + .onControllerStateChanged(argThat(jobs -> jobs.size() == 1)); assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); verify(handler, never()).sendMessageDelayed(any(), anyInt()); } @@ -4406,6 +4774,11 @@ public class QuotaControllerTest { spyOn(mQuotaController); doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked( + createJobStatus("testMaybeScheduleStartAlarmLocked_EJ", 1), null); + } + final int standbyBucket = WORKING_INDEX; setStandbyBucket(standbyBucket); setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_WORKING_MS, 20 * MINUTE_IN_MILLIS); @@ -4482,6 +4855,11 @@ public class QuotaControllerTest { spyOn(mQuotaController); doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked( + createJobStatus("testMaybeScheduleStartAlarmLocked_Ej_BucketChange", 1), null); + } + setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_ACTIVE_MS, 30 * MINUTE_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_WORKING_MS, 20 * MINUTE_IN_MILLIS); setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_FREQUENT_MS, 15 * MINUTE_IN_MILLIS); @@ -4590,6 +4968,11 @@ public class QuotaControllerTest { spyOn(mQuotaController); doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked( + createJobStatus("testMaybeScheduleStartAlarmLocked_Ej_SRQ", 1), null); + } + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); setStandbyBucket(WORKING_INDEX); final long contributionMs = mQcConstants.IN_QUOTA_BUFFER_MS / 2; @@ -5437,8 +5820,8 @@ public class QuotaControllerTest { // Wait for some extra time to allow for job processing. inOrder.verify(mJobSchedulerService, - timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) - .onControllerStateChanged(any()); + timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) + .onControllerStateChanged(argThat(jobs -> jobs.size() > 0)); synchronized (mQuotaController.mLock) { assertEquals(remainingTimeMs / 2, mQuotaController.getRemainingEJExecutionTimeLocked( @@ -5448,8 +5831,8 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); advanceElapsedClock(remainingTimeMs / 2 + 1); inOrder.verify(mJobSchedulerService, - timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1)) - .onControllerStateChanged(any()); + timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 2)); // Top should still be "in quota" since it started before the app ran on top out of quota. assertFalse(jobBg.isExpeditedQuotaApproved()); assertTrue(jobTop.isExpeditedQuotaApproved()); @@ -5473,7 +5856,7 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_TOP); // Confirm QC recognizes that jobUnstarted has changed from out-of-quota to in-quota. inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) - .onControllerStateChanged(any()); + .onControllerStateChanged(argThat(jobs -> jobs.size() == 2)); trackJobs(jobTop2, jobFg); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobTop2); @@ -5494,7 +5877,7 @@ public class QuotaControllerTest { advanceElapsedClock(20 * SECOND_IN_MILLIS); setProcessState(ActivityManager.PROCESS_STATE_SERVICE); inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) - .onControllerStateChanged(any()); + .onControllerStateChanged(argThat(jobs -> jobs.size() == 3)); // App is now in background and out of quota. Fg should now change to out of quota since it // wasn't started. Top should remain in quota since it started when the app was in TOP. assertTrue(jobTop2.isExpeditedQuotaApproved()); @@ -5633,7 +6016,7 @@ public class QuotaControllerTest { // Wait for some extra time to allow for job processing. verify(mJobSchedulerService, timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) - .onControllerStateChanged(any()); + .onControllerStateChanged(argThat(jobs -> jobs.size() > 0)); assertTrue(jobStatus.isExpeditedQuotaApproved()); // The job used up the remaining quota, but in that time, the same amount of time in the // old TimingSession also fell out of the quota window, so it should still have the same