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

Commit 0d46c77e authored by Kweku Adams's avatar Kweku Adams
Browse files

Fix bug and migrate test.

Migrate WorkCountTrackerTest to specify WorkType limits explicitly. Also
fix bug in how we assign slots to pending & already running jobs.

Bug: 141645789
Bug: 171305774
Bug: 178119369
Test: atest CtsJobSchedulerTestCases
Test: atest FrameworksMockingServicesTests:JobSchedulerServiceTest
Test: atest FrameworksServicesTests:PrioritySchedulingTest
Test: atest FrameworksServicesTests:WorkCountTrackerTest
Test: atest FrameworksServicesTests:WorkTypeConfigTest
Change-Id: I741e071391819ad3575ef981e8feb287093aa387
parent dfeb4031
Loading
Loading
Loading
Loading
+29 −16
Original line number Diff line number Diff line
@@ -791,15 +791,10 @@ class JobConcurrencyManager {
            mNumUnspecialized = mConfigMaxTotal;
            mNumUnspecialized -= mConfigNumReservedSlots.get(WORK_TYPE_TOP);
            mNumUnspecialized -= mConfigNumReservedSlots.get(WORK_TYPE_BG);
            mNumUnspecialized -= mConfigAbsoluteMaxSlots.get(WORK_TYPE_TOP);
            mNumUnspecialized -= mConfigAbsoluteMaxSlots.get(WORK_TYPE_BG);
            calculateUnspecializedRemaining();
        }

        private void calculateUnspecializedRemaining() {
            mNumUnspecializedRemaining = mNumUnspecialized;
            mNumUnspecializedRemaining = mConfigMaxTotal;
            for (int i = mNumRunningJobs.size() - 1; i >= 0; --i) {
                mNumUnspecializedRemaining -= mNumRunningJobs.valueAt(i);
                mNumUnspecializedRemaining -= Math.max(mNumRunningJobs.valueAt(i),
                        mConfigNumReservedSlots.get(mNumRunningJobs.keyAt(i)));
            }
        }

@@ -882,24 +877,42 @@ class JobConcurrencyManager {
            mNumUnspecialized = mConfigMaxTotal;
            final int numTop = mNumRunningJobs.get(WORK_TYPE_TOP)
                    + mNumPendingJobs.get(WORK_TYPE_TOP);
            final int resTop = Math.min(mConfigNumReservedSlots.get(WORK_TYPE_TOP), numTop);
            int resTop = Math.min(mConfigNumReservedSlots.get(WORK_TYPE_TOP), numTop);
            mNumActuallyReservedSlots.put(WORK_TYPE_TOP, resTop);
            mNumUnspecialized -= resTop;
            final int numBg = mNumRunningJobs.get(WORK_TYPE_BG) + mNumPendingJobs.get(WORK_TYPE_BG);
            final int resBg = Math.min(mConfigNumReservedSlots.get(WORK_TYPE_BG), numBg);
            int resBg = Math.min(mConfigNumReservedSlots.get(WORK_TYPE_BG), numBg);
            mNumActuallyReservedSlots.put(WORK_TYPE_BG, resBg);
            mNumUnspecialized -= resBg;
            calculateUnspecializedRemaining();

            mNumUnspecializedRemaining = mNumUnspecialized;
            // Account for already running jobs after we've assigned the minimum number of slots.
            int unspecializedAssigned;
            int extraRunning = (mNumRunningJobs.get(WORK_TYPE_TOP) - resTop);
            if (extraRunning > 0) {
                unspecializedAssigned = Math.max(0,
                        Math.min(mConfigAbsoluteMaxSlots.get(WORK_TYPE_TOP) - resTop,
                                extraRunning));
                resTop += unspecializedAssigned;
                mNumUnspecializedRemaining -= extraRunning;
            }
            extraRunning = (mNumRunningJobs.get(WORK_TYPE_BG) - resBg);
            if (extraRunning > 0) {
                unspecializedAssigned = Math.max(0,
                        Math.min(mConfigAbsoluteMaxSlots.get(WORK_TYPE_BG) - resBg, extraRunning));
                resBg += unspecializedAssigned;
                mNumUnspecializedRemaining -= extraRunning;
            }

            // Assign remaining unspecialized based on ranking.
            int unspecializedAssigned = Math.max(0,
                    Math.min(mConfigAbsoluteMaxSlots.get(WORK_TYPE_TOP),
                            Math.min(mNumUnspecializedRemaining, numTop - resTop)));
            unspecializedAssigned = Math.max(0,
                    Math.min(mNumUnspecializedRemaining,
                            Math.min(mConfigAbsoluteMaxSlots.get(WORK_TYPE_TOP), numTop) - resTop));
            mNumActuallyReservedSlots.put(WORK_TYPE_TOP, resTop + unspecializedAssigned);
            mNumUnspecializedRemaining -= unspecializedAssigned;
            unspecializedAssigned = Math.max(0,
                    Math.min(mConfigAbsoluteMaxSlots.get(WORK_TYPE_BG),
                            Math.min(mNumUnspecializedRemaining, numBg - resBg)));
                    Math.min(mNumUnspecializedRemaining,
                            Math.min(mConfigAbsoluteMaxSlots.get(WORK_TYPE_BG), numBg) - resBg));
            mNumActuallyReservedSlots.put(WORK_TYPE_BG, resBg + unspecializedAssigned);
            mNumUnspecializedRemaining -= unspecializedAssigned;
        }
+196 −99
Original line number Diff line number Diff line
@@ -21,9 +21,11 @@ import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import android.util.Log;
import android.annotation.NonNull;
import android.util.Pair;
import android.util.SparseIntArray;

import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
@@ -58,106 +60,112 @@ public class WorkCountTrackerTest {
     * Represents running and pending jobs.
     */
    class Jobs {
        public int runningFg;
        public int runningBg;
        public int pendingFg;
        public int pendingBg;
        public final SparseIntArray running = new SparseIntArray();
        public final SparseIntArray pending = new SparseIntArray();

        public void maybeEnqueueJobs(double startRatio, double fgJobRatio) {
            while (mRandom.nextDouble() < startRatio) {
                if (mRandom.nextDouble() < fgJobRatio) {
                    pendingFg++;
                    pending.put(WORK_TYPE_TOP, pending.get(WORK_TYPE_TOP) + 1);
                } else {
                    pendingBg++;
                    pending.put(WORK_TYPE_BG, pending.get(WORK_TYPE_BG) + 1);
                }
            }
        }

        public void maybeFinishJobs(double stopRatio) {
            for (int i = runningBg; i > 0; i--) {
            for (int i = running.get(WORK_TYPE_BG); i > 0; i--) {
                if (mRandom.nextDouble() < stopRatio) {
                    runningBg--;
                    running.put(WORK_TYPE_BG, running.get(WORK_TYPE_BG) - 1);
                }
            }
            for (int i = runningFg; i > 0; i--) {
            for (int i = running.get(WORK_TYPE_TOP); i > 0; i--) {
                if (mRandom.nextDouble() < stopRatio) {
                    runningFg--;
                    running.put(WORK_TYPE_TOP, running.get(WORK_TYPE_TOP) - 1);
                }
            }
        }
    }


    private void startPendingJobs(Jobs jobs, int totalMax, int maxBg, int minBg) {
        mWorkCountTracker.setConfig(new JobConcurrencyManager.WorkTypeConfig("critical",
                totalMax,
                // defaultMin
                List.of(Pair.create(WORK_TYPE_TOP, totalMax - maxBg),
                        Pair.create(WORK_TYPE_BG, minBg)),
                // defaultMax
                List.of(Pair.create(WORK_TYPE_BG, maxBg))));
    private void startPendingJobs(Jobs jobs, int totalMax,
            @NonNull List<Pair<Integer, Integer>> minLimits,
            @NonNull List<Pair<Integer, Integer>> maxLimits) {
        mWorkCountTracker.setConfig(new JobConcurrencyManager.WorkTypeConfig(
                "test", totalMax, minLimits, maxLimits));
        mWorkCountTracker.resetCounts();

        for (int i = 0; i < jobs.runningFg; i++) {
            mWorkCountTracker.incrementRunningJobCount(WORK_TYPE_TOP);
        for (int i = 0; i < jobs.running.size(); ++i) {
            final int workType = jobs.running.keyAt(i);
            final int count = jobs.running.valueAt(i);

            for (int c = 0; c < count; ++c) {
                mWorkCountTracker.incrementRunningJobCount(workType);
            }
        for (int i = 0; i < jobs.runningBg; i++) {
            mWorkCountTracker.incrementRunningJobCount(WORK_TYPE_BG);
        }
        for (int i = 0; i < jobs.pending.size(); ++i) {
            final int workType = jobs.pending.keyAt(i);
            final int count = jobs.pending.valueAt(i);

        for (int i = 0; i < jobs.pendingFg; i++) {
            mWorkCountTracker.incrementPendingJobCount(WORK_TYPE_TOP);
            for (int c = 0; c < count; ++c) {
                mWorkCountTracker.incrementPendingJobCount(workType);
            }
        for (int i = 0; i < jobs.pendingBg; i++) {
            mWorkCountTracker.incrementPendingJobCount(WORK_TYPE_BG);
        }

        mWorkCountTracker.onCountDone();

        while ((jobs.pendingFg > 0
        while ((jobs.pending.get(WORK_TYPE_TOP) > 0
                && mWorkCountTracker.canJobStart(WORK_TYPE_TOP) != WORK_TYPE_NONE)
                || (jobs.pendingBg > 0
                || (jobs.pending.get(WORK_TYPE_BG) > 0
                && mWorkCountTracker.canJobStart(WORK_TYPE_BG) != WORK_TYPE_NONE)) {
            final boolean isStartingFg = mRandom.nextBoolean();

            if (isStartingFg) {
                if (jobs.pendingFg > 0
                if (jobs.pending.get(WORK_TYPE_TOP) > 0
                        && mWorkCountTracker.canJobStart(WORK_TYPE_TOP) != WORK_TYPE_NONE) {
                    jobs.pendingFg--;
                    jobs.runningFg++;
                    jobs.pending.put(WORK_TYPE_TOP, jobs.pending.get(WORK_TYPE_TOP) - 1);
                    jobs.running.put(WORK_TYPE_TOP, jobs.running.get(WORK_TYPE_TOP) + 1);
                    mWorkCountTracker.stageJob(WORK_TYPE_TOP);
                    mWorkCountTracker.onJobStarted(WORK_TYPE_TOP);
                }
            } else {
                if (jobs.pendingBg > 0
                if (jobs.pending.get(WORK_TYPE_BG) > 0
                        && mWorkCountTracker.canJobStart(WORK_TYPE_BG) != WORK_TYPE_NONE) {
                    jobs.pendingBg--;
                    jobs.runningBg++;
                    jobs.pending.put(WORK_TYPE_BG, jobs.pending.get(WORK_TYPE_BG) - 1);
                    jobs.running.put(WORK_TYPE_BG, jobs.running.get(WORK_TYPE_BG) + 1);
                    mWorkCountTracker.stageJob(WORK_TYPE_BG);
                    mWorkCountTracker.onJobStarted(WORK_TYPE_BG);
                }
            }
        }

        Log.i(TAG, "" + mWorkCountTracker);
    }

    /**
     * Used by the following testRandom* tests.
     */
    private void checkRandom(Jobs jobs, int numTests, int totalMax, int maxBg, int minBg,
    private void checkRandom(Jobs jobs, int numTests, int totalMax,
            @NonNull List<Pair<Integer, Integer>> minLimits,
            @NonNull List<Pair<Integer, Integer>> maxLimits,
            double startRatio, double fgJobRatio, double stopRatio) {
        for (int i = 0; i < numTests; i++) {

            jobs.maybeFinishJobs(stopRatio);
            jobs.maybeEnqueueJobs(startRatio, fgJobRatio);

            startPendingJobs(jobs, totalMax, maxBg, minBg);
            startPendingJobs(jobs, totalMax, minLimits, maxLimits);

            assertThat(jobs.runningFg).isAtMost(totalMax);
            assertThat(jobs.runningBg).isAtMost(totalMax);
            assertThat(jobs.runningFg + jobs.runningBg).isAtMost(totalMax);
            assertThat(jobs.runningBg).isAtMost(maxBg);
            int totalRunning = 0;
            for (int r = 0; r < jobs.running.size(); ++r) {
                final int numRunning = jobs.running.valueAt(r);
                assertWithMessage(
                        "Work type " + jobs.running.keyAt(r) + " is running too many jobs")
                        .that(numRunning).isAtMost(totalMax);
                totalRunning += numRunning;
            }
            assertThat(totalRunning).isAtMost(totalMax);
            for (Pair<Integer, Integer> maxLimit : maxLimits) {
                assertWithMessage("Work type " + maxLimit.first + " is running too many jobs")
                        .that(jobs.running.get(maxLimit.first)).isAtMost(maxLimit.second);
            }
        }
    }

@@ -170,13 +178,14 @@ public class WorkCountTrackerTest {

        final int numTests = 5000;
        final int totalMax = 6;
        final int maxBg = 4;
        final int minBg = 2;
        final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 4));
        final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final double stopRatio = 0.1;
        final double fgJobRatio = 0.5;
        final double startRatio = 0.1;

        checkRandom(jobs, numTests, totalMax, maxBg, minBg, startRatio, fgJobRatio , stopRatio);
        checkRandom(jobs, numTests, totalMax, minLimits, maxLimits,
                startRatio, fgJobRatio, stopRatio);
    }

    @Test
@@ -185,13 +194,14 @@ public class WorkCountTrackerTest {

        final int numTests = 5000;
        final int totalMax = 2;
        final int maxBg = 2;
        final int minBg = 0;
        final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final List<Pair<Integer, Integer>> minLimits = List.of();
        final double stopRatio = 0.5;
        final double fgJobRatio = 0.5;
        final double startRatio = 0.5;

        checkRandom(jobs, numTests, totalMax, maxBg, minBg, startRatio, fgJobRatio, stopRatio);
        checkRandom(jobs, numTests, totalMax, minLimits, maxLimits,
                startRatio, fgJobRatio, stopRatio);
    }

    @Test
@@ -200,13 +210,14 @@ public class WorkCountTrackerTest {

        final int numTests = 5000;
        final int totalMax = 2;
        final int maxBg = 2;
        final int minBg = 2;
        final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final double stopRatio = 0.5;
        final double fgJobRatio = 0.5;
        final double startRatio = 0.5;

        checkRandom(jobs, numTests, totalMax, maxBg, minBg, startRatio, fgJobRatio, stopRatio);
        checkRandom(jobs, numTests, totalMax, minLimits, maxLimits,
                startRatio, fgJobRatio, stopRatio);
    }

    @Test
@@ -215,13 +226,14 @@ public class WorkCountTrackerTest {

        final int numTests = 5000;
        final int totalMax = 10;
        final int maxBg = 2;
        final int minBg = 0;
        final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final List<Pair<Integer, Integer>> minLimits = List.of();
        final double stopRatio = 0.5;
        final double fgJobRatio = 0.5;
        final double startRatio = 0.5;

        checkRandom(jobs, numTests, totalMax, maxBg, minBg, startRatio, fgJobRatio, stopRatio);
        checkRandom(jobs, numTests, totalMax, minLimits, maxLimits,
                startRatio, fgJobRatio, stopRatio);
    }

    @Test
@@ -230,13 +242,14 @@ public class WorkCountTrackerTest {

        final int numTests = 5000;
        final int totalMax = 6;
        final int maxBg = 4;
        final int minBg = 2;
        final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 4));
        final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final double stopRatio = 0.5;
        final double fgJobRatio = 0.1;
        final double startRatio = 0.5;

        checkRandom(jobs, numTests, totalMax, maxBg, minBg, startRatio, fgJobRatio, stopRatio);
        checkRandom(jobs, numTests, totalMax, minLimits, maxLimits,
                startRatio, fgJobRatio, stopRatio);
    }

    @Test
@@ -245,13 +258,14 @@ public class WorkCountTrackerTest {

        final int numTests = 5000;
        final int totalMax = 6;
        final int maxBg = 4;
        final int minBg = 2;
        final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 4));
        final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final double stopRatio = 0.5;
        final double fgJobRatio = 0.9;
        final double startRatio = 0.5;

        checkRandom(jobs, numTests, totalMax, maxBg, minBg, startRatio, fgJobRatio, stopRatio);
        checkRandom(jobs, numTests, totalMax, minLimits, maxLimits,
                startRatio, fgJobRatio, stopRatio);
    }

    @Test
@@ -260,13 +274,14 @@ public class WorkCountTrackerTest {

        final int numTests = 5000;
        final int totalMax = 6;
        final int maxBg = 4;
        final int minBg = 2;
        final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 4));
        final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final double stopRatio = 0.4;
        final double fgJobRatio = 0.1;
        final double startRatio = 0.5;

        checkRandom(jobs, numTests, totalMax, maxBg, minBg, startRatio, fgJobRatio, stopRatio);
        checkRandom(jobs, numTests, totalMax, minLimits, maxLimits,
                startRatio, fgJobRatio, stopRatio);
    }

    @Test
@@ -275,53 +290,135 @@ public class WorkCountTrackerTest {

        final int numTests = 5000;
        final int totalMax = 6;
        final int maxBg = 4;
        final int minBg = 2;
        final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 4));
        final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2));
        final double stopRatio = 0.4;
        final double fgJobRatio = 0.9;
        final double startRatio = 0.5;

        checkRandom(jobs, numTests, totalMax, maxBg, minBg, startRatio, fgJobRatio, stopRatio);
        checkRandom(jobs, numTests, totalMax, minLimits, maxLimits,
                startRatio, fgJobRatio, stopRatio);
    }

    /** Used by the following tests */
    private void checkSimple(int totalMax, int maxBg, int minBg,
            int runningFg, int runningBg, int pendingFg, int pendingBg,
            int resultRunningFg, int resultRunningBg, int resultPendingFg, int resultPendingBg) {
    private void checkSimple(int totalMax,
            @NonNull List<Pair<Integer, Integer>> minLimits,
            @NonNull List<Pair<Integer, Integer>> maxLimits,
            @NonNull List<Pair<Integer, Integer>> running,
            @NonNull List<Pair<Integer, Integer>> pending,
            @NonNull List<Pair<Integer, Integer>> resultRunning,
            @NonNull List<Pair<Integer, Integer>> resultPending) {
        final Jobs jobs = new Jobs();
        jobs.runningFg = runningFg;
        jobs.runningBg = runningBg;
        jobs.pendingFg = pendingFg;
        jobs.pendingBg = pendingBg;

        startPendingJobs(jobs, totalMax, maxBg, minBg);
        for (Pair<Integer, Integer> run : running) {
            jobs.running.put(run.first, run.second);
        }
        for (Pair<Integer, Integer> pend : pending) {
            jobs.pending.put(pend.first, pend.second);
        }

//        fail(mWorkerCountTracker.toString());
        assertThat(jobs.runningFg).isEqualTo(resultRunningFg);
        assertThat(jobs.runningBg).isEqualTo(resultRunningBg);
        startPendingJobs(jobs, totalMax, minLimits, maxLimits);

        assertThat(jobs.pendingFg).isEqualTo(resultPendingFg);
        assertThat(jobs.pendingBg).isEqualTo(resultPendingBg);
        for (Pair<Integer, Integer> run : resultRunning) {
            assertWithMessage("Incorrect running result for work type " + run.first)
                    .that(jobs.running.get(run.first)).isEqualTo(run.second);
        }
        for (Pair<Integer, Integer> pend : resultPending) {
            assertWithMessage("Incorrect pending result for work type " + pend.first)
                    .that(jobs.pending.get(pend.first)).isEqualTo(pend.second);
        }
    }


    @Test
    public void testBasic() {
        // Args are:
        // First 3: Total-max, bg-max, bg-min.
        // Next 2:  Running FG / BG
        // Next 2:  Pending FG / BG
        // Next 4:  Result running FG / BG, pending FG/BG.
        checkSimple(6, 4, 2, /*run=*/ 0, 0, /*pen=*/ 1, 0, /*res run/pen=*/ 1, 0, 0, 0);

        checkSimple(6, 4, 2, /*run=*/ 0, 0, /*pen=*/ 10, 0, /*res run/pen=*/ 6, 0, 4, 0);
        checkSimple(6,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_BG, 4)),
                /* run */ List.of(),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 1)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 1)),
                /* resPen */ List.of());

        checkSimple(6,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_BG, 4)),
                /* run */ List.of(),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 6)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 4)));

        // When there are BG jobs pending, 2 (min-BG) jobs should run.
        checkSimple(6, 4, 2, /*run=*/ 0, 0, /*pen=*/ 10, 1, /*res run/pen=*/ 5, 1, 5, 0);
        checkSimple(6, 4, 2, /*run=*/ 0, 0, /*pen=*/ 10, 3, /*res run/pen=*/ 4, 2, 6, 1);

        checkSimple(8, 6, 2, /*run=*/ 0, 0, /*pen=*/ 0, 49, /*res run/pen=*/ 0, 6, 0, 43);

        checkSimple(6, 4, 2, /*run=*/ 6, 0, /*pen=*/ 10, 3, /*res run/pen=*/ 6, 0, 10, 3);
        checkSimple(6,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_BG, 4)),
                /* run */ List.of(),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 1)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 1)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 5)));
        checkSimple(6,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_BG, 4)),
                /* run */ List.of(),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 2)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 1)));

        checkSimple(8,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_BG, 6)),
                /* run */ List.of(),
                /* pen */ List.of(Pair.create(WORK_TYPE_BG, 49)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_BG, 6)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_BG, 43)));

        checkSimple(8,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_BG, 6)),
                /* run */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 4)),
                /* pen */ List.of(Pair.create(WORK_TYPE_BG, 49)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 6)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_BG, 47)));

        checkSimple(8,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 6)),
                /* run */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 4)),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 49)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 4)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 8), Pair.create(WORK_TYPE_BG, 49)));

        checkSimple(8,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 6)),
                /* run */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 1)),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 49)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 2)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 48)));

        checkSimple(8,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 1)),
                /* max */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 2)),
                /* run */ List.of(Pair.create(WORK_TYPE_BG, 6)),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 49)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 6)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 8), Pair.create(WORK_TYPE_BG, 49)));

        checkSimple(6,
                /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)),
                /* max */ List.of(Pair.create(WORK_TYPE_BG, 4)),
                /* run */ List.of(Pair.create(WORK_TYPE_TOP, 6)),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 6)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3)));

        // This could happen if we lower the effective config due to higher memory pressure after
        // we've already started running jobs. We shouldn't stop already running jobs, but also
        // shouldn't start new ones.
        checkSimple(5,
                /* min */ List.of(),
                /* max */ List.of(Pair.create(WORK_TYPE_BG, 1)),
                /* run */ List.of(Pair.create(WORK_TYPE_BG, 6)),
                /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3)),
                /* resRun */ List.of(Pair.create(WORK_TYPE_BG, 6)),
                /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3)));
    }
}