diff options
3 files changed, 488 insertions, 6 deletions
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index 98e53ab97872..810be8fc4220 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -88,4 +88,11 @@ flag { namespace: "backstage_power" description: "Adjust quota default parameters" bug: "347058927" +} + +flag { + name: "enforce_quota_policy_to_top_started_jobs" + namespace: "backstage_power" + description: "Apply the quota policy to jobs started when the app was in TOP state" + bug: "374323858" }
\ No newline at end of file diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index 885bad5e31c8..37e2fe2e46f1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -619,7 +619,7 @@ public final class QuotaController extends StateController { } final int uid = jobStatus.getSourceUid(); - if (mTopAppCache.get(uid)) { + if (!Flags.enforceQuotaPolicyToTopStartedJobs() && mTopAppCache.get(uid)) { if (DEBUG) { Slog.d(TAG, jobStatus.toShortString() + " is top started job"); } @@ -656,7 +656,9 @@ public final class QuotaController extends StateController { timer.stopTrackingJob(jobStatus); } } - mTopStartedJobs.remove(jobStatus); + if (!Flags.enforceQuotaPolicyToTopStartedJobs()) { + mTopStartedJobs.remove(jobStatus); + } } @Override @@ -767,7 +769,7 @@ public final class QuotaController extends StateController { /** @return true if the job was started while the app was in the TOP state. */ private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) { - return mTopStartedJobs.contains(jobStatus); + return !Flags.enforceQuotaPolicyToTopStartedJobs() && mTopStartedJobs.contains(jobStatus); } /** Returns the maximum amount of time this job could run for. */ @@ -4379,11 +4381,13 @@ public final class QuotaController extends StateController { @Override public void dumpControllerStateLocked(final IndentingPrintWriter pw, final Predicate<JobStatus> predicate) { - pw.println("Flags: "); + pw.println("Aconfig Flags:"); pw.println(" " + Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS + ": " + Flags.adjustQuotaDefaultConstants()); pw.println(" " + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS + ": " + Flags.enforceQuotaPolicyToFgsJobs()); + pw.println(" " + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS + + ": " + Flags.enforceQuotaPolicyToTopStartedJobs()); pw.println(); pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis()); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index 5c718d982476..b2fe138e3342 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -1726,6 +1726,8 @@ public class QuotaControllerTest { } // Top-started job + mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); + // Top-stared jobs are out of quota enforcement. setProcessState(ActivityManager.PROCESS_STATE_TOP); synchronized (mQuotaController.mLock) { trackJobs(job, jobDefIWF, jobHigh); @@ -1755,6 +1757,38 @@ public class QuotaControllerTest { assertEquals(timeUntilQuotaConsumedMs, mQuotaController.getMaxJobExecutionTimeMsLocked(jobHigh)); } + + mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); + // Quota is enforced for top-started job after the process leaves TOP/BTOP state. + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + trackJobs(job, jobDefIWF, jobHigh); + mQuotaController.prepareForExecutionLocked(job); + mQuotaController.prepareForExecutionLocked(jobDefIWF); + mQuotaController.prepareForExecutionLocked(jobHigh); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + assertEquals(timeUntilQuotaConsumedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked((job))); + assertEquals(timeUntilQuotaConsumedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF))); + assertEquals(timeUntilQuotaConsumedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh))); + mQuotaController.maybeStopTrackingJobLocked(job, null); + mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null); + mQuotaController.maybeStopTrackingJobLocked(jobHigh, null); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(timeUntilQuotaConsumedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + assertEquals(timeUntilQuotaConsumedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF)); + assertEquals(timeUntilQuotaConsumedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(jobHigh)); + } } @Test @@ -1824,6 +1858,7 @@ public class QuotaControllerTest { mQuotaController.getMaxJobExecutionTimeMsLocked(job)); } + mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); // Top-started job setProcessState(ActivityManager.PROCESS_STATE_TOP); synchronized (mQuotaController.mLock) { @@ -1831,6 +1866,7 @@ public class QuotaControllerTest { } setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); synchronized (mQuotaController.mLock) { + // Top-started job is out of quota enforcement. assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); mQuotaController.maybeStopTrackingJobLocked(job, null); @@ -1842,6 +1878,28 @@ public class QuotaControllerTest { mQuotaController.getMaxJobExecutionTimeMsLocked(job)); } + mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + // Top-started job is enforced by quota policy after the app leaves the TOP state. + // The max execution time should be the total EJ session limit of the RARE bucket + // minus the time has been used. + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + // Test used quota rolling out of window. synchronized (mQuotaController.mLock) { mQuotaController.clearAppStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE); @@ -1856,6 +1914,7 @@ public class QuotaControllerTest { mQuotaController.getMaxJobExecutionTimeMsLocked(job)); } + mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); // Top-started job setProcessState(ActivityManager.PROCESS_STATE_TOP); synchronized (mQuotaController.mLock) { @@ -1864,6 +1923,7 @@ public class QuotaControllerTest { } setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); synchronized (mQuotaController.mLock) { + // Top-started job is out of quota enforcement. assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); mQuotaController.maybeStopTrackingJobLocked(job, null); @@ -1874,6 +1934,28 @@ public class QuotaControllerTest { assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); } + + mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job, null); + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + // Top-started job is enforced by quota policy after the app leaves the TOP state. + // The max execution time should be the total EJ session limit of the RARE bucket. + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } } @Test @@ -1902,6 +1984,7 @@ public class QuotaControllerTest { mQuotaController.getMaxJobExecutionTimeMsLocked(job)); } + mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); // Top-started job setProcessState(ActivityManager.PROCESS_STATE_TOP); synchronized (mQuotaController.mLock) { @@ -1909,6 +1992,7 @@ public class QuotaControllerTest { } setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); synchronized (mQuotaController.mLock) { + // Top-started job is out of quota enforcement. assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); mQuotaController.maybeStopTrackingJobLocked(job, null); @@ -1920,6 +2004,27 @@ public class QuotaControllerTest { mQuotaController.getMaxJobExecutionTimeMsLocked(job)); } + mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + // Top-started job is enforced by quota policy after the app leaves the TOP state. + // The max execution time should be the total EJ session limit of the RARE bucket. + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + // Test used quota rolling out of window. synchronized (mQuotaController.mLock) { mQuotaController.clearAppStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE); @@ -1935,6 +2040,7 @@ public class QuotaControllerTest { mQuotaController.getMaxJobExecutionTimeMsLocked(job)); } + mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); // Top-started job setProcessState(ActivityManager.PROCESS_STATE_TOP); synchronized (mQuotaController.mLock) { @@ -1943,6 +2049,7 @@ public class QuotaControllerTest { } setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); synchronized (mQuotaController.mLock) { + // Top-started job is out of quota enforcement. assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); mQuotaController.maybeStopTrackingJobLocked(job, null); @@ -1953,6 +2060,28 @@ public class QuotaControllerTest { assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); } + + mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job, null); + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + // Top-started job is enforced by quota policy after the app leaves the TOP state. + // The max execution time should be the total EJ session limit of the RARE bucket. + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } } /** @@ -4608,6 +4737,7 @@ public class QuotaControllerTest { assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); advanceElapsedClock(SECOND_IN_MILLIS); + mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); // Bg job starts while inactive, spans an entire active session, and ends after the // active session. @@ -4686,8 +4816,66 @@ public class QuotaControllerTest { mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); mQuotaController.maybeStopTrackingJobLocked(jobFg1, null); } + // jobBg2 and jobFg1 are counted, jobTop is not counted. expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + + advanceElapsedClock(SECOND_IN_MILLIS); + mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); + + // Bg job 1 starts, then top job starts. Bg job 1 job ends. Then app goes to + // foreground_service and a new job starts. Shortly after, uid goes + // "inactive" and then bg job 2 starts. Then top job ends, followed by bg and fg jobs. + // This should result in two TimingSessions: + // * The first should have a count of 1 + // * The second should have a count of 2, which accounts for the bg2 and fg and top jobs. + // Top started jobs are not quota free any more if the process leaves TOP/BTOP state. + start = JobSchedulerService.sElapsedRealtimeClock.millis(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(jobBg1, null); + mQuotaController.maybeStartTrackingJobLocked(jobBg2, null); + mQuotaController.maybeStartTrackingJobLocked(jobFg1, null); + mQuotaController.maybeStartTrackingJobLocked(jobTop, null); + } + setProcessState(ActivityManager.PROCESS_STATE_LAST_ACTIVITY); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobBg1); + } + advanceElapsedClock(10 * SECOND_IN_MILLIS); + expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobTop); + } + advanceElapsedClock(10 * SECOND_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); + } + advanceElapsedClock(5 * SECOND_IN_MILLIS); + setProcessState(getProcessStateQuotaFreeThreshold()); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobFg1); + } + advanceElapsedClock(5 * SECOND_IN_MILLIS); + setProcessState(ActivityManager.PROCESS_STATE_TOP); + advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now + start = JobSchedulerService.sElapsedRealtimeClock.millis(); + setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobBg2); + } + advanceElapsedClock(10 * SECOND_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(jobTop, null); + } + advanceElapsedClock(10 * SECOND_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); + mQuotaController.maybeStopTrackingJobLocked(jobFg1, null); + } + // jobBg2, jobFg1 and jobTop are counted. + expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 3)); + assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } /** @@ -4807,7 +4995,8 @@ public class QuotaControllerTest { * Tests that TOP jobs aren't stopped when an app runs out of quota. */ @Test - public void testTracking_OutOfQuota_ForegroundAndBackground() { + @DisableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS) + public void testTracking_OutOfQuota_ForegroundAndBackground_DisableTopStartedJobsThrottling() { setDischarging(); JobStatus jobBg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 1); @@ -4851,6 +5040,7 @@ public class QuotaControllerTest { // Go to a background state. setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); advanceElapsedClock(remainingTimeMs / 2 + 1); + // Only Bg job will be changed from in-quota to out-of-quota. inOrder.verify(mJobSchedulerService, timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1)) .onControllerStateChanged(argThat(jobs -> jobs.size() == 1)); @@ -4897,6 +5087,105 @@ public class QuotaControllerTest { } /** + * Tests that TOP jobs are stopped when an app runs out of quota. + */ + @Test + @EnableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS) + public void testTracking_OutOfQuota_ForegroundAndBackground_EnableTopStartedJobsThrottling() { + setDischarging(); + + JobStatus jobBg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 1); + JobStatus jobTop = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 2); + trackJobs(jobBg, jobTop); + setStandbyBucket(WORKING_INDEX, jobTop, jobBg); // 2 hour window + // Now the package only has 20 seconds to run. + final long remainingTimeMs = 20 * SECOND_IN_MILLIS; + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS, + 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1), false); + + InOrder inOrder = inOrder(mJobSchedulerService); + + // UID starts out inactive. + setProcessState(ActivityManager.PROCESS_STATE_SERVICE); + // Start the job. + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobBg); + } + advanceElapsedClock(remainingTimeMs / 2); + // New job starts after UID is in the foreground. Since the app is now in the foreground, it + // should continue to have remainingTimeMs / 2 time remaining. + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobTop); + } + advanceElapsedClock(remainingTimeMs); + + // Wait for some extra time to allow for job processing. + inOrder.verify(mJobSchedulerService, + timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) + .onControllerStateChanged(argThat(jobs -> jobs.size() > 0)); + synchronized (mQuotaController.mLock) { + assertEquals(remainingTimeMs / 2, + mQuotaController.getRemainingExecutionTimeLocked(jobBg)); + assertEquals(remainingTimeMs / 2, + mQuotaController.getRemainingExecutionTimeLocked(jobTop)); + } + // Go to a background state. + setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); + advanceElapsedClock(remainingTimeMs / 2 + 1); + // Both Bg and Top jobs should be changed from in-quota to out-of-quota + inOrder.verify(mJobSchedulerService, + timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 2)); + // Top job should NOT be allowed to run. + assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertFalse(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + + // New jobs to run. + JobStatus jobBg2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 3); + JobStatus jobTop2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 4); + JobStatus jobFg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 5); + setStandbyBucket(WORKING_INDEX, jobBg2, jobTop2, jobFg); + + advanceElapsedClock(20 * SECOND_IN_MILLIS); + setProcessState(ActivityManager.PROCESS_STATE_TOP); + // Both Bg and Top jobs should be changed from out-of-quota to in-quota. + inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 2)); + trackJobs(jobFg, jobTop); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobTop); + } + assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + + // App still in foreground so everything should be in quota. + advanceElapsedClock(20 * SECOND_IN_MILLIS); + setProcessState(getProcessStateQuotaFreeThreshold()); + assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + + advanceElapsedClock(20 * SECOND_IN_MILLIS); + // App is in background so everything should be out of quota. + setProcessState(ActivityManager.PROCESS_STATE_SERVICE); + // Bg, Fg and Top jobs should be changed from in-quota to out-of-quota. + inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 3)); + // App is now in background and out of quota. Fg should now change to out of quota + // since it wasn't started. Top should now changed to out of quota even it started + // when the app was in TOP. + assertFalse(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + trackJobs(jobBg2); + assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + } + + /** * Tests that a job is properly updated and JobSchedulerService is notified when a job reaches * its quota. */ @@ -6280,6 +6569,7 @@ public class QuotaControllerTest { mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); advanceElapsedClock(SECOND_IN_MILLIS); + mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); // Bg job 1 starts, then top job starts. Bg job 1 job ends. Then app goes to // foreground_service and a new job starts. Shortly after, uid goes @@ -6333,6 +6623,63 @@ public class QuotaControllerTest { expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2)); assertEquals(expected, mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + + advanceElapsedClock(SECOND_IN_MILLIS); + mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS); + + // Bg job 1 starts, then top job starts. Bg job 1 job ends. Then app goes to + // foreground_service and a new job starts. Shortly after, uid goes + // "inactive" and then bg job 2 starts. Then top job ends, followed by bg and fg jobs. + // This should result in two TimingSessions: + // * The first should have a count of 1 + // * The second should have a count of 3, which accounts for the bg2, fg and top jobs. + // Top started jobs are not quota free any more if the process leaves TOP/BTOP state. + start = JobSchedulerService.sElapsedRealtimeClock.millis(); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(jobBg1, null); + mQuotaController.maybeStartTrackingJobLocked(jobBg2, null); + mQuotaController.maybeStartTrackingJobLocked(jobFg1, null); + mQuotaController.maybeStartTrackingJobLocked(jobTop, null); + } + setProcessState(ActivityManager.PROCESS_STATE_LAST_ACTIVITY); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobBg1); + } + advanceElapsedClock(10 * SECOND_IN_MILLIS); + expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobTop); + } + advanceElapsedClock(10 * SECOND_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); + } + advanceElapsedClock(5 * SECOND_IN_MILLIS); + setProcessState(getProcessStateQuotaFreeThreshold()); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobFg1); + } + advanceElapsedClock(5 * SECOND_IN_MILLIS); + setProcessState(ActivityManager.PROCESS_STATE_TOP); + advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now + start = JobSchedulerService.sElapsedRealtimeClock.millis(); + setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobBg2); + } + advanceElapsedClock(10 * SECOND_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(jobTop, null); + } + advanceElapsedClock(10 * SECOND_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); + mQuotaController.maybeStopTrackingJobLocked(jobFg1, null); + } + expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 3)); + assertEquals(expected, + mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } /** @@ -6701,7 +7048,8 @@ public class QuotaControllerTest { * Tests that expedited jobs aren't stopped when an app runs out of quota. */ @Test - public void testEJTracking_OutOfQuota_ForegroundAndBackground() { + @DisableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS) + public void testEJTracking_OutOfQuota_ForegroundAndBackground_DisableTopStartedJobsThrottling() { setDischarging(); setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TOP_APP_MS, 0); @@ -6813,6 +7161,129 @@ public class QuotaControllerTest { } /** + * Tests that expedited jobs are stopped when an app runs out of quota. + */ + @Test + @EnableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS) + public void testEJTracking_OutOfQuota_ForegroundAndBackground_EnableTopStartedJobsThrottling() { + setDischarging(); + setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TOP_APP_MS, 0); + + JobStatus jobBg = + createExpeditedJobStatus("testEJTracking_OutOfQuota_ForegroundAndBackground", 1); + JobStatus jobTop = + createExpeditedJobStatus("testEJTracking_OutOfQuota_ForegroundAndBackground", 2); + JobStatus jobUnstarted = + createExpeditedJobStatus("testEJTracking_OutOfQuota_ForegroundAndBackground", 3); + trackJobs(jobBg, jobTop, jobUnstarted); + setStandbyBucket(WORKING_INDEX, jobTop, jobBg, jobUnstarted); + // Now the package only has 20 seconds to run. + final long remainingTimeMs = 20 * SECOND_IN_MILLIS; + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS, + mQcConstants.EJ_LIMIT_WORKING_MS - remainingTimeMs, 1), true); + + InOrder inOrder = inOrder(mJobSchedulerService); + + // UID starts out inactive. + setProcessState(ActivityManager.PROCESS_STATE_SERVICE); + // Start the job. + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobBg); + } + advanceElapsedClock(remainingTimeMs / 2); + // New job starts after UID is in the foreground. Since the app is now in the foreground, it + // should continue to have remainingTimeMs / 2 time remaining. + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobTop); + } + advanceElapsedClock(remainingTimeMs); + + // Wait for some extra time to allow for job processing. + inOrder.verify(mJobSchedulerService, + timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) + .onControllerStateChanged(argThat(jobs -> jobs.size() > 0)); + synchronized (mQuotaController.mLock) { + assertEquals(remainingTimeMs / 2, + mQuotaController.getRemainingEJExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + // Go to a background state. + setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); + advanceElapsedClock(remainingTimeMs / 2 + 1); + // Bg, Top and jobUnstarted should be changed from in-quota to out-of-quota. + inOrder.verify(mJobSchedulerService, + timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 3)); + // Top should still NOT be "in quota" even it started before the app + // ran on top out of quota. + assertFalse(jobBg.isExpeditedQuotaApproved()); + assertFalse(jobTop.isExpeditedQuotaApproved()); + assertFalse(jobUnstarted.isExpeditedQuotaApproved()); + synchronized (mQuotaController.mLock) { + assertTrue( + 0 >= mQuotaController + .getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + // New jobs to run. + JobStatus jobBg2 = + createExpeditedJobStatus("testEJTracking_OutOfQuota_ForegroundAndBackground", 4); + JobStatus jobTop2 = + createExpeditedJobStatus("testEJTracking_OutOfQuota_ForegroundAndBackground", 5); + JobStatus jobFg = + createExpeditedJobStatus("testEJTracking_OutOfQuota_ForegroundAndBackground", 6); + setStandbyBucket(WORKING_INDEX, jobBg2, jobTop2, jobFg); + + advanceElapsedClock(20 * SECOND_IN_MILLIS); + setProcessState(ActivityManager.PROCESS_STATE_TOP); + // Confirm QC recognizes that jobUnstarted has changed from out-of-quota to in-quota. + // jobBg, jobFg and jobUnstarted are changed from out-of-quota to in-quota. + inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 3)); + trackJobs(jobTop2, jobFg); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobTop2); + } + assertTrue(jobTop.isExpeditedQuotaApproved()); + assertTrue(jobTop2.isExpeditedQuotaApproved()); + assertTrue(jobFg.isExpeditedQuotaApproved()); + assertTrue(jobBg.isExpeditedQuotaApproved()); + assertTrue(jobUnstarted.isExpeditedQuotaApproved()); + + // App still in foreground so everything should be in quota. + advanceElapsedClock(20 * SECOND_IN_MILLIS); + setProcessState(getProcessStateQuotaFreeThreshold()); + assertTrue(jobTop.isExpeditedQuotaApproved()); + assertTrue(jobTop2.isExpeditedQuotaApproved()); + assertTrue(jobFg.isExpeditedQuotaApproved()); + assertTrue(jobBg.isExpeditedQuotaApproved()); + assertTrue(jobUnstarted.isExpeditedQuotaApproved()); + + advanceElapsedClock(20 * SECOND_IN_MILLIS); + setProcessState(ActivityManager.PROCESS_STATE_SERVICE); + // Bg, Fg, Top, Top2 and jobUnstarted should be changed from in-quota to out-of-quota + inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 5)); + // App is now in background and out of quota. Fg should now change to out of quota since it + // wasn't started. Top should change to out of quota as the app leaves TOP state. + assertFalse(jobTop.isExpeditedQuotaApproved()); + assertFalse(jobTop2.isExpeditedQuotaApproved()); + assertFalse(jobFg.isExpeditedQuotaApproved()); + assertFalse(jobBg.isExpeditedQuotaApproved()); + trackJobs(jobBg2); + assertFalse(jobBg2.isExpeditedQuotaApproved()); + assertFalse(jobUnstarted.isExpeditedQuotaApproved()); + synchronized (mQuotaController.mLock) { + assertTrue( + 0 >= mQuotaController + .getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE)); + } + } + + /** * Tests that Timers properly track overlapping top and background jobs. */ @Test |