diff options
author | Kweku Adams <kwekua@google.com> | 2021-02-17 13:57:13 -0800 |
---|---|---|
committer | Kweku Adams <kwekua@google.com> | 2021-02-18 09:09:19 -0800 |
commit | a4cb64d92cd5bec7997e68923e81dba57b51f88a (patch) | |
tree | c82a17ae120d5f6a6e94aeb323f28c4c567169df | |
parent | f3efab132663c5e96aaad5b405c23dc64f4575d6 (diff) |
Make job execution limits flexible.
1. Change the execution timeout to allow all jobs to run for longer than
10 minutes if the app has sufficient quota and JobScheduler is quiet.
If jobs end up running longer than 10 minutes, their execution is
capped at 30 minutes or their remaining quota, whichever is less.
Overrunning jobs will also be stopped if there are pending jobs
waiting for an execution slot.
2. Expedited jobs will have a minimum execution timeout of 3 minutes.
The above behavior with flexible limits also apply to EJs. However,
EJs will not be allowed to overrun if the device is in Doze or
battery saver. The limit is flexible, but EJs for apps in the
RESTRICTED bucket cannot have a minimum timeout greater than 5
minutes (ie. if we change the minimum timeout for EJs to be greater
than 5 minutes, RESTRICTED EJs will have their minimum set to 5
minutes).
Since TOP-started jobs and jobs running while the app is FGS don't
count towards quota, their calculations are handled separately. Those
jobs currently have a minimum guarantee of 15 minutes (50% of
ACTIVE/WORKING limits = 15 minutes). The gist of the allowance is:
min runtime = 5 minutes
TOP: max(min runtime, 50% ACTIVE limit, remaining quota)
FGS: max(min runtime, 50% WORKING limit, remaining quota)
Else: max(min runtime, remaining quota)
3. Update documentation to clarify execution limit differences across
versions.
Bug: 19536175
Bug: 171305774
Test: atest CtsJobSchedulerTestCases
Test: atest FrameworksMockingServicesTests:ConnectivityControllerTest
Test: atest FrameworksMockingServicesTests:JobSchedulerServiceTest
Test: atest FrameworksMockingServicesTests:QuotaControllerTest
Test: atest FrameworksServicesTests:PrioritySchedulingTest
Test: atest FrameworksServicesTests:WorkCountTrackerTest
Test: atest FrameworksServicesTests:WorkTypeConfigTest
Change-Id: I21674fdcddfa849581198a1a9133c431fb7cfa46
8 files changed, 447 insertions, 68 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 930415fcd8fd..b7a3f1083176 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -59,6 +59,12 @@ import java.util.Objects; * constraint on the JobInfo object that you are creating. Otherwise, the builder would throw an * exception when building. From Android version {@link Build.VERSION_CODES#Q} and onwards, it is * valid to schedule jobs with no constraints. + * <p> In Android version {@link Build.VERSION_CODES#LOLLIPOP}, jobs had a maximum execution time + * of one minute. Starting with Android version {@link Build.VERSION_CODES#M} and ending with + * Android version {@link Build.VERSION_CODES#R}, jobs had a maximum execution time of 10 minutes. + * Starting from Android version {@link Build.VERSION_CODES#S}, jobs will still be stopped after + * 10 minutes if the system is busy or needs the resources, but if not, jobs may continue running + * longer than 10 minutes. */ public class JobInfo implements Parcelable { private static String TAG = "JobInfo"; @@ -1461,11 +1467,13 @@ public class JobInfo implements Parcelable { * possible with stronger guarantees than regular jobs. These "expedited" jobs will: * <ol> * <li>Run as soon as possible</li> - * <li>Be exempted from Doze and battery saver restrictions</li> + * <li>Be less restricted during Doze and battery saver</li> * <li>Have network access</li> - * <li>Less likely to be killed than regular jobs</li> + * <li>Be less likely to be killed than regular jobs</li> + * <li>Be subject to background location throttling</li> * </ol> * + * <p> * Since these jobs have stronger guarantees than regular jobs, they will be subject to * stricter quotas. As long as an app has available expedited quota, jobs scheduled with * this set to true will run with these guarantees. If an app has run out of available @@ -1475,9 +1483,18 @@ public class JobInfo implements Parcelable { * will immediately return {@link JobScheduler#RESULT_FAILURE} if the app does not have * available quota (and the job will not be successfully scheduled). * + * <p> * Expedited jobs may only set network, storage-not-low, and persistence constraints. * No other constraints are allowed. * + * <p> + * Assuming all constraints remain satisfied (including ideal system load conditions), + * expedited jobs are guaranteed to have a minimum allowed runtime of 1 minute. If your + * app has remaining expedited job quota, then the expedited job <i>may</i> potentially run + * longer until remaining quota is used up. Just like with regular jobs, quota is not + * consumed while the app is on top and visible to the user. + * + * <p> * Note: Even though expedited jobs are meant to run as soon as possible, they may be * deferred if the system is under heavy load or requested constraints are not satisfied. * diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index 164781a250b7..af9771553063 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -20,6 +20,7 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.UserSwitchObserver; @@ -80,6 +81,8 @@ class JobConcurrencyManager { static final int WORK_TYPE_BGUSER = 1 << 3; @VisibleForTesting static final int NUM_WORK_TYPES = 4; + private static final int ALL_WORK_TYPES = + WORK_TYPE_TOP | WORK_TYPE_EJ | WORK_TYPE_BG | WORK_TYPE_BGUSER; @IntDef(prefix = {"WORK_TYPE_"}, flag = true, value = { WORK_TYPE_NONE, @@ -92,6 +95,23 @@ class JobConcurrencyManager { public @interface WorkType { } + private static String workTypeToString(@WorkType int workType) { + switch (workType) { + case WORK_TYPE_NONE: + return "NONE"; + case WORK_TYPE_TOP: + return "TOP"; + case WORK_TYPE_EJ: + return "EJ"; + case WORK_TYPE_BG: + return "BG"; + case WORK_TYPE_BGUSER: + return "BGUSER"; + default: + return "WORK(" + workType + ")"; + } + } + private final Object mLock; private final JobSchedulerService mService; private final Context mContext; @@ -182,10 +202,16 @@ class JobConcurrencyManager { int[] mRecycledWorkTypeForContext = new int[MAX_JOB_CONTEXTS_COUNT]; + String[] mRecycledPreemptReasonForContext = new String[MAX_JOB_CONTEXTS_COUNT]; + + String[] mRecycledShouldStopJobReason = new String[MAX_JOB_CONTEXTS_COUNT]; + private final ArraySet<JobStatus> mRunningJobs = new ArraySet<>(); private final WorkCountTracker mWorkCountTracker = new WorkCountTracker(); + private WorkTypeConfig mWorkTypeConfig = CONFIG_LIMITS_SCREEN_OFF.normal; + /** Wait for this long after screen off before adjusting the job concurrency. */ private long mScreenOffAdjustmentDelayMs = DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS; @@ -353,23 +379,22 @@ class JobConcurrencyManager { final WorkConfigLimitsPerMemoryTrimLevel workConfigs = mEffectiveInteractiveState ? CONFIG_LIMITS_SCREEN_ON : CONFIG_LIMITS_SCREEN_OFF; - WorkTypeConfig workTypeConfig; switch (mLastMemoryTrimLevel) { case ProcessStats.ADJ_MEM_FACTOR_MODERATE: - workTypeConfig = workConfigs.moderate; + mWorkTypeConfig = workConfigs.moderate; break; case ProcessStats.ADJ_MEM_FACTOR_LOW: - workTypeConfig = workConfigs.low; + mWorkTypeConfig = workConfigs.low; break; case ProcessStats.ADJ_MEM_FACTOR_CRITICAL: - workTypeConfig = workConfigs.critical; + mWorkTypeConfig = workConfigs.critical; break; default: - workTypeConfig = workConfigs.normal; + mWorkTypeConfig = workConfigs.normal; break; } - mWorkCountTracker.setConfig(workTypeConfig); + mWorkCountTracker.setConfig(mWorkTypeConfig); } /** @@ -401,13 +426,20 @@ class JobConcurrencyManager { boolean[] slotChanged = mRecycledSlotChanged; int[] preferredUidForContext = mRecycledPreferredUidForContext; int[] workTypeForContext = mRecycledWorkTypeForContext; + String[] preemptReasonForContext = mRecycledPreemptReasonForContext; + String[] shouldStopJobReason = mRecycledShouldStopJobReason; updateCounterConfigLocked(); // Reset everything since we'll re-evaluate the current state. mWorkCountTracker.resetCounts(); + // Update the priorities of jobs that aren't running, and also count the pending work types. + // Do this before the following loop to hopefully reduce the cost of + // shouldStopRunningJobLocked(). + updateNonRunningPriorities(pendingJobs, true); + for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { - final JobServiceContext js = mService.mActiveServices.get(i); + final JobServiceContext js = activeServices.get(i); final JobStatus status = js.getRunningJobLocked(); if ((contextIdToJobMap[i] = status) != null) { @@ -417,14 +449,13 @@ class JobConcurrencyManager { slotChanged[i] = false; preferredUidForContext[i] = js.getPreferredUid(); + preemptReasonForContext[i] = null; + shouldStopJobReason[i] = shouldStopRunningJobLocked(js); } if (DEBUG) { Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial")); } - // Next, update the job priorities, and also count the pending FG / BG jobs. - updateNonRunningPriorities(pendingJobs, true); - mWorkCountTracker.onCountDone(); for (int i = 0; i < pendingJobs.size(); i++) { @@ -434,8 +465,6 @@ class JobConcurrencyManager { continue; } - // TODO(171305774): make sure HPJs aren't pre-empted and add dedicated contexts for them - // Find an available slot for nextPending. The context should be available OR // it should have lowest priority among all running jobs // (sharing the same Uid as nextPending) @@ -444,6 +473,9 @@ class JobConcurrencyManager { int allWorkTypes = getJobWorkTypes(nextPending); int workType = mWorkCountTracker.canJobStart(allWorkTypes); boolean startingJob = false; + String preemptReason = null; + // TODO(141645789): rewrite this to look at empty contexts first so we don't + // unnecessarily preempt for (int j = 0; j < MAX_JOB_CONTEXTS_COUNT; j++) { JobStatus job = contextIdToJobMap[j]; int preferredUid = preferredUidForContext[j]; @@ -464,6 +496,15 @@ class JobConcurrencyManager { continue; } if (job.getUid() != nextPending.getUid()) { + // Maybe stop the job if it has had its day in the sun. + final String reason = shouldStopJobReason[j]; + if (reason != null && mWorkCountTracker.canJobStart(allWorkTypes, + activeServices.get(j).getRunningJobWorkType()) != WORK_TYPE_NONE) { + // Right now, the way the code is set up, we don't need to explicitly + // assign the new job to this context since we'll reassign when the + // preempted job finally stops. + preemptReason = reason; + } continue; } @@ -477,6 +518,7 @@ class JobConcurrencyManager { // the lowest-priority running job minPriorityForPreemption = jobPriority; selectedContextId = j; + preemptReason = "higher priority job found"; // In this case, we're just going to preempt a low priority job, we're not // actually starting a job, so don't set startingJob. } @@ -484,6 +526,7 @@ class JobConcurrencyManager { if (selectedContextId != -1) { contextIdToJobMap[selectedContextId] = nextPending; slotChanged[selectedContextId] = true; + preemptReasonForContext[selectedContextId] = preemptReason; } if (startingJob) { // Increase the counters when we're going to start a job. @@ -509,7 +552,7 @@ class JobConcurrencyManager { + activeServices.get(i).getRunningJobLocked()); } // preferredUid will be set to uid of currently running job. - activeServices.get(i).preemptExecutingJobLocked(); + activeServices.get(i).preemptExecutingJobLocked(preemptReasonForContext[i]); preservePreferredUid = true; } else { final JobStatus pendingJob = contextIdToJobMap[i]; @@ -692,6 +735,91 @@ class JobConcurrencyManager { noteConcurrency(); } + /** + * Returns {@code null} if the job can continue running and a non-null String if the job should + * be stopped. The non-null String details the reason for stopping the job. A job will generally + * be stopped if there similar job types waiting to be run and stopping this job would allow + * another job to run, or if system state suggests the job should stop. + */ + @Nullable + String shouldStopRunningJobLocked(@NonNull JobServiceContext context) { + final JobStatus js = context.getRunningJobLocked(); + if (js == null) { + // This can happen when we try to assign newly found pending jobs to contexts. + return null; + } + + if (context.isWithinExecutionGuaranteeTime()) { + return null; + } + + // Update config in case memory usage has changed significantly. + updateCounterConfigLocked(); + + @WorkType final int workType = context.getRunningJobWorkType(); + + // We're over the minimum guaranteed runtime. Stop the job if we're over config limits or + // there are pending jobs that could replace this one. + if (mRunningJobs.size() > mWorkTypeConfig.getMaxTotal() + || mWorkCountTracker.isOverTypeLimit(workType)) { + return "too many jobs running"; + } + + final List<JobStatus> pendingJobs = mService.mPendingJobs; + final int numPending = pendingJobs.size(); + if (numPending == 0) { + // All quiet. We can let this job run to completion. + return null; + } + + // Only expedited jobs can replace expedited jobs. + if (js.shouldTreatAsExpeditedJob()) { + // Keep fg/bg user distinction. + if (workType == WORK_TYPE_BGUSER) { + // For now, let any bg user job replace a bg user expedited job. + // TODO: limit to ej once we have dedicated bg user ej slots. + if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_BGUSER) > 0) { + return "blocking " + workTypeToString(workType) + " queue"; + } + } else { + if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) { + return "blocking " + workTypeToString(workType) + " queue"; + } + } + + if (mPowerManager.isPowerSaveMode()) { + return "battery saver"; + } + if (mPowerManager.isDeviceIdleMode()) { + return "deep doze"; + } + } + + // Easy check. If there are pending jobs of the same work type, then we know that + // something will replace this. + if (mWorkCountTracker.getPendingJobCount(workType) > 0) { + return "blocking " + workTypeToString(workType) + " queue"; + } + + // Harder check. We need to see if a different work type can replace this job. + int remainingWorkTypes = ALL_WORK_TYPES; + for (int i = 0; i < numPending; ++i) { + final JobStatus pending = pendingJobs.get(i); + final int workTypes = getJobWorkTypes(pending); + if ((workTypes & remainingWorkTypes) > 0 + && mWorkCountTracker.canJobStart(workTypes, workType) != WORK_TYPE_NONE) { + return "blocking other pending jobs"; + } + + remainingWorkTypes = remainingWorkTypes & ~workTypes; + if (remainingWorkTypes == 0) { + break; + } + } + + return null; + } + @GuardedBy("mLock") private String printPendingQueueLocked() { StringBuilder s = new StringBuilder("Pending queue: "); @@ -1362,10 +1490,40 @@ class JobConcurrencyManager { return WORK_TYPE_NONE; } + int canJobStart(int workTypes, @WorkType int replacingWorkType) { + final boolean changedNums; + int oldNumRunning = mNumRunningJobs.get(replacingWorkType); + if (replacingWorkType != WORK_TYPE_NONE && oldNumRunning > 0) { + mNumRunningJobs.put(replacingWorkType, oldNumRunning - 1); + // Lazy implementation to avoid lots of processing. Best way would be to go + // through the whole process of adjusting reservations, but the processing cost + // is likely not worth it. + mNumUnspecializedRemaining++; + changedNums = true; + } else { + changedNums = false; + } + + final int ret = canJobStart(workTypes); + if (changedNums) { + mNumRunningJobs.put(replacingWorkType, oldNumRunning); + mNumUnspecializedRemaining--; + } + return ret; + } + + int getPendingJobCount(@WorkType final int workType) { + return mNumPendingJobs.get(workType, 0); + } + int getRunningJobCount(@WorkType final int workType) { return mNumRunningJobs.get(workType, 0); } + boolean isOverTypeLimit(@WorkType final int workType) { + return getRunningJobCount(workType) > mConfigAbsoluteMaxSlots.get(workType); + } + public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index fdbc0864a59d..00fc937516ae 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -339,6 +339,7 @@ public class JobSchedulerService extends com.android.server.SystemService public void onPropertiesChanged(DeviceConfig.Properties properties) { boolean apiQuotaScheduleUpdated = false; boolean concurrencyUpdated = false; + boolean runtimeUpdated = false; for (int controller = 0; controller < mControllers.size(); controller++) { final StateController sc = mControllers.get(controller); sc.prepareForUpdatedConstantsLocked(); @@ -377,6 +378,14 @@ public class JobSchedulerService extends com.android.server.SystemService case Constants.KEY_CONN_PREFETCH_RELAX_FRAC: mConstants.updateConnectivityConstantsLocked(); break; + case Constants.KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS: + case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS: + case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS: + if (!runtimeUpdated) { + mConstants.updateRuntimeConstantsLocked(); + runtimeUpdated = true; + } + break; default: if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY) && !concurrencyUpdated) { @@ -432,6 +441,11 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = "aq_schedule_return_failure"; + private static final String KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = + "runtime_free_quota_max_limit_ms"; + private static final String KEY_RUNTIME_MIN_GUARANTEE_MS = "runtime_min_guarantee_ms"; + private static final String KEY_RUNTIME_MIN_EJ_GUARANTEE_MS = "runtime_min_ej_guarantee_ms"; + private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; private static final float DEFAULT_HEAVY_USE_FACTOR = .9f; @@ -445,6 +459,12 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; + @VisibleForTesting + public static final long DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = 30 * MINUTE_IN_MILLIS; + @VisibleForTesting + public static final long DEFAULT_RUNTIME_MIN_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS; + @VisibleForTesting + public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS; /** * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early. @@ -509,6 +529,19 @@ public class JobSchedulerService extends com.android.server.SystemService public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; + /** The maximum amount of time we will let a job run for when quota is "free". */ + public long RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; + + /** + * The minimum amount of time we try to guarantee regular jobs will run for. + */ + public long RUNTIME_MIN_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_GUARANTEE_MS; + + /** + * The minimum amount of time we try to guarantee EJs will run for. + */ + public long RUNTIME_MIN_EJ_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS; + private void updateBatchingConstantsLocked() { MIN_READY_NON_ACTIVE_JOBS_COUNT = DeviceConfig.getInt( DeviceConfig.NAMESPACE_JOB_SCHEDULER, @@ -568,6 +601,25 @@ public class JobSchedulerService extends com.android.server.SystemService DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); } + private void updateRuntimeConstantsLocked() { + DeviceConfig.Properties properties = DeviceConfig.getProperties( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS); + + // Make sure min runtime for regular jobs is at least 10 minutes. + RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS, + properties.getLong( + KEY_RUNTIME_MIN_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS)); + // Make sure min runtime for expedited jobs is at least one minute. + RUNTIME_MIN_EJ_GUARANTEE_MS = Math.max(MINUTE_IN_MILLIS, + properties.getLong( + KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS)); + RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS, + properties.getLong(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)); + } + void dump(IndentingPrintWriter pw) { pw.println("Settings:"); pw.increaseIndent(); @@ -591,6 +643,11 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); + pw.print(KEY_RUNTIME_MIN_GUARANTEE_MS, RUNTIME_MIN_GUARANTEE_MS).println(); + pw.print(KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, RUNTIME_MIN_EJ_GUARANTEE_MS).println(); + pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .println(); + pw.decreaseIndent(); } @@ -1602,7 +1659,7 @@ public class JobSchedulerService extends com.android.server.SystemService * time of the job to be the time of completion (i.e. the time at which this function is * called). * <p>This could be inaccurate b/c the job can run for as long as - * {@link com.android.server.job.JobServiceContext#DEFAULT_EXECUTING_TIMESLICE_MILLIS}, but + * {@link Constants#DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS}, but * will lead to underscheduling at least, rather than if we had taken the last execution time * to be the start of the execution. * @@ -2213,11 +2270,24 @@ public class JobSchedulerService extends com.android.server.SystemService return isComponentUsable(job); } + /** Returns the minimum amount of time we should let this job run before timing out. */ + public long getMinJobExecutionGuaranteeMs(JobStatus job) { + synchronized (mLock) { + if (job.shouldTreatAsExpeditedJob()) { + // Don't guarantee RESTRICTED jobs more than 5 minutes. + return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX + ? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS + : Math.min(mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, 5 * MINUTE_IN_MILLIS); + } else { + return mConstants.RUNTIME_MIN_GUARANTEE_MS; + } + } + } + /** Returns the maximum amount of time this job could run for. */ public long getMaxJobExecutionTimeMs(JobStatus job) { synchronized (mLock) { - return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job), - JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS); + return mQuotaController.getMaxJobExecutionTimeMsLocked(job); } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 0aca2461b41c..be91947b0445 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -17,9 +17,9 @@ package com.android.server.job; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; -import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.IJobCallback; import android.app.job.IJobService; @@ -76,14 +76,6 @@ public final class JobServiceContext implements ServiceConnection { private static final boolean DEBUG_STANDBY = JobSchedulerService.DEBUG_STANDBY; private static final String TAG = "JobServiceContext"; - /** Amount of time a job is allowed to execute for before being considered timed-out. */ - public static final long DEFAULT_EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins. - /** - * Amount of time a RESTRICTED expedited job is allowed to execute for before being considered - * timed-out. - */ - public static final long DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS = - DEFAULT_EXECUTING_TIMESLICE_MILLIS / 2; /** Amount of time the JobScheduler waits for the initial service launch+bind. */ private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000; /** Amount of time the JobScheduler will wait for a response from an app for a message. */ @@ -110,6 +102,7 @@ public final class JobServiceContext implements ServiceConnection { /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */ private final JobCompletedListener mCompletedListener; private final JobConcurrencyManager mJobConcurrencyManager; + private final JobSchedulerService mService; /** Used for service binding, etc. */ private final Context mContext; private final Object mLock; @@ -149,6 +142,13 @@ public final class JobServiceContext implements ServiceConnection { private long mExecutionStartTimeElapsed; /** Track when job will timeout. */ private long mTimeoutElapsed; + /** + * The minimum amount of time the context will allow the job to run before checking whether to + * stop it or not. + */ + private long mMinExecutionGuaranteeMillis; + /** The absolute maximum amount of time the job can run */ + private long mMaxExecutionTimeMillis; // Debugging: reason this job was last stopped. public String mStoppedReason; @@ -190,6 +190,7 @@ public final class JobServiceContext implements ServiceConnection { IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper) { mContext = service.getContext(); mLock = service.getLock(); + mService = service; mBatteryStats = batteryStats; mJobPackageTracker = tracker; mCallbackHandler = new JobServiceHandler(looper); @@ -239,6 +240,9 @@ public final class JobServiceContext implements ServiceConnection { isDeadlineExpired, job.shouldTreatAsExpeditedJob(), triggeredUris, triggeredAuthorities, job.network); mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); + mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job); + mMaxExecutionTimeMillis = + Math.max(mService.getMaxJobExecutionTimeMs(job), mMinExecutionGuaranteeMillis); final long whenDeferred = job.getWhenStandbyDeferred(); if (whenDeferred > 0) { @@ -352,8 +356,8 @@ public final class JobServiceContext implements ServiceConnection { } @GuardedBy("mLock") - void preemptExecutingJobLocked() { - doCancelLocked(JobParameters.REASON_PREEMPT, "cancelled due to preemption"); + void preemptExecutingJobLocked(@NonNull String reason) { + doCancelLocked(JobParameters.REASON_PREEMPT, reason); } int getPreferredUid() { @@ -372,6 +376,11 @@ public final class JobServiceContext implements ServiceConnection { return mTimeoutElapsed; } + boolean isWithinExecutionGuaranteeTime() { + return mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis + < sElapsedRealtimeClock.millis(); + } + @GuardedBy("mLock") boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId, String reason) { @@ -607,7 +616,7 @@ public final class JobServiceContext implements ServiceConnection { } @GuardedBy("mLock") - void doCancelLocked(int arg1, String debugReason) { + private void doCancelLocked(int stopReasonCode, String debugReason) { if (mVerb == VERB_FINISHED) { if (DEBUG) { Slog.d(TAG, @@ -615,8 +624,8 @@ public final class JobServiceContext implements ServiceConnection { } return; } - mParams.setStopReason(arg1, debugReason); - if (arg1 == JobParameters.REASON_PREEMPT) { + mParams.setStopReason(stopReasonCode, debugReason); + if (stopReasonCode == JobParameters.REASON_PREEMPT) { mPreferredUid = mRunningJob != null ? mRunningJob.getUid() : NO_PREFERRED_UID; } @@ -767,11 +776,30 @@ public final class JobServiceContext implements ServiceConnection { closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping"); break; case VERB_EXECUTING: - // Not an error - client ran out of time. - Slog.i(TAG, "Client timed out while executing (no jobFinished received), " + - "sending onStop: " + getRunningJobNameLocked()); - mParams.setStopReason(JobParameters.REASON_TIMEOUT, "client timed out"); - sendStopMessageLocked("timeout while executing"); + final long latestStopTimeElapsed = + mExecutionStartTimeElapsed + mMaxExecutionTimeMillis; + final long nowElapsed = sElapsedRealtimeClock.millis(); + if (nowElapsed >= latestStopTimeElapsed) { + // Not an error - client ran out of time. + Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + + " Sending onStop: " + getRunningJobNameLocked()); + mParams.setStopReason(JobParameters.REASON_TIMEOUT, "client timed out"); + sendStopMessageLocked("timeout while executing"); + } else { + // We've given the app the minimum execution time. See if we should stop it or + // let it continue running + final String reason = mJobConcurrencyManager.shouldStopRunningJobLocked(this); + if (reason != null) { + Slog.i(TAG, "Stopping client after min execution time: " + + getRunningJobNameLocked() + " because " + reason); + mParams.setStopReason(JobParameters.REASON_TIMEOUT, reason); + sendStopMessageLocked(reason); + } else { + Slog.i(TAG, "Letting " + getRunningJobNameLocked() + + " continue to run past min execution time"); + scheduleOpTimeOutLocked(); + } + } break; default: Slog.e(TAG, "Handling timeout for an invalid job state: " @@ -878,10 +906,16 @@ public final class JobServiceContext implements ServiceConnection { final long timeoutMillis; switch (mVerb) { case VERB_EXECUTING: - timeoutMillis = mRunningJob.shouldTreatAsExpeditedJob() - && mRunningJob.getStandbyBucket() == RESTRICTED_INDEX - ? DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS - : DEFAULT_EXECUTING_TIMESLICE_MILLIS; + final long earliestStopTimeElapsed = + mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis; + final long latestStopTimeElapsed = + mExecutionStartTimeElapsed + mMaxExecutionTimeMillis; + final long nowElapsed = sElapsedRealtimeClock.millis(); + if (nowElapsed < earliestStopTimeElapsed) { + timeoutMillis = earliestStopTimeElapsed - nowElapsed; + } else { + timeoutMillis = latestStopTimeElapsed - nowElapsed; + } break; case VERB_BINDING: @@ -925,6 +959,13 @@ public final class JobServiceContext implements ServiceConnection { pw.print(", timeout at: "); TimeUtils.formatDuration(mTimeoutElapsed - nowElapsed, pw); pw.println(); + pw.print("Remaining execution limits: ["); + TimeUtils.formatDuration( + (mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis) - nowElapsed, pw); + pw.print(", "); + TimeUtils.formatDuration( + (mExecutionStartTimeElapsed + mMaxExecutionTimeMillis) - nowElapsed, pw); + pw.println("]"); pw.decreaseIndent(); } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index d249f2ae813c..14484ff441ca 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -325,6 +325,8 @@ public final class ConnectivityController extends RestrictingController implemen */ private boolean isInsane(JobStatus jobStatus, Network network, NetworkCapabilities capabilities, Constants constants) { + // Use the maximum possible time since it gives us an upper bound, even though the job + // could end up stopping earlier. final long maxJobExecutionTimeMs = mService.getMaxJobExecutionTimeMs(jobStatus); final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index f2d10ac0f7d7..2196b16e0846 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -72,7 +72,6 @@ import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; import com.android.server.job.ConstantsProto; import com.android.server.job.JobSchedulerService; -import com.android.server.job.JobServiceContext; import com.android.server.job.StateControllerProto; import com.android.server.usage.AppStandbyInternal; import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; @@ -770,18 +769,38 @@ public final class QuotaController extends StateController { /** Returns the maximum amount of time this job could run for. */ public long getMaxJobExecutionTimeMsLocked(@NonNull final JobStatus jobStatus) { - // If quota is currently "free", then the job can run for the full amount of time. - if (mChargeTracker.isCharging() - || isTopStartedJobLocked(jobStatus) - || isUidInForeground(jobStatus.getSourceUid())) { - return JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS; - } - if (jobStatus.shouldTreatAsExpeditedJob()) { - return jobStatus.getStandbyBucket() == RESTRICTED_INDEX - ? JobServiceContext.DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS - : JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS; - } - return getRemainingExecutionTimeLocked(jobStatus); + // Need to look at current proc state as well in the case where the job hasn't started yet. + final boolean isTop = mActivityManagerInternal + .getUidProcessState(jobStatus.getSourceUid()) <= ActivityManager.PROCESS_STATE_TOP; + + if (!jobStatus.shouldTreatAsExpeditedJob()) { + // If quota is currently "free", then the job can run for the full amount of time. + if (mChargeTracker.isCharging() + || isTop + || isTopStartedJobLocked(jobStatus) + || isUidInForeground(jobStatus.getSourceUid())) { + return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; + } + return getTimeUntilQuotaConsumedLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); + } + + // Expedited job. + if (mChargeTracker.isCharging()) { + return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; + } + if (isTop || isTopStartedJobLocked(jobStatus)) { + return Math.max(mEJLimitsMs[ACTIVE_INDEX] / 2, + getTimeUntilEJQuotaConsumedLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName())); + } + if (isUidInForeground(jobStatus.getSourceUid())) { + return Math.max(mEJLimitsMs[WORKING_INDEX] / 2, + getTimeUntilEJQuotaConsumedLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName())); + } + return getTimeUntilEJQuotaConsumedLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); } /** @return true if the job is within expedited job quota. */ @@ -3577,8 +3596,8 @@ public final class QuotaController extends StateController { mEJLimitsMs[RARE_INDEX] = newRareLimitMs; mShouldReevaluateConstraints = true; } - // The limit must be in the range [0 minutes, rare limit]. - long newRestrictedLimitMs = Math.max(0, + // The limit must be in the range [5 minutes, rare limit]. + long newRestrictedLimitMs = Math.max(5 * MINUTE_IN_MILLIS, Math.min(newRareLimitMs, EJ_LIMIT_RESTRICTED_MS)); if (mEJLimitsMs[RESTRICTED_INDEX] != newRestrictedLimitMs) { mEJLimitsMs[RESTRICTED_INDEX] = newRestrictedLimitMs; diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java index 8099eda9a0af..775276bd03f6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java @@ -63,7 +63,6 @@ import android.util.DataUnit; import com.android.server.LocalServices; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobSchedulerService.Constants; -import com.android.server.job.JobServiceContext; import com.android.server.net.NetworkPolicyManagerInternal; import org.junit.Before; @@ -144,8 +143,7 @@ public class ConnectivityControllerTest { .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); final ConnectivityController controller = new ConnectivityController(mService); - when(mService.getMaxJobExecutionTimeMs(any())) - .thenReturn(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS); + when(mService.getMaxJobExecutionTimeMs(any())).thenReturn(10 * 60_000L); // Slow network is too slow assertFalse(controller.isSatisfied(createJobStatus(job), net, diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index c4c9173536ad..b72121f096ba 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -82,8 +82,6 @@ import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; import com.android.server.job.JobSchedulerService; -import com.android.server.job.JobSchedulerService.Constants; -import com.android.server.job.JobServiceContext; import com.android.server.job.JobStore; import com.android.server.job.controllers.QuotaController.ExecutionStats; import com.android.server.job.controllers.QuotaController.QcConstants; @@ -123,6 +121,7 @@ public class QuotaControllerTest { private BroadcastReceiver mChargingReceiver; private QuotaController mQuotaController; private QuotaController.QcConstants mQcConstants; + private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants(); private int mSourceUid; private PowerAllowlistInternal.TempAllowlistChangeListener mTempAllowlistListener; private IUidObserver mUidObserver; @@ -158,7 +157,7 @@ public class QuotaControllerTest { // Called in StateController constructor. when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); - when(mJobSchedulerService.getConstants()).thenReturn(mock(Constants.class)); + when(mJobSchedulerService.getConstants()).thenReturn(mConstants); // Called in QuotaController constructor. IActivityManager activityManager = ActivityManager.getService(); spyOn(activityManager); @@ -1282,23 +1281,23 @@ public class QuotaControllerTest { } @Test - public void testGetMaxJobExecutionTimeLocked() { + public void testGetMaxJobExecutionTimeLocked_Regular() { mQuotaController.saveTimingSession(0, SOURCE_PACKAGE, createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked", 0); - job.setStandbyBucket(RARE_INDEX); + setStandbyBucket(RARE_INDEX, job); setCharging(); synchronized (mQuotaController.mLock) { - assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS, + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mQuotaController.getMaxJobExecutionTimeMsLocked((job))); } setDischarging(); setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); synchronized (mQuotaController.mLock) { - assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS, + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mQuotaController.getMaxJobExecutionTimeMsLocked((job))); } @@ -1310,7 +1309,7 @@ public class QuotaControllerTest { } setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); synchronized (mQuotaController.mLock) { - assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS, + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mQuotaController.getMaxJobExecutionTimeMsLocked((job))); mQuotaController.maybeStopTrackingJobLocked(job, null, false); } @@ -1322,6 +1321,81 @@ public class QuotaControllerTest { } } + @Test + public void testGetMaxJobExecutionTimeLocked_EJ() { + final long timeUsedMs = 3 * MINUTE_IN_MILLIS; + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS), + timeUsedMs, 5), true); + JobStatus job = createExpeditedJobStatus("testGetMaxJobExecutionTimeLocked_EJ", 0); + setStandbyBucket(RARE_INDEX, job); + mQuotaController.maybeStartTrackingJobLocked(job, null); + + setCharging(); + synchronized (mQuotaController.mLock) { + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + setDischarging(); + setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null, false); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Test used quota rolling out of window. + synchronized (mQuotaController.mLock) { + mQuotaController.clearAppStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE); + } + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS, + timeUsedMs, 5), true); + + setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job, null); + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null, false); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + } + /** * Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket * window. @@ -2508,7 +2582,7 @@ public class QuotaControllerTest { assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[WORKING_INDEX]); assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[FREQUENT_INDEX]); assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[RARE_INDEX]); - assertEquals(0, mQuotaController.getEJLimitsMs()[RESTRICTED_INDEX]); + assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[RESTRICTED_INDEX]); assertEquals(0, mQuotaController.getEjLimitSpecialAdditionMs()); assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJLimitWindowSizeMs()); assertEquals(1, mQuotaController.getEJTopAppTimeChunkSizeMs()); |