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