From 1d0d4bffa5524286f144086232a93c5a2aa6707f Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Wed, 15 May 2019 07:35:23 -0700 Subject: [PATCH] Changing default limits, clarify flags, fix bug. 1. Lower job count limits to values that should be more reasonable. The following defaults have changed: -- MAX_JOB_COUNT_ACTIVE = 20/window = 120/hr = 1/session (down from 200/window = 1200/hr = 10/session) -- MAX_JOB_COUNT_WORKING = 120/window = 60/hr = 12/session (down from 1200/window = 600/hr = 120/session) -- MAX_JOB_COUNT_FREQUENT = 200/window = 25/hr = 25/session (down from 1800/window = 225/hr = 225/session) -- MAX_JOB_COUNT_RARE = 48/window = 2/hr = 16/session (down from 2400/window = 100/hr = 800/session) 2. Increase timing session coalescing duration to enable coalescing. -- TIMING_SESSION_COALESCING_DURATION_MS = 5 seconds (up from 0ms) 3. Separate allowed time flag from rate limiting period flag so they can be changed independently. 4. Fix bug: If an app reached the job count quota limit for its bucket, QuotaController would set an alarm only 10 minutes (allowed time) into the future to determine if the app was back in quota. This would result in unnecessary wakeup alarms. Now, QC will set an alarm for when the app will be under quota. 5. Expand some comments, clarify some constants, and simplify some code. Bug: 132227621 Test: atest com.android.server.job.controllers.QuotaControllerTest Test: CtsJobSchedulerTestCases Change-Id: I6f3f3a5eff7f64b429820d7370d82c1b4573f23b --- core/proto/android/server/jobscheduler.proto | 89 ++-- .../job/controllers/QuotaController.java | 474 ++++++++++-------- .../job/controllers/QuotaControllerTest.java | 223 +++++--- 3 files changed, 474 insertions(+), 312 deletions(-) diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 3f8ddff5327c1..28733798b9ad5 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -273,9 +273,11 @@ message ConstantsProto { // 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 period of time used to rate limit recently run jobs. + optional int32 rate_limiting_window_ms = 19; // 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; + // rate_limiting_window_ms. + optional int32 max_job_count_per_rate_limiting_window = 12; // The maximum number of timing sessions an app can run within this particular standby // bucket's window size. optional int32 max_session_count_active = 13; @@ -289,8 +291,8 @@ message ConstantsProto { // bucket's window size. optional int32 max_session_count_rare = 16; // The maximum number of timing sessions that should be allowed to run in the past - // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}. - optional int32 max_session_count_per_allowed_time = 17; + // rate_limiting_window_ms. + optional int32 max_session_count_per_rate_limiting_window = 17; // Treat two distinct {@link TimingSession}s as the same if they start and end within this // amount of time of each other. optional int64 timing_session_coalescing_duration_ms = 18; @@ -517,63 +519,56 @@ message StateControllerProto { optional int64 expiration_time_elapsed = 2; optional int64 window_size_ms = 3; - /** The total amount of time the app ran in its respective bucket window size. */ + optional int32 job_count_limit = 14; + optional int32 session_count_limit = 15; + + // The total amount of time the app ran in its respective bucket window size. optional int64 execution_time_in_window_ms = 4; optional int32 bg_job_count_in_window = 5; - /** - * The total amount of time the app ran in the last - * {@link QuotaController#MAX_PERIOD_MS}. - */ + // The total amount of time the app ran in the last + // {@link QuotaController#MAX_PERIOD_MS}. optional int64 execution_time_in_max_period_ms = 6; optional int32 bg_job_count_in_max_period = 7; - /** - * The number of {@link TimingSession}s within the bucket window size. This will include - * sessions that started before the window as long as they end within the window. - */ + // The number of {@link TimingSession}s within the bucket window size. This will include + // sessions that started before the window as long as they end within the window. optional int32 session_count_in_window = 11; - /** - * The time after which the sum of all the app's sessions plus - * ConstantsProto.QuotaController.in_quota_buffer_ms equals the quota. This is only - * valid if - * execution_time_in_window_ms >= - * ConstantsProto.QuotaController.allowed_time_per_period_ms - * or - * execution_time_in_max_period_ms >= - * ConstantsProto.QuotaController.max_execution_time_ms. - */ - optional int64 quota_cutoff_time_elapsed = 8; + // The time after which the app will be under the bucket quota. This is only valid if + // execution_time_in_window_ms >= + // ConstantsProto.QuotaController.allowed_time_per_period_ms + // or + // execution_time_in_max_period_ms >= + // ConstantsProto.QuotaController.max_execution_time_ms + // or + // bg_job_count_in_window >= job_count_limit + // or + // session_count_in_window >= session_count_limit. + optional int64 in_quota_time_elapsed = 8; - /** - * The time after which job_count_in_allowed_time should be considered invalid, in the - * elapsed realtime timebase. - */ + // The time after which job_count_in_rate_limiting_window should be considered invalid, + // in the elapsed realtime timebase. optional int64 job_count_expiration_time_elapsed = 9; - /** - * The number of jobs that ran in at least the last - * ConstantsProto.QuotaController.allowed_time_per_period_ms. - * It may contain a few stale entries since cleanup won't happen exactly every - * ConstantsProto.QuotaController.allowed_time_per_period_ms. - */ - optional int32 job_count_in_allowed_time = 10; + // The number of jobs that ran in at least the last + // ConstantsProto.QuotaController.rate_limiting_window_ms. + // It may contain a few stale entries since cleanup won't happen exactly every + // ConstantsProto.QuotaController.rate_limiting_window_ms. This should only be + // considered valid before elapsed realtime has reached + // job_count_expiration_time_elapsed. + optional int32 job_count_in_rate_limiting_window = 10; - /** - * The time after which {@link #timingSessionCountInAllowedTime} should be considered - * invalid, in the elapsed realtime timebase. - */ + // The time after which {@link #timingSessionCountInAllowedTime} should be considered + // invalid, in the elapsed realtime timebase. optional int64 session_count_expiration_time_elapsed = 12; - /** - * The number of {@link TimingSession}s 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}. This should only be considered - * valid before elapsed realtime has reached - * {@link #timingSessionCountExpirationTimeElapsed}. - */ - optional int32 session_count_in_allowed_time = 13; + // The number of {@link TimingSession}s that ran in at least the last + // ConstantsProto.QuotaController.rate_limiting_window_ms. It may contain a few stale + // entries since cleanup won't happen exactly every + // ConstantsProto.QuotaController.rate_limiting_window_ms. This should only be considered + // valid before elapsed realtime has reached session_count_expiration_time_elapsed. + optional int32 session_count_in_rate_limiting_window = 13; } message Package { 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 f560d69310a3a..ef6944e180de6 100644 --- a/services/core/java/com/android/server/job/controllers/QuotaController.java +++ b/services/core/java/com/android/server/job/controllers/QuotaController.java @@ -16,6 +16,10 @@ package com.android.server.job.controllers; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; + 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; @@ -75,17 +79,28 @@ import java.util.function.Predicate; /** * Controller that tracks whether an app has exceeded its standby bucket quota. * - * Each job in each bucket is given 10 minutes to run within its respective time window. Active - * jobs can run indefinitely, working set jobs can run for 10 minutes within a 2 hour window, - * frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run 10 minutes in - * a 24 hour window. The windows are rolling, so as soon as a job would have some quota based on its - * bucket, it will be eligible to run. When a job's bucket changes, its new quota is immediately - * applied to it. + * With initial defaults, each app in each bucket is given 10 minutes to run within its respective + * time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a + * 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run + * 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some + * quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new + * quota is immediately applied to it. + * + * Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on + * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will + * not be allowed to run more than 20 jobs within the past 10 minutes. * * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run * freely when an app enters the foreground state and are restricted when the app leaves the - * foreground state. However, jobs that are started while the app is in the TOP state are not - * restricted regardless of the app's state change. + * foreground state. However, jobs that are started while the app is in the TOP state do not count + * towards any quota and are not restricted regardless of the app's state change. + * + * Jobs will not be throttled when the device is charging. The device is considered to be charging + * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast. + * + * Note: all limits are enforced per bucket window unless explicitly stated otherwise. + * All stated values are configurable and subject to change. See {@link QcConstants} for current + * defaults. * * Test: atest com.android.server.job.controllers.QuotaControllerTest */ @@ -94,8 +109,6 @@ public final class QuotaController extends StateController { private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); - private static final long MINUTE_IN_MILLIS = 60 * 1000L; - private static final String ALARM_TAG_CLEANUP = "*job.cleanup*"; private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*"; @@ -244,6 +257,8 @@ public final class QuotaController extends StateController { public long expirationTimeElapsed; public long windowSizeMs; + public int jobCountLimit; + public int sessionCountLimit; /** The total amount of time the app ran in its respective bucket window size. */ public long executionTimeInWindowMs; @@ -260,54 +275,58 @@ public final class QuotaController extends StateController { public int sessionCountInWindow; /** - * 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 app will be under the bucket quota and can start running jobs + * again. This is only valid if + * {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs}, + * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs}, + * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or + * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}. */ - public long quotaCutoffTimeElapsed; + public long inQuotaTimeElapsed; /** - * The time after which {@link #jobCountInAllowedTime} should be considered invalid, in the - * elapsed realtime timebase. + * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid, + * in the elapsed realtime timebase. */ - public long jobCountExpirationTimeElapsed; + public long jobRateLimitExpirationTimeElapsed; /** - * The number of jobs that ran in at least the last {@link #mAllowedTimePerPeriodMs}. + * The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}. * It may contain a few stale entries since cleanup won't happen exactly every - * {@link #mAllowedTimePerPeriodMs}. + * {@link #mRateLimitingWindowMs}. */ - public int jobCountInAllowedTime; + public int jobCountInRateLimitingWindow; /** - * The time after which {@link #sessionCountInAllowedTime} should be considered + * The time after which {@link #sessionCountInRateLimitingWindow} should be considered * invalid, in the elapsed realtime timebase. */ - public long sessionCountExpirationTimeElapsed; + public long sessionRateLimitExpirationTimeElapsed; /** * The number of {@link TimingSession}s 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}. This should only be considered - * valid before elapsed realtime has reached {@link #sessionCountExpirationTimeElapsed}. + * {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't + * happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered + * valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}. */ - public int sessionCountInAllowedTime; + public int sessionCountInRateLimitingWindow; @Override public String toString() { return "expirationTime=" + expirationTimeElapsed + ", " - + "windowSize=" + windowSizeMs + ", " + + "windowSizeMs=" + windowSizeMs + ", " + + "jobCountLimit=" + jobCountLimit + ", " + + "sessionCountLimit=" + sessionCountLimit + ", " + "executionTimeInWindow=" + executionTimeInWindowMs + ", " + "bgJobCountInWindow=" + bgJobCountInWindow + ", " + "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", " + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", " + "sessionCountInWindow=" + sessionCountInWindow + ", " - + "quotaCutoffTime=" + quotaCutoffTimeElapsed + ", " - + "jobCountExpirationTime=" + jobCountExpirationTimeElapsed + ", " - + "jobCountInAllowedTime=" + jobCountInAllowedTime + ", " - + "sessionCountExpirationTime=" + sessionCountExpirationTimeElapsed + ", " - + "sessionCountInAllowedTime=" + sessionCountInAllowedTime; + + "inQuotaTime=" + inQuotaTimeElapsed + ", " + + "jobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", " + + "jobCountInRateLimitingWindow=" + jobCountInRateLimitingWindow + ", " + + "sessionCountExpirationTime=" + sessionRateLimitExpirationTimeElapsed + ", " + + "sessionCountInRateLimitingWindow=" + sessionCountInRateLimitingWindow; } @Override @@ -316,18 +335,21 @@ public final class QuotaController extends StateController { ExecutionStats other = (ExecutionStats) obj; return this.expirationTimeElapsed == other.expirationTimeElapsed && this.windowSizeMs == other.windowSizeMs + && this.jobCountLimit == other.jobCountLimit + && this.sessionCountLimit == other.sessionCountLimit && this.executionTimeInWindowMs == other.executionTimeInWindowMs && this.bgJobCountInWindow == other.bgJobCountInWindow && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs && this.sessionCountInWindow == other.sessionCountInWindow && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod - && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed - && this.jobCountExpirationTimeElapsed == other.jobCountExpirationTimeElapsed - && this.jobCountInAllowedTime == other.jobCountInAllowedTime - && this.sessionCountExpirationTimeElapsed - == other.sessionCountExpirationTimeElapsed - && this.sessionCountInAllowedTime - == other.sessionCountInAllowedTime; + && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed + && this.jobRateLimitExpirationTimeElapsed + == other.jobRateLimitExpirationTimeElapsed + && this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow + && this.sessionRateLimitExpirationTimeElapsed + == other.sessionRateLimitExpirationTimeElapsed + && this.sessionCountInRateLimitingWindow + == other.sessionCountInRateLimitingWindow; } else { return false; } @@ -338,16 +360,18 @@ public final class QuotaController extends StateController { int result = 0; result = 31 * result + hashLong(expirationTimeElapsed); result = 31 * result + hashLong(windowSizeMs); + result = 31 * result + hashLong(jobCountLimit); + result = 31 * result + hashLong(sessionCountLimit); result = 31 * result + hashLong(executionTimeInWindowMs); result = 31 * result + bgJobCountInWindow; result = 31 * result + hashLong(executionTimeInMaxPeriodMs); result = 31 * result + bgJobCountInMaxPeriod; result = 31 * result + sessionCountInWindow; - result = 31 * result + hashLong(quotaCutoffTimeElapsed); - result = 31 * result + hashLong(jobCountExpirationTimeElapsed); - result = 31 * result + jobCountInAllowedTime; - result = 31 * result + hashLong(sessionCountExpirationTimeElapsed); - result = 31 * result + sessionCountInAllowedTime; + result = 31 * result + hashLong(inQuotaTimeElapsed); + result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed); + result = 31 * result + jobCountInRateLimitingWindow; + result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed); + result = 31 * result + sessionCountInRateLimitingWindow; return result; } } @@ -399,19 +423,19 @@ public final class QuotaController extends StateController { private boolean mShouldThrottle; /** How much time each app will have to run jobs within their standby bucket window. */ - private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS; + private long mAllowedTimePerPeriodMs = QcConstants.DEFAULT_ALLOWED_TIME_PER_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; + private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS; /** * How much time the app should have before transitioning from out-of-quota to in-quota. * This should not affect processing if the app is already in-quota. */ - private long mQuotaBufferMs = 30 * 1000L; // 30 seconds + private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS; /** * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine @@ -425,14 +449,19 @@ public final class QuotaController extends StateController { */ private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; - /** The maximum number of jobs that can run within the past {@link #mAllowedTimePerPeriodMs}. */ - private int mMaxJobCountPerAllowedTime = 20; + /** The period of time used to rate limit recently run jobs. */ + private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS; + + /** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */ + private int mMaxJobCountPerRateLimitingWindow = + QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; /** * The maximum number of {@link TimingSession}s that can run within the past {@link - * #mAllowedTimePerPeriodMs}. + * #mRateLimitingWindowMs}. */ - private int mMaxSessionCountPerAllowedTime = 20; + private int mMaxSessionCountPerRateLimitingWindow = + QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; private long mNextCleanupTimeElapsed = 0; private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener = @@ -486,11 +515,11 @@ public final class QuotaController extends StateController { * The rolling window size for each standby bucket. Within each window, an app will have 10 * minutes to run its jobs. */ - private final long[] mBucketPeriodsMs = new long[] { - 10 * MINUTE_IN_MILLIS, // 10 minutes for ACTIVE -- ACTIVE apps can run jobs at any time - 2 * 60 * MINUTE_IN_MILLIS, // 2 hours for WORKING - 8 * 60 * MINUTE_IN_MILLIS, // 8 hours for FREQUENT - 24 * 60 * MINUTE_IN_MILLIS // 24 hours for RARE + private final long[] mBucketPeriodsMs = new long[]{ + QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS, + QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS, + QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS, + QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS }; /** The maximum period any bucket can have. */ @@ -503,16 +532,13 @@ public final class QuotaController extends StateController { * * @see #mBucketPeriodsMs */ - private final int[] mMaxBucketJobCounts = new int[] { - 200, // ACTIVE -- 1200/hr - 1200, // WORKING -- 600/hr - 1800, // FREQUENT -- 225/hr - 2400 // RARE -- 100/hr + private final int[] mMaxBucketJobCounts = new int[]{ + QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE, + QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING, + QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT, + QcConstants.DEFAULT_MAX_JOB_COUNT_RARE }; - /** The minimum number of jobs that any bucket will be allowed to run. */ - private static final int MIN_BUCKET_JOB_COUNT = 100; - /** * The maximum number of {@link TimingSession}s based on its standby bucket. For each max value * count in the array, the app will not be allowed to have more than that many number of @@ -527,14 +553,12 @@ public final class QuotaController extends StateController { QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE }; - /** The minimum number of {@link TimingSession}s that any bucket will be allowed to run. */ - private static final int MIN_BUCKET_SESSION_COUNT = 3; - /** * Treat two distinct {@link TimingSession}s as the same if they start and end within this * amount of time of each other. */ - private long mTimingSessionCoalescingDurationMs = 0; + private long mTimingSessionCoalescingDurationMs = + QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; /** An app has reached its quota. The message should contain a {@link Package} object. */ private static final int MSG_REACHED_QUOTA = 0; @@ -761,8 +785,8 @@ public final class QuotaController extends StateController { final int standbyBucket) { final long now = sElapsedRealtimeClock.millis(); final boolean isUnderAllowedTimeQuota = - (stats.jobCountExpirationTimeElapsed <= now - || stats.jobCountInAllowedTime < mMaxJobCountPerAllowedTime); + (stats.jobRateLimitExpirationTimeElapsed <= now + || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow); return isUnderAllowedTimeQuota && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]); } @@ -770,10 +794,8 @@ public final class QuotaController extends StateController { private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats, final int standbyBucket) { final long now = sElapsedRealtimeClock.millis(); - final boolean isUnderAllowedTimeQuota = - (stats.sessionCountExpirationTimeElapsed <= now - || stats.sessionCountInAllowedTime - < mMaxSessionCountPerAllowedTime); + final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now + || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow); return isUnderAllowedTimeQuota && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket]; } @@ -924,12 +946,18 @@ public final class QuotaController extends StateController { } if (refreshStatsIfOld) { final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket]; + final int jobCountLimit = mMaxBucketJobCounts[standbyBucket]; + final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket]; Timer timer = mPkgTimers.get(userId, packageName); if ((timer != null && timer.isActive()) || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis() - || stats.windowSizeMs != bucketWindowSizeMs) { + || stats.windowSizeMs != bucketWindowSizeMs + || stats.jobCountLimit != jobCountLimit + || stats.sessionCountLimit != sessionCountLimit) { // The stats are no longer valid. stats.windowSizeMs = bucketWindowSizeMs; + stats.jobCountLimit = jobCountLimit; + stats.sessionCountLimit = sessionCountLimit; updateExecutionStatsLocked(userId, packageName, stats); } } @@ -945,7 +973,7 @@ public final class QuotaController extends StateController { stats.executionTimeInMaxPeriodMs = 0; stats.bgJobCountInMaxPeriod = 0; stats.sessionCountInWindow = 0; - stats.quotaCutoffTimeElapsed = 0; + stats.inQuotaTimeElapsed = 0; Timer timer = mPkgTimers.get(userId, packageName); final long nowElapsed = sElapsedRealtimeClock.millis(); @@ -958,12 +986,12 @@ public final class QuotaController extends StateController { // invalidate now. stats.expirationTimeElapsed = nowElapsed; if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, - nowElapsed - mAllowedTimeIntoQuotaMs); + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs); } if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, - nowElapsed - mMaxExecutionTimeIntoQuotaMs); + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); } } @@ -985,36 +1013,40 @@ public final class QuotaController extends StateController { TimingSession session = sessions.get(i); // Window management. - if (startWindowElapsed < session.startTimeElapsed) { - stats.executionTimeInWindowMs += session.endTimeElapsed - session.startTimeElapsed; + if (startWindowElapsed < session.endTimeElapsed) { + final long start; + if (startWindowElapsed < session.startTimeElapsed) { + start = session.startTimeElapsed; + emptyTimeMs = + Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed); + } else { + // The session started before the window but ended within the window. Only + // include the portion that was within the window. + start = startWindowElapsed; + emptyTimeMs = 0; + } + + stats.executionTimeInWindowMs += session.endTimeElapsed - start; stats.bgJobCountInWindow += session.bgJobCount; - emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed); if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, - session.startTimeElapsed + stats.executionTimeInWindowMs - - mAllowedTimeIntoQuotaMs); + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs + + stats.windowSizeMs); + } + if (stats.bgJobCountInWindow >= stats.jobCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.endTimeElapsed + stats.windowSizeMs); } if (i == loopStart || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) > mTimingSessionCoalescingDurationMs) { // Coalesce sessions if they are very close to each other in time sessionCountInWindow++; - } - } else if (startWindowElapsed < session.endTimeElapsed) { - // The session started before the window but ended within the window. Only include - // the portion that was within the window. - stats.executionTimeInWindowMs += session.endTimeElapsed - startWindowElapsed; - stats.bgJobCountInWindow += session.bgJobCount; - emptyTimeMs = 0; - if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, - startWindowElapsed + stats.executionTimeInWindowMs - - mAllowedTimeIntoQuotaMs); - } - if (i == loopStart - || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) - > mTimingSessionCoalescingDurationMs) { - sessionCountInWindow++; + + if (sessionCountInWindow >= stats.sessionCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.endTimeElapsed + stats.windowSizeMs); + } } } @@ -1025,9 +1057,9 @@ public final class QuotaController extends StateController { stats.bgJobCountInMaxPeriod += session.bgJobCount; emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed); if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, session.startTimeElapsed + stats.executionTimeInMaxPeriodMs - - mMaxExecutionTimeIntoQuotaMs); + - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); } } else if (startMaxElapsed < session.endTimeElapsed) { // The session started before the window but ended within the window. Only include @@ -1036,9 +1068,9 @@ public final class QuotaController extends StateController { stats.bgJobCountInMaxPeriod += session.bgJobCount; emptyTimeMs = 0; if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, startMaxElapsed + stats.executionTimeInMaxPeriodMs - - mMaxExecutionTimeIntoQuotaMs); + - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); } } else { // This session ended before the window. No point in going any further. @@ -1094,11 +1126,11 @@ public final class QuotaController extends StateController { stats = new ExecutionStats(); appStats[i] = stats; } - if (stats.jobCountExpirationTimeElapsed <= now) { - stats.jobCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs; - stats.jobCountInAllowedTime = 0; + if (stats.jobRateLimitExpirationTimeElapsed <= now) { + stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs; + stats.jobCountInRateLimitingWindow = 0; } - stats.jobCountInAllowedTime += count; + stats.jobCountInRateLimitingWindow += count; } } @@ -1115,11 +1147,11 @@ public final class QuotaController extends StateController { stats = new ExecutionStats(); appStats[i] = stats; } - if (stats.sessionCountExpirationTimeElapsed <= now) { - stats.sessionCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs; - stats.sessionCountInAllowedTime = 0; + if (stats.sessionRateLimitExpirationTimeElapsed <= now) { + stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs; + stats.sessionCountInRateLimitingWindow = 0; } - stats.sessionCountInAllowedTime++; + stats.sessionCountInRateLimitingWindow++; } } @@ -1367,18 +1399,17 @@ public final class QuotaController extends StateController { } // The time this app will have quota again. - long inQuotaTimeElapsed = stats.quotaCutoffTimeElapsed + stats.windowSizeMs; - if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) { + long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; + if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { + // App hit the rate limit. inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, - stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS); + stats.jobRateLimitExpirationTimeElapsed); } - if (!isUnderJobCountQuota) { + if (!isUnderTimingSessionCountQuota + && stats.sessionCountInWindow < stats.sessionCountLimit) { + // App hit the rate limit. inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, - stats.jobCountExpirationTimeElapsed + mAllowedTimePerPeriodMs); - } - if (!isUnderTimingSessionCountQuota) { - inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, - stats.sessionCountExpirationTimeElapsed + mAllowedTimePerPeriodMs); + stats.sessionRateLimitExpirationTimeElapsed); } // Only schedule the alarm if: // 1. There isn't one currently scheduled @@ -1399,7 +1430,7 @@ public final class QuotaController extends StateController { ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler); alarmListener.setTriggerTime(inQuotaTimeElapsed); } else if (DEBUG) { - Slog.d(TAG, "No need to scheduling start alarm for " + pkgString); + Slog.d(TAG, "No need to schedule start alarm for " + pkgString); } } @@ -1968,14 +1999,15 @@ public final class QuotaController extends StateController { private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working"; private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent"; private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare"; - private static final String KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME = - "max_count_per_allowed_time"; + private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms"; + private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = + "max_job_count_per_rate_limiting_window"; private static final String KEY_MAX_SESSION_COUNT_ACTIVE = "max_session_count_active"; private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working"; private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent"; private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare"; - private static final String KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME = - "max_session_count_per_allowed_time"; + private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = + "max_session_count_per_rate_limiting_window"; private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS = "timing_session_coalescing_duration_ms"; @@ -1984,7 +2016,7 @@ public final class QuotaController extends StateController { private static final long DEFAULT_IN_QUOTA_BUFFER_MS = 30 * 1000L; // 30 seconds private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS = - 10 * 60 * 1000L; // 10 minutes for ACTIVE -- ACTIVE apps can run jobs at any time + DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time private static final long DEFAULT_WINDOW_SIZE_WORKING_MS = 2 * 60 * 60 * 1000L; // 2 hours private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS = @@ -1992,16 +2024,18 @@ public final class QuotaController extends StateController { private static final long DEFAULT_WINDOW_SIZE_RARE_MS = 24 * 60 * 60 * 1000L; // 24 hours private static final long DEFAULT_MAX_EXECUTION_TIME_MS = - 4 * 60 * 60 * 1000L; // 4 hours - private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = - 200; // 1200/hr - private static final int DEFAULT_MAX_JOB_COUNT_WORKING = - 1200; // 600/hr - private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = - 1800; // 225/hr - private static final int DEFAULT_MAX_JOB_COUNT_RARE = - 2400; // 100/hr - private static final int DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME = 20; + 4 * HOUR_IN_MILLIS; + private static final long DEFAULT_RATE_LIMITING_WINDOW_MS = + 10 * MINUTE_IN_MILLIS; + private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20; + private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = // 20/window = 120/hr = 1/session + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; + private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session + (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session + (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session + (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS); private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE = 20; // 120/hr private static final int DEFAULT_MAX_SESSION_COUNT_WORKING = @@ -2010,8 +2044,8 @@ public final class QuotaController extends StateController { 8; // 1/hr private static final int DEFAULT_MAX_SESSION_COUNT_RARE = 3; // .125/hr - private static final int DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME = 20; - private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 0; + private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20; + private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds /** How much time each app will have to run jobs within their standby bucket window. */ public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; @@ -2079,11 +2113,14 @@ public final class QuotaController extends StateController { */ public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE; + /** The period of time used to rate limit recently run jobs. */ + public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS; + /** - * The maximum number of jobs that can run within the past - * {@link #ALLOWED_TIME_PER_PERIOD_MS}. + * The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}. */ - public int MAX_JOB_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME; + public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; /** * The maximum number of {@link TimingSession}s an app can run within this particular @@ -2113,7 +2150,8 @@ public final class QuotaController extends StateController { * The maximum number of {@link TimingSession}s that can run within the past * {@link #ALLOWED_TIME_PER_PERIOD_MS}. */ - public int MAX_SESSION_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME; + public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = + DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; /** * Treat two distinct {@link TimingSession}s as the same if they start and end within this @@ -2122,6 +2160,29 @@ public final class QuotaController extends StateController { public long TIMING_SESSION_COALESCING_DURATION_MS = DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; + // Safeguards + + /** The minimum number of jobs that any bucket will be allowed to run within its window. */ + private static final int MIN_BUCKET_JOB_COUNT = 10; + + /** + * The minimum number of {@link TimingSession}s that any bucket will be allowed to run + * within its window. + */ + private static final int MIN_BUCKET_SESSION_COUNT = 1; + + /** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */ + private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS; + + /** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */ + private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10; + + /** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */ + private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10; + + /** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */ + private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS; + QcConstants(Handler handler) { super(handler); } @@ -2167,8 +2228,11 @@ public final class QuotaController extends StateController { KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT); MAX_JOB_COUNT_RARE = mParser.getInt( KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE); - MAX_JOB_COUNT_PER_ALLOWED_TIME = mParser.getInt( - KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME, DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME); + RATE_LIMITING_WINDOW_MS = mParser.getLong( + KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS); + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt( + KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); MAX_SESSION_COUNT_ACTIVE = mParser.getInt( KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE); MAX_SESSION_COUNT_WORKING = mParser.getInt( @@ -2177,9 +2241,9 @@ public final class QuotaController extends StateController { KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT); MAX_SESSION_COUNT_RARE = mParser.getInt( KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE); - MAX_SESSION_COUNT_PER_ALLOWED_TIME = mParser.getInt( - KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME, - DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME); + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt( + KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong( KEY_TIMING_SESSION_COALESCING_DURATION_MS, DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS); @@ -2192,7 +2256,14 @@ public final class QuotaController extends StateController { synchronized (mLock) { boolean changed = false; - long newAllowedTimeMs = Math.min(MAX_PERIOD_MS, + long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS, + Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS)); + if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) { + mMaxExecutionTimeMs = newMaxExecutionTimeMs; + mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + changed = true; + } + long newAllowedTimeMs = Math.min(mMaxExecutionTimeMs, Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_MS)); if (mAllowedTimePerPeriodMs != newAllowedTimeMs) { mAllowedTimePerPeriodMs = newAllowedTimeMs; @@ -2231,47 +2302,44 @@ public final class QuotaController extends StateController { mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs; changed = true; } - long newMaxExecutionTimeMs = Math.max(60 * MINUTE_IN_MILLIS, - Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS)); - if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) { - mMaxExecutionTimeMs = newMaxExecutionTimeMs; - mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS, + Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS)); + if (mRateLimitingWindowMs != newRateLimitingWindowMs) { + mRateLimitingWindowMs = newRateLimitingWindowMs; changed = true; } - int newMaxCountPerAllowedPeriod = Math.max(10, - MAX_JOB_COUNT_PER_ALLOWED_TIME); - if (mMaxJobCountPerAllowedTime != newMaxCountPerAllowedPeriod) { - mMaxJobCountPerAllowedTime = newMaxCountPerAllowedPeriod; + int newMaxJobCountPerRateLimitingWindow = Math.max( + MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); + if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) { + mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow; changed = true; } - int newActiveMaxJobCount = Math.max(mMaxJobCountPerAllowedTime, - Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE)); + int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, 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, MAX_JOB_COUNT_WORKING)); + int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, 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, MAX_JOB_COUNT_FREQUENT)); + int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, 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, MAX_JOB_COUNT_RARE)); + int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE); if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) { mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount; changed = true; } - int newMaxSessionCountPerAllowedPeriod = Math.max(10, - MAX_SESSION_COUNT_PER_ALLOWED_TIME); - if (mMaxSessionCountPerAllowedTime != newMaxSessionCountPerAllowedPeriod) { - mMaxSessionCountPerAllowedTime = newMaxSessionCountPerAllowedPeriod; + int newMaxSessionCountPerRateLimitPeriod = Math.max( + MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); + if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) { + mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod; changed = true; } int newActiveMaxSessionCount = @@ -2332,14 +2400,15 @@ public final class QuotaController extends StateController { pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println(); pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println(); pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println(); - pw.printPair(KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME, MAX_JOB_COUNT_PER_ALLOWED_TIME) - .println(); + pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println(); + pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println(); pw.printPair(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println(); pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println(); pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println(); pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println(); - pw.printPair(KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME, MAX_SESSION_COUNT_PER_ALLOWED_TIME) - .println(); + pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println(); pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS, TIMING_SESSION_COALESCING_DURATION_MS).println(); pw.decreaseIndent(); @@ -2365,8 +2434,10 @@ public final class QuotaController extends StateController { proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT); proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE); - proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_ALLOWED_TIME, - MAX_JOB_COUNT_PER_ALLOWED_TIME); + proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS, + RATE_LIMITING_WINDOW_MS); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE); proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING, @@ -2375,8 +2446,8 @@ public final class QuotaController extends StateController { MAX_SESSION_COUNT_FREQUENT); proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE); - proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_ALLOWED_TIME, - MAX_SESSION_COUNT_PER_ALLOWED_TIME); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS, TIMING_SESSION_COALESCING_DURATION_MS); proto.end(qcToken); @@ -2431,8 +2502,18 @@ public final class QuotaController extends StateController { } @VisibleForTesting - int getMaxJobCountPerAllowedTime() { - return mMaxJobCountPerAllowedTime; + int getMaxJobCountPerRateLimitingWindow() { + return mMaxJobCountPerRateLimitingWindow; + } + + @VisibleForTesting + int getMaxSessionCountPerRateLimitingWindow() { + return mMaxSessionCountPerRateLimitingWindow; + } + + @VisibleForTesting + long getRateLimitingWindowMs() { + return mRateLimitingWindowMs; } @VisibleForTesting @@ -2440,11 +2521,6 @@ public final class QuotaController extends StateController { return mTimingSessionCoalescingDurationMs; } - @VisibleForTesting - int getMaxSessionCountPerAllowedTime() { - return mMaxSessionCountPerAllowedTime; - } - @VisibleForTesting @Nullable List getTimingSessions(int userId, String packageName) { @@ -2658,6 +2734,12 @@ public final class QuotaController extends StateController { proto.write( StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS, es.windowSizeMs); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT, + es.jobCountLimit); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT, + es.sessionCountLimit); proto.write( StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS, es.executionTimeInWindowMs); @@ -2674,20 +2756,20 @@ public final class QuotaController extends StateController { StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW, es.sessionCountInWindow); proto.write( - StateControllerProto.QuotaController.ExecutionStats.QUOTA_CUTOFF_TIME_ELAPSED, - es.quotaCutoffTimeElapsed); + StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED, + es.inQuotaTimeElapsed); proto.write( StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED, - es.jobCountExpirationTimeElapsed); + es.jobRateLimitExpirationTimeElapsed); proto.write( - StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_ALLOWED_TIME, - es.jobCountInAllowedTime); + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW, + es.jobCountInRateLimitingWindow); proto.write( StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED, - es.sessionCountExpirationTimeElapsed); + es.sessionRateLimitExpirationTimeElapsed); proto.write( - StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_ALLOWED_TIME, - es.sessionCountInAllowedTime); + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW, + es.sessionCountInRateLimitingWindow); proto.end(esToken); } } 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 4e893575e9d40..1db6b8e581656 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 @@ -386,6 +386,8 @@ public class QuotaControllerTest { ExecutionStats expectedStats = new ExecutionStats(); expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS; expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; final int uid = 10001; mQuotaController.onAppRemovedLocked("com.android.test.remove", uid); @@ -424,6 +426,8 @@ public class QuotaControllerTest { ExecutionStats expectedStats = new ExecutionStats(); expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS; expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; mQuotaController.onUserRemovedLocked(0); assertNull(mQuotaController.getTimingSessions(0, "com.android.test")); @@ -456,6 +460,8 @@ public class QuotaControllerTest { ExecutionStats inputStats = new ExecutionStats(); inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS; + inputStats.jobCountLimit = expectedStats.jobCountLimit = 100; + inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 100; // Invalid time is now +24 hours since there are no sessions at all for the app. expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats); @@ -520,6 +526,7 @@ public class QuotaControllerTest { assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS; + inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 2; // Invalid time is now since the start of the session is at the very edge of the window // cutoff time. expectedStats.expirationTimeElapsed = now; @@ -528,10 +535,13 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; expectedStats.sessionCountInWindow = 3; + expectedStats.inQuotaTimeElapsed = now + 11 * MINUTE_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; + inputStats.jobCountLimit = expectedStats.jobCountLimit = 6; + inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 100; // Invalid time is now since the session straddles the window cutoff time. expectedStats.expirationTimeElapsed = now; expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS; @@ -539,8 +549,7 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; expectedStats.sessionCountInWindow = 4; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) - + mQcConstants.IN_QUOTA_BUFFER_MS; + expectedStats.inQuotaTimeElapsed = now + 5 * MINUTE_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -553,8 +562,9 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; expectedStats.sessionCountInWindow = 4; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) - + mQcConstants.IN_QUOTA_BUFFER_MS; + // App goes under job execution time limit in ~61 minutes, but will be under job count limit + // in 65 minutes. + expectedStats.inQuotaTimeElapsed = now + 65 * MINUTE_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -567,8 +577,7 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; expectedStats.sessionCountInWindow = 5; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) - + mQcConstants.IN_QUOTA_BUFFER_MS; + expectedStats.inQuotaTimeElapsed = now + 4 * HOUR_IN_MILLIS + 5 * MINUTE_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -577,6 +586,7 @@ public class QuotaControllerTest { .add(0, createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3)); inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS; + inputStats.jobCountLimit = expectedStats.jobCountLimit = 100; // 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.expirationTimeElapsed = now + HOUR_IN_MILLIS; @@ -585,7 +595,7 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 18; expectedStats.sessionCountInWindow = 5; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -602,7 +612,7 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; expectedStats.sessionCountInWindow = 5; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -627,6 +637,8 @@ public class QuotaControllerTest { // Active expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE; expectedStats.expirationTimeElapsed = now + 4 * MINUTE_IN_MILLIS; expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 5; @@ -638,44 +650,102 @@ public class QuotaControllerTest { // Working expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING; expectedStats.expirationTimeElapsed = now; expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 10; expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; expectedStats.sessionCountInWindow = 2; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 3 * MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX)); // Frequent expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT; expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 15; expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; expectedStats.sessionCountInWindow = 3; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX)); // Rare expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 20; expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; expectedStats.sessionCountInWindow = 4; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 22 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX)); } + /** + * Tests that getExecutionStatsLocked returns the correct stats soon after device startup. + */ + @Test + public void testGetExecutionStatsLocked_Values_BeginningOfTime() { + // Set time to 3 minutes after boot. + advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis()); + advanceElapsedClock(3 * MINUTE_IN_MILLIS); + + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2)); + + ExecutionStats expectedStats = new ExecutionStats(); + + // Active + expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE; + expectedStats.expirationTimeElapsed = 11 * MINUTE_IN_MILLIS; + expectedStats.executionTimeInWindowMs = MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 2; + expectedStats.executionTimeInMaxPeriodMs = MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 2; + expectedStats.sessionCountInWindow = 1; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX)); + + // Working + expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING; + expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX)); + + // Frequent + expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT; + expectedStats.expirationTimeElapsed = 8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX)); + + // Rare + expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; + expectedStats.expirationTimeElapsed = 24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX)); + } + /** * Tests that getExecutionStatsLocked returns the correct timing session stats when coalescing. */ @@ -850,13 +920,15 @@ public class QuotaControllerTest { ExecutionStats expectedStats = new ExecutionStats(); expectedStats.windowSizeMs = originalStatsActive.windowSizeMs; + expectedStats.jobCountLimit = originalStatsActive.jobCountLimit; + expectedStats.sessionCountLimit = originalStatsActive.sessionCountLimit; expectedStats.expirationTimeElapsed = originalStatsActive.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow; expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs; expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod; expectedStats.sessionCountInWindow = originalStatsActive.sessionCountInWindow; - expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed; + expectedStats.inQuotaTimeElapsed = originalStatsActive.inQuotaTimeElapsed; final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX); // Stats for the same bucket should use the same object. @@ -864,33 +936,39 @@ public class QuotaControllerTest { assertEquals(expectedStats, newStatsActive); expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs; + expectedStats.jobCountLimit = originalStatsWorking.jobCountLimit; + expectedStats.sessionCountLimit = originalStatsWorking.sessionCountLimit; expectedStats.expirationTimeElapsed = originalStatsWorking.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow; expectedStats.sessionCountInWindow = originalStatsWorking.sessionCountInWindow; - expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed; + expectedStats.inQuotaTimeElapsed = originalStatsWorking.inQuotaTimeElapsed; final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX); assertTrue(originalStatsWorking == newStatsWorking); assertNotEquals(expectedStats, newStatsWorking); expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs; + expectedStats.jobCountLimit = originalStatsFrequent.jobCountLimit; + expectedStats.sessionCountLimit = originalStatsFrequent.sessionCountLimit; expectedStats.expirationTimeElapsed = originalStatsFrequent.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow; expectedStats.sessionCountInWindow = originalStatsFrequent.sessionCountInWindow; - expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed; + expectedStats.inQuotaTimeElapsed = originalStatsFrequent.inQuotaTimeElapsed; final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX); assertTrue(originalStatsFrequent == newStatsFrequent); assertNotEquals(expectedStats, newStatsFrequent); expectedStats.windowSizeMs = originalStatsRare.windowSizeMs; + expectedStats.jobCountLimit = originalStatsRare.jobCountLimit; + expectedStats.sessionCountLimit = originalStatsRare.sessionCountLimit; expectedStats.expirationTimeElapsed = originalStatsRare.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow; expectedStats.sessionCountInWindow = originalStatsRare.sessionCountInWindow; - expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed; + expectedStats.inQuotaTimeElapsed = originalStatsRare.inQuotaTimeElapsed; final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX); assertTrue(originalStatsRare == newStatsRare); @@ -1065,7 +1143,7 @@ public class QuotaControllerTest { public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() { setDischarging(); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME; + final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; mQuotaController.saveTimingSession(0, "com.android.test.spam", createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25)); mQuotaController.saveTimingSession(0, "com.android.test.spam", @@ -1100,7 +1178,7 @@ public class QuotaControllerTest { public void testIsWithinQuotaLocked_OverDuration_OverJobCount() { setDischarging(); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME; + final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; mQuotaController.saveTimingSession(0, "com.android.test", createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25)); mQuotaController.saveTimingSession(0, "com.android.test", @@ -1141,7 +1219,7 @@ public class QuotaControllerTest { advanceElapsedClock(10 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS); assertEquals(2, mQuotaController.getExecutionStatsLocked( - SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInAllowedTime); + SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInRateLimitingWindow); assertTrue(mQuotaController.isWithinQuotaLocked(jobStatus)); } @@ -1212,9 +1290,9 @@ public class QuotaControllerTest { mQuotaController.getTimingSessions(SOURCE_USER_ID, fgChangerPkgName).size()); for (int i = ACTIVE_INDEX; i < RARE_INDEX; ++i) { assertEquals(42, mQuotaController.getExecutionStatsLocked( - SOURCE_USER_ID, fgChangerPkgName, i).jobCountInAllowedTime); + SOURCE_USER_ID, fgChangerPkgName, i).jobCountInRateLimitingWindow); assertEquals(1, mQuotaController.getExecutionStatsLocked( - SOURCE_USER_ID, unaffectedPkgName, i).jobCountInAllowedTime); + SOURCE_USER_ID, unaffectedPkgName, i).jobCountInRateLimitingWindow); } } @@ -1555,26 +1633,29 @@ public class QuotaControllerTest { } @Test - public void testMaybeScheduleStartAlarmLocked_JobCount_AllowedTime() { + public void testMaybeScheduleStartAlarmLocked_JobCount_RateLimitingWindow() { + // Set rate limiting period different from allowed time to confirm code sets based on + // the former. + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS = 10 * MINUTE_IN_MILLIS; + mQcConstants.RATE_LIMITING_WINDOW_MS = 5 * MINUTE_IN_MILLIS; + mQcConstants.updateConstants(); + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); final int standbyBucket = WORKING_INDEX; ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - stats.jobCountInAllowedTime = - mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME + 2; + stats.jobCountInRateLimitingWindow = + mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW + 2; // Invalid time in the past, so the count shouldn't be used. - stats.jobCountExpirationTimeElapsed = - now - mQuotaController.getAllowedTimePerPeriodMs() / 2; + stats.jobRateLimitExpirationTimeElapsed = now - 5 * MINUTE_IN_MILLIS / 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(); + // Valid time in the future, so the count should be used. + stats.jobRateLimitExpirationTimeElapsed = now + 5 * MINUTE_IN_MILLIS / 2; + final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed; mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); verify(mAlarmManager, times(1)) @@ -1732,12 +1813,13 @@ public class QuotaControllerTest { mQcConstants.MAX_JOB_COUNT_WORKING = 4000; mQcConstants.MAX_JOB_COUNT_FREQUENT = 3000; mQcConstants.MAX_JOB_COUNT_RARE = 2000; - mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 500; + mQcConstants.RATE_LIMITING_WINDOW_MS = 15 * MINUTE_IN_MILLIS; + mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 500; mQcConstants.MAX_SESSION_COUNT_ACTIVE = 500; mQcConstants.MAX_SESSION_COUNT_WORKING = 400; mQcConstants.MAX_SESSION_COUNT_FREQUENT = 300; mQcConstants.MAX_SESSION_COUNT_RARE = 200; - mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = 50; + mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 50; mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 10 * SECOND_IN_MILLIS; mQcConstants.updateConstants(); @@ -1750,12 +1832,13 @@ public class QuotaControllerTest { 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(15 * MINUTE_IN_MILLIS, mQuotaController.getRateLimitingWindowMs()); + assertEquals(500, mQuotaController.getMaxJobCountPerRateLimitingWindow()); assertEquals(5000, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]); assertEquals(4000, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]); assertEquals(3000, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]); assertEquals(2000, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]); - assertEquals(50, mQuotaController.getMaxSessionCountPerAllowedTime()); + assertEquals(50, mQuotaController.getMaxSessionCountPerRateLimitingWindow()); assertEquals(500, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]); assertEquals(400, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]); assertEquals(300, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]); @@ -1778,12 +1861,13 @@ public class QuotaControllerTest { mQcConstants.MAX_JOB_COUNT_WORKING = 1; mQcConstants.MAX_JOB_COUNT_FREQUENT = 1; mQcConstants.MAX_JOB_COUNT_RARE = 1; - mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 0; + mQcConstants.RATE_LIMITING_WINDOW_MS = 15 * SECOND_IN_MILLIS; + mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 0; mQcConstants.MAX_SESSION_COUNT_ACTIVE = -1; - mQcConstants.MAX_SESSION_COUNT_WORKING = 1; - mQcConstants.MAX_SESSION_COUNT_FREQUENT = 2; - mQcConstants.MAX_SESSION_COUNT_RARE = 1; - mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = 0; + mQcConstants.MAX_SESSION_COUNT_WORKING = 0; + mQcConstants.MAX_SESSION_COUNT_FREQUENT = -3; + mQcConstants.MAX_SESSION_COUNT_RARE = 0; + mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 0; mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = -1; mQcConstants.updateConstants(); @@ -1795,16 +1879,17 @@ public class QuotaControllerTest { 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]); - assertEquals(10, mQuotaController.getMaxSessionCountPerAllowedTime()); - assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]); - assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]); - assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]); - assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]); + assertEquals(30 * SECOND_IN_MILLIS, mQuotaController.getRateLimitingWindowMs()); + assertEquals(10, mQuotaController.getMaxJobCountPerRateLimitingWindow()); + assertEquals(10, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]); + assertEquals(10, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]); + assertEquals(10, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]); + assertEquals(10, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]); + assertEquals(10, mQuotaController.getMaxSessionCountPerRateLimitingWindow()); + assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]); + assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]); + assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]); + assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]); assertEquals(0, mQuotaController.getTimingSessionCoalescingDurationMs()); // Test larger than a day. Controller should cap at one day. @@ -1815,6 +1900,7 @@ public class QuotaControllerTest { mQcConstants.WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS; mQcConstants.WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS; mQcConstants.MAX_EXECUTION_TIME_MS = 25 * HOUR_IN_MILLIS; + mQcConstants.RATE_LIMITING_WINDOW_MS = 25 * HOUR_IN_MILLIS; mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 25 * HOUR_IN_MILLIS; mQcConstants.updateConstants(); @@ -1826,6 +1912,7 @@ public class QuotaControllerTest { assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]); assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]); assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs()); + assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getRateLimitingWindowMs()); assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getTimingSessionCoalescingDurationMs()); } @@ -2126,7 +2213,7 @@ public class QuotaControllerTest { assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - assertEquals(0, stats.jobCountInAllowedTime); + assertEquals(0, stats.jobCountInRateLimitingWindow); setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); mQuotaController.prepareForExecutionLocked(jobFg1); @@ -2138,7 +2225,7 @@ public class QuotaControllerTest { mQuotaController.maybeStopTrackingJobLocked(jobFg2, null, false); assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); - assertEquals(0, stats.jobCountInAllowedTime); + assertEquals(0, stats.jobCountInRateLimitingWindow); } /** @@ -2154,7 +2241,7 @@ public class QuotaControllerTest { ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - assertEquals(0, stats.jobCountInAllowedTime); + assertEquals(0, stats.jobCountInRateLimitingWindow); setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); mQuotaController.prepareForExecutionLocked(jobBg1); @@ -2165,7 +2252,7 @@ public class QuotaControllerTest { advanceElapsedClock(10 * SECOND_IN_MILLIS); mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); - assertEquals(2, stats.jobCountInAllowedTime); + assertEquals(2, stats.jobCountInRateLimitingWindow); } /** @@ -2421,10 +2508,10 @@ public class QuotaControllerTest { /** * Tests that the start alarm is properly scheduled when a job has been throttled due to the job - * count quota. + * count rate limiting. */ @Test - public void testStartAlarmScheduled_JobCount_AllowedTime() { + public void testStartAlarmScheduled_JobCount_RateLimitingWindow() { // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests // because it schedules an alarm too. Prevent it from doing so. spyOn(mQuotaController); @@ -2432,7 +2519,7 @@ public class QuotaControllerTest { // Essentially disable session throttling. mQcConstants.MAX_SESSION_COUNT_WORKING = - mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE; + mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = Integer.MAX_VALUE; mQcConstants.updateConstants(); final int standbyBucket = WORKING_INDEX; @@ -2444,7 +2531,7 @@ public class QuotaControllerTest { verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Ran jobs up to the job limit. All of them should be allowed to run. - for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME; ++i) { + for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; ++i) { JobStatus job = createJobStatus("testStartAlarmScheduled_JobCount_AllowedTime", i); setStandbyBucket(WORKING_INDEX, job); mQuotaController.maybeStartTrackingJobLocked(job, null); @@ -2466,18 +2553,17 @@ public class QuotaControllerTest { ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - final long expectedWorkingAlarmTime = - stats.jobCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS; + final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed; verify(mAlarmManager, times(1)) .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); } /** - * Tests that the start alarm is properly scheduled when a job has been throttled due to the job - * count quota. + * Tests that the start alarm is properly scheduled when a job has been throttled due to the + * session count rate limiting. */ @Test - public void testStartAlarmScheduled_TimingSessionCount_AllowedTime() { + public void testStartAlarmScheduled_TimingSessionCount_RateLimitingWindow() { // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests // because it schedules an alarm too. Prevent it from doing so. spyOn(mQuotaController); @@ -2485,10 +2571,10 @@ public class QuotaControllerTest { // Essentially disable job count throttling. mQcConstants.MAX_JOB_COUNT_FREQUENT = - mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE; - // Make sure throttling is because of COUNT_PER_ALLOWED_TIME. + mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = Integer.MAX_VALUE; + // Make sure throttling is because of COUNT_PER_RATE_LIMITING_WINDOW. mQcConstants.MAX_SESSION_COUNT_FREQUENT = - mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME + 1; + mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW + 1; mQcConstants.updateConstants(); final int standbyBucket = FREQUENT_INDEX; @@ -2500,7 +2586,7 @@ public class QuotaControllerTest { verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Ran jobs up to the job limit. All of them should be allowed to run. - for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME; ++i) { + for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; ++i) { JobStatus job = createJobStatus( "testStartAlarmScheduled_TimingSessionCount_AllowedTime", i); setStandbyBucket(FREQUENT_INDEX, job); @@ -2515,7 +2601,7 @@ public class QuotaControllerTest { // Start alarm shouldn't have been scheduled since the app was in quota up until this point. verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); - // The app is now out of job count quota + // The app is now out of session count quota JobStatus throttledJob = createJobStatus( "testStartAlarmScheduled_TimingSessionCount_AllowedTime", 42); mQuotaController.maybeStartTrackingJobLocked(throttledJob, null); @@ -2523,8 +2609,7 @@ public class QuotaControllerTest { ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - final long expectedWorkingAlarmTime = - stats.sessionCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS; + final long expectedWorkingAlarmTime = stats.sessionRateLimitExpirationTimeElapsed; verify(mAlarmManager, times(1)) .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); }