Merge "Make concurrency manager code more testable."

This commit is contained in:
Kweku Adams
2022-10-14 14:42:35 +00:00
committed by Android (Google) Code Review
2 changed files with 254 additions and 28 deletions

View File

@@ -34,6 +34,7 @@ import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.os.BatteryStats;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -181,6 +182,7 @@ class JobConcurrencyManager {
private final JobSchedulerService mService;
private final Context mContext;
private final Handler mHandler;
private final Injector mInjector;
private PowerManager mPowerManager;
@@ -378,9 +380,15 @@ class JobConcurrencyManager {
}
JobConcurrencyManager(JobSchedulerService service) {
this(service, new Injector());
}
@VisibleForTesting
JobConcurrencyManager(JobSchedulerService service, Injector injector) {
mService = service;
mLock = mService.mLock;
mContext = service.getTestableContext();
mInjector = injector;
mHandler = JobSchedulerBackgroundThread.getHandler();
@@ -414,7 +422,7 @@ class JobConcurrencyManager {
ServiceManager.getService(BatteryStats.SERVICE_NAME));
for (int i = 0; i < STANDARD_CONCURRENCY_LIMIT; i++) {
mIdleContexts.add(
new JobServiceContext(mService, this, batteryStats,
mInjector.createJobServiceContext(mService, this, batteryStats,
mService.mJobPackageTracker, mContext.getMainLooper()));
}
}
@@ -657,15 +665,40 @@ class JobConcurrencyManager {
return;
}
prepareForAssignmentDeterminationLocked(
mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
if (DEBUG) {
Slog.d(TAG, printAssignments("running jobs initial",
mRecycledStoppable, mRecycledPreferredUidOnly));
}
determineAssignmentsLocked(
mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
if (DEBUG) {
Slog.d(TAG, printAssignments("running jobs final",
mRecycledStoppable, mRecycledPreferredUidOnly, mRecycledChanged));
Slog.d(TAG, "work count results: " + mWorkCountTracker);
}
carryOutAssignmentChangesLocked(mRecycledChanged);
cleanUpAfterAssignmentChangesLocked(
mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
noteConcurrency();
}
@VisibleForTesting
@GuardedBy("mLock")
void prepareForAssignmentDeterminationLocked(final ArraySet<ContextAssignment> idle,
final List<ContextAssignment> preferredUidOnly,
final List<ContextAssignment> stoppable) {
final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
final List<JobServiceContext> activeServices = mActiveServices;
// To avoid GC churn, we recycle the arrays.
final ArraySet<ContextAssignment> changed = mRecycledChanged;
final ArraySet<ContextAssignment> idle = mRecycledIdle;
final ArrayList<ContextAssignment> preferredUidOnly = mRecycledPreferredUidOnly;
final ArrayList<ContextAssignment> stoppable = mRecycledStoppable;
updateCounterConfigLocked();
// Reset everything since we'll re-evaluate the current state.
mWorkCountTracker.resetCounts();
@@ -719,15 +752,21 @@ class JobConcurrencyManager {
assignment.context = jsc;
idle.add(assignment);
}
if (DEBUG) {
Slog.d(TAG, printAssignments("running jobs initial", stoppable, preferredUidOnly));
}
mWorkCountTracker.onCountDone();
}
JobStatus nextPending;
@VisibleForTesting
@GuardedBy("mLock")
void determineAssignmentsLocked(final ArraySet<ContextAssignment> changed,
final ArraySet<ContextAssignment> idle,
final List<ContextAssignment> preferredUidOnly,
final List<ContextAssignment> stoppable) {
final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
final List<JobServiceContext> activeServices = mActiveServices;
pendingJobQueue.resetIterator();
int projectedRunningCount = numRunningJobs;
JobStatus nextPending;
int projectedRunningCount = activeServices.size();
while ((nextPending = pendingJobQueue.next()) != null) {
if (mRunningJobs.contains(nextPending)) {
// Should never happen.
@@ -895,13 +934,10 @@ class JobConcurrencyManager {
packageStats);
}
}
if (DEBUG) {
Slog.d(TAG, printAssignments("running jobs final",
stoppable, preferredUidOnly, changed));
Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString());
}
}
@GuardedBy("mLock")
private void carryOutAssignmentChangesLocked(final ArraySet<ContextAssignment> changed) {
for (int c = changed.size() - 1; c >= 0; --c) {
final ContextAssignment assignment = changed.valueAt(c);
final JobStatus js = assignment.context.getRunningJobLocked();
@@ -925,6 +961,13 @@ class JobConcurrencyManager {
assignment.clear();
mContextAssignmentPool.release(assignment);
}
}
@GuardedBy("mLock")
private void cleanUpAfterAssignmentChangesLocked(final ArraySet<ContextAssignment> changed,
final ArraySet<ContextAssignment> idle,
final List<ContextAssignment> preferredUidOnly,
final List<ContextAssignment> stoppable) {
for (int s = stoppable.size() - 1; s >= 0; --s) {
final ContextAssignment assignment = stoppable.get(s);
assignment.clear();
@@ -947,7 +990,6 @@ class JobConcurrencyManager {
preferredUidOnly.clear();
mWorkCountTracker.resetStagingCount();
mActivePkgStats.forEach(mPackageStatsStagingCountClearer);
noteConcurrency();
}
@GuardedBy("mLock")
@@ -1496,7 +1538,7 @@ class JobConcurrencyManager {
@NonNull
private JobServiceContext createNewJobServiceContext() {
return new JobServiceContext(mService, this,
return mInjector.createJobServiceContext(mService, this,
IBatteryStats.Stub.asInterface(
ServiceManager.getService(BatteryStats.SERVICE_NAME)),
mService.mJobPackageTracker, mContext.getMainLooper());
@@ -1777,6 +1819,10 @@ class JobConcurrencyManager {
@VisibleForTesting
static class WorkTypeConfig {
@VisibleForTesting
static final String KEY_PREFIX_MAX = CONFIG_KEY_PREFIX_CONCURRENCY + "max_";
@VisibleForTesting
static final String KEY_PREFIX_MIN = CONFIG_KEY_PREFIX_CONCURRENCY + "min_";
@VisibleForTesting
static final String KEY_PREFIX_MAX_TOTAL = CONFIG_KEY_PREFIX_CONCURRENCY + "max_total_";
private static final String KEY_PREFIX_MAX_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "max_top_";
@@ -2329,7 +2375,8 @@ class JobConcurrencyManager {
}
}
private static final class ContextAssignment {
@VisibleForTesting
static final class ContextAssignment {
public JobServiceContext context;
public int preferredUid = JobServiceContext.NO_PREFERRED_UID;
public int workType = WORK_TYPE_NONE;
@@ -2378,4 +2425,15 @@ class JobConcurrencyManager {
mActivePkgStats.add(userId, packageName, packageStats);
return packageStats;
}
@VisibleForTesting
static class Injector {
@NonNull
JobServiceContext createJobServiceContext(JobSchedulerService service,
JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
JobPackageTracker tracker, Looper looper) {
return new JobServiceContext(service, concurrencyManager, batteryStats,
tracker, looper);
}
}
}

View File

@@ -16,30 +16,48 @@
package com.android.server.job;
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.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_EJ;
import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_REGULAR;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
import android.app.job.JobInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.IPackageManager;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.os.Looper;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.ArraySet;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER_IMPORTANT;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_EJ;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_FGS;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP;
import com.android.internal.R;
import com.android.internal.app.IBatteryStats;
import com.android.server.LocalServices;
import com.android.server.job.JobConcurrencyManager.GracePeriodObserver;
import com.android.server.job.JobConcurrencyManager.WorkTypeConfig;
@@ -52,6 +70,12 @@ import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import java.util.ArrayList;
import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -64,10 +88,24 @@ public final class JobConcurrencyManagerTest {
private int mDefaultUserId;
private GracePeriodObserver mGracePeriodObserver;
private Context mContext;
private InjectorForTest mInjector;
private MockitoSession mMockingSession;
private Resources mResources;
private PendingJobQueue mPendingJobQueue;
private DeviceConfig.Properties.Builder mConfigBuilder;
@Mock
private IPackageManager mIPackageManager;
static class InjectorForTest extends JobConcurrencyManager.Injector {
@Override
JobServiceContext createJobServiceContext(JobSchedulerService service,
JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
JobPackageTracker tracker, Looper looper) {
return mock(JobServiceContext.class);
}
}
@BeforeClass
public static void setUpOnce() {
LocalServices.addService(UserManagerInternal.class, mock(UserManagerInternal.class));
@@ -83,6 +121,11 @@ public final class JobConcurrencyManagerTest {
@Before
public void setUp() {
mMockingSession = mockitoSession()
.initMocks(this)
.mockStatic(AppGlobals.class)
.strictness(Strictness.LENIENT)
.startMocking();
final JobSchedulerService jobSchedulerService = mock(JobSchedulerService.class);
mContext = mock(Context.class);
mResources = mock(Resources.class);
@@ -93,7 +136,9 @@ public final class JobConcurrencyManagerTest {
mConfigBuilder = new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
mPendingJobQueue = new PendingJobQueue();
doReturn(mPendingJobQueue).when(jobSchedulerService).getPendingJobQueue();
mJobConcurrencyManager = new JobConcurrencyManager(jobSchedulerService);
doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
mInjector = new InjectorForTest();
mJobConcurrencyManager = new JobConcurrencyManager(jobSchedulerService, mInjector);
mGracePeriodObserver = mock(GracePeriodObserver.class);
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
@@ -106,6 +151,74 @@ public final class JobConcurrencyManagerTest {
@After
public void tearDown() throws Exception {
resetConfig();
if (mMockingSession != null) {
mMockingSession.finishMocking();
}
}
@Test
public void testPrepareForAssignmentDetermination_noJobs() {
mPendingJobQueue.clear();
final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
mJobConcurrencyManager
.prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size());
assertEquals(0, preferredUidOnly.size());
assertEquals(0, stoppable.size());
}
@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
.prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size());
assertEquals(0, preferredUidOnly.size());
assertEquals(0, stoppable.size());
}
@Test
public void testDetermineAssignments_allRegular() throws Exception {
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; ++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);
mPendingJobQueue.add(job);
jobs.add(job);
}
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<>();
mJobConcurrencyManager
.prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
mJobConcurrencyManager
.determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable);
assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, changed.size());
for (int i = changed.size() - 1; i >= 0; --i) {
jobs.remove(changed.valueAt(i).newJob);
}
assertTrue("Some jobs weren't assigned", jobs.isEmpty());
}
@Test
@@ -403,16 +516,58 @@ public final class JobConcurrencyManagerTest {
}
private static JobStatus createJob(int uid) {
return createJob(uid, 1);
return createJob(uid, 1, null);
}
private static JobStatus createJob(int uid, String sourcePackageName) {
return createJob(uid, 1, sourcePackageName);
}
private static JobStatus createJob(int uid, int jobId) {
return JobStatus.createFromJobInfo(
new JobInfo.Builder(jobId, new ComponentName("foo", "bar")).build(), uid,
null, UserHandle.getUserId(uid), "JobConcurrencyManagerTest");
return createJob(uid, jobId, null);
}
private void setConcurrencyConfig(int total) throws Exception {
private static JobStatus createJob(int uid, int jobId, @Nullable String sourcePackageName) {
return JobStatus.createFromJobInfo(
new JobInfo.Builder(jobId, new ComponentName("foo", "bar")).build(), uid,
sourcePackageName, UserHandle.getUserId(uid), "JobConcurrencyManagerTest");
}
private static final class TypeConfig {
public final String workTypeString;
public final int min;
public final int max;
private TypeConfig(@JobConcurrencyManager.WorkType int workType, int min, int max) {
switch (workType) {
case WORK_TYPE_TOP:
workTypeString = "top";
break;
case WORK_TYPE_FGS:
workTypeString = "fgs";
break;
case WORK_TYPE_EJ:
workTypeString = "ej";
break;
case WORK_TYPE_BG:
workTypeString = "bg";
break;
case WORK_TYPE_BGUSER:
workTypeString = "bguser";
break;
case WORK_TYPE_BGUSER_IMPORTANT:
workTypeString = "bguser_important";
break;
case WORK_TYPE_NONE:
default:
throw new IllegalArgumentException("invalid work type: " + workType);
}
this.min = min;
this.max = max;
}
}
private void setConcurrencyConfig(int total, TypeConfig... typeConfigs) throws Exception {
// Set the values for all memory states so we don't have to worry about memory on the device
// during testing.
final String[] identifiers = {
@@ -422,10 +577,23 @@ public final class JobConcurrencyManagerTest {
for (String identifier : identifiers) {
mConfigBuilder
.setInt(WorkTypeConfig.KEY_PREFIX_MAX_TOTAL + identifier, total);
for (TypeConfig config : typeConfigs) {
mConfigBuilder.setInt(
WorkTypeConfig.KEY_PREFIX_MAX + config.workTypeString + "_" + identifier,
config.max);
mConfigBuilder.setInt(
WorkTypeConfig.KEY_PREFIX_MIN + config.workTypeString + "_" + identifier,
config.min);
}
}
updateDeviceConfig();
}
private void setPackageUid(final String pkgName, final int uid) throws Exception {
doReturn(uid).when(mIPackageManager)
.getPackageUid(eq(pkgName), anyLong(), eq(UserHandle.getUserId(uid)));
}
private void updateDeviceConfig() throws Exception {
DeviceConfig.setProperties(mConfigBuilder.build());
mJobConcurrencyManager.updateConfigLocked();