diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index dfe210133a453..2b588ac666c33 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -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 may 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 may 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 NOT 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.
+ *
+ * NOTE: 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]";
+ }
}
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 e4b1e3e9beb0f..78140dce12f44 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -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 {
- 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 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 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;
}
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 5bdee5e636a91..b44178fc9ac90 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -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;
}
/**
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index b1ea14d83f64f..a8dd75248dd82 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -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 jobs = new ArrayList();
+ 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) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
index 56aa59034056f..7d12b95139819 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
+++ b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
@@ -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"}
]
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index dee716fd25487..f74a4facd24c7 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -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()));
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 31da526bead92..29c1108c48732 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
@@ -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);
diff --git a/core/api/current.txt b/core/api/current.txt
index 667b983dd2ad1..ec929ee9d1e43 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -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);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index a94f0ee554bb8..0c3e472b46a8c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -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 latestPriorityRegEnqueueTimesPerUid =
+ new SparseArray<>();
+ final SparseArray 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 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();
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
index 6a25560354705..7d42a52f8427b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -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.
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index 243f7b455a3d9..4de15c87dcc1b 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -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.
*/