1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.job; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.util.Pools; 22 import android.util.SparseArray; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.server.job.controllers.JobStatus; 26 27 import java.util.ArrayList; 28 import java.util.Collections; 29 import java.util.Comparator; 30 import java.util.List; 31 import java.util.Objects; 32 import java.util.PriorityQueue; 33 34 /** 35 * A utility class to maintain a sorted list of currently pending jobs. The sorting system is 36 * modeled after topological sort, so the returned order may not always be consistent. 37 */ 38 class PendingJobQueue { 39 private final Pools.Pool<AppJobQueue> mAppJobQueuePool = new Pools.SimplePool<>(8); 40 41 /** Set of currently used queues, keyed by source UID. */ 42 private final SparseArray<AppJobQueue> mCurrentQueues = new SparseArray<>(); 43 /** 44 * Same set of AppJobQueues as in {@link #mCurrentQueues}, but ordered by the next timestamp 45 * to make iterating through the job list faster. 46 */ 47 private final PriorityQueue<AppJobQueue> mOrderedQueues = new PriorityQueue<>( 48 (ajq1, ajq2) -> { 49 final long t1 = ajq1.peekNextTimestamp(); 50 final long t2 = ajq2.peekNextTimestamp(); 51 if (t1 == AppJobQueue.NO_NEXT_TIMESTAMP) { 52 if (t2 == AppJobQueue.NO_NEXT_TIMESTAMP) { 53 return 0; 54 } 55 return 1; 56 } else if (t2 == AppJobQueue.NO_NEXT_TIMESTAMP) { 57 return -1; 58 } 59 final int o1 = ajq1.peekNextOverrideState(); 60 final int o2 = ajq2.peekNextOverrideState(); 61 if (o1 != o2) { 62 // Higher override state (OVERRIDE_FULL) should be before lower state 63 // (OVERRIDE_SOFT) 64 return Integer.compare(o2, o1); 65 } 66 return Long.compare(t1, t2); 67 }); 68 69 private int mSize = 0; 70 71 /** 72 * Whether to batch iteration so that we pull several of an app's jobs from the queue at the 73 * same time (resulting in some out of order pulls) instead of pulling purely based on the 74 * sort order. Batching it this way will mean we try to run several jobs of the same app at the 75 * same, resulting in fewer process restarts, and can allow the iteration runtime to amortize 76 * to O(A*J) instead of O(A*J*log(A)), where A = # apps and J = average # jobs per app. 77 */ 78 private boolean mOptimizeIteration = true; 79 80 /** 81 * Number of jobs that have been pulled from the queue in succession. Used when 82 * {@link #mOptimizeIteration} is true to know when to switch to the next AppJobQueue. 83 */ 84 private int mPullCount = 0; 85 86 private boolean mNeedToResetIterators = false; 87 add(@onNull JobStatus job)88 void add(@NonNull JobStatus job) { 89 final AppJobQueue ajq = getAppJobQueue(job.getSourceUid(), true); 90 final long prevTimestamp = ajq.peekNextTimestamp(); 91 ajq.add(job); 92 mSize++; 93 if (prevTimestamp != ajq.peekNextTimestamp()) { 94 mOrderedQueues.remove(ajq); 95 mOrderedQueues.offer(ajq); 96 } 97 } 98 addAll(@onNull List<JobStatus> jobs)99 void addAll(@NonNull List<JobStatus> jobs) { 100 final SparseArray<List<JobStatus>> jobsByUid = new SparseArray<>(); 101 for (int i = jobs.size() - 1; i >= 0; --i) { 102 final JobStatus job = jobs.get(i); 103 List<JobStatus> appJobs = jobsByUid.get(job.getSourceUid()); 104 if (appJobs == null) { 105 appJobs = new ArrayList<>(); 106 jobsByUid.put(job.getSourceUid(), appJobs); 107 } 108 appJobs.add(job); 109 } 110 for (int i = jobsByUid.size() - 1; i >= 0; --i) { 111 final AppJobQueue ajq = getAppJobQueue(jobsByUid.keyAt(i), true); 112 ajq.addAll(jobsByUid.valueAt(i)); 113 } 114 mSize += jobs.size(); 115 mOrderedQueues.clear(); 116 } 117 clear()118 void clear() { 119 mSize = 0; 120 for (int i = mCurrentQueues.size() - 1; i >= 0; --i) { 121 final AppJobQueue ajq = mCurrentQueues.valueAt(i); 122 ajq.clear(); 123 mAppJobQueuePool.release(ajq); 124 } 125 mCurrentQueues.clear(); 126 mOrderedQueues.clear(); 127 } 128 contains(@onNull JobStatus job)129 boolean contains(@NonNull JobStatus job) { 130 final AppJobQueue ajq = mCurrentQueues.get(job.getSourceUid()); 131 if (ajq == null) { 132 return false; 133 } 134 return ajq.contains(job); 135 } 136 getAppJobQueue(int uid, boolean create)137 private AppJobQueue getAppJobQueue(int uid, boolean create) { 138 AppJobQueue ajq = mCurrentQueues.get(uid); 139 if (ajq == null && create) { 140 ajq = mAppJobQueuePool.acquire(); 141 if (ajq == null) { 142 ajq = new AppJobQueue(); 143 } 144 mCurrentQueues.put(uid, ajq); 145 } 146 return ajq; 147 } 148 149 @Nullable next()150 JobStatus next() { 151 if (mNeedToResetIterators) { 152 mOrderedQueues.clear(); 153 for (int i = mCurrentQueues.size() - 1; i >= 0; --i) { 154 final AppJobQueue ajq = mCurrentQueues.valueAt(i); 155 ajq.resetIterator(0); 156 mOrderedQueues.offer(ajq); 157 } 158 mNeedToResetIterators = false; 159 // Reset the pull count when the front of the queue changes. 160 mPullCount = 0; 161 } else if (mOrderedQueues.size() == 0) { 162 // Something significant changed, so the priority queue was cleared. Lazily regenerate 163 // the queue. 164 for (int i = mCurrentQueues.size() - 1; i >= 0; --i) { 165 final AppJobQueue ajq = mCurrentQueues.valueAt(i); 166 mOrderedQueues.offer(ajq); 167 } 168 // Reset the pull count when the front of the queue changes. 169 mPullCount = 0; 170 } 171 final int numQueues = mOrderedQueues.size(); 172 if (numQueues == 0) { 173 return null; 174 } 175 176 // Increase the pull limit at a slightly faster rate than log(A) increases (until A>=33). 177 // The pull limit increase is intended to balance fairness (one app can't starve out others) 178 // with efficiency (reducing process restarts). 179 // 1-4 apps --> pullLimit = 1, 5-8 apps --> pullLimit = 2, 9+ apps --> pullLimit = 3 180 final int pullLimit = mOptimizeIteration ? Math.min(3, ((numQueues - 1) >>> 2) + 1) : 1; 181 182 final AppJobQueue earliestQueue = mOrderedQueues.peek(); 183 if (earliestQueue != null) { 184 final JobStatus job = earliestQueue.next(); 185 // Change the front of the queue if we've pulled pullLimit jobs from the current head 186 // or we're dealing with test jobs 187 // or the current head has no more jobs to provide. 188 if (++mPullCount >= pullLimit 189 || (job != null && earliestQueue.peekNextOverrideState() != job.overrideState) 190 || earliestQueue.peekNextTimestamp() == AppJobQueue.NO_NEXT_TIMESTAMP) { 191 mOrderedQueues.poll(); 192 if (earliestQueue.peekNextTimestamp() != AppJobQueue.NO_NEXT_TIMESTAMP) { 193 // No need to put back in the queue if it has no more jobs to give. 194 mOrderedQueues.offer(earliestQueue); 195 } 196 // Reset the pull count when the front of the queue changes. 197 mPullCount = 0; 198 } 199 return job; 200 } 201 return null; 202 } 203 remove(@onNull JobStatus job)204 boolean remove(@NonNull JobStatus job) { 205 final AppJobQueue ajq = getAppJobQueue(job.getSourceUid(), false); 206 if (ajq == null) { 207 return false; 208 } 209 210 final long prevTimestamp = ajq.peekNextTimestamp(); 211 if (!ajq.remove(job)) { 212 return false; 213 } 214 215 mSize--; 216 if (ajq.size() == 0) { 217 mCurrentQueues.remove(job.getSourceUid()); 218 mOrderedQueues.remove(ajq); 219 ajq.clear(); 220 mAppJobQueuePool.release(ajq); 221 } else if (prevTimestamp != ajq.peekNextTimestamp()) { 222 // Removing the job changed the "next timestamp" in the queue, so we need to reinsert 223 // it to fix the ordering. 224 mOrderedQueues.remove(ajq); 225 mOrderedQueues.offer(ajq); 226 } 227 228 return true; 229 } 230 231 /** Resets the iterating index to the front of the queue. */ resetIterator()232 void resetIterator() { 233 // Lazily reset the iterating indices (avoid looping through all the current queues until 234 // absolutely necessary). 235 mNeedToResetIterators = true; 236 } 237 238 @VisibleForTesting setOptimizeIteration(boolean optimize)239 void setOptimizeIteration(boolean optimize) { 240 mOptimizeIteration = optimize; 241 } 242 size()243 int size() { 244 return mSize; 245 } 246 247 private static final class AppJobQueue { 248 static final long NO_NEXT_TIMESTAMP = -1L; 249 static final int NO_NEXT_OVERRIDE_STATE = -1; 250 251 private static class AdjustedJobStatus { 252 public long adjustedEnqueueTime; 253 public JobStatus job; 254 clear()255 void clear() { 256 adjustedEnqueueTime = 0; 257 job = null; 258 } 259 } 260 261 private static final Comparator<AdjustedJobStatus> sJobComparator = (aj1, aj2) -> { 262 if (aj1 == aj2) { 263 return 0; 264 } 265 final JobStatus job1 = aj1.job; 266 final JobStatus job2 = aj2.job; 267 if (job1 == job2) { 268 return 0; 269 } 270 // Jobs with an override state set (via adb) should be put first as tests/developers 271 // expect the jobs to run immediately. 272 if (job1.overrideState != job2.overrideState) { 273 // Higher override state (OVERRIDE_FULL) should be before lower state 274 // (OVERRIDE_SOFT) 275 return Integer.compare(job2.overrideState, job1.overrideState); 276 } 277 278 final boolean job1UI = job1.getJob().isUserInitiated(); 279 final boolean job2UI = job2.getJob().isUserInitiated(); 280 if (job1UI != job2UI) { 281 // Attempt to run user-initiated jobs ahead of all other jobs. 282 return job1UI ? -1 : 1; 283 } 284 285 final boolean job1EJ = job1.isRequestedExpeditedJob(); 286 final boolean job2EJ = job2.isRequestedExpeditedJob(); 287 if (job1EJ != job2EJ) { 288 // Attempt to run requested expedited jobs ahead of regular jobs, regardless of 289 // expedited job quota. 290 return job1EJ ? -1 : 1; 291 } 292 293 if (Objects.equals(job1.getNamespace(), job2.getNamespace())) { 294 final int job1Priority = job1.getEffectivePriority(); 295 final int job2Priority = job2.getEffectivePriority(); 296 if (job1Priority != job2Priority) { 297 // Use the priority set by an app for intra-app job ordering. Higher 298 // priority should be before lower priority. 299 return Integer.compare(job2Priority, job1Priority); 300 } 301 } 302 303 if (job1.lastEvaluatedBias != job2.lastEvaluatedBias) { 304 // Higher bias should go first. 305 return Integer.compare(job2.lastEvaluatedBias, job1.lastEvaluatedBias); 306 } 307 308 return Long.compare(job1.enqueueTime, job2.enqueueTime); 309 }; 310 311 private static final Pools.Pool<AdjustedJobStatus> mAdjustedJobStatusPool = 312 new Pools.SimplePool<>(16); 313 314 private final List<AdjustedJobStatus> mJobs = new ArrayList<>(); 315 private int mCurIndex = 0; 316 add(@onNull JobStatus jobStatus)317 void add(@NonNull JobStatus jobStatus) { 318 AdjustedJobStatus adjustedJobStatus = mAdjustedJobStatusPool.acquire(); 319 if (adjustedJobStatus == null) { 320 adjustedJobStatus = new AdjustedJobStatus(); 321 } 322 adjustedJobStatus.adjustedEnqueueTime = jobStatus.enqueueTime; 323 adjustedJobStatus.job = jobStatus; 324 325 int where = Collections.binarySearch(mJobs, adjustedJobStatus, sJobComparator); 326 if (where < 0) { 327 where = ~where; 328 } 329 mJobs.add(where, adjustedJobStatus); 330 if (where < mCurIndex) { 331 // Shift the current index back to make sure the new job is evaluated on the next 332 // iteration. 333 mCurIndex = where; 334 } 335 336 if (where > 0) { 337 final long prevTimestamp = mJobs.get(where - 1).adjustedEnqueueTime; 338 adjustedJobStatus.adjustedEnqueueTime = 339 Math.max(prevTimestamp, adjustedJobStatus.adjustedEnqueueTime); 340 } 341 final int numJobs = mJobs.size(); 342 if (where < numJobs - 1) { 343 // Potentially need to adjust following job timestamps as well. 344 for (int i = where; i < numJobs; ++i) { 345 final AdjustedJobStatus ajs = mJobs.get(i); 346 if (adjustedJobStatus.adjustedEnqueueTime < ajs.adjustedEnqueueTime) { 347 // No further need to adjust. 348 break; 349 } 350 ajs.adjustedEnqueueTime = adjustedJobStatus.adjustedEnqueueTime; 351 } 352 } 353 } 354 addAll(@onNull List<JobStatus> jobs)355 void addAll(@NonNull List<JobStatus> jobs) { 356 int earliestIndex = Integer.MAX_VALUE; 357 358 for (int i = jobs.size() - 1; i >= 0; --i) { 359 final JobStatus job = jobs.get(i); 360 361 AdjustedJobStatus adjustedJobStatus = mAdjustedJobStatusPool.acquire(); 362 if (adjustedJobStatus == null) { 363 adjustedJobStatus = new AdjustedJobStatus(); 364 } 365 adjustedJobStatus.adjustedEnqueueTime = job.enqueueTime; 366 adjustedJobStatus.job = job; 367 368 int where = Collections.binarySearch(mJobs, adjustedJobStatus, sJobComparator); 369 if (where < 0) { 370 where = ~where; 371 } 372 mJobs.add(where, adjustedJobStatus); 373 if (where < mCurIndex) { 374 // Shift the current index back to make sure the new job is evaluated on the 375 // next iteration. 376 mCurIndex = where; 377 } 378 earliestIndex = Math.min(earliestIndex, where); 379 } 380 381 final int numJobs = mJobs.size(); 382 for (int i = Math.max(earliestIndex, 1); i < numJobs; ++i) { 383 final AdjustedJobStatus ajs = mJobs.get(i); 384 final AdjustedJobStatus prev = mJobs.get(i - 1); 385 ajs.adjustedEnqueueTime = 386 Math.max(ajs.adjustedEnqueueTime, prev.adjustedEnqueueTime); 387 } 388 } 389 clear()390 void clear() { 391 mJobs.clear(); 392 mCurIndex = 0; 393 } 394 contains(@onNull JobStatus job)395 boolean contains(@NonNull JobStatus job) { 396 return indexOf(job) >= 0; 397 } 398 399 /** Returns the current index of the job, or -1 if the job isn't in the list. */ indexOf(@onNull JobStatus jobStatus)400 private int indexOf(@NonNull JobStatus jobStatus) { 401 // Binary search can't guarantee returning the correct index 402 // if there are multiple jobs whose sorting comparison are 0, so we need to iterate 403 // through the entire list. 404 for (int i = 0, size = mJobs.size(); i < size; ++i) { 405 AdjustedJobStatus adjustedJobStatus = mJobs.get(i); 406 if (adjustedJobStatus.job == jobStatus) { 407 return i; 408 } 409 } 410 return -1; 411 } 412 413 @Nullable next()414 JobStatus next() { 415 if (mCurIndex >= mJobs.size()) { 416 return null; 417 } 418 return mJobs.get(mCurIndex++).job; 419 } 420 peekNextOverrideState()421 int peekNextOverrideState() { 422 if (mCurIndex >= mJobs.size()) { 423 return NO_NEXT_OVERRIDE_STATE; 424 } 425 return mJobs.get(mCurIndex).job.overrideState; 426 } 427 peekNextTimestamp()428 long peekNextTimestamp() { 429 if (mCurIndex >= mJobs.size()) { 430 return NO_NEXT_TIMESTAMP; 431 } 432 return mJobs.get(mCurIndex).adjustedEnqueueTime; 433 } 434 remove(@onNull JobStatus jobStatus)435 boolean remove(@NonNull JobStatus jobStatus) { 436 final int idx = indexOf(jobStatus); 437 if (idx < 0) { 438 // Doesn't exist... 439 return false; 440 } 441 final AdjustedJobStatus adjustedJobStatus = mJobs.remove(idx); 442 adjustedJobStatus.clear(); 443 mAdjustedJobStatusPool.release(adjustedJobStatus); 444 if (idx < mCurIndex) { 445 mCurIndex--; 446 } 447 return true; 448 } 449 450 /** 451 * Resets the internal index to point to the first JobStatus whose adjusted time is equal to 452 * or after the given timestamp. 453 */ resetIterator(long earliestEnqueueTime)454 void resetIterator(long earliestEnqueueTime) { 455 if (earliestEnqueueTime == 0 || mJobs.size() == 0) { 456 mCurIndex = 0; 457 return; 458 } 459 460 // Binary search 461 int low = 0; 462 int high = mJobs.size() - 1; 463 464 while (low < high) { 465 int mid = (low + high) >>> 1; 466 AdjustedJobStatus midVal = mJobs.get(mid); 467 468 if (midVal.adjustedEnqueueTime < earliestEnqueueTime) { 469 low = mid + 1; 470 } else if (midVal.adjustedEnqueueTime > earliestEnqueueTime) { 471 high = mid - 1; 472 } else { 473 high = mid; 474 } 475 } 476 mCurIndex = high; 477 } 478 size()479 int size() { 480 return mJobs.size(); 481 } 482 } 483 } 484