| /* |
| * Copyright (C) 2018 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; |
| |
| import static com.android.server.job.JobSchedulerService.MAX_JOB_CONTEXTS_COUNT; |
| 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; |
| import android.app.job.JobInfo; |
| import android.app.job.JobParameters; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.UserInfo; |
| import android.os.Handler; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.provider.DeviceConfig; |
| import android.util.ArraySet; |
| import android.util.IndentingPrintWriter; |
| import android.util.Pair; |
| import android.util.Pools; |
| import android.util.Slog; |
| import android.util.SparseArrayMap; |
| import android.util.SparseIntArray; |
| import android.util.SparseLongArray; |
| import android.util.TimeUtils; |
| import android.util.proto.ProtoOutputStream; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.app.procstats.ProcessStats; |
| import com.android.internal.util.StatLogger; |
| import com.android.server.JobSchedulerBackgroundThread; |
| import com.android.server.LocalServices; |
| import com.android.server.job.controllers.JobStatus; |
| import com.android.server.job.controllers.StateController; |
| import com.android.server.pm.UserManagerInternal; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.function.Consumer; |
| |
| /** |
| * This class decides, given the various configuration and the system status, which jobs can start |
| * and which {@link JobServiceContext} to run each job on. |
| */ |
| class JobConcurrencyManager { |
| private static final String TAG = JobSchedulerService.TAG + ".Concurrency"; |
| private static final boolean DEBUG = JobSchedulerService.DEBUG; |
| |
| static final String CONFIG_KEY_PREFIX_CONCURRENCY = "concurrency_"; |
| private static final String KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS = |
| CONFIG_KEY_PREFIX_CONCURRENCY + "screen_off_adjustment_delay_ms"; |
| private static final long DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS = 30_000; |
| private static final String KEY_PKG_CONCURRENCY_LIMIT_EJ = |
| CONFIG_KEY_PREFIX_CONCURRENCY + "pkg_concurrency_limit_ej"; |
| private static final int DEFAULT_PKG_CONCURRENCY_LIMIT_EJ = 3; |
| private static final String KEY_PKG_CONCURRENCY_LIMIT_REGULAR = |
| CONFIG_KEY_PREFIX_CONCURRENCY + "pkg_concurrency_limit_regular"; |
| private static final int DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR = MAX_JOB_CONTEXTS_COUNT / 2; |
| |
| /** |
| * Set of possible execution types that a job can have. The actual type(s) of a job are based |
| * on the {@link JobStatus#lastEvaluatedPriority}, which is typically evaluated right before |
| * execution (when we're trying to determine which jobs to run next) and won't change after the |
| * job has started executing. |
| * |
| * Try to give higher priority types lower values. |
| * |
| * @see #getJobWorkTypes(JobStatus) |
| */ |
| |
| /** Job shouldn't run or qualify as any other work type. */ |
| static final int WORK_TYPE_NONE = 0; |
| /** The job is for an app in the TOP state for a currently active user. */ |
| static final int WORK_TYPE_TOP = 1 << 0; |
| /** |
| * The job is for an app in a {@link ActivityManager#PROCESS_STATE_FOREGROUND_SERVICE} or higher |
| * state (excluding {@link ActivityManager#PROCESS_STATE_TOP} for a currently active user. |
| */ |
| static final int WORK_TYPE_FGS = 1 << 1; |
| /** The job is allowed to run as an expedited job for a currently active user. */ |
| static final int WORK_TYPE_EJ = 1 << 2; |
| /** |
| * The job does not satisfy any of the conditions for {@link #WORK_TYPE_TOP}, |
| * {@link #WORK_TYPE_FGS}, or {@link #WORK_TYPE_EJ}, but is for a currently active user, so |
| * can run as a background job. |
| */ |
| static final int WORK_TYPE_BG = 1 << 3; |
| /** |
| * The job is for an app in a {@link ActivityManager#PROCESS_STATE_FOREGROUND_SERVICE} or higher |
| * state, or is allowed to run as an expedited job, but is for a completely background user. |
| */ |
| static final int WORK_TYPE_BGUSER_IMPORTANT = 1 << 4; |
| /** |
| * The job does not satisfy any of the conditions for {@link #WORK_TYPE_TOP}, |
| * {@link #WORK_TYPE_FGS}, or {@link #WORK_TYPE_EJ}, but is for a completely background user, |
| * so can run as a background user job. |
| */ |
| static final int WORK_TYPE_BGUSER = 1 << 5; |
| @VisibleForTesting |
| static final int NUM_WORK_TYPES = 6; |
| private static final int ALL_WORK_TYPES = (1 << NUM_WORK_TYPES) - 1; |
| |
| @IntDef(prefix = {"WORK_TYPE_"}, flag = true, value = { |
| WORK_TYPE_NONE, |
| WORK_TYPE_TOP, |
| WORK_TYPE_FGS, |
| WORK_TYPE_EJ, |
| WORK_TYPE_BG, |
| WORK_TYPE_BGUSER_IMPORTANT, |
| WORK_TYPE_BGUSER |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface WorkType { |
| } |
| |
| @VisibleForTesting |
| static String workTypeToString(@WorkType int workType) { |
| switch (workType) { |
| case WORK_TYPE_NONE: |
| return "NONE"; |
| case WORK_TYPE_TOP: |
| return "TOP"; |
| case WORK_TYPE_FGS: |
| return "FGS"; |
| case WORK_TYPE_EJ: |
| return "EJ"; |
| case WORK_TYPE_BG: |
| return "BG"; |
| case WORK_TYPE_BGUSER: |
| return "BGUSER"; |
| case WORK_TYPE_BGUSER_IMPORTANT: |
| return "BGUSER_IMPORTANT"; |
| default: |
| return "WORK(" + workType + ")"; |
| } |
| } |
| |
| private final Object mLock; |
| private final JobSchedulerService mService; |
| private final Context mContext; |
| private final Handler mHandler; |
| |
| private PowerManager mPowerManager; |
| |
| private boolean mCurrentInteractiveState; |
| private boolean mEffectiveInteractiveState; |
| |
| private long mLastScreenOnRealtime; |
| private long mLastScreenOffRealtime; |
| |
| private static final WorkConfigLimitsPerMemoryTrimLevel CONFIG_LIMITS_SCREEN_ON = |
| new WorkConfigLimitsPerMemoryTrimLevel( |
| new WorkTypeConfig("screen_on_normal", 11, |
| // defaultMin |
| List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), |
| Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)), |
| // defaultMax |
| List.of(Pair.create(WORK_TYPE_BG, 6), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 2), |
| Pair.create(WORK_TYPE_BGUSER, 3)) |
| ), |
| new WorkTypeConfig("screen_on_moderate", 9, |
| // defaultMin |
| List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), |
| Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 1), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)), |
| // defaultMax |
| List.of(Pair.create(WORK_TYPE_BG, 4), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), |
| Pair.create(WORK_TYPE_BGUSER, 1)) |
| ), |
| new WorkTypeConfig("screen_on_low", 6, |
| // defaultMin |
| List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), |
| Pair.create(WORK_TYPE_EJ, 1)), |
| // defaultMax |
| List.of(Pair.create(WORK_TYPE_BG, 2), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), |
| Pair.create(WORK_TYPE_BGUSER, 1)) |
| ), |
| new WorkTypeConfig("screen_on_critical", 6, |
| // defaultMin |
| List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), |
| Pair.create(WORK_TYPE_EJ, 1)), |
| // defaultMax |
| List.of(Pair.create(WORK_TYPE_BG, 1), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), |
| Pair.create(WORK_TYPE_BGUSER, 1)) |
| ) |
| ); |
| private static final WorkConfigLimitsPerMemoryTrimLevel CONFIG_LIMITS_SCREEN_OFF = |
| new WorkConfigLimitsPerMemoryTrimLevel( |
| new WorkTypeConfig("screen_off_normal", 16, |
| // defaultMin |
| List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 2), |
| Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)), |
| // defaultMax |
| List.of(Pair.create(WORK_TYPE_BG, 10), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 2), |
| Pair.create(WORK_TYPE_BGUSER, 3)) |
| ), |
| new WorkTypeConfig("screen_off_moderate", 14, |
| // defaultMin |
| List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 2), |
| Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)), |
| // defaultMax |
| List.of(Pair.create(WORK_TYPE_BG, 7), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), |
| Pair.create(WORK_TYPE_BGUSER, 1)) |
| ), |
| new WorkTypeConfig("screen_off_low", 9, |
| // defaultMin |
| List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), |
| Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 1)), |
| // defaultMax |
| List.of(Pair.create(WORK_TYPE_BG, 3), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), |
| Pair.create(WORK_TYPE_BGUSER, 1)) |
| ), |
| new WorkTypeConfig("screen_off_critical", 6, |
| // defaultMin |
| List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), |
| Pair.create(WORK_TYPE_EJ, 1)), |
| // defaultMax |
| List.of(Pair.create(WORK_TYPE_BG, 1), |
| Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), |
| Pair.create(WORK_TYPE_BGUSER, 1)) |
| ) |
| ); |
| |
| /** |
| * This array essentially stores the state of mActiveServices array. |
| * The ith index stores the job present on the ith JobServiceContext. |
| * We manipulate this array until we arrive at what jobs should be running on |
| * what JobServiceContext. |
| */ |
| JobStatus[] mRecycledAssignContextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT]; |
| |
| boolean[] mRecycledSlotChanged = new boolean[MAX_JOB_CONTEXTS_COUNT]; |
| |
| int[] mRecycledPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT]; |
| |
| int[] mRecycledWorkTypeForContext = new int[MAX_JOB_CONTEXTS_COUNT]; |
| |
| String[] mRecycledPreemptReasonForContext = new String[MAX_JOB_CONTEXTS_COUNT]; |
| |
| int[] mRecycledPreemptReasonCodeForContext = new int[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 final Pools.Pool<PackageStats> mPkgStatsPool = |
| new Pools.SimplePool<>(MAX_JOB_CONTEXTS_COUNT); |
| |
| private final SparseArrayMap<String, PackageStats> mActivePkgStats = new SparseArrayMap<>(); |
| |
| 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; |
| |
| /** |
| * The maximum number of expedited jobs a single userId-package can have running simultaneously. |
| * TOP apps are not limited. |
| */ |
| private long mPkgConcurrencyLimitEj = DEFAULT_PKG_CONCURRENCY_LIMIT_EJ; |
| |
| /** |
| * The maximum number of regular jobs a single userId-package can have running simultaneously. |
| * TOP apps are not limited. |
| */ |
| private long mPkgConcurrencyLimitRegular = DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR; |
| |
| /** Current memory trim level. */ |
| private int mLastMemoryTrimLevel; |
| |
| /** Used to throttle heavy API calls. */ |
| private long mNextSystemStateRefreshTime; |
| private static final int SYSTEM_STATE_REFRESH_MIN_INTERVAL = 1000; |
| |
| private final Consumer<PackageStats> mPackageStatsStagingCountClearer = |
| PackageStats::resetStagedCount; |
| |
| private final StatLogger mStatLogger = new StatLogger(new String[]{ |
| "assignJobsToContexts", |
| "refreshSystemState", |
| }); |
| @VisibleForTesting |
| GracePeriodObserver mGracePeriodObserver; |
| @VisibleForTesting |
| boolean mShouldRestrictBgUser; |
| |
| interface Stats { |
| int ASSIGN_JOBS_TO_CONTEXTS = 0; |
| int REFRESH_SYSTEM_STATE = 1; |
| |
| int COUNT = REFRESH_SYSTEM_STATE + 1; |
| } |
| |
| JobConcurrencyManager(JobSchedulerService service) { |
| mService = service; |
| mLock = mService.mLock; |
| mContext = service.getTestableContext(); |
| |
| mHandler = JobSchedulerBackgroundThread.getHandler(); |
| |
| mGracePeriodObserver = new GracePeriodObserver(mContext); |
| mShouldRestrictBgUser = mContext.getResources().getBoolean( |
| R.bool.config_jobSchedulerRestrictBackgroundUser); |
| } |
| |
| public void onSystemReady() { |
| mPowerManager = mContext.getSystemService(PowerManager.class); |
| |
| final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); |
| filter.addAction(Intent.ACTION_SCREEN_OFF); |
| filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); |
| filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); |
| mContext.registerReceiver(mReceiver, filter); |
| try { |
| ActivityManager.getService().registerUserSwitchObserver(mGracePeriodObserver, TAG); |
| } catch (RemoteException e) { |
| } |
| |
| onInteractiveStateChanged(mPowerManager.isInteractive()); |
| } |
| |
| @GuardedBy("mLock") |
| void onAppRemovedLocked(String pkgName, int uid) { |
| final PackageStats packageStats = mActivePkgStats.get(UserHandle.getUserId(uid), pkgName); |
| if (packageStats != null) { |
| if (packageStats.numRunningEj > 0 || packageStats.numRunningRegular > 0) { |
| // Don't delete the object just yet. We'll remove it in onJobCompleted() when the |
| // jobs officially stop running. |
| Slog.w(TAG, |
| pkgName + "(" + uid + ") marked as removed before jobs stopped running"); |
| } else { |
| mActivePkgStats.delete(UserHandle.getUserId(uid), pkgName); |
| } |
| } |
| } |
| |
| void onUserRemoved(int userId) { |
| mGracePeriodObserver.onUserRemoved(userId); |
| } |
| |
| private final BroadcastReceiver mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| switch (intent.getAction()) { |
| case Intent.ACTION_SCREEN_ON: |
| onInteractiveStateChanged(true); |
| break; |
| case Intent.ACTION_SCREEN_OFF: |
| onInteractiveStateChanged(false); |
| break; |
| case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED: |
| if (mPowerManager != null && mPowerManager.isDeviceIdleMode()) { |
| synchronized (mLock) { |
| stopLongRunningJobsLocked("deep doze"); |
| } |
| } |
| break; |
| case PowerManager.ACTION_POWER_SAVE_MODE_CHANGED: |
| if (mPowerManager != null && mPowerManager.isPowerSaveMode()) { |
| synchronized (mLock) { |
| stopLongRunningJobsLocked("battery saver"); |
| } |
| } |
| break; |
| } |
| } |
| }; |
| |
| /** |
| * Called when the screen turns on / off. |
| */ |
| private void onInteractiveStateChanged(boolean interactive) { |
| synchronized (mLock) { |
| if (mCurrentInteractiveState == interactive) { |
| return; |
| } |
| mCurrentInteractiveState = interactive; |
| if (DEBUG) { |
| Slog.d(TAG, "Interactive: " + interactive); |
| } |
| |
| final long nowRealtime = sElapsedRealtimeClock.millis(); |
| if (interactive) { |
| mLastScreenOnRealtime = nowRealtime; |
| mEffectiveInteractiveState = true; |
| |
| mHandler.removeCallbacks(mRampUpForScreenOff); |
| } else { |
| mLastScreenOffRealtime = nowRealtime; |
| |
| // Set mEffectiveInteractiveState to false after the delay, when we may increase |
| // the concurrency. |
| // We don't need a wakeup alarm here. When there's a pending job, there should |
| // also be jobs running too, meaning the device should be awake. |
| |
| // Note: we can't directly do postDelayed(this::rampUpForScreenOn), because |
| // we need the exact same instance for removeCallbacks(). |
| mHandler.postDelayed(mRampUpForScreenOff, mScreenOffAdjustmentDelayMs); |
| } |
| } |
| } |
| |
| private final Runnable mRampUpForScreenOff = this::rampUpForScreenOff; |
| |
| /** |
| * Called in {@link #mScreenOffAdjustmentDelayMs} after |
| * the screen turns off, in order to increase concurrency. |
| */ |
| private void rampUpForScreenOff() { |
| synchronized (mLock) { |
| // Make sure the screen has really been off for the configured duration. |
| // (There could be a race.) |
| if (!mEffectiveInteractiveState) { |
| return; |
| } |
| if (mLastScreenOnRealtime > mLastScreenOffRealtime) { |
| return; |
| } |
| final long now = sElapsedRealtimeClock.millis(); |
| if ((mLastScreenOffRealtime + mScreenOffAdjustmentDelayMs) > now) { |
| return; |
| } |
| |
| mEffectiveInteractiveState = false; |
| |
| if (DEBUG) { |
| Slog.d(TAG, "Ramping up concurrency"); |
| } |
| |
| mService.maybeRunPendingJobsLocked(); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| boolean isJobRunningLocked(JobStatus job) { |
| return mRunningJobs.contains(job); |
| } |
| |
| /** Return {@code true} if the state was updated. */ |
| @GuardedBy("mLock") |
| private boolean refreshSystemStateLocked() { |
| final long nowUptime = JobSchedulerService.sUptimeMillisClock.millis(); |
| |
| // Only refresh the information every so often. |
| if (nowUptime < mNextSystemStateRefreshTime) { |
| return false; |
| } |
| |
| final long start = mStatLogger.getTime(); |
| mNextSystemStateRefreshTime = nowUptime + SYSTEM_STATE_REFRESH_MIN_INTERVAL; |
| |
| mLastMemoryTrimLevel = ProcessStats.ADJ_MEM_FACTOR_NORMAL; |
| try { |
| mLastMemoryTrimLevel = ActivityManager.getService().getMemoryTrimLevel(); |
| } catch (RemoteException e) { |
| } |
| |
| mStatLogger.logDurationStat(Stats.REFRESH_SYSTEM_STATE, start); |
| return true; |
| } |
| |
| @GuardedBy("mLock") |
| private void updateCounterConfigLocked() { |
| if (!refreshSystemStateLocked()) { |
| return; |
| } |
| |
| final WorkConfigLimitsPerMemoryTrimLevel workConfigs = mEffectiveInteractiveState |
| ? CONFIG_LIMITS_SCREEN_ON : CONFIG_LIMITS_SCREEN_OFF; |
| |
| switch (mLastMemoryTrimLevel) { |
| case ProcessStats.ADJ_MEM_FACTOR_MODERATE: |
| mWorkTypeConfig = workConfigs.moderate; |
| break; |
| case ProcessStats.ADJ_MEM_FACTOR_LOW: |
| mWorkTypeConfig = workConfigs.low; |
| break; |
| case ProcessStats.ADJ_MEM_FACTOR_CRITICAL: |
| mWorkTypeConfig = workConfigs.critical; |
| break; |
| default: |
| mWorkTypeConfig = workConfigs.normal; |
| break; |
| } |
| |
| mWorkCountTracker.setConfig(mWorkTypeConfig); |
| } |
| |
| /** |
| * Takes jobs from pending queue and runs them on available contexts. |
| * If no contexts are available, preempts lower priority jobs to |
| * run higher priority ones. |
| * Lock on mJobs before calling this function. |
| */ |
| @GuardedBy("mLock") |
| void assignJobsToContextsLocked() { |
| final long start = mStatLogger.getTime(); |
| |
| assignJobsToContextsInternalLocked(); |
| |
| mStatLogger.logDurationStat(Stats.ASSIGN_JOBS_TO_CONTEXTS, start); |
| } |
| |
| @GuardedBy("mLock") |
| private void assignJobsToContextsInternalLocked() { |
| if (DEBUG) { |
| Slog.d(TAG, printPendingQueueLocked()); |
| } |
| |
| final List<JobStatus> pendingJobs = mService.mPendingJobs; |
| final List<JobServiceContext> activeServices = mService.mActiveServices; |
| |
| // To avoid GC churn, we recycle the arrays. |
| JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap; |
| boolean[] slotChanged = mRecycledSlotChanged; |
| int[] preferredUidForContext = mRecycledPreferredUidForContext; |
| int[] workTypeForContext = mRecycledWorkTypeForContext; |
| String[] preemptReasonForContext = mRecycledPreemptReasonForContext; |
| int[] preemptReasonCodeForContext = mRecycledPreemptReasonCodeForContext; |
| 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(). |
| updateNonRunningPrioritiesLocked(pendingJobs, true); |
| |
| for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { |
| final JobServiceContext js = activeServices.get(i); |
| final JobStatus status = js.getRunningJobLocked(); |
| |
| if ((contextIdToJobMap[i] = status) != null) { |
| mWorkCountTracker.incrementRunningJobCount(js.getRunningJobWorkType()); |
| workTypeForContext[i] = js.getRunningJobWorkType(); |
| } |
| |
| slotChanged[i] = false; |
| preferredUidForContext[i] = js.getPreferredUid(); |
| preemptReasonForContext[i] = null; |
| preemptReasonCodeForContext[i] = JobParameters.STOP_REASON_UNDEFINED; |
| shouldStopJobReason[i] = shouldStopRunningJobLocked(js); |
| } |
| if (DEBUG) { |
| Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial")); |
| } |
| |
| mWorkCountTracker.onCountDone(); |
| |
| for (int i = 0; i < pendingJobs.size(); i++) { |
| final JobStatus nextPending = pendingJobs.get(i); |
| |
| if (mRunningJobs.contains(nextPending)) { |
| continue; |
| } |
| |
| // 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) |
| int minPriorityForPreemption = Integer.MAX_VALUE; |
| int selectedContextId = -1; |
| int allWorkTypes = getJobWorkTypes(nextPending); |
| int workType = mWorkCountTracker.canJobStart(allWorkTypes); |
| boolean startingJob = false; |
| int preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED; |
| String preemptReason = null; |
| final boolean pkgConcurrencyOkay = !isPkgConcurrencyLimitedLocked(nextPending); |
| // 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]; |
| if (job == null) { |
| final boolean preferredUidOkay = (preferredUid == nextPending.getUid()) |
| || (preferredUid == JobServiceContext.NO_PREFERRED_UID); |
| |
| if (preferredUidOkay && pkgConcurrencyOkay && workType != WORK_TYPE_NONE) { |
| // This slot is free, and we haven't yet hit the limit on |
| // concurrent jobs... we can just throw the job in to here. |
| selectedContextId = j; |
| startingJob = true; |
| break; |
| } |
| // No job on this context, but nextPending can't run here because |
| // the context has a preferred Uid or we have reached the limit on |
| // concurrent jobs. |
| continue; |
| } |
| if (job.getUid() != nextPending.getUid()) { |
| // Maybe stop the job if it has had its day in the sun. Don't let a different |
| // app preempt jobs started for TOP apps though. |
| final String reason = shouldStopJobReason[j]; |
| if (job.lastEvaluatedPriority < JobInfo.PRIORITY_TOP_APP |
| && 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; |
| preemptReasonCode = JobParameters.STOP_REASON_DEVICE_STATE; |
| } |
| continue; |
| } |
| |
| final int jobPriority = mService.evaluateJobPriorityLocked(job); |
| if (jobPriority >= nextPending.lastEvaluatedPriority) { |
| continue; |
| } |
| |
| if (minPriorityForPreemption > jobPriority) { |
| // Step down the preemption threshold - wind up replacing |
| // the lowest-priority running job |
| minPriorityForPreemption = jobPriority; |
| selectedContextId = j; |
| preemptReason = "higher priority job found"; |
| preemptReasonCode = JobParameters.STOP_REASON_PREEMPT; |
| // 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. |
| } |
| } |
| final PackageStats packageStats = getPkgStatsLocked( |
| nextPending.getSourceUserId(), nextPending.getSourcePackageName()); |
| if (selectedContextId != -1) { |
| contextIdToJobMap[selectedContextId] = nextPending; |
| slotChanged[selectedContextId] = true; |
| preemptReasonCodeForContext[selectedContextId] = preemptReasonCode; |
| preemptReasonForContext[selectedContextId] = preemptReason; |
| packageStats.adjustStagedCount(true, nextPending.shouldTreatAsExpeditedJob()); |
| } |
| if (startingJob) { |
| // Increase the counters when we're going to start a job. |
| workTypeForContext[selectedContextId] = workType; |
| mWorkCountTracker.stageJob(workType, allWorkTypes); |
| mActivePkgStats.add( |
| nextPending.getSourceUserId(), nextPending.getSourcePackageName(), |
| packageStats); |
| } |
| } |
| if (DEBUG) { |
| Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final")); |
| |
| Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString()); |
| } |
| |
| for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { |
| boolean preservePreferredUid = false; |
| if (slotChanged[i]) { |
| JobStatus js = activeServices.get(i).getRunningJobLocked(); |
| if (js != null) { |
| if (DEBUG) { |
| Slog.d(TAG, "preempting job: " |
| + activeServices.get(i).getRunningJobLocked()); |
| } |
| // preferredUid will be set to uid of currently running job. |
| activeServices.get(i).cancelExecutingJobLocked( |
| preemptReasonCodeForContext[i], |
| JobParameters.INTERNAL_STOP_REASON_PREEMPT, preemptReasonForContext[i]); |
| // Only preserve the UID if we're preempting for the same UID. If we're stopping |
| // the job because something is pending (eg. EJs), then we shouldn't preserve |
| // the UID. |
| preservePreferredUid = |
| preemptReasonCodeForContext[i] == JobParameters.STOP_REASON_PREEMPT; |
| } else { |
| final JobStatus pendingJob = contextIdToJobMap[i]; |
| if (DEBUG) { |
| Slog.d(TAG, "About to run job on context " |
| + i + ", job: " + pendingJob); |
| } |
| startJobLocked(activeServices.get(i), pendingJob, workTypeForContext[i]); |
| } |
| } |
| if (!preservePreferredUid) { |
| activeServices.get(i).clearPreferredUid(); |
| } |
| } |
| mWorkCountTracker.resetStagingCount(); |
| mActivePkgStats.forEach(mPackageStatsStagingCountClearer); |
| noteConcurrency(); |
| } |
| |
| @GuardedBy("mLock") |
| private void stopLongRunningJobsLocked(@NonNull String debugReason) { |
| for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; ++i) { |
| final JobServiceContext jsc = mService.mActiveServices.get(i); |
| final JobStatus jobStatus = jsc.getRunningJobLocked(); |
| |
| if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime()) { |
| jsc.cancelExecutingJobLocked(JobParameters.STOP_REASON_DEVICE_STATE, |
| JobParameters.INTERNAL_STOP_REASON_TIMEOUT, debugReason); |
| } |
| } |
| } |
| |
| private void noteConcurrency() { |
| mService.mJobPackageTracker.noteConcurrency(mRunningJobs.size(), |
| // TODO: log per type instead of only TOP |
| mWorkCountTracker.getRunningJobCount(WORK_TYPE_TOP)); |
| } |
| |
| @GuardedBy("mLock") |
| private void updateNonRunningPrioritiesLocked(@NonNull final List<JobStatus> pendingJobs, |
| boolean updateCounter) { |
| for (int i = 0; i < pendingJobs.size(); i++) { |
| final JobStatus pending = pendingJobs.get(i); |
| |
| // If job is already running, go to next job. |
| if (mRunningJobs.contains(pending)) { |
| continue; |
| } |
| |
| pending.lastEvaluatedPriority = mService.evaluateJobPriorityLocked(pending); |
| |
| if (updateCounter) { |
| mWorkCountTracker.incrementPendingJobCount(getJobWorkTypes(pending)); |
| } |
| } |
| } |
| |
| @GuardedBy("mLock") |
| @NonNull |
| private PackageStats getPkgStatsLocked(int userId, @NonNull String packageName) { |
| PackageStats packageStats = mActivePkgStats.get(userId, packageName); |
| if (packageStats == null) { |
| packageStats = mPkgStatsPool.acquire(); |
| if (packageStats == null) { |
| packageStats = new PackageStats(); |
| } |
| packageStats.setPackage(userId, packageName); |
| } |
| return packageStats; |
| } |
| |
| @GuardedBy("mLock") |
| private boolean isPkgConcurrencyLimitedLocked(@NonNull JobStatus jobStatus) { |
| if (jobStatus.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) { |
| // Don't restrict top apps' concurrency. The work type limits will make sure |
| // background jobs have slots to run if the system has resources. |
| return false; |
| } |
| // Use < instead of <= as that gives us a little wiggle room in case a new job comes |
| // along very shortly. |
| if (mService.mPendingJobs.size() + mRunningJobs.size() < mWorkTypeConfig.getMaxTotal()) { |
| // Don't artificially limit a single package if we don't even have enough jobs to use |
| // the maximum number of slots. We'll preempt the job later if we need the slot. |
| return false; |
| } |
| final PackageStats packageStats = |
| mActivePkgStats.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); |
| if (packageStats == null) { |
| // No currently running jobs. |
| return false; |
| } |
| if (jobStatus.shouldTreatAsExpeditedJob()) { |
| return packageStats.numRunningEj + packageStats.numStagedEj < mPkgConcurrencyLimitEj; |
| } else { |
| return packageStats.numRunningRegular + packageStats.numStagedRegular |
| < mPkgConcurrencyLimitRegular; |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void startJobLocked(@NonNull JobServiceContext worker, @NonNull JobStatus jobStatus, |
| @WorkType final int workType) { |
| final List<StateController> controllers = mService.mControllers; |
| final int numControllers = controllers.size(); |
| for (int ic = 0; ic < numControllers; ic++) { |
| controllers.get(ic).prepareForExecutionLocked(jobStatus); |
| } |
| final PackageStats packageStats = |
| getPkgStatsLocked(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); |
| packageStats.adjustStagedCount(false, jobStatus.shouldTreatAsExpeditedJob()); |
| if (!worker.executeRunnableJob(jobStatus, workType)) { |
| Slog.e(TAG, "Error executing " + jobStatus); |
| mWorkCountTracker.onStagedJobFailed(workType); |
| for (int ic = 0; ic < numControllers; ic++) { |
| controllers.get(ic).unprepareFromExecutionLocked(jobStatus); |
| } |
| } else { |
| mRunningJobs.add(jobStatus); |
| mWorkCountTracker.onJobStarted(workType); |
| packageStats.adjustRunningCount(true, jobStatus.shouldTreatAsExpeditedJob()); |
| mActivePkgStats.add( |
| jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), packageStats); |
| } |
| final List<JobStatus> pendingJobs = mService.mPendingJobs; |
| if (pendingJobs.remove(jobStatus)) { |
| mService.mJobPackageTracker.noteNonpending(jobStatus); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| void onJobCompletedLocked(@NonNull JobServiceContext worker, @NonNull JobStatus jobStatus, |
| @WorkType final int workType) { |
| mWorkCountTracker.onJobFinished(workType); |
| mRunningJobs.remove(jobStatus); |
| final PackageStats packageStats = |
| mActivePkgStats.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); |
| if (packageStats == null) { |
| Slog.wtf(TAG, "Running job didn't have an active PackageStats object"); |
| } else { |
| packageStats.adjustRunningCount(false, jobStatus.startedAsExpeditedJob); |
| if (packageStats.numRunningEj <= 0 && packageStats.numRunningRegular <= 0) { |
| mActivePkgStats.delete(packageStats.userId, packageStats.packageName); |
| mPkgStatsPool.release(packageStats); |
| } |
| } |
| |
| final List<JobStatus> pendingJobs = mService.mPendingJobs; |
| if (worker.getPreferredUid() != JobServiceContext.NO_PREFERRED_UID) { |
| updateCounterConfigLocked(); |
| // Preemption case needs special care. |
| updateNonRunningPrioritiesLocked(pendingJobs, false); |
| |
| JobStatus highestPriorityJob = null; |
| int highPriWorkType = workType; |
| int highPriAllWorkTypes = workType; |
| JobStatus backupJob = null; |
| int backupWorkType = WORK_TYPE_NONE; |
| int backupAllWorkTypes = WORK_TYPE_NONE; |
| for (int i = 0; i < pendingJobs.size(); i++) { |
| final JobStatus nextPending = pendingJobs.get(i); |
| |
| if (mRunningJobs.contains(nextPending)) { |
| continue; |
| } |
| |
| if (worker.getPreferredUid() != nextPending.getUid()) { |
| if (backupJob == null && !isPkgConcurrencyLimitedLocked(nextPending)) { |
| int allWorkTypes = getJobWorkTypes(nextPending); |
| int workAsType = mWorkCountTracker.canJobStart(allWorkTypes); |
| if (workAsType != WORK_TYPE_NONE) { |
| backupJob = nextPending; |
| backupWorkType = workAsType; |
| backupAllWorkTypes = allWorkTypes; |
| } |
| } |
| continue; |
| } |
| |
| // Only bypass the concurrent limit if we had preempted the job due to a higher |
| // priority job. |
| if (nextPending.lastEvaluatedPriority <= jobStatus.lastEvaluatedPriority |
| && isPkgConcurrencyLimitedLocked(nextPending)) { |
| continue; |
| } |
| |
| if (highestPriorityJob == null |
| || highestPriorityJob.lastEvaluatedPriority |
| < nextPending.lastEvaluatedPriority) { |
| highestPriorityJob = nextPending; |
| } else { |
| continue; |
| } |
| |
| // In this path, we pre-empted an existing job. We don't fully care about the |
| // reserved slots. We should just run the highest priority job we can find, |
| // though it would be ideal to use an available WorkType slot instead of |
| // overloading slots. |
| highPriAllWorkTypes = getJobWorkTypes(nextPending); |
| final int workAsType = mWorkCountTracker.canJobStart(highPriAllWorkTypes); |
| if (workAsType == WORK_TYPE_NONE) { |
| // Just use the preempted job's work type since this new one is technically |
| // replacing it anyway. |
| highPriWorkType = workType; |
| } else { |
| highPriWorkType = workAsType; |
| } |
| } |
| if (highestPriorityJob != null) { |
| if (DEBUG) { |
| Slog.d(TAG, "Running job " + jobStatus + " as preemption"); |
| } |
| mWorkCountTracker.stageJob(highPriWorkType, highPriAllWorkTypes); |
| startJobLocked(worker, highestPriorityJob, highPriWorkType); |
| } else { |
| if (DEBUG) { |
| Slog.d(TAG, "Couldn't find preemption job for uid " + worker.getPreferredUid()); |
| } |
| worker.clearPreferredUid(); |
| if (backupJob != null) { |
| if (DEBUG) { |
| Slog.d(TAG, "Running job " + jobStatus + " instead"); |
| } |
| mWorkCountTracker.stageJob(backupWorkType, backupAllWorkTypes); |
| startJobLocked(worker, backupJob, backupWorkType); |
| } |
| } |
| } else if (pendingJobs.size() > 0) { |
| updateCounterConfigLocked(); |
| updateNonRunningPrioritiesLocked(pendingJobs, false); |
| |
| // This slot is now free and we have pending jobs. Start the highest priority job we |
| // find. |
| JobStatus highestPriorityJob = null; |
| int highPriWorkType = workType; |
| int highPriAllWorkTypes = workType; |
| for (int i = 0; i < pendingJobs.size(); i++) { |
| final JobStatus nextPending = pendingJobs.get(i); |
| |
| if (mRunningJobs.contains(nextPending)) { |
| continue; |
| } |
| |
| if (isPkgConcurrencyLimitedLocked(nextPending)) { |
| continue; |
| } |
| |
| final int allWorkTypes = getJobWorkTypes(nextPending); |
| final int workAsType = mWorkCountTracker.canJobStart(allWorkTypes); |
| if (workAsType == WORK_TYPE_NONE) { |
| continue; |
| } |
| if (highestPriorityJob == null |
| || highestPriorityJob.lastEvaluatedPriority |
| < nextPending.lastEvaluatedPriority) { |
| highestPriorityJob = nextPending; |
| highPriWorkType = workAsType; |
| highPriAllWorkTypes = allWorkTypes; |
| } |
| } |
| |
| if (highestPriorityJob != null) { |
| // This slot is free, and we haven't yet hit the limit on |
| // concurrent jobs... we can just throw the job in to here. |
| if (DEBUG) { |
| Slog.d(TAG, "About to run job: " + jobStatus); |
| } |
| mWorkCountTracker.stageJob(highPriWorkType, highPriAllWorkTypes); |
| startJobLocked(worker, highestPriorityJob, highPriWorkType); |
| } |
| } |
| |
| 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 |
| @GuardedBy("mLock") |
| 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; |
| } |
| |
| // We're over the minimum guaranteed runtime. Stop the job if we're over config limits, |
| // there are pending jobs that could replace this one, or the device state is not conducive |
| // to long runs. |
| |
| if (mPowerManager.isPowerSaveMode()) { |
| return "battery saver"; |
| } |
| if (mPowerManager.isDeviceIdleMode()) { |
| return "deep doze"; |
| } |
| |
| // Update config in case memory usage has changed significantly. |
| updateCounterConfigLocked(); |
| |
| @WorkType final int workType = context.getRunningJobWorkType(); |
| |
| 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() || 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. |
| if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_BGUSER_IMPORTANT) > 0) { |
| return "blocking " + workTypeToString(WORK_TYPE_BGUSER_IMPORTANT) + " queue"; |
| } |
| // Let a fg user EJ preempt a bg user EJ (if able), but not the other way around. |
| if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0 |
| && mWorkCountTracker.canJobStart(WORK_TYPE_EJ, workType) |
| != WORK_TYPE_NONE) { |
| return "blocking " + workTypeToString(WORK_TYPE_EJ) + " queue"; |
| } |
| } else if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) { |
| return "blocking " + workTypeToString(WORK_TYPE_EJ) + " queue"; |
| } |
| // No other pending EJs. Return null so we don't let regular jobs preempt an EJ. |
| return null; |
| } |
| |
| // 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: "); |
| Iterator<JobStatus> it = mService.mPendingJobs.iterator(); |
| while (it.hasNext()) { |
| JobStatus js = it.next(); |
| s.append("(") |
| .append(js.getJob().getId()) |
| .append(", ") |
| .append(js.getUid()) |
| .append(") "); |
| } |
| return s.toString(); |
| } |
| |
| private static String printContextIdToJobMap(JobStatus[] map, String initial) { |
| StringBuilder s = new StringBuilder(initial + ": "); |
| for (int i=0; i<map.length; i++) { |
| s.append("(") |
| .append(map[i] == null? -1: map[i].getJobId()) |
| .append(map[i] == null? -1: map[i].getUid()) |
| .append(")" ); |
| } |
| return s.toString(); |
| } |
| |
| @GuardedBy("mLock") |
| void updateConfigLocked() { |
| DeviceConfig.Properties properties = |
| DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER); |
| |
| mScreenOffAdjustmentDelayMs = properties.getLong( |
| KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS); |
| |
| CONFIG_LIMITS_SCREEN_ON.normal.update(properties); |
| CONFIG_LIMITS_SCREEN_ON.moderate.update(properties); |
| CONFIG_LIMITS_SCREEN_ON.low.update(properties); |
| CONFIG_LIMITS_SCREEN_ON.critical.update(properties); |
| |
| CONFIG_LIMITS_SCREEN_OFF.normal.update(properties); |
| CONFIG_LIMITS_SCREEN_OFF.moderate.update(properties); |
| CONFIG_LIMITS_SCREEN_OFF.low.update(properties); |
| CONFIG_LIMITS_SCREEN_OFF.critical.update(properties); |
| |
| // Package concurrency limits must in the range [1, MAX_JOB_CONTEXTS_COUNT]. |
| mPkgConcurrencyLimitEj = Math.max(1, Math.min(MAX_JOB_CONTEXTS_COUNT, |
| properties.getInt(KEY_PKG_CONCURRENCY_LIMIT_EJ, DEFAULT_PKG_CONCURRENCY_LIMIT_EJ))); |
| mPkgConcurrencyLimitRegular = Math.max(1, Math.min(MAX_JOB_CONTEXTS_COUNT, |
| properties.getInt( |
| KEY_PKG_CONCURRENCY_LIMIT_REGULAR, DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR))); |
| } |
| |
| @GuardedBy("mLock") |
| public void dumpLocked(IndentingPrintWriter pw, long now, long nowRealtime) { |
| pw.println("Concurrency:"); |
| |
| pw.increaseIndent(); |
| try { |
| pw.println("Configuration:"); |
| pw.increaseIndent(); |
| pw.print(KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, mScreenOffAdjustmentDelayMs).println(); |
| pw.print(KEY_PKG_CONCURRENCY_LIMIT_EJ, mPkgConcurrencyLimitEj).println(); |
| pw.print(KEY_PKG_CONCURRENCY_LIMIT_REGULAR, mPkgConcurrencyLimitRegular).println(); |
| pw.println(); |
| CONFIG_LIMITS_SCREEN_ON.normal.dump(pw); |
| pw.println(); |
| CONFIG_LIMITS_SCREEN_ON.moderate.dump(pw); |
| pw.println(); |
| CONFIG_LIMITS_SCREEN_ON.low.dump(pw); |
| pw.println(); |
| CONFIG_LIMITS_SCREEN_ON.critical.dump(pw); |
| pw.println(); |
| CONFIG_LIMITS_SCREEN_OFF.normal.dump(pw); |
| pw.println(); |
| CONFIG_LIMITS_SCREEN_OFF.moderate.dump(pw); |
| pw.println(); |
| CONFIG_LIMITS_SCREEN_OFF.low.dump(pw); |
| pw.println(); |
| CONFIG_LIMITS_SCREEN_OFF.critical.dump(pw); |
| pw.println(); |
| pw.decreaseIndent(); |
| |
| pw.print("Screen state: current "); |
| pw.print(mCurrentInteractiveState ? "ON" : "OFF"); |
| pw.print(" effective "); |
| pw.print(mEffectiveInteractiveState ? "ON" : "OFF"); |
| pw.println(); |
| |
| pw.print("Last screen ON: "); |
| TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOnRealtime, now); |
| pw.println(); |
| |
| pw.print("Last screen OFF: "); |
| TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOffRealtime, now); |
| pw.println(); |
| |
| pw.println(); |
| |
| pw.print("Current work counts: "); |
| pw.println(mWorkCountTracker); |
| |
| pw.println(); |
| |
| pw.print("mLastMemoryTrimLevel: "); |
| pw.println(mLastMemoryTrimLevel); |
| pw.println(); |
| |
| pw.println("Active Package stats:"); |
| pw.increaseIndent(); |
| mActivePkgStats.forEach(pkgStats -> pkgStats.dumpLocked(pw)); |
| pw.decreaseIndent(); |
| pw.println(); |
| |
| pw.print("User Grace Period: "); |
| pw.println(mGracePeriodObserver.mGracePeriodExpiration); |
| pw.println(); |
| |
| mStatLogger.dump(pw); |
| } finally { |
| pw.decreaseIndent(); |
| } |
| } |
| |
| public void dumpProtoLocked(ProtoOutputStream proto, long tag, long now, long nowRealtime) { |
| final long token = proto.start(tag); |
| |
| proto.write(JobConcurrencyManagerProto.CURRENT_INTERACTIVE_STATE, mCurrentInteractiveState); |
| proto.write(JobConcurrencyManagerProto.EFFECTIVE_INTERACTIVE_STATE, |
| mEffectiveInteractiveState); |
| |
| proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_ON_MS, |
| nowRealtime - mLastScreenOnRealtime); |
| proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_OFF_MS, |
| nowRealtime - mLastScreenOffRealtime); |
| |
| proto.write(JobConcurrencyManagerProto.MEMORY_TRIM_LEVEL, mLastMemoryTrimLevel); |
| |
| mStatLogger.dumpProto(proto, JobConcurrencyManagerProto.STATS); |
| |
| proto.end(token); |
| } |
| |
| /** |
| * Decides whether a job is from the current foreground user or the equivalent. |
| */ |
| @VisibleForTesting |
| boolean shouldRunAsFgUserJob(JobStatus job) { |
| if (!mShouldRestrictBgUser) return true; |
| int userId = job.getSourceUserId(); |
| UserManagerInternal um = LocalServices.getService(UserManagerInternal.class); |
| UserInfo userInfo = um.getUserInfo(userId); |
| |
| // If the user has a parent user (e.g. a work profile of another user), the user should be |
| // treated equivalent as its parent user. |
| if (userInfo.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID |
| && userInfo.profileGroupId != userId) { |
| userId = userInfo.profileGroupId; |
| userInfo = um.getUserInfo(userId); |
| } |
| |
| int currentUser = LocalServices.getService(ActivityManagerInternal.class) |
| .getCurrentUserId(); |
| // A user is treated as foreground user if any of the followings is true: |
| // 1. The user is current user |
| // 2. The user is primary user |
| // 3. The user's grace period has not expired |
| return currentUser == userId || userInfo.isPrimary() |
| || mGracePeriodObserver.isWithinGracePeriodForUser(userId); |
| } |
| |
| int getJobWorkTypes(@NonNull JobStatus js) { |
| int classification = 0; |
| |
| if (shouldRunAsFgUserJob(js)) { |
| if (js.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) { |
| classification |= WORK_TYPE_TOP; |
| } else if (js.lastEvaluatedPriority >= JobInfo.PRIORITY_FOREGROUND_SERVICE) { |
| classification |= WORK_TYPE_FGS; |
| } else { |
| classification |= WORK_TYPE_BG; |
| } |
| |
| if (js.shouldTreatAsExpeditedJob()) { |
| classification |= WORK_TYPE_EJ; |
| } |
| } else { |
| if (js.lastEvaluatedPriority >= JobInfo.PRIORITY_FOREGROUND_SERVICE |
| || js.shouldTreatAsExpeditedJob()) { |
| classification |= WORK_TYPE_BGUSER_IMPORTANT; |
| } |
| // BGUSER_IMPORTANT jobs can also run as BGUSER jobs, so not an 'else' here. |
| classification |= WORK_TYPE_BGUSER; |
| } |
| |
| return classification; |
| } |
| |
| @VisibleForTesting |
| static class WorkTypeConfig { |
| private static final String KEY_PREFIX_MAX_TOTAL = |
| CONFIG_KEY_PREFIX_CONCURRENCY + "max_total_"; |
| private static final String KEY_PREFIX_MAX_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "max_top_"; |
| private static final String KEY_PREFIX_MAX_FGS = CONFIG_KEY_PREFIX_CONCURRENCY + "max_fgs_"; |
| private static final String KEY_PREFIX_MAX_EJ = CONFIG_KEY_PREFIX_CONCURRENCY + "max_ej_"; |
| private static final String KEY_PREFIX_MAX_BG = CONFIG_KEY_PREFIX_CONCURRENCY + "max_bg_"; |
| private static final String KEY_PREFIX_MAX_BGUSER = |
| CONFIG_KEY_PREFIX_CONCURRENCY + "max_bguser_"; |
| private static final String KEY_PREFIX_MAX_BGUSER_IMPORTANT = |
| CONFIG_KEY_PREFIX_CONCURRENCY + "max_bguser_important_"; |
| private static final String KEY_PREFIX_MIN_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "min_top_"; |
| private static final String KEY_PREFIX_MIN_FGS = CONFIG_KEY_PREFIX_CONCURRENCY + "min_fgs_"; |
| private static final String KEY_PREFIX_MIN_EJ = CONFIG_KEY_PREFIX_CONCURRENCY + "min_ej_"; |
| private static final String KEY_PREFIX_MIN_BG = CONFIG_KEY_PREFIX_CONCURRENCY + "min_bg_"; |
| private static final String KEY_PREFIX_MIN_BGUSER = |
| CONFIG_KEY_PREFIX_CONCURRENCY + "min_bguser_"; |
| private static final String KEY_PREFIX_MIN_BGUSER_IMPORTANT = |
| CONFIG_KEY_PREFIX_CONCURRENCY + "min_bguser_important_"; |
| private final String mConfigIdentifier; |
| |
| private int mMaxTotal; |
| private final SparseIntArray mMinReservedSlots = new SparseIntArray(NUM_WORK_TYPES); |
| private final SparseIntArray mMaxAllowedSlots = new SparseIntArray(NUM_WORK_TYPES); |
| private final int mDefaultMaxTotal; |
| private final SparseIntArray mDefaultMinReservedSlots = new SparseIntArray(NUM_WORK_TYPES); |
| private final SparseIntArray mDefaultMaxAllowedSlots = new SparseIntArray(NUM_WORK_TYPES); |
| |
| WorkTypeConfig(@NonNull String configIdentifier, int defaultMaxTotal, |
| List<Pair<Integer, Integer>> defaultMin, List<Pair<Integer, Integer>> defaultMax) { |
| mConfigIdentifier = configIdentifier; |
| mDefaultMaxTotal = mMaxTotal = Math.min(defaultMaxTotal, MAX_JOB_CONTEXTS_COUNT); |
| int numReserved = 0; |
| for (int i = defaultMin.size() - 1; i >= 0; --i) { |
| mDefaultMinReservedSlots.put(defaultMin.get(i).first, defaultMin.get(i).second); |
| numReserved += defaultMin.get(i).second; |
| } |
| if (mDefaultMaxTotal < 0 || numReserved > mDefaultMaxTotal) { |
| // We only create new configs on boot, so this should trigger during development |
| // (before the code gets checked in), so this makes sure the hard-coded defaults |
| // make sense. DeviceConfig values will be handled gracefully in update(). |
| throw new IllegalArgumentException("Invalid default config: t=" + defaultMaxTotal |
| + " min=" + defaultMin + " max=" + defaultMax); |
| } |
| for (int i = defaultMax.size() - 1; i >= 0; --i) { |
| mDefaultMaxAllowedSlots.put(defaultMax.get(i).first, defaultMax.get(i).second); |
| } |
| update(new DeviceConfig.Properties.Builder( |
| DeviceConfig.NAMESPACE_JOB_SCHEDULER).build()); |
| } |
| |
| void update(@NonNull DeviceConfig.Properties properties) { |
| // Ensure total in the range [1, MAX_JOB_CONTEXTS_COUNT]. |
| mMaxTotal = Math.max(1, Math.min(MAX_JOB_CONTEXTS_COUNT, |
| properties.getInt(KEY_PREFIX_MAX_TOTAL + mConfigIdentifier, mDefaultMaxTotal))); |
| |
| mMaxAllowedSlots.clear(); |
| // Ensure they're in the range [1, total]. |
| final int maxTop = Math.max(1, Math.min(mMaxTotal, |
| properties.getInt(KEY_PREFIX_MAX_TOP + mConfigIdentifier, |
| mDefaultMaxAllowedSlots.get(WORK_TYPE_TOP, mMaxTotal)))); |
| mMaxAllowedSlots.put(WORK_TYPE_TOP, maxTop); |
| final int maxFgs = Math.max(1, Math.min(mMaxTotal, |
| properties.getInt(KEY_PREFIX_MAX_FGS + mConfigIdentifier, |
| mDefaultMaxAllowedSlots.get(WORK_TYPE_FGS, mMaxTotal)))); |
| mMaxAllowedSlots.put(WORK_TYPE_FGS, maxFgs); |
| final int maxEj = Math.max(1, Math.min(mMaxTotal, |
| properties.getInt(KEY_PREFIX_MAX_EJ + mConfigIdentifier, |
| mDefaultMaxAllowedSlots.get(WORK_TYPE_EJ, mMaxTotal)))); |
| mMaxAllowedSlots.put(WORK_TYPE_EJ, maxEj); |
| final int maxBg = Math.max(1, Math.min(mMaxTotal, |
| properties.getInt(KEY_PREFIX_MAX_BG + mConfigIdentifier, |
| mDefaultMaxAllowedSlots.get(WORK_TYPE_BG, mMaxTotal)))); |
| mMaxAllowedSlots.put(WORK_TYPE_BG, maxBg); |
| final int maxBgUserImp = Math.max(1, Math.min(mMaxTotal, |
| properties.getInt(KEY_PREFIX_MAX_BGUSER_IMPORTANT + mConfigIdentifier, |
| mDefaultMaxAllowedSlots.get(WORK_TYPE_BGUSER_IMPORTANT, mMaxTotal)))); |
| mMaxAllowedSlots.put(WORK_TYPE_BGUSER_IMPORTANT, maxBgUserImp); |
| final int maxBgUser = Math.max(1, Math.min(mMaxTotal, |
| properties.getInt(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier, |
| mDefaultMaxAllowedSlots.get(WORK_TYPE_BGUSER, mMaxTotal)))); |
| mMaxAllowedSlots.put(WORK_TYPE_BGUSER, maxBgUser); |
| |
| int remaining = mMaxTotal; |
| mMinReservedSlots.clear(); |
| // Ensure top is in the range [1, min(maxTop, total)] |
| final int minTop = Math.max(1, Math.min(Math.min(maxTop, mMaxTotal), |
| properties.getInt(KEY_PREFIX_MIN_TOP + mConfigIdentifier, |
| mDefaultMinReservedSlots.get(WORK_TYPE_TOP)))); |
| mMinReservedSlots.put(WORK_TYPE_TOP, minTop); |
| remaining -= minTop; |
| // Ensure fgs is in the range [0, min(maxFgs, remaining)] |
| final int minFgs = Math.max(0, Math.min(Math.min(maxFgs, remaining), |
| properties.getInt(KEY_PREFIX_MIN_FGS + mConfigIdentifier, |
| mDefaultMinReservedSlots.get(WORK_TYPE_FGS)))); |
| mMinReservedSlots.put(WORK_TYPE_FGS, minFgs); |
| remaining -= minFgs; |
| // Ensure ej is in the range [0, min(maxEj, remaining)] |
| final int minEj = Math.max(0, Math.min(Math.min(maxEj, remaining), |
| properties.getInt(KEY_PREFIX_MIN_EJ + mConfigIdentifier, |
| mDefaultMinReservedSlots.get(WORK_TYPE_EJ)))); |
| mMinReservedSlots.put(WORK_TYPE_EJ, minEj); |
| remaining -= minEj; |
| // Ensure bg is in the range [0, min(maxBg, remaining)] |
| final int minBg = Math.max(0, Math.min(Math.min(maxBg, remaining), |
| properties.getInt(KEY_PREFIX_MIN_BG + mConfigIdentifier, |
| mDefaultMinReservedSlots.get(WORK_TYPE_BG)))); |
| mMinReservedSlots.put(WORK_TYPE_BG, minBg); |
| remaining -= minBg; |
| // Ensure bg user imp is in the range [0, min(maxBgUserImp, remaining)] |
| final int minBgUserImp = Math.max(0, Math.min(Math.min(maxBgUserImp, remaining), |
| properties.getInt(KEY_PREFIX_MIN_BGUSER_IMPORTANT + mConfigIdentifier, |
| mDefaultMinReservedSlots.get(WORK_TYPE_BGUSER_IMPORTANT, 0)))); |
| mMinReservedSlots.put(WORK_TYPE_BGUSER_IMPORTANT, minBgUserImp); |
| // Ensure bg user is in the range [0, min(maxBgUser, remaining)] |
| final int minBgUser = Math.max(0, Math.min(Math.min(maxBgUser, remaining), |
| properties.getInt(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier, |
| mDefaultMinReservedSlots.get(WORK_TYPE_BGUSER, 0)))); |
| mMinReservedSlots.put(WORK_TYPE_BGUSER, minBgUser); |
| } |
| |
| int getMaxTotal() { |
| return mMaxTotal; |
| } |
| |
| int getMax(@WorkType int workType) { |
| return mMaxAllowedSlots.get(workType, mMaxTotal); |
| } |
| |
| int getMinReserved(@WorkType int workType) { |
| return mMinReservedSlots.get(workType); |
| } |
| |
| void dump(IndentingPrintWriter pw) { |
| pw.print(KEY_PREFIX_MAX_TOTAL + mConfigIdentifier, mMaxTotal).println(); |
| pw.print(KEY_PREFIX_MIN_TOP + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_TOP)) |
| .println(); |
| pw.print(KEY_PREFIX_MAX_TOP + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_TOP)) |
| .println(); |
| pw.print(KEY_PREFIX_MIN_FGS + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_FGS)) |
| .println(); |
| pw.print(KEY_PREFIX_MAX_FGS + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_FGS)) |
| .println(); |
| pw.print(KEY_PREFIX_MIN_EJ + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_EJ)) |
| .println(); |
| pw.print(KEY_PREFIX_MAX_EJ + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_EJ)) |
| .println(); |
| pw.print(KEY_PREFIX_MIN_BG + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_BG)) |
| .println(); |
| pw.print(KEY_PREFIX_MAX_BG + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_BG)) |
| .println(); |
| pw.print(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier, |
| mMinReservedSlots.get(WORK_TYPE_BGUSER_IMPORTANT)).println(); |
| pw.print(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier, |
| mMaxAllowedSlots.get(WORK_TYPE_BGUSER_IMPORTANT)).println(); |
| pw.print(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier, |
| mMinReservedSlots.get(WORK_TYPE_BGUSER)).println(); |
| pw.print(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier, |
| mMaxAllowedSlots.get(WORK_TYPE_BGUSER)).println(); |
| } |
| } |
| |
| /** {@link WorkTypeConfig} for each memory trim level. */ |
| static class WorkConfigLimitsPerMemoryTrimLevel { |
| public final WorkTypeConfig normal; |
| public final WorkTypeConfig moderate; |
| public final WorkTypeConfig low; |
| public final WorkTypeConfig critical; |
| |
| WorkConfigLimitsPerMemoryTrimLevel(WorkTypeConfig normal, WorkTypeConfig moderate, |
| WorkTypeConfig low, WorkTypeConfig critical) { |
| this.normal = normal; |
| this.moderate = moderate; |
| this.low = low; |
| this.critical = critical; |
| } |
| } |
| |
| /** |
| * This class keeps the track of when a user's grace period expires. |
| */ |
| @VisibleForTesting |
| static class GracePeriodObserver extends UserSwitchObserver { |
| // Key is UserId and Value is the time when grace period expires |
| @VisibleForTesting |
| final SparseLongArray mGracePeriodExpiration = new SparseLongArray(); |
| private int mCurrentUserId; |
| @VisibleForTesting |
| int mGracePeriod; |
| private final UserManagerInternal mUserManagerInternal; |
| final Object mLock = new Object(); |
| |
| |
| GracePeriodObserver(Context context) { |
| mCurrentUserId = LocalServices.getService(ActivityManagerInternal.class) |
| .getCurrentUserId(); |
| mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); |
| mGracePeriod = Math.max(0, context.getResources().getInteger( |
| R.integer.config_jobSchedulerUserGracePeriod)); |
| } |
| |
| @Override |
| public void onUserSwitchComplete(int newUserId) { |
| final long expiration = sElapsedRealtimeClock.millis() + mGracePeriod; |
| synchronized (mLock) { |
| if (mCurrentUserId != UserHandle.USER_NULL |
| && mUserManagerInternal.exists(mCurrentUserId)) { |
| mGracePeriodExpiration.append(mCurrentUserId, expiration); |
| } |
| mGracePeriodExpiration.delete(newUserId); |
| mCurrentUserId = newUserId; |
| } |
| } |
| |
| void onUserRemoved(int userId) { |
| synchronized (mLock) { |
| mGracePeriodExpiration.delete(userId); |
| } |
| } |
| |
| @VisibleForTesting |
| public boolean isWithinGracePeriodForUser(int userId) { |
| synchronized (mLock) { |
| return userId == mCurrentUserId |
| || sElapsedRealtimeClock.millis() |
| < mGracePeriodExpiration.get(userId, Long.MAX_VALUE); |
| } |
| } |
| } |
| |
| /** |
| * This class decides, taking into account the current {@link WorkTypeConfig} and how many jobs |
| * are running/pending, how many more job can start. |
| * |
| * Extracted for testing and logging. |
| */ |
| @VisibleForTesting |
| static class WorkCountTracker { |
| private int mConfigMaxTotal; |
| private final SparseIntArray mConfigNumReservedSlots = new SparseIntArray(NUM_WORK_TYPES); |
| private final SparseIntArray mConfigAbsoluteMaxSlots = new SparseIntArray(NUM_WORK_TYPES); |
| private final SparseIntArray mRecycledReserved = new SparseIntArray(NUM_WORK_TYPES); |
| |
| /** |
| * Numbers may be lower in this than in {@link #mConfigNumReservedSlots} if there aren't |
| * enough ready jobs of a type to take up all of the desired reserved slots. |
| */ |
| private final SparseIntArray mNumActuallyReservedSlots = new SparseIntArray(NUM_WORK_TYPES); |
| private final SparseIntArray mNumPendingJobs = new SparseIntArray(NUM_WORK_TYPES); |
| private final SparseIntArray mNumRunningJobs = new SparseIntArray(NUM_WORK_TYPES); |
| private final SparseIntArray mNumStartingJobs = new SparseIntArray(NUM_WORK_TYPES); |
| private int mNumUnspecializedRemaining = 0; |
| |
| void setConfig(@NonNull WorkTypeConfig workTypeConfig) { |
| mConfigMaxTotal = workTypeConfig.getMaxTotal(); |
| for (int workType = 1; workType < ALL_WORK_TYPES; workType <<= 1) { |
| mConfigNumReservedSlots.put(workType, workTypeConfig.getMinReserved(workType)); |
| mConfigAbsoluteMaxSlots.put(workType, workTypeConfig.getMax(workType)); |
| } |
| |
| mNumUnspecializedRemaining = mConfigMaxTotal; |
| for (int i = mNumRunningJobs.size() - 1; i >= 0; --i) { |
| mNumUnspecializedRemaining -= Math.max(mNumRunningJobs.valueAt(i), |
| mConfigNumReservedSlots.get(mNumRunningJobs.keyAt(i))); |
| } |
| } |
| |
| void resetCounts() { |
| mNumActuallyReservedSlots.clear(); |
| mNumPendingJobs.clear(); |
| mNumRunningJobs.clear(); |
| resetStagingCount(); |
| } |
| |
| void resetStagingCount() { |
| mNumStartingJobs.clear(); |
| } |
| |
| void incrementRunningJobCount(@WorkType int workType) { |
| mNumRunningJobs.put(workType, mNumRunningJobs.get(workType) + 1); |
| } |
| |
| void incrementPendingJobCount(int workTypes) { |
| adjustPendingJobCount(workTypes, true); |
| } |
| |
| void decrementPendingJobCount(int workTypes) { |
| if (adjustPendingJobCount(workTypes, false) > 1) { |
| // We don't need to adjust reservations if only one work type was modified |
| // because that work type is the one we're using. |
| |
| for (int workType = 1; workType <= workTypes; workType <<= 1) { |
| if ((workType & workTypes) == workType) { |
| maybeAdjustReservations(workType); |
| } |
| } |
| } |
| } |
| |
| /** Returns the number of WorkTypes that were modified. */ |
| private int adjustPendingJobCount(int workTypes, boolean add) { |
| final int adj = add ? 1 : -1; |
| |
| int numAdj = 0; |
| // We don't know which type we'll classify the job as when we run it yet, so make sure |
| // we have space in all applicable slots. |
| for (int workType = 1; workType <= workTypes; workType <<= 1) { |
| if ((workTypes & workType) == workType) { |
| mNumPendingJobs.put(workType, mNumPendingJobs.get(workType) + adj); |
| numAdj++; |
| } |
| } |
| |
| return numAdj; |
| } |
| |
| void stageJob(@WorkType int workType, int allWorkTypes) { |
| final int newNumStartingJobs = mNumStartingJobs.get(workType) + 1; |
| mNumStartingJobs.put(workType, newNumStartingJobs); |
| decrementPendingJobCount(allWorkTypes); |
| if (newNumStartingJobs + mNumRunningJobs.get(workType) |
| > mNumActuallyReservedSlots.get(workType)) { |
| mNumUnspecializedRemaining--; |
| } |
| } |
| |
| void onStagedJobFailed(@WorkType int workType) { |
| final int oldNumStartingJobs = mNumStartingJobs.get(workType); |
| if (oldNumStartingJobs == 0) { |
| Slog.e(TAG, "# staged jobs for " + workType + " went negative."); |
| // We are in a bad state. We will eventually recover when the pending list is |
| // regenerated. |
| return; |
| } |
| mNumStartingJobs.put(workType, oldNumStartingJobs - 1); |
| maybeAdjustReservations(workType); |
| } |
| |
| private void maybeAdjustReservations(@WorkType int workType) { |
| // Always make sure we reserve the minimum number of slots in case new jobs become ready |
| // soon. |
| final int numRemainingForType = Math.max(mConfigNumReservedSlots.get(workType), |
| mNumRunningJobs.get(workType) + mNumStartingJobs.get(workType) |
| + mNumPendingJobs.get(workType)); |
| if (numRemainingForType < mNumActuallyReservedSlots.get(workType)) { |
| // We've run all jobs for this type. Let another type use it now. |
| mNumActuallyReservedSlots.put(workType, numRemainingForType); |
| int assignWorkType = WORK_TYPE_NONE; |
| for (int i = 0; i < mNumActuallyReservedSlots.size(); ++i) { |
| int wt = mNumActuallyReservedSlots.keyAt(i); |
| if (assignWorkType == WORK_TYPE_NONE || wt < assignWorkType) { |
| // Try to give this slot to the highest priority one within its limits. |
| int total = mNumRunningJobs.get(wt) + mNumStartingJobs.get(wt) |
| + mNumPendingJobs.get(wt); |
| if (mNumActuallyReservedSlots.valueAt(i) < mConfigAbsoluteMaxSlots.get(wt) |
| && total > mNumActuallyReservedSlots.valueAt(i)) { |
| assignWorkType = wt; |
| } |
| } |
| } |
| if (assignWorkType != WORK_TYPE_NONE) { |
| mNumActuallyReservedSlots.put(assignWorkType, |
| mNumActuallyReservedSlots.get(assignWorkType) + 1); |
| } else { |
| mNumUnspecializedRemaining++; |
| } |
| } |
| } |
| |
| void onJobStarted(@WorkType int workType) { |
| mNumRunningJobs.put(workType, mNumRunningJobs.get(workType) + 1); |
| final int oldNumStartingJobs = mNumStartingJobs.get(workType); |
| if (oldNumStartingJobs == 0) { |
| Slog.e(TAG, "# stated jobs for " + workType + " went negative."); |
| // We are in a bad state. We will eventually recover when the pending list is |
| // regenerated. For now, only modify the running count. |
| } else { |
| mNumStartingJobs.put(workType, oldNumStartingJobs - 1); |
| } |
| } |
| |
| void onJobFinished(@WorkType int workType) { |
| final int newNumRunningJobs = mNumRunningJobs.get(workType) - 1; |
| if (newNumRunningJobs < 0) { |
| // We are in a bad state. We will eventually recover when the pending list is |
| // regenerated. |
| Slog.e(TAG, "# running jobs for " + workType + " went negative."); |
| return; |
| } |
| mNumRunningJobs.put(workType, newNumRunningJobs); |
| maybeAdjustReservations(workType); |
| } |
| |
| void onCountDone() { |
| // Calculate how many slots to reserve for each work type. "Unspecialized" slots will |
| // be reserved for higher importance types first (ie. top before ej before bg). |
| // Steps: |
| // 1. Account for slots for already running jobs |
| // 2. Use remaining unaccounted slots to try and ensure minimum reserved slots |
| // 3. Allocate remaining up to max, based on importance |
| |
| mNumUnspecializedRemaining = mConfigMaxTotal; |
| |
| // Step 1 |
| for (int workType = 1; workType < ALL_WORK_TYPES; workType <<= 1) { |
| int run = mNumRunningJobs.get(workType); |
| mRecycledReserved.put(workType, run); |
| mNumUnspecializedRemaining -= run; |
| } |
| |
| // Step 2 |
| for (int workType = 1; workType < ALL_WORK_TYPES; workType <<= 1) { |
| int num = mNumRunningJobs.get(workType) + mNumPendingJobs.get(workType); |
| int res = mRecycledReserved.get(workType); |
| int fillUp = Math.max(0, Math.min(mNumUnspecializedRemaining, |
| Math.min(num, mConfigNumReservedSlots.get(workType) - res))); |
| res += fillUp; |
| mRecycledReserved.put(workType, res); |
| mNumUnspecializedRemaining -= fillUp; |
| } |
| |
| // Step 3 |
| for (int workType = 1; workType < ALL_WORK_TYPES; workType <<= 1) { |
| int num = mNumRunningJobs.get(workType) + mNumPendingJobs.get(workType); |
| int res = mRecycledReserved.get(workType); |
| int unspecializedAssigned = Math.max(0, |
| Math.min(mNumUnspecializedRemaining, |
| Math.min(mConfigAbsoluteMaxSlots.get(workType), num) - res)); |
| mNumActuallyReservedSlots.put(workType, res + unspecializedAssigned); |
| mNumUnspecializedRemaining -= unspecializedAssigned; |
| } |
| } |
| |
| int canJobStart(int workTypes) { |
| for (int workType = 1; workType <= workTypes; workType <<= 1) { |
| if ((workTypes & workType) == workType) { |
| final int maxAllowed = Math.min( |
| mConfigAbsoluteMaxSlots.get(workType), |
| mNumActuallyReservedSlots.get(workType) + mNumUnspecializedRemaining); |
| if (mNumRunningJobs.get(workType) + mNumStartingJobs.get(workType) |
| < maxAllowed) { |
| return workType; |
| } |
| } |
| } |
| 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(); |
| |
| sb.append("Config={"); |
| sb.append("tot=").append(mConfigMaxTotal); |
| sb.append(" mins="); |
| sb.append(mConfigNumReservedSlots); |
| sb.append(" maxs="); |
| sb.append(mConfigAbsoluteMaxSlots); |
| sb.append("}"); |
| |
| sb.append(", act res=").append(mNumActuallyReservedSlots); |
| sb.append(", Pending=").append(mNumPendingJobs); |
| sb.append(", Running=").append(mNumRunningJobs); |
| sb.append(", Staged=").append(mNumStartingJobs); |
| sb.append(", # unspecialized remaining=").append(mNumUnspecializedRemaining); |
| |
| return sb.toString(); |
| } |
| } |
| |
| private static class PackageStats { |
| public int userId; |
| public String packageName; |
| public int numRunningEj; |
| public int numRunningRegular; |
| public int numStagedEj; |
| public int numStagedRegular; |
| |
| private void setPackage(int userId, @NonNull String packageName) { |
| this.userId = userId; |
| this.packageName = packageName; |
| numRunningEj = numRunningRegular = 0; |
| resetStagedCount(); |
| } |
| |
| private void resetStagedCount() { |
| numStagedEj = numStagedRegular = 0; |
| } |
| |
| private void adjustRunningCount(boolean add, boolean forEj) { |
| if (forEj) { |
| numRunningEj = Math.max(0, numRunningEj + (add ? 1 : -1)); |
| } else { |
| numRunningRegular = Math.max(0, numRunningRegular + (add ? 1 : -1)); |
| } |
| } |
| |
| private void adjustStagedCount(boolean add, boolean forEj) { |
| if (forEj) { |
| numStagedEj = Math.max(0, numStagedEj + (add ? 1 : -1)); |
| } else { |
| numStagedRegular = Math.max(0, numStagedRegular + (add ? 1 : -1)); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void dumpLocked(IndentingPrintWriter pw) { |
| pw.print("PackageStats{"); |
| pw.print(userId); |
| pw.print("-"); |
| pw.print(packageName); |
| pw.print("#runEJ", numRunningEj); |
| pw.print("#runReg", numRunningRegular); |
| pw.print("#stagedEJ", numStagedEj); |
| pw.print("#stagedReg", numStagedRegular); |
| pw.println("}"); |
| } |
| } |
| } |