Merge "Make job execution limits flexible." into sc-dev
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 930415f..b7a3f10 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 @@
* 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 @@
* 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 @@
* 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 164781a..af97715 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 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
+ 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 @@
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 @@
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 fdbc086..00fc937 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 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 @@
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 @@
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 @@
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 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 @@
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 @@
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 @@
* 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 @@
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 0aca246..be91947 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 @@
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 @@
/** 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 @@
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 @@
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 @@
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 @@
}
@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 @@
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 @@
}
@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 @@
}
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 @@
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 @@
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 @@
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 d249f2a..14484ff 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 @@
*/
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 f2d10ac..2196b16 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.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 @@
/** 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;
+ // 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());
}
- if (jobStatus.shouldTreatAsExpeditedJob()) {
- return jobStatus.getStandbyBucket() == RESTRICTED_INDEX
- ? JobServiceContext.DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS
- : JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS;
+
+ // Expedited job.
+ if (mChargeTracker.isCharging()) {
+ return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
}
- return getRemainingExecutionTimeLocked(jobStatus);
+ 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 @@
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 8099eda..775276b 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 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 @@
.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 c4c9173..b72121f 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.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 @@
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 @@
// 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 @@
}
@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 @@
}
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 @@
}
}
+ @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 @@
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());