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

Commit cbbec6b5 authored by Alex Bianchi's avatar Alex Bianchi Committed by Android (Google) Code Review
Browse files

Merge "Adds Flexibility Controller and its supporting JobStatus functions."

parents b2278b5a 34265622
Loading
Loading
Loading
Loading
+312 −0
Original line number Diff line number Diff line
/*
 * 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.controllers;

import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;

import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.app.job.JobInfo;
import android.content.Context;
import android.os.Looper;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.JobSchedulerBackgroundThread;
import com.android.server.job.JobSchedulerService;
import com.android.server.utils.AlarmQueue;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

/**
 * Controller that tracks the number of flexible constraints being actively satisfied.
 * Drops constraint for TOP apps and lowers number of required constraints with time.
 *
 * TODO: Plug in to other controllers (b/239047584), handle prefetch (b/238887951)
 */
public final class FlexibilityController extends StateController {
    /**
     * List of all potential flexible constraints
     */
    @VisibleForTesting
    static final int FLEXIBLE_CONSTRAINTS = JobStatus.CONSTRAINT_BATTERY_NOT_LOW
            | JobStatus.CONSTRAINT_CHARGING
            | JobStatus.CONSTRAINT_CONNECTIVITY
            | JobStatus.CONSTRAINT_IDLE;

    /** Hard cutoff to remove flexible constraints */
    private static final long DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS;

    /**
     * Keeps track of what flexible constraints are satisfied at the moment.
     * Is updated by the other controllers.
     */
    private int mSatisfiedFlexibleConstraints;

    @VisibleForTesting
    @GuardedBy("mLock")
    final FlexibilityTracker mFlexibilityTracker;

    private final FlexibilityAlarmQueue mFlexibilityAlarmQueue;
    private final long mMinTimeBetweenAlarmsMs = MINUTE_IN_MILLIS;

    /**
     * The percent of a Jobs lifecycle to drop number of required constraints.
     * mPercentToDropConstraints[i] denotes that at x% of a Jobs lifecycle,
     * the controller should have i+1 constraints dropped.
     */
    private final int[] mPercentToDropConstraints = {50, 60, 70, 80};

    /** The default deadline that all flexible constraints should be dropped by. */
    private final long mDefaultFlexibleDeadline = 72 * HOUR_IN_MILLIS;

    public FlexibilityController(JobSchedulerService service) {
        super(service);
        mFlexibilityTracker = new FlexibilityTracker(FLEXIBLE_CONSTRAINTS);
        mFlexibilityAlarmQueue = new FlexibilityAlarmQueue(
                mContext, JobSchedulerBackgroundThread.get().getLooper());
    }

    /**
     * StateController interface
     */
    @Override
    public void maybeStartTrackingJobLocked(JobStatus js, JobStatus lastJob) {
        if (js.hasFlexibilityConstraint()) {
            mFlexibilityTracker.add(js);
            js.setTrackingController(JobStatus.TRACKING_FLEXIBILITY);
            final long nowElapsed = sElapsedRealtimeClock.millis();
            js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
            mFlexibilityAlarmQueue.addAlarm(js, getNextConstraintDropTimeElapsed(js));
        }
    }

    @Override
    public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob, boolean forUpdate) {
        if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) {
            mFlexibilityAlarmQueue.removeAlarmForKey(js);
            mFlexibilityTracker.remove(js);
        }
    }

    /** Checks if the flexibility constraint is actively satisfied for a given job. */
    @VisibleForTesting
    boolean isFlexibilitySatisfiedLocked(JobStatus js) {
        synchronized (mLock) {
            return mService.getUidBias(js.getUid()) == JobInfo.BIAS_TOP_APP
                    || mService.isCurrentlyRunningLocked(js)
                    || getNumSatisfiedRequiredConstraintsLocked(js)
                    >= js.getNumRequiredFlexibleConstraints();
        }
    }

    @VisibleForTesting
    @GuardedBy("mLock")
    int getNumSatisfiedRequiredConstraintsLocked(JobStatus js) {
        return Integer.bitCount(js.getFlexibleConstraints() & mSatisfiedFlexibleConstraints);
    }

    /**
     * Sets the controller's constraint to a given state.
     * Changes flexibility constraint satisfaction for affected jobs.
     */
    @VisibleForTesting
    void setConstraintSatisfied(int constraint, boolean state) {
        synchronized (mLock) {
            final boolean old = (mSatisfiedFlexibleConstraints & constraint) != 0;
            if (old == state) {
                return;
            }

            final int prevSatisfied = Integer.bitCount(mSatisfiedFlexibleConstraints);
            mSatisfiedFlexibleConstraints =
                    (mSatisfiedFlexibleConstraints & ~constraint) | (state ? constraint : 0);
            final int curSatisfied = Integer.bitCount(mSatisfiedFlexibleConstraints);

            // Only the max of the number of required flexible constraints will need to be updated
            // The rest did not have a change in state and are still satisfied or unsatisfied.
            final int numConstraintsToUpdate = Math.max(curSatisfied, prevSatisfied);

            final ArraySet<JobStatus> jobs = mFlexibilityTracker.getJobsByNumRequiredConstraints(
                    numConstraintsToUpdate);
            final long nowElapsed = sElapsedRealtimeClock.millis();

            for (int i = 0; i < jobs.size(); i++) {
                JobStatus js = jobs.valueAt(i);
                js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
            }
        }
    }

    /** Checks if the given constraint is satisfied in the flexibility controller. */
    @VisibleForTesting
    boolean isConstraintSatisfied(int constraint) {
        return (mSatisfiedFlexibleConstraints & constraint) != 0;
    }

    /** The elapsed time that marks when the next constraint should be dropped. */
    @VisibleForTesting
    @ElapsedRealtimeLong
    long getNextConstraintDropTimeElapsed(JobStatus js) {
        final long earliest = js.getEarliestRunTime() == JobStatus.NO_EARLIEST_RUNTIME
                ? js.enqueueTime : js.getEarliestRunTime();
        final long latest = js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME
                ? earliest + mDefaultFlexibleDeadline
                : js.getLatestRunTimeElapsed();
        final int percent = mPercentToDropConstraints[js.getNumDroppedFlexibleConstraints()];
        final long percentInTime = ((latest - earliest) * percent) / 100;
        return earliest + percentInTime;
    }

    @Override
    @GuardedBy("mLock")
    public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
        if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) {
            return;
        }
        final long nowElapsed = sElapsedRealtimeClock.millis();
        List<JobStatus> jobsByUid = mService.getJobStore().getJobsByUid(uid);
        for (int i = 0; i < jobsByUid.size(); i++) {
            JobStatus js = jobsByUid.get(i);
            if (js.hasFlexibilityConstraint()) {
                js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
            }
        }
    }

    @VisibleForTesting
    class FlexibilityTracker {
        final ArrayList<ArraySet<JobStatus>> mTrackedJobs;

        FlexibilityTracker(int flexibleConstraints) {
            mTrackedJobs = new ArrayList<>();
            int numFlexibleConstraints = Integer.bitCount(flexibleConstraints);
            for (int i = 0; i <= numFlexibleConstraints; i++) {
                mTrackedJobs.add(new ArraySet<JobStatus>());
            }
        }

        /** Gets every tracked job with a given number of required constraints. */
        public ArraySet<JobStatus> getJobsByNumRequiredConstraints(int numRequired) {
            return mTrackedJobs.get(numRequired - 1);
        }

        /** adds a JobStatus object based on number of required flexible constraints. */
        public void add(JobStatus js) {
            if (js.getNumRequiredFlexibleConstraints() <= 0) {
                return;
            }
            mTrackedJobs.get(js.getNumRequiredFlexibleConstraints() - 1).add(js);
        }

        /** Removes a JobStatus object. */
        public void remove(JobStatus js) {
            if (js.getNumRequiredFlexibleConstraints() == 0) {
                return;
            }
            mTrackedJobs.get(js.getNumRequiredFlexibleConstraints() - 1).remove(js);
        }

        /** Returns all tracked jobs. */
        public ArrayList<ArraySet<JobStatus>> getArrayList() {
            return mTrackedJobs;
        }

        /**
         * Adjusts number of required flexible constraints and sorts it into the tracker.
         * Returns false if the job status's number of flexible constraints is now 0.
         * Jobs with 0 required flexible constraints are removed from the tracker.
         */
        public boolean adjustJobsRequiredConstraints(JobStatus js, int n) {
            remove(js);
            js.adjustNumRequiredFlexibleConstraints(n);
            final long nowElapsed = sElapsedRealtimeClock.millis();
            js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
            if (js.getNumRequiredFlexibleConstraints() <= 0) {
                maybeStopTrackingJobLocked(js, null, false);
                return false;
            }
            add(js);
            return true;
        }

        public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
            for (int i = 0; i < mTrackedJobs.size(); i++) {
                ArraySet<JobStatus> jobs = mTrackedJobs.get(i);
                for (int j = 0; j < mTrackedJobs.size(); j++) {
                    final JobStatus js = jobs.valueAt(j);
                    if (!predicate.test(js)) {
                        continue;
                    }
                    pw.print("#");
                    js.printUniqueId(pw);
                    pw.print(" from ");
                    UserHandle.formatUid(pw, js.getSourceUid());
                    pw.println();
                }
            }
        }
    }

    private class FlexibilityAlarmQueue extends AlarmQueue<JobStatus> {
        private FlexibilityAlarmQueue(Context context, Looper looper) {
            super(context, looper, "*job.flexibility_check*",
                    "Flexible Constraint Check", false, mMinTimeBetweenAlarmsMs);
        }

        @Override
        protected boolean isForUser(@NonNull JobStatus js, int userId) {
            return js.getSourceUserId() == userId;
        }

        @Override
        protected void processExpiredAlarms(@NonNull ArraySet<JobStatus> expired) {
            synchronized (mLock) {
                JobStatus js;
                for (int i = 0; i < expired.size(); i++) {
                    js = expired.valueAt(i);
                    long time = getNextConstraintDropTimeElapsed(js);
                    if (js.getLatestRunTimeElapsed() - time < DEADLINE_PROXIMITY_LIMIT_MS) {
                        mFlexibilityTracker.adjustJobsRequiredConstraints(js,
                                -js.getNumRequiredFlexibleConstraints());
                        continue;
                    }
                    if (mFlexibilityTracker.adjustJobsRequiredConstraints(js, -1)) {
                        mFlexibilityAlarmQueue.addAlarm(js, time);
                    }
                }
            }
        }
    }

    @Override
    @GuardedBy("mLock")
    public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
        pw.println("# Constraints Satisfied: " + Integer.bitCount(mSatisfiedFlexibleConstraints));
        pw.println();

        mFlexibilityTracker.dump(pw, predicate);
    }
}
+103 −2
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package com.android.server.job.controllers;

import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;

import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
@@ -99,6 +102,7 @@ public final class JobStatus {
    static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24;      // Implicit constraint
    static final int CONSTRAINT_PREFETCH = 1 << 23;
    static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint
    static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint

    // The following set of dynamic constraints are for specific use cases (as explained in their
    // relative naming and comments). Right now, they apply different constraints, which is fine,
@@ -117,6 +121,22 @@ public final class JobStatus {
                    | CONSTRAINT_CONNECTIVITY
                    | CONSTRAINT_IDLE;

    /**
     * The set of constraints that are required to satisfy flexible constraints.
     * Constraints explicitly requested by the job will not be added to the set.
     */
    private int mFlexibleConstraints;

    /**
     * Keeps track of how many flexible constraints must be satisfied for the job to execute.
     */
    private int mNumRequiredFlexibleConstraints;

    /**
     * Number of required flexible constraints that have been dropped.
     */
    private int mNumDroppedFlexibleConstraints;

    /**
     * The additional set of dynamic constraints that must be met if this is an expedited job that
     * had a long enough run while the device was Dozing or in battery saver.
@@ -304,6 +324,12 @@ public final class JobStatus {
     */
    public static final int TRACKING_QUOTA = 1 << 6;

    /**
     * Flag for {@link #trackingControllers}: the flexibility controller is currently tracking this
     * job.
     */
    public static final int TRACKING_FLEXIBILITY = 1 << 7;

    /**
     * Bit mask of controllers that are currently tracking the job.
     */
@@ -318,6 +344,8 @@ public final class JobStatus {
     */
    public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0;

    /** Minimum difference between start and end time to have flexible constraint */
    private static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS;
    /**
     * Versatile, persistable flags for a job that's updated within the system server,
     * as opposed to {@link JobInfo#flags} that's set by callers.
@@ -525,6 +553,33 @@ public final class JobStatus {
            }
        }
        mHasExemptedMediaUrisOnly = exemptedMediaUrisOnly;

        if (isRequestedExpeditedJob()
                || ((latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis)
                < MIN_WINDOW_FOR_FLEXIBILITY_MS)
                || job.isPrefetch()) {
            mFlexibleConstraints = 0;
        } else {
            if ((requiredConstraints & CONSTRAINT_CHARGING) == 0) {
                mFlexibleConstraints |= CONSTRAINT_CHARGING;
            }
            if ((requiredConstraints & CONSTRAINT_BATTERY_NOT_LOW) == 0) {
                mFlexibleConstraints |= CONSTRAINT_BATTERY_NOT_LOW;
            }
            if ((requiredConstraints & CONSTRAINT_IDLE) == 0) {
                mFlexibleConstraints |= CONSTRAINT_IDLE;
            }
            if (job.getRequiredNetwork() != null
                    && !job.getRequiredNetwork().hasCapability(NET_CAPABILITY_NOT_METERED)) {
                mFlexibleConstraints |= CONSTRAINT_CONNECTIVITY;
            }
        }
        if (mFlexibleConstraints != 0) {
            // TODO(b/239047584): Uncomment once Flexibility Controller is plugged in.
            // requiredConstraints |= CONSTRAINT_FLEXIBLE;
            mNumRequiredFlexibleConstraints = Integer.bitCount(mFlexibleConstraints);
        }

        this.requiredConstraints = requiredConstraints;
        mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST;
        addDynamicConstraints(dynamicConstraints);
@@ -1082,12 +1137,35 @@ public final class JobStatus {
        return hasConstraint(CONSTRAINT_IDLE);
    }

    /** Returns true if the job has a prefetch constraint */
    public boolean hasPrefetchConstraint() {
        return hasConstraint(CONSTRAINT_PREFETCH);
    }

    public boolean hasContentTriggerConstraint() {
        // No need to check mDynamicConstraints since content trigger will only be in that list if
        // it's already in the requiredConstraints list.
        return (requiredConstraints&CONSTRAINT_CONTENT_TRIGGER) != 0;
    }

    /** Returns true if the job has flexible job constraints enabled */
    public boolean hasFlexibilityConstraint() {
        return (requiredConstraints & CONSTRAINT_FLEXIBLE) != 0;
    }

    /** Returns the number of flexible job constraints required to be satisfied to execute */
    public int getNumRequiredFlexibleConstraints() {
        return mNumRequiredFlexibleConstraints;
    }

    /**
     * Returns the number of required flexible job constraints that have been dropped with time.
     * The lower this number is the easier it is for the flexibility constraint to be satisfied.
     */
    public int getNumDroppedFlexibleConstraints() {
        return mNumDroppedFlexibleConstraints;
    }

    /**
     * Checks both {@link #requiredConstraints} and {@link #mDynamicConstraints} to see if this job
     * requires the specified constraint.
@@ -1128,6 +1206,10 @@ public final class JobStatus {
        return mOriginalLatestRunTimeElapsedMillis;
    }

    public int getFlexibleConstraints() {
        return mFlexibleConstraints;
    }

    public void setOriginalLatestRunTimeElapsed(long latestRunTimeElapsed) {
        mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed;
    }
@@ -1301,6 +1383,11 @@ public final class JobStatus {
        return false;
    }

    /** @return true if the constraint was changed, false otherwise. */
    boolean setFlexibilityConstraintSatisfied(final long nowElapsed, boolean state) {
        return setConstraintSatisfied(CONSTRAINT_FLEXIBLE, nowElapsed, state);
    }

    /**
     * Sets whether or not this job is approved to be treated as an expedited job based on quota
     * policy.
@@ -1490,6 +1577,18 @@ public final class JobStatus {
        trackingControllers |= which;
    }

    /** Adjusts the number of required flexible constraints by the given number */
    public void adjustNumRequiredFlexibleConstraints(int adjustment) {
        mNumRequiredFlexibleConstraints += adjustment;
        if (mNumRequiredFlexibleConstraints < 0) {
            mNumRequiredFlexibleConstraints = 0;
        }
        mNumDroppedFlexibleConstraints -= adjustment;
        if (mNumDroppedFlexibleConstraints < 0) {
            mNumDroppedFlexibleConstraints = 0;
        }
    }

    /**
     * Add additional constraints to prevent this job from running when doze or battery saver are
     * active.
@@ -1650,12 +1749,14 @@ public final class JobStatus {
    /** All constraints besides implicit and deadline. */
    static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW
            | CONSTRAINT_STORAGE_NOT_LOW | CONSTRAINT_TIMING_DELAY | CONSTRAINT_CONNECTIVITY
            | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER | CONSTRAINT_PREFETCH;
            | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER | CONSTRAINT_PREFETCH
            | CONSTRAINT_FLEXIBLE;

    // Soft override covers all non-"functional" constraints
    static final int SOFT_OVERRIDE_CONSTRAINTS =
            CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW
                    | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE | CONSTRAINT_PREFETCH;
                    | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE | CONSTRAINT_PREFETCH
                    | CONSTRAINT_FLEXIBLE;

    /** Returns true whenever all dynamically set constraints are satisfied. */
    public boolean areDynamicConstraintsSatisfied() {
+242 −0

File added.

Preview size limit exceeded, changes collapsed.