From d6ff7d2a283cd2c0d30d4fac46f307cf81588490 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Thu, 25 Jun 2020 13:17:10 -0700 Subject: [PATCH] Expose job priority API. The API allows apps to indicate job execution preference. Ordering is done between an app's own jobs. * Expedited jobs can only be MAX or HIGH priority (with MAX as the default value). Regular jobs can only be HIGH, DEFAULT, LOW, or MIN priority (with DEFAULT as the default value). Periodic and prefetch jobs cannot be HIGH priority. EJs are always ordered ahead of regular jobs, even if both are HIGH priority. * HIGH priority jobs have a standard timeout of at least 4 minutes (5 as default), while DEFAULT and below maintain their 10 minute timeout. * To prevent certain starvation cases (where lower priority jobs continue to be deferred because higher priority jobs are retried and run ahead of the lower priority jobs), priorities will decay as a job is repeatedly retried. Bug: 142272435 Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job Test: atest CtsJobSchedulerTestCases Change-Id: I583d7436bea4975e2f0aecc4019712afdcd0ea77 --- .../java/android/app/job/JobInfo.java | 211 ++++++++++++++++- .../server/job/JobSchedulerService.java | 180 +++++++++++--- .../android/server/job/JobServiceContext.java | 36 ++- .../java/com/android/server/job/JobStore.java | 33 ++- .../java/com/android/server/job/TEST_MAPPING | 3 + .../server/job/controllers/JobStatus.java | 34 +++ .../job/controllers/QuotaController.java | 1 + core/api/current.txt | 7 + .../server/job/JobSchedulerServiceTest.java | 222 ++++++++++++++---- .../server/job/controllers/JobStatusTest.java | 100 ++++++++ .../com/android/server/job/JobStoreTest.java | 17 ++ 11 files changed, 735 insertions(+), 109 deletions(-) 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. */