From 7d15491708122a8d0a4aae7a3af48ffeb02a7cac Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Tue, 22 Mar 2022 18:49:48 +0000 Subject: [PATCH] Optimize pending job queue iteration. Batch the jobs pulled during iteration so that we pull several of an app's jobs from the queue sequentially (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)) for A <= 33, where A = # apps and J = average # jobs per app. Bug: 204924801 Test: atest FrameworksServicesTests:PendingJobQueueTest Change-Id: I4d3dc17afb4d4ab0fb573d75a319bdb20a68c634 --- .../android/server/job/PendingJobQueue.java | 55 ++++++++++++++++++- .../server/job/PendingJobQueueTest.java | 24 ++++++-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java index 993e178de0eef..b01b80734e640 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java +++ b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java @@ -21,6 +21,7 @@ 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; @@ -59,6 +60,21 @@ class PendingJobQueue { 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) { @@ -132,16 +148,44 @@ class PendingJobQueue { 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 AppJobQueue earliestQueue = mOrderedQueues.poll(); + 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) { - JobStatus job = earliestQueue.next(); - mOrderedQueues.offer(earliestQueue); + final JobStatus job = earliestQueue.next(); + // Change the front of the queue if we've pulled pullLimit jobs from the current head + // or the current head has no more jobs to provide. + if (++mPullCount >= pullLimit + || 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; @@ -179,6 +223,11 @@ class PendingJobQueue { mNeedToResetIterators = true; } + @VisibleForTesting + void setOptimizeIteration(boolean optimize) { + mOptimizeIteration = optimize; + } + int size() { return mSize; } diff --git a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java index 150e3c60b91ee..3e1769ad11a15 100644 --- a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java +++ b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java @@ -212,13 +212,26 @@ public class PendingJobQueueTest { 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; + final JobStatus[] expectedPureOrder = new JobStatus[]{ + eC3, rD4, eE5, eB6, rB2, eA7, rA1, rH8, eF9, rF8, eC11, rC10, rG12, rG13, eE14}; + int idx = 0; + jobQueue.setOptimizeIteration(false); + jobQueue.resetIterator(); while ((job = jobQueue.next()) != null) { assertEquals("List wasn't correctly sorted @ index " + idx, - expectedOrder[idx].getJobId(), job.getJobId()); + expectedPureOrder[idx].getJobId(), job.getJobId()); + idx++; + } + + final JobStatus[] expectedOptimizedOrder = new JobStatus[]{ + eC3, eC11, rD4, eE5, eE14, eB6, rB2, eA7, rA1, rH8, eF9, rF8, rC10, rG12, rG13}; + idx = 0; + jobQueue.setOptimizeIteration(true); + jobQueue.resetIterator(); + while ((job = jobQueue.next()) != null) { + assertEquals("Optimized list wasn't correctly sorted @ index " + idx, + expectedOptimizedOrder[idx].getJobId(), job.getJobId()); idx++; } } @@ -380,7 +393,9 @@ public class PendingJobQueueTest { JobStatus job; jobQueue.resetIterator(); + int count = 0; while ((job = jobQueue.next()) != null) { + count++; final int uid = job.getSourceUid(); // Invariant #1: All jobs (for a UID) are sorted by override state @@ -440,6 +455,7 @@ public class PendingJobQueueTest { fail("UID " + uid + " had an EJ ordered after a regular job"); } } + assertEquals("Iterator didn't go through all jobs", jobQueue.size(), count); } private static String testJobToString(JobStatus job) {