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