Merge "Make job execution limits flexible." into sc-dev

This commit is contained in:
Kweku Adams
2021-02-18 22:16:35 +00:00
committed by Android (Google) Code Review
8 changed files with 445 additions and 66 deletions

View File

@@ -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.
*

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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;
// 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 @@ 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;

View File

@@ -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,

View File

@@ -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());