diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 231caabe0335e..beba73f09f531 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -416,7 +416,8 @@ message StateControllerProto { optional int64 start_time_elapsed = 1; optional int64 end_time_elapsed = 2; - optional int32 job_count = 3; + // The number of background jobs that ran during this session. + optional int32 bg_job_count = 3; } message Timer { @@ -427,8 +428,9 @@ message StateControllerProto { optional bool is_active = 2; // The time this timer last became active. Only valid if is_active is true. optional int64 start_time_elapsed = 3; - // How many are currently running. Valid only if the device is_active is true. - optional int32 job_count = 4; + // How many background jobs are currently running. Valid only if the device is_active + // is true. + optional int32 bg_job_count = 4; // All of the jobs that the Timer is currently tracking. repeated JobStatusShortInfoProto running_jobs = 5; } diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java index f73ffac96dfa2..660c2383ea2f0 100644 --- a/services/core/java/com/android/server/job/controllers/QuotaController.java +++ b/services/core/java/com/android/server/job/controllers/QuotaController.java @@ -151,8 +151,7 @@ public final class QuotaController extends StateController { return "<" + userId + ">" + packageName; } - @VisibleForTesting - static final class Package { + private static final class Package { public final String packageName; public final int userId; @@ -387,8 +386,9 @@ public final class QuotaController extends StateController { private boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) { final int standbyBucket = getEffectiveStandbyBucket(jobStatus); - return isWithinQuotaLocked(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), - standbyBucket); + // Jobs for the active app should always be able to run. + return jobStatus.uidActive || isWithinQuotaLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); } private boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName, @@ -579,7 +579,10 @@ public final class QuotaController extends StateController { boolean changed = false; for (int i = jobs.size() - 1; i >= 0; --i) { final JobStatus js = jobs.valueAt(i); - if (realStandbyBucket == getEffectiveStandbyBucket(js)) { + if (js.uidActive) { + // Jobs for the active app should always be able to run. + changed |= js.setQuotaConstraintSatisfied(true); + } else if (realStandbyBucket == getEffectiveStandbyBucket(js)) { changed |= js.setQuotaConstraintSatisfied(realInQuota); } else { // This job is somehow exempted. Need to determine its own quota status. @@ -765,18 +768,18 @@ public final class QuotaController extends StateController { public final long startTimeElapsed; // End timestamp in elapsed realtime timebase. public final long endTimeElapsed; - // How many jobs ran during this session. - public final int jobCount; + // How many background jobs ran during this session. + public final int bgJobCount; TimingSession(long startElapsed, long endElapsed, int jobCount) { this.startTimeElapsed = startElapsed; this.endTimeElapsed = endElapsed; - this.jobCount = jobCount; + this.bgJobCount = jobCount; } @Override public String toString() { - return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + jobCount + return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount + "}"; } @@ -786,7 +789,7 @@ public final class QuotaController extends StateController { TimingSession other = (TimingSession) obj; return startTimeElapsed == other.startTimeElapsed && endTimeElapsed == other.endTimeElapsed - && jobCount == other.jobCount; + && bgJobCount == other.bgJobCount; } else { return false; } @@ -794,7 +797,7 @@ public final class QuotaController extends StateController { @Override public int hashCode() { - return Arrays.hashCode(new long[] {startTimeElapsed, endTimeElapsed, jobCount}); + return Arrays.hashCode(new long[] {startTimeElapsed, endTimeElapsed, bgJobCount}); } public void dump(IndentingPrintWriter pw) { @@ -804,8 +807,8 @@ public final class QuotaController extends StateController { pw.print(" ("); pw.print(endTimeElapsed - startTimeElapsed); pw.print("), "); - pw.print(jobCount); - pw.print(" jobs."); + pw.print(bgJobCount); + pw.print(" bg jobs."); pw.println(); } @@ -816,7 +819,8 @@ public final class QuotaController extends StateController { startTimeElapsed); proto.write(StateControllerProto.QuotaController.TimingSession.END_TIME_ELAPSED, endTimeElapsed); - proto.write(StateControllerProto.QuotaController.TimingSession.JOB_COUNT, jobCount); + proto.write(StateControllerProto.QuotaController.TimingSession.BG_JOB_COUNT, + bgJobCount); proto.end(token); } @@ -825,23 +829,32 @@ public final class QuotaController extends StateController { private final class Timer { private final Package mPkg; - // List of jobs currently running for this package. - private final ArraySet mRunningJobs = new ArraySet<>(); + // List of jobs currently running for this app that started when the app wasn't in the + // foreground. + private final ArraySet mRunningBgJobs = new ArraySet<>(); private long mStartTimeElapsed; - private int mJobCount; + private int mBgJobCount; Timer(int userId, String packageName) { mPkg = new Package(userId, packageName); } void startTrackingJob(@NonNull JobStatus jobStatus) { + if (jobStatus.uidActive) { + // We intentionally don't pay attention to fg state changes after a job has started. + if (DEBUG) { + Slog.v(TAG, + "Timer ignoring " + jobStatus.toShortString() + " because uidActive"); + } + return; + } if (DEBUG) Slog.v(TAG, "Starting to track " + jobStatus.toShortString()); synchronized (mLock) { // Always track jobs, even when charging. - mRunningJobs.add(jobStatus); + mRunningBgJobs.add(jobStatus); if (!mChargeTracker.isCharging()) { - mJobCount++; - if (mRunningJobs.size() == 1) { + mBgJobCount++; + if (mRunningBgJobs.size() == 1) { // Started tracking the first job. mStartTimeElapsed = sElapsedRealtimeClock.millis(); scheduleCutoff(); @@ -853,7 +866,7 @@ public final class QuotaController extends StateController { void stopTrackingJob(@NonNull JobStatus jobStatus) { if (DEBUG) Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString()); synchronized (mLock) { - if (mRunningJobs.size() == 0) { + if (mRunningBgJobs.size() == 0) { // maybeStopTrackingJobLocked can be called when an app cancels a job, so a // timer may not be running when it's asked to stop tracking a job. if (DEBUG) { @@ -861,8 +874,8 @@ public final class QuotaController extends StateController { } return; } - mRunningJobs.remove(jobStatus); - if (!mChargeTracker.isCharging() && mRunningJobs.size() == 0) { + if (mRunningBgJobs.remove(jobStatus) + && !mChargeTracker.isCharging() && mRunningBgJobs.size() == 0) { emitSessionLocked(sElapsedRealtimeClock.millis()); cancelCutoff(); } @@ -870,13 +883,13 @@ public final class QuotaController extends StateController { } private void emitSessionLocked(long nowElapsed) { - if (mJobCount <= 0) { + if (mBgJobCount <= 0) { // Nothing to emit. return; } - TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mJobCount); + TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount); saveTimingSession(mPkg.userId, mPkg.packageName, ts); - mJobCount = 0; + mBgJobCount = 0; // Don't reset the tracked jobs list as we need to keep tracking the current number // of jobs. // However, cancel the currently scheduled cutoff since it's not currently useful. @@ -889,7 +902,7 @@ public final class QuotaController extends StateController { */ public boolean isActive() { synchronized (mLock) { - return mJobCount > 0; + return mBgJobCount > 0; } } @@ -905,12 +918,12 @@ public final class QuotaController extends StateController { emitSessionLocked(nowElapsed); } else { // Start timing from unplug. - if (mRunningJobs.size() > 0) { + if (mRunningBgJobs.size() > 0) { mStartTimeElapsed = nowElapsed; // NOTE: this does have the unfortunate consequence that if the device is // repeatedly plugged in and unplugged, the job count for a package may be // artificially high. - mJobCount = mRunningJobs.size(); + mBgJobCount = mRunningBgJobs.size(); // Schedule cutoff since we're now actively tracking for quotas again. scheduleCutoff(); } @@ -958,12 +971,12 @@ public final class QuotaController extends StateController { pw.print("NOT active"); } pw.print(", "); - pw.print(mJobCount); - pw.print(" running jobs"); + pw.print(mBgJobCount); + pw.print(" running bg jobs"); pw.println(); pw.increaseIndent(); - for (int i = 0; i < mRunningJobs.size(); i++) { - JobStatus js = mRunningJobs.valueAt(i); + for (int i = 0; i < mRunningBgJobs.size(); i++) { + JobStatus js = mRunningBgJobs.valueAt(i); if (predicate.test(js)) { pw.println(js.toShortString()); } @@ -979,9 +992,9 @@ public final class QuotaController extends StateController { proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive()); proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED, mStartTimeElapsed); - proto.write(StateControllerProto.QuotaController.Timer.JOB_COUNT, mJobCount); - for (int i = 0; i < mRunningJobs.size(); i++) { - JobStatus js = mRunningJobs.valueAt(i); + proto.write(StateControllerProto.QuotaController.Timer.BG_JOB_COUNT, mBgJobCount); + for (int i = 0; i < mRunningBgJobs.size(); i++) { + JobStatus js = mRunningBgJobs.valueAt(i); if (predicate.test(js)) { js.writeToShortProto(proto, StateControllerProto.QuotaController.Timer.RUNNING_JOBS); 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 b2ec83583eba6..95da13fca7acb 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 @@ -775,6 +775,139 @@ public class QuotaControllerTest { assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } + /** Tests that TimingSessions are saved properly when all the jobs are background jobs. */ + @Test + public void testTimerTracking_AllBackground() { + setDischarging(); + + JobStatus jobStatus = createJobStatus("testTimerTracking_AllBackground", 1); + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + + assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + + List expected = new ArrayList<>(); + + // Test single job. + long start = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.prepareForExecutionLocked(jobStatus); + advanceElapsedClock(5 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1)); + assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + + // Test overlapping jobs. + JobStatus jobStatus2 = createJobStatus("testTimerTracking_AllBackground", 2); + mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null); + + JobStatus jobStatus3 = createJobStatus("testTimerTracking_AllBackground", 3); + mQuotaController.maybeStartTrackingJobLocked(jobStatus3, null); + + advanceElapsedClock(SECOND_IN_MILLIS); + + start = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + mQuotaController.prepareForExecutionLocked(jobStatus); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.prepareForExecutionLocked(jobStatus2); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.prepareForExecutionLocked(jobStatus3); + advanceElapsedClock(20 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3)); + assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + /** Tests that Timers don't count foreground jobs. */ + @Test + public void testTimerTracking_AllForeground() { + setDischarging(); + + JobStatus jobStatus = createJobStatus("testTimerTracking_AllForeground", 1); + jobStatus.uidActive = true; + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + + assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + + mQuotaController.prepareForExecutionLocked(jobStatus); + advanceElapsedClock(5 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + /** + * Tests that Timers properly track overlapping foreground and background jobs. + */ + @Test + public void testTimerTracking_ForegroundAndBackground() { + setDischarging(); + + JobStatus jobBg1 = createJobStatus("testTimerTracking_ForegroundAndBackground", 1); + JobStatus jobBg2 = createJobStatus("testTimerTracking_ForegroundAndBackground", 2); + JobStatus jobFg3 = createJobStatus("testTimerTracking_ForegroundAndBackground", 3); + jobFg3.uidActive = true; + mQuotaController.maybeStartTrackingJobLocked(jobBg1, null); + mQuotaController.maybeStartTrackingJobLocked(jobBg2, null); + mQuotaController.maybeStartTrackingJobLocked(jobFg3, null); + assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + List expected = new ArrayList<>(); + + // UID starts out inactive. + long start = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.prepareForExecutionLocked(jobBg1); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); + assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + + advanceElapsedClock(SECOND_IN_MILLIS); + + // Bg job starts while inactive, spans an entire active session, and ends after the + // active session. + // Fg job starts after the bg job and ends before the bg job. + // Entire bg job duration should be counted since it started before active session. However, + // count should only be 1 since Timer shouldn't count fg jobs. + start = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.maybeStartTrackingJobLocked(jobBg2, null); + mQuotaController.prepareForExecutionLocked(jobBg2); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.prepareForExecutionLocked(jobFg3); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + expected.add(createTimingSession(start, 30 * SECOND_IN_MILLIS, 1)); + assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + + advanceElapsedClock(SECOND_IN_MILLIS); + + // Bg job 1 starts, then fg job starts. Bg job 1 job ends. Shortly after, uid goes + // "inactive" and then bg job 2 starts. Then fg job ends. + // This should result in two TimingSessions with a count of one each. + start = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.maybeStartTrackingJobLocked(jobBg1, null); + mQuotaController.maybeStartTrackingJobLocked(jobBg2, null); + mQuotaController.maybeStartTrackingJobLocked(jobFg3, null); + mQuotaController.prepareForExecutionLocked(jobBg1); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.prepareForExecutionLocked(jobFg3); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1)); + advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now + start = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.prepareForExecutionLocked(jobBg2); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false); + advanceElapsedClock(10 * SECOND_IN_MILLIS); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1)); + assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + } + /** * Tests that a job is properly updated and JobSchedulerService is notified when a job reaches * its quota.