diff options
| author | 2022-01-24 22:55:21 +0000 | |
|---|---|---|
| committer | 2022-01-24 22:55:21 +0000 | |
| commit | 24f779dcf090af0e76f13330f0f2769bb85a6522 (patch) | |
| tree | d05a5c77a9e3b67df23646c9210df404b5e3d6c0 | |
| parent | 2f3e7488cb56a1d54c5d580433010ff344b0179f (diff) | |
| parent | 6765e85033ab823270cdd9573a7437123059680d (diff) | |
Merge "Deferring low and min priority jobs when quota is low."
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 65e1d49d1510..dd5246aebbb4 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; @@ -161,6 +162,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<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUserId(), - jobStatus.getSourcePackageName()); - if (jobs != null) { - jobs.remove(jobStatus); + final int userId = jobStatus.getSourceUserId(); + final String pkgName = jobStatus.getSourcePackageName(); + ArraySet<JobStatus> 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<JobStatus> maybeUpdateConstraintForPkgLocked(final long nowElapsed, + final int userId, @NonNull final String packageName) { ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName); + final ArraySet<JobStatus> 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<JobStatus> { @@ -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<JobStatus> 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<JobStatus> 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<JobStatus> 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<JobStatus> 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 = @@ -2951,6 +3106,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 * WINDOW_SIZE_MS. @@ -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(); @@ -3750,6 +3951,16 @@ public final class QuotaController extends StateController { } @VisibleForTesting + float getAllowedTimeSurplusPriorityLow() { + return mAllowedTimeSurplusPriorityLow; + } + + @VisibleForTesting + float getAllowedTimeSurplusPriorityMin() { + return mAllowedTimeSurplusPriorityMin; + } + + @VisibleForTesting @NonNull int[] getBucketMaxJobCounts() { return mMaxBucketJobCounts; 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 cfae9a3d586a..153ce17ec9dd 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,22 +1992,66 @@ 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); setStandbyBucket(NEVER_INDEX, js); @@ -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 |