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
This commit is contained in:
Kweku Adams
2021-10-07 10:31:03 -07:00
parent 638e4594a3
commit cd3e04b8e1
4 changed files with 181 additions and 72 deletions

View File

@@ -113,7 +113,6 @@ public class ComponentController extends StateController {
userFilter.addAction(Intent.ACTION_USER_STOPPED);
mContext.registerReceiverAsUser(
mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
}
@Override

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);
}