Merge "Expose job priority API."

This commit is contained in:
Kweku Adams
2021-11-03 22:33:45 +00:00
committed by Android (Google) Code Review
11 changed files with 735 additions and 109 deletions

View File

@@ -85,6 +85,17 @@ public class JobInfo implements Parcelable {
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long DISALLOW_DEADLINES_FOR_PREFETCH_JOBS = 194532703L;
/**
* Whether to throw an exception when an app provides an invalid priority value via
* {@link Builder#setPriority(int)}. Legacy apps may be incorrectly using the API and
* so the call will silently fail for them if they continue using the API.
*
* @hide
*/
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long THROW_ON_INVALID_PRIORITY_VALUE = 140852299L;
/** @hide */
@IntDef(prefix = { "NETWORK_TYPE_" }, value = {
NETWORK_TYPE_NONE,
@@ -206,6 +217,67 @@ public class JobInfo implements Parcelable {
*/
public static final int DEFAULT_BACKOFF_POLICY = BACKOFF_POLICY_EXPONENTIAL;
/**
* Job has minimal value to the user. The user has absolutely no expectation
* or knowledge of this task and it has no bearing on the user's perception of
* the app whatsoever. JobScheduler <i>may</i> decide to defer these tasks while
* there are higher priority tasks in order to ensure there is sufficient quota
* available for the higher priority tasks.
* A sample task of min priority: uploading analytics
*/
public static final int PRIORITY_MIN = 100;
/**
* Low priority. The task provides some benefit to users, but is not critical
* and is more of a nice-to-have. This is more important than minimum priority
* jobs and will be prioritized ahead of them, but may still be deferred in lieu
* of higher priority jobs. JobScheduler <i>may</i> decide to defer these tasks
* while there are higher priority tasks in order to ensure there is sufficient
* quota available for the higher priority tasks.
* A sample task of low priority: prefetching data the user hasn't requested
*/
public static final int PRIORITY_LOW = 200;
/**
* Default value for all regular jobs. As noted in {@link JobScheduler},
* these jobs have a general maximum execution time of 10 minutes.
* Receives the standard job management policy.
*/
public static final int PRIORITY_DEFAULT = 300;
/**
* This task should be ordered ahead of most other tasks. It may be
* deferred a little, but if it doesn't run at some point, the user may think
* something is wrong. Assuming all constraints remain satisfied
* (including ideal system load conditions), these jobs will have a maximum
* execution time of at least 4 minutes. Setting all of your jobs to high
* priority will not be beneficial to your app and in fact may hurt its
* performance in the long run.
*/
public static final int PRIORITY_HIGH = 400;
/**
* This task should be run ahead of all other tasks. Only Expedited Jobs
* {@link Builder#setExpedited(boolean)} can have this priority and as such,
* are subject to the same maximum execution time details noted in
* {@link Builder#setExpedited(boolean)}.
* A sample task of max priority: receiving a text message and processing it to
* show a notification
*/
public static final int PRIORITY_MAX = 500;
/** @hide */
@IntDef(prefix = {"PRIORITY_"}, value = {
PRIORITY_MIN,
PRIORITY_LOW,
PRIORITY_DEFAULT,
PRIORITY_HIGH,
PRIORITY_MAX,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Priority {
}
/**
* Default of {@link #getBias}.
* @hide
@@ -359,6 +431,8 @@ public class JobInfo implements Parcelable {
private final long initialBackoffMillis;
private final int backoffPolicy;
private final int mBias;
@Priority
private final int mPriority;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private final int flags;
@@ -410,6 +484,14 @@ public class JobInfo implements Parcelable {
return mBias;
}
/**
* @see JobInfo.Builder#setPriority(int)
*/
@Priority
public int getPriority() {
return mPriority;
}
/** @hide */
public int getFlags() {
return flags;
@@ -746,6 +828,9 @@ public class JobInfo implements Parcelable {
if (mBias != j.mBias) {
return false;
}
if (mPriority != j.mPriority) {
return false;
}
if (flags != j.flags) {
return false;
}
@@ -791,6 +876,7 @@ public class JobInfo implements Parcelable {
hashCode = 31 * hashCode + Long.hashCode(initialBackoffMillis);
hashCode = 31 * hashCode + backoffPolicy;
hashCode = 31 * hashCode + mBias;
hashCode = 31 * hashCode + mPriority;
hashCode = 31 * hashCode + flags;
return hashCode;
}
@@ -830,6 +916,7 @@ public class JobInfo implements Parcelable {
hasEarlyConstraint = in.readInt() == 1;
hasLateConstraint = in.readInt() == 1;
mBias = in.readInt();
mPriority = in.readInt();
flags = in.readInt();
}
@@ -861,6 +948,7 @@ public class JobInfo implements Parcelable {
hasEarlyConstraint = b.mHasEarlyConstraint;
hasLateConstraint = b.mHasLateConstraint;
mBias = b.mBias;
mPriority = b.mPriority;
flags = b.mFlags;
}
@@ -906,6 +994,7 @@ public class JobInfo implements Parcelable {
out.writeInt(hasEarlyConstraint ? 1 : 0);
out.writeInt(hasLateConstraint ? 1 : 0);
out.writeInt(mBias);
out.writeInt(mPriority);
out.writeInt(this.flags);
}
@@ -1024,6 +1113,8 @@ public class JobInfo implements Parcelable {
private ClipData mClipData;
private int mClipGrantFlags;
private int mBias = BIAS_DEFAULT;
@Priority
private int mPriority = PRIORITY_DEFAULT;
private int mFlags;
// Requirements.
private int mConstraintFlags;
@@ -1100,6 +1191,7 @@ public class JobInfo implements Parcelable {
// mBackoffPolicySet isn't set but it's fine since this is copying from an already valid
// job.
mBackoffPolicy = job.getBackoffPolicy();
mPriority = job.getPriority();
}
/** @hide */
@@ -1109,11 +1201,36 @@ public class JobInfo implements Parcelable {
return this;
}
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public Builder setPriority(int priority) {
// No-op for invalid calls. This wasn't a supported API before Tiramisu, so anyone
// calling this that isn't targeting T isn't guaranteed a behavior change.
/**
* Indicate the priority for this job. The priority set here will be used to sort jobs
* for a single app and apply slightly different policies based on the priority.
* The priority will <b>NOT</b> be used as a global sorting value to sort between
* different app's jobs. Use this to inform the system about which jobs it should try
* to run before other jobs. Giving the same priority to all of your jobs will result
* in them all being treated the same. The priorities each have slightly different
* behaviors, as noted in their relevant javadoc.
*
* <b>NOTE:</b> Setting all of your jobs to high priority will not be
* beneficial to your app and in fact may hurt its performance in the
* long run.
*
* In order to prevent starvation, repeatedly retried jobs (because of failures) will slowly
* have their priorities lowered.
*
* @see JobInfo#getPriority()
*/
@NonNull
public Builder setPriority(@Priority int priority) {
if (priority > PRIORITY_MAX || priority < PRIORITY_MIN) {
if (Compatibility.isChangeEnabled(THROW_ON_INVALID_PRIORITY_VALUE)) {
throw new IllegalArgumentException("Invalid priority value");
}
// No-op for invalid calls of apps that are targeting S-. This was an unsupported
// API before Tiramisu, so anyone calling this that isn't targeting T isn't
// guaranteed a behavior change.
return this;
}
mPriority = priority;
return this;
}
@@ -1637,7 +1754,17 @@ public class JobInfo implements Parcelable {
public Builder setExpedited(boolean expedited) {
if (expedited) {
mFlags |= FLAG_EXPEDITED;
if (mPriority == PRIORITY_DEFAULT) {
// The default priority for EJs is MAX, but only change this if .setPriority()
// hasn't been called yet.
mPriority = PRIORITY_MAX;
}
} else {
if (mPriority == PRIORITY_MAX && (mFlags & FLAG_EXPEDITED) != 0) {
// Reset the priority for the job, but only change this if .setPriority()
// hasn't been called yet.
mPriority = PRIORITY_DEFAULT;
}
mFlags &= (~FLAG_EXPEDITED);
}
return this;
@@ -1664,7 +1791,18 @@ public class JobInfo implements Parcelable {
public Builder setImportantWhileForeground(boolean importantWhileForeground) {
if (importantWhileForeground) {
mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND;
if (mPriority == PRIORITY_DEFAULT) {
// The default priority for important-while-foreground is HIGH, but only change
// this if .setPriority() hasn't been called yet.
mPriority = PRIORITY_HIGH;
}
} else {
if (mPriority == PRIORITY_HIGH
&& (mFlags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
// Reset the priority for the job, but only change this if .setPriority()
// hasn't been called yet.
mPriority = PRIORITY_DEFAULT;
}
mFlags &= (~FLAG_IMPORTANT_WHILE_FOREGROUND);
}
return this;
@@ -1812,12 +1950,42 @@ public class JobInfo implements Parcelable {
}
}
if ((flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0 && hasEarlyConstraint) {
throw new IllegalArgumentException(
"An important while foreground job cannot have a time delay");
if ((flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
if (hasEarlyConstraint) {
throw new IllegalArgumentException(
"An important while foreground job cannot have a time delay");
}
if (mPriority != PRIORITY_HIGH && mPriority != PRIORITY_DEFAULT) {
throw new IllegalArgumentException(
"An important while foreground job must be high or default priority."
+ " Don't mark unimportant tasks as important while foreground.");
}
}
if ((flags & FLAG_EXPEDITED) != 0) {
final boolean isExpedited = (flags & FLAG_EXPEDITED) != 0;
switch (mPriority) {
case PRIORITY_MAX:
if (!isExpedited) {
throw new IllegalArgumentException("Only expedited jobs can have max priority");
}
break;
case PRIORITY_HIGH:
if ((flags & FLAG_PREFETCH) != 0) {
throw new IllegalArgumentException("Prefetch jobs cannot be high priority");
}
if (isPeriodic) {
throw new IllegalArgumentException("Periodic jobs cannot be high priority");
}
break;
case PRIORITY_DEFAULT:
case PRIORITY_LOW:
case PRIORITY_MIN:
break;
default:
throw new IllegalArgumentException("Invalid priority level provided: " + mPriority);
}
if (isExpedited) {
if (hasEarlyConstraint) {
throw new IllegalArgumentException("An expedited job cannot have a time delay");
}
@@ -1827,6 +1995,11 @@ public class JobInfo implements Parcelable {
if (isPeriodic) {
throw new IllegalArgumentException("An expedited job cannot be periodic");
}
if (mPriority != PRIORITY_MAX && mPriority != PRIORITY_HIGH) {
throw new IllegalArgumentException(
"An expedited job must be high or max priority. Don't use expedited jobs"
+ " for unimportant tasks.");
}
if ((constraintFlags & ~CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0
|| (flags & ~(FLAG_EXPEDITED | FLAG_EXEMPT_FROM_APP_STANDBY)) != 0) {
throw new IllegalArgumentException(
@@ -1863,4 +2036,24 @@ public class JobInfo implements Parcelable {
}
return bias + " [UNKNOWN]";
}
/**
* Convert a priority integer into a human readable string for debugging.
* @hide
*/
public static String getPriorityString(@Priority int priority) {
switch (priority) {
case PRIORITY_MIN:
return priority + " [MIN]";
case PRIORITY_LOW:
return priority + " [LOW]";
case PRIORITY_DEFAULT:
return priority + " [DEFAULT]";
case PRIORITY_HIGH:
return priority + " [HIGH]";
case PRIORITY_MAX:
return priority + " [MAX]";
}
return priority + " [UNKNOWN]";
}
}

View File

@@ -21,6 +21,7 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -438,6 +439,7 @@ public class JobSchedulerService extends com.android.server.SystemService
case Constants.KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS:
case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS:
case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS:
case Constants.KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS:
if (!runtimeUpdated) {
mConstants.updateRuntimeConstantsLocked();
runtimeUpdated = true;
@@ -504,6 +506,8 @@ public class JobSchedulerService extends com.android.server.SystemService
"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 String KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS =
"runtime_min_high_priority_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;
@@ -525,6 +529,8 @@ public class JobSchedulerService extends com.android.server.SystemService
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;
@VisibleForTesting
static final long DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = 5 * MINUTE_IN_MILLIS;
private static final boolean DEFAULT_USE_TARE_POLICY = false;
/**
@@ -611,6 +617,12 @@ public class JobSchedulerService extends com.android.server.SystemService
*/
public long RUNTIME_MIN_EJ_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS;
/**
* The minimum amount of time we try to guarantee high priority jobs will run for.
*/
public long RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS =
DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS;
/**
* If true, use TARE policy for job limiting. If false, use quotas.
*/
@@ -686,12 +698,18 @@ public class JobSchedulerService extends com.android.server.SystemService
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);
KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS,
KEY_RUNTIME_MIN_HIGH_PRIORITY_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 high priority jobs is at least 4 minutes.
RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = Math.max(4 * MINUTE_IN_MILLIS,
properties.getLong(
KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS,
DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_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(
@@ -739,6 +757,8 @@ public class JobSchedulerService extends com.android.server.SystemService
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_MIN_HIGH_PRIORITY_GUARANTEE_MS,
RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS).println();
pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)
.println();
@@ -775,7 +795,31 @@ public class JobSchedulerService extends com.android.server.SystemService
@VisibleForTesting
class PendingJobComparator implements Comparator<JobStatus> {
private final SparseLongArray mEarliestRegEnqueueTimeCache = new SparseLongArray();
private static final int EJ_PRIORITY_MODIFIER = 10;
/** Cache of the earliest non-PRIORITY_MAX enqueue time found per UID. */
private final SparseLongArray mEarliestNonMaxEnqueueTimeCache = new SparseLongArray();
/**
* Cache of the last enqueue time of each priority for each UID. The SparseArray is keyed
* by UID and the SparseLongArray is keyed by the priority.
*/
private final SparseArray<SparseLongArray> mLastPriorityEnqueueTimeCache =
new SparseArray<>();
/**
* The earliest enqueue time each UID's priority's jobs should use. The SparseArray is keyed
* by UID and the SparseLongArray is keyed by the value returned from
* {@link #getPriorityIndex(int, boolean)}.
*/
private final SparseArray<SparseLongArray> mEarliestAllowedEnqueueTimes =
new SparseArray<>();
private int getPriorityIndex(int priority, boolean isEJ) {
// We need to separate HIGH priority EJs from HIGH priority regular jobs.
if (isEJ) {
return priority * EJ_PRIORITY_MODIFIER;
}
return priority;
}
/**
* Refresh sorting determinants based on the current state of {@link #mPendingJobs}.
@@ -783,17 +827,82 @@ public class JobSchedulerService extends com.android.server.SystemService
@GuardedBy("mLock")
@VisibleForTesting
void refreshLocked() {
mEarliestRegEnqueueTimeCache.clear();
mEarliestNonMaxEnqueueTimeCache.clear();
for (int i = 0; i < mPendingJobs.size(); ++i) {
final JobStatus job = mPendingJobs.get(i);
final int uid = job.getSourceUid();
if (!job.isRequestedExpeditedJob()) {
if (job.getEffectivePriority() < JobInfo.PRIORITY_MAX) {
final long earliestEnqueueTime =
mEarliestRegEnqueueTimeCache.get(uid, Long.MAX_VALUE);
mEarliestRegEnqueueTimeCache.put(uid,
mEarliestNonMaxEnqueueTimeCache.get(uid, Long.MAX_VALUE);
mEarliestNonMaxEnqueueTimeCache.put(uid,
Math.min(earliestEnqueueTime, job.enqueueTime));
}
final int pIdx =
getPriorityIndex(job.getEffectivePriority(), job.isRequestedExpeditedJob());
SparseLongArray lastPriorityEnqueueTime = mLastPriorityEnqueueTimeCache.get(uid);
if (lastPriorityEnqueueTime == null) {
lastPriorityEnqueueTime = new SparseLongArray();
mLastPriorityEnqueueTimeCache.put(uid, lastPriorityEnqueueTime);
}
lastPriorityEnqueueTime.put(pIdx,
Math.max(job.enqueueTime, lastPriorityEnqueueTime.get(pIdx, 0)));
}
// Move lower priority jobs behind higher priority jobs (instead of moving higher
// priority jobs ahead of lower priority jobs), except for EJs.
for (int i = 0; i < mLastPriorityEnqueueTimeCache.size(); ++i) {
final int uid = mLastPriorityEnqueueTimeCache.keyAt(i);
SparseLongArray lastEnqueueTimes = mLastPriorityEnqueueTimeCache.valueAt(i);
SparseLongArray earliestAllowedEnqueueTimes = new SparseLongArray();
mEarliestAllowedEnqueueTimes.put(uid, earliestAllowedEnqueueTimes);
long earliestAllowedEnqueueTime = mEarliestNonMaxEnqueueTimeCache.get(uid,
lastEnqueueTimes.get(getPriorityIndex(JobInfo.PRIORITY_MAX, true), -1));
earliestAllowedEnqueueTimes.put(getPriorityIndex(JobInfo.PRIORITY_MAX, true),
earliestAllowedEnqueueTime);
earliestAllowedEnqueueTime = 1
+ Math.max(earliestAllowedEnqueueTime,
lastEnqueueTimes.get(getPriorityIndex(JobInfo.PRIORITY_HIGH, true), -1));
earliestAllowedEnqueueTimes.put(getPriorityIndex(JobInfo.PRIORITY_HIGH, true),
earliestAllowedEnqueueTime);
earliestAllowedEnqueueTime++;
for (int p = JobInfo.PRIORITY_HIGH; p >= JobInfo.PRIORITY_MIN; --p) {
final int pIdx = getPriorityIndex(p, false);
earliestAllowedEnqueueTimes.put(pIdx, earliestAllowedEnqueueTime);
final long lastEnqueueTime = lastEnqueueTimes.get(pIdx, -1);
if (lastEnqueueTime != -1) {
// Add additional millisecond for the next priority to ensure sorting is
// stable/accurate when comparing to other apps.
earliestAllowedEnqueueTime = 1
+ Math.max(earliestAllowedEnqueueTime, lastEnqueueTime);
}
}
}
// Clear intermediate state that we don't need to reduce steady state memory usage.
mLastPriorityEnqueueTimeCache.clear();
}
@ElapsedRealtimeLong
private long getEffectiveEnqueueTime(@NonNull JobStatus job) {
// Move lower priority jobs behind higher priority jobs (instead of moving higher
// priority jobs ahead of lower priority jobs), except for MAX EJs.
final int uid = job.getSourceUid();
if (job.isRequestedExpeditedJob()
&& job.getEffectivePriority() == JobInfo.PRIORITY_MAX) {
return Math.min(job.enqueueTime,
mEarliestNonMaxEnqueueTimeCache.get(uid, Long.MAX_VALUE));
}
final int priorityIdx =
getPriorityIndex(job.getEffectivePriority(), job.isRequestedExpeditedJob());
final SparseLongArray earliestAllowedEnqueueTimes =
mEarliestAllowedEnqueueTimes.get(uid);
if (earliestAllowedEnqueueTimes == null) {
// We're probably trying to insert directly without refreshing the internal arrays.
// Since we haven't seen this UID before, we can just use the job's enqueue time.
return job.enqueueTime;
}
return Math.max(job.enqueueTime, earliestAllowedEnqueueTimes.get(priorityIdx));
}
@Override
@@ -816,38 +925,39 @@ public class JobSchedulerService extends com.android.server.SystemService
// expedited job quota.
return o1EJ ? -1 : 1;
}
}
if (o1EJ || o2EJ) {
// We MUST prioritize EJs ahead of regular jobs within a single app. Since we do
// that, in order to satisfy the transitivity constraint of the comparator, if
// any UID has an EJ, we must ensure that the EJ is ordered ahead of the regular
// job of a different app IF the app with an EJ had another job that came before
// the differing app. For example, if app A has regJob1 at t1 and eJob3 at t3 and
// app B has regJob2 at t2, eJob3 must be ordered before regJob2 because it will be
// ordered before regJob1.
// Regular jobs don't need to jump the line.
if (o1.getEffectivePriority() != o2.getEffectivePriority()) {
// Use the priority set by an app for intra-app job ordering. Higher
// priority should be before lower priority.
return o2.getEffectivePriority() - o1.getEffectivePriority();
}
} else {
// TODO: see if we can simplify this using explicit topological sorting
// Since we order jobs within a UID by the job's priority, in order to satisfy the
// transitivity constraint of the comparator, we must ensure consistent/appropriate
// ordering between apps as well. That is, if a job is ordered before or behind
// another job because of its priority, that ordering must translate to the
// relative ordering against other jobs.
// The effective ordering implementation here is to use HIGH priority EJs as a
// pivot point. MAX priority EJs are moved *ahead* of HIGH priority EJs. All
// regular jobs are moved *behind* HIGH priority EJs. The intention for moving jobs
// "behind" the EJs instead of moving all high priority jobs before lower priority
// jobs is to reduce any potential abuse (or just unfortunate execution) cases where
// there are early low priority jobs that don't get to run because so many of the
// app's high priority jobs are pushed before low priority job. This may still
// happen because of the job ordering mechanism, but moving jobs back prevents
// one app's jobs from always being at the front (due to the early scheduled low
// priority job and our base case of sorting by enqueue time).
final long uid1EarliestRegEnqueueTime = Math.min(o1.enqueueTime,
mEarliestRegEnqueueTimeCache.get(o1.getSourceUid(), Long.MAX_VALUE));
final long uid2EarliestRegEnqueueTime = Math.min(o2.enqueueTime,
mEarliestRegEnqueueTimeCache.get(o2.getSourceUid(), Long.MAX_VALUE));
final long o1EffectiveEnqueueTime = getEffectiveEnqueueTime(o1);
final long o2EffectiveEnqueueTime = getEffectiveEnqueueTime(o2);
if (o1EJ && o2EJ) {
if (uid1EarliestRegEnqueueTime < uid2EarliestRegEnqueueTime) {
return -1;
} else if (uid1EarliestRegEnqueueTime > uid2EarliestRegEnqueueTime) {
return 1;
}
} else if (o1EJ && uid1EarliestRegEnqueueTime <= o2.enqueueTime) {
// Include = to ensure that if we sorted an EJ ahead of a regular job at time X
// then we make sure to sort it ahead of all regular jobs at time X.
if (o1EffectiveEnqueueTime < o2EffectiveEnqueueTime) {
return -1;
} else if (o2EJ && uid2EarliestRegEnqueueTime <= o1.enqueueTime) {
// Include = to ensure that if we sorted an EJ ahead of a regular job at time X
// then we make sure to sort it ahead of all regular jobs at time X.
} else if (o1EffectiveEnqueueTime > o2EffectiveEnqueueTime) {
return 1;
}
}
if (o1.enqueueTime < o2.enqueueTime) {
return -1;
}
@@ -2313,8 +2423,8 @@ public class JobSchedulerService extends com.android.server.SystemService
private void postProcessLocked() {
noteJobsPending(newReadyJobs);
mPendingJobs.addAll(newReadyJobs);
mPendingJobComparator.refreshLocked();
if (mPendingJobs.size() > 1) {
mPendingJobComparator.refreshLocked();
mPendingJobs.sort(mPendingJobComparator);
}
@@ -2442,8 +2552,8 @@ public class JobSchedulerService extends com.android.server.SystemService
}
noteJobsPending(runnableJobs);
mPendingJobs.addAll(runnableJobs);
mPendingJobComparator.refreshLocked();
if (mPendingJobs.size() > 1) {
mPendingJobComparator.refreshLocked();
mPendingJobs.sort(mPendingJobComparator);
}
} else {
@@ -2653,6 +2763,8 @@ public class JobSchedulerService extends com.android.server.SystemService
return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX
? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS
: Math.min(mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, 5 * MINUTE_IN_MILLIS);
} else if (job.getEffectivePriority() == JobInfo.PRIORITY_HIGH) {
return mConstants.RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS;
} else {
return mConstants.RUNTIME_MIN_GUARANTEE_MS;
}

View File

@@ -16,6 +16,8 @@
package com.android.server.job;
import static android.app.job.JobInfo.getPriorityString;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
@@ -378,18 +380,40 @@ public final class JobServiceContext implements ServiceConnection {
@EconomicPolicy.AppAction
private static int getStartActionId(@NonNull JobStatus job) {
if (job.startedAsExpeditedJob || job.shouldTreatAsExpeditedJob()) {
return JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START;
switch (job.getEffectivePriority()) {
case JobInfo.PRIORITY_MAX:
return JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START;
case JobInfo.PRIORITY_HIGH:
return JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START;
case JobInfo.PRIORITY_LOW:
return JobSchedulerEconomicPolicy.ACTION_JOB_LOW_START;
case JobInfo.PRIORITY_MIN:
return JobSchedulerEconomicPolicy.ACTION_JOB_MIN_START;
default:
Slog.wtf(TAG, "Unknown priority: " + getPriorityString(job.getEffectivePriority()));
// Intentional fallthrough
case JobInfo.PRIORITY_DEFAULT:
return JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START;
}
return JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START;
}
@EconomicPolicy.AppAction
private static int getRunningActionId(@NonNull JobStatus job) {
if (job.startedAsExpeditedJob || job.shouldTreatAsExpeditedJob()) {
return JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING;
switch (job.getEffectivePriority()) {
case JobInfo.PRIORITY_MAX:
return JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING;
case JobInfo.PRIORITY_HIGH:
return JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING;
case JobInfo.PRIORITY_LOW:
return JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING;
case JobInfo.PRIORITY_MIN:
return JobSchedulerEconomicPolicy.ACTION_JOB_MIN_RUNNING;
default:
Slog.wtf(TAG, "Unknown priority: " + getPriorityString(job.getEffectivePriority()));
// Intentional fallthrough
case JobInfo.PRIORITY_DEFAULT:
return JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING;
}
return JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING;
}
/**

View File

@@ -335,7 +335,7 @@ public final class JobStore {
}
/** Version of the db schema. */
private static final int JOBS_FILE_VERSION = 0;
private static final int JOBS_FILE_VERSION = 1;
/** Tag corresponds to constraints this job needs. */
private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
/** Tag corresponds to execution parameters. */
@@ -548,6 +548,7 @@ public final class JobStore {
out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
out.attribute(null, "bias", String.valueOf(jobStatus.getBias()));
out.attribute(null, "priority", String.valueOf(jobStatus.getEffectivePriority()));
out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
if (jobStatus.getInternalFlags() != 0) {
out.attribute(null, "internalFlags", String.valueOf(jobStatus.getInternalFlags()));
@@ -771,10 +772,11 @@ public final class JobStore {
String tagName = parser.getName();
if ("job-info".equals(tagName)) {
final List<JobStatus> jobs = new ArrayList<JobStatus>();
final int version;
// Read in version info.
try {
int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
if (version != JOBS_FILE_VERSION) {
version = Integer.parseInt(parser.getAttributeValue(null, "version"));
if (version > JOBS_FILE_VERSION || version < 0) {
Slog.d(TAG, "Invalid version number, aborting jobs file read.");
return null;
}
@@ -789,7 +791,7 @@ public final class JobStore {
tagName = parser.getName();
// Start reading job.
if ("job".equals(tagName)) {
JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser);
JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser, version);
if (persistedJob != null) {
if (DEBUG) {
Slog.d(TAG, "Read out " + persistedJob);
@@ -812,8 +814,8 @@ public final class JobStore {
* will take the parser into the body of the job tag.
* @return Newly instantiated job holding all the information we just read out of the xml tag.
*/
private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser)
throws XmlPullParserException, IOException {
private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser,
int schemaVersion) throws XmlPullParserException, IOException {
JobInfo.Builder jobBuilder;
int uid, sourceUserId;
long lastSuccessfulRunTime;
@@ -826,12 +828,21 @@ public final class JobStore {
jobBuilder.setPersisted(true);
uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
String val = parser.getAttributeValue(null, "bias");
if (val == null) {
String val;
if (schemaVersion == 0) {
val = parser.getAttributeValue(null, "priority");
}
if (val != null) {
jobBuilder.setBias(Integer.parseInt(val));
if (val != null) {
jobBuilder.setBias(Integer.parseInt(val));
}
} else if (schemaVersion >= 1) {
val = parser.getAttributeValue(null, "bias");
if (val != null) {
jobBuilder.setBias(Integer.parseInt(val));
}
val = parser.getAttributeValue(null, "priority");
if (val != null) {
jobBuilder.setPriority(Integer.parseInt(val));
}
}
val = parser.getAttributeValue(null, "flags");
if (val != null) {

View File

@@ -4,6 +4,7 @@
"name": "CtsJobSchedulerTestCases",
"options": [
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "android.platform.test.annotations.LargeTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"},
{"exclude-annotation": "androidx.test.filters.LargeTest"}
]
@@ -13,6 +14,7 @@
"options": [
{"include-filter": "com.android.server.job"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "android.platform.test.annotations.LargeTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"}
]
},
@@ -21,6 +23,7 @@
"options": [
{"include-filter": "com.android.server.job"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "android.platform.test.annotations.LargeTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"}
]
}

View File

@@ -22,6 +22,7 @@ import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import android.annotation.ElapsedRealtimeLong;
import android.app.AppGlobals;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
@@ -349,6 +350,7 @@ public final class JobStatus {
public int overrideState = JobStatus.OVERRIDE_NONE;
// When this job was enqueued, for ordering. (in elapsedRealtimeMillis)
@ElapsedRealtimeLong
public long enqueueTime;
// Metrics about queue latency. (in uptimeMillis)
@@ -928,6 +930,30 @@ public final class JobStatus {
return job.getBias();
}
/**
* Returns the priority of the job, which may be adjusted due to various factors.
* @see JobInfo.Builder#setPriority(int)
*/
@JobInfo.Priority
public int getEffectivePriority() {
final int rawPriority = job.getPriority();
if (numFailures < 2) {
return rawPriority;
}
// Slowly decay priority of jobs to prevent starvation of other jobs.
if (isRequestedExpeditedJob()) {
// EJs can't fall below HIGH priority.
return JobInfo.PRIORITY_HIGH;
}
// Set a maximum priority based on the number of failures.
final int dropPower = numFailures / 2;
switch (dropPower) {
case 1: return Math.min(JobInfo.PRIORITY_DEFAULT, rawPriority);
case 2: return Math.min(JobInfo.PRIORITY_LOW, rawPriority);
default: return JobInfo.PRIORITY_MIN;
}
}
public int getFlags() {
return job.getFlags();
}
@@ -1951,6 +1977,14 @@ public final class JobStatus {
pw.print("Bias: ");
pw.println(JobInfo.getBiasString(job.getBias()));
}
pw.print("Priority: ");
pw.print(JobInfo.getPriorityString(job.getPriority()));
final int effectivePriority = getEffectivePriority();
if (effectivePriority != job.getPriority()) {
pw.print(" effective=");
pw.print(JobInfo.getPriorityString(effectivePriority));
}
pw.println();
if (job.getFlags() != 0) {
pw.print("Flags: ");
pw.println(Integer.toHexString(job.getFlags()));

View File

@@ -882,6 +882,7 @@ public final class QuotaController extends StateController {
if (isQuotaFreeLocked(standbyBucket)) return true;
ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
// TODO: use a higher minimum remaining time for jobs with MINIMUM priority
return getRemainingExecutionTimeLocked(stats) > 0
&& isUnderJobCountQuotaLocked(stats, standbyBucket)
&& isUnderSessionCountQuotaLocked(stats, standbyBucket);

View File

@@ -7973,6 +7973,7 @@ package android.app.job {
method public static final long getMinPeriodMillis();
method public long getMinimumNetworkChunkBytes();
method @Deprecated public int getNetworkType();
method public int getPriority();
method @Nullable public android.net.NetworkRequest getRequiredNetwork();
method @NonNull public android.content.ComponentName getService();
method @NonNull public android.os.Bundle getTransientExtras();
@@ -8001,6 +8002,11 @@ package android.app.job {
field public static final int NETWORK_TYPE_NONE = 0; // 0x0
field public static final int NETWORK_TYPE_NOT_ROAMING = 3; // 0x3
field public static final int NETWORK_TYPE_UNMETERED = 2; // 0x2
field public static final int PRIORITY_DEFAULT = 300; // 0x12c
field public static final int PRIORITY_HIGH = 400; // 0x190
field public static final int PRIORITY_LOW = 200; // 0xc8
field public static final int PRIORITY_MAX = 500; // 0x1f4
field public static final int PRIORITY_MIN = 100; // 0x64
}
public static final class JobInfo.Builder {
@@ -8020,6 +8026,7 @@ package android.app.job {
method public android.app.job.JobInfo.Builder setPeriodic(long, long);
method @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) public android.app.job.JobInfo.Builder setPersisted(boolean);
method public android.app.job.JobInfo.Builder setPrefetch(boolean);
method @NonNull public android.app.job.JobInfo.Builder setPriority(int);
method public android.app.job.JobInfo.Builder setRequiredNetwork(@Nullable android.net.NetworkRequest);
method public android.app.job.JobInfo.Builder setRequiredNetworkType(int);
method public android.app.job.JobInfo.Builder setRequiresBatteryNotLow(boolean);

View File

@@ -56,7 +56,9 @@ import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.platform.test.annotations.LargeTest;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseLongArray;
@@ -85,6 +87,11 @@ import java.util.Random;
public class JobSchedulerServiceTest {
private static final String TAG = JobSchedulerServiceTest.class.getSimpleName();
private static final int[] sRegJobPriorities = {
JobInfo.PRIORITY_HIGH, JobInfo.PRIORITY_DEFAULT,
JobInfo.PRIORITY_LOW, JobInfo.PRIORITY_MIN
};
private JobSchedulerService mService;
private MockitoSession mMockingSession;
@@ -893,7 +900,7 @@ public class JobSchedulerServiceTest {
createJobInfo(6).setExpedited(true), 2);
JobStatus eA7 = createJobStatus("testPendingJobSorting",
createJobInfo(7).setExpedited(true), 1);
JobStatus rH8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 14);
JobStatus rH8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 8);
JobStatus rF8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 6);
JobStatus eF9 = createJobStatus("testPendingJobSorting",
createJobInfo(9).setExpedited(true), 6);
@@ -905,21 +912,21 @@ public class JobSchedulerServiceTest {
JobStatus eE14 = createJobStatus("testPendingJobSorting",
createJobInfo(14).setExpedited(true), 5);
rA1.enqueueTime = 1;
rB2.enqueueTime = 2;
eC3.enqueueTime = 3;
rD4.enqueueTime = 4;
eE5.enqueueTime = 5;
eB6.enqueueTime = 6;
eA7.enqueueTime = 7;
rF8.enqueueTime = 8;
rH8.enqueueTime = 8;
eF9.enqueueTime = 9;
rC10.enqueueTime = 10;
eC11.enqueueTime = 11;
rG12.enqueueTime = 12;
rG13.enqueueTime = 13;
eE14.enqueueTime = 14;
rA1.enqueueTime = 10;
rB2.enqueueTime = 20;
eC3.enqueueTime = 30;
rD4.enqueueTime = 40;
eE5.enqueueTime = 50;
eB6.enqueueTime = 60;
eA7.enqueueTime = 70;
rF8.enqueueTime = 80;
rH8.enqueueTime = 80;
eF9.enqueueTime = 90;
rC10.enqueueTime = 100;
eC11.enqueueTime = 110;
rG12.enqueueTime = 120;
rG13.enqueueTime = 130;
eE14.enqueueTime = 140;
mService.mPendingJobs.clear();
// Add in random order so sorting is apparent.
@@ -951,39 +958,62 @@ public class JobSchedulerServiceTest {
}
private void checkPendingJobInvariants() {
long regJobEnqueueTime = 0;
final SparseBooleanArray regJobSeen = new SparseBooleanArray();
final SparseLongArray ejEnqueueTimes = new SparseLongArray();
// Latest priority enqueue times seen for each priority for each app.
final SparseArray<SparseLongArray> latestPriorityRegEnqueueTimesPerUid =
new SparseArray<>();
final SparseArray<SparseLongArray> latestPriorityEjEnqueueTimesPerUid = new SparseArray<>();
final long noEntry = -1;
for (int i = 0; i < mService.mPendingJobs.size(); ++i) {
final JobStatus job = mService.mPendingJobs.get(i);
final int uid = job.getSourceUid();
if (!job.isRequestedExpeditedJob()) {
// Invariant #1: Regular jobs are sorted by enqueue time.
assertTrue("Regular job with earlier enqueue time sorted after a later time: "
+ regJobEnqueueTime + " vs " + job.enqueueTime,
regJobEnqueueTime <= job.enqueueTime);
regJobEnqueueTime = job.enqueueTime;
regJobSeen.put(uid, true);
} else {
// Invariant #2: EJs should be before regular jobs for an individual app
if (regJobSeen.get(uid)) {
fail("UID " + uid + " had an EJ ordered after a regular job");
// Invariant #1: All jobs (for a UID) are sorted by priority order
// Invariant #2: Jobs (for a UID) with the same priority are sorted by enqueue time.
// Invariant #3: EJs (for a UID) should be before regular jobs
final int priority = job.getEffectivePriority();
final SparseArray<SparseLongArray> latestPriorityEnqueueTimesPerUid =
job.isRequestedExpeditedJob()
? latestPriorityEjEnqueueTimesPerUid
: latestPriorityRegEnqueueTimesPerUid;
SparseLongArray latestPriorityEnqueueTimes = latestPriorityEnqueueTimesPerUid.get(uid);
if (latestPriorityEnqueueTimes != null) {
// Invariant 1
for (int p = priority - 1; p >= JobInfo.PRIORITY_MIN; --p) {
// If we haven't seen the priority, there shouldn't be an entry in the array.
assertEquals("Jobs not properly sorted by priority for uid " + uid,
noEntry, latestPriorityEnqueueTimes.get(p, noEntry));
}
final long ejEnqueueTime = ejEnqueueTimes.get(uid, 0);
// Invariant #3: EJs for an individual app should be sorted by enqueue time.
assertTrue("EJ with earlier enqueue time sorted after a later time: "
+ ejEnqueueTime + " vs " + job.enqueueTime,
ejEnqueueTime <= job.enqueueTime);
ejEnqueueTimes.put(uid, job.enqueueTime);
// Invariant 2
final long lastSeenPriorityEnqueueTime =
latestPriorityEnqueueTimes.get(priority, noEntry);
if (lastSeenPriorityEnqueueTime != noEntry) {
assertTrue("Jobs with same priority not sorted by enqueue time: "
+ lastSeenPriorityEnqueueTime + " vs " + job.enqueueTime,
lastSeenPriorityEnqueueTime <= job.enqueueTime);
}
} else {
latestPriorityEnqueueTimes = new SparseLongArray();
latestPriorityEnqueueTimesPerUid.put(uid, latestPriorityEnqueueTimes);
}
latestPriorityEnqueueTimes.put(priority, job.enqueueTime);
// Invariant 3
if (!job.isRequestedExpeditedJob()) {
regJobSeen.put(uid, true);
} else if (regJobSeen.get(uid)) {
fail("UID " + uid + " had an EJ ordered after a regular job");
}
}
}
private static String sortedJobToString(JobStatus job) {
return "testJob " + job.getSourceUid() + "/" + job.getJobId() + "/"
+ job.isRequestedExpeditedJob() + "@" + job.enqueueTime;
return "testJob " + job.getSourceUid() + "/" + job.getJobId()
+ "/p" + job.getEffectivePriority()
+ "/" + job.isRequestedExpeditedJob() + "@" + job.enqueueTime;
}
@Test
@@ -992,23 +1022,23 @@ public class JobSchedulerServiceTest {
mService.mPendingJobs.clear();
for (int i = 0; i < 2500; ++i) {
for (int i = 0; i < 5000; ++i) {
JobStatus job = createJobStatus("testPendingJobSorting_Random",
createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(250));
job.enqueueTime = random.nextInt(1_000_000);
mService.mPendingJobs.add(job);
mService.mPendingJobComparator.refreshLocked();
try {
mService.mPendingJobs.sort(mService.mPendingJobComparator);
} catch (Exception e) {
for (JobStatus toDump : mService.mPendingJobs) {
Log.i(TAG, sortedJobToString(toDump));
}
throw e;
}
checkPendingJobInvariants();
}
mService.mPendingJobComparator.refreshLocked();
try {
mService.mPendingJobs.sort(mService.mPendingJobComparator);
} catch (Exception e) {
for (JobStatus toDump : mService.mPendingJobs) {
Log.i(TAG, sortedJobToString(toDump));
}
throw e;
}
checkPendingJobInvariants();
}
private int sign(int i) {
@@ -1042,6 +1072,7 @@ public class JobSchedulerServiceTest {
}
@Test
@LargeTest
public void testPendingJobSortingTransitivity_Concentrated() {
// Always use the same series of pseudo random values.
for (int seed : new int[]{1337, 6000, 637739, 6357, 1, 7, 13}) {
@@ -1064,6 +1095,99 @@ public class JobSchedulerServiceTest {
}
}
@Test
public void testPendingJobSorting_Random_WithPriority() {
Random random = new Random(1); // Always use the same series of pseudo random values.
mService.mPendingJobs.clear();
for (int i = 0; i < 5000; ++i) {
final boolean isEj = random.nextBoolean();
final int priority;
if (isEj) {
priority = random.nextBoolean() ? JobInfo.PRIORITY_MAX : JobInfo.PRIORITY_HIGH;
} else {
priority = sRegJobPriorities[random.nextInt(sRegJobPriorities.length)];
}
JobStatus job = createJobStatus("testPendingJobSorting_Random_WithPriority",
createJobInfo(i).setExpedited(isEj).setPriority(priority),
random.nextInt(250));
job.enqueueTime = random.nextInt(1_000_000);
mService.mPendingJobs.add(job);
}
mService.mPendingJobComparator.refreshLocked();
try {
mService.mPendingJobs.sort(mService.mPendingJobComparator);
} catch (Exception e) {
for (JobStatus toDump : mService.mPendingJobs) {
Log.i(TAG, sortedJobToString(toDump));
}
throw e;
}
checkPendingJobInvariants();
}
@Test
public void testPendingJobSortingTransitivity_WithPriority() {
// Always use the same series of pseudo random values.
for (int seed : new int[]{1337, 7357, 606, 6357, 41106010, 3, 2, 1}) {
Random random = new Random(seed);
mService.mPendingJobs.clear();
for (int i = 0; i < 300; ++i) {
final boolean isEj = random.nextBoolean();
final int priority;
if (isEj) {
priority = random.nextBoolean() ? JobInfo.PRIORITY_MAX : JobInfo.PRIORITY_HIGH;
} else {
priority = sRegJobPriorities[random.nextInt(sRegJobPriorities.length)];
}
JobStatus job = createJobStatus("testPendingJobSortingTransitivity_WithPriority",
createJobInfo(i).setExpedited(isEj).setPriority(priority),
random.nextInt(50));
job.enqueueTime = random.nextInt(1_000_000);
job.overrideState = random.nextInt(4);
mService.mPendingJobs.add(job);
}
verifyPendingJobComparatorTransitivity();
}
}
@Test
@LargeTest
public void testPendingJobSortingTransitivity_Concentrated_WithPriority() {
// Always use the same series of pseudo random values.
for (int seed : new int[]{1337, 6000, 637739, 6357, 1, 7, 13}) {
Random random = new Random(seed);
mService.mPendingJobs.clear();
for (int i = 0; i < 300; ++i) {
final boolean isEj = random.nextFloat() < .03;
final int priority;
if (isEj) {
priority = random.nextBoolean() ? JobInfo.PRIORITY_MAX : JobInfo.PRIORITY_HIGH;
} else {
priority = sRegJobPriorities[random.nextInt(sRegJobPriorities.length)];
}
JobStatus job = createJobStatus(
"testPendingJobSortingTransitivity_Concentrated_WithPriority",
createJobInfo(i).setExpedited(isEj).setPriority(priority),
random.nextInt(20));
job.enqueueTime = random.nextInt(250);
job.overrideState = random.nextFloat() < .01
? JobStatus.OVERRIDE_SORTING : JobStatus.OVERRIDE_NONE;
mService.mPendingJobs.add(job);
Log.d(TAG, sortedJobToString(job));
}
verifyPendingJobComparatorTransitivity();
}
}
private void verifyPendingJobComparatorTransitivity() {
mService.mPendingJobComparator.refreshLocked();

View File

@@ -37,6 +37,8 @@ import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_STORAGE_NOT_LOW;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_TIMING_DELAY;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_WITHIN_QUOTA;
import static com.android.server.job.controllers.JobStatus.NO_EARLIEST_RUNTIME;
import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -222,6 +224,104 @@ public class JobStatusTest {
assertEquals(1, createJobStatus(now - 2000, now).getFractionRunTime(), DELTA);
}
@Test
public void testGetEffectivePriority_Expedited() {
final JobInfo jobInfo =
new JobInfo.Builder(101, new ComponentName("foo", "bar"))
.setExpedited(true)
.build();
JobStatus job = createJobStatus(jobInfo);
// Less than 2 failures, priority shouldn't be affected.
assertEquals(JobInfo.PRIORITY_MAX, job.getEffectivePriority());
int backoffAttempt = 1;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_MAX, job.getEffectivePriority());
// 2+ failures, priority should be lowered as much as possible.
backoffAttempt = 2;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
backoffAttempt = 5;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
backoffAttempt = 8;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
}
@Test
public void testGetEffectivePriority_Regular_High() {
final JobInfo jobInfo =
new JobInfo.Builder(101, new ComponentName("foo", "bar"))
.setPriority(JobInfo.PRIORITY_HIGH)
.build();
JobStatus job = createJobStatus(jobInfo);
// Less than 2 failures, priority shouldn't be affected.
assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
int backoffAttempt = 1;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
// Failures in [2,4), priority should be lowered slightly.
backoffAttempt = 2;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_DEFAULT, job.getEffectivePriority());
backoffAttempt = 3;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_DEFAULT, job.getEffectivePriority());
// Failures in [4,6), priority should be lowered more.
backoffAttempt = 4;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
backoffAttempt = 5;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
// 6+ failures, priority should be lowered as much as possible.
backoffAttempt = 6;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority());
backoffAttempt = 12;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority());
}
/**
* Test that LOW priority jobs don't have their priority lowered as quickly as higher priority
* jobs.
*/
@Test
public void testGetEffectivePriority_Regular_Low() {
final JobInfo jobInfo =
new JobInfo.Builder(101, new ComponentName("foo", "bar"))
.setPriority(JobInfo.PRIORITY_LOW)
.build();
JobStatus job = createJobStatus(jobInfo);
// Less than 6 failures, priority shouldn't be affected.
assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
int backoffAttempt = 1;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
backoffAttempt = 4;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
backoffAttempt = 5;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
// 6+ failures, priority should be lowered as much as possible.
backoffAttempt = 6;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority());
backoffAttempt = 12;
job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority());
}
/**
* Test {@link JobStatus#wouldBeReadyWithConstraint} on explicit constraints that weren't
* requested.

View File

@@ -328,6 +328,23 @@ public class JobStoreTest {
assertEquals("Bias not correctly persisted.", 42, loaded.getBias());
}
@Test
public void testPriorityPersisted() throws Exception {
final JobInfo.Builder b = new Builder(92, mComponent)
.setOverrideDeadline(5000)
.setPriority(JobInfo.PRIORITY_MIN)
.setPersisted(true);
final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
mTaskStoreUnderTest.add(js);
waitForPendingIo();
final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
final JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
assertEquals("Priority not correctly persisted.",
JobInfo.PRIORITY_MIN, loaded.getEffectivePriority());
}
/**
* Test that non persisted job is not written to disk.
*/