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

Commit 1454998e authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "QuotaController tracking only jobs started in bg."

parents 8bac1090 cc5afbc8
Loading
Loading
Loading
Loading
+5 −3
Original line number Diff line number Diff line
@@ -417,7 +417,8 @@ message StateControllerProto {

            optional int64 start_time_elapsed = 1;
            optional int64 end_time_elapsed = 2;
            optional int32 job_count = 3;
            // The number of background jobs that ran during this session.
            optional int32 bg_job_count = 3;
        }

        message Timer {
@@ -428,8 +429,9 @@ message StateControllerProto {
            optional bool is_active = 2;
            // The time this timer last became active. Only valid if is_active is true.
            optional int64 start_time_elapsed = 3;
            // How many are currently running. Valid only if the device is_active is true.
            optional int32 job_count = 4;
            // How many background jobs are currently running. Valid only if the device is_active
            // is true.
            optional int32 bg_job_count = 4;
            // All of the jobs that the Timer is currently tracking.
            repeated JobStatusShortInfoProto running_jobs = 5;
        }
+49 −36
Original line number Diff line number Diff line
@@ -151,8 +151,7 @@ public final class QuotaController extends StateController {
        return "<" + userId + ">" + packageName;
    }

    @VisibleForTesting
    static final class Package {
    private static final class Package {
        public final String packageName;
        public final int userId;

@@ -387,8 +386,9 @@ public final class QuotaController extends StateController {

    private boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
        final int standbyBucket = getEffectiveStandbyBucket(jobStatus);
        return isWithinQuotaLocked(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(),
                standbyBucket);
        // Jobs for the active app should always be able to run.
        return jobStatus.uidActive || isWithinQuotaLocked(
                jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
    }

    private boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
@@ -579,7 +579,10 @@ public final class QuotaController extends StateController {
        boolean changed = false;
        for (int i = jobs.size() - 1; i >= 0; --i) {
            final JobStatus js = jobs.valueAt(i);
            if (realStandbyBucket == getEffectiveStandbyBucket(js)) {
            if (js.uidActive) {
                // Jobs for the active app should always be able to run.
                changed |= js.setQuotaConstraintSatisfied(true);
            } else if (realStandbyBucket == getEffectiveStandbyBucket(js)) {
                changed |= js.setQuotaConstraintSatisfied(realInQuota);
            } else {
                // This job is somehow exempted. Need to determine its own quota status.
@@ -765,18 +768,18 @@ public final class QuotaController extends StateController {
        public final long startTimeElapsed;
        // End timestamp in elapsed realtime timebase.
        public final long endTimeElapsed;
        // How many jobs ran during this session.
        public final int jobCount;
        // How many background jobs ran during this session.
        public final int bgJobCount;

        TimingSession(long startElapsed, long endElapsed, int jobCount) {
            this.startTimeElapsed = startElapsed;
            this.endTimeElapsed = endElapsed;
            this.jobCount = jobCount;
            this.bgJobCount = jobCount;
        }

        @Override
        public String toString() {
            return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + jobCount
            return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount
                    + "}";
        }

@@ -786,7 +789,7 @@ public final class QuotaController extends StateController {
                TimingSession other = (TimingSession) obj;
                return startTimeElapsed == other.startTimeElapsed
                        && endTimeElapsed == other.endTimeElapsed
                        && jobCount == other.jobCount;
                        && bgJobCount == other.bgJobCount;
            } else {
                return false;
            }
@@ -794,7 +797,7 @@ public final class QuotaController extends StateController {

        @Override
        public int hashCode() {
            return Arrays.hashCode(new long[] {startTimeElapsed, endTimeElapsed, jobCount});
            return Arrays.hashCode(new long[] {startTimeElapsed, endTimeElapsed, bgJobCount});
        }

        public void dump(IndentingPrintWriter pw) {
@@ -804,8 +807,8 @@ public final class QuotaController extends StateController {
            pw.print(" (");
            pw.print(endTimeElapsed - startTimeElapsed);
            pw.print("), ");
            pw.print(jobCount);
            pw.print(" jobs.");
            pw.print(bgJobCount);
            pw.print(" bg jobs.");
            pw.println();
        }

@@ -816,7 +819,8 @@ public final class QuotaController extends StateController {
                    startTimeElapsed);
            proto.write(StateControllerProto.QuotaController.TimingSession.END_TIME_ELAPSED,
                    endTimeElapsed);
            proto.write(StateControllerProto.QuotaController.TimingSession.JOB_COUNT, jobCount);
            proto.write(StateControllerProto.QuotaController.TimingSession.BG_JOB_COUNT,
                    bgJobCount);

            proto.end(token);
        }
@@ -825,23 +829,32 @@ public final class QuotaController extends StateController {
    private final class Timer {
        private final Package mPkg;

        // List of jobs currently running for this package.
        private final ArraySet<JobStatus> mRunningJobs = new ArraySet<>();
        // List of jobs currently running for this app that started when the app wasn't in the
        // foreground.
        private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>();
        private long mStartTimeElapsed;
        private int mJobCount;
        private int mBgJobCount;

        Timer(int userId, String packageName) {
            mPkg = new Package(userId, packageName);
        }

        void startTrackingJob(@NonNull JobStatus jobStatus) {
            if (jobStatus.uidActive) {
                // We intentionally don't pay attention to fg state changes after a job has started.
                if (DEBUG) {
                    Slog.v(TAG,
                            "Timer ignoring " + jobStatus.toShortString() + " because uidActive");
                }
                return;
            }
            if (DEBUG) Slog.v(TAG, "Starting to track " + jobStatus.toShortString());
            synchronized (mLock) {
                // Always track jobs, even when charging.
                mRunningJobs.add(jobStatus);
                mRunningBgJobs.add(jobStatus);
                if (!mChargeTracker.isCharging()) {
                    mJobCount++;
                    if (mRunningJobs.size() == 1) {
                    mBgJobCount++;
                    if (mRunningBgJobs.size() == 1) {
                        // Started tracking the first job.
                        mStartTimeElapsed = sElapsedRealtimeClock.millis();
                        scheduleCutoff();
@@ -853,7 +866,7 @@ public final class QuotaController extends StateController {
        void stopTrackingJob(@NonNull JobStatus jobStatus) {
            if (DEBUG) Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString());
            synchronized (mLock) {
                if (mRunningJobs.size() == 0) {
                if (mRunningBgJobs.size() == 0) {
                    // maybeStopTrackingJobLocked can be called when an app cancels a job, so a
                    // timer may not be running when it's asked to stop tracking a job.
                    if (DEBUG) {
@@ -861,8 +874,8 @@ public final class QuotaController extends StateController {
                    }
                    return;
                }
                mRunningJobs.remove(jobStatus);
                if (!mChargeTracker.isCharging() && mRunningJobs.size() == 0) {
                if (mRunningBgJobs.remove(jobStatus)
                        && !mChargeTracker.isCharging() && mRunningBgJobs.size() == 0) {
                    emitSessionLocked(sElapsedRealtimeClock.millis());
                    cancelCutoff();
                }
@@ -870,13 +883,13 @@ public final class QuotaController extends StateController {
        }

        private void emitSessionLocked(long nowElapsed) {
            if (mJobCount <= 0) {
            if (mBgJobCount <= 0) {
                // Nothing to emit.
                return;
            }
            TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mJobCount);
            TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount);
            saveTimingSession(mPkg.userId, mPkg.packageName, ts);
            mJobCount = 0;
            mBgJobCount = 0;
            // Don't reset the tracked jobs list as we need to keep tracking the current number
            // of jobs.
            // However, cancel the currently scheduled cutoff since it's not currently useful.
@@ -889,7 +902,7 @@ public final class QuotaController extends StateController {
         */
        public boolean isActive() {
            synchronized (mLock) {
                return mJobCount > 0;
                return mBgJobCount > 0;
            }
        }

@@ -905,12 +918,12 @@ public final class QuotaController extends StateController {
                    emitSessionLocked(nowElapsed);
                } else {
                    // Start timing from unplug.
                    if (mRunningJobs.size() > 0) {
                    if (mRunningBgJobs.size() > 0) {
                        mStartTimeElapsed = nowElapsed;
                        // NOTE: this does have the unfortunate consequence that if the device is
                        // repeatedly plugged in and unplugged, the job count for a package may be
                        // artificially high.
                        mJobCount = mRunningJobs.size();
                        mBgJobCount = mRunningBgJobs.size();
                        // Schedule cutoff since we're now actively tracking for quotas again.
                        scheduleCutoff();
                    }
@@ -958,12 +971,12 @@ public final class QuotaController extends StateController {
                pw.print("NOT active");
            }
            pw.print(", ");
            pw.print(mJobCount);
            pw.print(" running jobs");
            pw.print(mBgJobCount);
            pw.print(" running bg jobs");
            pw.println();
            pw.increaseIndent();
            for (int i = 0; i < mRunningJobs.size(); i++) {
                JobStatus js = mRunningJobs.valueAt(i);
            for (int i = 0; i < mRunningBgJobs.size(); i++) {
                JobStatus js = mRunningBgJobs.valueAt(i);
                if (predicate.test(js)) {
                    pw.println(js.toShortString());
                }
@@ -979,9 +992,9 @@ public final class QuotaController extends StateController {
            proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive());
            proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED,
                    mStartTimeElapsed);
            proto.write(StateControllerProto.QuotaController.Timer.JOB_COUNT, mJobCount);
            for (int i = 0; i < mRunningJobs.size(); i++) {
                JobStatus js = mRunningJobs.valueAt(i);
            proto.write(StateControllerProto.QuotaController.Timer.BG_JOB_COUNT, mBgJobCount);
            for (int i = 0; i < mRunningBgJobs.size(); i++) {
                JobStatus js = mRunningBgJobs.valueAt(i);
                if (predicate.test(js)) {
                    js.writeToShortProto(proto,
                            StateControllerProto.QuotaController.Timer.RUNNING_JOBS);
+133 −0
Original line number Diff line number Diff line
@@ -780,6 +780,139 @@ public class QuotaControllerTest {
        assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    /** Tests that TimingSessions are saved properly when all the jobs are background jobs. */
    @Test
    public void testTimerTracking_AllBackground() {
        setDischarging();

        JobStatus jobStatus = createJobStatus("testTimerTracking_AllBackground", 1);
        mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);

        assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        List<TimingSession> expected = new ArrayList<>();

        // Test single job.
        long start = JobSchedulerService.sElapsedRealtimeClock.millis();
        mQuotaController.prepareForExecutionLocked(jobStatus);
        advanceElapsedClock(5 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
        expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
        assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        // Test overlapping jobs.
        JobStatus jobStatus2 = createJobStatus("testTimerTracking_AllBackground", 2);
        mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null);

        JobStatus jobStatus3 = createJobStatus("testTimerTracking_AllBackground", 3);
        mQuotaController.maybeStartTrackingJobLocked(jobStatus3, null);

        advanceElapsedClock(SECOND_IN_MILLIS);

        start = JobSchedulerService.sElapsedRealtimeClock.millis();
        mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
        mQuotaController.prepareForExecutionLocked(jobStatus);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.prepareForExecutionLocked(jobStatus2);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.prepareForExecutionLocked(jobStatus3);
        advanceElapsedClock(20 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
        expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3));
        assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    /** Tests that Timers don't count foreground jobs. */
    @Test
    public void testTimerTracking_AllForeground() {
        setDischarging();

        JobStatus jobStatus = createJobStatus("testTimerTracking_AllForeground", 1);
        jobStatus.uidActive = true;
        mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);

        assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        mQuotaController.prepareForExecutionLocked(jobStatus);
        advanceElapsedClock(5 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
        assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    /**
     * Tests that Timers properly track overlapping foreground and background jobs.
     */
    @Test
    public void testTimerTracking_ForegroundAndBackground() {
        setDischarging();

        JobStatus jobBg1 = createJobStatus("testTimerTracking_ForegroundAndBackground", 1);
        JobStatus jobBg2 = createJobStatus("testTimerTracking_ForegroundAndBackground", 2);
        JobStatus jobFg3 = createJobStatus("testTimerTracking_ForegroundAndBackground", 3);
        jobFg3.uidActive = true;
        mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
        mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
        mQuotaController.maybeStartTrackingJobLocked(jobFg3, null);
        assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
        List<TimingSession> expected = new ArrayList<>();

        // UID starts out inactive.
        long start = JobSchedulerService.sElapsedRealtimeClock.millis();
        mQuotaController.prepareForExecutionLocked(jobBg1);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
        assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        advanceElapsedClock(SECOND_IN_MILLIS);

        // Bg job starts while inactive, spans an entire active session, and ends after the
        // active session.
        // Fg job starts after the bg job and ends before the bg job.
        // Entire bg job duration should be counted since it started before active session. However,
        // count should only be 1 since Timer shouldn't count fg jobs.
        start = JobSchedulerService.sElapsedRealtimeClock.millis();
        mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
        mQuotaController.prepareForExecutionLocked(jobBg2);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.prepareForExecutionLocked(jobFg3);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
        expected.add(createTimingSession(start, 30 * SECOND_IN_MILLIS, 1));
        assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        advanceElapsedClock(SECOND_IN_MILLIS);

        // Bg job 1 starts, then fg job starts. Bg job 1 job ends. Shortly after, uid goes
        // "inactive" and then bg job 2 starts. Then fg job ends.
        // This should result in two TimingSessions with a count of one each.
        start = JobSchedulerService.sElapsedRealtimeClock.millis();
        mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
        mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
        mQuotaController.maybeStartTrackingJobLocked(jobFg3, null);
        mQuotaController.prepareForExecutionLocked(jobBg1);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.prepareForExecutionLocked(jobFg3);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
        expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1));
        advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now
        start = JobSchedulerService.sElapsedRealtimeClock.millis();
        mQuotaController.prepareForExecutionLocked(jobBg2);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
        expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1));
        assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    /**
     * Tests that a job is properly updated and JobSchedulerService is notified when a job reaches
     * its quota.