diff options
| author | 2021-10-07 10:31:03 -0700 | |
|---|---|---|
| committer | 2021-10-12 08:03:39 -0700 | |
| commit | cd3e04b8e148b0d2a59bb69f6acfdd1f707cdebd (patch) | |
| tree | 86df98949ae4ec7ea74ba32ceae79143f60b7be9 | |
| parent | 638e4594a38a31e852de0637b84041aa2a769281 (diff) | |
Add ThresholdAlarmQueue.
Add an AlarmQueue to track and update the controller when an app crosses
the "will be launched soon" threshold.
Bug: 194532703
Test: atest FrameworksMockingServicesTests:PrefetchControllerTest
Change-Id: Iaad45f5a5dddc75412ba3c802685399d98a05ad4
4 files changed, 181 insertions, 72 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java index 98a39a65ae6c..aca381ff2043 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java @@ -113,7 +113,6 @@ public class ComponentController extends StateController { userFilter.addAction(Intent.ACTION_USER_STOPPED); mContext.registerReceiverAsUser( mBroadcastReceiver, UserHandle.ALL, userFilter, null, null); - } @Override diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java new file mode 100644 index 000000000000..78a77fe46f3c --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job.controllers; + +import java.util.Objects; + +/** Wrapper class to represent a userId-pkgName combo. */ +final class Package { + public final String packageName; + public final int userId; + + Package(int userId, String packageName) { + this.userId = userId; + this.packageName = packageName; + } + + @Override + public String toString() { + return packageToString(userId, packageName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Package)) { + return false; + } + Package other = (Package) obj; + return userId == other.userId && Objects.equals(packageName, other.packageName); + } + + @Override + public int hashCode() { + return packageName.hashCode() + userId; + } + + /** + * Standardize the output of userId-packageName combo. + */ + static String packageToString(int userId, String packageName) { + return "<" + userId + ">" + packageName; + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java index 725092c328e5..6232dfb12822 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java @@ -20,9 +20,13 @@ import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import static com.android.server.job.JobSchedulerService.sSystemClock; +import static com.android.server.job.controllers.Package.packageToString; import android.annotation.CurrentTimeMillisLong; +import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; +import android.content.Context; +import android.os.Looper; import android.os.UserHandle; import android.provider.DeviceConfig; import android.util.ArraySet; @@ -36,6 +40,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.JobSchedulerBackgroundThread; import com.android.server.job.JobSchedulerService; +import com.android.server.utils.AlarmQueue; import java.util.function.Predicate; @@ -57,6 +62,7 @@ public class PrefetchController extends StateController { */ @GuardedBy("mLock") private final SparseArrayMap<String, Long> mEstimatedLaunchTimes = new SparseArrayMap<>(); + private final ThresholdAlarmListener mThresholdAlarmListener; /** * The cutoff point to decide if a prefetch job is worth running or not. If the app is expected @@ -69,6 +75,8 @@ public class PrefetchController extends StateController { public PrefetchController(JobSchedulerService service) { super(service); mPcConstants = new PcConstants(); + mThresholdAlarmListener = new ThresholdAlarmListener( + mContext, JobSchedulerBackgroundThread.get().getLooper()); } @Override @@ -82,9 +90,13 @@ public class PrefetchController extends StateController { jobs = new ArraySet<>(); mTrackedJobs.add(userId, pkgName, jobs); } - jobs.add(jobStatus); - updateConstraintLocked(jobStatus, - sSystemClock.millis(), sElapsedRealtimeClock.millis()); + final long now = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + if (jobs.add(jobStatus) && jobs.size() == 1 + && !willBeLaunchedSoonLocked(userId, pkgName, now)) { + updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed); + } + updateConstraintLocked(jobStatus, now, nowElapsed); } } @@ -92,10 +104,11 @@ public class PrefetchController extends StateController { @GuardedBy("mLock") public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate) { - final ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUserId(), - jobStatus.getSourcePackageName()); - if (jobs != null) { - jobs.remove(jobStatus); + final int userId = jobStatus.getSourceUserId(); + final String pkgName = jobStatus.getSourcePackageName(); + final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); + if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) { + mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName)); } } @@ -109,6 +122,7 @@ public class PrefetchController extends StateController { final int userId = UserHandle.getUserId(uid); mTrackedJobs.delete(userId, packageName); mEstimatedLaunchTimes.delete(userId, packageName); + mThresholdAlarmListener.removeAlarmForKey(new Package(userId, packageName)); } @Override @@ -116,6 +130,7 @@ public class PrefetchController extends StateController { public void onUserRemovedLocked(int userId) { mTrackedJobs.delete(userId); mEstimatedLaunchTimes.delete(userId); + mThresholdAlarmListener.removeAlarmsForUserId(userId); } /** Return the app's next estimated launch time. */ @@ -124,8 +139,14 @@ public class PrefetchController extends StateController { public long getNextEstimatedLaunchTimeLocked(@NonNull JobStatus jobStatus) { final int userId = jobStatus.getSourceUserId(); final String pkgName = jobStatus.getSourcePackageName(); + return getNextEstimatedLaunchTimeLocked(userId, pkgName, sSystemClock.millis()); + } + + @GuardedBy("mLock") + @CurrentTimeMillisLong + private long getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName, + @CurrentTimeMillisLong long now) { Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName); - final long now = sSystemClock.millis(); if (nextEstimatedLaunchTime == null || nextEstimatedLaunchTime < now) { // TODO(194532703): get estimated time from UsageStats nextEstimatedLaunchTime = now + 2 * HOUR_IN_MILLIS; @@ -135,8 +156,8 @@ public class PrefetchController extends StateController { } @GuardedBy("mLock") - private boolean maybeUpdateConstraintForPkgLocked(long now, long nowElapsed, int userId, - String pkgName) { + private boolean maybeUpdateConstraintForPkgLocked(@CurrentTimeMillisLong long now, + @ElapsedRealtimeLong long nowElapsed, int userId, String pkgName) { final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); if (jobs == null) { return false; @@ -150,10 +171,43 @@ public class PrefetchController extends StateController { } @GuardedBy("mLock") - private boolean updateConstraintLocked(@NonNull JobStatus jobStatus, long now, - long nowElapsed) { + private boolean updateConstraintLocked(@NonNull JobStatus jobStatus, + @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) { return jobStatus.setPrefetchConstraintSatisfied(nowElapsed, - getNextEstimatedLaunchTimeLocked(jobStatus) <= now + mLaunchTimeThresholdMs); + willBeLaunchedSoonLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), now)); + } + + @GuardedBy("mLock") + private void updateThresholdAlarmLocked(int userId, @NonNull String pkgName, + @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) { + final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); + if (jobs == null || jobs.size() == 0) { + mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName)); + return; + } + + final long nextEstimatedLaunchTime = getNextEstimatedLaunchTimeLocked(userId, pkgName, now); + if (nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) { + // Set alarm to be notified when this crosses the threshold. + final long timeToCrossThresholdMs = + nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs); + mThresholdAlarmListener.addAlarm(new Package(userId, pkgName), + nowElapsed + timeToCrossThresholdMs); + } else { + mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName)); + } + } + + /** + * Returns true if the app is expected to be launched soon, where "soon" is within the next + * {@link #mLaunchTimeThresholdMs} time. + */ + @GuardedBy("mLock") + private boolean willBeLaunchedSoonLocked(int userId, @NonNull String pkgName, + @CurrentTimeMillisLong long now) { + return getNextEstimatedLaunchTimeLocked(userId, pkgName, now) + <= now + mLaunchTimeThresholdMs; } @Override @@ -186,6 +240,9 @@ public class PrefetchController extends StateController { now, nowElapsed, userId, packageName)) { changedJobs.addAll(mTrackedJobs.valueAt(u, p)); } + if (!willBeLaunchedSoonLocked(userId, packageName, now)) { + updateThresholdAlarmLocked(userId, packageName, now, nowElapsed); + } } } } @@ -196,6 +253,42 @@ public class PrefetchController extends StateController { } } + /** Track when apps will cross the "will run soon" threshold. */ + private class ThresholdAlarmListener extends AlarmQueue<Package> { + private ThresholdAlarmListener(Context context, Looper looper) { + super(context, looper, "*job.prefetch*", "Prefetch threshold", false, + PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS / 10); + } + + @Override + protected boolean isForUser(@NonNull Package key, int userId) { + return key.userId == userId; + } + + @Override + protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) { + final ArraySet<JobStatus> changedJobs = new ArraySet<>(); + synchronized (mLock) { + final long now = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + for (int i = 0; i < expired.size(); ++i) { + Package p = expired.valueAt(i); + if (!willBeLaunchedSoonLocked(p.userId, p.packageName, now)) { + Slog.e(TAG, "Alarm expired for " + + packageToString(p.userId, p.packageName) + " at the wrong time"); + updateThresholdAlarmLocked(p.userId, p.packageName, now, nowElapsed); + } else if (maybeUpdateConstraintForPkgLocked( + now, nowElapsed, p.userId, p.packageName)) { + changedJobs.addAll(mTrackedJobs.get(p.userId, p.packageName)); + } + } + } + if (changedJobs.size() > 0) { + mStateChangedListener.onControllerStateChanged(changedJobs); + } + } + } + @VisibleForTesting class PcConstants { private boolean mShouldReevaluateConstraints = false; @@ -225,6 +318,9 @@ public class PrefetchController extends StateController { if (mLaunchTimeThresholdMs != newLaunchTimeThresholdMs) { mLaunchTimeThresholdMs = newLaunchTimeThresholdMs; mShouldReevaluateConstraints = true; + // Give a leeway of 10% of the launch time threshold between alarms. + mThresholdAlarmListener.setMinTimeBetweenAlarmsMs( + mLaunchTimeThresholdMs / 10); } break; } @@ -294,6 +390,9 @@ public class PrefetchController extends StateController { pw.println(); } }); + + pw.println(); + mThresholdAlarmListener.dump(pw); } @Override diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index 1016294fba2e..31da526bead9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -27,6 +27,7 @@ import static com.android.server.job.JobSchedulerService.RARE_INDEX; import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.WORKING_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.controllers.Package.packageToString; import android.Manifest; import android.annotation.NonNull; @@ -79,7 +80,6 @@ import com.android.server.utils.AlarmQueue; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; @@ -123,52 +123,6 @@ public final class QuotaController extends StateController { PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.GET_PERMISSIONS | PackageManager.MATCH_KNOWN_PACKAGES; - /** - * Standardize the output of userId-packageName combo. - */ - private static String string(int userId, String packageName) { - return "<" + userId + ">" + packageName; - } - - private static final class Package { - public final String packageName; - public final int userId; - - Package(int userId, String packageName) { - this.userId = userId; - this.packageName = packageName; - } - - @Override - public String toString() { - return string(userId, packageName); - } - - public void dumpDebug(ProtoOutputStream proto, long fieldId) { - final long token = proto.start(fieldId); - - proto.write(StateControllerProto.QuotaController.Package.USER_ID, userId); - proto.write(StateControllerProto.QuotaController.Package.NAME, packageName); - - proto.end(token); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof Package) { - Package other = (Package) obj; - return userId == other.userId && Objects.equals(packageName, other.packageName); - } else { - return false; - } - } - - @Override - public int hashCode() { - return packageName.hashCode() + userId; - } - } - private static int hashLong(long val) { return (int) (val ^ (val >>> 32)); } @@ -1741,7 +1695,6 @@ public final class QuotaController extends StateController { return; } - final String pkgString = string(userId, packageName); ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket); final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats, @@ -1755,7 +1708,8 @@ public final class QuotaController extends StateController { if (inRegularQuota && remainingEJQuota > 0) { // Already in quota. Why was this method called? if (DEBUG) { - Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString + Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + + packageToString(userId, packageName) + " even though it already has " + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) + "ms in its quota."); @@ -1811,8 +1765,8 @@ public final class QuotaController extends StateController { // In some strange cases, an app may end be in the NEVER bucket but could have run // some regular jobs. This results in no EJ timing sessions and QC having a bad // time. - Slog.wtf(TAG, - string(userId, packageName) + " has 0 EJ quota without running anything"); + Slog.wtf(TAG, packageToString(userId, packageName) + + " has 0 EJ quota without running anything"); return; } } @@ -2272,7 +2226,6 @@ public final class QuotaController extends StateController { public void dump(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate) { final long token = proto.start(fieldId); - mPkg.dumpDebug(proto, StateControllerProto.QuotaController.Timer.PKG); proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive()); proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED, mStartTimeElapsed); @@ -2381,7 +2334,6 @@ public final class QuotaController extends StateController { public void dump(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); - mPkg.dumpDebug(proto, StateControllerProto.QuotaController.TopAppTimer.PKG); proto.write(StateControllerProto.QuotaController.TopAppTimer.IS_ACTIVE, isActive()); proto.write(StateControllerProto.QuotaController.TopAppTimer.START_TIME_ELAPSED, mStartTimeElapsed); @@ -2413,7 +2365,7 @@ public final class QuotaController extends StateController { void updateStandbyBucket( final int userId, final @NonNull String packageName, final int bucketIndex) { if (DEBUG) { - Slog.i(TAG, "Moving pkg " + string(userId, packageName) + Slog.i(TAG, "Moving pkg " + packageToString(userId, packageName) + " to bucketIndex " + bucketIndex); } List<JobStatus> restrictedChanges = new ArrayList<>(); @@ -2641,7 +2593,7 @@ public final class QuotaController extends StateController { String packageName = (String) msg.obj; int userId = msg.arg1; if (DEBUG) { - Slog.d(TAG, "Checking pkg " + string(userId, packageName)); + Slog.d(TAG, "Checking pkg " + packageToString(userId, packageName)); } if (maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(), userId, packageName)) { @@ -2722,7 +2674,7 @@ public final class QuotaController extends StateController { final String pkgName = event.getPackageName(); if (DEBUG) { Slog.d(TAG, "Processing event " + event.getEventType() - + " for " + string(userId, pkgName)); + + " for " + packageToString(userId, pkgName)); } switch (event.getEventType()) { case UsageEvents.Event.ACTIVITY_RESUMED: @@ -4119,7 +4071,7 @@ public final class QuotaController extends StateController { final String pkgName = mExecutionStatsCache.keyAt(u, p); ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p); - pw.println(string(userId, pkgName)); + pw.println(packageToString(userId, pkgName)); pw.increaseIndent(); for (int i = 0; i < stats.length; ++i) { ExecutionStats executionStats = stats[i]; @@ -4143,7 +4095,7 @@ public final class QuotaController extends StateController { final String pkgName = mEJStats.keyAt(u, p); ShrinkableDebits debits = mEJStats.valueAt(u, p); - pw.print(string(userId, pkgName)); + pw.print(packageToString(userId, pkgName)); pw.print(": "); debits.dumpLocked(pw); } |