Adding limit for active apps.
Add an overall time limit for all apps, including ACTIVE apps. Right
now, the default is 4 hours per day, so apps can only have their jobs
running for a maximum of 4 hours in a rolling 24 hour window.
Also fix calculation bug where an app could be brought back into quota
despite having less than the quota buffer time available.
Bug: 117846754
Bug: 111423978
Test: atest com.android.server.job.controllers.QuotaControllerTest
Change-Id: Ia0773ef9fe26f0a502fe487f1e11c243eede30b3
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 7fe3be8..4eb4aae 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -242,6 +242,8 @@
// 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 1325f04..ba6857d9 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 @@
"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 @@
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 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 @@
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 @@
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 @@
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 58ee217..ac2dbdf 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.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 @@
}
}
+ 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 @@
*/
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 @@
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 @@
/** 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 @@
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 @@
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 @@
mAlarmManager.cancel(alarmListener);
mInQuotaAlarmListeners.delete(userId, packageName);
}
+ mExecutionStatsCache.delete(userId, packageName);
}
@Override
@@ -414,6 +519,7 @@
mPkgTimers.delete(userId);
mTimingSessions.delete(userId);
mInQuotaAlarmListeners.delete(userId);
+ mExecutionStatsCache.delete(userId);
}
/**
@@ -439,7 +545,6 @@
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 @@
if (standbyBucket == NEVER_INDEX) {
return 0;
}
- final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
- final long trailingRunDurationMs = getTrailingExecutionTimeLocked(
- userId, packageName, bucketWindowSizeMs);
- return mAllowedTimePerPeriodMs - trailingRunDurationMs;
+ final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs,
+ mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
}
- /** Returns how long the uid has had jobs running within the most recent window. */
+ /** Returns the execution stats of the app in the most recent window. */
@VisibleForTesting
- long getTrailingExecutionTimeLocked(final int userId, @NonNull final String packageName,
- final long windowSizeMs) {
- long totalTime = 0;
+ @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];
+ 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;
+ }
+
+ @VisibleForTesting
+ 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;
}
}
- return totalTime;
+ 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;
+ }
+ }
+ }
}
@VisibleForTesting
@@ -524,6 +735,8 @@
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 @@
@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.
- if (DEBUG) {
- Slog.w(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
- + " even though it is active");
- }
- mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
+ }
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+ 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.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
+ + " even though it already has "
+ + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
+ + "ms in its quota.");
+ }
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 @@
// 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 @@
// 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 @@
@Override
public int hashCode() {
- return Arrays.hashCode(new long[] {startTimeElapsed, endTimeElapsed, bgJobCount});
+ return mHashCode;
}
public void dump(IndentingPrintWriter pw) {
@@ -902,6 +1081,9 @@
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 @@
}
}
+ int getBgJobCount() {
+ synchronized (mLock) {
+ return mBgJobCount;
+ }
+ }
+
void onChargingChanged(long nowElapsed, boolean isCharging) {
synchronized (mLock) {
if (isCharging) {
@@ -978,6 +1166,9 @@
// 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 @@
}
@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 effb5a7..8bbcd6f 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 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 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 @@
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 @@
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 @@
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 @@
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 @@
}
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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. */