diff --git a/services/core/java/com/android/server/job/JobSchedulerInternal.java b/services/core/java/com/android/server/job/JobSchedulerInternal.java index 08607bc8ef2c6..425ec473bc8e1 100644 --- a/services/core/java/com/android/server/job/JobSchedulerInternal.java +++ b/services/core/java/com/android/server/job/JobSchedulerInternal.java @@ -47,6 +47,11 @@ public interface JobSchedulerInternal { */ public long baseHeartbeatForApp(String packageName, @UserIdInt int userId, int appBucket); + /** + * Tell the scheduler when a JobServiceContext starts running a job in an app + */ + void noteJobStart(String packageName, int userId); + /** * Returns a list of pending jobs scheduled by the system service. */ diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index 790d39690947b..0b1f9a65868c4 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -108,6 +108,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.function.Consumer; @@ -238,6 +239,27 @@ public final class JobSchedulerService extends com.android.server.SystemService long mHeartbeat = 0; long mLastHeartbeatTime = sElapsedRealtimeClock.millis(); + /** + * Named indices into the STANDBY_BEATS array, for clarity in referring to + * specific buckets' bookkeeping. + */ + static final int ACTIVE_INDEX = 0; + static final int WORKING_INDEX = 1; + static final int FREQUENT_INDEX = 2; + static final int RARE_INDEX = 3; + + /** + * Bookkeeping about when jobs last run. We keep our own record in heartbeat time, + * rather than rely on Usage Stats' timestamps, because heartbeat time can be + * manipulated for testing purposes and we need job runnability to track that rather + * than real time. + * + * Outer SparseArray slices by user handle; inner map of package name to heartbeat + * is a HashMap<> rather than ArrayMap<> because we expect O(hundreds) of keys + * and it will be accessed in a known-hot code path. + */ + final SparseArray> mLastJobHeartbeats = new SparseArray<>(); + static final String HEARTBEAT_TAG = "*job.heartbeat*"; final HeartbeatAlarmListener mHeartbeatAlarm = new HeartbeatAlarmListener(); @@ -532,11 +554,11 @@ public final class JobSchedulerService extends com.android.server.SystemService DEFAULT_MIN_EXP_BACKOFF_TIME); STANDBY_HEARTBEAT_TIME = mParser.getDurationMillis(KEY_STANDBY_HEARTBEAT_TIME, DEFAULT_STANDBY_HEARTBEAT_TIME); - STANDBY_BEATS[1] = mParser.getInt(KEY_STANDBY_WORKING_BEATS, + STANDBY_BEATS[WORKING_INDEX] = mParser.getInt(KEY_STANDBY_WORKING_BEATS, DEFAULT_STANDBY_WORKING_BEATS); - STANDBY_BEATS[2] = mParser.getInt(KEY_STANDBY_FREQUENT_BEATS, + STANDBY_BEATS[FREQUENT_INDEX] = mParser.getInt(KEY_STANDBY_FREQUENT_BEATS, DEFAULT_STANDBY_FREQUENT_BEATS); - STANDBY_BEATS[3] = mParser.getInt(KEY_STANDBY_RARE_BEATS, + STANDBY_BEATS[RARE_INDEX] = mParser.getInt(KEY_STANDBY_RARE_BEATS, DEFAULT_STANDBY_RARE_BEATS); CONN_CONGESTION_DELAY_FRAC = mParser.getFloat(KEY_CONN_CONGESTION_DELAY_FRAC, DEFAULT_CONN_CONGESTION_DELAY_FRAC); @@ -1420,15 +1442,40 @@ public final class JobSchedulerService extends com.android.server.SystemService periodicToReschedule.getLastFailedRunTime()); } + /* + * We default to "long enough ago that every bucket's jobs are immediately runnable" to + * avoid starvation of apps in uncommon-use buckets that might arise from repeated + * reboot behavior. + */ long heartbeatWhenJobsLastRun(String packageName, final @UserIdInt int userId) { - final long heartbeat; - final long timeSinceLastJob = mUsageStats.getTimeSinceLastJobRun(packageName, userId); + // The furthest back in pre-boot time that we need to bother with + long heartbeat = -mConstants.STANDBY_BEATS[RARE_INDEX]; + boolean cacheHit = false; synchronized (mLock) { - heartbeat = mHeartbeat - (timeSinceLastJob / mConstants.STANDBY_HEARTBEAT_TIME); + HashMap jobPackages = mLastJobHeartbeats.get(userId); + if (jobPackages != null) { + long cachedValue = jobPackages.getOrDefault(packageName, Long.MAX_VALUE); + if (cachedValue < Long.MAX_VALUE) { + cacheHit = true; + heartbeat = cachedValue; + } + } + if (!cacheHit) { + // We haven't seen it yet; ask usage stats about it + final long timeSinceJob = mUsageStats.getTimeSinceLastJobRun(packageName, userId); + if (timeSinceJob < Long.MAX_VALUE) { + // Usage stats knows about it from before, so calculate back from that + // and go from there. + heartbeat = mHeartbeat - (timeSinceJob / mConstants.STANDBY_HEARTBEAT_TIME); + } + // If usage stats returned its "not found" MAX_VALUE, we still have the + // negative default 'heartbeat' value we established above + setLastJobHeartbeatLocked(packageName, userId, heartbeat); + } } if (DEBUG_STANDBY) { - Slog.v(TAG, "Last job heartbeat " + heartbeat + " for " + packageName + "/" + userId - + " delta=" + timeSinceLastJob); + Slog.v(TAG, "Last job heartbeat " + heartbeat + " for " + + packageName + "/" + userId); } return heartbeat; } @@ -1437,12 +1484,21 @@ public final class JobSchedulerService extends com.android.server.SystemService return heartbeatWhenJobsLastRun(job.getSourcePackageName(), job.getSourceUserId()); } + void setLastJobHeartbeatLocked(String packageName, int userId, long heartbeat) { + HashMap jobPackages = mLastJobHeartbeats.get(userId); + if (jobPackages == null) { + jobPackages = new HashMap<>(); + mLastJobHeartbeats.put(userId, jobPackages); + } + jobPackages.put(packageName, heartbeat); + } + // JobCompletedListener implementations. /** * A job just finished executing. We fetch the * {@link com.android.server.job.controllers.JobStatus} from the store and depending on - * whether we want to reschedule we readd it to the controllers. + * whether we want to reschedule we re-add it to the controllers. * @param jobStatus Completed job. * @param needsReschedule Whether the implementing class should reschedule this job. */ @@ -2208,6 +2264,12 @@ public final class JobSchedulerService extends com.android.server.SystemService return baseHeartbeat; } + public void noteJobStart(String packageName, int userId) { + synchronized (mLock) { + setLastJobHeartbeatLocked(packageName, userId, mHeartbeat); + } + } + /** * Returns a list of all pending jobs. A running job is not considered pending. Periodic * jobs are always considered pending. diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java index 1f8cf769ab989..0bb6854767f36 100644 --- a/services/core/java/com/android/server/job/JobServiceContext.java +++ b/services/core/java/com/android/server/job/JobServiceContext.java @@ -268,10 +268,14 @@ public final class JobServiceContext implements ServiceConnection { } catch (RemoteException e) { // Whatever. } + final String jobPackage = job.getSourcePackageName(); + final int jobUserId = job.getSourceUserId(); UsageStatsManagerInternal usageStats = LocalServices.getService(UsageStatsManagerInternal.class); - usageStats.setLastJobRunTime(job.getSourcePackageName(), job.getSourceUserId(), - mExecutionStartTimeElapsed); + usageStats.setLastJobRunTime(jobPackage, jobUserId, mExecutionStartTimeElapsed); + JobSchedulerInternal jobScheduler = + LocalServices.getService(JobSchedulerInternal.class); + jobScheduler.noteJobStart(jobPackage, jobUserId); mAvailable = false; mStoppedReason = null; mStoppedTime = 0;