Loading apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +29 −15 Original line number Original line Diff line number Diff line Loading @@ -1563,11 +1563,11 @@ public final class QuotaController extends StateController { standbyBucket); standbyBucket); final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName); final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName); if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs final boolean inRegularQuota = stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs && isUnderJobCountQuota && isUnderJobCountQuota && isUnderTimingSessionCountQuota && isUnderTimingSessionCountQuota; && remainingEJQuota > 0) { if (inRegularQuota && remainingEJQuota > 0) { // Already in quota. Why was this method called? // Already in quota. Why was this method called? if (DEBUG) { if (DEBUG) { Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString Loading @@ -1582,10 +1582,7 @@ public final class QuotaController extends StateController { long inRegularQuotaTimeElapsed = Long.MAX_VALUE; long inRegularQuotaTimeElapsed = Long.MAX_VALUE; long inEJQuotaTimeElapsed = Long.MAX_VALUE; long inEJQuotaTimeElapsed = Long.MAX_VALUE; if (!(stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs if (!inRegularQuota) { && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs && isUnderJobCountQuota && isUnderTimingSessionCountQuota)) { // The time this app will have quota again. // The time this app will have quota again. long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { Loading @@ -1603,8 +1600,17 @@ public final class QuotaController extends StateController { } } if (remainingEJQuota <= 0) { if (remainingEJQuota <= 0) { final long limitMs = mEJLimitsMs[standbyBucket] - mQuotaBufferMs; final long limitMs = mEJLimitsMs[standbyBucket] - mQuotaBufferMs; List<TimingSession> timingSessions = mEJTimingSessions.get(userId, packageName); long sumMs = 0; long sumMs = 0; final Timer ejTimer = mEJPkgTimers.get(userId, packageName); if (ejTimer != null && ejTimer.isActive()) { final long nowElapsed = sElapsedRealtimeClock.millis(); sumMs += ejTimer.getCurrentDuration(nowElapsed); if (sumMs >= limitMs) { inEJQuotaTimeElapsed = (nowElapsed - limitMs) + mEJLimitWindowSizeMs; } } List<TimingSession> timingSessions = mEJTimingSessions.get(userId, packageName); if (timingSessions != null) { for (int i = timingSessions.size() - 1; i >= 0; --i) { for (int i = timingSessions.size() - 1; i >= 0; --i) { TimingSession ts = timingSessions.get(i); TimingSession ts = timingSessions.get(i); final long durationMs = ts.endTimeElapsed - ts.startTimeElapsed; final long durationMs = ts.endTimeElapsed - ts.startTimeElapsed; Loading @@ -1615,6 +1621,14 @@ public final class QuotaController extends StateController { break; break; } } } } } else if ((ejTimer == null || !ejTimer.isActive()) && inRegularQuota) { // In some strange cases, an app may end be in the NEVER bucket but could have run // some regular jobs. This results in no EJ timing sessions and QC having a bad // time. Slog.wtf(TAG, string(userId, packageName) + " has 0 EJ quota without running anything"); return; } } } long inQuotaTimeElapsed = Math.min(inRegularQuotaTimeElapsed, inEJQuotaTimeElapsed); long inQuotaTimeElapsed = Math.min(inRegularQuotaTimeElapsed, inEJQuotaTimeElapsed); Loading services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +73 −0 Original line number Original line Diff line number Diff line Loading @@ -1960,6 +1960,79 @@ public class QuotaControllerTest { .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); } } /** * Test that QC handles invalid cases where an app is in the NEVER bucket but has still run * jobs. */ @Test public void testMaybeScheduleStartAlarmLocked_Never_EffectiveNotNever() { // 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(); // The app is really in the NEVER bucket but is elevated somehow (eg via uidActive). setStandbyBucket(NEVER_INDEX); final int effectiveStandbyBucket = FREQUENT_INDEX; // No sessions saved yet. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, times(1)) .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, times(1)) .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); } @Test @Test public void testMaybeScheduleStartAlarmLocked_Rare() { public void testMaybeScheduleStartAlarmLocked_Rare() { // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests Loading Loading
apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +29 −15 Original line number Original line Diff line number Diff line Loading @@ -1563,11 +1563,11 @@ public final class QuotaController extends StateController { standbyBucket); standbyBucket); final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName); final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName); if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs final boolean inRegularQuota = stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs && isUnderJobCountQuota && isUnderJobCountQuota && isUnderTimingSessionCountQuota && isUnderTimingSessionCountQuota; && remainingEJQuota > 0) { if (inRegularQuota && remainingEJQuota > 0) { // Already in quota. Why was this method called? // Already in quota. Why was this method called? if (DEBUG) { if (DEBUG) { Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString Loading @@ -1582,10 +1582,7 @@ public final class QuotaController extends StateController { long inRegularQuotaTimeElapsed = Long.MAX_VALUE; long inRegularQuotaTimeElapsed = Long.MAX_VALUE; long inEJQuotaTimeElapsed = Long.MAX_VALUE; long inEJQuotaTimeElapsed = Long.MAX_VALUE; if (!(stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs if (!inRegularQuota) { && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs && isUnderJobCountQuota && isUnderTimingSessionCountQuota)) { // The time this app will have quota again. // The time this app will have quota again. long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { Loading @@ -1603,8 +1600,17 @@ public final class QuotaController extends StateController { } } if (remainingEJQuota <= 0) { if (remainingEJQuota <= 0) { final long limitMs = mEJLimitsMs[standbyBucket] - mQuotaBufferMs; final long limitMs = mEJLimitsMs[standbyBucket] - mQuotaBufferMs; List<TimingSession> timingSessions = mEJTimingSessions.get(userId, packageName); long sumMs = 0; long sumMs = 0; final Timer ejTimer = mEJPkgTimers.get(userId, packageName); if (ejTimer != null && ejTimer.isActive()) { final long nowElapsed = sElapsedRealtimeClock.millis(); sumMs += ejTimer.getCurrentDuration(nowElapsed); if (sumMs >= limitMs) { inEJQuotaTimeElapsed = (nowElapsed - limitMs) + mEJLimitWindowSizeMs; } } List<TimingSession> timingSessions = mEJTimingSessions.get(userId, packageName); if (timingSessions != null) { for (int i = timingSessions.size() - 1; i >= 0; --i) { for (int i = timingSessions.size() - 1; i >= 0; --i) { TimingSession ts = timingSessions.get(i); TimingSession ts = timingSessions.get(i); final long durationMs = ts.endTimeElapsed - ts.startTimeElapsed; final long durationMs = ts.endTimeElapsed - ts.startTimeElapsed; Loading @@ -1615,6 +1621,14 @@ public final class QuotaController extends StateController { break; break; } } } } } else if ((ejTimer == null || !ejTimer.isActive()) && inRegularQuota) { // In some strange cases, an app may end be in the NEVER bucket but could have run // some regular jobs. This results in no EJ timing sessions and QC having a bad // time. Slog.wtf(TAG, string(userId, packageName) + " has 0 EJ quota without running anything"); return; } } } long inQuotaTimeElapsed = Math.min(inRegularQuotaTimeElapsed, inEJQuotaTimeElapsed); long inQuotaTimeElapsed = Math.min(inRegularQuotaTimeElapsed, inEJQuotaTimeElapsed); Loading
services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +73 −0 Original line number Original line Diff line number Diff line Loading @@ -1960,6 +1960,79 @@ public class QuotaControllerTest { .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); } } /** * Test that QC handles invalid cases where an app is in the NEVER bucket but has still run * jobs. */ @Test public void testMaybeScheduleStartAlarmLocked_Never_EffectiveNotNever() { // 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(); // The app is really in the NEVER bucket but is elevated somehow (eg via uidActive). setStandbyBucket(NEVER_INDEX); final int effectiveStandbyBucket = FREQUENT_INDEX; // No sessions saved yet. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1), false); synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, times(1)) .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, times(1)) .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); } @Test @Test public void testMaybeScheduleStartAlarmLocked_Rare() { public void testMaybeScheduleStartAlarmLocked_Rare() { // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests Loading