blob: bec1947df22845ae8d566baac93ab1fbe6f3830f [file] [log] [blame]
/*
* 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 android.app.ActivityManager;
import android.app.job.JobInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.PowerManager;
import android.os.RemoteException;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.procstats.ProcessStats;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.StatLogger;
import com.android.server.job.JobSchedulerService.Constants;
import com.android.server.job.JobSchedulerService.MaxJobCountsPerMemoryTrimLevel;
import com.android.server.job.controllers.JobStatus;
import com.android.server.job.controllers.StateController;
import java.util.Iterator;
import java.util.List;
/**
* This class decides, given the various configuration and the system status, how many more jobs
* can start.
*/
class JobConcurrencyManager {
private static final String TAG = JobSchedulerService.TAG;
private static final boolean DEBUG = JobSchedulerService.DEBUG;
private final Object mLock;
private final JobSchedulerService mService;
private final JobSchedulerService.Constants mConstants;
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 int MAX_JOB_CONTEXTS_COUNT = JobSchedulerService.MAX_JOB_CONTEXTS_COUNT;
/**
* 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];
/** Max job counts according to the current system state. */
private JobSchedulerService.MaxJobCounts mMaxJobCounts;
private final JobCountTracker mJobCountTracker = new JobCountTracker();
/** 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 StatLogger mStatLogger = new StatLogger(new String[]{
"assignJobsToContexts",
"refreshSystemState",
});
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;
mConstants = service.mConstants;
mContext = service.getContext();
mHandler = BackgroundThread.getHandler();
}
public void onSystemReady() {
mPowerManager = mContext.getSystemService(PowerManager.class);
final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
mContext.registerReceiver(mReceiver, filter);
onInteractiveStateChanged(mPowerManager.isInteractive());
}
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;
}
}
};
/**
* 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 = JobSchedulerService.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,
mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue());
}
}
}
private final Runnable mRampUpForScreenOff = this::rampUpForScreenOff;
/**
* Called in {@link Constants#SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS} 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 = JobSchedulerService.sElapsedRealtimeClock.millis();
if ((mLastScreenOffRealtime
+ mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue())
> now) {
return;
}
mEffectiveInteractiveState = false;
if (DEBUG) {
Slog.d(TAG, "Ramping up concurrency");
}
mService.maybeRunPendingJobsLocked();
}
}
private boolean isFgJob(JobStatus job) {
return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP;
}
@GuardedBy("mLock")
private void refreshSystemStateLocked() {
final long nowUptime = JobSchedulerService.sUptimeMillisClock.millis();
// Only refresh the information every so often.
if (nowUptime < mNextSystemStateRefreshTime) {
return;
}
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);
}
@GuardedBy("mLock")
private void updateMaxCountsLocked() {
refreshSystemStateLocked();
final MaxJobCountsPerMemoryTrimLevel jobCounts = mEffectiveInteractiveState
? mConstants.MAX_JOB_COUNTS_SCREEN_ON
: mConstants.MAX_JOB_COUNTS_SCREEN_OFF;
switch (mLastMemoryTrimLevel) {
case ProcessStats.ADJ_MEM_FACTOR_MODERATE:
mMaxJobCounts = jobCounts.moderate;
break;
case ProcessStats.ADJ_MEM_FACTOR_LOW:
mMaxJobCounts = jobCounts.low;
break;
case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
mMaxJobCounts = jobCounts.critical;
break;
default:
mMaxJobCounts = jobCounts.normal;
break;
}
}
/**
* 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 JobPackageTracker tracker = mService.mJobPackageTracker;
final List<JobStatus> pendingJobs = mService.mPendingJobs;
final List<JobServiceContext> activeServices = mService.mActiveServices;
final List<StateController> controllers = mService.mControllers;
updateMaxCountsLocked();
// To avoid GC churn, we recycle the arrays.
JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap;
boolean[] slotChanged = mRecycledSlotChanged;
int[] preferredUidForContext = mRecycledPreferredUidForContext;
// Initialize the work variables and also count running jobs.
mJobCountTracker.reset(
mMaxJobCounts.getMaxTotal(),
mMaxJobCounts.getMaxBg(),
mMaxJobCounts.getMinBg());
for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
final JobServiceContext js = mService.mActiveServices.get(i);
final JobStatus status = js.getRunningJobLocked();
if ((contextIdToJobMap[i] = status) != null) {
mJobCountTracker.incrementRunningJobCount(isFgJob(status));
}
slotChanged[i] = false;
preferredUidForContext[i] = js.getPreferredUid();
}
if (DEBUG) {
Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial"));
}
// Next, update the job priorities, and also count the pending FG / BG jobs.
for (int i = 0; i < pendingJobs.size(); i++) {
final JobStatus pending = pendingJobs.get(i);
// If job is already running, go to next job.
int jobRunningContext = findJobContextIdFromMap(pending, contextIdToJobMap);
if (jobRunningContext != -1) {
continue;
}
final int priority = mService.evaluateJobPriorityLocked(pending);
pending.lastEvaluatedPriority = priority;
mJobCountTracker.incrementPendingJobCount(isFgJob(pending));
}
mJobCountTracker.onCountDone();
for (int i = 0; i < pendingJobs.size(); i++) {
final JobStatus nextPending = pendingJobs.get(i);
// Unfortunately we need to repeat this relatively expensive check.
int jobRunningContext = findJobContextIdFromMap(nextPending, contextIdToJobMap);
if (jobRunningContext != -1) {
continue;
}
final boolean isPendingFg = isFgJob(nextPending);
// 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;
boolean startingJob = false;
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 && mJobCountTracker.canJobStart(isPendingFg)) {
// 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()) {
continue;
}
final int jobPriority = mService.evaluateJobPriorityLocked(job);
if (jobPriority >= nextPending.lastEvaluatedPriority) {
continue;
}
// TODO lastEvaluatedPriority should be evaluateJobPriorityLocked. (double check it)
if (minPriorityForPreemption > nextPending.lastEvaluatedPriority) {
minPriorityForPreemption = nextPending.lastEvaluatedPriority;
selectedContextId = j;
// 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.
}
}
if (selectedContextId != -1) {
contextIdToJobMap[selectedContextId] = nextPending;
slotChanged[selectedContextId] = true;
}
if (startingJob) {
// Increase the counters when we're going to start a job.
mJobCountTracker.onStartingNewJob(isPendingFg);
}
}
if (DEBUG) {
Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final"));
}
mJobCountTracker.logStatus();
tracker.noteConcurrency(mJobCountTracker.getTotalRunningJobCountToNote(),
mJobCountTracker.getFgRunningJobCountToNote());
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).preemptExecutingJobLocked();
preservePreferredUid = true;
} else {
final JobStatus pendingJob = contextIdToJobMap[i];
if (DEBUG) {
Slog.d(TAG, "About to run job on context "
+ i + ", job: " + pendingJob);
}
for (int ic=0; ic<controllers.size(); ic++) {
controllers.get(ic).prepareForExecutionLocked(pendingJob);
}
if (!activeServices.get(i).executeRunnableJob(pendingJob)) {
Slog.d(TAG, "Error executing " + pendingJob);
}
if (pendingJobs.remove(pendingJob)) {
tracker.noteNonpending(pendingJob);
}
}
}
if (!preservePreferredUid) {
activeServices.get(i).clearPreferredUid();
}
}
}
private static int findJobContextIdFromMap(JobStatus jobStatus, JobStatus[] map) {
for (int i=0; i<map.length; i++) {
if (map[i] != null && map[i].matches(jobStatus.getUid(), jobStatus.getJobId())) {
return i;
}
}
return -1;
}
@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();
}
public void dumpLocked(IndentingPrintWriter pw, long now, long nowRealtime) {
pw.println("Concurrency:");
pw.increaseIndent();
try {
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.println("Current max jobs:");
pw.println(" ");
pw.println(mJobCountTracker);
pw.println();
pw.print("mLastMemoryTrimLevel: ");
pw.print(mLastMemoryTrimLevel);
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,
mCurrentInteractiveState);
proto.write(JobConcurrencyManagerProto.EFFECTIVE_INTERACTIVE,
mEffectiveInteractiveState);
proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_ON_MS,
nowRealtime - mLastScreenOnRealtime);
proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_OFF_MS,
nowRealtime - mLastScreenOffRealtime);
mJobCountTracker.dumpProto(proto, JobConcurrencyManagerProto.JOB_COUNT_TRACKER);
proto.write(JobConcurrencyManagerProto.MEMORY_TRIM_LEVEL,
mLastMemoryTrimLevel);
proto.end(token);
}
/**
* This class decides, taking into account {@link #mMaxJobCounts} and how mny jos are running /
* pending, how many more job can start.
*
* Extracted for testing and logging.
*/
@VisibleForTesting
static class JobCountTracker {
private int mConfigNumMaxTotalJobs;
private int mConfigNumMaxBgJobs;
private int mConfigNumMinBgJobs;
private int mNumRunningFgJobs;
private int mNumRunningBgJobs;
private int mNumPendingFgJobs;
private int mNumPendingBgJobs;
private int mNumStartingFgJobs;
private int mNumStartingBgJobs;
private int mNumReservedForBg;
private int mNumActualMaxFgJobs;
private int mNumActualMaxBgJobs;
void reset(int numTotalMaxJobs, int numMaxBgJobs, int numMinBgJobs) {
mConfigNumMaxTotalJobs = numTotalMaxJobs;
mConfigNumMaxBgJobs = numMaxBgJobs;
mConfigNumMinBgJobs = numMinBgJobs;
mNumRunningFgJobs = 0;
mNumRunningBgJobs = 0;
mNumPendingFgJobs = 0;
mNumPendingBgJobs = 0;
mNumStartingFgJobs = 0;
mNumStartingBgJobs = 0;
mNumReservedForBg = 0;
mNumActualMaxFgJobs = 0;
mNumActualMaxBgJobs = 0;
}
void incrementRunningJobCount(boolean isFg) {
if (isFg) {
mNumRunningFgJobs++;
} else {
mNumRunningBgJobs++;
}
}
void incrementPendingJobCount(boolean isFg) {
if (isFg) {
mNumPendingFgJobs++;
} else {
mNumPendingBgJobs++;
}
}
void onStartingNewJob(boolean isFg) {
if (isFg) {
mNumStartingFgJobs++;
} else {
mNumStartingBgJobs++;
}
}
void onCountDone() {
// Note some variables are used only here but are made class members in order to have
// them on logcat / dumpsys.
// How many slots should we allocate to BG jobs at least?
// That's basically "getMinBg()", but if there are less jobs, decrease it.
// (e.g. even if min-bg is 2, if there's only 1 running+pending job, this has to be 1.)
final int reservedForBg = Math.min(
mConfigNumMinBgJobs,
mNumRunningBgJobs + mNumPendingBgJobs);
// However, if there are FG jobs already running, we have to adjust it.
mNumReservedForBg = Math.min(reservedForBg,
mConfigNumMaxTotalJobs - mNumRunningFgJobs);
// Max FG is [total - [number needed for BG jobs]]
// [number needed for BG jobs] is the bigger one of [running BG] or [reserved BG]
final int maxFg =
mConfigNumMaxTotalJobs - Math.max(mNumRunningBgJobs, mNumReservedForBg);
// The above maxFg is the theoretical max. If there are less FG jobs, the actual
// max FG will be lower accordingly.
mNumActualMaxFgJobs = Math.min(
maxFg,
mNumRunningFgJobs + mNumPendingFgJobs);
// Max BG is [total - actual max FG], but cap at [config max BG].
final int maxBg = Math.min(
mConfigNumMaxBgJobs,
mConfigNumMaxTotalJobs - mNumActualMaxFgJobs);
// If there are less BG jobs than maxBg, then reduce the actual max BG accordingly.
// This isn't needed for the logic to work, but this will give consistent output
// on logcat and dumpsys.
mNumActualMaxBgJobs = Math.min(
maxBg,
mNumRunningBgJobs + mNumPendingBgJobs);
}
boolean canJobStart(boolean isFg) {
if (isFg) {
return mNumRunningFgJobs + mNumStartingFgJobs < mNumActualMaxFgJobs;
} else {
return mNumRunningBgJobs + mNumStartingBgJobs < mNumActualMaxBgJobs;
}
}
public int getNumStartingFgJobs() {
return mNumStartingFgJobs;
}
public int getNumStartingBgJobs() {
return mNumStartingBgJobs;
}
int getTotalRunningJobCountToNote() {
return mNumRunningFgJobs + mNumRunningBgJobs
+ mNumStartingFgJobs + mNumStartingBgJobs;
}
int getFgRunningJobCountToNote() {
return mNumRunningFgJobs + mNumStartingFgJobs;
}
void logStatus() {
if (DEBUG) {
Slog.d(TAG, "assignJobsToContexts: " + this);
}
}
public String toString() {
final int totalFg = mNumRunningFgJobs + mNumStartingFgJobs;
final int totalBg = mNumRunningBgJobs + mNumStartingBgJobs;
return String.format(
"Config={tot=%d bg min/max=%d/%d}"
+ " Running[FG/BG (total)]: %d / %d (%d)"
+ " Pending: %d / %d (%d)"
+ " Actual max: %d%s / %d%s (%d%s)"
+ " Res BG: %d"
+ " Starting: %d / %d (%d)"
+ " Total: %d%s / %d%s (%d%s)",
mConfigNumMaxTotalJobs,
mConfigNumMinBgJobs,
mConfigNumMaxBgJobs,
mNumRunningFgJobs, mNumRunningBgJobs,
mNumRunningFgJobs + mNumRunningBgJobs,
mNumPendingFgJobs, mNumPendingBgJobs,
mNumPendingFgJobs + mNumPendingBgJobs,
mNumActualMaxFgJobs, (totalFg <= mConfigNumMaxTotalJobs) ? "" : "*",
mNumActualMaxBgJobs, (totalBg <= mConfigNumMaxBgJobs) ? "" : "*",
mNumActualMaxFgJobs + mNumActualMaxBgJobs,
(mNumActualMaxFgJobs + mNumActualMaxBgJobs <= mConfigNumMaxTotalJobs)
? "" : "*",
mNumReservedForBg,
mNumStartingFgJobs, mNumStartingBgJobs, mNumStartingFgJobs + mNumStartingBgJobs,
totalFg, (totalFg <= mNumActualMaxFgJobs) ? "" : "*",
totalBg, (totalBg <= mNumActualMaxBgJobs) ? "" : "*",
totalFg + totalBg, (totalFg + totalBg <= mConfigNumMaxTotalJobs) ? "" : "*"
);
}
public void dumpProto(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_TOTAL_JOBS, mConfigNumMaxTotalJobs);
proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_BG_JOBS, mConfigNumMaxBgJobs);
proto.write(JobCountTrackerProto.CONFIG_NUM_MIN_BG_JOBS, mConfigNumMinBgJobs);
proto.write(JobCountTrackerProto.NUM_RUNNING_FG_JOBS, mNumRunningFgJobs);
proto.write(JobCountTrackerProto.NUM_RUNNING_BG_JOBS, mNumRunningBgJobs);
proto.write(JobCountTrackerProto.NUM_PENDING_FG_JOBS, mNumPendingFgJobs);
proto.write(JobCountTrackerProto.NUM_PENDING_BG_JOBS, mNumPendingBgJobs);
proto.end(token);
}
}
}