Merge "Reducing quota expired check churn."

This commit is contained in:
Kweku Adams
2019-03-28 23:53:00 +00:00
committed by Android (Google) Code Review
2 changed files with 246 additions and 3 deletions

View File

@@ -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.");
}

View File

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