diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index 6ab10518b5578a56ea0e46c624b95164cd4cb565..18856f782b7d51b47bad0a3d778fa6b2f34cae31 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -62,6 +62,7 @@ class JobConcurrencyManager { CONFIG_KEY_PREFIX_CONCURRENCY + "screen_off_adjustment_delay_ms"; private static final long DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS = 30_000; + // Try to give higher priority types lower values. static final int WORK_TYPE_NONE = 0; static final int WORK_TYPE_TOP = 1 << 0; static final int WORK_TYPE_BG = 1 << 1; @@ -520,6 +521,120 @@ class JobConcurrencyManager { } } + void onJobCompletedLocked(@NonNull JobServiceContext worker, @NonNull JobStatus jobStatus, + @WorkType final int workType) { + mWorkCountTracker.onJobFinished(workType); + mRunningJobs.remove(jobStatus); + final List<JobStatus> pendingJobs = mService.mPendingJobs; + if (worker.getPreferredUid() != JobServiceContext.NO_PREFERRED_UID) { + updateCounterConfigLocked(); + // Preemption case needs special care. + updateNonRunningPriorities(pendingJobs, false); + + JobStatus highestPriorityJob = null; + int highPriWorkType = workType; + JobStatus backupJob = null; + int backupWorkType = WORK_TYPE_NONE; + for (int i = 0; i < pendingJobs.size(); i++) { + final JobStatus nextPending = pendingJobs.get(i); + + if (mRunningJobs.contains(nextPending)) { + continue; + } + + if (worker.getPreferredUid() != nextPending.getUid()) { + if (backupJob == null) { + int workAsType = + mWorkCountTracker.canJobStart(getJobWorkTypes(nextPending)); + if (workAsType != WORK_TYPE_NONE) { + backupJob = nextPending; + backupWorkType = workAsType; + } + } + continue; + } + + if (highestPriorityJob == null + || highestPriorityJob.lastEvaluatedPriority + < nextPending.lastEvaluatedPriority) { + highestPriorityJob = nextPending; + } else { + continue; + } + + // In this path, we pre-empted an existing job. We don't fully care about the + // reserved slots. We should just run the highest priority job we can find, + // though it would be ideal to use an available WorkType slot instead of + // overloading slots. + final int workAsType = mWorkCountTracker.canJobStart(getJobWorkTypes(nextPending)); + if (workAsType == WORK_TYPE_NONE) { + // Just use the preempted job's work type since this new one is technically + // replacing it anyway. + highPriWorkType = workType; + } else { + highPriWorkType = workAsType; + } + } + if (highestPriorityJob != null) { + if (DEBUG) { + Slog.d(TAG, "Running job " + jobStatus + " as preemption"); + } + mWorkCountTracker.stageJob(highPriWorkType); + startJobLocked(worker, highestPriorityJob, highPriWorkType); + } else { + if (DEBUG) { + Slog.d(TAG, "Couldn't find preemption job for uid " + worker.getPreferredUid()); + } + worker.clearPreferredUid(); + if (backupJob != null) { + if (DEBUG) { + Slog.d(TAG, "Running job " + jobStatus + " instead"); + } + mWorkCountTracker.stageJob(backupWorkType); + startJobLocked(worker, backupJob, backupWorkType); + } + } + } else if (pendingJobs.size() > 0) { + updateCounterConfigLocked(); + updateNonRunningPriorities(pendingJobs, false); + + // This slot is now free and we have pending jobs. Start the highest priority job we + // find. + JobStatus highestPriorityJob = null; + int highPriWorkType = workType; + for (int i = 0; i < pendingJobs.size(); i++) { + final JobStatus nextPending = pendingJobs.get(i); + + if (mRunningJobs.contains(nextPending)) { + continue; + } + + final int workAsType = mWorkCountTracker.canJobStart(getJobWorkTypes(nextPending)); + if (workAsType == WORK_TYPE_NONE) { + continue; + } + if (highestPriorityJob == null + || highestPriorityJob.lastEvaluatedPriority + < nextPending.lastEvaluatedPriority) { + highestPriorityJob = nextPending; + highPriWorkType = workAsType; + } + } + + if (highestPriorityJob != null) { + // This slot is free, and we haven't yet hit the limit on + // concurrent jobs... we can just throw the job in to here. + if (DEBUG) { + Slog.d(TAG, "About to run job: " + jobStatus); + } + mWorkCountTracker.stageJob(highPriWorkType); + startJobLocked(worker, highestPriorityJob, highPriWorkType); + } + } + + noteConcurrency(); + } + @GuardedBy("mLock") private String printPendingQueueLocked() { StringBuilder s = new StringBuilder("Pending queue: "); @@ -855,7 +970,25 @@ class JobConcurrencyManager { if (numRemainingForType < mNumActuallyReservedSlots.get(workType)) { // We've run all jobs for this type. Let another type use it now. mNumActuallyReservedSlots.put(workType, numRemainingForType); - mNumUnspecializedRemaining++; + int assignWorkType = WORK_TYPE_NONE; + for (int i = 0; i < mNumActuallyReservedSlots.size(); ++i) { + int wt = mNumActuallyReservedSlots.keyAt(i); + if (assignWorkType == WORK_TYPE_NONE || wt < assignWorkType) { + // Try to give this slot to the highest priority one within its limits. + int total = mNumRunningJobs.get(wt) + mNumStartingJobs.get(wt) + + mNumPendingJobs.get(wt); + if (mNumActuallyReservedSlots.valueAt(i) < mConfigAbsoluteMaxSlots.get(wt) + && total > mNumActuallyReservedSlots.valueAt(i)) { + assignWorkType = wt; + } + } + } + if (assignWorkType != WORK_TYPE_NONE) { + mNumActuallyReservedSlots.put(assignWorkType, + mNumActuallyReservedSlots.get(assignWorkType) + 1); + } else { + mNumUnspecializedRemaining++; + } } } @@ -871,6 +1004,18 @@ class JobConcurrencyManager { } } + void onJobFinished(@WorkType int workType) { + final int newNumRunningJobs = mNumRunningJobs.get(workType) - 1; + if (newNumRunningJobs < 0) { + // We are in a bad state. We will eventually recover when the pending list is + // regenerated. + Slog.e(TAG, "# running jobs for " + workType + " went negative."); + return; + } + mNumRunningJobs.put(workType, newNumRunningJobs); + maybeAdjustReservations(workType); + } + void onCountDone() { // Calculate how many slots to reserve for each work type. "Unspecialized" slots will // be reserved for higher importance types first (ie. top before bg). diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index ba78bda0d2faefcdd154f4e836c9eeec28dc194d..7ce867c6c850022c87b877d72da1682b5009ada4 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1426,8 +1426,8 @@ public class JobSchedulerService extends com.android.server.SystemService // Create the "runners". for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { mActiveServices.add( - new JobServiceContext(this, mBatteryStats, mJobPackageTracker, - getContext().getMainLooper())); + new JobServiceContext(this, mConcurrencyManager, mBatteryStats, + mJobPackageTracker, getContext().getMainLooper())); } // Attach jobs to their controllers. mJobs.forEachJob((job) -> { @@ -1710,9 +1710,6 @@ public class JobSchedulerService extends com.android.server.SystemService if (DEBUG) { Slog.d(TAG, "Could not find job to remove. Was job removed while executing?"); } - // We still want to check for jobs to execute, because this job may have - // scheduled a new job under the same job id, and now we can run it. - mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget(); return; } @@ -1734,7 +1731,6 @@ public class JobSchedulerService extends com.android.server.SystemService } jobStatus.unprepareLocked(); reportActiveLocked(); - mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget(); } // StateChangedListener implementations. diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 247b4211fdf916b030b5813acd48055e60e3c242..d15bae0274ac3c9064655456646415756fa950c1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -107,6 +107,7 @@ public final class JobServiceContext implements ServiceConnection { private final Handler mCallbackHandler; /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */ private final JobCompletedListener mCompletedListener; + private final JobConcurrencyManager mJobConcurrencyManager; /** Used for service binding, etc. */ private final Context mContext; private final Object mLock; @@ -183,13 +184,14 @@ public final class JobServiceContext implements ServiceConnection { } } - JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats, - JobPackageTracker tracker, Looper looper) { + JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager, + IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper) { mContext = service.getContext(); mLock = service.getLock(); mBatteryStats = batteryStats; mJobPackageTracker = tracker; mCallbackHandler = new JobServiceHandler(looper); + mJobConcurrencyManager = concurrencyManager; mCompletedListener = service; mAvailable = true; mVerb = VERB_FINISHED; @@ -835,6 +837,7 @@ public final class JobServiceContext implements ServiceConnection { if (mWakeLock != null) { mWakeLock.release(); } + final int workType = mRunningJobWorkType; mContext.unbindService(JobServiceContext.this); mWakeLock = null; mRunningJob = null; @@ -847,6 +850,7 @@ public final class JobServiceContext implements ServiceConnection { mAvailable = true; removeOpTimeOutLocked(); mCompletedListener.onJobCompletedLocked(completedJob, reschedule); + mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType); } private void applyStoppedReasonLocked(String reason) { diff --git a/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java b/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java index 62088015cbd578ae3e8d158911a158f1d0c3bacc..263cf48a8a18f19c63ae1b9d9998dfb57de8e9f0 100644 --- a/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java @@ -77,18 +77,19 @@ public class WorkCountTrackerTest { for (int i = running.get(WORK_TYPE_BG); i > 0; i--) { if (mRandom.nextDouble() < stopRatio) { running.put(WORK_TYPE_BG, running.get(WORK_TYPE_BG) - 1); + mWorkCountTracker.onJobFinished(WORK_TYPE_BG); } } for (int i = running.get(WORK_TYPE_TOP); i > 0; i--) { if (mRandom.nextDouble() < stopRatio) { running.put(WORK_TYPE_TOP, running.get(WORK_TYPE_TOP) - 1); + mWorkCountTracker.onJobFinished(WORK_TYPE_TOP); } } } } - - private void startPendingJobs(Jobs jobs, int totalMax, + private void recount(Jobs jobs, int totalMax, @NonNull List<Pair<Integer, Integer>> minLimits, @NonNull List<Pair<Integer, Integer>> maxLimits) { mWorkCountTracker.setConfig(new JobConcurrencyManager.WorkTypeConfig( @@ -113,7 +114,9 @@ public class WorkCountTrackerTest { } mWorkCountTracker.onCountDone(); + } + private void startPendingJobs(Jobs jobs) { while ((jobs.pending.get(WORK_TYPE_TOP) > 0 && mWorkCountTracker.canJobStart(WORK_TYPE_TOP) != WORK_TYPE_NONE) || (jobs.pending.get(WORK_TYPE_BG) > 0 @@ -151,7 +154,8 @@ public class WorkCountTrackerTest { jobs.maybeFinishJobs(stopRatio); jobs.maybeEnqueueJobs(startRatio, fgJobRatio); - startPendingJobs(jobs, totalMax, minLimits, maxLimits); + recount(jobs, totalMax, minLimits, maxLimits); + startPendingJobs(jobs); int totalRunning = 0; for (int r = 0; r < jobs.running.size(); ++r) { @@ -316,7 +320,8 @@ public class WorkCountTrackerTest { jobs.pending.put(pend.first, pend.second); } - startPendingJobs(jobs, totalMax, minLimits, maxLimits); + recount(jobs, totalMax, minLimits, maxLimits); + startPendingJobs(jobs); for (Pair<Integer, Integer> run : resultRunning) { assertWithMessage("Incorrect running result for work type " + run.first) @@ -421,4 +426,81 @@ public class WorkCountTrackerTest { /* resRun */ List.of(Pair.create(WORK_TYPE_BG, 6)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3))); } + + /** Tests that the counter updates properly when jobs are stopped. */ + @Test + public void testJobLifecycleLoop() { + final Jobs jobs = new Jobs(); + jobs.pending.put(WORK_TYPE_TOP, 11); + jobs.pending.put(WORK_TYPE_BG, 10); + + final int totalMax = 6; + final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 1)); + final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 5)); + + recount(jobs, totalMax, minLimits, maxLimits); + + startPendingJobs(jobs); + + assertThat(jobs.running.get(WORK_TYPE_TOP)).isEqualTo(5); + assertThat(jobs.running.get(WORK_TYPE_BG)).isEqualTo(1); + assertThat(jobs.pending.get(WORK_TYPE_TOP)).isEqualTo(6); + assertThat(jobs.pending.get(WORK_TYPE_BG)).isEqualTo(9); + + // Stop all jobs + jobs.maybeFinishJobs(1); + + assertThat(mWorkCountTracker.canJobStart(WORK_TYPE_TOP)).isEqualTo(WORK_TYPE_TOP); + assertThat(mWorkCountTracker.canJobStart(WORK_TYPE_BG)).isEqualTo(WORK_TYPE_BG); + + startPendingJobs(jobs); + + assertThat(jobs.running.get(WORK_TYPE_TOP)).isEqualTo(5); + assertThat(jobs.running.get(WORK_TYPE_BG)).isEqualTo(1); + assertThat(jobs.pending.get(WORK_TYPE_TOP)).isEqualTo(1); + assertThat(jobs.pending.get(WORK_TYPE_BG)).isEqualTo(8); + + // Stop only a bg job and make sure the counter only allows another bg job to start. + jobs.running.put(WORK_TYPE_BG, jobs.running.get(WORK_TYPE_BG) - 1); + mWorkCountTracker.onJobFinished(WORK_TYPE_BG); + + assertThat(mWorkCountTracker.canJobStart(WORK_TYPE_TOP)).isEqualTo(WORK_TYPE_NONE); + assertThat(mWorkCountTracker.canJobStart(WORK_TYPE_BG)).isEqualTo(WORK_TYPE_BG); + + startPendingJobs(jobs); + + assertThat(jobs.running.get(WORK_TYPE_TOP)).isEqualTo(5); + assertThat(jobs.running.get(WORK_TYPE_BG)).isEqualTo(1); + assertThat(jobs.pending.get(WORK_TYPE_TOP)).isEqualTo(1); + assertThat(jobs.pending.get(WORK_TYPE_BG)).isEqualTo(7); + + // Stop only a top job and make sure the counter only allows another top job to start. + jobs.running.put(WORK_TYPE_TOP, jobs.running.get(WORK_TYPE_TOP) - 1); + mWorkCountTracker.onJobFinished(WORK_TYPE_TOP); + + assertThat(mWorkCountTracker.canJobStart(WORK_TYPE_TOP)).isEqualTo(WORK_TYPE_TOP); + assertThat(mWorkCountTracker.canJobStart(WORK_TYPE_BG)).isEqualTo(WORK_TYPE_NONE); + + startPendingJobs(jobs); + + assertThat(jobs.running.get(WORK_TYPE_TOP)).isEqualTo(5); + assertThat(jobs.running.get(WORK_TYPE_BG)).isEqualTo(1); + assertThat(jobs.pending.get(WORK_TYPE_TOP)).isEqualTo(0); + assertThat(jobs.pending.get(WORK_TYPE_BG)).isEqualTo(7); + + // Now that there are no more TOP jobs pending, BG should be able to start when TOP stops. + for (int i = jobs.running.get(WORK_TYPE_TOP); i > 0; --i) { + jobs.running.put(WORK_TYPE_TOP, jobs.running.get(WORK_TYPE_TOP) - 1); + mWorkCountTracker.onJobFinished(WORK_TYPE_TOP); + + assertThat(mWorkCountTracker.canJobStart(WORK_TYPE_BG)).isEqualTo(WORK_TYPE_BG); + } + + startPendingJobs(jobs); + + assertThat(jobs.running.get(WORK_TYPE_TOP)).isEqualTo(0); + assertThat(jobs.running.get(WORK_TYPE_BG)).isEqualTo(5); + assertThat(jobs.pending.get(WORK_TYPE_TOP)).isEqualTo(0); + assertThat(jobs.pending.get(WORK_TYPE_BG)).isEqualTo(3); + } }