From 891e399b43872ff5808bc1c28235b563f4f38e64 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Wed, 21 Jul 2021 13:41:21 -0700 Subject: [PATCH] Add basic TARE StateController. Add a StateController that will handle the TARE wealth constraint for jobs. This adds the basic implementation but doesn't fully hook up the controller with the whole JobScheduler flow. Bug: 158300259 Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job Test: atest CtsJobSchedulerTestCases Change-Id: I5a288546f0ec215a394edf821318b00d4f8db63d --- .../server/job/controllers/JobStatus.java | 55 ++- .../job/controllers/StateController.java | 4 +- .../job/controllers/TareController.java | 418 ++++++++++++++++++ 3 files changed, 474 insertions(+), 3 deletions(-) create mode 100644 apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 76836e40c36ef..844a480529af9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -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(")"); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java index f0fc3b00a7bca..3fe8df24fa305 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java @@ -160,8 +160,8 @@ public abstract class StateController { public abstract void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate predicate); - public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, - Predicate predicate); + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate predicate) {} /** Dump any internal constants the Controller may have. */ public void dumpConstants(IndentingPrintWriter pw) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java new file mode 100644 index 0000000000000..401646d0aac20 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java @@ -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> 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>> + 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 actionAffordability = + mAffordabilityCache.get(userId, pkgName); + if (actionAffordability == null) { + actionAffordability = new ArrayMap<>(); + mAffordabilityCache.add(userId, pkgName, actionAffordability); + } + actionAffordability.put(bill, canAfford); + + final ArrayMap> billToJobMap = + mRegisteredBillsAndJobs.get(userId, pkgName); + if (billToJobMap != null) { + final ArraySet jobs = billToJobMap.get(bill); + if (jobs != null) { + final ArraySet 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 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> 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 bills = getPossibleStartBills(jobStatus); + ArrayMap> 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> 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> billToJobMap = + mRegisteredBillsAndJobs.get(userId, pkgName); + if (billToJobMap == null) { + billToJobMap = new ArrayMap<>(); + mRegisteredBillsAndJobs.add(userId, pkgName, billToJobMap); + } + ArraySet 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> billToJobMap = + mRegisteredBillsAndJobs.get(userId, pkgName); + if (billToJobMap != null) { + final ArraySet 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 actionAffordability = + mAffordabilityCache.get(userId, pkgName); + if (actionAffordability != null) { + actionAffordability.remove(bill); + } + } + } + } + + @NonNull + private ArraySet getPossibleStartBills(JobStatus jobStatus) { + // TODO: factor in network cost when available + final ArraySet 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 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 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(); + } +}