Add new sorting mechanism.

Create a pending job sorting mechanism that is more like
topological-sort. The new system sorts pending jobs for each
individual app and then picks from each app's list based on the adjusted
"enqueue" time to retain some fairness. This new sorting may result in
an app's jobs being run more closely together (which helps reduce the
number of separate process startups).

Runtime changes (A=# of apps, J=average # jobs per app):
                 Previous implementation      New implementation
Sorting:              A*J*log(A*J)                A*J*log(J)
Insertion:             log(A*J)                    log(A*J)+J
Remove(Object):          A*J                       log(A*J)
Iteration:               A*J                      A*J*log(A)
Contains:                A*J                       log(A*J)

Bug: 141645789
Bug: 204924801
Bug: 223437753
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job
Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job
Change-Id: Ie077c5ad1cd6c0bd4ff4a9f1e2f000ed99c048b0
This commit is contained in:
Kweku Adams 2022-03-21 14:18:36 +00:00
parent c6c57cdef8
commit fcea75f520
2 changed files with 859 additions and 0 deletions

View File

@ -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();
}
}
}

View File

@ -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;
}
}