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 52dc01b8df301..53f5c6e641299 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -16,6 +16,8 @@ package com.android.server.job; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; @@ -99,6 +101,18 @@ class JobConcurrencyManager { static final String KEY_PKG_CONCURRENCY_LIMIT_REGULAR = CONFIG_KEY_PREFIX_CONCURRENCY + "pkg_concurrency_limit_regular"; private static final int DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR = STANDARD_CONCURRENCY_LIMIT / 2; + @VisibleForTesting + static final String KEY_ENABLE_MAX_WAIT_TIME_BYPASS = + CONFIG_KEY_PREFIX_CONCURRENCY + "enable_max_wait_time_bypass"; + private static final boolean DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS = true; + private static final String KEY_MAX_WAIT_EJ_MS = + CONFIG_KEY_PREFIX_CONCURRENCY + "max_wait_ej_ms"; + @VisibleForTesting + static final long DEFAULT_MAX_WAIT_EJ_MS = 5 * MINUTE_IN_MILLIS; + private static final String KEY_MAX_WAIT_REGULAR_MS = + CONFIG_KEY_PREFIX_CONCURRENCY + "max_wait_regular_ms"; + @VisibleForTesting + static final long DEFAULT_MAX_WAIT_REGULAR_MS = 30 * MINUTE_IN_MILLIS; /** * Set of possible execution types that a job can have. The actual type(s) of a job are based @@ -353,6 +367,20 @@ class JobConcurrencyManager { */ private int mPkgConcurrencyLimitRegular = DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR; + private boolean mMaxWaitTimeBypassEnabled = DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS; + + /** + * The maximum time an expedited job would have to be potentially waiting for an available + * slot before we would consider creating a new slot for it. + */ + private long mMaxWaitEjMs = DEFAULT_MAX_WAIT_EJ_MS; + + /** + * The maximum time a regular job would have to be potentially waiting for an available + * slot before we would consider creating a new slot for it. + */ + private long mMaxWaitRegularMs = DEFAULT_MAX_WAIT_REGULAR_MS; + /** Current memory trim level. */ private int mLastMemoryTrimLevel; @@ -665,7 +693,7 @@ class JobConcurrencyManager { return; } - prepareForAssignmentDeterminationLocked( + final long minPreferredUidOnlyWaitingTimeMs = prepareForAssignmentDeterminationLocked( mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable); if (DEBUG) { @@ -674,7 +702,8 @@ class JobConcurrencyManager { } determineAssignmentsLocked( - mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable); + mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable, + minPreferredUidOnlyWaitingTimeMs); if (DEBUG) { Slog.d(TAG, printAssignments("running jobs final", @@ -691,9 +720,10 @@ class JobConcurrencyManager { noteConcurrency(); } + /** @return the minimum remaining execution time for preferred UID only JobServiceContexts. */ @VisibleForTesting @GuardedBy("mLock") - void prepareForAssignmentDeterminationLocked(final ArraySet idle, + long prepareForAssignmentDeterminationLocked(final ArraySet idle, final List preferredUidOnly, final List stoppable) { final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue(); @@ -709,6 +739,8 @@ class JobConcurrencyManager { updateNonRunningPrioritiesLocked(pendingJobQueue, true); final int numRunningJobs = activeServices.size(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + long minPreferredUidOnlyWaitingTimeMs = Long.MAX_VALUE; for (int i = 0; i < numRunningJobs; ++i) { final JobServiceContext jsc = activeServices.get(i); final JobStatus js = jsc.getRunningJobLocked(); @@ -729,6 +761,9 @@ class JobConcurrencyManager { if ((assignment.shouldStopJobReason = shouldStopRunningJobLocked(jsc)) != null) { stoppable.add(assignment); } else { + assignment.timeUntilStoppableMs = jsc.getRemainingGuaranteedTimeMs(nowElapsed); + minPreferredUidOnlyWaitingTimeMs = + Math.min(minPreferredUidOnlyWaitingTimeMs, assignment.timeUntilStoppableMs); preferredUidOnly.add(assignment); } } @@ -754,6 +789,10 @@ class JobConcurrencyManager { } mWorkCountTracker.onCountDone(); + // Return 0 if there were no preferred UID only contexts to indicate no waiting time due + // to such jobs. + return minPreferredUidOnlyWaitingTimeMs == Long.MAX_VALUE + ? 0 : minPreferredUidOnlyWaitingTimeMs; } @VisibleForTesting @@ -761,12 +800,14 @@ class JobConcurrencyManager { void determineAssignmentsLocked(final ArraySet changed, final ArraySet idle, final List preferredUidOnly, - final List stoppable) { + final List stoppable, + long minPreferredUidOnlyWaitingTimeMs) { final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue(); final List activeServices = mActiveServices; pendingJobQueue.resetIterator(); JobStatus nextPending; int projectedRunningCount = activeServices.size(); + long minChangedWaitingTimeMs = Long.MAX_VALUE; while ((nextPending = pendingJobQueue.next()) != null) { if (mRunningJobs.contains(nextPending)) { // Should never happen. @@ -785,6 +826,14 @@ class JobConcurrencyManager { + " to: " + nextPending); } + // Factoring minChangedWaitingTimeMs into the min waiting time effectively limits + // the number of additional contexts that are created due to long waiting times. + // By factoring it in, we imply that the new slot will be available for other + // pending jobs that could be designated as waiting too long, and those other jobs + // would only have to wait for the new slots to become available. + final long minWaitingTimeMs = + Math.min(minPreferredUidOnlyWaitingTimeMs, minChangedWaitingTimeMs); + // Find an available slot for nextPending. The context should be one of the following: // 1. Unused // 2. Its job should have used up its minimum execution guarantee so it @@ -833,6 +882,7 @@ class JobConcurrencyManager { // app was on TOP, the app is still TOP, but there are too many TOP+EJs // running (because we don't want them to starve out other apps and the // current job has already run for the minimum guaranteed time). + // 5. This new job could be waiting for too long for a slot to open up boolean canReplace = isTopEj; // Case 1 if (!canReplace && !isInOverage) { final int currentJobBias = mService.evaluateJobBiasLocked(runningJob); @@ -840,6 +890,13 @@ class JobConcurrencyManager { || currentJobBias < JobInfo.BIAS_TOP_APP // Case 3 || topEjCount > .5 * mWorkTypeConfig.getMaxTotal(); // Case 4 } + if (!canReplace && mMaxWaitTimeBypassEnabled) { // Case 5 + if (nextPending.shouldTreatAsExpeditedJob()) { + canReplace = minWaitingTimeMs >= mMaxWaitEjMs; + } else { + canReplace = minWaitingTimeMs >= mMaxWaitRegularMs; + } + } if (canReplace) { int replaceWorkType = mWorkCountTracker.canJobStart(allWorkTypes, assignment.context.getRunningJobWorkType()); @@ -860,6 +917,7 @@ class JobConcurrencyManager { } if (selectedContext == null && (!isInOverage || isTopEj)) { int lowestBiasSeen = Integer.MAX_VALUE; + long newMinPreferredUidOnlyWaitingTimeMs = Long.MAX_VALUE; for (int p = preferredUidOnly.size() - 1; p >= 0; --p) { final ContextAssignment assignment = preferredUidOnly.get(p); final JobStatus runningJob = assignment.context.getRunningJobLocked(); @@ -872,6 +930,13 @@ class JobConcurrencyManager { } if (selectedContext == null || lowestBiasSeen > jobBias) { + if (selectedContext != null) { + // We're no longer using the previous context, so factor it into the + // calculation. + newMinPreferredUidOnlyWaitingTimeMs = Math.min( + newMinPreferredUidOnlyWaitingTimeMs, + selectedContext.timeUntilStoppableMs); + } // Step down the preemption threshold - wind up replacing // the lowest-bias running job lowestBiasSeen = jobBias; @@ -880,11 +945,17 @@ class JobConcurrencyManager { assignment.preemptReasonCode = JobParameters.STOP_REASON_PREEMPT; // In this case, we're just going to preempt a low bias job, we're not // actually starting a job, so don't set startingJob to true. + } else { + // We're not going to use this context, so factor it into the calculation. + newMinPreferredUidOnlyWaitingTimeMs = Math.min( + newMinPreferredUidOnlyWaitingTimeMs, + assignment.timeUntilStoppableMs); } } if (selectedContext != null) { selectedContext.newJob = nextPending; preferredUidOnly.remove(selectedContext); + minPreferredUidOnlyWaitingTimeMs = newMinPreferredUidOnlyWaitingTimeMs; } } // Make sure to run EJs for the TOP app immediately. @@ -901,6 +972,9 @@ class JobConcurrencyManager { selectedContext = null; } if (selectedContext == null) { + if (DEBUG) { + Slog.d(TAG, "Allowing additional context because EJ would wait too long"); + } selectedContext = mContextAssignmentPool.acquire(); if (selectedContext == null) { selectedContext = new ContextAssignment(); @@ -913,6 +987,35 @@ class JobConcurrencyManager { selectedContext.newWorkType = (workType != WORK_TYPE_NONE) ? workType : WORK_TYPE_TOP; } + } else if (selectedContext == null && mMaxWaitTimeBypassEnabled) { + final boolean wouldBeWaitingTooLong = nextPending.shouldTreatAsExpeditedJob() + ? minWaitingTimeMs >= mMaxWaitEjMs + : minWaitingTimeMs >= mMaxWaitRegularMs; + if (wouldBeWaitingTooLong) { + if (DEBUG) { + Slog.d(TAG, "Allowing additional context because job would wait too long"); + } + selectedContext = mContextAssignmentPool.acquire(); + if (selectedContext == null) { + selectedContext = new ContextAssignment(); + } + selectedContext.context = mIdleContexts.size() > 0 + ? mIdleContexts.removeAt(mIdleContexts.size() - 1) + : createNewJobServiceContext(); + selectedContext.newJob = nextPending; + final int workType = mWorkCountTracker.canJobStart(allWorkTypes); + if (workType != WORK_TYPE_NONE) { + selectedContext.newWorkType = workType; + } else { + // Use the strongest work type possible for this job. + for (int type = 1; type <= ALL_WORK_TYPES; type = type << 1) { + if ((type & allWorkTypes) != 0) { + selectedContext.newWorkType = type; + break; + } + } + } + } } final PackageStats packageStats = getPkgStatsLocked( nextPending.getSourceUserId(), nextPending.getSourcePackageName()); @@ -923,6 +1026,8 @@ class JobConcurrencyManager { } if (selectedContext.newJob != null) { projectedRunningCount++; + minChangedWaitingTimeMs = Math.min(minChangedWaitingTimeMs, + mService.getMinJobExecutionGuaranteeMs(selectedContext.newJob)); } packageStats.adjustStagedCount(true, nextPending.shouldTreatAsExpeditedJob()); } @@ -1251,13 +1356,37 @@ class JobConcurrencyManager { } final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue(); - if (mActiveServices.size() >= STANDARD_CONCURRENCY_LIMIT || pendingJobQueue.size() == 0) { + if (pendingJobQueue.size() == 0) { worker.clearPreferredUid(); - // We're over the limit (because the TOP app scheduled a lot of EJs). Don't start - // running anything new until we get back below the limit. noteConcurrency(); return; } + if (mActiveServices.size() >= STANDARD_CONCURRENCY_LIMIT) { + final boolean respectConcurrencyLimit; + if (!mMaxWaitTimeBypassEnabled) { + respectConcurrencyLimit = true; + } else { + long minWaitingTimeMs = Long.MAX_VALUE; + final long nowElapsed = sElapsedRealtimeClock.millis(); + for (int i = mActiveServices.size() - 1; i >= 0; --i) { + minWaitingTimeMs = Math.min(minWaitingTimeMs, + mActiveServices.get(i).getRemainingGuaranteedTimeMs(nowElapsed)); + } + final boolean wouldBeWaitingTooLong = + mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0 + ? minWaitingTimeMs >= mMaxWaitEjMs + : minWaitingTimeMs >= mMaxWaitRegularMs; + respectConcurrencyLimit = !wouldBeWaitingTooLong; + } + if (respectConcurrencyLimit) { + worker.clearPreferredUid(); + // We're over the limit (because the TOP app scheduled a lot of EJs), but we should + // be able to stop the other jobs soon so don't start running anything new until we + // get back below the limit. + noteConcurrency(); + return; + } + } if (worker.getPreferredUid() != JobServiceContext.NO_PREFERRED_UID) { updateCounterConfigLocked(); @@ -1609,6 +1738,14 @@ class JobConcurrencyManager { mPkgConcurrencyLimitRegular = Math.max(1, Math.min(STANDARD_CONCURRENCY_LIMIT, properties.getInt( KEY_PKG_CONCURRENCY_LIMIT_REGULAR, DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR))); + + mMaxWaitTimeBypassEnabled = properties.getBoolean( + KEY_ENABLE_MAX_WAIT_TIME_BYPASS, DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS); + // EJ max wait must be in the range [0, infinity). + mMaxWaitEjMs = Math.max(0, properties.getLong(KEY_MAX_WAIT_EJ_MS, DEFAULT_MAX_WAIT_EJ_MS)); + // Regular max wait must be in the range [EJ max wait, infinity). + mMaxWaitRegularMs = Math.max(mMaxWaitEjMs, + properties.getLong(KEY_MAX_WAIT_REGULAR_MS, DEFAULT_MAX_WAIT_REGULAR_MS)); } @GuardedBy("mLock") @@ -1622,6 +1759,9 @@ class JobConcurrencyManager { pw.print(KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, mScreenOffAdjustmentDelayMs).println(); pw.print(KEY_PKG_CONCURRENCY_LIMIT_EJ, mPkgConcurrencyLimitEj).println(); pw.print(KEY_PKG_CONCURRENCY_LIMIT_REGULAR, mPkgConcurrencyLimitRegular).println(); + pw.print(KEY_ENABLE_MAX_WAIT_TIME_BYPASS, mMaxWaitTimeBypassEnabled).println(); + pw.print(KEY_MAX_WAIT_EJ_MS, mMaxWaitEjMs).println(); + pw.print(KEY_MAX_WAIT_REGULAR_MS, mMaxWaitRegularMs).println(); pw.println(); CONFIG_LIMITS_SCREEN_ON.normal.dump(pw); pw.println(); @@ -2382,6 +2522,7 @@ class JobConcurrencyManager { public int workType = WORK_TYPE_NONE; public String preemptReason; public int preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED; + public long timeUntilStoppableMs; public String shouldStopJobReason; public JobStatus newJob; public int newWorkType = WORK_TYPE_NONE; @@ -2392,6 +2533,7 @@ class JobConcurrencyManager { workType = WORK_TYPE_NONE; preemptReason = null; preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED; + timeUntilStoppableMs = 0; shouldStopJobReason = null; newJob = null; newWorkType = WORK_TYPE_NONE; @@ -2406,6 +2548,15 @@ class JobConcurrencyManager { final PackageStats packageStats = getPackageStatsForTesting(job.getSourceUserId(), job.getSourcePackageName()); packageStats.adjustRunningCount(true, job.shouldTreatAsExpeditedJob()); + + final JobServiceContext context; + if (mIdleContexts.size() > 0) { + context = mIdleContexts.removeAt(mIdleContexts.size() - 1); + } else { + context = createNewJobServiceContext(); + } + context.executeRunnableJob(job, mWorkCountTracker.canJobStart(getJobWorkTypes(job))); + mActiveServices.add(context); } @VisibleForTesting 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 d6456f0edea5d..889061f7665a1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -454,6 +454,10 @@ public final class JobServiceContext implements ServiceConnection { return mTimeoutElapsed; } + long getRemainingGuaranteedTimeMs(long nowElapsed) { + return Math.max(0, mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis - nowElapsed); + } + boolean isWithinExecutionGuaranteeTime() { return sElapsedRealtimeClock.millis() < mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis; diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java index 25473477e8b96..6279b87eb6030 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java @@ -34,6 +34,8 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -51,6 +53,7 @@ import android.content.res.Resources; import android.os.Looper; import android.os.UserHandle; import android.provider.DeviceConfig; +import android.util.ArrayMap; import android.util.ArraySet; import androidx.test.filters.SmallTest; @@ -98,12 +101,23 @@ public final class JobConcurrencyManagerTest { @Mock private IPackageManager mIPackageManager; - static class InjectorForTest extends JobConcurrencyManager.Injector { + private static class InjectorForTest extends JobConcurrencyManager.Injector { + public final ArrayMap contexts = new ArrayMap<>(); + @Override JobServiceContext createJobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper) { - return mock(JobServiceContext.class); + final JobServiceContext context = mock(JobServiceContext.class); + doAnswer((Answer) invocationOnMock -> { + Object[] args = invocationOnMock.getArguments(); + final JobStatus job = (JobStatus) args[0]; + contexts.put(context, job); + doReturn(job).when(context).getRunningJobLocked(); + return true; + }).when(context).executeRunnableJob(any(), anyInt()); + contexts.put(context, null); + return context; } } @@ -142,6 +156,13 @@ public final class JobConcurrencyManagerTest { doReturn(mPendingJobQueue).when(jobSchedulerService).getPendingJobQueue(); doReturn(mIPackageManager).when(AppGlobals::getPackageManager); mInjector = new InjectorForTest(); + doAnswer((Answer) invocationOnMock -> { + Object[] args = invocationOnMock.getArguments(); + final JobStatus job = (JobStatus) args[0]; + return job.shouldTreatAsExpeditedJob() + ? JobSchedulerService.Constants.DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS + : JobSchedulerService.Constants.DEFAULT_RUNTIME_MIN_GUARANTEE_MS; + }).when(jobSchedulerService).getMinJobExecutionGuaranteeMs(any()); mJobConcurrencyManager = new JobConcurrencyManager(jobSchedulerService, mInjector); mGracePeriodObserver = mock(GracePeriodObserver.class); mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -167,32 +188,55 @@ public final class JobConcurrencyManagerTest { final ArraySet idle = new ArraySet<>(); final List preferredUidOnly = new ArrayList<>(); final List stoppable = new ArrayList<>(); - mJobConcurrencyManager + final long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size()); assertEquals(0, preferredUidOnly.size()); assertEquals(0, stoppable.size()); + assertEquals(0, minPreferredUidOnlyWaitingTimeMs); } @Test public void testPrepareForAssignmentDetermination_onlyPendingJobs() { - final ArraySet jobs = new ArraySet<>(); for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i); mPendingJobQueue.add(job); - jobs.add(job); } final ArraySet idle = new ArraySet<>(); final List preferredUidOnly = new ArrayList<>(); final List stoppable = new ArrayList<>(); - mJobConcurrencyManager + final long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size()); assertEquals(0, preferredUidOnly.size()); assertEquals(0, stoppable.size()); + assertEquals(0, minPreferredUidOnlyWaitingTimeMs); + } + + @Test + public void testPrepareForAssignmentDetermination_onlyPreferredUidOnly() { + for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i); + mJobConcurrencyManager.addRunningJobForTesting(job); + } + + for (int i = 0; i < mInjector.contexts.size(); ++i) { + doReturn(true).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime(); + } + + final ArraySet idle = new ArraySet<>(); + final List preferredUidOnly = new ArrayList<>(); + final List stoppable = new ArrayList<>(); + final long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager + .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); + + assertEquals(0, idle.size()); + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(0, stoppable.size()); + assertEquals(0, minPreferredUidOnlyWaitingTimeMs); } @Test @@ -216,7 +260,8 @@ public final class JobConcurrencyManagerTest { mJobConcurrencyManager .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); mJobConcurrencyManager - .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable); + .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, + Long.MAX_VALUE); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, changed.size()); for (int i = changed.size() - 1; i >= 0; --i) { @@ -225,6 +270,169 @@ public final class JobConcurrencyManagerTest { assertTrue("Some jobs weren't assigned", jobs.isEmpty()); } + @Test + public void testDetermineAssignments_allPreferredUidOnly_shortTimeLeft() throws Exception { + mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true); + setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, + new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT)); + for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) { + final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i; + final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid); + setPackageUid(sourcePkgName, uid); + final JobStatus job = createJob(uid, sourcePkgName); + spyOn(job); + doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob(); + if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) { + mJobConcurrencyManager.addRunningJobForTesting(job); + } else { + mPendingJobQueue.add(job); + } + } + + // Waiting time is too short, so we shouldn't create any extra contexts. + final long remainingTimeMs = JobConcurrencyManager.DEFAULT_MAX_WAIT_EJ_MS / 2; + for (int i = 0; i < mInjector.contexts.size(); ++i) { + doReturn(true).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime(); + doReturn(remainingTimeMs) + .when(mInjector.contexts.keyAt(i)).getRemainingGuaranteedTimeMs(anyLong()); + } + + final ArraySet changed = new ArraySet<>(); + final ArraySet idle = new ArraySet<>(); + final List preferredUidOnly = new ArrayList<>(); + final List stoppable = new ArrayList<>(); + + long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager + .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); + assertEquals(remainingTimeMs, minPreferredUidOnlyWaitingTimeMs); + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + + mJobConcurrencyManager + .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, + minPreferredUidOnlyWaitingTimeMs); + + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(0, changed.size()); + } + + @Test + public void testDetermineAssignments_allPreferredUidOnly_mediumTimeLeft() throws Exception { + mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true); + setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, + new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT)); + final ArraySet jobs = new ArraySet<>(); + for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) { + final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i; + final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid); + setPackageUid(sourcePkgName, uid); + final JobStatus job = createJob(uid, sourcePkgName); + spyOn(job); + doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob(); + if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) { + mJobConcurrencyManager.addRunningJobForTesting(job); + } else { + mPendingJobQueue.add(job); + jobs.add(job); + } + } + + // Waiting time is longer than the EJ waiting time, but shorter than regular job waiting + // time, so we should only create an extra context for an EJ. + final long remainingTimeMs = (JobConcurrencyManager.DEFAULT_MAX_WAIT_EJ_MS + + JobConcurrencyManager.DEFAULT_MAX_WAIT_REGULAR_MS) / 2; + for (int i = 0; i < mInjector.contexts.size(); ++i) { + doReturn(true).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime(); + doReturn(remainingTimeMs) + .when(mInjector.contexts.keyAt(i)).getRemainingGuaranteedTimeMs(anyLong()); + } + + final ArraySet changed = new ArraySet<>(); + final ArraySet idle = new ArraySet<>(); + final List preferredUidOnly = new ArrayList<>(); + final List stoppable = new ArrayList<>(); + + long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager + .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); + assertEquals(remainingTimeMs, minPreferredUidOnlyWaitingTimeMs); + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + + mJobConcurrencyManager + .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, + minPreferredUidOnlyWaitingTimeMs); + + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + for (int i = changed.size() - 1; i >= 0; --i) { + jobs.remove(changed.valueAt(i).newJob); + } + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT - 1, jobs.size()); + assertEquals(1, changed.size()); + JobStatus assignedJob = changed.valueAt(0).newJob; + assertTrue(assignedJob.shouldTreatAsExpeditedJob()); + } + + @Test + public void testDetermineAssignments_allPreferredUidOnly_longTimeLeft() throws Exception { + mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true); + setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, + new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT)); + final ArraySet jobs = new ArraySet<>(); + for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) { + final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i; + final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid); + setPackageUid(sourcePkgName, uid); + final JobStatus job = createJob(uid, sourcePkgName); + spyOn(job); + doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob(); + if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) { + mJobConcurrencyManager.addRunningJobForTesting(job); + } else { + mPendingJobQueue.add(job); + jobs.add(job); + } + } + + // Waiting time is longer than even the regular job waiting time, so we should + // create an extra context for an EJ, and potentially one for a regular job. + final long remainingTimeMs = 2 * JobConcurrencyManager.DEFAULT_MAX_WAIT_REGULAR_MS; + for (int i = 0; i < mInjector.contexts.size(); ++i) { + doReturn(true).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime(); + doReturn(remainingTimeMs) + .when(mInjector.contexts.keyAt(i)).getRemainingGuaranteedTimeMs(anyLong()); + } + + final ArraySet changed = new ArraySet<>(); + final ArraySet idle = new ArraySet<>(); + final List preferredUidOnly = new ArrayList<>(); + final List stoppable = new ArrayList<>(); + + long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager + .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); + assertEquals(remainingTimeMs, minPreferredUidOnlyWaitingTimeMs); + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + + mJobConcurrencyManager + .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, + minPreferredUidOnlyWaitingTimeMs); + + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + // Depending on iteration order, we may create 1 or 2 contexts. + final long numAssignedJobs = changed.size(); + assertTrue(numAssignedJobs > 0); + assertTrue(numAssignedJobs <= 2); + for (int i = 0; i < numAssignedJobs; ++i) { + jobs.remove(changed.valueAt(i).newJob); + } + assertEquals(numAssignedJobs, + JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT - jobs.size()); + JobStatus firstAssignedJob = changed.valueAt(0).newJob; + if (!firstAssignedJob.shouldTreatAsExpeditedJob()) { + assertEquals(2, numAssignedJobs); + assertTrue(changed.valueAt(1).newJob.shouldTreatAsExpeditedJob()); + } else if (numAssignedJobs == 2) { + assertFalse(changed.valueAt(1).newJob.shouldTreatAsExpeditedJob()); + } + } + @Test public void testIsPkgConcurrencyLimited_top() { final JobStatus topJob = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE, 0);