Loading apex/jobscheduler/framework/java/android/app/job/JobInfo.java +19 −2 Original line number Diff line number Diff line Loading @@ -59,6 +59,12 @@ import java.util.Objects; * constraint on the JobInfo object that you are creating. Otherwise, the builder would throw an * exception when building. From Android version {@link Build.VERSION_CODES#Q} and onwards, it is * valid to schedule jobs with no constraints. * <p> In Android version {@link Build.VERSION_CODES#LOLLIPOP}, jobs had a maximum execution time * of one minute. Starting with Android version {@link Build.VERSION_CODES#M} and ending with * Android version {@link Build.VERSION_CODES#R}, jobs had a maximum execution time of 10 minutes. * Starting from Android version {@link Build.VERSION_CODES#S}, jobs will still be stopped after * 10 minutes if the system is busy or needs the resources, but if not, jobs may continue running * longer than 10 minutes. */ public class JobInfo implements Parcelable { private static String TAG = "JobInfo"; Loading Loading @@ -1461,11 +1467,13 @@ public class JobInfo implements Parcelable { * possible with stronger guarantees than regular jobs. These "expedited" jobs will: * <ol> * <li>Run as soon as possible</li> * <li>Be exempted from Doze and battery saver restrictions</li> * <li>Be less restricted during Doze and battery saver</li> * <li>Have network access</li> * <li>Less likely to be killed than regular jobs</li> * <li>Be less likely to be killed than regular jobs</li> * <li>Be subject to background location throttling</li> * </ol> * * <p> * Since these jobs have stronger guarantees than regular jobs, they will be subject to * stricter quotas. As long as an app has available expedited quota, jobs scheduled with * this set to true will run with these guarantees. If an app has run out of available Loading @@ -1475,9 +1483,18 @@ public class JobInfo implements Parcelable { * will immediately return {@link JobScheduler#RESULT_FAILURE} if the app does not have * available quota (and the job will not be successfully scheduled). * * <p> * Expedited jobs may only set network, storage-not-low, and persistence constraints. * No other constraints are allowed. * * <p> * Assuming all constraints remain satisfied (including ideal system load conditions), * expedited jobs are guaranteed to have a minimum allowed runtime of 1 minute. If your * app has remaining expedited job quota, then the expedited job <i>may</i> potentially run * longer until remaining quota is used up. Just like with regular jobs, quota is not * consumed while the app is on top and visible to the user. * * <p> * Note: Even though expedited jobs are meant to run as soon as possible, they may be * deferred if the system is under heavy load or requested constraints are not satisfied. * Loading apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +171 −13 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.UserSwitchObserver; Loading Loading @@ -80,6 +81,8 @@ class JobConcurrencyManager { static final int WORK_TYPE_BGUSER = 1 << 3; @VisibleForTesting static final int NUM_WORK_TYPES = 4; private static final int ALL_WORK_TYPES = WORK_TYPE_TOP | WORK_TYPE_EJ | WORK_TYPE_BG | WORK_TYPE_BGUSER; @IntDef(prefix = {"WORK_TYPE_"}, flag = true, value = { WORK_TYPE_NONE, Loading @@ -92,6 +95,23 @@ class JobConcurrencyManager { public @interface WorkType { } private static String workTypeToString(@WorkType int workType) { switch (workType) { case WORK_TYPE_NONE: return "NONE"; case WORK_TYPE_TOP: return "TOP"; case WORK_TYPE_EJ: return "EJ"; case WORK_TYPE_BG: return "BG"; case WORK_TYPE_BGUSER: return "BGUSER"; default: return "WORK(" + workType + ")"; } } private final Object mLock; private final JobSchedulerService mService; private final Context mContext; Loading Loading @@ -182,10 +202,16 @@ class JobConcurrencyManager { int[] mRecycledWorkTypeForContext = new int[MAX_JOB_CONTEXTS_COUNT]; String[] mRecycledPreemptReasonForContext = new String[MAX_JOB_CONTEXTS_COUNT]; String[] mRecycledShouldStopJobReason = new String[MAX_JOB_CONTEXTS_COUNT]; private final ArraySet<JobStatus> mRunningJobs = new ArraySet<>(); private final WorkCountTracker mWorkCountTracker = new WorkCountTracker(); private WorkTypeConfig mWorkTypeConfig = CONFIG_LIMITS_SCREEN_OFF.normal; /** Wait for this long after screen off before adjusting the job concurrency. */ private long mScreenOffAdjustmentDelayMs = DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS; Loading Loading @@ -353,23 +379,22 @@ class JobConcurrencyManager { final WorkConfigLimitsPerMemoryTrimLevel workConfigs = mEffectiveInteractiveState ? CONFIG_LIMITS_SCREEN_ON : CONFIG_LIMITS_SCREEN_OFF; WorkTypeConfig workTypeConfig; switch (mLastMemoryTrimLevel) { case ProcessStats.ADJ_MEM_FACTOR_MODERATE: workTypeConfig = workConfigs.moderate; mWorkTypeConfig = workConfigs.moderate; break; case ProcessStats.ADJ_MEM_FACTOR_LOW: workTypeConfig = workConfigs.low; mWorkTypeConfig = workConfigs.low; break; case ProcessStats.ADJ_MEM_FACTOR_CRITICAL: workTypeConfig = workConfigs.critical; mWorkTypeConfig = workConfigs.critical; break; default: workTypeConfig = workConfigs.normal; mWorkTypeConfig = workConfigs.normal; break; } mWorkCountTracker.setConfig(workTypeConfig); mWorkCountTracker.setConfig(mWorkTypeConfig); } /** Loading Loading @@ -401,13 +426,20 @@ class JobConcurrencyManager { boolean[] slotChanged = mRecycledSlotChanged; int[] preferredUidForContext = mRecycledPreferredUidForContext; int[] workTypeForContext = mRecycledWorkTypeForContext; String[] preemptReasonForContext = mRecycledPreemptReasonForContext; String[] shouldStopJobReason = mRecycledShouldStopJobReason; updateCounterConfigLocked(); // Reset everything since we'll re-evaluate the current state. mWorkCountTracker.resetCounts(); // Update the priorities of jobs that aren't running, and also count the pending work types. // Do this before the following loop to hopefully reduce the cost of // shouldStopRunningJobLocked(). updateNonRunningPriorities(pendingJobs, true); for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { final JobServiceContext js = mService.mActiveServices.get(i); final JobServiceContext js = activeServices.get(i); final JobStatus status = js.getRunningJobLocked(); if ((contextIdToJobMap[i] = status) != null) { Loading @@ -417,14 +449,13 @@ class JobConcurrencyManager { slotChanged[i] = false; preferredUidForContext[i] = js.getPreferredUid(); preemptReasonForContext[i] = null; shouldStopJobReason[i] = shouldStopRunningJobLocked(js); } if (DEBUG) { Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial")); } // Next, update the job priorities, and also count the pending FG / BG jobs. updateNonRunningPriorities(pendingJobs, true); mWorkCountTracker.onCountDone(); for (int i = 0; i < pendingJobs.size(); i++) { Loading @@ -434,8 +465,6 @@ class JobConcurrencyManager { continue; } // TODO(171305774): make sure HPJs aren't pre-empted and add dedicated contexts for them // Find an available slot for nextPending. The context should be available OR // it should have lowest priority among all running jobs // (sharing the same Uid as nextPending) Loading @@ -444,6 +473,9 @@ class JobConcurrencyManager { int allWorkTypes = getJobWorkTypes(nextPending); int workType = mWorkCountTracker.canJobStart(allWorkTypes); boolean startingJob = false; String preemptReason = null; // 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]; Loading @@ -464,6 +496,15 @@ class JobConcurrencyManager { continue; } if (job.getUid() != nextPending.getUid()) { // Maybe stop the job if it has had its day in the sun. final String reason = shouldStopJobReason[j]; if (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; } continue; } Loading @@ -477,6 +518,7 @@ class JobConcurrencyManager { // the lowest-priority running job minPriorityForPreemption = jobPriority; selectedContextId = j; preemptReason = "higher priority job found"; // In this case, we're just going to preempt a low priority job, we're not // actually starting a job, so don't set startingJob. } Loading @@ -484,6 +526,7 @@ class JobConcurrencyManager { if (selectedContextId != -1) { contextIdToJobMap[selectedContextId] = nextPending; slotChanged[selectedContextId] = true; preemptReasonForContext[selectedContextId] = preemptReason; } if (startingJob) { // Increase the counters when we're going to start a job. Loading @@ -509,7 +552,7 @@ class JobConcurrencyManager { + activeServices.get(i).getRunningJobLocked()); } // preferredUid will be set to uid of currently running job. activeServices.get(i).preemptExecutingJobLocked(); activeServices.get(i).preemptExecutingJobLocked(preemptReasonForContext[i]); preservePreferredUid = true; } else { final JobStatus pendingJob = contextIdToJobMap[i]; Loading Loading @@ -692,6 +735,91 @@ class JobConcurrencyManager { noteConcurrency(); } /** * 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 * another job to run, or if system state suggests the job should stop. */ @Nullable String shouldStopRunningJobLocked(@NonNull JobServiceContext context) { final JobStatus js = context.getRunningJobLocked(); if (js == null) { // This can happen when we try to assign newly found pending jobs to contexts. return null; } if (context.isWithinExecutionGuaranteeTime()) { return null; } // Update config in case memory usage has changed significantly. updateCounterConfigLocked(); @WorkType final int workType = context.getRunningJobWorkType(); // We're over the minimum guaranteed runtime. Stop the job if we're over config limits or // there are pending jobs that could replace this one. if (mRunningJobs.size() > mWorkTypeConfig.getMaxTotal() || mWorkCountTracker.isOverTypeLimit(workType)) { return "too many jobs running"; } final List<JobStatus> pendingJobs = mService.mPendingJobs; final int numPending = pendingJobs.size(); if (numPending == 0) { // All quiet. We can let this job run to completion. return null; } // Only expedited jobs can replace expedited jobs. if (js.shouldTreatAsExpeditedJob()) { // Keep fg/bg user distinction. if (workType == WORK_TYPE_BGUSER) { // For now, let any bg user job replace a bg user expedited job. // TODO: limit to ej once we have dedicated bg user ej slots. if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_BGUSER) > 0) { return "blocking " + workTypeToString(workType) + " queue"; } } else { if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) { return "blocking " + workTypeToString(workType) + " queue"; } } if (mPowerManager.isPowerSaveMode()) { return "battery saver"; } if (mPowerManager.isDeviceIdleMode()) { return "deep doze"; } } // Easy check. If there are pending jobs of the same work type, then we know that // something will replace this. if (mWorkCountTracker.getPendingJobCount(workType) > 0) { return "blocking " + workTypeToString(workType) + " queue"; } // Harder check. We need to see if a different work type can replace this job. int remainingWorkTypes = ALL_WORK_TYPES; for (int i = 0; i < numPending; ++i) { final JobStatus pending = pendingJobs.get(i); final int workTypes = getJobWorkTypes(pending); if ((workTypes & remainingWorkTypes) > 0 && mWorkCountTracker.canJobStart(workTypes, workType) != WORK_TYPE_NONE) { return "blocking other pending jobs"; } remainingWorkTypes = remainingWorkTypes & ~workTypes; if (remainingWorkTypes == 0) { break; } } return null; } @GuardedBy("mLock") private String printPendingQueueLocked() { StringBuilder s = new StringBuilder("Pending queue: "); Loading Loading @@ -1362,10 +1490,40 @@ class JobConcurrencyManager { return WORK_TYPE_NONE; } int canJobStart(int workTypes, @WorkType int replacingWorkType) { final boolean changedNums; int oldNumRunning = mNumRunningJobs.get(replacingWorkType); if (replacingWorkType != WORK_TYPE_NONE && oldNumRunning > 0) { mNumRunningJobs.put(replacingWorkType, oldNumRunning - 1); // Lazy implementation to avoid lots of processing. Best way would be to go // through the whole process of adjusting reservations, but the processing cost // is likely not worth it. mNumUnspecializedRemaining++; changedNums = true; } else { changedNums = false; } final int ret = canJobStart(workTypes); if (changedNums) { mNumRunningJobs.put(replacingWorkType, oldNumRunning); mNumUnspecializedRemaining--; } return ret; } int getPendingJobCount(@WorkType final int workType) { return mNumPendingJobs.get(workType, 0); } int getRunningJobCount(@WorkType final int workType) { return mNumRunningJobs.get(workType, 0); } boolean isOverTypeLimit(@WorkType final int workType) { return getRunningJobCount(workType) > mConfigAbsoluteMaxSlots.get(workType); } public String toString() { StringBuilder sb = new StringBuilder(); Loading apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +73 −3 Original line number Diff line number Diff line Loading @@ -339,6 +339,7 @@ public class JobSchedulerService extends com.android.server.SystemService public void onPropertiesChanged(DeviceConfig.Properties properties) { boolean apiQuotaScheduleUpdated = false; boolean concurrencyUpdated = false; boolean runtimeUpdated = false; for (int controller = 0; controller < mControllers.size(); controller++) { final StateController sc = mControllers.get(controller); sc.prepareForUpdatedConstantsLocked(); Loading Loading @@ -377,6 +378,14 @@ public class JobSchedulerService extends com.android.server.SystemService case Constants.KEY_CONN_PREFETCH_RELAX_FRAC: mConstants.updateConnectivityConstantsLocked(); break; case Constants.KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS: case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS: case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS: if (!runtimeUpdated) { mConstants.updateRuntimeConstantsLocked(); runtimeUpdated = true; } break; default: if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY) && !concurrencyUpdated) { Loading Loading @@ -432,6 +441,11 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = "aq_schedule_return_failure"; private static final String KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = "runtime_free_quota_max_limit_ms"; private static final String KEY_RUNTIME_MIN_GUARANTEE_MS = "runtime_min_guarantee_ms"; private static final String KEY_RUNTIME_MIN_EJ_GUARANTEE_MS = "runtime_min_ej_guarantee_ms"; private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; private static final float DEFAULT_HEAVY_USE_FACTOR = .9f; Loading @@ -445,6 +459,12 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; @VisibleForTesting public static final long DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = 30 * MINUTE_IN_MILLIS; @VisibleForTesting public static final long DEFAULT_RUNTIME_MIN_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS; @VisibleForTesting public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS; /** * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early. Loading Loading @@ -509,6 +529,19 @@ public class JobSchedulerService extends com.android.server.SystemService public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; /** The maximum amount of time we will let a job run for when quota is "free". */ public long RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; /** * The minimum amount of time we try to guarantee regular jobs will run for. */ public long RUNTIME_MIN_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_GUARANTEE_MS; /** * The minimum amount of time we try to guarantee EJs will run for. */ public long RUNTIME_MIN_EJ_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS; private void updateBatchingConstantsLocked() { MIN_READY_NON_ACTIVE_JOBS_COUNT = DeviceConfig.getInt( DeviceConfig.NAMESPACE_JOB_SCHEDULER, Loading Loading @@ -568,6 +601,25 @@ public class JobSchedulerService extends com.android.server.SystemService DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); } private void updateRuntimeConstantsLocked() { DeviceConfig.Properties properties = DeviceConfig.getProperties( DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS); // Make sure min runtime for regular jobs is at least 10 minutes. RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS, properties.getLong( KEY_RUNTIME_MIN_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS)); // Make sure min runtime for expedited jobs is at least one minute. RUNTIME_MIN_EJ_GUARANTEE_MS = Math.max(MINUTE_IN_MILLIS, properties.getLong( KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS)); RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS, properties.getLong(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)); } void dump(IndentingPrintWriter pw) { pw.println("Settings:"); pw.increaseIndent(); Loading @@ -591,6 +643,11 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); pw.print(KEY_RUNTIME_MIN_GUARANTEE_MS, RUNTIME_MIN_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, RUNTIME_MIN_EJ_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) .println(); pw.decreaseIndent(); } Loading Loading @@ -1602,7 +1659,7 @@ public class JobSchedulerService extends com.android.server.SystemService * time of the job to be the time of completion (i.e. the time at which this function is * called). * <p>This could be inaccurate b/c the job can run for as long as * {@link com.android.server.job.JobServiceContext#DEFAULT_EXECUTING_TIMESLICE_MILLIS}, but * {@link Constants#DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS}, but * will lead to underscheduling at least, rather than if we had taken the last execution time * to be the start of the execution. * Loading Loading @@ -2213,11 +2270,24 @@ public class JobSchedulerService extends com.android.server.SystemService return isComponentUsable(job); } /** Returns the minimum amount of time we should let this job run before timing out. */ public long getMinJobExecutionGuaranteeMs(JobStatus job) { synchronized (mLock) { if (job.shouldTreatAsExpeditedJob()) { // Don't guarantee RESTRICTED jobs more than 5 minutes. return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX ? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS : Math.min(mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, 5 * MINUTE_IN_MILLIS); } else { return mConstants.RUNTIME_MIN_GUARANTEE_MS; } } } /** Returns the maximum amount of time this job could run for. */ public long getMaxJobExecutionTimeMs(JobStatus job) { synchronized (mLock) { return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job), JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS); return mQuotaController.getMaxJobExecutionTimeMsLocked(job); } } Loading apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +64 −23 File changed.Preview size limit exceeded, changes collapsed. Show changes apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +2 −0 Original line number Diff line number Diff line Loading @@ -325,6 +325,8 @@ public final class ConnectivityController extends RestrictingController implemen */ private boolean isInsane(JobStatus jobStatus, Network network, NetworkCapabilities capabilities, Constants constants) { // Use the maximum possible time since it gives us an upper bound, even though the job // could end up stopping earlier. final long maxJobExecutionTimeMs = mService.getMaxJobExecutionTimeMs(jobStatus); final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes(); Loading Loading
apex/jobscheduler/framework/java/android/app/job/JobInfo.java +19 −2 Original line number Diff line number Diff line Loading @@ -59,6 +59,12 @@ import java.util.Objects; * constraint on the JobInfo object that you are creating. Otherwise, the builder would throw an * exception when building. From Android version {@link Build.VERSION_CODES#Q} and onwards, it is * valid to schedule jobs with no constraints. * <p> In Android version {@link Build.VERSION_CODES#LOLLIPOP}, jobs had a maximum execution time * of one minute. Starting with Android version {@link Build.VERSION_CODES#M} and ending with * Android version {@link Build.VERSION_CODES#R}, jobs had a maximum execution time of 10 minutes. * Starting from Android version {@link Build.VERSION_CODES#S}, jobs will still be stopped after * 10 minutes if the system is busy or needs the resources, but if not, jobs may continue running * longer than 10 minutes. */ public class JobInfo implements Parcelable { private static String TAG = "JobInfo"; Loading Loading @@ -1461,11 +1467,13 @@ public class JobInfo implements Parcelable { * possible with stronger guarantees than regular jobs. These "expedited" jobs will: * <ol> * <li>Run as soon as possible</li> * <li>Be exempted from Doze and battery saver restrictions</li> * <li>Be less restricted during Doze and battery saver</li> * <li>Have network access</li> * <li>Less likely to be killed than regular jobs</li> * <li>Be less likely to be killed than regular jobs</li> * <li>Be subject to background location throttling</li> * </ol> * * <p> * Since these jobs have stronger guarantees than regular jobs, they will be subject to * stricter quotas. As long as an app has available expedited quota, jobs scheduled with * this set to true will run with these guarantees. If an app has run out of available Loading @@ -1475,9 +1483,18 @@ public class JobInfo implements Parcelable { * will immediately return {@link JobScheduler#RESULT_FAILURE} if the app does not have * available quota (and the job will not be successfully scheduled). * * <p> * Expedited jobs may only set network, storage-not-low, and persistence constraints. * No other constraints are allowed. * * <p> * Assuming all constraints remain satisfied (including ideal system load conditions), * expedited jobs are guaranteed to have a minimum allowed runtime of 1 minute. If your * app has remaining expedited job quota, then the expedited job <i>may</i> potentially run * longer until remaining quota is used up. Just like with regular jobs, quota is not * consumed while the app is on top and visible to the user. * * <p> * Note: Even though expedited jobs are meant to run as soon as possible, they may be * deferred if the system is under heavy load or requested constraints are not satisfied. * Loading
apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +171 −13 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.UserSwitchObserver; Loading Loading @@ -80,6 +81,8 @@ class JobConcurrencyManager { static final int WORK_TYPE_BGUSER = 1 << 3; @VisibleForTesting static final int NUM_WORK_TYPES = 4; private static final int ALL_WORK_TYPES = WORK_TYPE_TOP | WORK_TYPE_EJ | WORK_TYPE_BG | WORK_TYPE_BGUSER; @IntDef(prefix = {"WORK_TYPE_"}, flag = true, value = { WORK_TYPE_NONE, Loading @@ -92,6 +95,23 @@ class JobConcurrencyManager { public @interface WorkType { } private static String workTypeToString(@WorkType int workType) { switch (workType) { case WORK_TYPE_NONE: return "NONE"; case WORK_TYPE_TOP: return "TOP"; case WORK_TYPE_EJ: return "EJ"; case WORK_TYPE_BG: return "BG"; case WORK_TYPE_BGUSER: return "BGUSER"; default: return "WORK(" + workType + ")"; } } private final Object mLock; private final JobSchedulerService mService; private final Context mContext; Loading Loading @@ -182,10 +202,16 @@ class JobConcurrencyManager { int[] mRecycledWorkTypeForContext = new int[MAX_JOB_CONTEXTS_COUNT]; String[] mRecycledPreemptReasonForContext = new String[MAX_JOB_CONTEXTS_COUNT]; String[] mRecycledShouldStopJobReason = new String[MAX_JOB_CONTEXTS_COUNT]; private final ArraySet<JobStatus> mRunningJobs = new ArraySet<>(); private final WorkCountTracker mWorkCountTracker = new WorkCountTracker(); private WorkTypeConfig mWorkTypeConfig = CONFIG_LIMITS_SCREEN_OFF.normal; /** Wait for this long after screen off before adjusting the job concurrency. */ private long mScreenOffAdjustmentDelayMs = DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS; Loading Loading @@ -353,23 +379,22 @@ class JobConcurrencyManager { final WorkConfigLimitsPerMemoryTrimLevel workConfigs = mEffectiveInteractiveState ? CONFIG_LIMITS_SCREEN_ON : CONFIG_LIMITS_SCREEN_OFF; WorkTypeConfig workTypeConfig; switch (mLastMemoryTrimLevel) { case ProcessStats.ADJ_MEM_FACTOR_MODERATE: workTypeConfig = workConfigs.moderate; mWorkTypeConfig = workConfigs.moderate; break; case ProcessStats.ADJ_MEM_FACTOR_LOW: workTypeConfig = workConfigs.low; mWorkTypeConfig = workConfigs.low; break; case ProcessStats.ADJ_MEM_FACTOR_CRITICAL: workTypeConfig = workConfigs.critical; mWorkTypeConfig = workConfigs.critical; break; default: workTypeConfig = workConfigs.normal; mWorkTypeConfig = workConfigs.normal; break; } mWorkCountTracker.setConfig(workTypeConfig); mWorkCountTracker.setConfig(mWorkTypeConfig); } /** Loading Loading @@ -401,13 +426,20 @@ class JobConcurrencyManager { boolean[] slotChanged = mRecycledSlotChanged; int[] preferredUidForContext = mRecycledPreferredUidForContext; int[] workTypeForContext = mRecycledWorkTypeForContext; String[] preemptReasonForContext = mRecycledPreemptReasonForContext; String[] shouldStopJobReason = mRecycledShouldStopJobReason; updateCounterConfigLocked(); // Reset everything since we'll re-evaluate the current state. mWorkCountTracker.resetCounts(); // Update the priorities of jobs that aren't running, and also count the pending work types. // Do this before the following loop to hopefully reduce the cost of // shouldStopRunningJobLocked(). updateNonRunningPriorities(pendingJobs, true); for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { final JobServiceContext js = mService.mActiveServices.get(i); final JobServiceContext js = activeServices.get(i); final JobStatus status = js.getRunningJobLocked(); if ((contextIdToJobMap[i] = status) != null) { Loading @@ -417,14 +449,13 @@ class JobConcurrencyManager { slotChanged[i] = false; preferredUidForContext[i] = js.getPreferredUid(); preemptReasonForContext[i] = null; shouldStopJobReason[i] = shouldStopRunningJobLocked(js); } if (DEBUG) { Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial")); } // Next, update the job priorities, and also count the pending FG / BG jobs. updateNonRunningPriorities(pendingJobs, true); mWorkCountTracker.onCountDone(); for (int i = 0; i < pendingJobs.size(); i++) { Loading @@ -434,8 +465,6 @@ class JobConcurrencyManager { continue; } // TODO(171305774): make sure HPJs aren't pre-empted and add dedicated contexts for them // Find an available slot for nextPending. The context should be available OR // it should have lowest priority among all running jobs // (sharing the same Uid as nextPending) Loading @@ -444,6 +473,9 @@ class JobConcurrencyManager { int allWorkTypes = getJobWorkTypes(nextPending); int workType = mWorkCountTracker.canJobStart(allWorkTypes); boolean startingJob = false; String preemptReason = null; // 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]; Loading @@ -464,6 +496,15 @@ class JobConcurrencyManager { continue; } if (job.getUid() != nextPending.getUid()) { // Maybe stop the job if it has had its day in the sun. final String reason = shouldStopJobReason[j]; if (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; } continue; } Loading @@ -477,6 +518,7 @@ class JobConcurrencyManager { // the lowest-priority running job minPriorityForPreemption = jobPriority; selectedContextId = j; preemptReason = "higher priority job found"; // In this case, we're just going to preempt a low priority job, we're not // actually starting a job, so don't set startingJob. } Loading @@ -484,6 +526,7 @@ class JobConcurrencyManager { if (selectedContextId != -1) { contextIdToJobMap[selectedContextId] = nextPending; slotChanged[selectedContextId] = true; preemptReasonForContext[selectedContextId] = preemptReason; } if (startingJob) { // Increase the counters when we're going to start a job. Loading @@ -509,7 +552,7 @@ class JobConcurrencyManager { + activeServices.get(i).getRunningJobLocked()); } // preferredUid will be set to uid of currently running job. activeServices.get(i).preemptExecutingJobLocked(); activeServices.get(i).preemptExecutingJobLocked(preemptReasonForContext[i]); preservePreferredUid = true; } else { final JobStatus pendingJob = contextIdToJobMap[i]; Loading Loading @@ -692,6 +735,91 @@ class JobConcurrencyManager { noteConcurrency(); } /** * 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 * another job to run, or if system state suggests the job should stop. */ @Nullable String shouldStopRunningJobLocked(@NonNull JobServiceContext context) { final JobStatus js = context.getRunningJobLocked(); if (js == null) { // This can happen when we try to assign newly found pending jobs to contexts. return null; } if (context.isWithinExecutionGuaranteeTime()) { return null; } // Update config in case memory usage has changed significantly. updateCounterConfigLocked(); @WorkType final int workType = context.getRunningJobWorkType(); // We're over the minimum guaranteed runtime. Stop the job if we're over config limits or // there are pending jobs that could replace this one. if (mRunningJobs.size() > mWorkTypeConfig.getMaxTotal() || mWorkCountTracker.isOverTypeLimit(workType)) { return "too many jobs running"; } final List<JobStatus> pendingJobs = mService.mPendingJobs; final int numPending = pendingJobs.size(); if (numPending == 0) { // All quiet. We can let this job run to completion. return null; } // Only expedited jobs can replace expedited jobs. if (js.shouldTreatAsExpeditedJob()) { // Keep fg/bg user distinction. if (workType == WORK_TYPE_BGUSER) { // For now, let any bg user job replace a bg user expedited job. // TODO: limit to ej once we have dedicated bg user ej slots. if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_BGUSER) > 0) { return "blocking " + workTypeToString(workType) + " queue"; } } else { if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) { return "blocking " + workTypeToString(workType) + " queue"; } } if (mPowerManager.isPowerSaveMode()) { return "battery saver"; } if (mPowerManager.isDeviceIdleMode()) { return "deep doze"; } } // Easy check. If there are pending jobs of the same work type, then we know that // something will replace this. if (mWorkCountTracker.getPendingJobCount(workType) > 0) { return "blocking " + workTypeToString(workType) + " queue"; } // Harder check. We need to see if a different work type can replace this job. int remainingWorkTypes = ALL_WORK_TYPES; for (int i = 0; i < numPending; ++i) { final JobStatus pending = pendingJobs.get(i); final int workTypes = getJobWorkTypes(pending); if ((workTypes & remainingWorkTypes) > 0 && mWorkCountTracker.canJobStart(workTypes, workType) != WORK_TYPE_NONE) { return "blocking other pending jobs"; } remainingWorkTypes = remainingWorkTypes & ~workTypes; if (remainingWorkTypes == 0) { break; } } return null; } @GuardedBy("mLock") private String printPendingQueueLocked() { StringBuilder s = new StringBuilder("Pending queue: "); Loading Loading @@ -1362,10 +1490,40 @@ class JobConcurrencyManager { return WORK_TYPE_NONE; } int canJobStart(int workTypes, @WorkType int replacingWorkType) { final boolean changedNums; int oldNumRunning = mNumRunningJobs.get(replacingWorkType); if (replacingWorkType != WORK_TYPE_NONE && oldNumRunning > 0) { mNumRunningJobs.put(replacingWorkType, oldNumRunning - 1); // Lazy implementation to avoid lots of processing. Best way would be to go // through the whole process of adjusting reservations, but the processing cost // is likely not worth it. mNumUnspecializedRemaining++; changedNums = true; } else { changedNums = false; } final int ret = canJobStart(workTypes); if (changedNums) { mNumRunningJobs.put(replacingWorkType, oldNumRunning); mNumUnspecializedRemaining--; } return ret; } int getPendingJobCount(@WorkType final int workType) { return mNumPendingJobs.get(workType, 0); } int getRunningJobCount(@WorkType final int workType) { return mNumRunningJobs.get(workType, 0); } boolean isOverTypeLimit(@WorkType final int workType) { return getRunningJobCount(workType) > mConfigAbsoluteMaxSlots.get(workType); } public String toString() { StringBuilder sb = new StringBuilder(); Loading
apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +73 −3 Original line number Diff line number Diff line Loading @@ -339,6 +339,7 @@ public class JobSchedulerService extends com.android.server.SystemService public void onPropertiesChanged(DeviceConfig.Properties properties) { boolean apiQuotaScheduleUpdated = false; boolean concurrencyUpdated = false; boolean runtimeUpdated = false; for (int controller = 0; controller < mControllers.size(); controller++) { final StateController sc = mControllers.get(controller); sc.prepareForUpdatedConstantsLocked(); Loading Loading @@ -377,6 +378,14 @@ public class JobSchedulerService extends com.android.server.SystemService case Constants.KEY_CONN_PREFETCH_RELAX_FRAC: mConstants.updateConnectivityConstantsLocked(); break; case Constants.KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS: case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS: case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS: if (!runtimeUpdated) { mConstants.updateRuntimeConstantsLocked(); runtimeUpdated = true; } break; default: if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY) && !concurrencyUpdated) { Loading Loading @@ -432,6 +441,11 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = "aq_schedule_return_failure"; private static final String KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = "runtime_free_quota_max_limit_ms"; private static final String KEY_RUNTIME_MIN_GUARANTEE_MS = "runtime_min_guarantee_ms"; private static final String KEY_RUNTIME_MIN_EJ_GUARANTEE_MS = "runtime_min_ej_guarantee_ms"; private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; private static final float DEFAULT_HEAVY_USE_FACTOR = .9f; Loading @@ -445,6 +459,12 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; @VisibleForTesting public static final long DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = 30 * MINUTE_IN_MILLIS; @VisibleForTesting public static final long DEFAULT_RUNTIME_MIN_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS; @VisibleForTesting public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS; /** * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early. Loading Loading @@ -509,6 +529,19 @@ public class JobSchedulerService extends com.android.server.SystemService public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; /** The maximum amount of time we will let a job run for when quota is "free". */ public long RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; /** * The minimum amount of time we try to guarantee regular jobs will run for. */ public long RUNTIME_MIN_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_GUARANTEE_MS; /** * The minimum amount of time we try to guarantee EJs will run for. */ public long RUNTIME_MIN_EJ_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS; private void updateBatchingConstantsLocked() { MIN_READY_NON_ACTIVE_JOBS_COUNT = DeviceConfig.getInt( DeviceConfig.NAMESPACE_JOB_SCHEDULER, Loading Loading @@ -568,6 +601,25 @@ public class JobSchedulerService extends com.android.server.SystemService DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); } private void updateRuntimeConstantsLocked() { DeviceConfig.Properties properties = DeviceConfig.getProperties( DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS); // Make sure min runtime for regular jobs is at least 10 minutes. RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS, properties.getLong( KEY_RUNTIME_MIN_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS)); // Make sure min runtime for expedited jobs is at least one minute. RUNTIME_MIN_EJ_GUARANTEE_MS = Math.max(MINUTE_IN_MILLIS, properties.getLong( KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS)); RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS, properties.getLong(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)); } void dump(IndentingPrintWriter pw) { pw.println("Settings:"); pw.increaseIndent(); Loading @@ -591,6 +643,11 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); pw.print(KEY_RUNTIME_MIN_GUARANTEE_MS, RUNTIME_MIN_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, RUNTIME_MIN_EJ_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) .println(); pw.decreaseIndent(); } Loading Loading @@ -1602,7 +1659,7 @@ public class JobSchedulerService extends com.android.server.SystemService * time of the job to be the time of completion (i.e. the time at which this function is * called). * <p>This could be inaccurate b/c the job can run for as long as * {@link com.android.server.job.JobServiceContext#DEFAULT_EXECUTING_TIMESLICE_MILLIS}, but * {@link Constants#DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS}, but * will lead to underscheduling at least, rather than if we had taken the last execution time * to be the start of the execution. * Loading Loading @@ -2213,11 +2270,24 @@ public class JobSchedulerService extends com.android.server.SystemService return isComponentUsable(job); } /** Returns the minimum amount of time we should let this job run before timing out. */ public long getMinJobExecutionGuaranteeMs(JobStatus job) { synchronized (mLock) { if (job.shouldTreatAsExpeditedJob()) { // Don't guarantee RESTRICTED jobs more than 5 minutes. return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX ? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS : Math.min(mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, 5 * MINUTE_IN_MILLIS); } else { return mConstants.RUNTIME_MIN_GUARANTEE_MS; } } } /** Returns the maximum amount of time this job could run for. */ public long getMaxJobExecutionTimeMs(JobStatus job) { synchronized (mLock) { return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job), JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS); return mQuotaController.getMaxJobExecutionTimeMsLocked(job); } } Loading
apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +64 −23 File changed.Preview size limit exceeded, changes collapsed. Show changes
apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +2 −0 Original line number Diff line number Diff line Loading @@ -325,6 +325,8 @@ public final class ConnectivityController extends RestrictingController implemen */ private boolean isInsane(JobStatus jobStatus, Network network, NetworkCapabilities capabilities, Constants constants) { // Use the maximum possible time since it gives us an upper bound, even though the job // could end up stopping earlier. final long maxJobExecutionTimeMs = mService.getMaxJobExecutionTimeMs(jobStatus); final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes(); Loading