Add additional lanes if current jobs will run too long.

With the incoming long-running job feature comes the possibility that
all usable slots are occupied for long periods of time (eg. for hours),
so if we detect that other jobs would be stuck waiting in the queue for
too long then we'll create at most 2 extra slots (one for EJs, one for
regular jobs) so that we can make forward progress with the other jobs.

Bug: 248386641
Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job
Test: atest CtsJobSchedulerTestCases
Change-Id: I7a41a5c8371714a0285bb2dcfe115c762c0d8ed7
This commit is contained in:
Kweku Adams
2022-10-12 17:48:12 +00:00
parent b30d5466b3
commit 9bfcae02d0
3 changed files with 377 additions and 14 deletions

View File

@@ -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<ContextAssignment> idle,
long prepareForAssignmentDeterminationLocked(final ArraySet<ContextAssignment> idle,
final List<ContextAssignment> preferredUidOnly,
final List<ContextAssignment> 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<ContextAssignment> changed,
final ArraySet<ContextAssignment> idle,
final List<ContextAssignment> preferredUidOnly,
final List<ContextAssignment> stoppable) {
final List<ContextAssignment> stoppable,
long minPreferredUidOnlyWaitingTimeMs) {
final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
final List<JobServiceContext> 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

View File

@@ -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;

View File

@@ -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<JobServiceContext, JobStatus> 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<Boolean>) 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<Long>) 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<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
final List<JobConcurrencyManager.ContextAssignment> 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<JobStatus> 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<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
final List<JobConcurrencyManager.ContextAssignment> 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<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
final List<JobConcurrencyManager.ContextAssignment> 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<JobConcurrencyManager.ContextAssignment> changed = new ArraySet<>();
final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
final List<JobConcurrencyManager.ContextAssignment> 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<JobStatus> 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<JobConcurrencyManager.ContextAssignment> changed = new ArraySet<>();
final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
final List<JobConcurrencyManager.ContextAssignment> 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<JobStatus> 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<JobConcurrencyManager.ContextAssignment> changed = new ArraySet<>();
final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
final List<JobConcurrencyManager.ContextAssignment> 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);