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