diff options
8 files changed, 447 insertions, 68 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 930415fcd8fd..b7a3f1083176 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -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"; @@ -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 @@ -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. * 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 164781a250b7..af9771553063 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -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; @@ -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, @@ -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; @@ -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; @@ -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); } /** @@ -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) { @@ -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++) { @@ -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) @@ -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]; @@ -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; } @@ -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. } @@ -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. @@ -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]; @@ -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: "); @@ -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(); 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 fdbc0864a59d..00fc937516ae 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -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(); @@ -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) { @@ -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; @@ -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. @@ -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, @@ -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(); @@ -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(); } @@ -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. * @@ -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); } } 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 0aca2461b41c..be91947b0445 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -17,9 +17,9 @@ package com.android.server.job; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; -import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.IJobCallback; import android.app.job.IJobService; @@ -76,14 +76,6 @@ public final class JobServiceContext implements ServiceConnection { private static final boolean DEBUG_STANDBY = JobSchedulerService.DEBUG_STANDBY; private static final String TAG = "JobServiceContext"; - /** Amount of time a job is allowed to execute for before being considered timed-out. */ - public static final long DEFAULT_EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins. - /** - * Amount of time a RESTRICTED expedited job is allowed to execute for before being considered - * timed-out. - */ - public static final long DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS = - DEFAULT_EXECUTING_TIMESLICE_MILLIS / 2; /** Amount of time the JobScheduler waits for the initial service launch+bind. */ private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000; /** Amount of time the JobScheduler will wait for a response from an app for a message. */ @@ -110,6 +102,7 @@ public final class JobServiceContext implements ServiceConnection { /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */ private final JobCompletedListener mCompletedListener; private final JobConcurrencyManager mJobConcurrencyManager; + private final JobSchedulerService mService; /** Used for service binding, etc. */ private final Context mContext; private final Object mLock; @@ -149,6 +142,13 @@ public final class JobServiceContext implements ServiceConnection { private long mExecutionStartTimeElapsed; /** Track when job will timeout. */ private long mTimeoutElapsed; + /** + * The minimum amount of time the context will allow the job to run before checking whether to + * stop it or not. + */ + private long mMinExecutionGuaranteeMillis; + /** The absolute maximum amount of time the job can run */ + private long mMaxExecutionTimeMillis; // Debugging: reason this job was last stopped. public String mStoppedReason; @@ -190,6 +190,7 @@ public final class JobServiceContext implements ServiceConnection { IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper) { mContext = service.getContext(); mLock = service.getLock(); + mService = service; mBatteryStats = batteryStats; mJobPackageTracker = tracker; mCallbackHandler = new JobServiceHandler(looper); @@ -239,6 +240,9 @@ public final class JobServiceContext implements ServiceConnection { isDeadlineExpired, job.shouldTreatAsExpeditedJob(), triggeredUris, triggeredAuthorities, job.network); mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); + mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job); + mMaxExecutionTimeMillis = + Math.max(mService.getMaxJobExecutionTimeMs(job), mMinExecutionGuaranteeMillis); final long whenDeferred = job.getWhenStandbyDeferred(); if (whenDeferred > 0) { @@ -352,8 +356,8 @@ public final class JobServiceContext implements ServiceConnection { } @GuardedBy("mLock") - void preemptExecutingJobLocked() { - doCancelLocked(JobParameters.REASON_PREEMPT, "cancelled due to preemption"); + void preemptExecutingJobLocked(@NonNull String reason) { + doCancelLocked(JobParameters.REASON_PREEMPT, reason); } int getPreferredUid() { @@ -372,6 +376,11 @@ public final class JobServiceContext implements ServiceConnection { return mTimeoutElapsed; } + boolean isWithinExecutionGuaranteeTime() { + return mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis + < sElapsedRealtimeClock.millis(); + } + @GuardedBy("mLock") boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId, String reason) { @@ -607,7 +616,7 @@ public final class JobServiceContext implements ServiceConnection { } @GuardedBy("mLock") - void doCancelLocked(int arg1, String debugReason) { + private void doCancelLocked(int stopReasonCode, String debugReason) { if (mVerb == VERB_FINISHED) { if (DEBUG) { Slog.d(TAG, @@ -615,8 +624,8 @@ public final class JobServiceContext implements ServiceConnection { } return; } - mParams.setStopReason(arg1, debugReason); - if (arg1 == JobParameters.REASON_PREEMPT) { + mParams.setStopReason(stopReasonCode, debugReason); + if (stopReasonCode == JobParameters.REASON_PREEMPT) { mPreferredUid = mRunningJob != null ? mRunningJob.getUid() : NO_PREFERRED_UID; } @@ -767,11 +776,30 @@ public final class JobServiceContext implements ServiceConnection { closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping"); break; case VERB_EXECUTING: - // Not an error - client ran out of time. - Slog.i(TAG, "Client timed out while executing (no jobFinished received), " + - "sending onStop: " + getRunningJobNameLocked()); - mParams.setStopReason(JobParameters.REASON_TIMEOUT, "client timed out"); - sendStopMessageLocked("timeout while executing"); + final long latestStopTimeElapsed = + mExecutionStartTimeElapsed + mMaxExecutionTimeMillis; + final long nowElapsed = sElapsedRealtimeClock.millis(); + if (nowElapsed >= latestStopTimeElapsed) { + // Not an error - client ran out of time. + Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + + " Sending onStop: " + getRunningJobNameLocked()); + mParams.setStopReason(JobParameters.REASON_TIMEOUT, "client timed out"); + sendStopMessageLocked("timeout while executing"); + } else { + // We've given the app the minimum execution time. See if we should stop it or + // let it continue running + final String reason = mJobConcurrencyManager.shouldStopRunningJobLocked(this); + if (reason != null) { + Slog.i(TAG, "Stopping client after min execution time: " + + getRunningJobNameLocked() + " because " + reason); + mParams.setStopReason(JobParameters.REASON_TIMEOUT, reason); + sendStopMessageLocked(reason); + } else { + Slog.i(TAG, "Letting " + getRunningJobNameLocked() + + " continue to run past min execution time"); + scheduleOpTimeOutLocked(); + } + } break; default: Slog.e(TAG, "Handling timeout for an invalid job state: " @@ -878,10 +906,16 @@ public final class JobServiceContext implements ServiceConnection { final long timeoutMillis; switch (mVerb) { case VERB_EXECUTING: - timeoutMillis = mRunningJob.shouldTreatAsExpeditedJob() - && mRunningJob.getStandbyBucket() == RESTRICTED_INDEX - ? DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS - : DEFAULT_EXECUTING_TIMESLICE_MILLIS; + final long earliestStopTimeElapsed = + mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis; + final long latestStopTimeElapsed = + mExecutionStartTimeElapsed + mMaxExecutionTimeMillis; + final long nowElapsed = sElapsedRealtimeClock.millis(); + if (nowElapsed < earliestStopTimeElapsed) { + timeoutMillis = earliestStopTimeElapsed - nowElapsed; + } else { + timeoutMillis = latestStopTimeElapsed - nowElapsed; + } break; case VERB_BINDING: @@ -925,6 +959,13 @@ public final class JobServiceContext implements ServiceConnection { pw.print(", timeout at: "); TimeUtils.formatDuration(mTimeoutElapsed - nowElapsed, pw); pw.println(); + pw.print("Remaining execution limits: ["); + TimeUtils.formatDuration( + (mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis) - nowElapsed, pw); + pw.print(", "); + TimeUtils.formatDuration( + (mExecutionStartTimeElapsed + mMaxExecutionTimeMillis) - nowElapsed, pw); + pw.println("]"); pw.decreaseIndent(); } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index d249f2ae813c..14484ff441ca 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -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(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index f2d10ac0f7d7..2196b16e0846 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -72,7 +72,6 @@ import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; import com.android.server.job.ConstantsProto; import com.android.server.job.JobSchedulerService; -import com.android.server.job.JobServiceContext; import com.android.server.job.StateControllerProto; import com.android.server.usage.AppStandbyInternal; import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; @@ -770,18 +769,38 @@ public final class QuotaController extends StateController { /** Returns the maximum amount of time this job could run for. */ public long getMaxJobExecutionTimeMsLocked(@NonNull final JobStatus jobStatus) { - // If quota is currently "free", then the job can run for the full amount of time. - if (mChargeTracker.isCharging() - || isTopStartedJobLocked(jobStatus) - || isUidInForeground(jobStatus.getSourceUid())) { - return JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS; - } - if (jobStatus.shouldTreatAsExpeditedJob()) { - return jobStatus.getStandbyBucket() == RESTRICTED_INDEX - ? JobServiceContext.DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS - : JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS; - } - return getRemainingExecutionTimeLocked(jobStatus); + // Need to look at current proc state as well in the case where the job hasn't started yet. + final boolean isTop = mActivityManagerInternal + .getUidProcessState(jobStatus.getSourceUid()) <= ActivityManager.PROCESS_STATE_TOP; + + if (!jobStatus.shouldTreatAsExpeditedJob()) { + // If quota is currently "free", then the job can run for the full amount of time. + if (mChargeTracker.isCharging() + || isTop + || isTopStartedJobLocked(jobStatus) + || isUidInForeground(jobStatus.getSourceUid())) { + return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; + } + return getTimeUntilQuotaConsumedLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); + } + + // Expedited job. + if (mChargeTracker.isCharging()) { + return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; + } + if (isTop || isTopStartedJobLocked(jobStatus)) { + return Math.max(mEJLimitsMs[ACTIVE_INDEX] / 2, + getTimeUntilEJQuotaConsumedLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName())); + } + if (isUidInForeground(jobStatus.getSourceUid())) { + return Math.max(mEJLimitsMs[WORKING_INDEX] / 2, + getTimeUntilEJQuotaConsumedLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName())); + } + return getTimeUntilEJQuotaConsumedLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); } /** @return true if the job is within expedited job quota. */ @@ -3577,8 +3596,8 @@ public final class QuotaController extends StateController { mEJLimitsMs[RARE_INDEX] = newRareLimitMs; mShouldReevaluateConstraints = true; } - // The limit must be in the range [0 minutes, rare limit]. - long newRestrictedLimitMs = Math.max(0, + // The limit must be in the range [5 minutes, rare limit]. + long newRestrictedLimitMs = Math.max(5 * MINUTE_IN_MILLIS, Math.min(newRareLimitMs, EJ_LIMIT_RESTRICTED_MS)); if (mEJLimitsMs[RESTRICTED_INDEX] != newRestrictedLimitMs) { mEJLimitsMs[RESTRICTED_INDEX] = newRestrictedLimitMs; diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java index 8099eda9a0af..775276bd03f6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java @@ -63,7 +63,6 @@ import android.util.DataUnit; import com.android.server.LocalServices; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobSchedulerService.Constants; -import com.android.server.job.JobServiceContext; import com.android.server.net.NetworkPolicyManagerInternal; import org.junit.Before; @@ -144,8 +143,7 @@ public class ConnectivityControllerTest { .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); final ConnectivityController controller = new ConnectivityController(mService); - when(mService.getMaxJobExecutionTimeMs(any())) - .thenReturn(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS); + when(mService.getMaxJobExecutionTimeMs(any())).thenReturn(10 * 60_000L); // Slow network is too slow assertFalse(controller.isSatisfied(createJobStatus(job), net, diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index c4c9173536ad..b72121f096ba 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -82,8 +82,6 @@ import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; import com.android.server.job.JobSchedulerService; -import com.android.server.job.JobSchedulerService.Constants; -import com.android.server.job.JobServiceContext; import com.android.server.job.JobStore; import com.android.server.job.controllers.QuotaController.ExecutionStats; import com.android.server.job.controllers.QuotaController.QcConstants; @@ -123,6 +121,7 @@ public class QuotaControllerTest { private BroadcastReceiver mChargingReceiver; private QuotaController mQuotaController; private QuotaController.QcConstants mQcConstants; + private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants(); private int mSourceUid; private PowerAllowlistInternal.TempAllowlistChangeListener mTempAllowlistListener; private IUidObserver mUidObserver; @@ -158,7 +157,7 @@ public class QuotaControllerTest { // Called in StateController constructor. when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); - when(mJobSchedulerService.getConstants()).thenReturn(mock(Constants.class)); + when(mJobSchedulerService.getConstants()).thenReturn(mConstants); // Called in QuotaController constructor. IActivityManager activityManager = ActivityManager.getService(); spyOn(activityManager); @@ -1282,23 +1281,23 @@ public class QuotaControllerTest { } @Test - public void testGetMaxJobExecutionTimeLocked() { + public void testGetMaxJobExecutionTimeLocked_Regular() { mQuotaController.saveTimingSession(0, SOURCE_PACKAGE, createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked", 0); - job.setStandbyBucket(RARE_INDEX); + setStandbyBucket(RARE_INDEX, job); setCharging(); synchronized (mQuotaController.mLock) { - assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS, + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mQuotaController.getMaxJobExecutionTimeMsLocked((job))); } setDischarging(); setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); synchronized (mQuotaController.mLock) { - assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS, + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mQuotaController.getMaxJobExecutionTimeMsLocked((job))); } @@ -1310,7 +1309,7 @@ public class QuotaControllerTest { } setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); synchronized (mQuotaController.mLock) { - assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS, + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mQuotaController.getMaxJobExecutionTimeMsLocked((job))); mQuotaController.maybeStopTrackingJobLocked(job, null, false); } @@ -1322,6 +1321,81 @@ public class QuotaControllerTest { } } + @Test + public void testGetMaxJobExecutionTimeLocked_EJ() { + final long timeUsedMs = 3 * MINUTE_IN_MILLIS; + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS), + timeUsedMs, 5), true); + JobStatus job = createExpeditedJobStatus("testGetMaxJobExecutionTimeLocked_EJ", 0); + setStandbyBucket(RARE_INDEX, job); + mQuotaController.maybeStartTrackingJobLocked(job, null); + + setCharging(); + synchronized (mQuotaController.mLock) { + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + setDischarging(); + setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null, false); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Test used quota rolling out of window. + synchronized (mQuotaController.mLock) { + mQuotaController.clearAppStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE); + } + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS, + timeUsedMs, 5), true); + + setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job, null); + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null, false); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + } + /** * Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket * window. @@ -2508,7 +2582,7 @@ public class QuotaControllerTest { assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[WORKING_INDEX]); assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[FREQUENT_INDEX]); assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[RARE_INDEX]); - assertEquals(0, mQuotaController.getEJLimitsMs()[RESTRICTED_INDEX]); + assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[RESTRICTED_INDEX]); assertEquals(0, mQuotaController.getEjLimitSpecialAdditionMs()); assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJLimitWindowSizeMs()); assertEquals(1, mQuotaController.getEJTopAppTimeChunkSizeMs()); |