Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e3e1640c authored by Kweku Adams's avatar Kweku Adams
Browse files

Reducing quota expired check churn.

QuotaController would check if an app had used up all of its quota every
time the 'time remaining' had passed. However, if there was a session at
the very beginning of the window, the app would still have quota since
it would phase out as time went on. QC would then reschedule another
check for the same amount of time in the future and this could continue
for a while. We avoid this repeated and unnecessary check by
including any session phase outs as part of the calculation for when to
check if the quota has expired.

Bug: 126948148
Test: atest com.android.server.job.controllers.QuotaControllerTest
Test: atest CtsJobSchedulerTestCases
Change-Id: I0bfa0163b243558c2834ba6e460db63124425d9a
parent c6479fd8
Loading
Loading
Loading
Loading
+88 −1
Original line number Diff line number Diff line
@@ -769,6 +769,91 @@ public final class QuotaController extends StateController {
                mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
    }

    /**
     * Returns the amount of time, in milliseconds, until the package would have reached its
     * duration quota, assuming it has a job counting towards its quota the entire time. This takes
     * into account any {@link TimingSession}s that may roll out of the window as the job is
     * running.
     */
    @VisibleForTesting
    long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) {
        final long nowElapsed = sElapsedRealtimeClock.millis();
        final int standbyBucket = JobSchedulerService.standbyBucketForPackage(
                packageName, userId, nowElapsed);
        if (standbyBucket == NEVER_INDEX) {
            return 0;
        }
        List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
        if (sessions == null || sessions.size() == 0) {
            return mAllowedTimePerPeriodMs;
        }

        final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
        final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
        final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
        final long allowedTimeRemainingMs = mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs;
        final long maxExecutionTimeRemainingMs =
                mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs;

        // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
        // essentially run until they reach the maximum limit.
        if (stats.windowSizeMs == mAllowedTimePerPeriodMs) {
            return calculateTimeUntilQuotaConsumedLocked(
                    sessions, startMaxElapsed, maxExecutionTimeRemainingMs);
        }

        // Need to check both max time and period time in case one is less than the other.
        // For example, max time remaining could be less than bucket time remaining, but sessions
        // contributing to the max time remaining could phase out enough that we'd want to use the
        // bucket value.
        return Math.min(
                calculateTimeUntilQuotaConsumedLocked(
                        sessions, startMaxElapsed, maxExecutionTimeRemainingMs),
                calculateTimeUntilQuotaConsumedLocked(
                        sessions, startWindowElapsed, allowedTimeRemainingMs));
    }

    /**
     * Calculates how much time it will take, in milliseconds, until the quota is fully consumed.
     *
     * @param windowStartElapsed The start of the window, in the elapsed realtime timebase.
     * @param deadSpaceMs        How much time can be allowed to count towards the quota
     */
    private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimingSession> sessions,
            final long windowStartElapsed, long deadSpaceMs) {
        long timeUntilQuotaConsumedMs = 0;
        long start = windowStartElapsed;
        for (int i = 0; i < sessions.size(); ++i) {
            TimingSession session = sessions.get(i);

            if (session.endTimeElapsed < windowStartElapsed) {
                // Outside of window. Ignore.
                continue;
            } else if (session.startTimeElapsed <= windowStartElapsed) {
                // Overlapping session. Can extend time by portion of session in window.
                timeUntilQuotaConsumedMs += session.endTimeElapsed - windowStartElapsed;
                start = session.endTimeElapsed;
            } else {
                // Completely within the window. Can only consider if there's enough dead space
                // to get to the start of the session.
                long diff = session.startTimeElapsed - start;
                if (diff > deadSpaceMs) {
                    break;
                }
                timeUntilQuotaConsumedMs += diff
                        + (session.endTimeElapsed - session.startTimeElapsed);
                deadSpaceMs -= diff;
                start = session.endTimeElapsed;
            }
        }
        // Will be non-zero if the loop didn't look at any sessions.
        timeUntilQuotaConsumedMs += deadSpaceMs;
        if (timeUntilQuotaConsumedMs > mMaxExecutionTimeMs) {
            Slog.wtf(TAG, "Calculated quota consumed time too high: " + timeUntilQuotaConsumedMs);
        }
        return timeUntilQuotaConsumedMs;
    }

    /** Returns the execution stats of the app in the most recent window. */
    @VisibleForTesting
    @NonNull
@@ -1483,7 +1568,7 @@ public final class QuotaController extends StateController {
                    return;
                }
                Message msg = mHandler.obtainMessage(MSG_REACHED_QUOTA, mPkg);
                final long timeRemainingMs = getRemainingExecutionTimeLocked(mPkg.userId,
                final long timeRemainingMs = getTimeUntilQuotaConsumedLocked(mPkg.userId,
                        mPkg.packageName);
                if (DEBUG) {
                    Slog.i(TAG, "Job for " + mPkg + " has " + timeRemainingMs + "ms left.");
@@ -1642,6 +1727,8 @@ public final class QuotaController extends StateController {
                            // job is currently running.
                            // Reschedule message
                            Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg);
                            timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId,
                                    pkg.packageName);
                            if (DEBUG) {
                                Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left.");
                            }
+158 −2
Original line number Diff line number Diff line
@@ -722,6 +722,147 @@ public class QuotaControllerTest {
        assertEquals(expectedStats, newStatsRare);
    }

    /**
     * Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket
     * window.
     */
    @Test
    public void testGetTimeUntilQuotaConsumedLocked_BucketWindow() {
        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
        // Close to RARE boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (24 * HOUR_IN_MILLIS - 30 * SECOND_IN_MILLIS),
                        30 * SECOND_IN_MILLIS, 5));
        // Far away from FREQUENT boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
        // Overlap WORKING_SET boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
                        3 * MINUTE_IN_MILLIS, 5));
        // Close to ACTIVE boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));

        setStandbyBucket(RARE_INDEX);
        assertEquals(30 * SECOND_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        assertEquals(MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));

        setStandbyBucket(FREQUENT_INDEX);
        assertEquals(MINUTE_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        assertEquals(MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));

        setStandbyBucket(WORKING_INDEX);
        assertEquals(5 * MINUTE_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        assertEquals(7 * MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));

        // ACTIVE window = allowed time, so jobs can essentially run non-stop until they reach the
        // max execution time.
        setStandbyBucket(ACTIVE_INDEX);
        assertEquals(7 * MINUTE_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        assertEquals(mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS - 9 * MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    /**
     * Test getTimeUntilQuotaConsumedLocked when the app is close to the max execution limit.
     */
    @Test
    public void testGetTimeUntilQuotaConsumedLocked_MaxExecution() {
        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
        // Overlap boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(
                        now - (24 * HOUR_IN_MILLIS + 8 * MINUTE_IN_MILLIS), 4 * HOUR_IN_MILLIS, 5));

        setStandbyBucket(WORKING_INDEX);
        assertEquals(8 * MINUTE_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        // Max time will phase out, so should use bucket limit.
        assertEquals(10 * MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));

        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
        // Close to boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (24 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS),
                        4 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS, 5));

        setStandbyBucket(WORKING_INDEX);
        assertEquals(5 * MINUTE_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        assertEquals(10 * MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));

        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
        // Far from boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(
                        now - (20 * HOUR_IN_MILLIS), 4 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS, 5));

        setStandbyBucket(WORKING_INDEX);
        assertEquals(3 * MINUTE_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        assertEquals(3 * MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    /**
     * Test getTimeUntilQuotaConsumedLocked when the max execution time and bucket window time
     * remaining are equal.
     */
    @Test
    public void testGetTimeUntilQuotaConsumedLocked_EqualTimeRemaining() {
        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
        setStandbyBucket(FREQUENT_INDEX);

        // Overlap boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(
                        now - (24 * HOUR_IN_MILLIS + 11 * MINUTE_IN_MILLIS),
                        4 * HOUR_IN_MILLIS,
                        5));
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(
                        now - (8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));

        // Both max and bucket time have 8 minutes left.
        assertEquals(8 * MINUTE_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        // Max time essentially free. Bucket time has 2 min phase out plus original 8 minute
        // window time.
        assertEquals(10 * MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));

        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
        // Overlap boundary.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(
                        now - (24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 2 * MINUTE_IN_MILLIS, 5));
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(
                        now - (20 * HOUR_IN_MILLIS),
                        3 * HOUR_IN_MILLIS + 48 * MINUTE_IN_MILLIS,
                        5));
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(
                        now - (8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));

        // Both max and bucket time have 8 minutes left.
        assertEquals(8 * MINUTE_IN_MILLIS,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        // Max time only has one minute phase out. Bucket time has 2 minute phase out.
        assertEquals(9 * MINUTE_IN_MILLIS,
                mQuotaController.getTimeUntilQuotaConsumedLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    @Test
    public void testIsWithinQuotaLocked_NeverApp() {
        assertFalse(mQuotaController.isWithinQuotaLocked(0, "com.android.test.never", NEVER_INDEX));
@@ -1902,7 +2043,10 @@ public class QuotaControllerTest {
        // window, so as the package "reaches its quota" it will have more to keep running.
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - 2 * HOUR_IN_MILLIS,
                        10 * MINUTE_IN_MILLIS - remainingTimeMs, 1));
                        10 * SECOND_IN_MILLIS - remainingTimeMs, 1));
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - HOUR_IN_MILLIS,
                        9 * MINUTE_IN_MILLIS + 50 * SECOND_IN_MILLIS, 1));

        assertEquals(remainingTimeMs, mQuotaController.getRemainingExecutionTimeLocked(jobStatus));
        // Start the job.
@@ -1919,6 +2063,18 @@ public class QuotaControllerTest {
        // amount of remaining time left its quota.
        assertEquals(remainingTimeMs,
                mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
        verify(handler, atLeast(1)).sendMessageDelayed(any(), eq(remainingTimeMs));
        // Handler is told to check when the quota will be consumed, not when the initial
        // remaining time is over.
        verify(handler, atLeast(1)).sendMessageDelayed(any(), eq(10 * SECOND_IN_MILLIS));
        verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs));

        // After 10 seconds, the job should finally be out of quota.
        advanceElapsedClock(10 * SECOND_IN_MILLIS - remainingTimeMs);
        // Wait for some extra time to allow for job processing.
        verify(mJobSchedulerService,
                timeout(12 * SECOND_IN_MILLIS).times(1))
                .onControllerStateChanged();
        assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        verify(handler, never()).sendMessageDelayed(any(), anyInt());
    }
}