summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/aconfig/job.aconfig7
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java12
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java475
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