1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.job.controllers;
18 
19 import static android.text.format.DateUtils.DAY_IN_MILLIS;
20 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
21 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
22 
23 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
24 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
25 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
26 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONNECTIVITY;
27 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE;
28 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE;
29 
30 import android.annotation.ElapsedRealtimeLong;
31 import android.annotation.NonNull;
32 import android.annotation.Nullable;
33 import android.app.job.JobInfo;
34 import android.content.Context;
35 import android.content.pm.PackageManager;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.Message;
39 import android.os.UserHandle;
40 import android.provider.DeviceConfig;
41 import android.util.ArraySet;
42 import android.util.IndentingPrintWriter;
43 import android.util.Log;
44 import android.util.Slog;
45 import android.util.SparseArrayMap;
46 
47 import com.android.internal.annotations.GuardedBy;
48 import com.android.internal.annotations.VisibleForTesting;
49 import com.android.server.AppSchedulingModuleThread;
50 import com.android.server.job.JobSchedulerService;
51 import com.android.server.utils.AlarmQueue;
52 
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.function.Predicate;
56 
57 /**
58  * Controller that tracks the number of flexible constraints being actively satisfied.
59  * Drops constraint for TOP apps and lowers number of required constraints with time.
60  */
61 public final class FlexibilityController extends StateController {
62     private static final String TAG = "JobScheduler.Flex";
63     private static final boolean DEBUG = JobSchedulerService.DEBUG
64             || Log.isLoggable(TAG, Log.DEBUG);
65 
66     /** List of all system-wide flexible constraints whose satisfaction is independent of job. */
67     static final int SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS = CONSTRAINT_BATTERY_NOT_LOW
68             | CONSTRAINT_CHARGING
69             | CONSTRAINT_IDLE;
70 
71     /** List of flexible constraints a job can opt into. */
72     static final int OPTIONAL_FLEXIBLE_CONSTRAINTS = CONSTRAINT_BATTERY_NOT_LOW
73             | CONSTRAINT_CHARGING
74             | CONSTRAINT_IDLE;
75 
76     /** List of all job flexible constraints whose satisfaction is job specific. */
77     private static final int JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS = CONSTRAINT_CONNECTIVITY;
78 
79     /** List of all flexible constraints. */
80     private static final int FLEXIBLE_CONSTRAINTS =
81             JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS | SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS;
82 
83     private static final int NUM_JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS =
84             Integer.bitCount(JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS);
85 
86     static final int NUM_OPTIONAL_FLEXIBLE_CONSTRAINTS =
87             Integer.bitCount(OPTIONAL_FLEXIBLE_CONSTRAINTS);
88 
89     static final int NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS =
90             Integer.bitCount(SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS);
91 
92     static final int NUM_FLEXIBLE_CONSTRAINTS = Integer.bitCount(FLEXIBLE_CONSTRAINTS);
93 
94     private static final long NO_LIFECYCLE_END = Long.MAX_VALUE;
95 
96     /**
97      * The default deadline that all flexible constraints should be dropped by if a job lacks
98      * a deadline.
99      */
100     private long mFallbackFlexibilityDeadlineMs =
101             FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
102 
103     private long mRescheduledJobDeadline = FcConfig.DEFAULT_RESCHEDULED_JOB_DEADLINE_MS;
104     private long mMaxRescheduledDeadline = FcConfig.DEFAULT_MAX_RESCHEDULED_DEADLINE_MS;
105 
106     @VisibleForTesting
107     @GuardedBy("mLock")
108     boolean mFlexibilityEnabled = FcConfig.DEFAULT_FLEXIBILITY_ENABLED;
109 
110     private long mMinTimeBetweenFlexibilityAlarmsMs =
111             FcConfig.DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS;
112 
113     /** Hard cutoff to remove flexible constraints. */
114     private long mDeadlineProximityLimitMs =
115             FcConfig.DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS;
116 
117     /**
118      * The percent of a job's lifecycle to drop number of required constraints.
119      * mPercentToDropConstraints[i] denotes that at x% of a Jobs lifecycle,
120      * the controller should have i+1 constraints dropped.
121      */
122     private int[] mPercentToDropConstraints;
123 
124     @VisibleForTesting
125     boolean mDeviceSupportsFlexConstraints;
126 
127     /**
128      * Keeps track of what flexible constraints are satisfied at the moment.
129      * Is updated by the other controllers.
130      */
131     @VisibleForTesting
132     @GuardedBy("mLock")
133     int mSatisfiedFlexibleConstraints;
134 
135     @VisibleForTesting
136     @GuardedBy("mLock")
137     final FlexibilityTracker mFlexibilityTracker;
138     @VisibleForTesting
139     @GuardedBy("mLock")
140     final FlexibilityAlarmQueue mFlexibilityAlarmQueue;
141     @VisibleForTesting
142     final FcConfig mFcConfig;
143     private final FcHandler mHandler;
144     @VisibleForTesting
145     final PrefetchController mPrefetchController;
146 
147     /**
148      * Stores the beginning of prefetch jobs lifecycle per app as a maximum of
149      * the last time the app was used and the last time the launch time was updated.
150      */
151     @VisibleForTesting
152     @GuardedBy("mLock")
153     final SparseArrayMap<String, Long> mPrefetchLifeCycleStart = new SparseArrayMap<>();
154 
155     @VisibleForTesting
156     final PrefetchController.PrefetchChangedListener mPrefetchChangedListener =
157             new PrefetchController.PrefetchChangedListener() {
158                 @Override
159                 public void onPrefetchCacheUpdated(ArraySet<JobStatus> jobs, int userId,
160                         String pkgName, long prevEstimatedLaunchTime,
161                         long newEstimatedLaunchTime, long nowElapsed) {
162                     synchronized (mLock) {
163                         final long prefetchThreshold =
164                                 mPrefetchController.getLaunchTimeThresholdMs();
165                         boolean jobWasInPrefetchWindow  = prevEstimatedLaunchTime
166                                 - prefetchThreshold < nowElapsed;
167                         boolean jobIsInPrefetchWindow  = newEstimatedLaunchTime
168                                 - prefetchThreshold < nowElapsed;
169                         if (jobIsInPrefetchWindow != jobWasInPrefetchWindow) {
170                             // If the job was in the window previously then changing the start
171                             // of the lifecycle to the current moment without a large change in the
172                             // end would squeeze the window too tight fail to drop constraints.
173                             mPrefetchLifeCycleStart.add(userId, pkgName, Math.max(nowElapsed,
174                                     mPrefetchLifeCycleStart.getOrDefault(userId, pkgName, 0L)));
175                         }
176                         for (int i = 0; i < jobs.size(); i++) {
177                             JobStatus js = jobs.valueAt(i);
178                             if (!js.hasFlexibilityConstraint()) {
179                                 continue;
180                             }
181                             mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
182                             mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
183                         }
184                     }
185                 }
186             };
187 
188     private static final int MSG_UPDATE_JOBS = 0;
189 
FlexibilityController( JobSchedulerService service, PrefetchController prefetchController)190     public FlexibilityController(
191             JobSchedulerService service, PrefetchController prefetchController) {
192         super(service);
193         mHandler = new FcHandler(AppSchedulingModuleThread.get().getLooper());
194         mDeviceSupportsFlexConstraints = !mContext.getPackageManager().hasSystemFeature(
195                 PackageManager.FEATURE_AUTOMOTIVE);
196         mFlexibilityEnabled &= mDeviceSupportsFlexConstraints;
197         mFlexibilityTracker = new FlexibilityTracker(NUM_FLEXIBLE_CONSTRAINTS);
198         mFcConfig = new FcConfig();
199         mFlexibilityAlarmQueue = new FlexibilityAlarmQueue(
200                 mContext, AppSchedulingModuleThread.get().getLooper());
201         mPercentToDropConstraints =
202                 mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
203         mPrefetchController = prefetchController;
204         if (mFlexibilityEnabled) {
205             mPrefetchController.registerPrefetchChangedListener(mPrefetchChangedListener);
206         }
207     }
208 
209     /**
210      * StateController interface.
211      */
212     @Override
213     @GuardedBy("mLock")
maybeStartTrackingJobLocked(JobStatus js, JobStatus lastJob)214     public void maybeStartTrackingJobLocked(JobStatus js, JobStatus lastJob) {
215         if (js.hasFlexibilityConstraint()) {
216             final long nowElapsed = sElapsedRealtimeClock.millis();
217             if (!mDeviceSupportsFlexConstraints) {
218                 js.setFlexibilityConstraintSatisfied(nowElapsed, true);
219                 return;
220             }
221             js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
222             mFlexibilityTracker.add(js);
223             js.setTrackingController(JobStatus.TRACKING_FLEXIBILITY);
224             mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
225         }
226     }
227 
228     @Override
229     @GuardedBy("mLock")
maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob)230     public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob) {
231         if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) {
232             mFlexibilityAlarmQueue.removeAlarmForKey(js);
233             mFlexibilityTracker.remove(js);
234         }
235     }
236 
237     @Override
238     @GuardedBy("mLock")
onAppRemovedLocked(String packageName, int uid)239     public void onAppRemovedLocked(String packageName, int uid) {
240         final int userId = UserHandle.getUserId(uid);
241         mPrefetchLifeCycleStart.delete(userId, packageName);
242     }
243 
244     @Override
245     @GuardedBy("mLock")
onUserRemovedLocked(int userId)246     public void onUserRemovedLocked(int userId) {
247         mPrefetchLifeCycleStart.delete(userId);
248     }
249 
250     /** Checks if the flexibility constraint is actively satisfied for a given job. */
251     @GuardedBy("mLock")
isFlexibilitySatisfiedLocked(JobStatus js)252     boolean isFlexibilitySatisfiedLocked(JobStatus js) {
253         return !mFlexibilityEnabled
254                 || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP
255                 || getNumSatisfiedFlexibleConstraintsLocked(js)
256                         >= js.getNumRequiredFlexibleConstraints()
257                 || mService.isCurrentlyRunningLocked(js);
258     }
259 
260     @VisibleForTesting
261     @GuardedBy("mLock")
getNumSatisfiedFlexibleConstraintsLocked(JobStatus js)262     int getNumSatisfiedFlexibleConstraintsLocked(JobStatus js) {
263         return Integer.bitCount(mSatisfiedFlexibleConstraints & js.getPreferredConstraintFlags())
264                 // Connectivity is job-specific, so must be handled separately.
265                 + (js.getHasAccessToUnmetered() ? 1 : 0);
266     }
267 
268     /**
269      * Sets the controller's constraint to a given state.
270      * Changes flexibility constraint satisfaction for affected jobs.
271      */
272     @VisibleForTesting
setConstraintSatisfied(int constraint, boolean state, long nowElapsed)273     void setConstraintSatisfied(int constraint, boolean state, long nowElapsed) {
274         synchronized (mLock) {
275             final boolean old = (mSatisfiedFlexibleConstraints & constraint) != 0;
276             if (old == state) {
277                 return;
278             }
279 
280             if (DEBUG) {
281                 Slog.d(TAG, "setConstraintSatisfied: "
282                        + " constraint: " + constraint + " state: " + state);
283             }
284 
285             mSatisfiedFlexibleConstraints =
286                     (mSatisfiedFlexibleConstraints & ~constraint) | (state ? constraint : 0);
287             // Push the job update to the handler to avoid blocking other controllers and
288             // potentially batch back-to-back controller state updates together.
289             mHandler.obtainMessage(MSG_UPDATE_JOBS).sendToTarget();
290         }
291     }
292 
293     /** Checks if the given constraint is satisfied in the flexibility controller. */
294     @VisibleForTesting
isConstraintSatisfied(int constraint)295     boolean isConstraintSatisfied(int constraint) {
296         return (mSatisfiedFlexibleConstraints & constraint) != 0;
297     }
298 
299     @VisibleForTesting
300     @GuardedBy("mLock")
getLifeCycleBeginningElapsedLocked(JobStatus js)301     long getLifeCycleBeginningElapsedLocked(JobStatus js) {
302         if (js.getJob().isPrefetch()) {
303             final long earliestRuntime = Math.max(js.enqueueTime, js.getEarliestRunTime());
304             final long estimatedLaunchTime =
305                     mPrefetchController.getNextEstimatedLaunchTimeLocked(js);
306             long prefetchWindowStart = mPrefetchLifeCycleStart.getOrDefault(
307                     js.getSourceUserId(), js.getSourcePackageName(), 0L);
308             if (estimatedLaunchTime != Long.MAX_VALUE) {
309                 prefetchWindowStart = Math.max(prefetchWindowStart,
310                         estimatedLaunchTime - mPrefetchController.getLaunchTimeThresholdMs());
311             }
312             return Math.max(prefetchWindowStart, earliestRuntime);
313         }
314         return js.getEarliestRunTime() == JobStatus.NO_EARLIEST_RUNTIME
315                 ? js.enqueueTime : js.getEarliestRunTime();
316     }
317 
318     @VisibleForTesting
319     @GuardedBy("mLock")
getLifeCycleEndElapsedLocked(JobStatus js, long earliest)320     long getLifeCycleEndElapsedLocked(JobStatus js, long earliest) {
321         if (js.getJob().isPrefetch()) {
322             final long estimatedLaunchTime =
323                     mPrefetchController.getNextEstimatedLaunchTimeLocked(js);
324             // Prefetch jobs aren't supposed to have deadlines after T.
325             // But some legacy apps might still schedule them with deadlines.
326             if (js.getLatestRunTimeElapsed() != JobStatus.NO_LATEST_RUNTIME) {
327                 // If there is a deadline, the earliest time is the end of the lifecycle.
328                 return Math.min(
329                         estimatedLaunchTime - mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS,
330                         js.getLatestRunTimeElapsed());
331             }
332             if (estimatedLaunchTime != Long.MAX_VALUE) {
333                 return estimatedLaunchTime - mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS;
334             }
335             // There is no deadline and no estimated launch time.
336             return NO_LIFECYCLE_END;
337         }
338         // Increase the flex deadline for jobs rescheduled more than once.
339         if (js.getNumPreviousAttempts() > 1) {
340             return earliest + Math.min(
341                     (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2),
342                     mMaxRescheduledDeadline);
343         }
344         return js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME
345                 ? earliest + mFallbackFlexibilityDeadlineMs : js.getLatestRunTimeElapsed();
346     }
347 
348     @VisibleForTesting
349     @GuardedBy("mLock")
getCurPercentOfLifecycleLocked(JobStatus js, long nowElapsed)350     int getCurPercentOfLifecycleLocked(JobStatus js, long nowElapsed) {
351         final long earliest = getLifeCycleBeginningElapsedLocked(js);
352         final long latest = getLifeCycleEndElapsedLocked(js, earliest);
353         if (latest == NO_LIFECYCLE_END || earliest >= nowElapsed) {
354             return 0;
355         }
356         if (nowElapsed > latest || latest == earliest) {
357             return 100;
358         }
359         final int percentInTime = (int) ((nowElapsed - earliest) * 100 / (latest - earliest));
360         return percentInTime;
361     }
362 
363     @VisibleForTesting
364     @ElapsedRealtimeLong
365     @GuardedBy("mLock")
getNextConstraintDropTimeElapsedLocked(JobStatus js)366     long getNextConstraintDropTimeElapsedLocked(JobStatus js) {
367         final long earliest = getLifeCycleBeginningElapsedLocked(js);
368         final long latest = getLifeCycleEndElapsedLocked(js, earliest);
369         return getNextConstraintDropTimeElapsedLocked(js, earliest, latest);
370     }
371 
372     /** The elapsed time that marks when the next constraint should be dropped. */
373     @ElapsedRealtimeLong
374     @GuardedBy("mLock")
getNextConstraintDropTimeElapsedLocked(JobStatus js, long earliest, long latest)375     long getNextConstraintDropTimeElapsedLocked(JobStatus js, long earliest, long latest) {
376         if (latest == NO_LIFECYCLE_END
377                 || js.getNumDroppedFlexibleConstraints() == mPercentToDropConstraints.length) {
378             return NO_LIFECYCLE_END;
379         }
380         final int percent = mPercentToDropConstraints[js.getNumDroppedFlexibleConstraints()];
381         final long percentInTime = ((latest - earliest) * percent) / 100;
382         return earliest + percentInTime;
383     }
384 
385     @Override
386     @GuardedBy("mLock")
onUidBiasChangedLocked(int uid, int prevBias, int newBias)387     public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
388         if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) {
389             return;
390         }
391         final long nowElapsed = sElapsedRealtimeClock.millis();
392         ArraySet<JobStatus> jobsByUid = mService.getJobStore().getJobsBySourceUid(uid);
393         boolean hasPrefetch = false;
394         for (int i = 0; i < jobsByUid.size(); i++) {
395             JobStatus js = jobsByUid.valueAt(i);
396             if (js.hasFlexibilityConstraint()) {
397                 js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
398                 hasPrefetch |= js.getJob().isPrefetch();
399             }
400         }
401 
402         // Prefetch jobs can't run when the app is TOP, so it should not be included in their
403         // lifecycle, and marks the beginning of a new lifecycle.
404         if (hasPrefetch && prevBias == JobInfo.BIAS_TOP_APP) {
405             final int userId = UserHandle.getUserId(uid);
406             final ArraySet<String> pkgs = mService.getPackagesForUidLocked(uid);
407             if (pkgs == null) {
408                 return;
409             }
410             for (int i = 0; i < pkgs.size(); i++) {
411                 String pkg = pkgs.valueAt(i);
412                 mPrefetchLifeCycleStart.add(userId, pkg,
413                         Math.max(mPrefetchLifeCycleStart.getOrDefault(userId, pkg, 0L),
414                                 nowElapsed));
415             }
416         }
417     }
418 
419     @Override
420     @GuardedBy("mLock")
onConstantsUpdatedLocked()421     public void onConstantsUpdatedLocked() {
422         if (mFcConfig.mShouldReevaluateConstraints) {
423             AppSchedulingModuleThread.getHandler().post(() -> {
424                 final ArraySet<JobStatus> changedJobs = new ArraySet<>();
425                 synchronized (mLock) {
426                     final long nowElapsed = sElapsedRealtimeClock.millis();
427                     for (int j = 0; j < mFlexibilityTracker.size(); j++) {
428                         final ArraySet<JobStatus> jobs = mFlexibilityTracker
429                                 .getJobsByNumRequiredConstraints(j);
430                         for (int i = 0; i < jobs.size(); i++) {
431                             JobStatus js = jobs.valueAt(i);
432                             mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
433                             mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
434                             if (js.setFlexibilityConstraintSatisfied(
435                                     nowElapsed, isFlexibilitySatisfiedLocked(js))) {
436                                 changedJobs.add(js);
437                             }
438                         }
439                     }
440                 }
441                 if (changedJobs.size() > 0) {
442                     mStateChangedListener.onControllerStateChanged(changedJobs);
443                 }
444             });
445         }
446     }
447 
448     @Override
449     @GuardedBy("mLock")
prepareForUpdatedConstantsLocked()450     public void prepareForUpdatedConstantsLocked() {
451         mFcConfig.mShouldReevaluateConstraints = false;
452     }
453 
454     @Override
455     @GuardedBy("mLock")
processConstantLocked(DeviceConfig.Properties properties, String key)456     public void processConstantLocked(DeviceConfig.Properties properties, String key) {
457         mFcConfig.processConstantLocked(properties, key);
458     }
459 
460     @VisibleForTesting
461     class FlexibilityTracker {
462         final ArrayList<ArraySet<JobStatus>> mTrackedJobs;
463 
FlexibilityTracker(int numFlexibleConstraints)464         FlexibilityTracker(int numFlexibleConstraints) {
465             mTrackedJobs = new ArrayList<>();
466             for (int i = 0; i <= numFlexibleConstraints; i++) {
467                 mTrackedJobs.add(new ArraySet<JobStatus>());
468             }
469         }
470 
471         /** Gets every tracked job with a given number of required constraints. */
472         @Nullable
getJobsByNumRequiredConstraints(int numRequired)473         public ArraySet<JobStatus> getJobsByNumRequiredConstraints(int numRequired) {
474             if (numRequired > mTrackedJobs.size()) {
475                 Slog.wtfStack(TAG, "Asked for a larger number of constraints than exists.");
476                 return null;
477             }
478             return mTrackedJobs.get(numRequired);
479         }
480 
481         /** adds a JobStatus object based on number of required flexible constraints. */
add(JobStatus js)482         public void add(JobStatus js) {
483             if (js.getNumRequiredFlexibleConstraints() < 0) {
484                 return;
485             }
486             mTrackedJobs.get(js.getNumRequiredFlexibleConstraints()).add(js);
487         }
488 
489         /** Removes a JobStatus object. */
remove(JobStatus js)490         public void remove(JobStatus js) {
491             mTrackedJobs.get(js.getNumRequiredFlexibleConstraints()).remove(js);
492         }
493 
resetJobNumDroppedConstraints(JobStatus js, long nowElapsed)494         public void resetJobNumDroppedConstraints(JobStatus js, long nowElapsed) {
495             final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed);
496             int toDrop = 0;
497             final int jsMaxFlexibleConstraints = NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS
498                     + (js.getPreferUnmetered() ? 1 : 0);
499             for (int i = 0; i < jsMaxFlexibleConstraints; i++) {
500                 if (curPercent >= mPercentToDropConstraints[i]) {
501                     toDrop++;
502                 }
503             }
504             adjustJobsRequiredConstraints(
505                     js, js.getNumDroppedFlexibleConstraints() - toDrop, nowElapsed);
506         }
507 
508         /** Returns all tracked jobs. */
getArrayList()509         public ArrayList<ArraySet<JobStatus>> getArrayList() {
510             return mTrackedJobs;
511         }
512 
513         /**
514          * Adjusts number of required flexible constraints and sorts it into the tracker.
515          * Returns false if the job status's number of flexible constraints is now 0.
516          */
adjustJobsRequiredConstraints(JobStatus js, int adjustBy, long nowElapsed)517         public boolean adjustJobsRequiredConstraints(JobStatus js, int adjustBy, long nowElapsed) {
518             if (adjustBy != 0) {
519                 remove(js);
520                 js.adjustNumRequiredFlexibleConstraints(adjustBy);
521                 js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
522                 add(js);
523             }
524             return js.getNumRequiredFlexibleConstraints() > 0;
525         }
526 
size()527         public int size() {
528             return mTrackedJobs.size();
529         }
530 
dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate)531         public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
532             for (int i = 0; i < mTrackedJobs.size(); i++) {
533                 ArraySet<JobStatus> jobs = mTrackedJobs.get(i);
534                 for (int j = 0; j < jobs.size(); j++) {
535                     final JobStatus js = jobs.valueAt(j);
536                     if (!predicate.test(js)) {
537                         continue;
538                     }
539                     pw.print("#");
540                     js.printUniqueId(pw);
541                     pw.print(" from ");
542                     UserHandle.formatUid(pw, js.getSourceUid());
543                     pw.print(" Num Required Constraints: ");
544                     pw.print(js.getNumRequiredFlexibleConstraints());
545                     pw.println();
546                 }
547             }
548         }
549     }
550 
551     @VisibleForTesting
552     class FlexibilityAlarmQueue extends AlarmQueue<JobStatus> {
FlexibilityAlarmQueue(Context context, Looper looper)553         private FlexibilityAlarmQueue(Context context, Looper looper) {
554             super(context, looper, "*job.flexibility_check*",
555                     "Flexible Constraint Check", true,
556                     mMinTimeBetweenFlexibilityAlarmsMs);
557         }
558 
559         @Override
isForUser(@onNull JobStatus js, int userId)560         protected boolean isForUser(@NonNull JobStatus js, int userId) {
561             return js.getSourceUserId() == userId;
562         }
563 
scheduleDropNumConstraintsAlarm(JobStatus js, long nowElapsed)564         public void scheduleDropNumConstraintsAlarm(JobStatus js, long nowElapsed) {
565             synchronized (mLock) {
566                 final long earliest = getLifeCycleBeginningElapsedLocked(js);
567                 final long latest = getLifeCycleEndElapsedLocked(js, earliest);
568                 final long nextTimeElapsed =
569                         getNextConstraintDropTimeElapsedLocked(js, earliest, latest);
570 
571                 if (DEBUG) {
572                     Slog.d(TAG, "scheduleDropNumConstraintsAlarm: "
573                             + js.getSourcePackageName() + " " + js.getSourceUserId()
574                             + " numRequired: " + js.getNumRequiredFlexibleConstraints()
575                             + " numSatisfied: " + Integer.bitCount(mSatisfiedFlexibleConstraints)
576                             + " curTime: " + nowElapsed
577                             + " earliest: " + earliest
578                             + " latest: " + latest
579                             + " nextTime: " + nextTimeElapsed);
580                 }
581                 if (latest - nowElapsed < mDeadlineProximityLimitMs) {
582                     if (DEBUG) {
583                         Slog.d(TAG, "deadline proximity met: " + js);
584                     }
585                     mFlexibilityTracker.adjustJobsRequiredConstraints(js,
586                             -js.getNumRequiredFlexibleConstraints(), nowElapsed);
587                     return;
588                 }
589                 if (nextTimeElapsed == NO_LIFECYCLE_END) {
590                     // There is no known or estimated next time to drop a constraint.
591                     removeAlarmForKey(js);
592                     return;
593                 }
594                 if (latest - nextTimeElapsed <= mDeadlineProximityLimitMs) {
595                     if (DEBUG) {
596                         Slog.d(TAG, "last alarm set: " + js);
597                     }
598                     addAlarm(js, latest - mDeadlineProximityLimitMs);
599                     return;
600                 }
601                 addAlarm(js, nextTimeElapsed);
602             }
603         }
604 
605         @Override
processExpiredAlarms(@onNull ArraySet<JobStatus> expired)606         protected void processExpiredAlarms(@NonNull ArraySet<JobStatus> expired) {
607             synchronized (mLock) {
608                 ArraySet<JobStatus> changedJobs = new ArraySet<>();
609                 final long nowElapsed = sElapsedRealtimeClock.millis();
610                 for (int i = 0; i < expired.size(); i++) {
611                     JobStatus js = expired.valueAt(i);
612                     boolean wasFlexibilitySatisfied = js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE);
613 
614                     if (mFlexibilityTracker.adjustJobsRequiredConstraints(js, -1, nowElapsed)) {
615                         scheduleDropNumConstraintsAlarm(js, nowElapsed);
616                     }
617                     if (wasFlexibilitySatisfied != js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE)) {
618                         changedJobs.add(js);
619                     }
620                 }
621                 mStateChangedListener.onControllerStateChanged(changedJobs);
622             }
623         }
624     }
625 
626     private class FcHandler extends Handler {
FcHandler(Looper looper)627         FcHandler(Looper looper) {
628             super(looper);
629         }
630 
631         @Override
handleMessage(Message msg)632         public void handleMessage(Message msg) {
633             switch (msg.what) {
634                 case MSG_UPDATE_JOBS:
635                     removeMessages(MSG_UPDATE_JOBS);
636 
637                     synchronized (mLock) {
638                         final long nowElapsed = sElapsedRealtimeClock.millis();
639                         final ArraySet<JobStatus> changedJobs = new ArraySet<>();
640 
641                         for (int o = 0; o <= NUM_OPTIONAL_FLEXIBLE_CONSTRAINTS; ++o) {
642                             final ArraySet<JobStatus> jobsByNumConstraints = mFlexibilityTracker
643                                     .getJobsByNumRequiredConstraints(o);
644 
645                             if (jobsByNumConstraints != null) {
646                                 for (int i = 0; i < jobsByNumConstraints.size(); i++) {
647                                     final JobStatus js = jobsByNumConstraints.valueAt(i);
648                                     if (js.setFlexibilityConstraintSatisfied(
649                                             nowElapsed, isFlexibilitySatisfiedLocked(js))) {
650                                         changedJobs.add(js);
651                                     }
652                                 }
653                             }
654                         }
655                         if (changedJobs.size() > 0) {
656                             mStateChangedListener.onControllerStateChanged(changedJobs);
657                         }
658                     }
659                     break;
660             }
661         }
662     }
663 
664     @VisibleForTesting
665     class FcConfig {
666         private boolean mShouldReevaluateConstraints = false;
667 
668         /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */
669         private static final String FC_CONFIG_PREFIX = "fc_";
670 
671         static final String KEY_FLEXIBILITY_ENABLED = FC_CONFIG_PREFIX + "enable_flexibility";
672         static final String KEY_DEADLINE_PROXIMITY_LIMIT =
673                 FC_CONFIG_PREFIX + "flexibility_deadline_proximity_limit_ms";
674         static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE =
675                 FC_CONFIG_PREFIX + "fallback_flexibility_deadline_ms";
676         static final String KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS =
677                 FC_CONFIG_PREFIX + "min_time_between_flexibility_alarms_ms";
678         static final String KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS =
679                 FC_CONFIG_PREFIX + "percents_to_drop_num_flexible_constraints";
680         static final String KEY_MAX_RESCHEDULED_DEADLINE_MS =
681                 FC_CONFIG_PREFIX + "max_rescheduled_deadline_ms";
682         static final String KEY_RESCHEDULED_JOB_DEADLINE_MS =
683                 FC_CONFIG_PREFIX + "rescheduled_job_deadline_ms";
684 
685         private static final boolean DEFAULT_FLEXIBILITY_ENABLED = false;
686         @VisibleForTesting
687         static final long DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS;
688         @VisibleForTesting
689         static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 72 * HOUR_IN_MILLIS;
690         private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS;
691         @VisibleForTesting
692         final int[] DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS = {50, 60, 70, 80};
693         private static final long DEFAULT_RESCHEDULED_JOB_DEADLINE_MS = HOUR_IN_MILLIS;
694         private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = 5 * DAY_IN_MILLIS;
695 
696         /**
697          * If false the controller will not track new jobs
698          * and the flexibility constraint will always be satisfied.
699          */
700         public boolean FLEXIBILITY_ENABLED = DEFAULT_FLEXIBILITY_ENABLED;
701         /** How close to a jobs' deadline all flexible constraints will be dropped. */
702         public long DEADLINE_PROXIMITY_LIMIT_MS = DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS;
703         /** For jobs that lack a deadline, the time that will be used to drop all constraints by. */
704         public long FALLBACK_FLEXIBILITY_DEADLINE_MS = DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
705         public long MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS =
706                 DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS;
707         /** The percentages of a jobs' lifecycle to drop the number of required constraints. */
708         public int[] PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS =
709                 DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
710         /** Initial fallback flexible deadline for rescheduled jobs. */
711         public long RESCHEDULED_JOB_DEADLINE_MS = DEFAULT_RESCHEDULED_JOB_DEADLINE_MS;
712         /** The max deadline for rescheduled jobs. */
713         public long MAX_RESCHEDULED_DEADLINE_MS = DEFAULT_MAX_RESCHEDULED_DEADLINE_MS;
714 
715         @GuardedBy("mLock")
processConstantLocked(@onNull DeviceConfig.Properties properties, @NonNull String key)716         public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
717                 @NonNull String key) {
718             switch (key) {
719                 case KEY_FLEXIBILITY_ENABLED:
720                     FLEXIBILITY_ENABLED = properties.getBoolean(key, DEFAULT_FLEXIBILITY_ENABLED)
721                             && mDeviceSupportsFlexConstraints;
722                     if (mFlexibilityEnabled != FLEXIBILITY_ENABLED) {
723                         mFlexibilityEnabled = FLEXIBILITY_ENABLED;
724                         mShouldReevaluateConstraints = true;
725                         if (mFlexibilityEnabled) {
726                             mPrefetchController
727                                     .registerPrefetchChangedListener(mPrefetchChangedListener);
728                         } else {
729                             mPrefetchController
730                                     .unRegisterPrefetchChangedListener(mPrefetchChangedListener);
731                         }
732                     }
733                     break;
734                 case KEY_RESCHEDULED_JOB_DEADLINE_MS:
735                     RESCHEDULED_JOB_DEADLINE_MS =
736                             properties.getLong(key, DEFAULT_RESCHEDULED_JOB_DEADLINE_MS);
737                     if (mRescheduledJobDeadline != RESCHEDULED_JOB_DEADLINE_MS) {
738                         mRescheduledJobDeadline = RESCHEDULED_JOB_DEADLINE_MS;
739                         mShouldReevaluateConstraints = true;
740                     }
741                     break;
742                 case KEY_MAX_RESCHEDULED_DEADLINE_MS:
743                     MAX_RESCHEDULED_DEADLINE_MS =
744                             properties.getLong(key, DEFAULT_MAX_RESCHEDULED_DEADLINE_MS);
745                     if (mMaxRescheduledDeadline != MAX_RESCHEDULED_DEADLINE_MS) {
746                         mMaxRescheduledDeadline = MAX_RESCHEDULED_DEADLINE_MS;
747                         mShouldReevaluateConstraints = true;
748                     }
749                     break;
750                 case KEY_DEADLINE_PROXIMITY_LIMIT:
751                     DEADLINE_PROXIMITY_LIMIT_MS =
752                             properties.getLong(key, DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS);
753                     if (mDeadlineProximityLimitMs != DEADLINE_PROXIMITY_LIMIT_MS) {
754                         mDeadlineProximityLimitMs = DEADLINE_PROXIMITY_LIMIT_MS;
755                         mShouldReevaluateConstraints = true;
756                     }
757                     break;
758                 case KEY_FALLBACK_FLEXIBILITY_DEADLINE:
759                     FALLBACK_FLEXIBILITY_DEADLINE_MS =
760                             properties.getLong(key, DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS);
761                     if (mFallbackFlexibilityDeadlineMs != FALLBACK_FLEXIBILITY_DEADLINE_MS) {
762                         mFallbackFlexibilityDeadlineMs = FALLBACK_FLEXIBILITY_DEADLINE_MS;
763                         mShouldReevaluateConstraints = true;
764                     }
765                     break;
766                 case KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS:
767                     MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS =
768                             properties.getLong(key, DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS);
769                     if (mMinTimeBetweenFlexibilityAlarmsMs
770                             != MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS) {
771                         mMinTimeBetweenFlexibilityAlarmsMs = MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS;
772                         mFlexibilityAlarmQueue
773                                 .setMinTimeBetweenAlarmsMs(MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS);
774                         mShouldReevaluateConstraints = true;
775                     }
776                     break;
777                 case KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS:
778                     String dropPercentString = properties.getString(key, "");
779                     PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS =
780                             parsePercentToDropString(dropPercentString);
781                     if (PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS != null
782                             && !Arrays.equals(mPercentToDropConstraints,
783                             PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS)) {
784                         mPercentToDropConstraints = PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS;
785                         mShouldReevaluateConstraints = true;
786                     }
787                     break;
788             }
789         }
790 
parsePercentToDropString(String s)791         private int[] parsePercentToDropString(String s) {
792             String[] dropPercentString = s.split(",");
793             int[] dropPercentInt = new int[NUM_FLEXIBLE_CONSTRAINTS];
794             if (dropPercentInt.length != dropPercentString.length) {
795                 return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
796             }
797             int prevPercent = 0;
798             for (int i = 0; i < dropPercentString.length; i++) {
799                 try {
800                     dropPercentInt[i] =
801                             Integer.parseInt(dropPercentString[i]);
802                 } catch (NumberFormatException ex) {
803                     Slog.e(TAG, "Provided string was improperly formatted.", ex);
804                     return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
805                 }
806                 if (dropPercentInt[i] < prevPercent) {
807                     Slog.wtf(TAG, "Percents to drop constraints were not in increasing order.");
808                     return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
809                 }
810                 prevPercent = dropPercentInt[i];
811             }
812 
813             return dropPercentInt;
814         }
815 
dump(IndentingPrintWriter pw)816         private void dump(IndentingPrintWriter pw) {
817             pw.println();
818             pw.print(FlexibilityController.class.getSimpleName());
819             pw.println(":");
820             pw.increaseIndent();
821 
822             pw.print(KEY_FLEXIBILITY_ENABLED, FLEXIBILITY_ENABLED).println();
823             pw.print(KEY_DEADLINE_PROXIMITY_LIMIT, DEADLINE_PROXIMITY_LIMIT_MS).println();
824             pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE, FALLBACK_FLEXIBILITY_DEADLINE_MS).println();
825             pw.print(KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS,
826                     MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS).println();
827             pw.print(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS,
828                     PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS).println();
829             pw.print(KEY_RESCHEDULED_JOB_DEADLINE_MS, RESCHEDULED_JOB_DEADLINE_MS).println();
830             pw.print(KEY_MAX_RESCHEDULED_DEADLINE_MS, MAX_RESCHEDULED_DEADLINE_MS).println();
831 
832             pw.decreaseIndent();
833         }
834     }
835 
836     @VisibleForTesting
837     @NonNull
getFcConfig()838     FcConfig getFcConfig() {
839         return mFcConfig;
840     }
841 
842     @Override
843     @GuardedBy("mLock")
dumpConstants(IndentingPrintWriter pw)844     public void dumpConstants(IndentingPrintWriter pw) {
845         mFcConfig.dump(pw);
846     }
847 
848     @Override
849     @GuardedBy("mLock")
dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)850     public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
851         pw.println("# Constraints Satisfied: " + Integer.bitCount(mSatisfiedFlexibleConstraints));
852         pw.print("Satisfied Flexible Constraints: ");
853         JobStatus.dumpConstraints(pw, mSatisfiedFlexibleConstraints);
854         pw.println();
855         pw.println();
856 
857         mFlexibilityTracker.dump(pw, predicate);
858         pw.println();
859         mFlexibilityAlarmQueue.dump(pw);
860     }
861 }
862