From 325768c9b2e936d3018240a1209bb8fb01b06d98 Mon Sep 17 00:00:00 2001 From: Christopher Tate Date: Wed, 7 Mar 2018 16:07:56 -0800 Subject: [PATCH] Track last job execution in heartbeat time, not strictly real time We need to be able to handle instrumented / externally driven job scheduling time, so we need to decouple that from "real" time. One other effect is getting a cross-call to the usage stats module out of the hot path of job runnability evaluation. Bug: 73664387 Bug: 70297229 Test: atest CtsJobSchedulerTestCases Change-Id: I0dce8af6e7fc50ce736b13572482b2db33e42b02 --- .../server/job/JobSchedulerInternal.java | 5 ++ .../server/job/JobSchedulerService.java | 80 ++++++++++++++++--- .../android/server/job/JobServiceContext.java | 8 +- 3 files changed, 82 insertions(+), 11 deletions(-) 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 0e7e5401caafd..b56dd7ea85f84 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -109,6 +109,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; @@ -239,6 +240,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(); @@ -533,11 +555,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); @@ -1421,15 +1443,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; } @@ -1438,12 +1485,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. */ @@ -2194,6 +2250,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;