Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7d154917 authored by Kweku Adams's avatar Kweku Adams
Browse files

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
parent b0ea1d0b
Loading
Loading
Loading
Loading
+52 −3
Original line number Diff line number Diff line
@@ -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 int numQueues = mOrderedQueues.size();
        if (numQueues == 0) {
            return null;
        }
        final AppJobQueue earliestQueue = mOrderedQueues.poll();

        // 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();
            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;
    }
+20 −4
Original line number Diff line number Diff line
@@ -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) {