Merge "Make sure out-of-quota EJs stop in time." into sc-dev am: 31a6954900

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13959205

Change-Id: I3e682b1389575e684bddbd082532ad4f8207982b
This commit is contained in:
Kweku Adams
2021-03-23 16:52:45 +00:00
committed by Automerger Merge Worker
6 changed files with 82 additions and 22 deletions

View File

@@ -891,7 +891,7 @@ class JobConcurrencyManager {
}
// Only expedited jobs can replace expedited jobs.
if (js.shouldTreatAsExpeditedJob()) {
if (js.shouldTreatAsExpeditedJob() || js.startedAsExpeditedJob) {
// Keep fg/bg user distinction.
if (workType == WORK_TYPE_BGUSER_IMPORTANT || workType == WORK_TYPE_BGUSER) {
// Let any important bg user job replace a bg user expedited job.

View File

@@ -2187,6 +2187,10 @@ public class JobSchedulerService extends com.android.server.SystemService
*/
@VisibleForTesting
boolean isReadyToBeExecutedLocked(JobStatus job) {
return isReadyToBeExecutedLocked(job, true);
}
boolean isReadyToBeExecutedLocked(JobStatus job, boolean rejectActive) {
final boolean jobReady = job.isReady();
if (DEBUG) {
@@ -2225,7 +2229,7 @@ public class JobSchedulerService extends com.android.server.SystemService
}
final boolean jobPending = mPendingJobs.contains(job);
final boolean jobActive = isCurrentlyActiveLocked(job);
final boolean jobActive = rejectActive && isCurrentlyActiveLocked(job);
if (DEBUG) {
Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()

View File

@@ -151,6 +151,14 @@ public final class JobServiceContext implements ServiceConnection {
/** The absolute maximum amount of time the job can run */
private long mMaxExecutionTimeMillis;
/**
* The stop reason for a pending cancel. If there's not pending cancel, then the value should be
* {@link JobParameters#STOP_REASON_UNDEFINED}.
*/
private int mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
private int mPendingLegacyStopReason;
private String mPendingDebugStopReason;
// Debugging: reason this job was last stopped.
public String mStoppedReason;
@@ -328,6 +336,7 @@ public final class JobServiceContext implements ServiceConnection {
mAvailable = false;
mStoppedReason = null;
mStoppedTime = 0;
job.startedAsExpeditedJob = job.shouldTreatAsExpeditedJob();
return true;
}
}
@@ -625,6 +634,19 @@ public final class JobServiceContext implements ServiceConnection {
}
return;
}
if (mRunningJob.startedAsExpeditedJob
&& stopReasonCode == JobParameters.STOP_REASON_QUOTA) {
// EJs should be able to run for at least the min upper limit regardless of quota.
final long earliestStopTimeElapsed =
mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis;
final long nowElapsed = sElapsedRealtimeClock.millis();
if (nowElapsed < earliestStopTimeElapsed) {
mPendingStopReason = stopReasonCode;
mPendingLegacyStopReason = legacyStopReason;
mPendingDebugStopReason = debugReason;
return;
}
}
mParams.setStopReason(stopReasonCode, legacyStopReason, debugReason);
if (legacyStopReason == JobParameters.REASON_PREEMPT) {
mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
@@ -777,6 +799,23 @@ public final class JobServiceContext implements ServiceConnection {
closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping");
break;
case VERB_EXECUTING:
if (mPendingStopReason != JobParameters.STOP_REASON_UNDEFINED) {
if (mService.isReadyToBeExecutedLocked(mRunningJob, false)) {
// Job became ready again while we were waiting to stop it (for example,
// the device was temporarily taken off the charger). Ignore the pending
// stop and see what the manager says.
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
mPendingLegacyStopReason = 0;
mPendingDebugStopReason = null;
} else {
Slog.i(TAG, "JS was waiting to stop this job."
+ " Sending onStop: " + getRunningJobNameLocked());
mParams.setStopReason(mPendingStopReason, mPendingLegacyStopReason,
mPendingDebugStopReason);
sendStopMessageLocked(mPendingDebugStopReason);
break;
}
}
final long latestStopTimeElapsed =
mExecutionStartTimeElapsed + mMaxExecutionTimeMillis;
final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -886,6 +925,9 @@ public final class JobServiceContext implements ServiceConnection {
mCancelled = false;
service = null;
mAvailable = true;
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
mPendingLegacyStopReason = 0;
mPendingDebugStopReason = null;
removeOpTimeOutLocked();
mCompletedListener.onJobCompletedLocked(completedJob, legacyStopReason, reschedule);
mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType);
@@ -972,7 +1014,16 @@ public final class JobServiceContext implements ServiceConnection {
pw.print(", ");
TimeUtils.formatDuration(
(mExecutionStartTimeElapsed + mMaxExecutionTimeMillis) - nowElapsed, pw);
pw.println("]");
pw.print("]");
if (mPendingStopReason != JobParameters.STOP_REASON_UNDEFINED) {
pw.print(" Pending stop because ");
pw.print(mPendingStopReason);
pw.print("/");
pw.print(mPendingLegacyStopReason);
pw.print("/");
pw.print(mPendingDebugStopReason);
}
pw.println();
pw.decreaseIndent();
}
}

View File

@@ -328,6 +328,12 @@ public final class JobStatus {
/** The evaluated priority of the job when it started running. */
public int lastEvaluatedPriority;
/**
* Whether or not this particular JobStatus instance was treated as an EJ when it started
* running. This isn't copied over when a job is rescheduled.
*/
public boolean startedAsExpeditedJob = false;
// If non-null, this is work that has been enqueued for the job.
public ArrayList<JobWorkItem> pendingWork;
@@ -1122,18 +1128,19 @@ public final class JobStatus {
*/
public boolean canRunInDoze() {
return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0
|| (shouldTreatAsExpeditedJob()
|| ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob)
&& (mDynamicConstraints & CONSTRAINT_DEVICE_NOT_DOZING) == 0);
}
boolean canRunInBatterySaver() {
return (getInternalFlags() & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0
|| (shouldTreatAsExpeditedJob()
|| ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob)
&& (mDynamicConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) == 0);
}
boolean shouldIgnoreNetworkBlocking() {
return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0 || shouldTreatAsExpeditedJob();
return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0
|| (shouldTreatAsExpeditedJob() || startedAsExpeditedJob);
}
/** @return true if the constraint was changed, false otherwise. */
@@ -2032,7 +2039,10 @@ public final class JobStatus {
pw.println(serviceInfo != null);
if ((getFlags() & JobInfo.FLAG_EXPEDITED) != 0) {
pw.print("readyWithinExpeditedQuota: ");
pw.println(mReadyWithinExpeditedQuota);
pw.print(mReadyWithinExpeditedQuota);
pw.print(" (started as EJ: ");
pw.print(startedAsExpeditedJob);
pw.println(")");
}
pw.decreaseIndent();

View File

@@ -849,10 +849,9 @@ public final class QuotaController extends StateController {
return true;
}
// A job is within quota if one of the following is true:
// 1. it's already running (already executing expedited jobs should be allowed to finish)
// 2. the app is currently in the foreground
// 3. the app overall is within its quota
// 4. It's on the temp allowlist (or within the grace period)
// 1. the app is currently in the foreground
// 2. the app overall is within its quota
// 3. It's on the temp allowlist (or within the grace period)
if (isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) {
return true;
}
@@ -873,13 +872,6 @@ public final class QuotaController extends StateController {
return true;
}
Timer ejTimer = mEJPkgTimers.get(jobStatus.getSourceUserId(),
jobStatus.getSourcePackageName());
// Any already executing expedited jobs should be allowed to finish.
if (ejTimer != null && ejTimer.isRunning(jobStatus)) {
return true;
}
return 0 < getRemainingEJExecutionTimeLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
}
@@ -4153,6 +4145,8 @@ public final class QuotaController extends StateController {
pw.print(", ");
if (js.shouldTreatAsExpeditedJob()) {
pw.print("within EJ quota");
} else if (js.startedAsExpeditedJob) {
pw.print("out of EJ quota");
} else if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
pw.print("within regular quota");
} else {
@@ -4163,6 +4157,8 @@ public final class QuotaController extends StateController {
pw.print(getRemainingEJExecutionTimeLocked(
js.getSourceUserId(), js.getSourcePackageName()));
pw.print("ms remaining in EJ quota");
} else if (js.startedAsExpeditedJob) {
pw.print("should be stopped after min execution time");
} else {
pw.print(getRemainingExecutionTimeLocked(js));
pw.print("ms remaining in quota");

View File

@@ -5416,9 +5416,8 @@ public class QuotaControllerTest {
inOrder.verify(mJobSchedulerService,
timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1))
.onControllerStateChanged();
// Top and bg EJs should still be allowed to run since they started before the app ran
// out of quota.
assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
// Top should still be "in quota" since it started before the app ran on top out of quota.
assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
assertFalse(
jobUnstarted.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
@@ -5467,7 +5466,7 @@ public class QuotaControllerTest {
// wasn't started. Top should remain in quota since it started when the app was in TOP.
assertTrue(jobTop2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
trackJobs(jobBg2);
assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_EXPEDITED_QUOTA));
assertFalse(