diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java index 227b8276abe11..56b97f2e57572 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java @@ -50,6 +50,9 @@ public final class TimeController extends StateController { private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); + @VisibleForTesting + static final long DELAY_COALESCE_TIME_MS = 30_000L; + /** Deadline alarm tag for logging purposes */ private final String DEADLINE_TAG = "*job.deadline*"; /** Delay alarm tag for logging purposes */ @@ -57,6 +60,7 @@ public final class TimeController extends StateController { private long mNextJobExpiredElapsedMillis; private long mNextDelayExpiredElapsedMillis; + private volatile long mLastFiredDelayExpiredElapsedMillis; private final boolean mChainedAttributionEnabled; @@ -273,7 +277,6 @@ public final class TimeController extends StateController { @VisibleForTesting void checkExpiredDelaysAndResetAlarm() { synchronized (mLock) { - final long nowElapsedMillis = sElapsedRealtimeClock.millis(); long nextDelayTime = Long.MAX_VALUE; int nextDelayUid = 0; String nextDelayPackageName = null; @@ -284,7 +287,7 @@ public final class TimeController extends StateController { if (!job.hasTimingDelayConstraint()) { continue; } - if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { + if (evaluateTimingDelayConstraint(job, sElapsedRealtimeClock.millis())) { if (canStopTrackingJobLocked(job)) { it.remove(); } @@ -356,7 +359,11 @@ public final class TimeController extends StateController { * This alarm will not wake up the phone. */ private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) { - alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); + // To avoid spamming AlarmManager in the case where many delay times are a few milliseconds + // apart, make sure the alarm is set no earlier than DELAY_COALESCE_TIME_MS since the last + // time a delay alarm went off and that the alarm is not scheduled for the past. + alarmTimeElapsedMillis = maybeAdjustAlarmTime(Math.max(alarmTimeElapsedMillis, + mLastFiredDelayExpiredElapsedMillis + DELAY_COALESCE_TIME_MS)); if (mNextDelayExpiredElapsedMillis == alarmTimeElapsedMillis) { return; } @@ -416,6 +423,7 @@ public final class TimeController extends StateController { if (DEBUG) { Slog.d(TAG, "Delay-expired alarm fired"); } + mLastFiredDelayExpiredElapsedMillis = sElapsedRealtimeClock.millis(); checkExpiredDelaysAndResetAlarm(); } }; @@ -429,6 +437,9 @@ public final class TimeController extends StateController { pw.print("Next delay alarm in "); TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw); pw.println(); + pw.print("Last delay alarm fired @ "); + TimeUtils.formatDuration(nowElapsed, mLastFiredDelayExpiredElapsedMillis, pw); + pw.println(); pw.print("Next deadline alarm in "); TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw); pw.println(); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java index 3614763fecabb..9d847153661f8 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java @@ -52,6 +52,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoSession; @@ -631,6 +632,50 @@ public class TimeControllerTest { .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any()); } + @Test + public void testDelayAlarmSchedulingCoalescedIntervals() { + doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt()); + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + + JobStatus jobLatest = createJobStatus("testDelayAlarmSchedulingCoalescedIntervals", + createJob().setMinimumLatency(HOUR_IN_MILLIS)); + JobStatus jobMiddle = createJobStatus("testDelayAlarmSchedulingCoalescedIntervals", + createJob().setMinimumLatency(TimeController.DELAY_COALESCE_TIME_MS / 2)); + JobStatus jobEarliest = createJobStatus("testDelayAlarmSchedulingCoalescedIntervals", + createJob().setMinimumLatency(TimeController.DELAY_COALESCE_TIME_MS / 10)); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(AlarmManager.OnAlarmListener.class); + InOrder inOrder = inOrder(mAlarmManager); + + mTimeController.maybeStartTrackingJobLocked(jobEarliest, null); + mTimeController.maybeStartTrackingJobLocked(jobMiddle, null); + mTimeController.maybeStartTrackingJobLocked(jobLatest, null); + inOrder.verify(mAlarmManager, times(1)) + .set(anyInt(), eq(now + TimeController.DELAY_COALESCE_TIME_MS / 10), anyLong(), + anyLong(), eq(TAG_DELAY), + listenerCaptor.capture(), any(), any()); + final AlarmManager.OnAlarmListener delayListener = listenerCaptor.getValue(); + + advanceElapsedClock(TimeController.DELAY_COALESCE_TIME_MS / 10); + delayListener.onAlarm(); + // The next delay alarm time should be TimeController.DELAY_COALESCE_TIME_MS after the last + // time the delay alarm fired. + inOrder.verify(mAlarmManager, times(1)) + .set(anyInt(), eq(now + TimeController.DELAY_COALESCE_TIME_MS / 10 + + TimeController.DELAY_COALESCE_TIME_MS), anyLong(), + anyLong(), eq(TAG_DELAY), any(), any(), any()); + + advanceElapsedClock(TimeController.DELAY_COALESCE_TIME_MS); + delayListener.onAlarm(); + // The last job is significantly after the coalesce time, so the 3rd scheduling shouldn't be + // affected by the first two jobs' alarms. + inOrder.verify(mAlarmManager, times(1)) + .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), + anyLong(), eq(TAG_DELAY), any(), any(), any()); + } + @Test public void testEvaluateStateLocked_Delay() { final long now = JobSchedulerService.sElapsedRealtimeClock.millis();