Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 325768c9 authored by Christopher Tate's avatar Christopher Tate
Browse files

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
parent 655d98bd
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -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.
     */
+71 −9
Original line number Diff line number Diff line
@@ -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<HashMap<String, Long>> 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<String, Long> 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<String, Long> 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.
+6 −2
Original line number Diff line number Diff line
@@ -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;