Adding job count quota limits.
The main change is to have a limit for the past 10 minutes to avoid
short job bursts/spam. I've included bucket limits in case we want to
try them, but the limits are extremely high, so they should only affect
bad/pathological cases.
Bug: 117846754
Bug: 111423978
Test: atest com.android.server.job.controllers.QuotaControllerTest
Change-Id: I7bf7f1da64981187fa0295d0f6382779667e09dc
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 7f3ea7a..e68f9db 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -221,6 +221,8 @@
optional bool use_heartbeats = 23;
message TimeController {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
// Whether or not TimeController should skip setting wakeup alarms for jobs that aren't
// ready now.
optional bool skip_not_ready_jobs = 1;
@@ -228,6 +230,8 @@
optional TimeController time_controller = 25;
message QuotaController {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
// How much time each app will have to run jobs within their standby bucket window.
optional int64 allowed_time_per_period_ms = 1;
// How much time the package should have before transitioning from out-of-quota to in-quota.
@@ -251,6 +255,21 @@
optional int64 rare_window_size_ms = 6;
// The maximum amount of time an app can have its jobs running within a 24 hour window.
optional int64 max_execution_time_ms = 7;
+ // The maximum number of jobs an app can run within this particular standby bucket's
+ // window size.
+ optional int32 max_job_count_active = 8;
+ // The maximum number of jobs an app can run within this particular standby bucket's
+ // window size.
+ optional int32 max_job_count_working = 9;
+ // The maximum number of jobs an app can run within this particular standby bucket's
+ // window size.
+ optional int32 max_job_count_frequent = 10;
+ // The maximum number of jobs an app can run within this particular standby bucket's
+ // window size.
+ optional int32 max_job_count_rare = 11;
+ // The maximum number of jobs that should be allowed to run in the past
+ // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}.
+ optional int32 max_job_count_per_allowed_time = 12;
}
optional QuotaController quota_controller = 24;
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 2464ca7..9ca6cf6 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -419,6 +419,16 @@
"qc_window_size_rare_ms";
private static final String KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
"qc_max_execution_time_ms";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE =
+ "qc_max_job_count_active";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING =
+ "qc_max_job_count_working";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT =
+ "qc_max_job_count_frequent";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE =
+ "qc_max_job_count_rare";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME =
+ "qc_max_count_per_allowed_time";
private static final int DEFAULT_MIN_IDLE_COUNT = 1;
private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
@@ -460,6 +470,15 @@
24 * 60 * 60 * 1000L; // 24 hours
private static final long DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
4 * 60 * 60 * 1000L; // 4 hours
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE =
+ 200; // 1200/hr
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING =
+ 1200; // 600/hr
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT =
+ 1800; // 225/hr
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE =
+ 2400; // 100/hr
+ private static final int DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME = 20;
/**
* Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
@@ -677,6 +696,41 @@
public long QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS;
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE;
+
+ /**
+ * The maximum number of jobs that can run within the past
+ * {@link #QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}.
+ */
+ public int QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME =
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME;
+
private final KeyValueListParser mParser = new KeyValueListParser(',');
void updateConstantsLocked(String value) {
@@ -784,6 +838,21 @@
QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = mParser.getDurationMillis(
KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS,
DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE);
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME = mParser.getInt(
+ KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME,
+ DEFAULT_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME);
}
void dump(IndentingPrintWriter pw) {
@@ -845,6 +914,16 @@
QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS).println();
pw.printPair(KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS,
QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME).println();
pw.decreaseIndent();
}
@@ -899,6 +978,16 @@
QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS);
proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS,
QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_ACTIVE,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_WORKING,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_ALLOWED_TIME,
+ QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME);
proto.end(qcToken);
proto.end(token);
diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java
index c16d1b4..5a0b991 100644
--- a/services/core/java/com/android/server/job/controllers/QuotaController.java
+++ b/services/core/java/com/android/server/job/controllers/QuotaController.java
@@ -230,10 +230,10 @@
@VisibleForTesting
static class ExecutionStats {
/**
- * The time at which this record should be considered invalid, in the elapsed realtime
- * timebase.
+ * The time after which this record should be considered invalid (out of date), in the
+ * elapsed realtime timebase.
*/
- public long invalidTimeElapsed;
+ public long expirationTimeElapsed;
public long windowSizeMs;
@@ -241,29 +241,45 @@
public long executionTimeInWindowMs;
public int bgJobCountInWindow;
- /** The total amount of time the app ran in the last {@link MAX_PERIOD_MS}. */
+ /** The total amount of time the app ran in the last {@link #MAX_PERIOD_MS}. */
public long executionTimeInMaxPeriodMs;
public int bgJobCountInMaxPeriod;
/**
- * The time after which the sum of all the app's sessions plus {@link mQuotaBufferMs} equals
- * the quota. This is only valid if
- * executionTimeInWindowMs >= {@link mAllowedTimePerPeriodMs} or
- * executionTimeInMaxPeriodMs >= {@link mMaxExecutionTimeMs}.
+ * The time after which the sum of all the app's sessions plus {@link #mQuotaBufferMs}
+ * equals the quota. This is only valid if
+ * executionTimeInWindowMs >= {@link #mAllowedTimePerPeriodMs} or
+ * executionTimeInMaxPeriodMs >= {@link #mMaxExecutionTimeMs}.
*/
public long quotaCutoffTimeElapsed;
+ /**
+ * The time after which {@link #jobCountInAllowedTime} should be considered invalid, in the
+ * elapsed realtime timebase.
+ */
+ public long jobCountExpirationTimeElapsed;
+
+ /**
+ * The number of jobs that ran in at least the last {@link #mAllowedTimePerPeriodMs}.
+ * It may contain a few stale entries since cleanup won't happen exactly every
+ * {@link #mAllowedTimePerPeriodMs}.
+ */
+ public int jobCountInAllowedTime;
+
@Override
public String toString() {
return new StringBuilder()
- .append("invalidTime=").append(invalidTimeElapsed).append(", ")
+ .append("expirationTime=").append(expirationTimeElapsed).append(", ")
.append("windowSize=").append(windowSizeMs).append(", ")
.append("executionTimeInWindow=").append(executionTimeInWindowMs).append(", ")
.append("bgJobCountInWindow=").append(bgJobCountInWindow).append(", ")
.append("executionTimeInMaxPeriod=").append(executionTimeInMaxPeriodMs)
.append(", ")
.append("bgJobCountInMaxPeriod=").append(bgJobCountInMaxPeriod).append(", ")
- .append("quotaCutoffTime=").append(quotaCutoffTimeElapsed)
+ .append("quotaCutoffTime=").append(quotaCutoffTimeElapsed).append(", ")
+ .append("jobCountExpirationTime").append(jobCountExpirationTimeElapsed)
+ .append(", ")
+ .append("jobCountInAllowedTime").append(jobCountInAllowedTime)
.toString();
}
@@ -271,13 +287,15 @@
public boolean equals(Object obj) {
if (obj instanceof ExecutionStats) {
ExecutionStats other = (ExecutionStats) obj;
- return this.invalidTimeElapsed == other.invalidTimeElapsed
+ return this.expirationTimeElapsed == other.expirationTimeElapsed
&& this.windowSizeMs == other.windowSizeMs
&& this.executionTimeInWindowMs == other.executionTimeInWindowMs
&& this.bgJobCountInWindow == other.bgJobCountInWindow
&& this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
&& this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
- && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed;
+ && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed
+ && this.jobCountExpirationTimeElapsed == other.jobCountExpirationTimeElapsed
+ && this.jobCountInAllowedTime == other.jobCountInAllowedTime;
} else {
return false;
}
@@ -286,13 +304,15 @@
@Override
public int hashCode() {
int result = 0;
- result = 31 * result + hashLong(invalidTimeElapsed);
+ result = 31 * result + hashLong(expirationTimeElapsed);
result = 31 * result + hashLong(windowSizeMs);
result = 31 * result + hashLong(executionTimeInWindowMs);
result = 31 * result + bgJobCountInWindow;
result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
result = 31 * result + bgJobCountInMaxPeriod;
result = 31 * result + hashLong(quotaCutoffTimeElapsed);
+ result = 31 * result + hashLong(jobCountExpirationTimeElapsed);
+ result = 31 * result + jobCountInAllowedTime;
return result;
}
}
@@ -320,7 +340,7 @@
/**
* List of jobs that started while the UID was in the TOP state. There will be no more than
- * 16 ({@link JobSchedulerService.MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is
+ * 16 ({@link JobSchedulerService#MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is
* fine.
*/
private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
@@ -343,7 +363,7 @@
private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS;
/**
- * The maximum amount of time an app can have its jobs running within a {@link MAX_PERIOD_MS}
+ * The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS}
* window.
*/
private long mMaxExecutionTimeMs = 4 * 60 * MINUTE_IN_MILLIS;
@@ -355,17 +375,20 @@
private long mQuotaBufferMs = 30 * 1000L; // 30 seconds
/**
- * {@link mAllowedTimePerPeriodMs} - {@link mQuotaBufferMs}. This can be used to determine when
- * an app will have enough quota to transition from out-of-quota to in-quota.
+ * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine
+ * when an app will have enough quota to transition from out-of-quota to in-quota.
*/
private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
/**
- * {@link mMaxExecutionTimeMs} - {@link mQuotaBufferMs}. This can be used to determine when an
+ * {@link #mMaxExecutionTimeMs} - {@link #mQuotaBufferMs}. This can be used to determine when an
* app will have enough quota to transition from out-of-quota to in-quota.
*/
private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+ /** The maximum number of jobs that can run within the past {@link #mAllowedTimePerPeriodMs}. */
+ private int mMaxJobCountPerAllowedTime = 20;
+
private long mNextCleanupTimeElapsed = 0;
private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
new AlarmManager.OnAlarmListener() {
@@ -412,6 +435,23 @@
/** The maximum period any bucket can have. */
private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS;
+ /**
+ * The maximum number of jobs based on its standby bucket. For each max value count in the
+ * array, the app will not be allowed to run more than that many number of jobs within the
+ * latest time interval of its rolling window size.
+ *
+ * @see #mBucketPeriodsMs
+ */
+ private final int[] mMaxBucketJobCounts = new int[] {
+ 200, // ACTIVE -- 1200/hr
+ 1200, // WORKING -- 600/hr
+ 1800, // FREQUENT -- 225/hr
+ 2400 // RARE -- 100/hr
+ };
+
+ /** The minimum number of jobs that any bucket will be allowed to run. */
+ private static final int MIN_BUCKET_JOB_COUNT = 100;
+
/** An app has reached its quota. The message should contain a {@link Package} object. */
private static final int MSG_REACHED_QUOTA = 0;
/** Drop any old timing sessions. */
@@ -463,17 +503,21 @@
@Override
public void prepareForExecutionLocked(JobStatus jobStatus) {
if (DEBUG) Slog.d(TAG, "Prepping for " + jobStatus.toShortString());
+
+ final int uid = jobStatus.getSourceUid();
+ if (mActivityManagerInternal.getUidProcessState(uid) <= ActivityManager.PROCESS_STATE_TOP) {
+ mTopStartedJobs.add(jobStatus);
+ // Top jobs won't count towards quota so there's no need to involve the Timer.
+ return;
+ }
+
final int userId = jobStatus.getSourceUserId();
final String packageName = jobStatus.getSourcePackageName();
- final int uid = jobStatus.getSourceUid();
Timer timer = mPkgTimers.get(userId, packageName);
if (timer == null) {
timer = new Timer(uid, userId, packageName);
mPkgTimers.add(userId, packageName, timer);
}
- if (mActivityManagerInternal.getUidProcessState(uid) == ActivityManager.PROCESS_STATE_TOP) {
- mTopStartedJobs.add(jobStatus);
- }
timer.startTrackingJob(jobStatus);
}
@@ -548,6 +592,36 @@
mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
changed = true;
}
+ int newMaxCountPerAllowedPeriod = Math.max(10,
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME);
+ if (mMaxJobCountPerAllowedTime != newMaxCountPerAllowedPeriod) {
+ mMaxJobCountPerAllowedTime = newMaxCountPerAllowedPeriod;
+ changed = true;
+ }
+ int newActiveMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
+ Math.max(MIN_BUCKET_JOB_COUNT, mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE));
+ if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) {
+ mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount;
+ changed = true;
+ }
+ int newWorkingMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
+ Math.max(MIN_BUCKET_JOB_COUNT, mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING));
+ if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) {
+ mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount;
+ changed = true;
+ }
+ int newFrequentMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
+ Math.max(MIN_BUCKET_JOB_COUNT, mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT));
+ if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) {
+ mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount;
+ changed = true;
+ }
+ int newRareMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
+ Math.max(MIN_BUCKET_JOB_COUNT, mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE));
+ if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) {
+ mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount;
+ changed = true;
+ }
if (changed) {
// Update job bookkeeping out of band.
@@ -631,18 +705,39 @@
return isTopStartedJob(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid())
|| isWithinQuotaLocked(
- jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
+ jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
}
- private boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
+ @VisibleForTesting
+ boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
if (standbyBucket == NEVER_INDEX) return false;
// This check is needed in case the flag is toggled after a job has been registered.
if (!mShouldThrottle) return true;
// Quota constraint is not enforced while charging or when parole is on.
- return mChargeTracker.isCharging() || mInParole
- || getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) > 0;
+ if (mChargeTracker.isCharging() || mInParole) {
+ return true;
+ }
+
+ return getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) > 0
+ && isUnderJobCountQuotaLocked(userId, packageName, standbyBucket);
+ }
+
+ private boolean isUnderJobCountQuotaLocked(final int userId, @NonNull final String packageName,
+ final int standbyBucket) {
+ ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket, false);
+ return isUnderJobCountQuotaLocked(stats, standbyBucket);
+ }
+
+ private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats,
+ final int standbyBucket) {
+ final long now = sElapsedRealtimeClock.millis();
+ final boolean isUnderAllowedTimeQuota =
+ (stats.jobCountExpirationTimeElapsed <= now
+ || stats.jobCountInAllowedTime < mMaxJobCountPerAllowedTime);
+ return isUnderAllowedTimeQuota
+ && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]);
}
@VisibleForTesting
@@ -679,6 +774,13 @@
@NonNull
ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
+ return getExecutionStatsLocked(userId, packageName, standbyBucket, true);
+ }
+
+ @NonNull
+ private ExecutionStats getExecutionStatsLocked(final int userId,
+ @NonNull final String packageName, final int standbyBucket,
+ final boolean refreshStatsIfOld) {
if (standbyBucket == NEVER_INDEX) {
Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app.");
return new ExecutionStats();
@@ -693,14 +795,16 @@
stats = new ExecutionStats();
appStats[standbyBucket] = stats;
}
- final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
- Timer timer = mPkgTimers.get(userId, packageName);
- if ((timer != null && timer.isActive())
- || stats.invalidTimeElapsed <= sElapsedRealtimeClock.millis()
- || stats.windowSizeMs != bucketWindowSizeMs) {
- // The stats are no longer valid.
- stats.windowSizeMs = bucketWindowSizeMs;
- updateExecutionStatsLocked(userId, packageName, stats);
+ if (refreshStatsIfOld) {
+ final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if ((timer != null && timer.isActive())
+ || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis()
+ || stats.windowSizeMs != bucketWindowSizeMs) {
+ // The stats are no longer valid.
+ stats.windowSizeMs = bucketWindowSizeMs;
+ updateExecutionStatsLocked(userId, packageName, stats);
+ }
}
return stats;
@@ -717,14 +821,14 @@
Timer timer = mPkgTimers.get(userId, packageName);
final long nowElapsed = sElapsedRealtimeClock.millis();
- stats.invalidTimeElapsed = nowElapsed + MAX_PERIOD_MS;
+ stats.expirationTimeElapsed = nowElapsed + MAX_PERIOD_MS;
if (timer != null && timer.isActive()) {
stats.executionTimeInWindowMs =
stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed);
stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount();
// If the timer is active, the value will be stale at the next method call, so
// invalidate now.
- stats.invalidTimeElapsed = nowElapsed;
+ stats.expirationTimeElapsed = nowElapsed;
if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
nowElapsed - mAllowedTimeIntoQuotaMs);
@@ -800,7 +904,7 @@
break;
}
}
- stats.invalidTimeElapsed = nowElapsed + emptyTimeMs;
+ stats.expirationTimeElapsed = nowElapsed + emptyTimeMs;
}
private void invalidateAllExecutionStatsLocked(final int userId,
@@ -811,13 +915,35 @@
for (int i = 0; i < appStats.length; ++i) {
ExecutionStats stats = appStats[i];
if (stats != null) {
- stats.invalidTimeElapsed = nowElapsed;
+ stats.expirationTimeElapsed = nowElapsed;
}
}
}
}
@VisibleForTesting
+ void incrementJobCount(final int userId, @NonNull final String packageName, int count) {
+ final long now = sElapsedRealtimeClock.millis();
+ ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+ if (appStats == null) {
+ appStats = new ExecutionStats[mBucketPeriodsMs.length];
+ mExecutionStatsCache.add(userId, packageName, appStats);
+ }
+ for (int i = 0; i < appStats.length; ++i) {
+ ExecutionStats stats = appStats[i];
+ if (stats == null) {
+ stats = new ExecutionStats();
+ appStats[i] = stats;
+ }
+ if (stats.jobCountExpirationTimeElapsed <= now) {
+ stats.jobCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs;
+ stats.jobCountInAllowedTime = 0;
+ }
+ stats.jobCountInAllowedTime += count;
+ }
+ }
+
+ @VisibleForTesting
void saveTimingSession(final int userId, @NonNull final String packageName,
@NonNull final TimingSession session) {
synchronized (mLock) {
@@ -1023,9 +1149,12 @@
final String pkgString = string(userId, packageName);
ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket);
+
QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
- && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs) {
+ && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
+ && isUnderJobCountQuota) {
// Already in quota. Why was this method called?
if (DEBUG) {
Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
@@ -1042,18 +1171,22 @@
mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
return;
}
+
if (alarmListener == null) {
alarmListener = new QcAlarmListener(userId, packageName);
mInQuotaAlarmListeners.add(userId, packageName, alarmListener);
}
// The time this app will have quota again.
- long inQuotaTimeElapsed =
- stats.quotaCutoffTimeElapsed + stats.windowSizeMs;
+ long inQuotaTimeElapsed = stats.quotaCutoffTimeElapsed + stats.windowSizeMs;
if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) {
inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS);
}
+ if (!isUnderJobCountQuota) {
+ inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
+ stats.jobCountExpirationTimeElapsed + mAllowedTimePerPeriodMs);
+ }
// Only schedule the alarm if:
// 1. There isn't one currently scheduled
// 2. The new alarm is significantly earlier than the previous alarm (which could be the
@@ -1228,6 +1361,7 @@
mRunningBgJobs.add(jobStatus);
if (shouldTrackLocked()) {
mBgJobCount++;
+ incrementJobCount(mPkg.userId, mPkg.packageName, 1);
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis();
@@ -1324,6 +1458,7 @@
// repeatedly plugged in and unplugged, or an app changes foreground state
// very frequently, the job count for a package may be artificially high.
mBgJobCount = mRunningBgJobs.size();
+ incrementJobCount(mPkg.userId, mPkg.packageName, mBgJobCount);
// Starting the timer means that all cached execution stats are now
// incorrect.
invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
@@ -1604,6 +1739,12 @@
@VisibleForTesting
@NonNull
+ int[] getBucketMaxJobCounts() {
+ return mMaxBucketJobCounts;
+ }
+
+ @VisibleForTesting
+ @NonNull
long[] getBucketWindowSizes() {
return mBucketPeriodsMs;
}
@@ -1631,6 +1772,11 @@
}
@VisibleForTesting
+ int getMaxJobCountPerAllowedTime() {
+ return mMaxJobCountPerAllowedTime;
+ }
+
+ @VisibleForTesting
@Nullable
List<TimingSession> getTimingSessions(int userId, String packageName) {
return mTimingSessions.get(userId, packageName);
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 57ee6dc..cad71a2 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
@@ -25,6 +25,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
+import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
@@ -370,16 +371,19 @@
mQuotaController.saveTimingSession(0, "com.android.test.stay", one);
ExecutionStats expectedStats = new ExecutionStats();
- expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
- mQuotaController.onAppRemovedLocked("com.android.test.remove", 10001);
+ final int uid = 10001;
+ mQuotaController.onAppRemovedLocked("com.android.test.remove", uid);
assertNull(mQuotaController.getTimingSessions(0, "com.android.test.remove"));
assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test.stay"));
assertEquals(expectedStats,
mQuotaController.getExecutionStatsLocked(0, "com.android.test.remove", RARE_INDEX));
assertNotEquals(expectedStats,
mQuotaController.getExecutionStatsLocked(0, "com.android.test.stay", RARE_INDEX));
+
+ assertFalse(mQuotaController.getForegroundUids().get(uid));
}
@Test
@@ -405,7 +409,7 @@
mQuotaController.saveTimingSession(10, "com.android.test", one);
ExecutionStats expectedStats = new ExecutionStats();
- expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
mQuotaController.onUserRemovedLocked(0);
@@ -440,14 +444,14 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS;
// Invalid time is now +24 hours since there are no sessions at all for the app.
- expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = MINUTE_IN_MILLIS;
// Invalid time is now +18 hours since there are no sessions in the window but the earliest
// session is 6 hours ago.
- expectedStats.invalidTimeElapsed = now + 18 * HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 18 * HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 0;
expectedStats.bgJobCountInWindow = 0;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -457,7 +461,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS;
// Invalid time is now since the session straddles the window cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 2 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 3;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -468,7 +472,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * MINUTE_IN_MILLIS;
// Invalid time is now since the start of the session is at the very edge of the window
// cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 3;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -479,7 +483,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS;
// Invalid time is now +44 minutes since the earliest session in the window is now-5
// minutes.
- expectedStats.invalidTimeElapsed = now + 44 * MINUTE_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 44 * MINUTE_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 3;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -489,7 +493,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS;
// Invalid time is now since the session is at the very edge of the window cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 5 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 4;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -500,7 +504,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS;
// Invalid time is now since the start of the session is at the very edge of the window
// cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 6 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 5;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -510,7 +514,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
// Invalid time is now since the session straddles the window cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 10;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -523,7 +527,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * HOUR_IN_MILLIS;
// Invalid time is now +59 minutes since the earliest session in the window is now-121
// minutes.
- expectedStats.invalidTimeElapsed = now + 59 * MINUTE_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 59 * MINUTE_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 12 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 10;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -536,7 +540,7 @@
inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS;
// Invalid time is now since the start of the session is at the very edge of the window
// cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
@@ -546,14 +550,14 @@
mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
assertEquals(expectedStats, inputStats);
- // Make sure invalidTimeElapsed is set correctly when it's dependent on the max period.
+ // Make sure expirationTimeElapsed is set correctly when it's dependent on the max period.
mQuotaController.getTimingSessions(0, "com.android.test")
.add(0,
createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3));
inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
// Invalid time is now +1 hour since the earliest session in the max period is 1 hour
// before the end of the max period cutoff time.
- expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS;
@@ -569,7 +573,7 @@
2 * MINUTE_IN_MILLIS, 2));
inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
// Invalid time is now since the earlist session straddles the max period cutoff time.
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS;
@@ -599,7 +603,7 @@
// Active
expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
- expectedStats.invalidTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 5;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
@@ -609,7 +613,7 @@
// Working
expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
- expectedStats.invalidTimeElapsed = now;
+ expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 10;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
@@ -621,7 +625,7 @@
// Frequent
expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
- expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
@@ -633,7 +637,7 @@
// Rare
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
- expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 20;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
@@ -675,7 +679,7 @@
ExecutionStats expectedStats = new ExecutionStats();
expectedStats.windowSizeMs = originalStatsActive.windowSizeMs;
- expectedStats.invalidTimeElapsed = originalStatsActive.invalidTimeElapsed;
+ expectedStats.expirationTimeElapsed = originalStatsActive.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow;
expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs;
@@ -688,7 +692,7 @@
assertEquals(expectedStats, newStatsActive);
expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs;
- expectedStats.invalidTimeElapsed = originalStatsWorking.invalidTimeElapsed;
+ expectedStats.expirationTimeElapsed = originalStatsWorking.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow;
expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed;
@@ -698,7 +702,7 @@
assertNotEquals(expectedStats, newStatsWorking);
expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs;
- expectedStats.invalidTimeElapsed = originalStatsFrequent.invalidTimeElapsed;
+ expectedStats.expirationTimeElapsed = originalStatsFrequent.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow;
expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed;
@@ -708,7 +712,7 @@
assertNotEquals(expectedStats, newStatsFrequent);
expectedStats.windowSizeMs = originalStatsRare.windowSizeMs;
- expectedStats.invalidTimeElapsed = originalStatsRare.invalidTimeElapsed;
+ expectedStats.expirationTimeElapsed = originalStatsRare.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow;
expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed;
@@ -719,6 +723,77 @@
}
@Test
+ public void testIsWithinQuotaLocked_NeverApp() {
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test.never", NEVER_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_Charging() {
+ setCharging();
+ assertTrue(mQuotaController.isWithinQuotaLocked(0, "com.android.test", RARE_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_UnderDuration_UnderJobCount() {
+ setDischarging();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.incrementJobCount(0, "com.android.test", 5);
+ assertTrue(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() {
+ setDischarging();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ final int jobCount = mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME;
+ mQuotaController.saveTimingSession(0, "com.android.test.spam",
+ createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
+ mQuotaController.saveTimingSession(0, "com.android.test.spam",
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, jobCount));
+ mQuotaController.incrementJobCount(0, "com.android.test.spam", jobCount);
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test.spam",
+ WORKING_INDEX));
+
+ mQuotaController.saveTimingSession(0, "com.android.test.frequent",
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 2000));
+ mQuotaController.saveTimingSession(0, "com.android.test.frequent",
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 500));
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test.frequent",
+ FREQUENT_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_OverDuration_UnderJobCount() {
+ setDischarging();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (30 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 4 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.incrementJobCount(0, "com.android.test", 5);
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
+ }
+
+ @Test
+ public void testIsWithinQuotaLocked_OverDuration_OverJobCount() {
+ setDischarging();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ final int jobCount = mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME;
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (5 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, jobCount));
+ mQuotaController.incrementJobCount(0, "com.android.test", jobCount);
+ assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX));
+ }
+
+ @Test
public void testMaybeScheduleCleanupAlarmLocked() {
// No sessions saved yet.
mQuotaController.maybeScheduleCleanupAlarmLocked();
@@ -752,6 +827,7 @@
// Active window size is 10 minutes.
final int standbyBucket = ACTIVE_INDEX;
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND);
// No sessions saved yet.
mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -1016,11 +1092,37 @@
.set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX);
- inOrder.verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
- any());
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
}
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_JobCount_AllowedTime() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ final int standbyBucket = WORKING_INDEX;
+ ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
+ SOURCE_PACKAGE, standbyBucket);
+ stats.jobCountInAllowedTime =
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME + 2;
+
+ // Invalid time in the past, so the count shouldn't be used.
+ stats.jobCountExpirationTimeElapsed =
+ now - mQuotaController.getAllowedTimePerPeriodMs() / 2;
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ // Invalid time in the future, so the count should be used.
+ stats.jobCountExpirationTimeElapsed =
+ now + mQuotaController.getAllowedTimePerPeriodMs() / 2;
+ final long expectedWorkingAlarmTime =
+ stats.jobCountExpirationTimeElapsed + mQuotaController.getAllowedTimePerPeriodMs();
+ mQuotaController.maybeScheduleStartAlarmLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+ verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+ }
/**
* Tests that the start alarm is properly rescheduled if the earliest session that contributes
@@ -1172,6 +1274,11 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 45 * MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 60 * MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 3 * HOUR_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE = 5000;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING = 4000;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT = 3000;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE = 2000;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME = 500;
mQuotaController.onConstantsUpdatedLocked();
@@ -1183,11 +1290,16 @@
mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
+ assertEquals(500, mQuotaController.getMaxJobCountPerAllowedTime());
+ assertEquals(5000, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
+ assertEquals(4000, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
+ assertEquals(3000, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
+ assertEquals(2000, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
}
@Test
public void testConstantsUpdating_InvalidValues() {
- // Test negatives
+ // Test negatives/too low.
mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_ACTIVE_MS = -MINUTE_IN_MILLIS;
@@ -1195,6 +1307,11 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = -MINUTE_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_ACTIVE = -1;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_WORKING = 1;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_FREQUENT = 1;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_RARE = 1;
+ mConstants.QUOTA_CONTROLLER_MAX_JOB_COUNT_PER_ALLOWED_TIME = 0;
mQuotaController.onConstantsUpdatedLocked();
@@ -1205,6 +1322,11 @@
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
+ assertEquals(10, mQuotaController.getMaxJobCountPerAllowedTime());
+ assertEquals(100, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
+ assertEquals(100, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
+ assertEquals(100, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
+ assertEquals(100, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
// Test larger than a day. Controller should cap at one day.
mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = 25 * HOUR_IN_MILLIS;
@@ -1246,6 +1368,7 @@
@Test
public void testTimerTracking_Discharging() {
setDischarging();
+ setProcessState(ActivityManager.PROCESS_STATE_BACKUP);
JobStatus jobStatus = createJobStatus("testTimerTracking_Discharging", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
@@ -1293,6 +1416,8 @@
*/
@Test
public void testTimerTracking_ChargingAndDischarging() {
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+
JobStatus jobStatus = createJobStatus("testTimerTracking_ChargingAndDischarging", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
JobStatus jobStatus2 = createJobStatus("testTimerTracking_ChargingAndDischarging", 2);
@@ -1363,6 +1488,7 @@
@Test
public void testTimerTracking_AllBackground() {
setDischarging();
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
JobStatus jobStatus = createJobStatus("testTimerTracking_AllBackground", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
@@ -1503,6 +1629,64 @@
}
/**
+ * Tests that Timers don't track job counts while in the foreground.
+ */
+ @Test
+ public void testTimerTracking_JobCount_Foreground() {
+ setDischarging();
+
+ final int standbyBucket = ACTIVE_INDEX;
+ JobStatus jobFg1 = createJobStatus("testTimerTracking_JobCount_Foreground", 1);
+ JobStatus jobFg2 = createJobStatus("testTimerTracking_JobCount_Foreground", 2);
+
+ mQuotaController.maybeStartTrackingJobLocked(jobFg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobFg2, null);
+ assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
+ SOURCE_PACKAGE, standbyBucket);
+ assertEquals(0, stats.jobCountInAllowedTime);
+
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ mQuotaController.prepareForExecutionLocked(jobFg1);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.prepareForExecutionLocked(jobFg2);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobFg2, null, false);
+ assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ assertEquals(0, stats.jobCountInAllowedTime);
+ }
+
+ /**
+ * Tests that Timers properly track job counts while in the background.
+ */
+ @Test
+ public void testTimerTracking_JobCount_Background() {
+ final int standbyBucket = WORKING_INDEX;
+ JobStatus jobBg1 = createJobStatus("testTimerTracking_JobCount_Background", 1);
+ JobStatus jobBg2 = createJobStatus("testTimerTracking_JobCount_Background", 2);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+
+ ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
+ SOURCE_PACKAGE, standbyBucket);
+ assertEquals(0, stats.jobCountInAllowedTime);
+
+ setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+ mQuotaController.prepareForExecutionLocked(jobBg1);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.prepareForExecutionLocked(jobBg2);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobBg1, null, false);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+
+ assertEquals(2, stats.jobCountInAllowedTime);
+ }
+
+ /**
* Tests that Timers properly track overlapping top and background jobs.
*/
@Test
@@ -1680,6 +1864,7 @@
JobStatus jobStatus = createJobStatus("testTracking_OutOfQuota", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
setStandbyBucket(WORKING_INDEX, jobStatus); // 2 hour window
+ setProcessState(ActivityManager.PROCESS_STATE_HOME);
// Now the package only has two seconds to run.
final long remainingTimeMs = 2 * SECOND_IN_MILLIS;
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -1707,6 +1892,7 @@
JobStatus jobStatus = createJobStatus("testTracking_OutOfQuota", 1);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
setStandbyBucket(WORKING_INDEX, jobStatus); // 2 hour window
+ setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
Handler handler = mQuotaController.getHandler();
spyOn(handler);