1 /* 2 * Copyright (C) 2021 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.HOUR_IN_MILLIS; 20 import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 21 22 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; 23 import static com.android.server.job.JobSchedulerService.sSystemClock; 24 import static com.android.server.job.controllers.Package.packageToString; 25 26 import android.annotation.CurrentTimeMillisLong; 27 import android.annotation.ElapsedRealtimeLong; 28 import android.annotation.NonNull; 29 import android.app.job.JobInfo; 30 import android.app.usage.UsageStatsManagerInternal; 31 import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener; 32 import android.appwidget.AppWidgetManager; 33 import android.content.Context; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.Message; 37 import android.os.UserHandle; 38 import android.provider.DeviceConfig; 39 import android.util.ArraySet; 40 import android.util.IndentingPrintWriter; 41 import android.util.Log; 42 import android.util.Slog; 43 import android.util.SparseArrayMap; 44 import android.util.TimeUtils; 45 46 import com.android.internal.annotations.GuardedBy; 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.internal.os.SomeArgs; 49 import com.android.server.JobSchedulerBackgroundThread; 50 import com.android.server.LocalServices; 51 import com.android.server.job.JobSchedulerService; 52 import com.android.server.utils.AlarmQueue; 53 54 import java.util.function.Predicate; 55 56 /** 57 * Controller to delay prefetch jobs until we get close to an expected app launch. 58 */ 59 public class PrefetchController extends StateController { 60 private static final String TAG = "JobScheduler.Prefetch"; 61 private static final boolean DEBUG = JobSchedulerService.DEBUG 62 || Log.isLoggable(TAG, Log.DEBUG); 63 64 private final PcConstants mPcConstants; 65 private final PcHandler mHandler; 66 67 // Note: when determining prefetch bit satisfaction, we mark the bit as satisfied for apps with 68 // active widgets assuming that any prefetch jobs are being used for the widget. However, we 69 // don't have a callback telling us when widget status changes, which is incongruent with the 70 // aforementioned assumption. This inconsistency _should_ be fine since any jobs scheduled 71 // before the widget is activated are definitely not for the widget and don't have to be updated 72 // to "satisfied=true". 73 private AppWidgetManager mAppWidgetManager; 74 private final UsageStatsManagerInternal mUsageStatsManagerInternal; 75 76 @GuardedBy("mLock") 77 private final SparseArrayMap<String, ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>(); 78 /** 79 * Cached set of the estimated next launch times of each app. Time are in the current time 80 * millis ({@link CurrentTimeMillisLong}) timebase. 81 */ 82 @GuardedBy("mLock") 83 private final SparseArrayMap<String, Long> mEstimatedLaunchTimes = new SparseArrayMap<>(); 84 private final ThresholdAlarmListener mThresholdAlarmListener; 85 86 /** 87 * The cutoff point to decide if a prefetch job is worth running or not. If the app is expected 88 * to launch within this amount of time into the future, then we will let a prefetch job run. 89 */ 90 @GuardedBy("mLock") 91 @CurrentTimeMillisLong 92 private long mLaunchTimeThresholdMs = PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS; 93 94 /** 95 * The additional time we'll add to a launch time estimate before considering it obsolete and 96 * try to get a new estimate. This will help make prefetch jobs more viable in case an estimate 97 * is a few minutes early. 98 */ 99 @GuardedBy("mLock") 100 private long mLaunchTimeAllowanceMs = PcConstants.DEFAULT_LAUNCH_TIME_ALLOWANCE_MS; 101 102 @SuppressWarnings("FieldCanBeLocal") 103 private final EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener = 104 new EstimatedLaunchTimeChangedListener() { 105 @Override 106 public void onEstimatedLaunchTimeChanged(int userId, @NonNull String packageName, 107 @CurrentTimeMillisLong long newEstimatedLaunchTime) { 108 final SomeArgs args = SomeArgs.obtain(); 109 args.arg1 = packageName; 110 args.argi1 = userId; 111 args.argl1 = newEstimatedLaunchTime; 112 mHandler.obtainMessage(MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME, args) 113 .sendToTarget(); 114 } 115 }; 116 117 private static final int MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME = 0; 118 private static final int MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME = 1; 119 private static final int MSG_PROCESS_TOP_STATE_CHANGE = 2; 120 PrefetchController(JobSchedulerService service)121 public PrefetchController(JobSchedulerService service) { 122 super(service); 123 mPcConstants = new PcConstants(); 124 mHandler = new PcHandler(mContext.getMainLooper()); 125 mThresholdAlarmListener = new ThresholdAlarmListener( 126 mContext, JobSchedulerBackgroundThread.get().getLooper()); 127 mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class); 128 129 mUsageStatsManagerInternal 130 .registerLaunchTimeChangedListener(mEstimatedLaunchTimeChangedListener); 131 } 132 133 @Override onSystemServicesReady()134 public void onSystemServicesReady() { 135 mAppWidgetManager = mContext.getSystemService(AppWidgetManager.class); 136 } 137 138 @Override 139 @GuardedBy("mLock") maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob)140 public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { 141 if (jobStatus.getJob().isPrefetch()) { 142 final int userId = jobStatus.getSourceUserId(); 143 final String pkgName = jobStatus.getSourcePackageName(); 144 ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); 145 if (jobs == null) { 146 jobs = new ArraySet<>(); 147 mTrackedJobs.add(userId, pkgName, jobs); 148 } 149 final long now = sSystemClock.millis(); 150 final long nowElapsed = sElapsedRealtimeClock.millis(); 151 if (jobs.add(jobStatus) && jobs.size() == 1 152 && !willBeLaunchedSoonLocked(userId, pkgName, now)) { 153 updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed); 154 } 155 updateConstraintLocked(jobStatus, now, nowElapsed); 156 } 157 } 158 159 @Override 160 @GuardedBy("mLock") maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate)161 public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, 162 boolean forUpdate) { 163 final int userId = jobStatus.getSourceUserId(); 164 final String pkgName = jobStatus.getSourcePackageName(); 165 final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); 166 if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) { 167 mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName)); 168 } 169 } 170 171 @Override 172 @GuardedBy("mLock") onAppRemovedLocked(String packageName, int uid)173 public void onAppRemovedLocked(String packageName, int uid) { 174 if (packageName == null) { 175 Slog.wtf(TAG, "Told app removed but given null package name."); 176 return; 177 } 178 final int userId = UserHandle.getUserId(uid); 179 mTrackedJobs.delete(userId, packageName); 180 mEstimatedLaunchTimes.delete(userId, packageName); 181 mThresholdAlarmListener.removeAlarmForKey(new Package(userId, packageName)); 182 } 183 184 @Override 185 @GuardedBy("mLock") onUserRemovedLocked(int userId)186 public void onUserRemovedLocked(int userId) { 187 mTrackedJobs.delete(userId); 188 mEstimatedLaunchTimes.delete(userId); 189 mThresholdAlarmListener.removeAlarmsForUserId(userId); 190 } 191 192 @GuardedBy("mLock") 193 @Override onUidBiasChangedLocked(int uid, int prevBias, int newBias)194 public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { 195 final boolean isNowTop = newBias == JobInfo.BIAS_TOP_APP; 196 final boolean wasTop = prevBias == JobInfo.BIAS_TOP_APP; 197 if (isNowTop != wasTop) { 198 mHandler.obtainMessage(MSG_PROCESS_TOP_STATE_CHANGE, uid, 0).sendToTarget(); 199 } 200 } 201 202 /** Return the app's next estimated launch time. */ 203 @GuardedBy("mLock") 204 @CurrentTimeMillisLong getNextEstimatedLaunchTimeLocked(@onNull JobStatus jobStatus)205 public long getNextEstimatedLaunchTimeLocked(@NonNull JobStatus jobStatus) { 206 final int userId = jobStatus.getSourceUserId(); 207 final String pkgName = jobStatus.getSourcePackageName(); 208 return getNextEstimatedLaunchTimeLocked(userId, pkgName, sSystemClock.millis()); 209 } 210 211 @GuardedBy("mLock") 212 @CurrentTimeMillisLong getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long now)213 private long getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName, 214 @CurrentTimeMillisLong long now) { 215 final Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName); 216 if (nextEstimatedLaunchTime == null 217 || nextEstimatedLaunchTime < now - mLaunchTimeAllowanceMs) { 218 // Don't query usage stats here because it may have to read from disk. 219 mHandler.obtainMessage(MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME, userId, 0, pkgName) 220 .sendToTarget(); 221 // Store something in the cache so we don't keep posting retrieval messages. 222 mEstimatedLaunchTimes.add(userId, pkgName, Long.MAX_VALUE); 223 return Long.MAX_VALUE; 224 } 225 return nextEstimatedLaunchTime; 226 } 227 228 @GuardedBy("mLock") maybeUpdateConstraintForPkgLocked(@urrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed, int userId, String pkgName)229 private boolean maybeUpdateConstraintForPkgLocked(@CurrentTimeMillisLong long now, 230 @ElapsedRealtimeLong long nowElapsed, int userId, String pkgName) { 231 final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); 232 if (jobs == null) { 233 return false; 234 } 235 boolean changed = false; 236 for (int i = 0; i < jobs.size(); i++) { 237 final JobStatus js = jobs.valueAt(i); 238 changed |= updateConstraintLocked(js, now, nowElapsed); 239 } 240 return changed; 241 } 242 maybeUpdateConstraintForUid(int uid)243 private void maybeUpdateConstraintForUid(int uid) { 244 synchronized (mLock) { 245 final ArraySet<String> pkgs = mService.getPackagesForUidLocked(uid); 246 if (pkgs == null) { 247 return; 248 } 249 final int userId = UserHandle.getUserId(uid); 250 final ArraySet<JobStatus> changedJobs = new ArraySet<>(); 251 final long now = sSystemClock.millis(); 252 final long nowElapsed = sElapsedRealtimeClock.millis(); 253 for (int p = pkgs.size() - 1; p >= 0; --p) { 254 final String pkgName = pkgs.valueAt(p); 255 final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); 256 if (jobs == null) { 257 continue; 258 } 259 for (int i = 0; i < jobs.size(); i++) { 260 final JobStatus js = jobs.valueAt(i); 261 if (updateConstraintLocked(js, now, nowElapsed)) { 262 changedJobs.add(js); 263 } 264 } 265 } 266 if (changedJobs.size() > 0) { 267 mStateChangedListener.onControllerStateChanged(changedJobs); 268 } 269 } 270 } 271 processUpdatedEstimatedLaunchTime(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long newEstimatedLaunchTime)272 private void processUpdatedEstimatedLaunchTime(int userId, @NonNull String pkgName, 273 @CurrentTimeMillisLong long newEstimatedLaunchTime) { 274 if (DEBUG) { 275 Slog.d(TAG, "Estimated launch time for " + packageToString(userId, pkgName) 276 + " changed to " + newEstimatedLaunchTime 277 + " (" 278 + TimeUtils.formatDuration(newEstimatedLaunchTime - sSystemClock.millis()) 279 + " from now)"); 280 } 281 282 synchronized (mLock) { 283 final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); 284 if (jobs == null) { 285 if (DEBUG) { 286 Slog.i(TAG, 287 "Not caching launch time since we haven't seen any prefetch" 288 + " jobs for " + packageToString(userId, pkgName)); 289 } 290 } else { 291 // Don't bother caching the value unless the app has scheduled prefetch jobs 292 // before. This is based on the assumption that if an app has scheduled a 293 // prefetch job before, then it will probably schedule another one again. 294 mEstimatedLaunchTimes.add(userId, pkgName, newEstimatedLaunchTime); 295 296 if (!jobs.isEmpty()) { 297 final long now = sSystemClock.millis(); 298 final long nowElapsed = sElapsedRealtimeClock.millis(); 299 updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed); 300 if (maybeUpdateConstraintForPkgLocked(now, nowElapsed, userId, pkgName)) { 301 mStateChangedListener.onControllerStateChanged(jobs); 302 } 303 } 304 } 305 } 306 } 307 308 @GuardedBy("mLock") updateConstraintLocked(@onNull JobStatus jobStatus, @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed)309 private boolean updateConstraintLocked(@NonNull JobStatus jobStatus, 310 @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) { 311 // Mark a prefetch constraint as satisfied in the following scenarios: 312 // 1. The app is not open but it will be launched soon 313 // 2. The app is open and the job is already running (so we let it finish) 314 // 3. The app is not open but has an active widget (we can't tell if a widget displays 315 // status/data, so this assumes the prefetch job is to update the data displayed on 316 // the widget). 317 final boolean appIsOpen = 318 mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP; 319 final boolean satisfied; 320 if (!appIsOpen) { 321 final int userId = jobStatus.getSourceUserId(); 322 final String pkgName = jobStatus.getSourcePackageName(); 323 satisfied = willBeLaunchedSoonLocked(userId, pkgName, now) 324 // At the time of implementation, isBoundWidgetPackage() results in a process ID 325 // check and then a lookup into a map. Calling the method here every time 326 // is based on the assumption that widgets won't change often and 327 // AppWidgetManager won't be a bottleneck, so having a local cache won't provide 328 // huge performance gains. If anything changes, we should reconsider having a 329 // local cache. 330 || (mAppWidgetManager != null 331 && mAppWidgetManager.isBoundWidgetPackage(pkgName, userId)); 332 } else { 333 satisfied = mService.isCurrentlyRunningLocked(jobStatus); 334 } 335 return jobStatus.setPrefetchConstraintSatisfied(nowElapsed, satisfied); 336 } 337 338 @GuardedBy("mLock") updateThresholdAlarmLocked(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed)339 private void updateThresholdAlarmLocked(int userId, @NonNull String pkgName, 340 @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) { 341 final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); 342 if (jobs == null || jobs.size() == 0) { 343 mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName)); 344 return; 345 } 346 347 final long nextEstimatedLaunchTime = getNextEstimatedLaunchTimeLocked(userId, pkgName, now); 348 // Avoid setting an alarm for the end of time. 349 if (nextEstimatedLaunchTime != Long.MAX_VALUE 350 && nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) { 351 // Set alarm to be notified when this crosses the threshold. 352 final long timeToCrossThresholdMs = 353 nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs); 354 mThresholdAlarmListener.addAlarm(new Package(userId, pkgName), 355 nowElapsed + timeToCrossThresholdMs); 356 } else { 357 mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName)); 358 } 359 } 360 361 /** 362 * Returns true if the app is expected to be launched soon, where "soon" is within the next 363 * {@link #mLaunchTimeThresholdMs} time. 364 */ 365 @GuardedBy("mLock") willBeLaunchedSoonLocked(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long now)366 private boolean willBeLaunchedSoonLocked(int userId, @NonNull String pkgName, 367 @CurrentTimeMillisLong long now) { 368 return getNextEstimatedLaunchTimeLocked(userId, pkgName, now) 369 <= now + mLaunchTimeThresholdMs - mLaunchTimeAllowanceMs; 370 } 371 372 @Override 373 @GuardedBy("mLock") prepareForUpdatedConstantsLocked()374 public void prepareForUpdatedConstantsLocked() { 375 mPcConstants.mShouldReevaluateConstraints = false; 376 } 377 378 @Override 379 @GuardedBy("mLock") processConstantLocked(DeviceConfig.Properties properties, String key)380 public void processConstantLocked(DeviceConfig.Properties properties, String key) { 381 mPcConstants.processConstantLocked(properties, key); 382 } 383 384 @Override 385 @GuardedBy("mLock") onConstantsUpdatedLocked()386 public void onConstantsUpdatedLocked() { 387 if (mPcConstants.mShouldReevaluateConstraints) { 388 // Update job bookkeeping out of band. 389 JobSchedulerBackgroundThread.getHandler().post(() -> { 390 final ArraySet<JobStatus> changedJobs = new ArraySet<>(); 391 synchronized (mLock) { 392 final long nowElapsed = sElapsedRealtimeClock.millis(); 393 final long now = sSystemClock.millis(); 394 for (int u = 0; u < mTrackedJobs.numMaps(); ++u) { 395 final int userId = mTrackedJobs.keyAt(u); 396 for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) { 397 final String packageName = mTrackedJobs.keyAt(u, p); 398 if (maybeUpdateConstraintForPkgLocked( 399 now, nowElapsed, userId, packageName)) { 400 changedJobs.addAll(mTrackedJobs.valueAt(u, p)); 401 } 402 if (!willBeLaunchedSoonLocked(userId, packageName, now)) { 403 updateThresholdAlarmLocked(userId, packageName, now, nowElapsed); 404 } 405 } 406 } 407 } 408 if (changedJobs.size() > 0) { 409 mStateChangedListener.onControllerStateChanged(changedJobs); 410 } 411 }); 412 } 413 } 414 415 /** Track when apps will cross the "will run soon" threshold. */ 416 private class ThresholdAlarmListener extends AlarmQueue<Package> { ThresholdAlarmListener(Context context, Looper looper)417 private ThresholdAlarmListener(Context context, Looper looper) { 418 super(context, looper, "*job.prefetch*", "Prefetch threshold", false, 419 PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS / 10); 420 } 421 422 @Override isForUser(@onNull Package key, int userId)423 protected boolean isForUser(@NonNull Package key, int userId) { 424 return key.userId == userId; 425 } 426 427 @Override processExpiredAlarms(@onNull ArraySet<Package> expired)428 protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) { 429 final ArraySet<JobStatus> changedJobs = new ArraySet<>(); 430 synchronized (mLock) { 431 final long now = sSystemClock.millis(); 432 final long nowElapsed = sElapsedRealtimeClock.millis(); 433 for (int i = 0; i < expired.size(); ++i) { 434 Package p = expired.valueAt(i); 435 if (!willBeLaunchedSoonLocked(p.userId, p.packageName, now)) { 436 Slog.e(TAG, "Alarm expired for " 437 + packageToString(p.userId, p.packageName) + " at the wrong time"); 438 updateThresholdAlarmLocked(p.userId, p.packageName, now, nowElapsed); 439 } else if (maybeUpdateConstraintForPkgLocked( 440 now, nowElapsed, p.userId, p.packageName)) { 441 changedJobs.addAll(mTrackedJobs.get(p.userId, p.packageName)); 442 } 443 } 444 } 445 if (changedJobs.size() > 0) { 446 mStateChangedListener.onControllerStateChanged(changedJobs); 447 } 448 } 449 } 450 451 private class PcHandler extends Handler { PcHandler(Looper looper)452 PcHandler(Looper looper) { 453 super(looper); 454 } 455 456 @Override handleMessage(Message msg)457 public void handleMessage(Message msg) { 458 switch (msg.what) { 459 case MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME: 460 final int userId = msg.arg1; 461 final String pkgName = (String) msg.obj; 462 // It's okay to get the time without holding the lock since all updates to 463 // the local cache go through the handler (and therefore will be sequential). 464 final long nextEstimatedLaunchTime = mUsageStatsManagerInternal 465 .getEstimatedPackageLaunchTime(pkgName, userId); 466 if (DEBUG) { 467 Slog.d(TAG, "Retrieved launch time for " 468 + packageToString(userId, pkgName) 469 + " of " + nextEstimatedLaunchTime 470 + " (" + TimeUtils.formatDuration( 471 nextEstimatedLaunchTime - sSystemClock.millis()) 472 + " from now)"); 473 } 474 synchronized (mLock) { 475 final Long curEstimatedLaunchTime = 476 mEstimatedLaunchTimes.get(userId, pkgName); 477 if (curEstimatedLaunchTime == null 478 || nextEstimatedLaunchTime != curEstimatedLaunchTime) { 479 processUpdatedEstimatedLaunchTime( 480 userId, pkgName, nextEstimatedLaunchTime); 481 } 482 } 483 break; 484 485 case MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME: 486 final SomeArgs args = (SomeArgs) msg.obj; 487 processUpdatedEstimatedLaunchTime(args.argi1, (String) args.arg1, args.argl1); 488 args.recycle(); 489 break; 490 491 case MSG_PROCESS_TOP_STATE_CHANGE: 492 final int uid = msg.arg1; 493 maybeUpdateConstraintForUid(uid); 494 break; 495 } 496 } 497 } 498 499 @VisibleForTesting 500 class PcConstants { 501 private boolean mShouldReevaluateConstraints = false; 502 503 /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */ 504 private static final String PC_CONSTANT_PREFIX = "pc_"; 505 506 @VisibleForTesting 507 static final String KEY_LAUNCH_TIME_THRESHOLD_MS = 508 PC_CONSTANT_PREFIX + "launch_time_threshold_ms"; 509 @VisibleForTesting 510 static final String KEY_LAUNCH_TIME_ALLOWANCE_MS = 511 PC_CONSTANT_PREFIX + "launch_time_allowance_ms"; 512 513 private static final long DEFAULT_LAUNCH_TIME_THRESHOLD_MS = 7 * HOUR_IN_MILLIS; 514 private static final long DEFAULT_LAUNCH_TIME_ALLOWANCE_MS = 20 * MINUTE_IN_MILLIS; 515 516 /** How much time each app will have to run jobs within their standby bucket window. */ 517 public long LAUNCH_TIME_THRESHOLD_MS = DEFAULT_LAUNCH_TIME_THRESHOLD_MS; 518 519 /** 520 * How much additional time to add to an estimated launch time before considering it 521 * unusable. 522 */ 523 public long LAUNCH_TIME_ALLOWANCE_MS = DEFAULT_LAUNCH_TIME_ALLOWANCE_MS; 524 525 @GuardedBy("mLock") processConstantLocked(@onNull DeviceConfig.Properties properties, @NonNull String key)526 public void processConstantLocked(@NonNull DeviceConfig.Properties properties, 527 @NonNull String key) { 528 switch (key) { 529 case KEY_LAUNCH_TIME_ALLOWANCE_MS: 530 LAUNCH_TIME_ALLOWANCE_MS = 531 properties.getLong(key, DEFAULT_LAUNCH_TIME_ALLOWANCE_MS); 532 // Limit the allowance to the range [0 minutes, 2 hours]. 533 long newLaunchTimeAllowanceMs = Math.min(2 * HOUR_IN_MILLIS, 534 Math.max(0, LAUNCH_TIME_ALLOWANCE_MS)); 535 if (mLaunchTimeAllowanceMs != newLaunchTimeAllowanceMs) { 536 mLaunchTimeAllowanceMs = newLaunchTimeAllowanceMs; 537 mShouldReevaluateConstraints = true; 538 } 539 break; 540 case KEY_LAUNCH_TIME_THRESHOLD_MS: 541 LAUNCH_TIME_THRESHOLD_MS = 542 properties.getLong(key, DEFAULT_LAUNCH_TIME_THRESHOLD_MS); 543 // Limit the threshold to the range [1, 24] hours. 544 long newLaunchTimeThresholdMs = Math.min(24 * HOUR_IN_MILLIS, 545 Math.max(HOUR_IN_MILLIS, LAUNCH_TIME_THRESHOLD_MS)); 546 if (mLaunchTimeThresholdMs != newLaunchTimeThresholdMs) { 547 mLaunchTimeThresholdMs = newLaunchTimeThresholdMs; 548 mShouldReevaluateConstraints = true; 549 // Give a leeway of 10% of the launch time threshold between alarms. 550 mThresholdAlarmListener.setMinTimeBetweenAlarmsMs( 551 mLaunchTimeThresholdMs / 10); 552 } 553 break; 554 } 555 } 556 dump(IndentingPrintWriter pw)557 private void dump(IndentingPrintWriter pw) { 558 pw.println(); 559 pw.print(PrefetchController.class.getSimpleName()); 560 pw.println(":"); 561 pw.increaseIndent(); 562 563 pw.print(KEY_LAUNCH_TIME_THRESHOLD_MS, LAUNCH_TIME_THRESHOLD_MS).println(); 564 pw.print(KEY_LAUNCH_TIME_ALLOWANCE_MS, LAUNCH_TIME_ALLOWANCE_MS).println(); 565 566 pw.decreaseIndent(); 567 } 568 } 569 570 //////////////////////// TESTING HELPERS ///////////////////////////// 571 572 @VisibleForTesting getLaunchTimeAllowanceMs()573 long getLaunchTimeAllowanceMs() { 574 return mLaunchTimeAllowanceMs; 575 } 576 577 @VisibleForTesting getLaunchTimeThresholdMs()578 long getLaunchTimeThresholdMs() { 579 return mLaunchTimeThresholdMs; 580 } 581 582 @VisibleForTesting 583 @NonNull getPcConstants()584 PcConstants getPcConstants() { 585 return mPcConstants; 586 } 587 588 //////////////////////////// DATA DUMP ////////////////////////////// 589 590 @Override 591 @GuardedBy("mLock") dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)592 public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { 593 final long now = sSystemClock.millis(); 594 595 pw.println("Cached launch times:"); 596 pw.increaseIndent(); 597 for (int u = 0; u < mEstimatedLaunchTimes.numMaps(); ++u) { 598 final int userId = mEstimatedLaunchTimes.keyAt(u); 599 for (int p = 0; p < mEstimatedLaunchTimes.numElementsForKey(userId); ++p) { 600 final String pkgName = mEstimatedLaunchTimes.keyAt(u, p); 601 final long estimatedLaunchTime = mEstimatedLaunchTimes.valueAt(u, p); 602 603 pw.print(packageToString(userId, pkgName)); 604 pw.print(": "); 605 pw.print(estimatedLaunchTime); 606 pw.print(" ("); 607 TimeUtils.formatDuration(estimatedLaunchTime - now, pw, 608 TimeUtils.HUNDRED_DAY_FIELD_LEN); 609 pw.println(" from now)"); 610 } 611 } 612 pw.decreaseIndent(); 613 614 pw.println(); 615 mTrackedJobs.forEach((jobs) -> { 616 for (int j = 0; j < jobs.size(); j++) { 617 final JobStatus js = jobs.valueAt(j); 618 if (!predicate.test(js)) { 619 continue; 620 } 621 pw.print("#"); 622 js.printUniqueId(pw); 623 pw.print(" from "); 624 UserHandle.formatUid(pw, js.getSourceUid()); 625 pw.println(); 626 } 627 }); 628 629 pw.println(); 630 mThresholdAlarmListener.dump(pw); 631 } 632 633 @Override dumpConstants(IndentingPrintWriter pw)634 public void dumpConstants(IndentingPrintWriter pw) { 635 mPcConstants.dump(pw); 636 } 637 } 638