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

Commit 0b289b30 authored by Kweku Adams's avatar Kweku Adams Committed by Android (Google) Code Review
Browse files

Merge "Add basic TARE StateController."

parents eb719e8c 891e399b
Loading
Loading
Loading
Loading
+54 −1
Original line number Diff line number Diff line
@@ -89,6 +89,7 @@ public final class JobStatus {
    static final int CONSTRAINT_TIMING_DELAY = 1<<31;
    static final int CONSTRAINT_DEADLINE = 1<<30;
    static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
    static final int CONSTRAINT_TARE_WEALTH = 1 << 27; // Implicit constraint
    static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
    static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
    static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24;      // Implicit constraint
@@ -146,6 +147,7 @@ public final class JobStatus {
    private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER
            | CONSTRAINT_DEADLINE
            | CONSTRAINT_IDLE
            | CONSTRAINT_TARE_WEALTH
            | CONSTRAINT_TIMING_DELAY
            | CONSTRAINT_WITHIN_QUOTA;

@@ -395,6 +397,10 @@ public final class JobStatus {
     * Whether or not this job is approved to be treated as expedited per quota policy.
     */
    private boolean mExpeditedQuotaApproved;
    /**
     * Whether or not this job is approved to be treated as expedited per TARE policy.
     */
    private boolean mExpeditedTareApproved;

    /////// Booleans that track if a job is ready to run. They should be updated whenever dependent
    /////// states change.
@@ -421,6 +427,9 @@ public final class JobStatus {
    /** The job is within its quota based on its standby bucket. */
    private boolean mReadyWithinQuota;

    /** The job has enough credits to run based on TARE. */
    private boolean mReadyTareWealth;

    /** The job's dynamic requirements have been satisfied. */
    private boolean mReadyDynamicSatisfied;

@@ -1230,6 +1239,16 @@ public final class JobStatus {
        return false;
    }

    /** @return true if the constraint was changed, false otherwise. */
    boolean setTareWealthConstraintSatisfied(final long nowElapsed, boolean state) {
        if (setConstraintSatisfied(CONSTRAINT_TARE_WEALTH, nowElapsed, state)) {
            // The constraint was changed. Update the ready flag.
            mReadyTareWealth = state;
            return true;
        }
        return false;
    }

    /**
     * Sets whether or not this job is approved to be treated as an expedited job based on quota
     * policy.
@@ -1245,6 +1264,21 @@ public final class JobStatus {
        return true;
    }

    /**
     * Sets whether or not this job is approved to be treated as an expedited job based on TARE
     * policy.
     *
     * @return true if the approval bit was changed, false otherwise.
     */
    boolean setExpeditedJobTareApproved(final long nowElapsed, boolean state) {
        if (mExpeditedTareApproved == state) {
            return false;
        }
        mExpeditedTareApproved = state;
        updateExpeditedDependencies();
        return true;
    }

    private void updateExpeditedDependencies() {
        // DeviceIdleJobsController currently only tracks jobs with the WILL_BE_FOREGROUND flag.
        // Making it also track requested-expedited jobs would add unnecessary hops since the
@@ -1353,6 +1387,7 @@ public final class JobStatus {
            case CONSTRAINT_DEVICE_NOT_DOZING:
                return JobParameters.STOP_REASON_DEVICE_STATE;

            case CONSTRAINT_TARE_WEALTH:
            case CONSTRAINT_WITHIN_QUOTA:
                return JobParameters.STOP_REASON_QUOTA;

@@ -1405,6 +1440,11 @@ public final class JobStatus {
            Slog.wtf(TAG, "Tried to set quota as a dynamic constraint");
            constraints &= ~CONSTRAINT_WITHIN_QUOTA;
        }
        if ((constraints & CONSTRAINT_TARE_WEALTH) != 0) {
            // Quota should never be used as a dynamic constraint.
            Slog.wtf(TAG, "Tried to set TARE as a dynamic constraint");
            constraints &= ~CONSTRAINT_TARE_WEALTH;
        }

        // Connectivity and content trigger are special since they're only valid to add if the
        // job has requested network or specific content URIs. Adding these constraints to jobs
@@ -1472,6 +1512,10 @@ public final class JobStatus {
                oldValue = mReadyNotDozing;
                mReadyNotDozing = value;
                break;
            case CONSTRAINT_TARE_WEALTH:
                oldValue = mReadyTareWealth;
                mReadyTareWealth = value;
                break;
            case CONSTRAINT_WITHIN_QUOTA:
                oldValue = mReadyWithinQuota;
                mReadyWithinQuota = value;
@@ -1500,6 +1544,9 @@ public final class JobStatus {
            case CONSTRAINT_DEVICE_NOT_DOZING:
                mReadyNotDozing = oldValue;
                break;
            case CONSTRAINT_TARE_WEALTH:
                mReadyTareWealth = oldValue;
                break;
            case CONSTRAINT_WITHIN_QUOTA:
                mReadyWithinQuota = oldValue;
                break;
@@ -1727,6 +1774,9 @@ public final class JobStatus {
        if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
            pw.print(" BACKGROUND_NOT_RESTRICTED");
        }
        if ((constraints & CONSTRAINT_TARE_WEALTH) != 0) {
            pw.print(" TARE_WEALTH");
        }
        if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) {
            pw.print(" WITHIN_QUOTA");
        }
@@ -1991,7 +2041,8 @@ public final class JobStatus {
            pw.println();
            pw.print("Unsatisfied constraints:");
            dumpConstraints(pw,
                    ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA) & ~satisfiedConstraints));
                    ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA | CONSTRAINT_TARE_WEALTH)
                            & ~satisfiedConstraints));
            pw.println();

            pw.println("Constraint history:");
@@ -2050,6 +2101,8 @@ public final class JobStatus {
        if ((getFlags() & JobInfo.FLAG_EXPEDITED) != 0) {
            pw.print("expeditedQuotaApproved: ");
            pw.print(mExpeditedQuotaApproved);
            pw.print(" expeditedTareApproved: ");
            pw.print(mExpeditedTareApproved);
            pw.print(" (started as EJ: ");
            pw.print(startedAsExpeditedJob);
            pw.println(")");
+2 −2
Original line number Diff line number Diff line
@@ -160,8 +160,8 @@ public abstract class StateController {

    public abstract void dumpControllerStateLocked(IndentingPrintWriter pw,
            Predicate<JobStatus> predicate);
    public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
            Predicate<JobStatus> predicate);
    public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
            Predicate<JobStatus> predicate) {}

    /** Dump any internal constants the Controller may have. */
    public void dumpConstants(IndentingPrintWriter pw) {
+418 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;

import android.annotation.NonNull;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.tare.EconomyManagerInternal;
import com.android.server.tare.EconomyManagerInternal.ActionBill;
import com.android.server.tare.JobSchedulerEconomicPolicy;

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

/**
 * Controller that interfaces with Tare ({@link EconomyManagerInternal} and manages each job's
 * ability to run per TARE policies.
 *
 * @see JobSchedulerEconomicPolicy
 */
public class TareController extends StateController {
    private static final String TAG = "JobScheduler.TARE";
    private static final boolean DEBUG = JobSchedulerService.DEBUG
            || Log.isLoggable(TAG, Log.DEBUG);

    /**
     * Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
     * eligible to run unless it can pay for a job start and at least some period of execution time.
     */
    private static final ActionBill BILL_JOB_START_DEFAULT =
            new ActionBill(List.of(
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 1, 0),
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 30_000L)
            ));

    /**
     * Bill to use when a default is currently running. We want to track and make sure the app can
     * continue to pay for 1 more second of execution time. We stop the job when the app can no
     * longer pay for that time.
     */
    private static final ActionBill BILL_JOB_RUNNING_DEFAULT =
            new ActionBill(List.of(
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 1_000L)
            ));

    /**
     * Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
     * eligible to run unless it can pay for a job start and at least some period of execution time.
     */
    private static final ActionBill BILL_JOB_START_EXPEDITED =
            new ActionBill(List.of(
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0),
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 30_000L)
            ));

    /**
     * Bill to use when an EJ is currently running (as an EJ). We want to track and make sure the
     * app can continue to pay for 1 more second of execution time. We stop the job when the app can
     * no longer pay for that time.
     */
    private static final ActionBill BILL_JOB_RUNNING_EXPEDITED =
            new ActionBill(List.of(
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 1_000L)
            ));

    private final EconomyManagerInternal mEconomyManagerInternal;

    private final BackgroundJobsController mBackgroundJobsController;
    private final ConnectivityController mConnectivityController;

    /**
     * Local cache of the ability of each userId-pkg to afford the various bills we're tracking for
     * them.
     */
    @GuardedBy("mLock")
    private final SparseArrayMap<String, ArrayMap<ActionBill, Boolean>> mAffordabilityCache =
            new SparseArrayMap<>();

    /**
     * List of all tracked jobs. Out SparseArrayMap is userId-sourcePkg. The inner mapping is the
     * anticipated actions and all the jobs that are applicable to them.
     */
    @GuardedBy("mLock")
    private final SparseArrayMap<String, ArrayMap<ActionBill, ArraySet<JobStatus>>>
            mRegisteredBillsAndJobs = new SparseArrayMap<>();

    private final EconomyManagerInternal.AffordabilityChangeListener mAffordabilityChangeListener =
            (userId, pkgName, bill, canAfford) -> {
                final long nowElapsed = sElapsedRealtimeClock.millis();
                if (DEBUG) {
                    Slog.d(TAG,
                            userId + ":" + pkgName + " affordability for " + getBillName(bill)
                                    + " changed to " + canAfford);
                }
                synchronized (mLock) {
                    ArrayMap<ActionBill, Boolean> actionAffordability =
                            mAffordabilityCache.get(userId, pkgName);
                    if (actionAffordability == null) {
                        actionAffordability = new ArrayMap<>();
                        mAffordabilityCache.add(userId, pkgName, actionAffordability);
                    }
                    actionAffordability.put(bill, canAfford);

                    final ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                            mRegisteredBillsAndJobs.get(userId, pkgName);
                    if (billToJobMap != null) {
                        final ArraySet<JobStatus> jobs = billToJobMap.get(bill);
                        if (jobs != null) {
                            final ArraySet<JobStatus> changedJobs = new ArraySet<>();
                            for (int i = 0; i < jobs.size(); ++i) {
                                final JobStatus job = jobs.valueAt(i);
                                // Use hasEnoughWealth if canAfford is false in case the job has
                                // other bills it can depend on (eg. EJs being demoted to
                                // regular jobs).
                                if (job.setTareWealthConstraintSatisfied(nowElapsed,
                                        canAfford || hasEnoughWealthLocked(job))) {
                                    changedJobs.add(job);
                                }
                                if (job.isRequestedExpeditedJob()
                                        && setExpeditedTareApproved(job, nowElapsed,
                                        canAffordExpeditedBillLocked(job))) {
                                    changedJobs.add(job);
                                }
                            }
                            if (changedJobs.size() > 0) {
                                mStateChangedListener.onControllerStateChanged(changedJobs);
                            }
                        }
                    }
                }
            };

    @GuardedBy("mLock")
    private boolean mIsEnabled;

    public TareController(JobSchedulerService service,
            @NonNull BackgroundJobsController backgroundJobsController,
            @NonNull ConnectivityController connectivityController) {
        super(service);
        mBackgroundJobsController = backgroundJobsController;
        mConnectivityController = connectivityController;
        mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
    }

    @Override
    public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
        final long nowElapsed = sElapsedRealtimeClock.millis();
        jobStatus.setTareWealthConstraintSatisfied(nowElapsed, hasEnoughWealthLocked(jobStatus));
        setExpeditedTareApproved(jobStatus, nowElapsed,
                jobStatus.isRequestedExpeditedJob() && canAffordExpeditedBillLocked(jobStatus));

        final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
        for (int i = 0; i < bills.size(); ++i) {
            addJobToBillList(jobStatus, bills.valueAt(i));
        }
    }

    @Override
    public void prepareForExecutionLocked(JobStatus jobStatus) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap == null) {
            Slog.e(TAG, "Job is being prepared but doesn't have a pre-existing billToJobMap");
        } else {
            for (int i = 0; i < billToJobMap.size(); ++i) {
                removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
            }
        }
        if (jobStatus.shouldTreatAsExpeditedJob()) {
            addJobToBillList(jobStatus, BILL_JOB_RUNNING_EXPEDITED);
        }
        addJobToBillList(jobStatus, BILL_JOB_RUNNING_DEFAULT);
    }

    @Override
    public void unprepareFromExecutionLocked(JobStatus jobStatus) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
        ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap == null) {
            Slog.e(TAG, "Job was just unprepared but didn't have a pre-existing billToJobMap");
        } else {
            for (int i = 0; i < billToJobMap.size(); ++i) {
                removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
            }
        }
        for (int i = 0; i < bills.size(); ++i) {
            addJobToBillList(jobStatus, bills.valueAt(i));
        }
    }

    @Override
    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
            boolean forUpdate) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap != null) {
            for (int i = 0; i < billToJobMap.size(); ++i) {
                removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
            }
        }
    }

    @GuardedBy("mLock")
    public boolean canScheduleEJ(@NonNull JobStatus jobStatus) {
        if (!mIsEnabled) {
            return true;
        }
        return canAffordBillLocked(jobStatus, BILL_JOB_START_EXPEDITED);
    }

    @GuardedBy("mLock")
    private void addJobToBillList(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap == null) {
            billToJobMap = new ArrayMap<>();
            mRegisteredBillsAndJobs.add(userId, pkgName, billToJobMap);
        }
        ArraySet<JobStatus> jobs = billToJobMap.get(bill);
        if (jobs == null) {
            jobs = new ArraySet<>();
            billToJobMap.put(bill, jobs);
        }
        if (jobs.add(jobStatus)) {
            mEconomyManagerInternal.registerAffordabilityChangeListener(userId, pkgName,
                    mAffordabilityChangeListener, bill);
        }
    }

    @GuardedBy("mLock")
    private void removeJobFromBillList(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        final ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap != null) {
            final ArraySet<JobStatus> jobs = billToJobMap.get(bill);
            if (jobs == null || (jobs.remove(jobStatus) && jobs.size() == 0)) {
                mEconomyManagerInternal.unregisterAffordabilityChangeListener(
                        userId, pkgName, mAffordabilityChangeListener, bill);
                // Remove the cached value so we don't accidentally use it when the app
                // schedules a new job.
                final ArrayMap<ActionBill, Boolean> actionAffordability =
                        mAffordabilityCache.get(userId, pkgName);
                if (actionAffordability != null) {
                    actionAffordability.remove(bill);
                }
            }
        }
    }

    @NonNull
    private ArraySet<ActionBill> getPossibleStartBills(JobStatus jobStatus) {
        // TODO: factor in network cost when available
        final ArraySet<ActionBill> bills = new ArraySet<>();
        if (jobStatus.isRequestedExpeditedJob()) {
            bills.add(BILL_JOB_START_EXPEDITED);
        }
        bills.add(BILL_JOB_START_DEFAULT);
        return bills;
    }

    @GuardedBy("mLock")
    private boolean canAffordBillLocked(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
        if (!mIsEnabled) {
            return true;
        }
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        ArrayMap<ActionBill, Boolean> actionAffordability =
                mAffordabilityCache.get(userId, pkgName);
        if (actionAffordability == null) {
            actionAffordability = new ArrayMap<>();
            mAffordabilityCache.add(userId, pkgName, actionAffordability);
        }

        if (actionAffordability.containsKey(bill)) {
            return actionAffordability.get(bill);
        }

        final boolean canAfford = mEconomyManagerInternal.canPayFor(userId, pkgName, bill);
        actionAffordability.put(bill, canAfford);
        return canAfford;
    }

    @GuardedBy("mLock")
    private boolean canAffordExpeditedBillLocked(@NonNull JobStatus jobStatus) {
        if (!mIsEnabled) {
            return true;
        }
        if (mService.isCurrentlyRunningLocked(jobStatus)) {
            return canAffordBillLocked(jobStatus, BILL_JOB_RUNNING_EXPEDITED);
        }

        return canAffordBillLocked(jobStatus, BILL_JOB_START_EXPEDITED);
    }

    @GuardedBy("mLock")
    private boolean hasEnoughWealthLocked(@NonNull JobStatus jobStatus) {
        if (!mIsEnabled) {
            return true;
        }
        if (mService.isCurrentlyRunningLocked(jobStatus)) {
            if (jobStatus.isRequestedExpeditedJob()) {
                return canAffordBillLocked(jobStatus, BILL_JOB_RUNNING_EXPEDITED)
                        || canAffordBillLocked(jobStatus, BILL_JOB_RUNNING_DEFAULT);
            }
            return canAffordBillLocked(jobStatus, BILL_JOB_RUNNING_DEFAULT);
        }

        if (jobStatus.isRequestedExpeditedJob()
                && canAffordBillLocked(jobStatus, BILL_JOB_START_EXPEDITED)) {
            return true;
        }

        return canAffordBillLocked(jobStatus, BILL_JOB_START_DEFAULT);
    }

    /**
     * If the satisfaction changes, this will tell connectivity & background jobs controller to
     * also re-evaluate their state.
     */
    private boolean setExpeditedTareApproved(@NonNull JobStatus jobStatus, long nowElapsed,
            boolean isApproved) {
        if (jobStatus.setExpeditedJobTareApproved(nowElapsed, isApproved)) {
            mBackgroundJobsController.evaluateStateLocked(jobStatus);
            mConnectivityController.evaluateStateLocked(jobStatus);
            if (isApproved && jobStatus.isReady()) {
                mStateChangedListener.onRunJobNow(jobStatus);
            }
            return true;
        }
        return false;
    }

    @NonNull
    private String getBillName(@NonNull ActionBill bill) {
        if (bill.equals(BILL_JOB_START_EXPEDITED)) {
            return "EJ_START_BILL";
        }
        if (bill.equals(BILL_JOB_RUNNING_EXPEDITED)) {
            return "EJ_RUNNING_BILL";
        }
        if (bill.equals(BILL_JOB_START_DEFAULT)) {
            return "DEFAULT_START_BILL";
        }
        if (bill.equals(BILL_JOB_RUNNING_DEFAULT)) {
            return "DEFAULT_RUNNING_BILL";
        }
        return "UNKNOWN_BILL (" + bill.toString() + ")";
    }

    @Override
    public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
        pw.print("Is enabled: ");
        pw.println(mIsEnabled);

        pw.println("Affordability cache:");
        pw.increaseIndent();
        mAffordabilityCache.forEach((userId, pkgName, billMap) -> {
            final int numBills = billMap.size();
            if (numBills > 0) {
                pw.print(userId);
                pw.print(":");
                pw.print(pkgName);
                pw.println(":");

                pw.increaseIndent();
                for (int i = 0; i < numBills; ++i) {
                    pw.print(getBillName(billMap.keyAt(i)));
                    pw.print(": ");
                    pw.println(billMap.valueAt(i));
                }
                pw.decreaseIndent();
            }
        });
        pw.decreaseIndent();
    }
}