From c3c5fa5a3edff8051f589fecabcbbb89ffaad7bb Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Tue, 9 Nov 2021 14:12:04 -0800 Subject: [PATCH] Don't start prefetch jobs for TOP apps. If a prefetch job hasn't started by the time an app is opened, then we decide not to run it until the app is closed again. The rationale being that "prefetch" is to do something before the app launch and it wouldn't be useful while the app is on TOP. Any prefetch job that is already running when the app is launched will be allowed to finish. Bug: 194532703 Test: atest FrameworksMockingServicesTests:PrefetchControllerTest Change-Id: I485cd0170a0066a82ab66bf92e36206a9a78bd78 --- .../job/controllers/PrefetchController.java | 71 ++++++++++++++++++- .../controllers/PrefetchControllerTest.java | 60 ++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) 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 393f368920b73..788bfe4a0d5d2 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 @@ -25,6 +25,7 @@ import static com.android.server.job.controllers.Package.packageToString; import android.annotation.CurrentTimeMillisLong; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; +import android.app.job.JobInfo; import android.app.usage.UsageStatsManagerInternal; import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener; import android.content.Context; @@ -38,6 +39,7 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArrayMap; +import android.util.SparseBooleanArray; import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; @@ -71,6 +73,9 @@ public class PrefetchController extends StateController { */ @GuardedBy("mLock") private final SparseArrayMap mEstimatedLaunchTimes = new SparseArrayMap<>(); + /** Cached list of UIDs in the TOP state. */ + @GuardedBy("mLock") + private final SparseBooleanArray mTopUids = new SparseBooleanArray(); private final ThresholdAlarmListener mThresholdAlarmListener; /** @@ -98,6 +103,7 @@ public class PrefetchController extends StateController { private static final int MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME = 0; private static final int MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME = 1; + private static final int MSG_PROCESS_TOP_STATE_CHANGE = 2; public PrefetchController(JobSchedulerService service) { super(service); @@ -165,6 +171,22 @@ public class PrefetchController extends StateController { mThresholdAlarmListener.removeAlarmsForUserId(userId); } + @GuardedBy("mLock") + @Override + public void onUidBiasChangedLocked(int uid, int newBias) { + final boolean isNowTop = newBias == JobInfo.BIAS_TOP_APP; + final boolean wasTop = mTopUids.get(uid); + if (isNowTop) { + mTopUids.put(uid, true); + } else { + // Delete entries of non-top apps so the set doesn't get too large. + mTopUids.delete(uid); + } + if (isNowTop != wasTop) { + mHandler.obtainMessage(MSG_PROCESS_TOP_STATE_CHANGE, uid, 0).sendToTarget(); + } + } + /** Return the app's next estimated launch time. */ @GuardedBy("mLock") @CurrentTimeMillisLong @@ -205,6 +227,35 @@ public class PrefetchController extends StateController { return changed; } + private void maybeUpdateConstraintForUid(int uid) { + synchronized (mLock) { + final ArraySet pkgs = mService.getPackagesForUidLocked(uid); + if (pkgs == null) { + return; + } + final int userId = UserHandle.getUserId(uid); + final ArraySet changedJobs = new ArraySet<>(); + final long now = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + for (int p = pkgs.size() - 1; p >= 0; --p) { + final String pkgName = pkgs.valueAt(p); + final ArraySet jobs = mTrackedJobs.get(userId, pkgName); + if (jobs == null) { + continue; + } + for (int i = 0; i < jobs.size(); i++) { + final JobStatus js = jobs.valueAt(i); + if (updateConstraintLocked(js, now, nowElapsed)) { + changedJobs.add(js); + } + } + } + if (changedJobs.size() > 0) { + mStateChangedListener.onControllerStateChanged(changedJobs); + } + } + } + private void processUpdatedEstimatedLaunchTime(int userId, @NonNull String pkgName, @CurrentTimeMillisLong long newEstimatedLaunchTime) { if (DEBUG) { @@ -244,9 +295,18 @@ public class PrefetchController extends StateController { @GuardedBy("mLock") private boolean updateConstraintLocked(@NonNull JobStatus jobStatus, @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) { - return jobStatus.setPrefetchConstraintSatisfied(nowElapsed, - willBeLaunchedSoonLocked( - jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), now)); + // Mark a prefetch constraint as satisfied in the following scenarios: + // 1. The app is not open but it will be launched soon + // 2. The app is open and the job is already running (so we let it finish) + final boolean appIsOpen = mTopUids.get(jobStatus.getSourceUid()); + final boolean satisfied; + if (!appIsOpen) { + satisfied = willBeLaunchedSoonLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), now); + } else { + satisfied = mService.isCurrentlyRunningLocked(jobStatus); + } + return jobStatus.setPrefetchConstraintSatisfied(nowElapsed, satisfied); } @GuardedBy("mLock") @@ -399,6 +459,11 @@ public class PrefetchController extends StateController { processUpdatedEstimatedLaunchTime(args.argi1, (String) args.arg1, args.argl1); args.recycle(); break; + + case MSG_PROCESS_TOP_STATE_CHANGE: + final int uid = msg.arg1; + maybeUpdateConstraintForUid(uid); + break; } } } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java index e5b2d141ba103..b17ff53b166cc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java @@ -47,8 +47,11 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Looper; +import android.os.Process; import android.os.SystemClock; import android.provider.DeviceConfig; +import android.util.ArraySet; +import android.util.SparseArray; import androidx.test.runner.AndroidJUnit4; @@ -85,6 +88,7 @@ public class PrefetchControllerTest { private PcConstants mPcConstants; private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder; private EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener; + private SparseArray> mPackagesForUid = new SparseArray<>(); private MockitoSession mMockingSession; @Mock @@ -125,6 +129,10 @@ public class PrefetchControllerTest { -> mDeviceConfigPropertiesBuilder.build()) .when(() -> DeviceConfig.getProperties( eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.any())); + // Used in PrefetchController.maybeUpdateConstraintForUid + when(mJobSchedulerService.getPackagesForUidLocked(anyInt())) + .thenAnswer(invocationOnMock + -> mPackagesForUid.get(invocationOnMock.getArgument(0))); // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions // in the past, and PrefetchController sometimes floors values at 0, so if the test time @@ -146,6 +154,8 @@ public class PrefetchControllerTest { mPrefetchController = new PrefetchController(mJobSchedulerService); mPcConstants = mPrefetchController.getPcConstants(); + setUidBias(Process.myUid(), JobInfo.BIAS_DEFAULT); + verify(mUsageStatsManagerInternal) .registerLaunchTimeChangedListener(eltListenerCaptor.capture()); mEstimatedLaunchTimeChangedListener = eltListenerCaptor.getValue(); @@ -185,6 +195,12 @@ public class PrefetchControllerTest { return Clock.offset(clock, Duration.ofMillis(incrementMs)); } + private void setUidBias(int uid, int bias) { + synchronized (mPrefetchController.mLock) { + mPrefetchController.onUidBiasChangedLocked(uid, bias); + } + } + private void setDeviceConfigLong(String key, long val) { mDeviceConfigPropertiesBuilder.setLong(key, val); synchronized (mPrefetchController.mLock) { @@ -196,6 +212,12 @@ public class PrefetchControllerTest { private void trackJobs(JobStatus... jobs) { for (JobStatus job : jobs) { + ArraySet pkgs = mPackagesForUid.get(job.getSourceUid()); + if (pkgs == null) { + pkgs = new ArraySet<>(); + mPackagesForUid.put(job.getSourceUid(), pkgs); + } + pkgs.add(job.getSourcePackageName()); synchronized (mPrefetchController.mLock) { mPrefetchController.maybeStartTrackingJobLocked(job, null); } @@ -269,9 +291,45 @@ public class PrefetchControllerTest { trackJobs(job); verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS)) .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID); + verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any()); assertTrue(job.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); } + @Test + public void testConstraintSatisfiedWhenTop() { + final JobStatus jobPending = createJobStatus("testConstraintSatisfiedWhenTop", 1); + final JobStatus jobRunning = createJobStatus("testConstraintSatisfiedWhenTop", 2); + final int uid = jobPending.getSourceUid(); + + when(mJobSchedulerService.isCurrentlyRunningLocked(jobPending)).thenReturn(false); + when(mJobSchedulerService.isCurrentlyRunningLocked(jobRunning)).thenReturn(true); + + InOrder inOrder = inOrder(mJobSchedulerService); + + when(mUsageStatsManagerInternal + .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID)) + .thenReturn(sSystemClock.millis() + 10 * MINUTE_IN_MILLIS); + trackJobs(jobPending, jobRunning); + verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS)) + .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID); + inOrder.verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)) + .onControllerStateChanged(any()); + assertTrue(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); + assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); + setUidBias(uid, JobInfo.BIAS_TOP_APP); + // Processing happens on the handler, so wait until we're sure the change has been processed + inOrder.verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)) + .onControllerStateChanged(any()); + // Already running job should continue but pending job must wait. + assertFalse(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); + assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); + setUidBias(uid, JobInfo.BIAS_DEFAULT); + inOrder.verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)) + .onControllerStateChanged(any()); + assertTrue(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); + assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); + } + @Test public void testEstimatedLaunchTimeChangedToLate() { setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS); @@ -285,6 +343,7 @@ public class PrefetchControllerTest { trackJobs(jobStatus); inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS)) .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID); + verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any()); assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); mEstimatedLaunchTimeChangedListener.onEstimatedLaunchTimeChanged(SOURCE_USER_ID, @@ -315,6 +374,7 @@ public class PrefetchControllerTest { inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS).times(0)) .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID); + verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any()); assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); } }