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