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
This commit is contained in:
Kweku Adams
2018-12-11 14:29:10 -08:00
parent 96dc163894
commit 045fb57278
4 changed files with 791 additions and 123 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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 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;
}
}
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.
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 @@ 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();
}
@@ -1238,6 +1429,11 @@ public final class QuotaController extends StateController {
return mQuotaBufferMs;
}
@VisibleForTesting
long getMaxExecutionTimeMs() {
return mMaxExecutionTimeMs;
}
@VisibleForTesting
@Nullable
List<TimingSession> getTimingSessions(int userId, String packageName) {

View File

@@ -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
@@ -393,6 +672,56 @@ public class QuotaControllerTest {
.set(anyInt(), eq(end + 24 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any());
}
@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
@@ -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. */