Merge "Add basic TARE StateController."

This commit is contained in:
Kweku Adams
2021-08-03 11:18:01 +00:00
committed by Android (Google) Code Review
3 changed files with 474 additions and 3 deletions

View File

@@ -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(")");

View File

@@ -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) {

View File

@@ -0,0 +1,418 @@
/*
* 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();
}
}