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
This commit is contained in:
Kweku Adams
2022-01-20 19:48:03 +00:00
parent 8b22e7b706
commit 6765e85033
2 changed files with 774 additions and 180 deletions

View File

@@ -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<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 =
@@ -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() {