Loading apex/jobscheduler/service/aconfig/job.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -28,3 +28,13 @@ flag { description: "Only relax a prefetch job's connectivity constraint when the device is charging and battery is not low" bug: "299329948" } flag { name: "count_quota_fix" namespace: "backstage_power" description: "Fix job count quota check" bug: "300862949" metadata { purpose: PURPOSE_BUGFIX } } apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +20 −0 Original line number Diff line number Diff line Loading @@ -1650,6 +1650,16 @@ class JobConcurrencyManager { continue; } if (Flags.countQuotaFix() && !nextPending.isReady()) { // This could happen when the constraints for the job have been marked // as unsatisfiled but hasn't been removed from the pending queue yet. if (DEBUG) { Slog.w(TAG, "Pending+not ready job: " + nextPending); } pendingJobQueue.remove(nextPending); continue; } if (DEBUG && isSimilarJobRunningLocked(nextPending)) { Slog.w(TAG, "Already running similar job to: " + nextPending); } Loading Loading @@ -1737,6 +1747,16 @@ class JobConcurrencyManager { continue; } if (Flags.countQuotaFix() && !nextPending.isReady()) { // This could happen when the constraints for the job have been marked // as unsatisfiled but hasn't been removed from the pending queue yet. if (DEBUG) { Slog.w(TAG, "Pending+not ready job: " + nextPending); } pendingJobQueue.remove(nextPending); continue; } if (DEBUG && isSimilarJobRunningLocked(nextPending)) { Slog.w(TAG, "Already running similar job to: " + nextPending); } Loading apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +80 −25 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ import com.android.server.AppSchedulingModuleThread; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; import com.android.server.job.ConstantsProto; import com.android.server.job.Flags; import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; import com.android.server.usage.AppStandbyInternal; Loading Loading @@ -512,7 +513,7 @@ public final class QuotaController extends StateController { /** An app has reached its quota. The message should contain a {@link UserPackage} object. */ @VisibleForTesting static final int MSG_REACHED_QUOTA = 0; static final int MSG_REACHED_TIME_QUOTA = 0; /** Drop any old timing sessions. */ private static final int MSG_CLEAN_UP_SESSIONS = 1; /** Check if a package is now within its quota. */ Loading @@ -524,7 +525,7 @@ public final class QuotaController extends StateController { * object. */ @VisibleForTesting static final int MSG_REACHED_EJ_QUOTA = 4; static final int MSG_REACHED_EJ_TIME_QUOTA = 4; /** * Process a new {@link UsageEvents.Event}. The event will be the message's object and the * userId will the first arg. Loading @@ -533,6 +534,11 @@ public final class QuotaController extends StateController { /** A UID's free quota grace period has ended. */ @VisibleForTesting static final int MSG_END_GRACE_PERIOD = 6; /** * An app has reached its job count quota. The message should contain a {@link UserPackage} * object. */ static final int MSG_REACHED_COUNT_QUOTA = 7; public QuotaController(@NonNull JobSchedulerService service, @NonNull BackgroundJobsController backgroundJobsController, Loading Loading @@ -874,12 +880,14 @@ public final class QuotaController extends StateController { } @VisibleForTesting @GuardedBy("mLock") boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) { final int standbyBucket = jobStatus.getEffectiveStandbyBucket(); // A job is within quota if one of the following is true: // 1. it was started while the app was in the TOP state // 2. the app is currently in the foreground // 3. the app overall is within its quota if (!Flags.countQuotaFix()) { return jobStatus.shouldTreatAsUserInitiatedJob() || isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid()) Loading @@ -887,6 +895,33 @@ public final class QuotaController extends StateController { jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); } if (jobStatus.shouldTreatAsUserInitiatedJob() || isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) { return true; } if (standbyBucket == NEVER_INDEX) return false; if (isQuotaFreeLocked(standbyBucket)) return true; final ExecutionStats stats = getExecutionStatsLocked(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); if (!(getRemainingExecutionTimeLocked(stats) > 0)) { // Out of execution time quota. return false; } if (standbyBucket != RESTRICTED_INDEX && mService.isCurrentlyRunningLocked(jobStatus)) { // Running job is considered as within quota except for the restricted one, which // requires additional constraints. return true; } // Check if the app is within job count quota. return isUnderJobCountQuotaLocked(stats) && isUnderSessionCountQuotaLocked(stats); } @GuardedBy("mLock") private boolean isQuotaFreeLocked(final int standbyBucket) { // Quota constraint is not enforced while charging. Loading @@ -909,12 +944,11 @@ public final class QuotaController extends StateController { ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); // TODO: use a higher minimum remaining time for jobs with MINIMUM priority return getRemainingExecutionTimeLocked(stats) > 0 && isUnderJobCountQuotaLocked(stats, standbyBucket) && isUnderSessionCountQuotaLocked(stats, standbyBucket); && isUnderJobCountQuotaLocked(stats) && isUnderSessionCountQuotaLocked(stats); } private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats, final int standbyBucket) { private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats) { final long now = sElapsedRealtimeClock.millis(); final boolean isUnderAllowedTimeQuota = (stats.jobRateLimitExpirationTimeElapsed <= now Loading @@ -923,8 +957,7 @@ public final class QuotaController extends StateController { && stats.bgJobCountInWindow < stats.jobCountLimit; } private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats, final int standbyBucket) { private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats) { final long now = sElapsedRealtimeClock.millis(); final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow); Loading Loading @@ -1449,6 +1482,9 @@ public final class QuotaController extends StateController { stats.jobCountInRateLimitingWindow = 0; } stats.jobCountInRateLimitingWindow += count; if (Flags.countQuotaFix()) { stats.bgJobCountInWindow += count; } } } Loading Loading @@ -1683,10 +1719,11 @@ public final class QuotaController extends StateController { changedJobs.add(js); } } else if (realStandbyBucket != EXEMPTED_INDEX && realStandbyBucket != ACTIVE_INDEX && realStandbyBucket == js.getEffectiveStandbyBucket()) { && realStandbyBucket == js.getEffectiveStandbyBucket() && !(Flags.countQuotaFix() && mService.isCurrentlyRunningLocked(js))) { // An app in the ACTIVE bucket may be out of quota while the job could be in quota // for some reason. Therefore, avoid setting the real value here and check each job // individually. // individually. Running job need to determine its own quota status as well. if (setConstraintSatisfied(js, nowElapsed, realInQuota, isWithinEJQuota)) { changedJobs.add(js); } Loading Loading @@ -1805,9 +1842,8 @@ public final class QuotaController extends StateController { } ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket); final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats, standbyBucket); final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats); final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats); final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName); final boolean inRegularQuota = Loading Loading @@ -2126,6 +2162,13 @@ public final class QuotaController extends StateController { mBgJobCount++; if (mRegularJobTimer) { incrementJobCountLocked(mPkg.userId, mPkg.packageName, 1); if (Flags.countQuotaFix()) { final ExecutionStats stats = getExecutionStatsLocked(mPkg.userId, mPkg.packageName, jobStatus.getEffectiveStandbyBucket(), false); if (!isUnderJobCountQuotaLocked(stats)) { mHandler.obtainMessage(MSG_REACHED_COUNT_QUOTA, mPkg).sendToTarget(); } } } if (mRunningBgJobs.size() == 1) { // Started tracking the first job. Loading Loading @@ -2257,7 +2300,6 @@ public final class QuotaController extends StateController { // repeatedly plugged in and unplugged, or an app changes foreground state // very frequently, the job count for a package may be artificially high. mBgJobCount = mRunningBgJobs.size(); if (mRegularJobTimer) { incrementJobCountLocked(mPkg.userId, mPkg.packageName, mBgJobCount); // Starting the timer means that all cached execution stats are now Loading @@ -2284,7 +2326,8 @@ public final class QuotaController extends StateController { return; } Message msg = mHandler.obtainMessage( mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg); mRegularJobTimer ? MSG_REACHED_TIME_QUOTA : MSG_REACHED_EJ_TIME_QUOTA, mPkg); final long timeRemainingMs = mRegularJobTimer ? getTimeUntilQuotaConsumedLocked(mPkg.userId, mPkg.packageName) : getTimeUntilEJQuotaConsumedLocked(mPkg.userId, mPkg.packageName); Loading @@ -2301,7 +2344,7 @@ public final class QuotaController extends StateController { private void cancelCutoff() { mHandler.removeMessages( mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg); mRegularJobTimer ? MSG_REACHED_TIME_QUOTA : MSG_REACHED_EJ_TIME_QUOTA, mPkg); } public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { Loading Loading @@ -2557,7 +2600,7 @@ public final class QuotaController extends StateController { break; default: if (DEBUG) { Slog.d(TAG, "Dropping event " + event.getEventType()); Slog.d(TAG, "Dropping usage event " + event.getEventType()); } break; } Loading Loading @@ -2666,7 +2709,7 @@ public final class QuotaController extends StateController { public void handleMessage(Message msg) { synchronized (mLock) { switch (msg.what) { case MSG_REACHED_QUOTA: { case MSG_REACHED_TIME_QUOTA: { UserPackage pkg = (UserPackage) msg.obj; if (DEBUG) { Slog.d(TAG, "Checking if " + pkg + " has reached its quota."); Loading @@ -2685,7 +2728,7 @@ public final class QuotaController extends StateController { // This could potentially happen if an old session phases out while a // job is currently running. // Reschedule message Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg); Message rescheduleMsg = obtainMessage(MSG_REACHED_TIME_QUOTA, pkg); timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId, pkg.packageName); if (DEBUG) { Loading @@ -2695,7 +2738,7 @@ public final class QuotaController extends StateController { } break; } case MSG_REACHED_EJ_QUOTA: { case MSG_REACHED_EJ_TIME_QUOTA: { UserPackage pkg = (UserPackage) msg.obj; if (DEBUG) { Slog.d(TAG, "Checking if " + pkg + " has reached its EJ quota."); Loading @@ -2713,7 +2756,7 @@ public final class QuotaController extends StateController { // This could potentially happen if an old session phases out while a // job is currently running. // Reschedule message Message rescheduleMsg = obtainMessage(MSG_REACHED_EJ_QUOTA, pkg); Message rescheduleMsg = obtainMessage(MSG_REACHED_EJ_TIME_QUOTA, pkg); timeRemainingMs = getTimeUntilEJQuotaConsumedLocked( pkg.userId, pkg.packageName); if (DEBUG) { Loading @@ -2723,6 +2766,18 @@ public final class QuotaController extends StateController { } break; } case MSG_REACHED_COUNT_QUOTA: { UserPackage pkg = (UserPackage) msg.obj; if (DEBUG) { Slog.d(TAG, pkg + " has reached its count quota."); } mStateChangedListener.onControllerStateChanged( maybeUpdateConstraintForPkgLocked( sElapsedRealtimeClock.millis(), pkg.userId, pkg.packageName)); break; } case MSG_CLEAN_UP_SESSIONS: if (DEBUG) { Slog.d(TAG, "Cleaning up timing sessions."); Loading services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +81 −4 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; Loading Loading @@ -75,6 +76,9 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.SparseBooleanArray; Loading @@ -98,6 +102,7 @@ import com.android.server.usage.AppStandbyInternal; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; Loading Loading @@ -154,6 +159,10 @@ public class QuotaControllerTest { @Mock private UsageStatsManagerInternal mUsageStatsManager; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private JobStore mJobStore; @Before Loading Loading @@ -1978,7 +1987,7 @@ public class QuotaControllerTest { } @Test public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() { public void testIsWithinQuotaLocked_UnderDuration_OverJobCountRateLimitWindow() { setDischarging(); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; Loading Loading @@ -2021,7 +2030,7 @@ public class QuotaControllerTest { } @Test public void testIsWithinQuotaLocked_OverDuration_OverJobCount() { public void testIsWithinQuotaLocked_OverDuration_OverJobCountRateLimitWindow() { setDischarging(); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; Loading Loading @@ -2166,6 +2175,74 @@ public class QuotaControllerTest { } } @Test @RequiresFlagsEnabled(FLAG_COUNT_QUOTA_FIX) public void testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow() { setDischarging(); JobStatus jobRunning = createJobStatus( "testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow", 1); JobStatus jobPending = createJobStatus( "testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow", 2); setStandbyBucket(WORKING_INDEX, jobRunning, jobPending); setDeviceConfigInt(QcConstants.KEY_MAX_JOB_COUNT_WORKING, 10); long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - (HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 9), false); final ExecutionStats stats; synchronized (mQuotaController.mLock) { stats = mQuotaController.getExecutionStatsLocked( SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); assertTrue(mQuotaController .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); assertEquals(10, stats.jobCountLimit); assertEquals(9, stats.bgJobCountInWindow); } when(mJobSchedulerService.isCurrentlyRunningLocked(jobRunning)).thenReturn(true); when(mJobSchedulerService.isCurrentlyRunningLocked(jobPending)).thenReturn(false); InOrder inOrder = inOrder(mJobSchedulerService); trackJobs(jobRunning, jobPending); // UID in the background. setProcessState(ActivityManager.PROCESS_STATE_SERVICE); // Start the job. synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobRunning); } advanceElapsedClock(MINUTE_IN_MILLIS); // Wait for some extra time to allow for job processing. ArraySet<JobStatus> expected = new ArraySet<>(); expected.add(jobPending); inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) .onControllerStateChanged(eq(expected)); synchronized (mQuotaController.mLock) { assertTrue(mQuotaController.isWithinQuotaLocked(jobRunning)); assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); assertTrue(jobRunning.isReady()); assertFalse(mQuotaController.isWithinQuotaLocked(jobPending)); assertFalse(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); assertFalse(jobPending.isReady()); assertEquals(10, stats.bgJobCountInWindow); } advanceElapsedClock(MINUTE_IN_MILLIS); synchronized (mQuotaController.mLock) { mQuotaController.maybeStopTrackingJobLocked(jobRunning, null); } synchronized (mQuotaController.mLock) { assertFalse(mQuotaController .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); assertEquals(10, stats.bgJobCountInWindow); } } @Test public void testIsWithinQuotaLocked_TimingSession() { setDischarging(); Loading Loading @@ -4651,7 +4728,7 @@ public class QuotaControllerTest { // Handler is told to check when the quota will be consumed, not when the initial // remaining time is over. verify(handler, atLeast(1)).sendMessageDelayed( argThat(msg -> msg.what == QuotaController.MSG_REACHED_QUOTA), argThat(msg -> msg.what == QuotaController.MSG_REACHED_TIME_QUOTA), eq(10 * SECOND_IN_MILLIS)); verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs)); Loading Loading @@ -6618,7 +6695,7 @@ public class QuotaControllerTest { // Handler is told to check when the quota will be consumed, not when the initial // remaining time is over. verify(handler, atLeast(1)).sendMessageDelayed( argThat(msg -> msg.what == QuotaController.MSG_REACHED_EJ_QUOTA), argThat(msg -> msg.what == QuotaController.MSG_REACHED_EJ_TIME_QUOTA), eq(10 * SECOND_IN_MILLIS)); verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs)); } Loading Loading
apex/jobscheduler/service/aconfig/job.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -28,3 +28,13 @@ flag { description: "Only relax a prefetch job's connectivity constraint when the device is charging and battery is not low" bug: "299329948" } flag { name: "count_quota_fix" namespace: "backstage_power" description: "Fix job count quota check" bug: "300862949" metadata { purpose: PURPOSE_BUGFIX } }
apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +20 −0 Original line number Diff line number Diff line Loading @@ -1650,6 +1650,16 @@ class JobConcurrencyManager { continue; } if (Flags.countQuotaFix() && !nextPending.isReady()) { // This could happen when the constraints for the job have been marked // as unsatisfiled but hasn't been removed from the pending queue yet. if (DEBUG) { Slog.w(TAG, "Pending+not ready job: " + nextPending); } pendingJobQueue.remove(nextPending); continue; } if (DEBUG && isSimilarJobRunningLocked(nextPending)) { Slog.w(TAG, "Already running similar job to: " + nextPending); } Loading Loading @@ -1737,6 +1747,16 @@ class JobConcurrencyManager { continue; } if (Flags.countQuotaFix() && !nextPending.isReady()) { // This could happen when the constraints for the job have been marked // as unsatisfiled but hasn't been removed from the pending queue yet. if (DEBUG) { Slog.w(TAG, "Pending+not ready job: " + nextPending); } pendingJobQueue.remove(nextPending); continue; } if (DEBUG && isSimilarJobRunningLocked(nextPending)) { Slog.w(TAG, "Already running similar job to: " + nextPending); } Loading
apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +80 −25 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ import com.android.server.AppSchedulingModuleThread; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; import com.android.server.job.ConstantsProto; import com.android.server.job.Flags; import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; import com.android.server.usage.AppStandbyInternal; Loading Loading @@ -512,7 +513,7 @@ public final class QuotaController extends StateController { /** An app has reached its quota. The message should contain a {@link UserPackage} object. */ @VisibleForTesting static final int MSG_REACHED_QUOTA = 0; static final int MSG_REACHED_TIME_QUOTA = 0; /** Drop any old timing sessions. */ private static final int MSG_CLEAN_UP_SESSIONS = 1; /** Check if a package is now within its quota. */ Loading @@ -524,7 +525,7 @@ public final class QuotaController extends StateController { * object. */ @VisibleForTesting static final int MSG_REACHED_EJ_QUOTA = 4; static final int MSG_REACHED_EJ_TIME_QUOTA = 4; /** * Process a new {@link UsageEvents.Event}. The event will be the message's object and the * userId will the first arg. Loading @@ -533,6 +534,11 @@ public final class QuotaController extends StateController { /** A UID's free quota grace period has ended. */ @VisibleForTesting static final int MSG_END_GRACE_PERIOD = 6; /** * An app has reached its job count quota. The message should contain a {@link UserPackage} * object. */ static final int MSG_REACHED_COUNT_QUOTA = 7; public QuotaController(@NonNull JobSchedulerService service, @NonNull BackgroundJobsController backgroundJobsController, Loading Loading @@ -874,12 +880,14 @@ public final class QuotaController extends StateController { } @VisibleForTesting @GuardedBy("mLock") boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) { final int standbyBucket = jobStatus.getEffectiveStandbyBucket(); // A job is within quota if one of the following is true: // 1. it was started while the app was in the TOP state // 2. the app is currently in the foreground // 3. the app overall is within its quota if (!Flags.countQuotaFix()) { return jobStatus.shouldTreatAsUserInitiatedJob() || isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid()) Loading @@ -887,6 +895,33 @@ public final class QuotaController extends StateController { jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); } if (jobStatus.shouldTreatAsUserInitiatedJob() || isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) { return true; } if (standbyBucket == NEVER_INDEX) return false; if (isQuotaFreeLocked(standbyBucket)) return true; final ExecutionStats stats = getExecutionStatsLocked(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); if (!(getRemainingExecutionTimeLocked(stats) > 0)) { // Out of execution time quota. return false; } if (standbyBucket != RESTRICTED_INDEX && mService.isCurrentlyRunningLocked(jobStatus)) { // Running job is considered as within quota except for the restricted one, which // requires additional constraints. return true; } // Check if the app is within job count quota. return isUnderJobCountQuotaLocked(stats) && isUnderSessionCountQuotaLocked(stats); } @GuardedBy("mLock") private boolean isQuotaFreeLocked(final int standbyBucket) { // Quota constraint is not enforced while charging. Loading @@ -909,12 +944,11 @@ public final class QuotaController extends StateController { ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); // TODO: use a higher minimum remaining time for jobs with MINIMUM priority return getRemainingExecutionTimeLocked(stats) > 0 && isUnderJobCountQuotaLocked(stats, standbyBucket) && isUnderSessionCountQuotaLocked(stats, standbyBucket); && isUnderJobCountQuotaLocked(stats) && isUnderSessionCountQuotaLocked(stats); } private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats, final int standbyBucket) { private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats) { final long now = sElapsedRealtimeClock.millis(); final boolean isUnderAllowedTimeQuota = (stats.jobRateLimitExpirationTimeElapsed <= now Loading @@ -923,8 +957,7 @@ public final class QuotaController extends StateController { && stats.bgJobCountInWindow < stats.jobCountLimit; } private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats, final int standbyBucket) { private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats) { final long now = sElapsedRealtimeClock.millis(); final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow); Loading Loading @@ -1449,6 +1482,9 @@ public final class QuotaController extends StateController { stats.jobCountInRateLimitingWindow = 0; } stats.jobCountInRateLimitingWindow += count; if (Flags.countQuotaFix()) { stats.bgJobCountInWindow += count; } } } Loading Loading @@ -1683,10 +1719,11 @@ public final class QuotaController extends StateController { changedJobs.add(js); } } else if (realStandbyBucket != EXEMPTED_INDEX && realStandbyBucket != ACTIVE_INDEX && realStandbyBucket == js.getEffectiveStandbyBucket()) { && realStandbyBucket == js.getEffectiveStandbyBucket() && !(Flags.countQuotaFix() && mService.isCurrentlyRunningLocked(js))) { // An app in the ACTIVE bucket may be out of quota while the job could be in quota // for some reason. Therefore, avoid setting the real value here and check each job // individually. // individually. Running job need to determine its own quota status as well. if (setConstraintSatisfied(js, nowElapsed, realInQuota, isWithinEJQuota)) { changedJobs.add(js); } Loading Loading @@ -1805,9 +1842,8 @@ public final class QuotaController extends StateController { } ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket); final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats, standbyBucket); final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats); final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats); final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName); final boolean inRegularQuota = Loading Loading @@ -2126,6 +2162,13 @@ public final class QuotaController extends StateController { mBgJobCount++; if (mRegularJobTimer) { incrementJobCountLocked(mPkg.userId, mPkg.packageName, 1); if (Flags.countQuotaFix()) { final ExecutionStats stats = getExecutionStatsLocked(mPkg.userId, mPkg.packageName, jobStatus.getEffectiveStandbyBucket(), false); if (!isUnderJobCountQuotaLocked(stats)) { mHandler.obtainMessage(MSG_REACHED_COUNT_QUOTA, mPkg).sendToTarget(); } } } if (mRunningBgJobs.size() == 1) { // Started tracking the first job. Loading Loading @@ -2257,7 +2300,6 @@ public final class QuotaController extends StateController { // repeatedly plugged in and unplugged, or an app changes foreground state // very frequently, the job count for a package may be artificially high. mBgJobCount = mRunningBgJobs.size(); if (mRegularJobTimer) { incrementJobCountLocked(mPkg.userId, mPkg.packageName, mBgJobCount); // Starting the timer means that all cached execution stats are now Loading @@ -2284,7 +2326,8 @@ public final class QuotaController extends StateController { return; } Message msg = mHandler.obtainMessage( mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg); mRegularJobTimer ? MSG_REACHED_TIME_QUOTA : MSG_REACHED_EJ_TIME_QUOTA, mPkg); final long timeRemainingMs = mRegularJobTimer ? getTimeUntilQuotaConsumedLocked(mPkg.userId, mPkg.packageName) : getTimeUntilEJQuotaConsumedLocked(mPkg.userId, mPkg.packageName); Loading @@ -2301,7 +2344,7 @@ public final class QuotaController extends StateController { private void cancelCutoff() { mHandler.removeMessages( mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg); mRegularJobTimer ? MSG_REACHED_TIME_QUOTA : MSG_REACHED_EJ_TIME_QUOTA, mPkg); } public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { Loading Loading @@ -2557,7 +2600,7 @@ public final class QuotaController extends StateController { break; default: if (DEBUG) { Slog.d(TAG, "Dropping event " + event.getEventType()); Slog.d(TAG, "Dropping usage event " + event.getEventType()); } break; } Loading Loading @@ -2666,7 +2709,7 @@ public final class QuotaController extends StateController { public void handleMessage(Message msg) { synchronized (mLock) { switch (msg.what) { case MSG_REACHED_QUOTA: { case MSG_REACHED_TIME_QUOTA: { UserPackage pkg = (UserPackage) msg.obj; if (DEBUG) { Slog.d(TAG, "Checking if " + pkg + " has reached its quota."); Loading @@ -2685,7 +2728,7 @@ public final class QuotaController extends StateController { // This could potentially happen if an old session phases out while a // job is currently running. // Reschedule message Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg); Message rescheduleMsg = obtainMessage(MSG_REACHED_TIME_QUOTA, pkg); timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId, pkg.packageName); if (DEBUG) { Loading @@ -2695,7 +2738,7 @@ public final class QuotaController extends StateController { } break; } case MSG_REACHED_EJ_QUOTA: { case MSG_REACHED_EJ_TIME_QUOTA: { UserPackage pkg = (UserPackage) msg.obj; if (DEBUG) { Slog.d(TAG, "Checking if " + pkg + " has reached its EJ quota."); Loading @@ -2713,7 +2756,7 @@ public final class QuotaController extends StateController { // This could potentially happen if an old session phases out while a // job is currently running. // Reschedule message Message rescheduleMsg = obtainMessage(MSG_REACHED_EJ_QUOTA, pkg); Message rescheduleMsg = obtainMessage(MSG_REACHED_EJ_TIME_QUOTA, pkg); timeRemainingMs = getTimeUntilEJQuotaConsumedLocked( pkg.userId, pkg.packageName); if (DEBUG) { Loading @@ -2723,6 +2766,18 @@ public final class QuotaController extends StateController { } break; } case MSG_REACHED_COUNT_QUOTA: { UserPackage pkg = (UserPackage) msg.obj; if (DEBUG) { Slog.d(TAG, pkg + " has reached its count quota."); } mStateChangedListener.onControllerStateChanged( maybeUpdateConstraintForPkgLocked( sElapsedRealtimeClock.millis(), pkg.userId, pkg.packageName)); break; } case MSG_CLEAN_UP_SESSIONS: if (DEBUG) { Slog.d(TAG, "Cleaning up timing sessions."); Loading
services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +81 −4 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; Loading Loading @@ -75,6 +76,9 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.SparseBooleanArray; Loading @@ -98,6 +102,7 @@ import com.android.server.usage.AppStandbyInternal; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; Loading Loading @@ -154,6 +159,10 @@ public class QuotaControllerTest { @Mock private UsageStatsManagerInternal mUsageStatsManager; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private JobStore mJobStore; @Before Loading Loading @@ -1978,7 +1987,7 @@ public class QuotaControllerTest { } @Test public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() { public void testIsWithinQuotaLocked_UnderDuration_OverJobCountRateLimitWindow() { setDischarging(); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; Loading Loading @@ -2021,7 +2030,7 @@ public class QuotaControllerTest { } @Test public void testIsWithinQuotaLocked_OverDuration_OverJobCount() { public void testIsWithinQuotaLocked_OverDuration_OverJobCountRateLimitWindow() { setDischarging(); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; Loading Loading @@ -2166,6 +2175,74 @@ public class QuotaControllerTest { } } @Test @RequiresFlagsEnabled(FLAG_COUNT_QUOTA_FIX) public void testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow() { setDischarging(); JobStatus jobRunning = createJobStatus( "testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow", 1); JobStatus jobPending = createJobStatus( "testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow", 2); setStandbyBucket(WORKING_INDEX, jobRunning, jobPending); setDeviceConfigInt(QcConstants.KEY_MAX_JOB_COUNT_WORKING, 10); long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - (HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 9), false); final ExecutionStats stats; synchronized (mQuotaController.mLock) { stats = mQuotaController.getExecutionStatsLocked( SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); assertTrue(mQuotaController .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); assertEquals(10, stats.jobCountLimit); assertEquals(9, stats.bgJobCountInWindow); } when(mJobSchedulerService.isCurrentlyRunningLocked(jobRunning)).thenReturn(true); when(mJobSchedulerService.isCurrentlyRunningLocked(jobPending)).thenReturn(false); InOrder inOrder = inOrder(mJobSchedulerService); trackJobs(jobRunning, jobPending); // UID in the background. setProcessState(ActivityManager.PROCESS_STATE_SERVICE); // Start the job. synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobRunning); } advanceElapsedClock(MINUTE_IN_MILLIS); // Wait for some extra time to allow for job processing. ArraySet<JobStatus> expected = new ArraySet<>(); expected.add(jobPending); inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) .onControllerStateChanged(eq(expected)); synchronized (mQuotaController.mLock) { assertTrue(mQuotaController.isWithinQuotaLocked(jobRunning)); assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); assertTrue(jobRunning.isReady()); assertFalse(mQuotaController.isWithinQuotaLocked(jobPending)); assertFalse(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); assertFalse(jobPending.isReady()); assertEquals(10, stats.bgJobCountInWindow); } advanceElapsedClock(MINUTE_IN_MILLIS); synchronized (mQuotaController.mLock) { mQuotaController.maybeStopTrackingJobLocked(jobRunning, null); } synchronized (mQuotaController.mLock) { assertFalse(mQuotaController .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); assertEquals(10, stats.bgJobCountInWindow); } } @Test public void testIsWithinQuotaLocked_TimingSession() { setDischarging(); Loading Loading @@ -4651,7 +4728,7 @@ public class QuotaControllerTest { // Handler is told to check when the quota will be consumed, not when the initial // remaining time is over. verify(handler, atLeast(1)).sendMessageDelayed( argThat(msg -> msg.what == QuotaController.MSG_REACHED_QUOTA), argThat(msg -> msg.what == QuotaController.MSG_REACHED_TIME_QUOTA), eq(10 * SECOND_IN_MILLIS)); verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs)); Loading Loading @@ -6618,7 +6695,7 @@ public class QuotaControllerTest { // Handler is told to check when the quota will be consumed, not when the initial // remaining time is over. verify(handler, atLeast(1)).sendMessageDelayed( argThat(msg -> msg.what == QuotaController.MSG_REACHED_EJ_QUOTA), argThat(msg -> msg.what == QuotaController.MSG_REACHED_EJ_TIME_QUOTA), eq(10 * SECOND_IN_MILLIS)); verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs)); } Loading