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 405b12d6d13ad..3ba4df8fe0b52 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -273,23 +273,22 @@ class JobConcurrencyManager { */ JobStatus[] mRecycledAssignContextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT]; - boolean[] mRecycledSlotChanged = new boolean[MAX_JOB_CONTEXTS_COUNT]; + private final ArraySet mRecycledChanged = new ArraySet<>(); + private final ArraySet mRecycledIdle = new ArraySet<>(); + private final ArraySet mRecycledPreferredUidOnly = new ArraySet<>(); + private final ArraySet mRecycledStoppable = new ArraySet<>(); - int[] mRecycledPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT]; - - int[] mRecycledWorkTypeForContext = new int[MAX_JOB_CONTEXTS_COUNT]; - - String[] mRecycledPreemptReasonForContext = new String[MAX_JOB_CONTEXTS_COUNT]; - - int[] mRecycledPreemptReasonCodeForContext = new int[MAX_JOB_CONTEXTS_COUNT]; - - String[] mRecycledShouldStopJobReason = new String[MAX_JOB_CONTEXTS_COUNT]; + private final Pools.Pool mContextAssignmentPool = + new Pools.SimplePool<>(MAX_JOB_CONTEXTS_COUNT); /** - * Set of JobServiceContexts that we use to run jobs. + * Set of JobServiceContexts that are actively running jobs. */ final List mActiveServices = new ArrayList<>(); + /** Set of JobServiceContexts that aren't currently running any jobs. */ + final ArraySet mIdleContexts = new ArraySet<>(); + private final ArraySet mRunningJobs = new ArraySet<>(); private final WorkCountTracker mWorkCountTracker = new WorkCountTracker(); @@ -378,7 +377,7 @@ class JobConcurrencyManager { final IBatteryStats batteryStats = IBatteryStats.Stub.asInterface( ServiceManager.getService(BatteryStats.SERVICE_NAME)); for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { - mActiveServices.add( + mIdleContexts.add( new JobServiceContext(mService, this, batteryStats, mService.mJobPackageTracker, mContext.getMainLooper())); } @@ -582,12 +581,10 @@ class JobConcurrencyManager { // To avoid GC churn, we recycle the arrays. JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap; - boolean[] slotChanged = mRecycledSlotChanged; - int[] preferredUidForContext = mRecycledPreferredUidForContext; - int[] workTypeForContext = mRecycledWorkTypeForContext; - String[] preemptReasonForContext = mRecycledPreemptReasonForContext; - int[] preemptReasonCodeForContext = mRecycledPreemptReasonCodeForContext; - String[] shouldStopJobReason = mRecycledShouldStopJobReason; + final ArraySet changed = mRecycledChanged; + final ArraySet idle = mRecycledIdle; + final ArraySet preferredUidOnly = mRecycledPreferredUidOnly; + final ArraySet stoppable = mRecycledStoppable; updateCounterConfigLocked(); // Reset everything since we'll re-evaluate the current state. @@ -598,20 +595,47 @@ class JobConcurrencyManager { // shouldStopRunningJobLocked(). updateNonRunningPrioritiesLocked(pendingJobQueue, true); - for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { - final JobServiceContext js = activeServices.get(i); - final JobStatus status = js.getRunningJobLocked(); + final int numRunningJobs = activeServices.size(); + for (int i = 0; i < numRunningJobs; ++i) { + final JobServiceContext jsc = activeServices.get(i); + final JobStatus js = jsc.getRunningJobLocked(); - if ((contextIdToJobMap[i] = status) != null) { - mWorkCountTracker.incrementRunningJobCount(js.getRunningJobWorkType()); - workTypeForContext[i] = js.getRunningJobWorkType(); + ContextAssignment assignment = mContextAssignmentPool.acquire(); + if (assignment == null) { + assignment = new ContextAssignment(); } - slotChanged[i] = false; - preferredUidForContext[i] = js.getPreferredUid(); - preemptReasonForContext[i] = null; - preemptReasonCodeForContext[i] = JobParameters.STOP_REASON_UNDEFINED; - shouldStopJobReason[i] = shouldStopRunningJobLocked(js); + assignment.context = jsc; + + if (js != null) { + mWorkCountTracker.incrementRunningJobCount(jsc.getRunningJobWorkType()); + assignment.workType = jsc.getRunningJobWorkType(); + } + + assignment.preferredUid = jsc.getPreferredUid(); + if ((assignment.shouldStopJobReason = shouldStopRunningJobLocked(jsc)) != null) { + stoppable.add(assignment); + } else { + preferredUidOnly.add(assignment); + } + } + for (int i = numRunningJobs; i < MAX_JOB_CONTEXTS_COUNT; ++i) { + final JobServiceContext jsc; + final int numIdleContexts = mIdleContexts.size(); + if (numIdleContexts > 0) { + jsc = mIdleContexts.removeAt(numIdleContexts - 1); + } else { + Slog.wtf(TAG, "Had fewer than " + MAX_JOB_CONTEXTS_COUNT + " in existence"); + jsc = createNewJobServiceContext(); + } + + ContextAssignment assignment = mContextAssignmentPool.acquire(); + if (assignment == null) { + assignment = new ContextAssignment(); + } + + assignment.context = jsc; + idle.add(assignment); } if (DEBUG) { Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial")); @@ -626,83 +650,97 @@ class JobConcurrencyManager { continue; } - // Find an available slot for nextPending. The context should be available OR - // it should have the lowest bias among all running jobs - // (sharing the same Uid as nextPending) - int minBiasForPreemption = Integer.MAX_VALUE; - int selectedContextId = -1; - int allWorkTypes = getJobWorkTypes(nextPending); - int workType = mWorkCountTracker.canJobStart(allWorkTypes); - boolean startingJob = false; - int preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED; - String preemptReason = null; + // 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 + // 3. Its job should have the lowest bias among all running jobs (sharing the same UID + // as nextPending) + ContextAssignment selectedContext = null; + final int allWorkTypes = getJobWorkTypes(nextPending); final boolean pkgConcurrencyOkay = !isPkgConcurrencyLimitedLocked(nextPending); - // TODO(141645789): rewrite this to look at empty contexts first so we don't - // unnecessarily preempt - for (int j = 0; j < MAX_JOB_CONTEXTS_COUNT; j++) { - JobStatus job = contextIdToJobMap[j]; - int preferredUid = preferredUidForContext[j]; - if (job == null) { - final boolean preferredUidOkay = (preferredUid == nextPending.getUid()) - || (preferredUid == JobServiceContext.NO_PREFERRED_UID); - - if (preferredUidOkay && pkgConcurrencyOkay && workType != WORK_TYPE_NONE) { - // This slot is free, and we haven't yet hit the limit on - // concurrent jobs... we can just throw the job in to here. - selectedContextId = j; - startingJob = true; - break; - } - // No job on this context, but nextPending can't run here because - // the context has a preferred Uid or we have reached the limit on - // concurrent jobs. - continue; + boolean startingJob = false; + if (idle.size() > 0) { + final int idx = idle.size() - 1; + final ContextAssignment assignment = idle.valueAt(idx); + final boolean preferredUidOkay = (assignment.preferredUid == nextPending.getUid()) + || (assignment.preferredUid == JobServiceContext.NO_PREFERRED_UID); + int workType = mWorkCountTracker.canJobStart(allWorkTypes); + if (preferredUidOkay && pkgConcurrencyOkay && workType != WORK_TYPE_NONE) { + // This slot is free, and we haven't yet hit the limit on + // concurrent jobs... we can just throw the job in to here. + selectedContext = assignment; + startingJob = true; + idle.removeAt(idx); + assignment.newJob = nextPending; + assignment.newWorkType = workType; } - if (job.getUid() != nextPending.getUid()) { + } + if (selectedContext == null) { + for (int s = stoppable.size() - 1; s >= 0; --s) { + ContextAssignment assignment = stoppable.valueAt(s); + JobStatus runningJob = assignment.context.getRunningJobLocked(); // Maybe stop the job if it has had its day in the sun. Don't let a different // app preempt jobs started for TOP apps though. - final String reason = shouldStopJobReason[j]; - if (job.lastEvaluatedBias < JobInfo.BIAS_TOP_APP - && reason != null && mWorkCountTracker.canJobStart(allWorkTypes, - activeServices.get(j).getRunningJobWorkType()) != WORK_TYPE_NONE) { - // Right now, the way the code is set up, we don't need to explicitly - // assign the new job to this context since we'll reassign when the - // preempted job finally stops. - preemptReason = reason; - preemptReasonCode = JobParameters.STOP_REASON_DEVICE_STATE; + if (runningJob.lastEvaluatedBias < JobInfo.BIAS_TOP_APP + && assignment.shouldStopJobReason != null) { + int replaceWorkType = mWorkCountTracker.canJobStart(allWorkTypes, + assignment.context.getRunningJobWorkType()); + if (replaceWorkType != WORK_TYPE_NONE) { + // Right now, the way the code is set up, we don't need to explicitly + // assign the new job to this context since we'll reassign when the + // preempted job finally stops. + assignment.preemptReason = assignment.shouldStopJobReason; + assignment.preemptReasonCode = JobParameters.STOP_REASON_DEVICE_STATE; + selectedContext = assignment; + stoppable.removeAt(s); + assignment.newJob = nextPending; + assignment.newWorkType = replaceWorkType; + // Don't preserve the UID since we're stopping the job because + // something is pending (eg. EJs). + assignment.context.clearPreferredUid(); + break; + } } - continue; } + } + if (selectedContext == null) { + int lowestBiasSeen = Integer.MAX_VALUE; + for (int p = preferredUidOnly.size() - 1; p >= 0; --p) { + final ContextAssignment assignment = preferredUidOnly.valueAt(p); + final JobStatus runningJob = assignment.context.getRunningJobLocked(); + if (runningJob.getUid() != nextPending.getUid()) { + continue; + } + final int jobBias = mService.evaluateJobBiasLocked(runningJob); + if (jobBias >= nextPending.lastEvaluatedBias) { + continue; + } - final int jobBias = mService.evaluateJobBiasLocked(job); - if (jobBias >= nextPending.lastEvaluatedBias) { - continue; + if (selectedContext == null || lowestBiasSeen > jobBias) { + // Step down the preemption threshold - wind up replacing + // the lowest-bias running job + lowestBiasSeen = jobBias; + selectedContext = assignment; + assignment.preemptReason = "higher bias job found"; + 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. + } } - - if (minBiasForPreemption > jobBias) { - // Step down the preemption threshold - wind up replacing - // the lowest-bias running job - minBiasForPreemption = jobBias; - selectedContextId = j; - preemptReason = "higher bias job found"; - 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. + if (selectedContext != null) { + selectedContext.newJob = nextPending; + preferredUidOnly.remove(selectedContext); } } final PackageStats packageStats = getPkgStatsLocked( nextPending.getSourceUserId(), nextPending.getSourcePackageName()); - if (selectedContextId != -1) { - contextIdToJobMap[selectedContextId] = nextPending; - slotChanged[selectedContextId] = true; - preemptReasonCodeForContext[selectedContextId] = preemptReasonCode; - preemptReasonForContext[selectedContextId] = preemptReason; + if (selectedContext != null) { + changed.add(selectedContext); packageStats.adjustStagedCount(true, nextPending.shouldTreatAsExpeditedJob()); } if (startingJob) { // Increase the counters when we're going to start a job. - workTypeForContext[selectedContextId] = workType; - mWorkCountTracker.stageJob(workType, allWorkTypes); + mWorkCountTracker.stageJob(selectedContext.newWorkType, allWorkTypes); mActivePkgStats.add( nextPending.getSourceUserId(), nextPending.getSourcePackageName(), packageStats); @@ -714,37 +752,52 @@ class JobConcurrencyManager { Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString()); } - for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { - boolean preservePreferredUid = false; - if (slotChanged[i]) { - JobStatus js = activeServices.get(i).getRunningJobLocked(); - if (js != null) { - if (DEBUG) { - Slog.d(TAG, "preempting job: " - + activeServices.get(i).getRunningJobLocked()); - } - // preferredUid will be set to uid of currently running job. - activeServices.get(i).cancelExecutingJobLocked( - preemptReasonCodeForContext[i], - JobParameters.INTERNAL_STOP_REASON_PREEMPT, preemptReasonForContext[i]); - // Only preserve the UID if we're preempting for the same UID. If we're stopping - // the job because something is pending (eg. EJs), then we shouldn't preserve - // the UID. - preservePreferredUid = - preemptReasonCodeForContext[i] == JobParameters.STOP_REASON_PREEMPT; - } else { - final JobStatus pendingJob = contextIdToJobMap[i]; - if (DEBUG) { - Slog.d(TAG, "About to run job on context " - + i + ", job: " + pendingJob); - } - startJobLocked(activeServices.get(i), pendingJob, workTypeForContext[i]); + for (int c = changed.size() - 1; c >= 0; --c) { + final ContextAssignment assignment = changed.valueAt(c); + final JobStatus js = assignment.context.getRunningJobLocked(); + if (js != null) { + if (DEBUG) { + Slog.d(TAG, "preempting job: " + js); } + // preferredUid will be set to uid of currently running job. + assignment.context.cancelExecutingJobLocked( + assignment.preemptReasonCode, + JobParameters.INTERNAL_STOP_REASON_PREEMPT, assignment.preemptReason); + } else { + final JobStatus pendingJob = assignment.newJob; + if (DEBUG) { + Slog.d(TAG, "About to run job on context " + + assignment.context.getId() + ", job: " + pendingJob); + } + startJobLocked(assignment.context, pendingJob, assignment.newWorkType); } - if (!preservePreferredUid) { - activeServices.get(i).clearPreferredUid(); - } + + assignment.clear(); + mContextAssignmentPool.release(assignment); } + for (int s = stoppable.size() - 1; s >= 0; --s) { + final ContextAssignment assignment = stoppable.valueAt(s); + assignment.context.clearPreferredUid(); + assignment.clear(); + mContextAssignmentPool.release(assignment); + } + for (int p = preferredUidOnly.size() - 1; p >= 0; --p) { + final ContextAssignment assignment = preferredUidOnly.valueAt(p); + assignment.context.clearPreferredUid(); + assignment.clear(); + mContextAssignmentPool.release(assignment); + } + for (int i = idle.size() - 1; i >= 0; --i) { + final ContextAssignment assignment = idle.valueAt(i); + mIdleContexts.add(assignment.context); + assignment.context.clearPreferredUid(); + assignment.clear(); + mContextAssignmentPool.release(assignment); + } + changed.clear(); + idle.clear(); + stoppable.clear(); + preferredUidOnly.clear(); mWorkCountTracker.resetStagingCount(); mActivePkgStats.forEach(mPackageStatsStagingCountClearer); noteConcurrency(); @@ -787,7 +840,7 @@ class JobConcurrencyManager { @GuardedBy("mLock") private void stopLongRunningJobsLocked(@NonNull String debugReason) { - for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; ++i) { + for (int i = 0; i < mActiveServices.size(); ++i) { final JobServiceContext jsc = mActiveServices.get(i); final JobStatus jobStatus = jsc.getRunningJobLocked(); @@ -931,6 +984,8 @@ class JobConcurrencyManager { } } else { mRunningJobs.add(jobStatus); + mActiveServices.add(worker); + mIdleContexts.remove(worker); mWorkCountTracker.onJobStarted(workType); packageStats.adjustRunningCount(true, jobStatus.shouldTreatAsExpeditedJob()); mActivePkgStats.add( @@ -950,6 +1005,8 @@ class JobConcurrencyManager { @WorkType final int workType) { mWorkCountTracker.onJobFinished(workType); mRunningJobs.remove(jobStatus); + mActiveServices.remove(worker); + mIdleContexts.add(worker); final PackageStats packageStats = mActivePkgStats.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); if (packageStats == null) { @@ -1093,7 +1150,7 @@ class JobConcurrencyManager { /** * Returns {@code null} if the job can continue running and a non-null String if the job should * be stopped. The non-null String details the reason for stopping the job. A job will generally - * be stopped if there similar job types waiting to be run and stopping this job would allow + * be stopped if there are similar job types waiting to be run and stopping this job would allow * another job to run, or if system state suggests the job should stop. */ @Nullable @@ -1202,6 +1259,14 @@ class JobConcurrencyManager { return foundSome; } + @NonNull + private JobServiceContext createNewJobServiceContext() { + return new JobServiceContext(mService, this, + IBatteryStats.Stub.asInterface( + ServiceManager.getService(BatteryStats.SERVICE_NAME)), + mService.mJobPackageTracker, mContext.getMainLooper()); + } + @GuardedBy("mLock") private String printPendingQueueLocked() { StringBuilder s = new StringBuilder("Pending queue: "); @@ -1327,10 +1392,13 @@ class JobConcurrencyManager { } @GuardedBy("mLock") - void dumpActiveJobsLocked(IndentingPrintWriter pw, Predicate predicate, + void dumpContextInfoLocked(IndentingPrintWriter pw, Predicate predicate, long nowElapsed, long nowUptime) { pw.println("Active jobs:"); pw.increaseIndent(); + if (mActiveServices.size() == 0) { + pw.println("N/A"); + } for (int i = 0; i < mActiveServices.size(); i++) { JobServiceContext jsc = mActiveServices.get(i); final JobStatus job = jsc.getRunningJobLocked(); @@ -1339,7 +1407,8 @@ class JobConcurrencyManager { continue; } - pw.print("Slot #"); pw.print(i); pw.print(": "); + pw.print("Slot #"); pw.print(i); + pw.print("(ID="); pw.print(jsc.getId()); pw.print("): "); jsc.dumpLocked(pw, nowElapsed); if (job != null) { @@ -1361,6 +1430,19 @@ class JobConcurrencyManager { } } pw.decreaseIndent(); + + pw.println(); + pw.print("Idle contexts ("); + pw.print(mIdleContexts.size()); + pw.println("):"); + pw.increaseIndent(); + for (int i = 0; i < mIdleContexts.size(); i++) { + JobServiceContext jsc = mIdleContexts.valueAt(i); + + pw.print("ID="); pw.print(jsc.getId()); pw.print(": "); + jsc.dumpLocked(pw, nowElapsed); + } + pw.decreaseIndent(); } public void dumpProtoLocked(ProtoOutputStream proto, long tag, long now, long nowRealtime) { @@ -1989,4 +2071,26 @@ class JobConcurrencyManager { pw.println("}"); } } + + private static final class ContextAssignment { + public JobServiceContext context; + public int preferredUid = JobServiceContext.NO_PREFERRED_UID; + public int workType = WORK_TYPE_NONE; + public String preemptReason; + public int preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED; + public String shouldStopJobReason; + public JobStatus newJob; + public int newWorkType = WORK_TYPE_NONE; + + void clear() { + context = null; + preferredUid = JobServiceContext.NO_PREFERRED_UID; + workType = WORK_TYPE_NONE; + preemptReason = null; + preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED; + shouldStopJobReason = null; + newJob = null; + newWorkType = WORK_TYPE_NONE; + } + } } 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 2028be7382389..0b8162a3df956 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -3770,7 +3770,7 @@ public class JobSchedulerService extends com.android.server.SystemService pw.decreaseIndent(); pw.println(); - mConcurrencyManager.dumpActiveJobsLocked(pw, predicate, nowElapsed, nowUptime); + mConcurrencyManager.dumpContextInfoLocked(pw, predicate, nowElapsed, nowUptime); pw.println(); boolean recentPrinted = false; 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 00d1bfffd9a66..54e0a4c6663ce 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -433,6 +433,10 @@ public final class JobServiceContext implements ServiceConnection { mPreferredUid = NO_PREFERRED_UID; } + int getId() { + return hashCode(); + } + long getExecutionStartTimeElapsed() { return mExecutionStartTimeElapsed; }