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

Commit 41cb3328 authored by Kweku Adams's avatar Kweku Adams
Browse files

Force batching non-ACTIVE jobs.

Non-ACTIVE jobs means jobs whose apps are in the WORKING_SET, FREQUENT,
or RARE standby buckets.

Trying to batch the run of non-ACTIVE jobs as much as possible. The policy is:
1. Run non-ACTIVE jobs if there are a minimum number of non-ACTIVE jobs
   ready to be run at the same time. The default minimum is 5.
2. Run non-ACTIVE jobs if any non-batched jobs are going to run.
   Non-batched jobs are jobs in the ACTIVE bucket or jobs that satisfy
   condition #3.
3. Run a non-ACTIVE job if it's been significantly delayed. By default,
   this happens if it's been delayed because we couldn't find a large
   enough batch for over 31 minutes. It is still required to be eligible
   to run per the active standby throttling mechanism. If it is eligible
   and delayed significantly, it will not be batched.

The minimum value and max delay time are both configurable.

Bug: 136107840
Test: atest com.android.server.job.JobSchedulerServiceTest
Test: atest CtsJobSchedulerTestCases
Change-Id: I7f751ac3cb8e57328a1baaa044120bc5a157ad3e
parent ee577653
Loading
Loading
Loading
Loading
+16 −1
Original line number Diff line number Diff line
@@ -175,6 +175,11 @@ message ConstantsProto {
    // is now set to 1, to prevent any batching at this level. Since we now do
    // batching through doze, that is a much better mechanism.
    optional int32 min_ready_jobs_count = 7;
    // Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early.
    optional int32 min_ready_non_active_jobs_count = 29;
    // Don't batch a non-ACTIVE job if it's been delayed due to force batching attempts for
    // at least this amount of time.
    optional int64 max_non_active_job_batch_delay_ms = 30;
    // This is the job execution factor that is considered to be heavy use of
    // the system.
    optional double heavy_use_factor = 8;
@@ -307,6 +312,8 @@ message ConstantsProto {

    // In this time after screen turns on, we increase job concurrency.
    optional int32 screen_off_job_concurrency_increase_delay_ms = 28;

    // Next tag: 31
}

// Next tag: 4
@@ -938,7 +945,15 @@ message JobStatusDumpProto {

    optional int64 internal_flags = 24;

    // Next tag: 28
    // Amount of time since this job was first deferred due to standby bucketing policy. Will be
    // 0 if this job was never deferred.
    optional int64 time_since_first_deferral_ms = 28;

    // Amount of time since JobScheduler first tried to force batch this job. Will be 0 if there
    // was no attempt.
    optional int64 time_since_first_force_batch_attempt_ms = 29;

    // Next tag: 30
}

// Dump from com.android.server.job.JobConcurrencyManager.
+87 −41
Original line number Diff line number Diff line
@@ -443,6 +443,10 @@ public class JobSchedulerService extends com.android.server.SystemService
        private static final String KEY_MIN_CONNECTIVITY_COUNT = "min_connectivity_count";
        private static final String KEY_MIN_CONTENT_COUNT = "min_content_count";
        private static final String KEY_MIN_READY_JOBS_COUNT = "min_ready_jobs_count";
        private static final String KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT =
                "min_ready_non_active_jobs_count";
        private static final String KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS =
                "max_non_active_job_batch_delay_ms";
        private static final String KEY_HEAVY_USE_FACTOR = "heavy_use_factor";
        private static final String KEY_MODERATE_USE_FACTOR = "moderate_use_factor";

@@ -473,6 +477,8 @@ public class JobSchedulerService extends com.android.server.SystemService
        private static final int DEFAULT_MIN_CONNECTIVITY_COUNT = 1;
        private static final int DEFAULT_MIN_CONTENT_COUNT = 1;
        private static final int DEFAULT_MIN_READY_JOBS_COUNT = 1;
        private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
        private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS;
        private static final float DEFAULT_HEAVY_USE_FACTOR = .9f;
        private static final float DEFAULT_MODERATE_USE_FACTOR = .5f;
        private static final int DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT = Integer.MAX_VALUE;
@@ -524,6 +530,18 @@ public class JobSchedulerService extends com.android.server.SystemService
         * a much better mechanism.
         */
        int MIN_READY_JOBS_COUNT = DEFAULT_MIN_READY_JOBS_COUNT;

        /**
         * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early.
         */
        int MIN_READY_NON_ACTIVE_JOBS_COUNT = DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT;

        /**
         * Don't batch a non-ACTIVE job if it's been delayed due to force batching attempts for
         * at least this amount of time.
         */
        long MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;

        /**
         * This is the job execution factor that is considered to be heavy use of the system.
         */
@@ -657,6 +675,12 @@ public class JobSchedulerService extends com.android.server.SystemService
                    DEFAULT_MIN_CONTENT_COUNT);
            MIN_READY_JOBS_COUNT = mParser.getInt(KEY_MIN_READY_JOBS_COUNT,
                    DEFAULT_MIN_READY_JOBS_COUNT);
            MIN_READY_NON_ACTIVE_JOBS_COUNT = mParser.getInt(
                    KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
                    DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT);
            MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = mParser.getLong(
                    KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
                    DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS);
            HEAVY_USE_FACTOR = mParser.getFloat(KEY_HEAVY_USE_FACTOR,
                    DEFAULT_HEAVY_USE_FACTOR);
            MODERATE_USE_FACTOR = mParser.getFloat(KEY_MODERATE_USE_FACTOR,
@@ -707,6 +731,10 @@ public class JobSchedulerService extends com.android.server.SystemService
            pw.printPair(KEY_MIN_CONNECTIVITY_COUNT, MIN_CONNECTIVITY_COUNT).println();
            pw.printPair(KEY_MIN_CONTENT_COUNT, MIN_CONTENT_COUNT).println();
            pw.printPair(KEY_MIN_READY_JOBS_COUNT, MIN_READY_JOBS_COUNT).println();
            pw.printPair(KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
                    MIN_READY_NON_ACTIVE_JOBS_COUNT).println();
            pw.printPair(KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
                    MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS).println();
            pw.printPair(KEY_HEAVY_USE_FACTOR, HEAVY_USE_FACTOR).println();
            pw.printPair(KEY_MODERATE_USE_FACTOR, MODERATE_USE_FACTOR).println();

@@ -749,6 +777,10 @@ public class JobSchedulerService extends com.android.server.SystemService
            proto.write(ConstantsProto.MIN_CONNECTIVITY_COUNT, MIN_CONNECTIVITY_COUNT);
            proto.write(ConstantsProto.MIN_CONTENT_COUNT, MIN_CONTENT_COUNT);
            proto.write(ConstantsProto.MIN_READY_JOBS_COUNT, MIN_READY_JOBS_COUNT);
            proto.write(ConstantsProto.MIN_READY_NON_ACTIVE_JOBS_COUNT,
                    MIN_READY_NON_ACTIVE_JOBS_COUNT);
            proto.write(ConstantsProto.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
                    MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS);
            proto.write(ConstantsProto.HEAVY_USE_FACTOR, HEAVY_USE_FACTOR);
            proto.write(ConstantsProto.MODERATE_USE_FACTOR, MODERATE_USE_FACTOR);

@@ -1985,7 +2017,7 @@ public class JobSchedulerService extends com.android.server.SystemService
    }

    final class ReadyJobQueueFunctor implements Consumer<JobStatus> {
        ArrayList<JobStatus> newReadyJobs;
        final ArrayList<JobStatus> newReadyJobs = new ArrayList<>();

        @Override
        public void accept(JobStatus job) {
@@ -1993,9 +2025,6 @@ public class JobSchedulerService extends com.android.server.SystemService
                if (DEBUG) {
                    Slog.d(TAG, "    queued " + job.toShortString());
                }
                if (newReadyJobs == null) {
                    newReadyJobs = new ArrayList<JobStatus>();
                }
                newReadyJobs.add(job);
            } else {
                evaluateControllerStatesLocked(job);
@@ -2003,14 +2032,13 @@ public class JobSchedulerService extends com.android.server.SystemService
        }

        public void postProcess() {
            if (newReadyJobs != null) {
            noteJobsPending(newReadyJobs);
            mPendingJobs.addAll(newReadyJobs);
            if (mPendingJobs.size() > 1) {
                mPendingJobs.sort(mEnqueueTimeComparator);
            }
            }
            newReadyJobs = null;

            newReadyJobs.clear();
        }
    }
    private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor();
@@ -2027,7 +2055,9 @@ public class JobSchedulerService extends com.android.server.SystemService
        int backoffCount;
        int connectivityCount;
        int contentCount;
        List<JobStatus> runnableJobs;
        int forceBatchedCount;
        int unbatchedCount;
        final List<JobStatus> runnableJobs = new ArrayList<>();

        public MaybeReadyJobQueueFunctor() {
            reset();
@@ -2047,6 +2077,18 @@ public class JobSchedulerService extends com.android.server.SystemService
                    }
                } catch (RemoteException e) {
                }
                if (mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1
                        && job.getStandbyBucket() != ACTIVE_INDEX
                        && (job.getFirstForceBatchedTimeElapsed() == 0
                        || sElapsedRealtimeClock.millis() - job.getFirstForceBatchedTimeElapsed()
                                < mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS)) {
                    // Force batching non-ACTIVE jobs. Don't include them in the other counts.
                    forceBatchedCount++;
                    if (job.getFirstForceBatchedTimeElapsed() == 0) {
                        job.setFirstForceBatchedTimeElapsed(sElapsedRealtimeClock.millis());
                    }
                } else {
                    unbatchedCount++;
                    if (job.getNumFailures() > 0) {
                        backoffCount++;
                    }
@@ -2068,8 +2110,6 @@ public class JobSchedulerService extends com.android.server.SystemService
                    if (job.hasContentTriggerConstraint()) {
                        contentCount++;
                    }
                if (runnableJobs == null) {
                    runnableJobs = new ArrayList<>();
                }
                runnableJobs.add(job);
            } else {
@@ -2085,8 +2125,9 @@ public class JobSchedulerService extends com.android.server.SystemService
                    batteryNotLowCount >= mConstants.MIN_BATTERY_NOT_LOW_COUNT ||
                    storageNotLowCount >= mConstants.MIN_STORAGE_NOT_LOW_COUNT ||
                    contentCount >= mConstants.MIN_CONTENT_COUNT ||
                    (runnableJobs != null
                            && runnableJobs.size() >= mConstants.MIN_READY_JOBS_COUNT)) {
                    forceBatchedCount >= mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT ||
                    (unbatchedCount > 0 && (unbatchedCount + forceBatchedCount)
                            >= mConstants.MIN_READY_JOBS_COUNT)) {
                if (DEBUG) {
                    Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running jobs.");
                }
@@ -2105,7 +2146,8 @@ public class JobSchedulerService extends com.android.server.SystemService
            reset();
        }

        private void reset() {
        @VisibleForTesting
        void reset() {
            chargingCount = 0;
            idleCount =  0;
            backoffCount = 0;
@@ -2113,7 +2155,9 @@ public class JobSchedulerService extends com.android.server.SystemService
            batteryNotLowCount = 0;
            storageNotLowCount = 0;
            contentCount = 0;
            runnableJobs = null;
            forceBatchedCount = 0;
            unbatchedCount = 0;
            runnableJobs.clear();
        }
    }
    private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor();
@@ -2222,7 +2266,8 @@ public class JobSchedulerService extends com.android.server.SystemService
     *      - The job's standby bucket has come due to be runnable.
     *      - The component is enabled and runnable.
     */
    private boolean isReadyToBeExecutedLocked(JobStatus job) {
    @VisibleForTesting
    boolean isReadyToBeExecutedLocked(JobStatus job) {
        final boolean jobReady = job.isReady();

        if (DEBUG) {
@@ -2354,7 +2399,8 @@ public class JobSchedulerService extends com.android.server.SystemService
        return !appIsBad;
    }

    private void evaluateControllerStatesLocked(final JobStatus job) {
    @VisibleForTesting
    void evaluateControllerStatesLocked(final JobStatus job) {
        for (int c = mControllers.size() - 1; c >= 0; --c) {
            final StateController sc = mControllers.get(c);
            sc.evaluateStateLocked(job);
+26 −0
Original line number Diff line number Diff line
@@ -188,6 +188,9 @@ public final class JobStatus {
     */
    private long whenStandbyDeferred;

    /** The first time this job was force batched. */
    private long mFirstForceBatchedTimeElapsed;

    // Constraints.
    final int requiredConstraints;
    private final int mRequiredConstraintsOfInterest;
@@ -737,6 +740,18 @@ public final class JobStatus {
        whenStandbyDeferred = now;
    }

    /**
     * Returns the first time this job was force batched, in the elapsed realtime timebase. Will be
     * 0 if this job was never force batched.
     */
    public long getFirstForceBatchedTimeElapsed() {
        return mFirstForceBatchedTimeElapsed;
    }

    public void setFirstForceBatchedTimeElapsed(long now) {
        mFirstForceBatchedTimeElapsed = now;
    }

    public String getSourceTag() {
        return sourceTag;
    }
@@ -1638,6 +1653,12 @@ public final class JobStatus {
            TimeUtils.formatDuration(whenStandbyDeferred, elapsedRealtimeMillis, pw);
            pw.println();
        }
        if (mFirstForceBatchedTimeElapsed != 0) {
            pw.print(prefix);
            pw.print("  Time since first force batch attempt: ");
            TimeUtils.formatDuration(mFirstForceBatchedTimeElapsed, elapsedRealtimeMillis, pw);
            pw.println();
        }
        pw.print(prefix); pw.print("Enqueue time: ");
        TimeUtils.formatDuration(enqueueTime, elapsedRealtimeMillis, pw);
        pw.println();
@@ -1830,6 +1851,11 @@ public final class JobStatus {

        proto.write(JobStatusDumpProto.STANDBY_BUCKET, standbyBucket);
        proto.write(JobStatusDumpProto.ENQUEUE_DURATION_MS, elapsedRealtimeMillis - enqueueTime);
        proto.write(JobStatusDumpProto.TIME_SINCE_FIRST_DEFERRAL_MS,
                whenStandbyDeferred == 0 ? 0 : elapsedRealtimeMillis - whenStandbyDeferred);
        proto.write(JobStatusDumpProto.TIME_SINCE_FIRST_FORCE_BATCH_ATTEMPT_MS,
                mFirstForceBatchedTimeElapsed == 0
                        ? 0 : elapsedRealtimeMillis - mFirstForceBatchedTimeElapsed);
        if (earliestRunTimeElapsedMillis == NO_EARLIEST_RUNTIME) {
            proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS, 0);
        } else {
+84 −0
Original line number Diff line number Diff line
@@ -25,6 +25,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;

import static org.junit.Assert.assertEquals;
@@ -662,4 +664,86 @@ public class JobSchedulerServiceTest {
        assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
        assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
    }

    /** Tests that rare job batching works as expected. */
    @Test
    public void testRareJobBatching() {
        spyOn(mService);
        doNothing().when(mService).evaluateControllerStatesLocked(any());
        doNothing().when(mService).noteJobsPending(any());
        doReturn(true).when(mService).isReadyToBeExecutedLocked(any());
        advanceElapsedClock(24 * HOUR_IN_MILLIS);

        JobSchedulerService.MaybeReadyJobQueueFunctor maybeQueueFunctor =
                mService.new MaybeReadyJobQueueFunctor();
        mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
        mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS;
        mService.mConstants.MIN_CONNECTIVITY_COUNT = 2;
        mService.mConstants.MIN_READY_JOBS_COUNT = 1;

        JobStatus job = createJobStatus(
                "testRareJobBatching",
                createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
        job.setStandbyBucket(RARE_INDEX);

        // Not enough RARE jobs to run.
        mService.mPendingJobs.clear();
        maybeQueueFunctor.reset();
        for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
            maybeQueueFunctor.accept(job);
            assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
            assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
            assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
        }
        maybeQueueFunctor.postProcess();
        assertEquals(0, mService.mPendingJobs.size());

        // Enough RARE jobs to run.
        mService.mPendingJobs.clear();
        maybeQueueFunctor.reset();
        for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT; ++i) {
            maybeQueueFunctor.accept(job);
            assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
            assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
            assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
        }
        maybeQueueFunctor.postProcess();
        assertEquals(5, mService.mPendingJobs.size());

        // Not enough RARE jobs to run, but a non-batched job saves the day.
        mService.mPendingJobs.clear();
        maybeQueueFunctor.reset();
        JobStatus activeJob = createJobStatus(
                "testRareJobBatching",
                createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
        activeJob.setStandbyBucket(ACTIVE_INDEX);
        for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
            maybeQueueFunctor.accept(job);
            assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
            assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
            assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
        }
        maybeQueueFunctor.accept(activeJob);
        maybeQueueFunctor.postProcess();
        assertEquals(3, mService.mPendingJobs.size());

        // Not enough RARE jobs to run, but an old RARE job saves the day.
        mService.mPendingJobs.clear();
        maybeQueueFunctor.reset();
        JobStatus oldRareJob = createJobStatus("testRareJobBatching", createJobInfo());
        oldRareJob.setStandbyBucket(RARE_INDEX);
        final long oldBatchTime = sElapsedRealtimeClock.millis()
                - 2 * mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;
        oldRareJob.setFirstForceBatchedTimeElapsed(oldBatchTime);
        for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
            maybeQueueFunctor.accept(job);
            assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
            assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
            assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
        }
        maybeQueueFunctor.accept(oldRareJob);
        assertEquals(oldBatchTime, oldRareJob.getFirstForceBatchedTimeElapsed());
        maybeQueueFunctor.postProcess();
        assertEquals(3, mService.mPendingJobs.size());
    }
}