• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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