Loading apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +54 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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. Loading @@ -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; Loading Loading @@ -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. Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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"); } Loading Loading @@ -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:"); Loading Loading @@ -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(")"); Loading apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java +2 −2 Original line number Diff line number Diff line Loading @@ -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) { Loading apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java 0 → 100644 +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(); } } Loading
apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +54 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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. Loading @@ -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; Loading Loading @@ -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. Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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"); } Loading Loading @@ -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:"); Loading Loading @@ -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(")"); Loading
apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java +2 −2 Original line number Diff line number Diff line Loading @@ -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) { Loading
apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java 0 → 100644 +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(); } }