diff options
| author | 2019-05-14 13:44:32 -0700 | |
|---|---|---|
| committer | 2019-05-17 16:13:45 -0700 | |
| commit | d48eef0d22e1c6442af636b97a8ed39f1550c8b4 (patch) | |
| tree | 9e729547ad208ffdff39b9d3f8c08a2d5b0f5e5a | |
| parent | b3196e53d7bb17745526ab3c6821a57784d2bf7c (diff) | |
Add throttling by job run session.
A session is considered a period of time when jobs for an app ran.
Overlapping jobs are counted as part of the same session. This adds a
way to limit the number of job sessions an app can run. This includes a
mechanism to coalesce sessions -- if a second session started soon after
one just ended, they will only be counted as one session.
Bug: 132227621
Test: atest com.android.server.job.controllers.QuotaControllerTest
Test: atest CtsJobSchedulerTestCases
Change-Id: Id2ac4037731f57547d00985e8d549b9e990a5f3e
3 files changed, 634 insertions, 28 deletions
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 0df2c833fd9d..3f8ddff5327c 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -276,6 +276,24 @@ message ConstantsProto { // 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; + // 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; + // The maximum number of timing sessions an app can run within this particular standby + // bucket's window size. + optional int32 max_session_count_working = 14; + // The maximum number of timing sessions an app can run within this particular standby + // bucket's window size. + optional int32 max_session_count_frequent = 15; + // The maximum number of timing sessions an app can run within this particular standby + // 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; + // 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; } optional QuotaController quota_controller = 24; @@ -511,6 +529,12 @@ message StateControllerProto { 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. + */ + 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 @@ -535,6 +559,21 @@ message StateControllerProto { * ConstantsProto.QuotaController.allowed_time_per_period_ms. */ optional int32 job_count_in_allowed_time = 10; + + /** + * 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; } 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 2a9d3f30c782..f560d69310a3 100644 --- a/services/core/java/com/android/server/job/controllers/QuotaController.java +++ b/services/core/java/com/android/server/job/controllers/QuotaController.java @@ -254,6 +254,12 @@ public final class QuotaController extends StateController { public int bgJobCountInMaxPeriod; /** + * 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. + */ + 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 @@ -274,21 +280,34 @@ public final class QuotaController extends StateController { */ public int jobCountInAllowedTime; + /** + * The time after which {@link #sessionCountInAllowedTime} should be considered + * invalid, in the elapsed realtime timebase. + */ + public long sessionCountExpirationTimeElapsed; + + /** + * 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}. + */ + public int sessionCountInAllowedTime; + @Override public String toString() { - return new StringBuilder() - .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(", ") - .append("jobCountExpirationTime=").append(jobCountExpirationTimeElapsed) - .append(", ") - .append("jobCountInAllowedTime=").append(jobCountInAllowedTime) - .toString(); + return "expirationTime=" + expirationTimeElapsed + ", " + + "windowSize=" + windowSizeMs + ", " + + "executionTimeInWindow=" + executionTimeInWindowMs + ", " + + "bgJobCountInWindow=" + bgJobCountInWindow + ", " + + "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", " + + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", " + + "sessionCountInWindow=" + sessionCountInWindow + ", " + + "quotaCutoffTime=" + quotaCutoffTimeElapsed + ", " + + "jobCountExpirationTime=" + jobCountExpirationTimeElapsed + ", " + + "jobCountInAllowedTime=" + jobCountInAllowedTime + ", " + + "sessionCountExpirationTime=" + sessionCountExpirationTimeElapsed + ", " + + "sessionCountInAllowedTime=" + sessionCountInAllowedTime; } @Override @@ -300,10 +319,15 @@ public final class QuotaController extends StateController { && 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.jobCountInAllowedTime == other.jobCountInAllowedTime + && this.sessionCountExpirationTimeElapsed + == other.sessionCountExpirationTimeElapsed + && this.sessionCountInAllowedTime + == other.sessionCountInAllowedTime; } else { return false; } @@ -318,9 +342,12 @@ public final class QuotaController extends StateController { 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; return result; } } @@ -401,6 +428,12 @@ public final class QuotaController extends StateController { /** The maximum number of jobs that can run within the past {@link #mAllowedTimePerPeriodMs}. */ private int mMaxJobCountPerAllowedTime = 20; + /** + * The maximum number of {@link TimingSession}s that can run within the past {@link + * #mAllowedTimePerPeriodMs}. + */ + private int mMaxSessionCountPerAllowedTime = 20; + private long mNextCleanupTimeElapsed = 0; private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener = new AlarmManager.OnAlarmListener() { @@ -480,6 +513,29 @@ public final class QuotaController extends StateController { /** 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 + * {@link TimingSession}s within the latest time interval of its rolling window size. + * + * @see #mBucketPeriodsMs + */ + private final int[] mMaxBucketSessionCounts = new int[]{ + QcConstants.DEFAULT_MAX_SESSION_COUNT_ACTIVE, + QcConstants.DEFAULT_MAX_SESSION_COUNT_WORKING, + QcConstants.DEFAULT_MAX_SESSION_COUNT_FREQUENT, + 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; + /** 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. */ @@ -695,14 +751,10 @@ public final class QuotaController extends StateController { 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); + ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + return getRemainingExecutionTimeLocked(stats) > 0 + && isUnderJobCountQuotaLocked(stats, standbyBucket) + && isUnderSessionCountQuotaLocked(stats, standbyBucket); } private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats, @@ -715,6 +767,17 @@ public final class QuotaController extends StateController { && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]); } + private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats, + final int standbyBucket) { + final long now = sElapsedRealtimeClock.millis(); + final boolean isUnderAllowedTimeQuota = + (stats.sessionCountExpirationTimeElapsed <= now + || stats.sessionCountInAllowedTime + < mMaxSessionCountPerAllowedTime); + return isUnderAllowedTimeQuota + && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket]; + } + @VisibleForTesting long getRemainingExecutionTimeLocked(@NonNull final JobStatus jobStatus) { return getRemainingExecutionTimeLocked(jobStatus.getSourceUserId(), @@ -739,7 +802,11 @@ public final class QuotaController extends StateController { if (standbyBucket == NEVER_INDEX) { return 0; } - final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + return getRemainingExecutionTimeLocked( + getExecutionStatsLocked(userId, packageName, standbyBucket)); + } + + private long getRemainingExecutionTimeLocked(@NonNull ExecutionStats stats) { return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs, mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs); } @@ -877,6 +944,7 @@ public final class QuotaController extends StateController { stats.bgJobCountInWindow = 0; stats.executionTimeInMaxPeriodMs = 0; stats.bgJobCountInMaxPeriod = 0; + stats.sessionCountInWindow = 0; stats.quotaCutoffTimeElapsed = 0; Timer timer = mPkgTimers.get(userId, packageName); @@ -906,12 +974,14 @@ public final class QuotaController extends StateController { final long startWindowElapsed = nowElapsed - stats.windowSizeMs; final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; + int sessionCountInWindow = 0; // The minimum time between the start time and the beginning of the sessions that were // looked at --> how much time the stats will be valid for. long emptyTimeMs = Long.MAX_VALUE; // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get // the most recent ones. - for (int i = sessions.size() - 1; i >= 0; --i) { + final int loopStart = sessions.size() - 1; + for (int i = loopStart; i >= 0; --i) { TimingSession session = sessions.get(i); // Window management. @@ -924,6 +994,12 @@ public final class QuotaController extends StateController { session.startTimeElapsed + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs); } + 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. @@ -935,6 +1011,11 @@ public final class QuotaController extends StateController { startWindowElapsed + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs); } + if (i == loopStart + || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) + > mTimingSessionCoalescingDurationMs) { + sessionCountInWindow++; + } } // Max period check. @@ -965,9 +1046,27 @@ public final class QuotaController extends StateController { } } stats.expirationTimeElapsed = nowElapsed + emptyTimeMs; + stats.sessionCountInWindow = sessionCountInWindow; } - private void invalidateAllExecutionStatsLocked(final int userId, + /** Invalidate ExecutionStats for all apps. */ + @VisibleForTesting + void invalidateAllExecutionStatsLocked() { + final long nowElapsed = sElapsedRealtimeClock.millis(); + mExecutionStatsCache.forEach((appStats) -> { + if (appStats != null) { + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats != null) { + stats.expirationTimeElapsed = nowElapsed; + } + } + } + }); + } + + @VisibleForTesting + void invalidateAllExecutionStatsLocked(final int userId, @NonNull final String packageName) { ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); if (appStats != null) { @@ -1003,6 +1102,27 @@ public final class QuotaController extends StateController { } } + private void incrementTimingSessionCount(final int userId, @NonNull final String packageName) { + 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.sessionCountExpirationTimeElapsed <= now) { + stats.sessionCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs; + stats.sessionCountInAllowedTime = 0; + } + stats.sessionCountInAllowedTime++; + } + } + @VisibleForTesting void saveTimingSession(final int userId, @NonNull final String packageName, @NonNull final TimingSession session) { @@ -1216,11 +1336,14 @@ public final class QuotaController extends StateController { final String pkgString = string(userId, packageName); ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket); + final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats, + standbyBucket); QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs - && isUnderJobCountQuota) { + && isUnderJobCountQuota + && isUnderTimingSessionCountQuota) { // Already in quota. Why was this method called? if (DEBUG) { Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString @@ -1253,6 +1376,10 @@ public final class QuotaController extends StateController { inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, stats.jobCountExpirationTimeElapsed + mAllowedTimePerPeriodMs); } + if (!isUnderTimingSessionCountQuota) { + inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, + stats.sessionCountExpirationTimeElapsed + 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 @@ -1483,6 +1610,7 @@ public final class QuotaController extends StateController { // of jobs. // However, cancel the currently scheduled cutoff since it's not currently useful. cancelCutoff(); + incrementTimingSessionCount(mPkg.userId, mPkg.packageName); } /** @@ -1842,6 +1970,14 @@ public final class QuotaController extends StateController { 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_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_TIMING_SESSION_COALESCING_DURATION_MS = + "timing_session_coalescing_duration_ms"; private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_MS = 10 * 60 * 1000L; // 10 minutes @@ -1866,6 +2002,16 @@ public final class QuotaController extends StateController { private static final int DEFAULT_MAX_JOB_COUNT_RARE = 2400; // 100/hr private static final int DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME = 20; + private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE = + 20; // 120/hr + private static final int DEFAULT_MAX_SESSION_COUNT_WORKING = + 10; // 5/hr + private static final int DEFAULT_MAX_SESSION_COUNT_FREQUENT = + 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; /** 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; @@ -1939,6 +2085,43 @@ public final class QuotaController extends StateController { */ public int MAX_JOB_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME; + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_ACTIVE = DEFAULT_MAX_SESSION_COUNT_ACTIVE; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_WORKING = DEFAULT_MAX_SESSION_COUNT_WORKING; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_FREQUENT = DEFAULT_MAX_SESSION_COUNT_FREQUENT; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_RARE = DEFAULT_MAX_SESSION_COUNT_RARE; + + /** + * 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; + + /** + * Treat two distinct {@link TimingSession}s as the same if they start and end within this + * amount of time of each other. + */ + public long TIMING_SESSION_COALESCING_DURATION_MS = + DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; + QcConstants(Handler handler) { super(handler); } @@ -1986,6 +2169,20 @@ public final class QuotaController extends StateController { 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); + MAX_SESSION_COUNT_ACTIVE = mParser.getInt( + KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE); + MAX_SESSION_COUNT_WORKING = mParser.getInt( + KEY_MAX_SESSION_COUNT_WORKING, DEFAULT_MAX_SESSION_COUNT_WORKING); + MAX_SESSION_COUNT_FREQUENT = mParser.getInt( + 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); + TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong( + KEY_TIMING_SESSION_COALESCING_DURATION_MS, + DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS); updateConstants(); } @@ -2071,11 +2268,48 @@ public final class QuotaController extends StateController { mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount; changed = true; } + int newMaxSessionCountPerAllowedPeriod = Math.max(10, + MAX_SESSION_COUNT_PER_ALLOWED_TIME); + if (mMaxSessionCountPerAllowedTime != newMaxSessionCountPerAllowedPeriod) { + mMaxSessionCountPerAllowedTime = newMaxSessionCountPerAllowedPeriod; + changed = true; + } + int newActiveMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_ACTIVE); + if (mMaxBucketSessionCounts[ACTIVE_INDEX] != newActiveMaxSessionCount) { + mMaxBucketSessionCounts[ACTIVE_INDEX] = newActiveMaxSessionCount; + changed = true; + } + int newWorkingMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_WORKING); + if (mMaxBucketSessionCounts[WORKING_INDEX] != newWorkingMaxSessionCount) { + mMaxBucketSessionCounts[WORKING_INDEX] = newWorkingMaxSessionCount; + changed = true; + } + int newFrequentMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_FREQUENT); + if (mMaxBucketSessionCounts[FREQUENT_INDEX] != newFrequentMaxSessionCount) { + mMaxBucketSessionCounts[FREQUENT_INDEX] = newFrequentMaxSessionCount; + changed = true; + } + int newRareMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_RARE); + if (mMaxBucketSessionCounts[RARE_INDEX] != newRareMaxSessionCount) { + mMaxBucketSessionCounts[RARE_INDEX] = newRareMaxSessionCount; + changed = true; + } + long newSessionCoalescingDurationMs = Math.min(15 * MINUTE_IN_MILLIS, + Math.max(0, TIMING_SESSION_COALESCING_DURATION_MS)); + if (mTimingSessionCoalescingDurationMs != newSessionCoalescingDurationMs) { + mTimingSessionCoalescingDurationMs = newSessionCoalescingDurationMs; + changed = true; + } if (changed && mShouldThrottle) { // Update job bookkeeping out of band. BackgroundThread.getHandler().post(() -> { synchronized (mLock) { + invalidateAllExecutionStatsLocked(); maybeUpdateAllConstraintsLocked(); } }); @@ -2100,6 +2334,14 @@ public final class QuotaController extends StateController { 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_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_TIMING_SESSION_COALESCING_DURATION_MS, + TIMING_SESSION_COALESCING_DURATION_MS).println(); pw.decreaseIndent(); } @@ -2125,6 +2367,18 @@ public final class QuotaController extends StateController { 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.MAX_SESSION_COUNT_ACTIVE, + MAX_SESSION_COUNT_ACTIVE); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING, + MAX_SESSION_COUNT_WORKING); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_FREQUENT, + 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.TIMING_SESSION_COALESCING_DURATION_MS, + TIMING_SESSION_COALESCING_DURATION_MS); proto.end(qcToken); } } @@ -2144,6 +2398,12 @@ public final class QuotaController extends StateController { @VisibleForTesting @NonNull + int[] getBucketMaxSessionCounts() { + return mMaxBucketSessionCounts; + } + + @VisibleForTesting + @NonNull long[] getBucketWindowSizes() { return mBucketPeriodsMs; } @@ -2176,6 +2436,16 @@ public final class QuotaController extends StateController { } @VisibleForTesting + long getTimingSessionCoalescingDurationMs() { + return mTimingSessionCoalescingDurationMs; + } + + @VisibleForTesting + int getMaxSessionCountPerAllowedTime() { + return mMaxSessionCountPerAllowedTime; + } + + @VisibleForTesting @Nullable List<TimingSession> getTimingSessions(int userId, String packageName) { return mTimingSessions.get(userId, packageName); @@ -2401,6 +2671,9 @@ public final class QuotaController extends StateController { StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_MAX_PERIOD, es.bgJobCountInMaxPeriod); proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW, + es.sessionCountInWindow); + proto.write( StateControllerProto.QuotaController.ExecutionStats.QUOTA_CUTOFF_TIME_ELAPSED, es.quotaCutoffTimeElapsed); proto.write( @@ -2409,6 +2682,12 @@ public final class QuotaController extends StateController { proto.write( StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_ALLOWED_TIME, es.jobCountInAllowedTime); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED, + es.sessionCountExpirationTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_ALLOWED_TIME, + es.sessionCountInAllowedTime); 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 2e7283c287bf..4e893575e9d4 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 @@ -469,6 +469,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 0; expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.sessionCountInWindow = 0; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -479,6 +480,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 3; expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.sessionCountInWindow = 1; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -490,6 +492,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 3; expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.sessionCountInWindow = 1; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -501,6 +504,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 3; expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.sessionCountInWindow = 1; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -511,6 +515,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 4; expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.sessionCountInWindow = 2; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -522,6 +527,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 5; expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.sessionCountInWindow = 3; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -532,6 +538,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 10; 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; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); @@ -545,6 +552,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 10; 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; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); @@ -558,6 +566,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 15; 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; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); @@ -575,6 +584,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 15; expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 18; + expectedStats.sessionCountInWindow = 5; expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + mQcConstants.IN_QUOTA_BUFFER_MS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); @@ -585,12 +595,13 @@ public class QuotaControllerTest { createTimingSession(now - (24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 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. + // Invalid time is now since the earliest session straddles the max period cutoff time. expectedStats.expirationTimeElapsed = now; expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 15; expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.sessionCountInWindow = 5; expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + mQcConstants.IN_QUOTA_BUFFER_MS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); @@ -621,6 +632,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = 5; expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.sessionCountInWindow = 1; assertEquals(expectedStats, mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX)); @@ -631,6 +643,7 @@ public class QuotaControllerTest { 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) + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, @@ -643,6 +656,7 @@ public class QuotaControllerTest { 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) + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, @@ -655,6 +669,7 @@ public class QuotaControllerTest { 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) + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, @@ -662,10 +677,153 @@ public class QuotaControllerTest { } /** + * Tests that getExecutionStatsLocked returns the correct timing session stats when coalescing. + */ + @Test + public void testGetExecutionStatsLocked_CoalescingSessions() { + for (int i = 0; i < 10; ++i) { + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis(), + 5 * MINUTE_IN_MILLIS, 5)); + advanceElapsedClock(5 * MINUTE_IN_MILLIS); + advanceElapsedClock(5 * MINUTE_IN_MILLIS); + for (int j = 0; j < 5; ++j) { + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis(), + MINUTE_IN_MILLIS, 2)); + advanceElapsedClock(MINUTE_IN_MILLIS); + advanceElapsedClock(54 * SECOND_IN_MILLIS); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis(), 500, 1)); + advanceElapsedClock(500); + advanceElapsedClock(400); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis(), 100, 1)); + advanceElapsedClock(100); + advanceElapsedClock(5 * SECOND_IN_MILLIS); + } + advanceElapsedClock(40 * MINUTE_IN_MILLIS); + } + + mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 0; + mQcConstants.updateConstants(); + + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(32, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(128, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(160, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + + mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 500; + mQcConstants.updateConstants(); + + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(22, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(88, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(110, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + + mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 1000; + mQcConstants.updateConstants(); + + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(22, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(88, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(110, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + + mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 5 * SECOND_IN_MILLIS; + mQcConstants.updateConstants(); + + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(14, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(56, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(70, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + + mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = MINUTE_IN_MILLIS; + mQcConstants.updateConstants(); + + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(4, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(16, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(20, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + + mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 5 * MINUTE_IN_MILLIS; + mQcConstants.updateConstants(); + + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(2, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(8, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(10, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + + mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 15 * MINUTE_IN_MILLIS; + mQcConstants.updateConstants(); + + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(2, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(8, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(10, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + + // QuotaController caps the duration at 15 minutes, so there shouldn't be any difference + // between an hour and 15 minutes. + mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = HOUR_IN_MILLIS; + mQcConstants.updateConstants(); + + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(2, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(8, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(10, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + + /** * Tests that getExecutionStatsLocked properly caches the stats and returns the cached object. */ @Test public void testGetExecutionStatsLocked_Caching() { + spyOn(mQuotaController); + doNothing().when(mQuotaController).invalidateAllExecutionStatsLocked(); + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(0, "com.android.test", createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5)); @@ -697,6 +855,7 @@ public class QuotaControllerTest { expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow; expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs; expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod; + expectedStats.sessionCountInWindow = originalStatsActive.sessionCountInWindow; expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed; final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX); @@ -708,6 +867,7 @@ public class QuotaControllerTest { expectedStats.expirationTimeElapsed = originalStatsWorking.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow; + expectedStats.sessionCountInWindow = originalStatsWorking.sessionCountInWindow; expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed; final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX); @@ -718,6 +878,7 @@ public class QuotaControllerTest { expectedStats.expirationTimeElapsed = originalStatsFrequent.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow; + expectedStats.sessionCountInWindow = originalStatsFrequent.sessionCountInWindow; expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed; final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX); @@ -728,6 +889,7 @@ public class QuotaControllerTest { expectedStats.expirationTimeElapsed = originalStatsRare.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow; + expectedStats.sessionCountInWindow = originalStatsRare.sessionCountInWindow; expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed; final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX); @@ -1057,6 +1219,37 @@ public class QuotaControllerTest { } @Test + public void testIsWithinQuotaLocked_TimingSession() { + setDischarging(); + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQcConstants.MAX_SESSION_COUNT_RARE = 3; + mQcConstants.MAX_SESSION_COUNT_FREQUENT = 4; + mQcConstants.MAX_SESSION_COUNT_WORKING = 5; + mQcConstants.MAX_SESSION_COUNT_ACTIVE = 6; + mQcConstants.updateConstants(); + + for (int i = 0; i < 7; ++i) { + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - ((10 - i) * MINUTE_IN_MILLIS), 30 * SECOND_IN_MILLIS, + 2)); + mQuotaController.incrementJobCount(0, "com.android.test", 2); + + assertEquals("Rare has incorrect quota status with " + (i + 1) + " sessions", + i < 2, + mQuotaController.isWithinQuotaLocked(0, "com.android.test", RARE_INDEX)); + assertEquals("Frequent has incorrect quota status with " + (i + 1) + " sessions", + i < 3, + mQuotaController.isWithinQuotaLocked(0, "com.android.test", FREQUENT_INDEX)); + assertEquals("Working has incorrect quota status with " + (i + 1) + " sessions", + i < 4, + mQuotaController.isWithinQuotaLocked(0, "com.android.test", WORKING_INDEX)); + assertEquals("Active has incorrect quota status with " + (i + 1) + " sessions", + i < 5, + mQuotaController.isWithinQuotaLocked(0, "com.android.test", ACTIVE_INDEX)); + } + } + + @Test public void testMaybeScheduleCleanupAlarmLocked() { // No sessions saved yet. mQuotaController.maybeScheduleCleanupAlarmLocked(); @@ -1244,6 +1437,10 @@ public class QuotaControllerTest { // Rare window size is 24 hours. final int standbyBucket = RARE_INDEX; + // Prevent timing session throttling from affecting the test. + mQcConstants.MAX_SESSION_COUNT_RARE = 50; + mQcConstants.updateConstants(); + // No sessions saved yet. mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); @@ -1536,6 +1733,12 @@ public class QuotaControllerTest { mQcConstants.MAX_JOB_COUNT_FREQUENT = 3000; mQcConstants.MAX_JOB_COUNT_RARE = 2000; mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 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.TIMING_SESSION_COALESCING_DURATION_MS = 10 * SECOND_IN_MILLIS; mQcConstants.updateConstants(); @@ -1552,6 +1755,13 @@ public class QuotaControllerTest { assertEquals(4000, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]); assertEquals(3000, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]); assertEquals(2000, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]); + assertEquals(50, mQuotaController.getMaxSessionCountPerAllowedTime()); + assertEquals(500, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]); + assertEquals(400, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]); + assertEquals(300, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]); + assertEquals(200, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]); + assertEquals(10 * SECOND_IN_MILLIS, + mQuotaController.getTimingSessionCoalescingDurationMs()); } @Test @@ -1569,6 +1779,12 @@ public class QuotaControllerTest { mQcConstants.MAX_JOB_COUNT_FREQUENT = 1; mQcConstants.MAX_JOB_COUNT_RARE = 1; mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 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.TIMING_SESSION_COALESCING_DURATION_MS = -1; mQcConstants.updateConstants(); @@ -1584,6 +1800,12 @@ public class QuotaControllerTest { 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(0, mQuotaController.getTimingSessionCoalescingDurationMs()); // Test larger than a day. Controller should cap at one day. mQcConstants.ALLOWED_TIME_PER_PERIOD_MS = 25 * HOUR_IN_MILLIS; @@ -1593,6 +1815,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.TIMING_SESSION_COALESCING_DURATION_MS = 25 * HOUR_IN_MILLIS; mQcConstants.updateConstants(); @@ -1603,6 +1826,8 @@ 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(15 * MINUTE_IN_MILLIS, + mQuotaController.getTimingSessionCoalescingDurationMs()); } /** Tests that TimingSessions aren't saved when the device is charging. */ @@ -2205,7 +2430,11 @@ public class QuotaControllerTest { spyOn(mQuotaController); doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); - final long start = JobSchedulerService.sElapsedRealtimeClock.millis(); + // Essentially disable session throttling. + mQcConstants.MAX_SESSION_COUNT_WORKING = + mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE; + mQcConstants.updateConstants(); + final int standbyBucket = WORKING_INDEX; setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); @@ -2217,6 +2446,7 @@ public class QuotaControllerTest { // 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) { JobStatus job = createJobStatus("testStartAlarmScheduled_JobCount_AllowedTime", i); + setStandbyBucket(WORKING_INDEX, job); mQuotaController.maybeStartTrackingJobLocked(job, null); assertTrue(job.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); mQuotaController.prepareForExecutionLocked(job); @@ -2230,6 +2460,7 @@ public class QuotaControllerTest { // The app is now out of job count quota JobStatus throttledJob = createJobStatus( "testStartAlarmScheduled_JobCount_AllowedTime", 42); + setStandbyBucket(WORKING_INDEX, throttledJob); mQuotaController.maybeStartTrackingJobLocked(throttledJob, null); assertFalse(throttledJob.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); @@ -2240,4 +2471,61 @@ public class QuotaControllerTest { 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. + */ + @Test + public void testStartAlarmScheduled_TimingSessionCount_AllowedTime() { + // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests + // because it schedules an alarm too. Prevent it from doing so. + spyOn(mQuotaController); + doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + + // 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_SESSION_COUNT_FREQUENT = + mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME + 1; + mQcConstants.updateConstants(); + + final int standbyBucket = FREQUENT_INDEX; + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + + // No sessions saved yet. + mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE, + standbyBucket); + 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) { + JobStatus job = createJobStatus( + "testStartAlarmScheduled_TimingSessionCount_AllowedTime", i); + setStandbyBucket(FREQUENT_INDEX, job); + mQuotaController.maybeStartTrackingJobLocked(job, null); + assertTrue("Constraint not satisfied for job #" + (i + 1), + job.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + mQuotaController.prepareForExecutionLocked(job); + advanceElapsedClock(SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(job, null, false); + advanceElapsedClock(SECOND_IN_MILLIS); + } + // 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 + JobStatus throttledJob = createJobStatus( + "testStartAlarmScheduled_TimingSessionCount_AllowedTime", 42); + mQuotaController.maybeStartTrackingJobLocked(throttledJob, null); + assertFalse(throttledJob.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + + ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, + SOURCE_PACKAGE, standbyBucket); + final long expectedWorkingAlarmTime = + stats.sessionCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS; + verify(mAlarmManager, times(1)) + .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + } } |