Merge "Reducing quota expired check churn."
This commit is contained in:
@@ -769,6 +769,91 @@ public final class QuotaController extends StateController {
|
||||
mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of time, in milliseconds, until the package would have reached its
|
||||
* duration quota, assuming it has a job counting towards its quota the entire time. This takes
|
||||
* into account any {@link TimingSession}s that may roll out of the window as the job is
|
||||
* running.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) {
|
||||
final long nowElapsed = sElapsedRealtimeClock.millis();
|
||||
final int standbyBucket = JobSchedulerService.standbyBucketForPackage(
|
||||
packageName, userId, nowElapsed);
|
||||
if (standbyBucket == NEVER_INDEX) {
|
||||
return 0;
|
||||
}
|
||||
List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
|
||||
if (sessions == null || sessions.size() == 0) {
|
||||
return mAllowedTimePerPeriodMs;
|
||||
}
|
||||
|
||||
final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
|
||||
final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
|
||||
final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
|
||||
final long allowedTimeRemainingMs = mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs;
|
||||
final long maxExecutionTimeRemainingMs =
|
||||
mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs;
|
||||
|
||||
// Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
|
||||
// essentially run until they reach the maximum limit.
|
||||
if (stats.windowSizeMs == mAllowedTimePerPeriodMs) {
|
||||
return calculateTimeUntilQuotaConsumedLocked(
|
||||
sessions, startMaxElapsed, maxExecutionTimeRemainingMs);
|
||||
}
|
||||
|
||||
// Need to check both max time and period time in case one is less than the other.
|
||||
// For example, max time remaining could be less than bucket time remaining, but sessions
|
||||
// contributing to the max time remaining could phase out enough that we'd want to use the
|
||||
// bucket value.
|
||||
return Math.min(
|
||||
calculateTimeUntilQuotaConsumedLocked(
|
||||
sessions, startMaxElapsed, maxExecutionTimeRemainingMs),
|
||||
calculateTimeUntilQuotaConsumedLocked(
|
||||
sessions, startWindowElapsed, allowedTimeRemainingMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how much time it will take, in milliseconds, until the quota is fully consumed.
|
||||
*
|
||||
* @param windowStartElapsed The start of the window, in the elapsed realtime timebase.
|
||||
* @param deadSpaceMs How much time can be allowed to count towards the quota
|
||||
*/
|
||||
private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimingSession> sessions,
|
||||
final long windowStartElapsed, long deadSpaceMs) {
|
||||
long timeUntilQuotaConsumedMs = 0;
|
||||
long start = windowStartElapsed;
|
||||
for (int i = 0; i < sessions.size(); ++i) {
|
||||
TimingSession session = sessions.get(i);
|
||||
|
||||
if (session.endTimeElapsed < windowStartElapsed) {
|
||||
// Outside of window. Ignore.
|
||||
continue;
|
||||
} else if (session.startTimeElapsed <= windowStartElapsed) {
|
||||
// Overlapping session. Can extend time by portion of session in window.
|
||||
timeUntilQuotaConsumedMs += session.endTimeElapsed - windowStartElapsed;
|
||||
start = session.endTimeElapsed;
|
||||
} else {
|
||||
// Completely within the window. Can only consider if there's enough dead space
|
||||
// to get to the start of the session.
|
||||
long diff = session.startTimeElapsed - start;
|
||||
if (diff > deadSpaceMs) {
|
||||
break;
|
||||
}
|
||||
timeUntilQuotaConsumedMs += diff
|
||||
+ (session.endTimeElapsed - session.startTimeElapsed);
|
||||
deadSpaceMs -= diff;
|
||||
start = session.endTimeElapsed;
|
||||
}
|
||||
}
|
||||
// Will be non-zero if the loop didn't look at any sessions.
|
||||
timeUntilQuotaConsumedMs += deadSpaceMs;
|
||||
if (timeUntilQuotaConsumedMs > mMaxExecutionTimeMs) {
|
||||
Slog.wtf(TAG, "Calculated quota consumed time too high: " + timeUntilQuotaConsumedMs);
|
||||
}
|
||||
return timeUntilQuotaConsumedMs;
|
||||
}
|
||||
|
||||
/** Returns the execution stats of the app in the most recent window. */
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
@@ -1483,7 +1568,7 @@ public final class QuotaController extends StateController {
|
||||
return;
|
||||
}
|
||||
Message msg = mHandler.obtainMessage(MSG_REACHED_QUOTA, mPkg);
|
||||
final long timeRemainingMs = getRemainingExecutionTimeLocked(mPkg.userId,
|
||||
final long timeRemainingMs = getTimeUntilQuotaConsumedLocked(mPkg.userId,
|
||||
mPkg.packageName);
|
||||
if (DEBUG) {
|
||||
Slog.i(TAG, "Job for " + mPkg + " has " + timeRemainingMs + "ms left.");
|
||||
@@ -1642,6 +1727,8 @@ public final class QuotaController extends StateController {
|
||||
// job is currently running.
|
||||
// Reschedule message
|
||||
Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg);
|
||||
timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId,
|
||||
pkg.packageName);
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left.");
|
||||
}
|
||||
|
||||
@@ -722,6 +722,147 @@ public class QuotaControllerTest {
|
||||
assertEquals(expectedStats, newStatsRare);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket
|
||||
* window.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTimeUntilQuotaConsumedLocked_BucketWindow() {
|
||||
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
|
||||
// Close to RARE boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(now - (24 * HOUR_IN_MILLIS - 30 * SECOND_IN_MILLIS),
|
||||
30 * SECOND_IN_MILLIS, 5));
|
||||
// Far away from FREQUENT boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
|
||||
// Overlap WORKING_SET boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
|
||||
3 * MINUTE_IN_MILLIS, 5));
|
||||
// Close to ACTIVE boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
|
||||
|
||||
setStandbyBucket(RARE_INDEX);
|
||||
assertEquals(30 * SECOND_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
assertEquals(MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
|
||||
setStandbyBucket(FREQUENT_INDEX);
|
||||
assertEquals(MINUTE_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
assertEquals(MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
|
||||
setStandbyBucket(WORKING_INDEX);
|
||||
assertEquals(5 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
assertEquals(7 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
|
||||
// ACTIVE window = allowed time, so jobs can essentially run non-stop until they reach the
|
||||
// max execution time.
|
||||
setStandbyBucket(ACTIVE_INDEX);
|
||||
assertEquals(7 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
assertEquals(mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS - 9 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getTimeUntilQuotaConsumedLocked when the app is close to the max execution limit.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTimeUntilQuotaConsumedLocked_MaxExecution() {
|
||||
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
|
||||
// Overlap boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(
|
||||
now - (24 * HOUR_IN_MILLIS + 8 * MINUTE_IN_MILLIS), 4 * HOUR_IN_MILLIS, 5));
|
||||
|
||||
setStandbyBucket(WORKING_INDEX);
|
||||
assertEquals(8 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
// Max time will phase out, so should use bucket limit.
|
||||
assertEquals(10 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
|
||||
mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
|
||||
// Close to boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(now - (24 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS),
|
||||
4 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS, 5));
|
||||
|
||||
setStandbyBucket(WORKING_INDEX);
|
||||
assertEquals(5 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
assertEquals(10 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
|
||||
mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
|
||||
// Far from boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(
|
||||
now - (20 * HOUR_IN_MILLIS), 4 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS, 5));
|
||||
|
||||
setStandbyBucket(WORKING_INDEX);
|
||||
assertEquals(3 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
assertEquals(3 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getTimeUntilQuotaConsumedLocked when the max execution time and bucket window time
|
||||
* remaining are equal.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTimeUntilQuotaConsumedLocked_EqualTimeRemaining() {
|
||||
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
|
||||
setStandbyBucket(FREQUENT_INDEX);
|
||||
|
||||
// Overlap boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(
|
||||
now - (24 * HOUR_IN_MILLIS + 11 * MINUTE_IN_MILLIS),
|
||||
4 * HOUR_IN_MILLIS,
|
||||
5));
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(
|
||||
now - (8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
|
||||
|
||||
// Both max and bucket time have 8 minutes left.
|
||||
assertEquals(8 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
// Max time essentially free. Bucket time has 2 min phase out plus original 8 minute
|
||||
// window time.
|
||||
assertEquals(10 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
|
||||
mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
|
||||
// Overlap boundary.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(
|
||||
now - (24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 2 * MINUTE_IN_MILLIS, 5));
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(
|
||||
now - (20 * HOUR_IN_MILLIS),
|
||||
3 * HOUR_IN_MILLIS + 48 * MINUTE_IN_MILLIS,
|
||||
5));
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(
|
||||
now - (8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
|
||||
|
||||
// Both max and bucket time have 8 minutes left.
|
||||
assertEquals(8 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
// Max time only has one minute phase out. Bucket time has 2 minute phase out.
|
||||
assertEquals(9 * MINUTE_IN_MILLIS,
|
||||
mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsWithinQuotaLocked_NeverApp() {
|
||||
assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test.never", NEVER_INDEX));
|
||||
@@ -1902,7 +2043,10 @@ public class QuotaControllerTest {
|
||||
// window, so as the package "reaches its quota" it will have more to keep running.
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(now - 2 * HOUR_IN_MILLIS,
|
||||
10 * MINUTE_IN_MILLIS - remainingTimeMs, 1));
|
||||
10 * SECOND_IN_MILLIS - remainingTimeMs, 1));
|
||||
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
|
||||
createTimingSession(now - HOUR_IN_MILLIS,
|
||||
9 * MINUTE_IN_MILLIS + 50 * SECOND_IN_MILLIS, 1));
|
||||
|
||||
assertEquals(remainingTimeMs, mQuotaController.getRemainingExecutionTimeLocked(jobStatus));
|
||||
// Start the job.
|
||||
@@ -1919,6 +2063,18 @@ public class QuotaControllerTest {
|
||||
// amount of remaining time left its quota.
|
||||
assertEquals(remainingTimeMs,
|
||||
mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
|
||||
verify(handler, atLeast(1)).sendMessageDelayed(any(), eq(remainingTimeMs));
|
||||
// Handler is told to check when the quota will be consumed, not when the initial
|
||||
// remaining time is over.
|
||||
verify(handler, atLeast(1)).sendMessageDelayed(any(), eq(10 * SECOND_IN_MILLIS));
|
||||
verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs));
|
||||
|
||||
// After 10 seconds, the job should finally be out of quota.
|
||||
advanceElapsedClock(10 * SECOND_IN_MILLIS - remainingTimeMs);
|
||||
// Wait for some extra time to allow for job processing.
|
||||
verify(mJobSchedulerService,
|
||||
timeout(12 * SECOND_IN_MILLIS).times(1))
|
||||
.onControllerStateChanged();
|
||||
assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
|
||||
verify(handler, never()).sendMessageDelayed(any(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user