blob: 4f4096f69ad5e19258b789a5f5003b5ea50f2918 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.job;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Pools;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.job.controllers.JobStatus;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.PriorityQueue;
/**
* A utility class to maintain a sorted list of currently pending jobs. The sorting system is
* modeled after topological sort, so the returned order may not always be consistent.
*/
class PendingJobQueue {
private final Pools.Pool<AppJobQueue> mAppJobQueuePool = new Pools.SimplePool<>(8);
/** Set of currently used queues, keyed by source UID. */
private final SparseArray<AppJobQueue> mCurrentQueues = new SparseArray<>();
/**
* Same set of AppJobQueues as in {@link #mCurrentQueues}, but ordered by the next timestamp
* to make iterating through the job list faster.
*/
private final PriorityQueue<AppJobQueue> mOrderedQueues = new PriorityQueue<>(
(ajq1, ajq2) -> {
final long t1 = ajq1.peekNextTimestamp();
final long t2 = ajq2.peekNextTimestamp();
if (t1 == AppJobQueue.NO_NEXT_TIMESTAMP) {
if (t2 == AppJobQueue.NO_NEXT_TIMESTAMP) {
return 0;
}
return 1;
} else if (t2 == AppJobQueue.NO_NEXT_TIMESTAMP) {
return -1;
}
final int o1 = ajq1.peekNextOverrideState();
final int o2 = ajq2.peekNextOverrideState();
if (o1 != o2) {
// Higher override state (OVERRIDE_FULL) should be before lower state
// (OVERRIDE_SOFT)
return Integer.compare(o2, o1);
}
return Long.compare(t1, t2);
});
private int mSize = 0;
/**
* Whether to batch iteration so that we pull several of an app's jobs from the queue at the
* same time (resulting in some out of order pulls) instead of pulling purely based on the
* sort order. Batching it this way will mean we try to run several jobs of the same app at the
* same, resulting in fewer process restarts, and can allow the iteration runtime to amortize
* to O(A*J) instead of O(A*J*log(A)), where A = # apps and J = average # jobs per app.
*/
private boolean mOptimizeIteration = true;
/**
* Number of jobs that have been pulled from the queue in succession. Used when
* {@link #mOptimizeIteration} is true to know when to switch to the next AppJobQueue.
*/
private int mPullCount = 0;
private boolean mNeedToResetIterators = false;
void add(@NonNull JobStatus job) {
final AppJobQueue ajq = getAppJobQueue(job.getSourceUid(), true);
final long prevTimestamp = ajq.peekNextTimestamp();
ajq.add(job);
mSize++;
if (prevTimestamp != ajq.peekNextTimestamp()) {
mOrderedQueues.remove(ajq);
mOrderedQueues.offer(ajq);
}
}
void addAll(@NonNull List<JobStatus> jobs) {
final SparseArray<List<JobStatus>> jobsByUid = new SparseArray<>();
for (int i = jobs.size() - 1; i >= 0; --i) {
final JobStatus job = jobs.get(i);
List<JobStatus> appJobs = jobsByUid.get(job.getSourceUid());
if (appJobs == null) {
appJobs = new ArrayList<>();
jobsByUid.put(job.getSourceUid(), appJobs);
}
appJobs.add(job);
}
for (int i = jobsByUid.size() - 1; i >= 0; --i) {
final AppJobQueue ajq = getAppJobQueue(jobsByUid.keyAt(i), true);
ajq.addAll(jobsByUid.valueAt(i));
}
mSize += jobs.size();
mOrderedQueues.clear();
}
void clear() {
mSize = 0;
for (int i = mCurrentQueues.size() - 1; i >= 0; --i) {
final AppJobQueue ajq = mCurrentQueues.valueAt(i);
ajq.clear();
mAppJobQueuePool.release(ajq);
}
mCurrentQueues.clear();
mOrderedQueues.clear();
}
boolean contains(@NonNull JobStatus job) {
final AppJobQueue ajq = mCurrentQueues.get(job.getSourceUid());
if (ajq == null) {
return false;
}
return ajq.contains(job);
}
private AppJobQueue getAppJobQueue(int uid, boolean create) {
AppJobQueue ajq = mCurrentQueues.get(uid);
if (ajq == null && create) {
ajq = mAppJobQueuePool.acquire();
if (ajq == null) {
ajq = new AppJobQueue();
}
mCurrentQueues.put(uid, ajq);
}
return ajq;
}
@Nullable
JobStatus next() {
if (mNeedToResetIterators) {
mOrderedQueues.clear();
for (int i = mCurrentQueues.size() - 1; i >= 0; --i) {
final AppJobQueue ajq = mCurrentQueues.valueAt(i);
ajq.resetIterator(0);
mOrderedQueues.offer(ajq);
}
mNeedToResetIterators = false;
// Reset the pull count when the front of the queue changes.
mPullCount = 0;
} else if (mOrderedQueues.size() == 0) {
// Something significant changed, so the priority queue was cleared. Lazily regenerate
// the queue.
for (int i = mCurrentQueues.size() - 1; i >= 0; --i) {
final AppJobQueue ajq = mCurrentQueues.valueAt(i);
mOrderedQueues.offer(ajq);
}
// Reset the pull count when the front of the queue changes.
mPullCount = 0;
}
final int numQueues = mOrderedQueues.size();
if (numQueues == 0) {
return null;
}
// Increase the pull limit at a slightly faster rate than log(A) increases (until A>=33).
// The pull limit increase is intended to balance fairness (one app can't starve out others)
// with efficiency (reducing process restarts).
// 1-4 apps --> pullLimit = 1, 5-8 apps --> pullLimit = 2, 9+ apps --> pullLimit = 3
final int pullLimit = mOptimizeIteration ? Math.min(3, ((numQueues - 1) >>> 2) + 1) : 1;
final AppJobQueue earliestQueue = mOrderedQueues.peek();
if (earliestQueue != null) {
final JobStatus job = earliestQueue.next();
// Change the front of the queue if we've pulled pullLimit jobs from the current head
// or we're dealing with test jobs
// or the current head has no more jobs to provide.
if (++mPullCount >= pullLimit
|| (job != null && earliestQueue.peekNextOverrideState() != job.overrideState)
|| earliestQueue.peekNextTimestamp() == AppJobQueue.NO_NEXT_TIMESTAMP) {
mOrderedQueues.poll();
if (earliestQueue.peekNextTimestamp() != AppJobQueue.NO_NEXT_TIMESTAMP) {
// No need to put back in the queue if it has no more jobs to give.
mOrderedQueues.offer(earliestQueue);
}
// Reset the pull count when the front of the queue changes.
mPullCount = 0;
}
return job;
}
return null;
}
boolean remove(@NonNull JobStatus job) {
final AppJobQueue ajq = getAppJobQueue(job.getSourceUid(), false);
if (ajq == null) {
return false;
}
final long prevTimestamp = ajq.peekNextTimestamp();
if (!ajq.remove(job)) {
return false;
}
mSize--;
if (ajq.size() == 0) {
mCurrentQueues.remove(job.getSourceUid());
mOrderedQueues.remove(ajq);
ajq.clear();
mAppJobQueuePool.release(ajq);
} else if (prevTimestamp != ajq.peekNextTimestamp()) {
// Removing the job changed the "next timestamp" in the queue, so we need to reinsert
// it to fix the ordering.
mOrderedQueues.remove(ajq);
mOrderedQueues.offer(ajq);
}
return true;
}
/** Resets the iterating index to the front of the queue. */
void resetIterator() {
// Lazily reset the iterating indices (avoid looping through all the current queues until
// absolutely necessary).
mNeedToResetIterators = true;
}
@VisibleForTesting
void setOptimizeIteration(boolean optimize) {
mOptimizeIteration = optimize;
}
int size() {
return mSize;
}
private static final class AppJobQueue {
static final long NO_NEXT_TIMESTAMP = -1L;
static final int NO_NEXT_OVERRIDE_STATE = -1;
private static class AdjustedJobStatus {
public long adjustedEnqueueTime;
public JobStatus job;
void clear() {
adjustedEnqueueTime = 0;
job = null;
}
}
private static final Comparator<AdjustedJobStatus> sJobComparator = (aj1, aj2) -> {
if (aj1 == aj2) {
return 0;
}
final JobStatus job1 = aj1.job;
final JobStatus job2 = aj2.job;
if (job1 == job2) {
return 0;
}
// Jobs with an override state set (via adb) should be put first as tests/developers
// expect the jobs to run immediately.
if (job1.overrideState != job2.overrideState) {
// Higher override state (OVERRIDE_FULL) should be before lower state
// (OVERRIDE_SOFT)
return Integer.compare(job2.overrideState, job1.overrideState);
}
final boolean job1UI = job1.getJob().isUserInitiated();
final boolean job2UI = job2.getJob().isUserInitiated();
if (job1UI != job2UI) {
// Attempt to run user-initiated jobs ahead of all other jobs.
return job1UI ? -1 : 1;
}
final boolean job1EJ = job1.isRequestedExpeditedJob();
final boolean job2EJ = job2.isRequestedExpeditedJob();
if (job1EJ != job2EJ) {
// Attempt to run requested expedited jobs ahead of regular jobs, regardless of
// expedited job quota.
return job1EJ ? -1 : 1;
}
if (Objects.equals(job1.getNamespace(), job2.getNamespace())) {
final int job1Priority = job1.getEffectivePriority();
final int job2Priority = job2.getEffectivePriority();
if (job1Priority != job2Priority) {
// Use the priority set by an app for intra-app job ordering. Higher
// priority should be before lower priority.
return Integer.compare(job2Priority, job1Priority);
}
}
if (job1.lastEvaluatedBias != job2.lastEvaluatedBias) {
// Higher bias should go first.
return Integer.compare(job2.lastEvaluatedBias, job1.lastEvaluatedBias);
}
return Long.compare(job1.enqueueTime, job2.enqueueTime);
};
private static final Pools.Pool<AdjustedJobStatus> mAdjustedJobStatusPool =
new Pools.SimplePool<>(16);
private final List<AdjustedJobStatus> mJobs = new ArrayList<>();
private int mCurIndex = 0;
void add(@NonNull JobStatus jobStatus) {
AdjustedJobStatus adjustedJobStatus = mAdjustedJobStatusPool.acquire();
if (adjustedJobStatus == null) {
adjustedJobStatus = new AdjustedJobStatus();
}
adjustedJobStatus.adjustedEnqueueTime = jobStatus.enqueueTime;
adjustedJobStatus.job = jobStatus;
int where = Collections.binarySearch(mJobs, adjustedJobStatus, sJobComparator);
if (where < 0) {
where = ~where;
}
mJobs.add(where, adjustedJobStatus);
if (where < mCurIndex) {
// Shift the current index back to make sure the new job is evaluated on the next
// iteration.
mCurIndex = where;
}
if (where > 0) {
final long prevTimestamp = mJobs.get(where - 1).adjustedEnqueueTime;
adjustedJobStatus.adjustedEnqueueTime =
Math.max(prevTimestamp, adjustedJobStatus.adjustedEnqueueTime);
}
final int numJobs = mJobs.size();
if (where < numJobs - 1) {
// Potentially need to adjust following job timestamps as well.
for (int i = where; i < numJobs; ++i) {
final AdjustedJobStatus ajs = mJobs.get(i);
if (adjustedJobStatus.adjustedEnqueueTime < ajs.adjustedEnqueueTime) {
// No further need to adjust.
break;
}
ajs.adjustedEnqueueTime = adjustedJobStatus.adjustedEnqueueTime;
}
}
}
void addAll(@NonNull List<JobStatus> jobs) {
int earliestIndex = Integer.MAX_VALUE;
for (int i = jobs.size() - 1; i >= 0; --i) {
final JobStatus job = jobs.get(i);
AdjustedJobStatus adjustedJobStatus = mAdjustedJobStatusPool.acquire();
if (adjustedJobStatus == null) {
adjustedJobStatus = new AdjustedJobStatus();
}
adjustedJobStatus.adjustedEnqueueTime = job.enqueueTime;
adjustedJobStatus.job = job;
int where = Collections.binarySearch(mJobs, adjustedJobStatus, sJobComparator);
if (where < 0) {
where = ~where;
}
mJobs.add(where, adjustedJobStatus);
if (where < mCurIndex) {
// Shift the current index back to make sure the new job is evaluated on the
// next iteration.
mCurIndex = where;
}
earliestIndex = Math.min(earliestIndex, where);
}
final int numJobs = mJobs.size();
for (int i = Math.max(earliestIndex, 1); i < numJobs; ++i) {
final AdjustedJobStatus ajs = mJobs.get(i);
final AdjustedJobStatus prev = mJobs.get(i - 1);
ajs.adjustedEnqueueTime =
Math.max(ajs.adjustedEnqueueTime, prev.adjustedEnqueueTime);
}
}
void clear() {
mJobs.clear();
mCurIndex = 0;
}
boolean contains(@NonNull JobStatus job) {
return indexOf(job) >= 0;
}
/** Returns the current index of the job, or -1 if the job isn't in the list. */
private int indexOf(@NonNull JobStatus jobStatus) {
// Binary search can't guarantee returning the correct index
// if there are multiple jobs whose sorting comparison are 0, so we need to iterate
// through the entire list.
for (int i = 0, size = mJobs.size(); i < size; ++i) {
AdjustedJobStatus adjustedJobStatus = mJobs.get(i);
if (adjustedJobStatus.job == jobStatus) {
return i;
}
}
return -1;
}
@Nullable
JobStatus next() {
if (mCurIndex >= mJobs.size()) {
return null;
}
return mJobs.get(mCurIndex++).job;
}
int peekNextOverrideState() {
if (mCurIndex >= mJobs.size()) {
return NO_NEXT_OVERRIDE_STATE;
}
return mJobs.get(mCurIndex).job.overrideState;
}
long peekNextTimestamp() {
if (mCurIndex >= mJobs.size()) {
return NO_NEXT_TIMESTAMP;
}
return mJobs.get(mCurIndex).adjustedEnqueueTime;
}
boolean remove(@NonNull JobStatus jobStatus) {
final int idx = indexOf(jobStatus);
if (idx < 0) {
// Doesn't exist...
return false;
}
final AdjustedJobStatus adjustedJobStatus = mJobs.remove(idx);
adjustedJobStatus.clear();
mAdjustedJobStatusPool.release(adjustedJobStatus);
if (idx < mCurIndex) {
mCurIndex--;
}
return true;
}
/**
* Resets the internal index to point to the first JobStatus whose adjusted time is equal to
* or after the given timestamp.
*/
void resetIterator(long earliestEnqueueTime) {
if (earliestEnqueueTime == 0 || mJobs.size() == 0) {
mCurIndex = 0;
return;
}
// Binary search
int low = 0;
int high = mJobs.size() - 1;
while (low < high) {
int mid = (low + high) >>> 1;
AdjustedJobStatus midVal = mJobs.get(mid);
if (midVal.adjustedEnqueueTime < earliestEnqueueTime) {
low = mid + 1;
} else if (midVal.adjustedEnqueueTime > earliestEnqueueTime) {
high = mid - 1;
} else {
high = mid;
}
}
mCurIndex = high;
}
int size() {
return mJobs.size();
}
}
}