Merge "Add new sorting mechanism." into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
fad9dab6a6
@@ -0,0 +1,407 @@
|
|||||||
|
/*
|
||||||
|
* 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.server.job.controllers.JobStatus;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return Long.compare(t1, t2);
|
||||||
|
});
|
||||||
|
|
||||||
|
private int mSize = 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;
|
||||||
|
} else if (mOrderedQueues.size() == 0) {
|
||||||
|
for (int i = mCurrentQueues.size() - 1; i >= 0; --i) {
|
||||||
|
final AppJobQueue ajq = mCurrentQueues.valueAt(i);
|
||||||
|
mOrderedQueues.offer(ajq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final AppJobQueue earliestQueue = mOrderedQueues.poll();
|
||||||
|
if (earliestQueue != null) {
|
||||||
|
JobStatus job = earliestQueue.next();
|
||||||
|
mOrderedQueues.offer(earliestQueue);
|
||||||
|
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()) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() {
|
||||||
|
return mSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AppJobQueue {
|
||||||
|
static final long NO_NEXT_TIMESTAMP = -1L;
|
||||||
|
|
||||||
|
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;
|
||||||
|
// 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 job2.overrideState - job1.overrideState;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 job2Priority - job1Priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job1.lastEvaluatedBias != job2.lastEvaluatedBias) {
|
||||||
|
// Higher bias should go first.
|
||||||
|
return job2.lastEvaluatedBias - job1.lastEvaluatedBias;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job1.enqueueTime < job2.enqueueTime) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return job1.enqueueTime > job2.enqueueTime ? 1 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int indexOf(@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);
|
||||||
|
adjustedJobStatus.clear();
|
||||||
|
mAdjustedJobStatusPool.release(adjustedJobStatus);
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
JobStatus next() {
|
||||||
|
if (mCurIndex >= mJobs.size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JobStatus next = mJobs.get(mCurIndex).job;
|
||||||
|
mCurIndex++;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 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 static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import android.app.job.JobInfo;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.platform.test.annotations.LargeTest;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.util.SparseBooleanArray;
|
||||||
|
import android.util.SparseIntArray;
|
||||||
|
import android.util.SparseLongArray;
|
||||||
|
|
||||||
|
import com.android.server.job.controllers.JobStatus;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class PendingJobQueueTest {
|
||||||
|
private static final String TAG = PendingJobQueueTest.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int[] sRegJobPriorities = {
|
||||||
|
JobInfo.PRIORITY_HIGH, JobInfo.PRIORITY_DEFAULT,
|
||||||
|
JobInfo.PRIORITY_LOW, JobInfo.PRIORITY_MIN
|
||||||
|
};
|
||||||
|
|
||||||
|
private static JobInfo.Builder createJobInfo(int jobId) {
|
||||||
|
return new JobInfo.Builder(jobId, new ComponentName("foo", "bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder,
|
||||||
|
int callingUid) {
|
||||||
|
return JobStatus.createFromJobInfo(
|
||||||
|
jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAdd() {
|
||||||
|
List<JobStatus> jobs = new ArrayList<>();
|
||||||
|
jobs.add(createJobStatus("testAdd", createJobInfo(1), 1));
|
||||||
|
jobs.add(createJobStatus("testAdd", createJobInfo(2), 2));
|
||||||
|
jobs.add(createJobStatus("testAdd", createJobInfo(3).setExpedited(true), 3));
|
||||||
|
jobs.add(createJobStatus("testAdd", createJobInfo(4), 4));
|
||||||
|
jobs.add(createJobStatus("testAdd", createJobInfo(5).setExpedited(true), 5));
|
||||||
|
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
for (int i = 0; i < jobs.size(); ++i) {
|
||||||
|
jobQueue.add(jobs.get(i));
|
||||||
|
assertEquals(i + 1, jobQueue.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
JobStatus job;
|
||||||
|
while ((job = jobQueue.next()) != null) {
|
||||||
|
jobs.remove(job);
|
||||||
|
}
|
||||||
|
assertEquals(0, jobs.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddAll() {
|
||||||
|
List<JobStatus> jobs = new ArrayList<>();
|
||||||
|
jobs.add(createJobStatus("testAddAll", createJobInfo(1), 1));
|
||||||
|
jobs.add(createJobStatus("testAddAll", createJobInfo(2), 2));
|
||||||
|
jobs.add(createJobStatus("testAddAll", createJobInfo(3).setExpedited(true), 3));
|
||||||
|
jobs.add(createJobStatus("testAddAll", createJobInfo(4), 4));
|
||||||
|
jobs.add(createJobStatus("testAddAll", createJobInfo(5).setExpedited(true), 5));
|
||||||
|
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
jobQueue.addAll(jobs);
|
||||||
|
assertEquals(jobs.size(), jobQueue.size());
|
||||||
|
|
||||||
|
JobStatus job;
|
||||||
|
while ((job = jobQueue.next()) != null) {
|
||||||
|
jobs.remove(job);
|
||||||
|
}
|
||||||
|
assertEquals(0, jobs.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClear() {
|
||||||
|
List<JobStatus> jobs = new ArrayList<>();
|
||||||
|
jobs.add(createJobStatus("testClear", createJobInfo(1), 1));
|
||||||
|
jobs.add(createJobStatus("testClear", createJobInfo(2), 2));
|
||||||
|
jobs.add(createJobStatus("testClear", createJobInfo(3).setExpedited(true), 3));
|
||||||
|
jobs.add(createJobStatus("testClear", createJobInfo(4), 4));
|
||||||
|
jobs.add(createJobStatus("testClear", createJobInfo(5).setExpedited(true), 5));
|
||||||
|
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
jobQueue.addAll(jobs);
|
||||||
|
assertEquals(jobs.size(), jobQueue.size());
|
||||||
|
assertNotNull(jobQueue.next());
|
||||||
|
|
||||||
|
jobQueue.clear();
|
||||||
|
assertEquals(0, jobQueue.size());
|
||||||
|
assertNull(jobQueue.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemove() {
|
||||||
|
List<JobStatus> jobs = new ArrayList<>();
|
||||||
|
jobs.add(createJobStatus("testRemove", createJobInfo(1), 1));
|
||||||
|
jobs.add(createJobStatus("testRemove", createJobInfo(2), 2));
|
||||||
|
jobs.add(createJobStatus("testRemove", createJobInfo(3).setExpedited(true), 3));
|
||||||
|
jobs.add(createJobStatus("testRemove", createJobInfo(4), 4));
|
||||||
|
jobs.add(createJobStatus("testRemove", createJobInfo(5).setExpedited(true), 5));
|
||||||
|
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
jobQueue.addAll(jobs);
|
||||||
|
|
||||||
|
for (int i = 0; i < jobs.size(); ++i) {
|
||||||
|
jobQueue.remove(jobs.get(i));
|
||||||
|
assertEquals(jobs.size() - i - 1, jobQueue.size());
|
||||||
|
}
|
||||||
|
assertNull(jobQueue.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPendingJobSorting() {
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
|
||||||
|
// First letter in job variable name indicate regular (r) or expedited (e).
|
||||||
|
// Capital letters in job variable name indicate the app/UID.
|
||||||
|
// Numbers in job variable name indicate the enqueue time.
|
||||||
|
// Expected sort order:
|
||||||
|
// eA7 > rA1 > eB6 > rB2 > eC3 > rD4 > eE5 > eF9 > rF8 > eC11 > rC10 > rG12 > rG13 > eE14
|
||||||
|
// Intentions:
|
||||||
|
// * A jobs let us test skipping both regular and expedited jobs of other apps
|
||||||
|
// * B jobs let us test skipping only regular job of another app without going too far
|
||||||
|
// * C jobs test that regular jobs don't skip over other app's jobs and that EJs only
|
||||||
|
// skip up to level of the earliest regular job
|
||||||
|
// * E jobs test that expedited jobs don't skip the line when the app has no regular jobs
|
||||||
|
// * F jobs test correct expedited/regular ordering doesn't push jobs too high in list
|
||||||
|
// * G jobs test correct ordering for regular jobs
|
||||||
|
// * H job tests correct behavior when enqueue times are the same
|
||||||
|
JobStatus rA1 = createJobStatus("testPendingJobSorting", createJobInfo(1), 1);
|
||||||
|
JobStatus rB2 = createJobStatus("testPendingJobSorting", createJobInfo(2), 2);
|
||||||
|
JobStatus eC3 = createJobStatus("testPendingJobSorting",
|
||||||
|
createJobInfo(3).setExpedited(true), 3);
|
||||||
|
JobStatus rD4 = createJobStatus("testPendingJobSorting", createJobInfo(4), 4);
|
||||||
|
JobStatus eE5 = createJobStatus("testPendingJobSorting",
|
||||||
|
createJobInfo(5).setExpedited(true), 5);
|
||||||
|
JobStatus eB6 = createJobStatus("testPendingJobSorting",
|
||||||
|
createJobInfo(6).setExpedited(true), 2);
|
||||||
|
JobStatus eA7 = createJobStatus("testPendingJobSorting",
|
||||||
|
createJobInfo(7).setExpedited(true), 1);
|
||||||
|
JobStatus rH8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 8);
|
||||||
|
JobStatus rF8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 6);
|
||||||
|
JobStatus eF9 = createJobStatus("testPendingJobSorting",
|
||||||
|
createJobInfo(9).setExpedited(true), 6);
|
||||||
|
JobStatus rC10 = createJobStatus("testPendingJobSorting", createJobInfo(10), 3);
|
||||||
|
JobStatus eC11 = createJobStatus("testPendingJobSorting",
|
||||||
|
createJobInfo(11).setExpedited(true), 3);
|
||||||
|
JobStatus rG12 = createJobStatus("testPendingJobSorting", createJobInfo(12), 7);
|
||||||
|
JobStatus rG13 = createJobStatus("testPendingJobSorting", createJobInfo(13), 7);
|
||||||
|
JobStatus eE14 = createJobStatus("testPendingJobSorting",
|
||||||
|
createJobInfo(14).setExpedited(true), 5);
|
||||||
|
|
||||||
|
rA1.enqueueTime = 10;
|
||||||
|
rB2.enqueueTime = 20;
|
||||||
|
eC3.enqueueTime = 30;
|
||||||
|
rD4.enqueueTime = 40;
|
||||||
|
eE5.enqueueTime = 50;
|
||||||
|
eB6.enqueueTime = 60;
|
||||||
|
eA7.enqueueTime = 70;
|
||||||
|
rF8.enqueueTime = 80;
|
||||||
|
rH8.enqueueTime = 80;
|
||||||
|
eF9.enqueueTime = 90;
|
||||||
|
rC10.enqueueTime = 100;
|
||||||
|
eC11.enqueueTime = 110;
|
||||||
|
rG12.enqueueTime = 120;
|
||||||
|
rG13.enqueueTime = 130;
|
||||||
|
eE14.enqueueTime = 140;
|
||||||
|
|
||||||
|
// Add in random order so sorting is apparent.
|
||||||
|
jobQueue.add(eC3);
|
||||||
|
jobQueue.add(eE5);
|
||||||
|
jobQueue.add(rA1);
|
||||||
|
jobQueue.add(rG13);
|
||||||
|
jobQueue.add(rD4);
|
||||||
|
jobQueue.add(eA7);
|
||||||
|
jobQueue.add(rG12);
|
||||||
|
jobQueue.add(rH8);
|
||||||
|
jobQueue.add(rF8);
|
||||||
|
jobQueue.add(eB6);
|
||||||
|
jobQueue.add(eE14);
|
||||||
|
jobQueue.add(eF9);
|
||||||
|
jobQueue.add(rB2);
|
||||||
|
jobQueue.add(rC10);
|
||||||
|
jobQueue.add(eC11);
|
||||||
|
|
||||||
|
checkPendingJobInvariants(jobQueue);
|
||||||
|
final JobStatus[] expectedOrder = new JobStatus[]{
|
||||||
|
eA7, rA1, eB6, rB2, eC3, rD4, eE5, eF9, rH8, rF8, eC11, rC10, rG12, rG13, eE14};
|
||||||
|
int idx = 0;
|
||||||
|
JobStatus job;
|
||||||
|
while ((job = jobQueue.next()) != null) {
|
||||||
|
assertEquals("List wasn't correctly sorted @ index " + idx,
|
||||||
|
expectedOrder[idx].getJobId(), job.getJobId());
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPendingJobSorting_Random() {
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
Random random = new Random(1); // Always use the same series of pseudo random values.
|
||||||
|
|
||||||
|
for (int i = 0; i < 5000; ++i) {
|
||||||
|
JobStatus job = createJobStatus("testPendingJobSorting_Random",
|
||||||
|
createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(250));
|
||||||
|
job.enqueueTime = random.nextInt(1_000_000);
|
||||||
|
jobQueue.add(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPendingJobInvariants(jobQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPendingJobSortingTransitivity() {
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
// Always use the same series of pseudo random values.
|
||||||
|
for (int seed : new int[]{1337, 7357, 606, 6357, 41106010, 3, 2, 1}) {
|
||||||
|
Random random = new Random(seed);
|
||||||
|
|
||||||
|
jobQueue.clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < 300; ++i) {
|
||||||
|
JobStatus job = createJobStatus("testPendingJobSortingTransitivity",
|
||||||
|
createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(50));
|
||||||
|
job.enqueueTime = random.nextInt(1_000_000);
|
||||||
|
job.overrideState = random.nextInt(4);
|
||||||
|
jobQueue.add(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPendingJobInvariants(jobQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@LargeTest
|
||||||
|
public void testPendingJobSortingTransitivity_Concentrated() {
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
// Always use the same series of pseudo random values.
|
||||||
|
for (int seed : new int[]{1337, 6000, 637739, 6357, 1, 7, 13}) {
|
||||||
|
Random random = new Random(seed);
|
||||||
|
|
||||||
|
jobQueue.clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < 300; ++i) {
|
||||||
|
JobStatus job = createJobStatus("testPendingJobSortingTransitivity_Concentrated",
|
||||||
|
createJobInfo(i).setExpedited(random.nextFloat() < .03),
|
||||||
|
random.nextInt(20));
|
||||||
|
job.enqueueTime = random.nextInt(250);
|
||||||
|
job.overrideState = random.nextFloat() < .01
|
||||||
|
? JobStatus.OVERRIDE_SORTING : JobStatus.OVERRIDE_NONE;
|
||||||
|
jobQueue.add(job);
|
||||||
|
Log.d(TAG, testJobToString(job));
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPendingJobInvariants(jobQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPendingJobSorting_Random_WithPriority() {
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
Random random = new Random(1); // Always use the same series of pseudo random values.
|
||||||
|
|
||||||
|
for (int i = 0; i < 5000; ++i) {
|
||||||
|
final boolean isEj = random.nextBoolean();
|
||||||
|
final int priority;
|
||||||
|
if (isEj) {
|
||||||
|
priority = random.nextBoolean() ? JobInfo.PRIORITY_MAX : JobInfo.PRIORITY_HIGH;
|
||||||
|
} else {
|
||||||
|
priority = sRegJobPriorities[random.nextInt(sRegJobPriorities.length)];
|
||||||
|
}
|
||||||
|
JobStatus job = createJobStatus("testPendingJobSorting_Random_WithPriority",
|
||||||
|
createJobInfo(i).setExpedited(isEj).setPriority(priority),
|
||||||
|
random.nextInt(250));
|
||||||
|
job.enqueueTime = random.nextInt(1_000_000);
|
||||||
|
jobQueue.add(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPendingJobInvariants(jobQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPendingJobSortingTransitivity_WithPriority() {
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
// Always use the same series of pseudo random values.
|
||||||
|
for (int seed : new int[]{1337, 7357, 606, 6357, 41106010, 3, 2, 1}) {
|
||||||
|
Random random = new Random(seed);
|
||||||
|
|
||||||
|
jobQueue.clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < 300; ++i) {
|
||||||
|
final boolean isEj = random.nextBoolean();
|
||||||
|
final int priority;
|
||||||
|
if (isEj) {
|
||||||
|
priority = random.nextBoolean() ? JobInfo.PRIORITY_MAX : JobInfo.PRIORITY_HIGH;
|
||||||
|
} else {
|
||||||
|
priority = sRegJobPriorities[random.nextInt(sRegJobPriorities.length)];
|
||||||
|
}
|
||||||
|
JobStatus job = createJobStatus("testPendingJobSortingTransitivity_WithPriority",
|
||||||
|
createJobInfo(i).setExpedited(isEj).setPriority(priority),
|
||||||
|
random.nextInt(50));
|
||||||
|
job.enqueueTime = random.nextInt(1_000_000);
|
||||||
|
job.overrideState = random.nextInt(4);
|
||||||
|
jobQueue.add(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPendingJobInvariants(jobQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@LargeTest
|
||||||
|
public void testPendingJobSortingTransitivity_Concentrated_WithPriority() {
|
||||||
|
PendingJobQueue jobQueue = new PendingJobQueue();
|
||||||
|
// Always use the same series of pseudo random values.
|
||||||
|
for (int seed : new int[]{1337, 6000, 637739, 6357, 1, 7, 13}) {
|
||||||
|
Random random = new Random(seed);
|
||||||
|
|
||||||
|
jobQueue.clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < 300; ++i) {
|
||||||
|
final boolean isEj = random.nextFloat() < .03;
|
||||||
|
final int priority;
|
||||||
|
if (isEj) {
|
||||||
|
priority = random.nextBoolean() ? JobInfo.PRIORITY_MAX : JobInfo.PRIORITY_HIGH;
|
||||||
|
} else {
|
||||||
|
priority = sRegJobPriorities[random.nextInt(sRegJobPriorities.length)];
|
||||||
|
}
|
||||||
|
JobStatus job = createJobStatus(
|
||||||
|
"testPendingJobSortingTransitivity_Concentrated_WithPriority",
|
||||||
|
createJobInfo(i).setExpedited(isEj).setPriority(priority),
|
||||||
|
random.nextInt(20));
|
||||||
|
job.enqueueTime = random.nextInt(250);
|
||||||
|
job.overrideState = random.nextFloat() < .01
|
||||||
|
? JobStatus.OVERRIDE_SORTING : JobStatus.OVERRIDE_NONE;
|
||||||
|
jobQueue.add(job);
|
||||||
|
Log.d(TAG, testJobToString(job));
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPendingJobInvariants(jobQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkPendingJobInvariants(PendingJobQueue jobQueue) {
|
||||||
|
final SparseBooleanArray regJobSeen = new SparseBooleanArray();
|
||||||
|
final SparseIntArray lastOverrideStateSeen = new SparseIntArray();
|
||||||
|
// Latest priority enqueue times seen for each priority for each app.
|
||||||
|
final SparseArray<SparseLongArray> latestPriorityRegEnqueueTimesPerUid =
|
||||||
|
new SparseArray<>();
|
||||||
|
final SparseArray<SparseLongArray> latestPriorityEjEnqueueTimesPerUid = new SparseArray<>();
|
||||||
|
final int noEntry = -1;
|
||||||
|
|
||||||
|
JobStatus job;
|
||||||
|
jobQueue.resetIterator();
|
||||||
|
while ((job = jobQueue.next()) != null) {
|
||||||
|
final int uid = job.getSourceUid();
|
||||||
|
|
||||||
|
// Invariant #1: All jobs (for a UID) are sorted by override state
|
||||||
|
// Invariant #2: All jobs (for a UID) are sorted by priority order
|
||||||
|
// Invariant #3: Jobs (for a UID) with the same priority are sorted by enqueue time.
|
||||||
|
// Invariant #4: EJs (for a UID) should be before regular jobs
|
||||||
|
|
||||||
|
final int prevOverrideState = lastOverrideStateSeen.get(uid, noEntry);
|
||||||
|
lastOverrideStateSeen.put(uid, job.overrideState);
|
||||||
|
if (prevOverrideState == noEntry) {
|
||||||
|
// First job for UID
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invariant 1
|
||||||
|
if (prevOverrideState != job.overrideState) {
|
||||||
|
assertTrue(prevOverrideState > job.overrideState);
|
||||||
|
// Override state can make ordering weird. Clear the other cached states for this
|
||||||
|
// UID to avoid confusion in the other checks.
|
||||||
|
latestPriorityEjEnqueueTimesPerUid.remove(uid);
|
||||||
|
latestPriorityRegEnqueueTimesPerUid.remove(uid);
|
||||||
|
regJobSeen.delete(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int priority = job.getEffectivePriority();
|
||||||
|
final SparseArray<SparseLongArray> latestPriorityEnqueueTimesPerUid =
|
||||||
|
job.isRequestedExpeditedJob()
|
||||||
|
? latestPriorityEjEnqueueTimesPerUid
|
||||||
|
: latestPriorityRegEnqueueTimesPerUid;
|
||||||
|
SparseLongArray latestPriorityEnqueueTimes = latestPriorityEnqueueTimesPerUid.get(uid);
|
||||||
|
if (latestPriorityEnqueueTimes != null) {
|
||||||
|
// Invariant 2
|
||||||
|
for (int p = priority - 1; p >= JobInfo.PRIORITY_MIN; --p) {
|
||||||
|
// If we haven't seen the priority, there shouldn't be an entry in the array.
|
||||||
|
assertEquals("Jobs not properly sorted by priority for uid " + uid,
|
||||||
|
noEntry, latestPriorityEnqueueTimes.get(p, noEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invariant 3
|
||||||
|
final long lastSeenPriorityEnqueueTime =
|
||||||
|
latestPriorityEnqueueTimes.get(priority, noEntry);
|
||||||
|
if (lastSeenPriorityEnqueueTime != noEntry) {
|
||||||
|
assertTrue("Jobs with same priority not sorted by enqueue time: "
|
||||||
|
+ lastSeenPriorityEnqueueTime + " vs " + job.enqueueTime,
|
||||||
|
lastSeenPriorityEnqueueTime <= job.enqueueTime);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
latestPriorityEnqueueTimes = new SparseLongArray();
|
||||||
|
latestPriorityEnqueueTimesPerUid.put(uid, latestPriorityEnqueueTimes);
|
||||||
|
}
|
||||||
|
latestPriorityEnqueueTimes.put(priority, job.enqueueTime);
|
||||||
|
|
||||||
|
// Invariant 4
|
||||||
|
if (!job.isRequestedExpeditedJob()) {
|
||||||
|
regJobSeen.put(uid, true);
|
||||||
|
} else if (regJobSeen.get(uid)) {
|
||||||
|
fail("UID " + uid + " had an EJ ordered after a regular job");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String testJobToString(JobStatus job) {
|
||||||
|
return "testJob " + job.getSourceUid() + "/" + job.getJobId()
|
||||||
|
+ "/o" + job.overrideState
|
||||||
|
+ "/p" + job.getEffectivePriority()
|
||||||
|
+ "/b" + job.lastEvaluatedBias
|
||||||
|
+ "/" + job.isRequestedExpeditedJob() + "@" + job.enqueueTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user