diff options
| author | 2018-12-15 02:29:40 +0000 | |
|---|---|---|
| committer | 2018-12-15 02:29:40 +0000 | |
| commit | cb03e4f9f04dcdbbf71a0b8570a884e416fa4a81 (patch) | |
| tree | 34941bd8c8c46de5178ea6ff6d8bde91992d4a77 | |
| parent | dab1ef34b165209ec8d9fc0d4aff8f5670b167ad (diff) | |
| parent | 045fb572782881efe182a45adbad476d8a5aeec8 (diff) | |
Merge "Adding limit for active apps."
4 files changed, 789 insertions, 121 deletions
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 7fe3be870994..4eb4aaeb3499 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -242,6 +242,8 @@ message ConstantsProto { // expected to run only {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS} within the past // WINDOW_SIZE_MS. optional int64 rare_window_size_ms = 6; + // The maximum amount of time an app can have its jobs running within a 24 hour window. + optional int64 max_execution_time_ms = 7; } optional QuotaController quota_controller = 24; } diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index f86cbfa933ba..7f2b7ba92dc1 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -376,6 +376,8 @@ public class JobSchedulerService extends com.android.server.SystemService "qc_window_size_frequent_ms"; private static final String KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = "qc_window_size_rare_ms"; + private static final String KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = + "qc_max_execution_time_ms"; private static final int DEFAULT_MIN_IDLE_COUNT = 1; private static final int DEFAULT_MIN_CHARGING_COUNT = 1; @@ -414,6 +416,8 @@ public class JobSchedulerService extends com.android.server.SystemService 8 * 60 * 60 * 1000L; // 8 hours private static final long DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 24 * 60 * 60 * 1000L; // 24 hours + private static final long DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = + 4 * 60 * 60 * 1000L; // 4 hours /** * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things @@ -581,6 +585,12 @@ public class JobSchedulerService extends com.android.server.SystemService public long QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS; + /** + * The maximum amount of time an app can have its jobs running within a 24 hour window. + */ + public long QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = + DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS; + private final KeyValueListParser mParser = new KeyValueListParser(','); void updateConstantsLocked(String value) { @@ -671,6 +681,9 @@ public class JobSchedulerService extends com.android.server.SystemService QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = mParser.getDurationMillis( KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS, DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS); + QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = mParser.getDurationMillis( + KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS, + DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS); } void dump(IndentingPrintWriter pw) { @@ -717,6 +730,8 @@ public class JobSchedulerService extends com.android.server.SystemService QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS).println(); pw.printPair(KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS, QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS).println(); + pw.printPair(KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS, + QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS).println(); pw.decreaseIndent(); } @@ -761,6 +776,8 @@ public class JobSchedulerService extends com.android.server.SystemService QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS); proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS, QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS); + proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS, + QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS); proto.end(qcToken); proto.end(token); diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java index 58ee21795d99..ac2dbdf9450e 100644 --- a/services/core/java/com/android/server/job/controllers/QuotaController.java +++ b/services/core/java/com/android/server/job/controllers/QuotaController.java @@ -54,14 +54,13 @@ import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; /** - * Controller that tracks whether a package has exceeded its standby bucket quota. + * 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, @@ -203,6 +202,80 @@ public final class QuotaController extends StateController { } } + private static int hashLong(long val) { + return (int) (val ^ (val >>> 32)); + } + + @VisibleForTesting + static class ExecutionStats { + /** + * The time at which this record should be considered invalid, in the elapsed realtime + * timebase. + */ + public long invalidTimeElapsed; + + public long windowSizeMs; + + /** The total amount of time the app ran in its respective bucket window size. */ + public long executionTimeInWindowMs; + public int bgJobCountInWindow; + + /** The total amount of time the app ran in the last {@link MAX_PERIOD_MS}. */ + public long executionTimeInMaxPeriodMs; + public int bgJobCountInMaxPeriod; + + /** + * The time after which the sum of all the app's sessions plus {@link mQuotaBufferMs} equals + * the quota. This is only valid if + * executionTimeInWindowMs >= {@link mAllowedTimePerPeriodMs} or + * executionTimeInMaxPeriodMs >= {@link mMaxExecutionTimeMs}. + */ + public long quotaCutoffTimeElapsed; + + @Override + public String toString() { + return new StringBuilder() + .append("invalidTime=").append(invalidTimeElapsed).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) + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ExecutionStats) { + ExecutionStats other = (ExecutionStats) obj; + return this.invalidTimeElapsed == other.invalidTimeElapsed + && this.windowSizeMs == other.windowSizeMs + && this.executionTimeInWindowMs == other.executionTimeInWindowMs + && this.bgJobCountInWindow == other.bgJobCountInWindow + && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs + && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod + && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed; + } else { + return false; + } + } + + @Override + public int hashCode() { + int result = 0; + result = 31 * result + hashLong(invalidTimeElapsed); + result = 31 * result + hashLong(windowSizeMs); + result = 31 * result + hashLong(executionTimeInWindowMs); + result = 31 * result + bgJobCountInWindow; + result = 31 * result + hashLong(executionTimeInMaxPeriodMs); + result = 31 * result + bgJobCountInMaxPeriod; + result = 31 * result + hashLong(quotaCutoffTimeElapsed); + return result; + } + } + /** List of all tracked jobs keyed by source package-userId combo. */ private final UserPackageMap<ArraySet<JobStatus>> mTrackedJobs = new UserPackageMap<>(); @@ -218,6 +291,9 @@ public final class QuotaController extends StateController { */ private final UserPackageMap<QcAlarmListener> mInQuotaAlarmListeners = new UserPackageMap<>(); + /** Cached calculation results for each app, with the standby buckets as the array indices. */ + private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>(); + private final AlarmManager mAlarmManager; private final ChargingTracker mChargeTracker; private final Handler mHandler; @@ -235,11 +311,29 @@ public final class QuotaController extends StateController { private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS; /** - * How much time the package should have before transitioning from out-of-quota to in-quota. - * This should not affect processing if the package is already in-quota. + * 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; + + /** + * 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 + /** + * {@link mAllowedTimePerPeriodMs} - {@link mQuotaBufferMs}. This can be used to determine when + * an app will have enough quota to transition from out-of-quota to in-quota. + */ + private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; + + /** + * {@link mMaxExecutionTimeMs} - {@link mQuotaBufferMs}. This can be used to determine when an + * app will have enough quota to transition from out-of-quota to in-quota. + */ + private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + private long mNextCleanupTimeElapsed = 0; private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener = new AlarmManager.OnAlarmListener() { @@ -263,7 +357,7 @@ public final class QuotaController extends StateController { /** The maximum period any bucket can have. */ private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS; - /** A package has reached its quota. The message should contain a {@link Package} object. */ + /** 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. */ private static final int MSG_CLEAN_UP_SESSIONS = 1; @@ -341,12 +435,15 @@ public final class QuotaController extends StateController { Math.max(MINUTE_IN_MILLIS, mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS)); if (mAllowedTimePerPeriodMs != newAllowedTimeMs) { mAllowedTimePerPeriodMs = newAllowedTimeMs; + mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; changed = true; } long newQuotaBufferMs = Math.max(0, Math.min(5 * MINUTE_IN_MILLIS, mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS)); if (mQuotaBufferMs != newQuotaBufferMs) { mQuotaBufferMs = newQuotaBufferMs; + mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; + mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; changed = true; } long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs, @@ -373,6 +470,13 @@ 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, mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS)); + if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) { + mMaxExecutionTimeMs = newMaxExecutionTimeMs; + mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + changed = true; + } if (changed) { // Update job bookkeeping out of band. @@ -406,6 +510,7 @@ public final class QuotaController extends StateController { mAlarmManager.cancel(alarmListener); mInQuotaAlarmListeners.delete(userId, packageName); } + mExecutionStatsCache.delete(userId, packageName); } @Override @@ -414,6 +519,7 @@ public final class QuotaController extends StateController { mPkgTimers.delete(userId); mTimingSessions.delete(userId); mInQuotaAlarmListeners.delete(userId); + mExecutionStatsCache.delete(userId); } /** @@ -439,7 +545,6 @@ public final class QuotaController extends StateController { private boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName, final int standbyBucket) { if (standbyBucket == NEVER_INDEX) return false; - if (standbyBucket == ACTIVE_INDEX) return true; // This check is needed in case the flag is toggled after a job has been registered. if (!mShouldThrottle) return true; @@ -472,46 +577,152 @@ public final class QuotaController extends StateController { if (standbyBucket == NEVER_INDEX) { return 0; } + final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs, + mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs); + } + + /** Returns the execution stats of the app in the most recent window. */ + @VisibleForTesting + @NonNull + ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName, + final int standbyBucket) { + if (standbyBucket == NEVER_INDEX) { + Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app."); + return new ExecutionStats(); + } + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats == null) { + appStats = new ExecutionStats[mBucketPeriodsMs.length]; + mExecutionStatsCache.add(userId, packageName, appStats); + } + ExecutionStats stats = appStats[standbyBucket]; + if (stats == null) { + stats = new ExecutionStats(); + appStats[standbyBucket] = stats; + } final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket]; - final long trailingRunDurationMs = getTrailingExecutionTimeLocked( - userId, packageName, bucketWindowSizeMs); - return mAllowedTimePerPeriodMs - trailingRunDurationMs; + Timer timer = mPkgTimers.get(userId, packageName); + if ((timer != null && timer.isActive()) + || stats.invalidTimeElapsed <= sElapsedRealtimeClock.millis() + || stats.windowSizeMs != bucketWindowSizeMs) { + // The stats are no longer valid. + stats.windowSizeMs = bucketWindowSizeMs; + updateExecutionStatsLocked(userId, packageName, stats); + } + + return stats; } - /** Returns how long the uid has had jobs running within the most recent window. */ @VisibleForTesting - long getTrailingExecutionTimeLocked(final int userId, @NonNull final String packageName, - final long windowSizeMs) { - long totalTime = 0; + void updateExecutionStatsLocked(final int userId, @NonNull final String packageName, + @NonNull ExecutionStats stats) { + stats.executionTimeInWindowMs = 0; + stats.bgJobCountInWindow = 0; + stats.executionTimeInMaxPeriodMs = 0; + stats.bgJobCountInMaxPeriod = 0; + stats.quotaCutoffTimeElapsed = 0; Timer timer = mPkgTimers.get(userId, packageName); final long nowElapsed = sElapsedRealtimeClock.millis(); + stats.invalidTimeElapsed = nowElapsed + MAX_PERIOD_MS; if (timer != null && timer.isActive()) { - totalTime = timer.getCurrentDuration(nowElapsed); + stats.executionTimeInWindowMs = + stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed); + stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount(); + // If the timer is active, the value will be stale at the next method call, so + // invalidate now. + stats.invalidTimeElapsed = nowElapsed; + if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { + stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, + nowElapsed - mAllowedTimeIntoQuotaMs); + } + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, + nowElapsed - mMaxExecutionTimeIntoQuotaMs); + } } List<TimingSession> sessions = mTimingSessions.get(userId, packageName); if (sessions == null || sessions.size() == 0) { - return totalTime; + return; } - final long startElapsed = nowElapsed - windowSizeMs; + final long startWindowElapsed = nowElapsed - stats.windowSizeMs; + final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; + // 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) { TimingSession session = sessions.get(i); - if (startElapsed < session.startTimeElapsed) { - totalTime += session.endTimeElapsed - session.startTimeElapsed; - } else if (startElapsed < session.endTimeElapsed) { + + // Window management. + if (startWindowElapsed < session.startTimeElapsed) { + stats.executionTimeInWindowMs += session.endTimeElapsed - session.startTimeElapsed; + 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); + } + } 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. - totalTime += session.endTimeElapsed - startElapsed; + 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); + } + } + + // Max period check. + if (startMaxElapsed < session.startTimeElapsed) { + stats.executionTimeInMaxPeriodMs += + session.endTimeElapsed - session.startTimeElapsed; + stats.bgJobCountInMaxPeriod += session.bgJobCount; + emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed); + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, + session.startTimeElapsed + stats.executionTimeInMaxPeriodMs + - mMaxExecutionTimeIntoQuotaMs); + } + } else if (startMaxElapsed < session.endTimeElapsed) { + // The session started before the window but ended within the window. Only include + // the portion that was within the window. + stats.executionTimeInMaxPeriodMs += session.endTimeElapsed - startMaxElapsed; + stats.bgJobCountInMaxPeriod += session.bgJobCount; + emptyTimeMs = 0; + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, + startMaxElapsed + stats.executionTimeInMaxPeriodMs + - mMaxExecutionTimeIntoQuotaMs); + } } else { // This session ended before the window. No point in going any further. - return totalTime; + break; + } + } + stats.invalidTimeElapsed = nowElapsed + emptyTimeMs; + } + + private void invalidateAllExecutionStatsLocked(final int userId, + @NonNull final String packageName) { + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats != null) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats != null) { + stats.invalidTimeElapsed = nowElapsed; + } } } - return totalTime; } @VisibleForTesting @@ -524,6 +735,8 @@ public final class QuotaController extends StateController { mTimingSessions.add(userId, packageName, sessions); } sessions.add(session); + // Adding a new session means that the current stats are now incorrect. + invalidateAllExecutionStatsLocked(userId, packageName); maybeScheduleCleanupAlarmLocked(); } @@ -657,87 +870,43 @@ public final class QuotaController extends StateController { @VisibleForTesting void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName, final int standbyBucket) { - final String pkgString = string(userId, packageName); if (standbyBucket == NEVER_INDEX) { return; - } else if (standbyBucket == ACTIVE_INDEX) { - // ACTIVE apps are "always" in quota. + } + + final String pkgString = string(userId, packageName); + ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); + if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs + && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs) { + // Already in quota. Why was this method called? if (DEBUG) { - Slog.w(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString - + " even though it is active"); + Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString + + " even though it already has " + + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) + + "ms in its quota."); } - mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); - - QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); if (alarmListener != null) { // Cancel any pending alarm. mAlarmManager.cancel(alarmListener); // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. alarmListener.setTriggerTime(0); } - return; - } - - List<TimingSession> sessions = mTimingSessions.get(userId, packageName); - if (sessions == null || sessions.size() == 0) { - // If there are no sessions, then the job is probably in quota. - if (DEBUG) { - Slog.wtf(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString - + " even though it is likely within its quota."); - } mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); return; } - - final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket]; - final long nowElapsed = sElapsedRealtimeClock.millis(); - // How far back we need to look. - final long startElapsed = nowElapsed - bucketWindowSizeMs; - - long totalTime = 0; - long cutoffTimeElapsed = nowElapsed; - for (int i = sessions.size() - 1; i >= 0; i--) { - TimingSession session = sessions.get(i); - if (startElapsed < session.startTimeElapsed) { - cutoffTimeElapsed = session.startTimeElapsed; - totalTime += session.endTimeElapsed - session.startTimeElapsed; - } else if (startElapsed < session.endTimeElapsed) { - // The session started before the window but ended within the window. Only - // include the portion that was within the window. - cutoffTimeElapsed = startElapsed; - totalTime += session.endTimeElapsed - startElapsed; - } else { - // This session ended before the window. No point in going any further. - break; - } - if (totalTime >= mAllowedTimePerPeriodMs) { - break; - } - } - if (totalTime < mAllowedTimePerPeriodMs) { - // Already in quota. Why was this method called? - if (DEBUG) { - Slog.w(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString - + " even though it already has " + (mAllowedTimePerPeriodMs - totalTime) - + "ms in its quota."); - } - mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); - return; - } - - QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); if (alarmListener == null) { alarmListener = new QcAlarmListener(userId, packageName); mInQuotaAlarmListeners.add(userId, packageName, alarmListener); } - // We add all the way back to the beginning of a session (or the window) even when we don't - // need to (in order to simplify the for loop above), so there might be some extra we - // need to add back. - final long extraTimeMs = totalTime - mAllowedTimePerPeriodMs; // The time this app will have quota again. - final long inQuotaTimeElapsed = - cutoffTimeElapsed + extraTimeMs + mQuotaBufferMs + bucketWindowSizeMs; + long inQuotaTimeElapsed = + stats.quotaCutoffTimeElapsed + stats.windowSizeMs; + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) { + inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, + stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS); + } // 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 @@ -747,13 +916,15 @@ public final class QuotaController extends StateController { // TODO: this might be overengineering. Simplify if proven safe. if (!alarmListener.isWaiting() || inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS - || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed - mQuotaBufferMs) { + || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) { if (DEBUG) Slog.d(TAG, "Scheduling start alarm for " + pkgString); // If the next time this app will have quota is at least 3 minutes before the // alarm is supposed to go off, reschedule the alarm. mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed, ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler); alarmListener.setTriggerTime(inQuotaTimeElapsed); + } else if (DEBUG) { + Slog.d(TAG, "No need to scheduling start alarm for " + pkgString); } } @@ -816,10 +987,18 @@ public final class QuotaController extends StateController { // How many background jobs ran during this session. public final int bgJobCount; - TimingSession(long startElapsed, long endElapsed, int jobCount) { + private final int mHashCode; + + TimingSession(long startElapsed, long endElapsed, int bgJobCount) { this.startTimeElapsed = startElapsed; this.endTimeElapsed = endElapsed; - this.bgJobCount = jobCount; + this.bgJobCount = bgJobCount; + + int hashCode = 0; + hashCode = 31 * hashCode + hashLong(startTimeElapsed); + hashCode = 31 * hashCode + hashLong(endTimeElapsed); + hashCode = 31 * hashCode + bgJobCount; + mHashCode = hashCode; } @Override @@ -842,7 +1021,7 @@ public final class QuotaController extends StateController { @Override public int hashCode() { - return Arrays.hashCode(new long[] {startTimeElapsed, endTimeElapsed, bgJobCount}); + return mHashCode; } public void dump(IndentingPrintWriter pw) { @@ -902,6 +1081,9 @@ public final class QuotaController extends StateController { if (mRunningBgJobs.size() == 1) { // Started tracking the first job. mStartTimeElapsed = sElapsedRealtimeClock.millis(); + // Starting the timer means that all cached execution stats are now + // incorrect. + invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName); scheduleCutoff(); } } @@ -966,6 +1148,12 @@ public final class QuotaController extends StateController { } } + int getBgJobCount() { + synchronized (mLock) { + return mBgJobCount; + } + } + void onChargingChanged(long nowElapsed, boolean isCharging) { synchronized (mLock) { if (isCharging) { @@ -978,6 +1166,9 @@ public final class QuotaController extends StateController { // repeatedly plugged in and unplugged, the job count for a package may be // artificially high. mBgJobCount = mRunningBgJobs.size(); + // Starting the timer means that all cached execution stats are now + // incorrect. + invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName); // Schedule cutoff since we're now actively tracking for quotas again. scheduleCutoff(); } @@ -1239,6 +1430,11 @@ public final class QuotaController extends StateController { } @VisibleForTesting + long getMaxExecutionTimeMs() { + return mMaxExecutionTimeMs; + } + + @VisibleForTesting @Nullable List<TimingSession> getTimingSessions(int userId, String packageName) { return mTimingSessions.get(userId, packageName); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index effb5a72bd66..8bbcd6fa209a 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 @@ -30,6 +30,7 @@ import static com.android.server.job.JobSchedulerService.WORKING_INDEX; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -62,6 +63,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobSchedulerService.Constants; +import com.android.server.job.controllers.QuotaController.ExecutionStats; import com.android.server.job.controllers.QuotaController.TimingSession; import org.junit.After; @@ -131,13 +133,18 @@ public class QuotaControllerTest { doReturn(mock(PackageManagerInternal.class)) .when(() -> LocalServices.getService(PackageManagerInternal.class)); - // Freeze the clocks at this moment in time + // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions + // in the past, and QuotaController sometimes floors values at 0, so if the test time + // causes sessions with negative timestamps, they will fail. JobSchedulerService.sSystemClock = - Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); - JobSchedulerService.sUptimeMillisClock = - Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC); - JobSchedulerService.sElapsedRealtimeClock = - Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); + getAdvancedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + JobSchedulerService.sUptimeMillisClock = getAdvancedClock( + Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock( + Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); // Initialize real objects. // Capture the listeners. @@ -291,9 +298,17 @@ public class QuotaControllerTest { mQuotaController.saveTimingSession(0, "com.android.test.stay", two); mQuotaController.saveTimingSession(0, "com.android.test.stay", one); + ExecutionStats expectedStats = new ExecutionStats(); + expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS; + expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + mQuotaController.onAppRemovedLocked("com.android.test.remove", 10001); assertNull(mQuotaController.getTimingSessions(0, "com.android.test.remove")); assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test.stay")); + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test.remove", RARE_INDEX)); + assertNotEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test.stay", RARE_INDEX)); } @Test @@ -318,13 +333,21 @@ public class QuotaControllerTest { mQuotaController.saveTimingSession(10, "com.android.test", two); mQuotaController.saveTimingSession(10, "com.android.test", one); + ExecutionStats expectedStats = new ExecutionStats(); + expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS; + expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + mQuotaController.onUserRemovedLocked(0); assertNull(mQuotaController.getTimingSessions(0, "com.android.test")); assertEquals(expected, mQuotaController.getTimingSessions(10, "com.android.test")); + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX)); + assertNotEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(10, "com.android.test", RARE_INDEX)); } @Test - public void testGetTrailingExecutionTimeLocked_NoTimer() { + public void testUpdateExecutionStatsLocked_NoTimer() { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); // Added in chronological order. mQuotaController.saveTimingSession(0, "com.android.test", @@ -340,32 +363,288 @@ public class QuotaControllerTest { mQuotaController.saveTimingSession(0, "com.android.test", createTimingSession(now - 5 * MINUTE_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3)); - assertEquals(0, mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - MINUTE_IN_MILLIS)); - assertEquals(2 * MINUTE_IN_MILLIS, - mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - 3 * MINUTE_IN_MILLIS)); - assertEquals(4 * MINUTE_IN_MILLIS, - mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - 5 * MINUTE_IN_MILLIS)); - assertEquals(4 * MINUTE_IN_MILLIS, - mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - 49 * MINUTE_IN_MILLIS)); - assertEquals(5 * MINUTE_IN_MILLIS, - mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - 50 * MINUTE_IN_MILLIS)); - assertEquals(6 * MINUTE_IN_MILLIS, - mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - HOUR_IN_MILLIS)); - assertEquals(11 * MINUTE_IN_MILLIS, - mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - 2 * HOUR_IN_MILLIS)); - assertEquals(12 * MINUTE_IN_MILLIS, - mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - 3 * HOUR_IN_MILLIS)); - assertEquals(22 * MINUTE_IN_MILLIS, - mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test", - 6 * HOUR_IN_MILLIS)); + // Test an app that hasn't had any activity. + ExecutionStats expectedStats = new ExecutionStats(); + ExecutionStats inputStats = new ExecutionStats(); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS; + // Invalid time is now +24 hours since there are no sessions at all for the app. + expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = MINUTE_IN_MILLIS; + // Invalid time is now +18 hours since there are no sessions in the window but the earliest + // session is 6 hours ago. + expectedStats.invalidTimeElapsed = now + 18 * HOUR_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 0; + expectedStats.bgJobCountInWindow = 0; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS; + // Invalid time is now since the session straddles the window cutoff time. + expectedStats.invalidTimeElapsed = now; + expectedStats.executionTimeInWindowMs = 2 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 3; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * MINUTE_IN_MILLIS; + // Invalid time is now since the start of the session is at the very edge of the window + // cutoff time. + expectedStats.invalidTimeElapsed = now; + expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 3; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS; + // Invalid time is now +44 minutes since the earliest session in the window is now-5 + // minutes. + expectedStats.invalidTimeElapsed = now + 44 * MINUTE_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 3; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS; + // Invalid time is now since the session is at the very edge of the window cutoff time. + expectedStats.invalidTimeElapsed = now; + expectedStats.executionTimeInWindowMs = 5 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 4; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS; + // Invalid time is now since the start of the session is at the very edge of the window + // cutoff time. + expectedStats.invalidTimeElapsed = now; + expectedStats.executionTimeInWindowMs = 6 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 5; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; + // Invalid time is now since the session straddles the window cutoff time. + expectedStats.invalidTimeElapsed = now; + expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 10; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * HOUR_IN_MILLIS; + // Invalid time is now +59 minutes since the earliest session in the window is now-121 + // minutes. + expectedStats.invalidTimeElapsed = now + 59 * MINUTE_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 12 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 10; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS; + // Invalid time is now since the start of the session is at the very edge of the window + // cutoff time. + expectedStats.invalidTimeElapsed = now; + expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 15; + expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 15; + expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + // Make sure invalidTimeElapsed is set correctly when it's dependent on the max period. + mQuotaController.getTimingSessions(0, "com.android.test") + .add(0, + createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3)); + inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS; + // Invalid time is now +1 hour since the earliest session in the max period is 1 hour + // before the end of the max period cutoff time. + expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 15; + expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 18; + expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + + mQuotaController.getTimingSessions(0, "com.android.test") + .add(0, + 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. + expectedStats.invalidTimeElapsed = now; + expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 15; + expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); + assertEquals(expectedStats, inputStats); + } + + /** + * Tests that getExecutionStatsLocked returns the correct stats. + */ + @Test + public void testGetExecutionStatsLocked_Values() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5)); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5)); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5)); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5)); + + ExecutionStats expectedStats = new ExecutionStats(); + + // Active + expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS; + expectedStats.invalidTimeElapsed = now + 4 * MINUTE_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 5; + expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX)); + + // Working + expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; + expectedStats.invalidTimeElapsed = now; + expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 10; + expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX)); + + // Frequent + expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS; + expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 15; + expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX)); + + // Rare + expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 20; + expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX)); + } + + /** + * Tests that getExecutionStatsLocked properly caches the stats and returns the cached object. + */ + @Test + public void testGetExecutionStatsLocked_Caching() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5)); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5)); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5)); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5)); + final ExecutionStats originalStatsActive = mQuotaController.getExecutionStatsLocked(0, + "com.android.test", ACTIVE_INDEX); + final ExecutionStats originalStatsWorking = mQuotaController.getExecutionStatsLocked(0, + "com.android.test", WORKING_INDEX); + final ExecutionStats originalStatsFrequent = mQuotaController.getExecutionStatsLocked(0, + "com.android.test", FREQUENT_INDEX); + final ExecutionStats originalStatsRare = mQuotaController.getExecutionStatsLocked(0, + "com.android.test", RARE_INDEX); + + // Advance clock so that the working stats shouldn't be the same. + advanceElapsedClock(MINUTE_IN_MILLIS); + // Change frequent bucket size so that the stats need to be recalculated. + mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 6 * HOUR_IN_MILLIS; + mQuotaController.onConstantsUpdatedLocked(); + + ExecutionStats expectedStats = new ExecutionStats(); + expectedStats.windowSizeMs = originalStatsActive.windowSizeMs; + expectedStats.invalidTimeElapsed = originalStatsActive.invalidTimeElapsed; + expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs; + expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow; + expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs; + expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod; + expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed; + final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0, + "com.android.test", ACTIVE_INDEX); + // Stats for the same bucket should use the same object. + assertTrue(originalStatsActive == newStatsActive); + assertEquals(expectedStats, newStatsActive); + + expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs; + expectedStats.invalidTimeElapsed = originalStatsWorking.invalidTimeElapsed; + expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs; + expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow; + expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed; + final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0, + "com.android.test", WORKING_INDEX); + assertTrue(originalStatsWorking == newStatsWorking); + assertNotEquals(expectedStats, newStatsWorking); + + expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs; + expectedStats.invalidTimeElapsed = originalStatsFrequent.invalidTimeElapsed; + expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs; + expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow; + expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed; + final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0, + "com.android.test", FREQUENT_INDEX); + assertTrue(originalStatsFrequent == newStatsFrequent); + assertNotEquals(expectedStats, newStatsFrequent); + + expectedStats.windowSizeMs = originalStatsRare.windowSizeMs; + expectedStats.invalidTimeElapsed = originalStatsRare.invalidTimeElapsed; + expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs; + expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow; + expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed; + final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0, + "com.android.test", RARE_INDEX); + assertTrue(originalStatsRare == newStatsRare); + assertEquals(expectedStats, newStatsRare); } @Test @@ -394,6 +673,56 @@ public class QuotaControllerTest { } @Test + public void testMaybeScheduleStartAlarmLocked_Active() { + // 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(); + + // Active window size is 10 minutes. + final int standbyBucket = ACTIVE_INDEX; + + // No sessions saved yet. + mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE, + standbyBucket); + verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + // Test with timing sessions out of window but still under max execution limit. + final long expectedAlarmTime = + (now - 18 * HOUR_IN_MILLIS) + 24 * HOUR_IN_MILLIS + + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS; + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - 18 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1)); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - 12 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1)); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - 7 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1)); + mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE, + standbyBucket); + verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1)); + mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE, + standbyBucket); + verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + + JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1); + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + mQuotaController.prepareForExecutionLocked(jobStatus); + advanceElapsedClock(5 * MINUTE_IN_MILLIS); + // Timer has only been going for 5 minutes in the past 10 minutes, which is under the window + // size limit, but the total execution time for the past 24 hours is 6 hours, so the job no + // longer has quota. + assertEquals(0, mQuotaController.getRemainingExecutionTimeLocked(jobStatus)); + mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE, + standbyBucket); + verify(mAlarmManager, times(1)).set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), + any(), any()); + } + + @Test public void testMaybeScheduleStartAlarmLocked_WorkingSet() { // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests // because it schedules an alarm too. Prevent it from doing so. @@ -620,6 +949,124 @@ public class QuotaControllerTest { inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class)); } + + /** + * Tests that the start alarm is properly rescheduled if the earliest session that contributes + * to the app being out of quota contributes less than the quota buffer time. + */ + @Test + public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_DefaultValues() { + // Use the default values + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck(); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck(); + } + + @Test + public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedBufferSize() { + // Make sure any new value is used correctly. + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS *= 2; + mQuotaController.onConstantsUpdatedLocked(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck(); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck(); + } + + @Test + public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedAllowedTime() { + // Make sure any new value is used correctly. + mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS /= 2; + mQuotaController.onConstantsUpdatedLocked(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck(); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck(); + } + + @Test + public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedMaxTime() { + // Make sure any new value is used correctly. + mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS /= 2; + mQuotaController.onConstantsUpdatedLocked(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck(); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck(); + } + + @Test + public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedEverything() { + // Make sure any new value is used correctly. + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS *= 2; + mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS /= 2; + mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS /= 2; + mQuotaController.onConstantsUpdatedLocked(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck(); + mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear(); + runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck(); + } + + private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck() { + // 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(); + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + // Working set window size is 2 hours. + final int standbyBucket = WORKING_INDEX; + final long contributionMs = mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS / 2; + final long remainingTimeMs = + mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS - contributionMs; + + // Session straddles edge of bucket window. Only the contribution should be counted towards + // the quota. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (2 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS), + 3 * MINUTE_IN_MILLIS + contributionMs, 3)); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - HOUR_IN_MILLIS, remainingTimeMs, 2)); + // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which + // is 2 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session. + final long expectedAlarmTime = now - HOUR_IN_MILLIS + 2 * HOUR_IN_MILLIS + + (mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS - contributionMs); + mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE, + standbyBucket); + verify(mAlarmManager, times(1)) + .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + } + + + private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck() { + // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests + // because it schedules an alarm too. Prevent it from doing so. + spyOn(mQuotaController); + doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked(); + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + // Working set window size is 2 hours. + final int standbyBucket = WORKING_INDEX; + final long contributionMs = mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS / 2; + final long remainingTimeMs = + mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS - contributionMs; + + // Session straddles edge of 24 hour window. Only the contribution should be counted towards + // the quota. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (24 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS), + 3 * MINUTE_IN_MILLIS + contributionMs, 3)); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - 20 * HOUR_IN_MILLIS, remainingTimeMs, 300)); + // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which + // is 24 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session. + final long expectedAlarmTime = now - 20 * HOUR_IN_MILLIS + //+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS + + 24 * HOUR_IN_MILLIS + + (mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS - contributionMs); + mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE, + standbyBucket); + verify(mAlarmManager, times(1)) + .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); + } + /** Tests that QuotaController doesn't throttle if throttling is turned off. */ @Test public void testThrottleToggling() throws Exception { @@ -652,6 +1099,7 @@ public class QuotaControllerTest { mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 30 * MINUTE_IN_MILLIS; mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 45 * MINUTE_IN_MILLIS; mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 60 * MINUTE_IN_MILLIS; + mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 3 * HOUR_IN_MILLIS; mQuotaController.onConstantsUpdatedLocked(); @@ -662,6 +1110,7 @@ public class QuotaControllerTest { assertEquals(45 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]); assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]); + assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs()); } @Test @@ -673,6 +1122,7 @@ public class QuotaControllerTest { mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = -MINUTE_IN_MILLIS; mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = -MINUTE_IN_MILLIS; mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = -MINUTE_IN_MILLIS; + mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = -MINUTE_IN_MILLIS; mQuotaController.onConstantsUpdatedLocked(); @@ -682,6 +1132,7 @@ public class QuotaControllerTest { assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]); + assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs()); // Test larger than a day. Controller should cap at one day. mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = 25 * HOUR_IN_MILLIS; @@ -690,6 +1141,7 @@ public class QuotaControllerTest { mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 25 * HOUR_IN_MILLIS; mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS; mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS; + mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 25 * HOUR_IN_MILLIS; mQuotaController.onConstantsUpdatedLocked(); @@ -699,6 +1151,7 @@ public class QuotaControllerTest { assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]); 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()); } /** Tests that TimingSessions aren't saved when the device is charging. */ |