Merge "Make sure EJ rewarding quota works." into sc-dev

This commit is contained in:
Kweku Adams
2021-02-24 19:27:29 +00:00
committed by Android (Google) Code Review
2 changed files with 359 additions and 53 deletions

View File

@@ -828,8 +828,8 @@ public final class QuotaController extends StateController {
} }
@NonNull @NonNull
private ShrinkableDebits getEJQuotaLocked(final int userId, @VisibleForTesting
@NonNull final String packageName) { ShrinkableDebits getEJDebitsLocked(final int userId, @NonNull final String packageName) {
ShrinkableDebits debits = mEJStats.get(userId, packageName); ShrinkableDebits debits = mEJStats.get(userId, packageName);
if (debits == null) { if (debits == null) {
debits = new ShrinkableDebits( debits = new ShrinkableDebits(
@@ -931,7 +931,7 @@ public final class QuotaController extends StateController {
@VisibleForTesting @VisibleForTesting
long getRemainingEJExecutionTimeLocked(final int userId, @NonNull final String packageName) { long getRemainingEJExecutionTimeLocked(final int userId, @NonNull final String packageName) {
ShrinkableDebits quota = getEJQuotaLocked(userId, packageName); ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
if (quota.getStandbyBucketLocked() == NEVER_INDEX) { if (quota.getStandbyBucketLocked() == NEVER_INDEX) {
return 0; return 0;
} }
@@ -948,7 +948,7 @@ public final class QuotaController extends StateController {
if (ts.endTimeElapsed < windowStartTimeElapsed) { if (ts.endTimeElapsed < windowStartTimeElapsed) {
final long duration = ts.endTimeElapsed - ts.startTimeElapsed; final long duration = ts.endTimeElapsed - ts.startTimeElapsed;
remainingMs += duration; remainingMs += duration;
quota.transactOnDebitsLocked(-duration); quota.transactLocked(-duration);
timingSessions.remove(0); timingSessions.remove(0);
} else if (ts.startTimeElapsed < windowStartTimeElapsed) { } else if (ts.startTimeElapsed < windowStartTimeElapsed) {
remainingMs += windowStartTimeElapsed - ts.startTimeElapsed; remainingMs += windowStartTimeElapsed - ts.startTimeElapsed;
@@ -960,15 +960,16 @@ public final class QuotaController extends StateController {
} }
} }
TopAppTimer topAppTimer = mTopAppTrackers.get(userId, packageName);
if (topAppTimer != null && topAppTimer.isActive()) {
remainingMs += topAppTimer.getPendingReward(nowElapsed);
}
Timer timer = mEJPkgTimers.get(userId, packageName); Timer timer = mEJPkgTimers.get(userId, packageName);
if (timer == null) { if (timer == null) {
return remainingMs; return remainingMs;
} }
// There's a case where the debits tally is 0 but a currently running HPJ still counts
// towards quota. If the app gets a reward in this case, the reward is lost and the HPJ
// run is still fully counted.
// TODO(171305774)/STOPSHIP: make sure getting rewards while HPJ currently executing isn't
// treated negatively
return remainingMs - timer.getCurrentDuration(sElapsedRealtimeClock.millis()); return remainingMs - timer.getCurrentDuration(sElapsedRealtimeClock.millis());
} }
@@ -1081,7 +1082,7 @@ public final class QuotaController extends StateController {
} }
final long nowElapsed = sElapsedRealtimeClock.millis(); final long nowElapsed = sElapsedRealtimeClock.millis();
ShrinkableDebits quota = getEJQuotaLocked(userId, packageName); ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
final long limitMs = getEJLimitMsLocked(packageName, quota.getStandbyBucketLocked()); final long limitMs = getEJLimitMsLocked(packageName, quota.getStandbyBucketLocked());
final long startWindowElapsed = Math.max(0, nowElapsed - mEJLimitWindowSizeMs); final long startWindowElapsed = Math.max(0, nowElapsed - mEJLimitWindowSizeMs);
long remainingDeadSpaceMs = remainingExecutionTimeMs; long remainingDeadSpaceMs = remainingExecutionTimeMs;
@@ -1372,6 +1373,11 @@ public final class QuotaController extends StateController {
@VisibleForTesting @VisibleForTesting
void saveTimingSession(final int userId, @NonNull final String packageName, void saveTimingSession(final int userId, @NonNull final String packageName,
@NonNull final TimingSession session, boolean isExpedited) { @NonNull final TimingSession session, boolean isExpedited) {
saveTimingSession(userId, packageName, session, isExpedited, 0);
}
private void saveTimingSession(final int userId, @NonNull final String packageName,
@NonNull final TimingSession session, boolean isExpedited, long debitAdjustment) {
synchronized (mLock) { synchronized (mLock) {
final SparseArrayMap<String, List<TimingSession>> sessionMap = final SparseArrayMap<String, List<TimingSession>> sessionMap =
isExpedited ? mEJTimingSessions : mTimingSessions; isExpedited ? mEJTimingSessions : mTimingSessions;
@@ -1382,8 +1388,9 @@ public final class QuotaController extends StateController {
} }
sessions.add(session); sessions.add(session);
if (isExpedited) { if (isExpedited) {
final ShrinkableDebits quota = getEJQuotaLocked(userId, packageName); final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
quota.transactOnDebitsLocked(session.endTimeElapsed - session.startTimeElapsed); quota.transactLocked(session.endTimeElapsed - session.startTimeElapsed
+ debitAdjustment);
} else { } else {
// Adding a new session means that the current stats are now incorrect. // Adding a new session means that the current stats are now incorrect.
invalidateAllExecutionStatsLocked(userId, packageName); invalidateAllExecutionStatsLocked(userId, packageName);
@@ -1396,15 +1403,34 @@ public final class QuotaController extends StateController {
private void grantRewardForInstantEvent( private void grantRewardForInstantEvent(
final int userId, @NonNull final String packageName, final long credit) { final int userId, @NonNull final String packageName, final long credit) {
synchronized (mLock) { synchronized (mLock) {
final ShrinkableDebits quota = getEJQuotaLocked(userId, packageName); final long nowElapsed = sElapsedRealtimeClock.millis();
quota.transactOnDebitsLocked(-credit); final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
if (maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(), if (transactQuotaLocked(userId, packageName, nowElapsed, quota, credit)
userId, packageName)) { && maybeUpdateConstraintForPkgLocked(nowElapsed, userId, packageName)) {
mStateChangedListener.onControllerStateChanged(); mStateChangedListener.onControllerStateChanged();
} }
} }
} }
private boolean transactQuotaLocked(final int userId, @NonNull final String packageName,
final long nowElapsed, @NonNull ShrinkableDebits debits, final long credit) {
final long oldTally = debits.getTallyLocked();
final long leftover = debits.transactLocked(-credit);
if (DEBUG) {
Slog.d(TAG, "debits overflowed by " + leftover);
}
boolean changed = oldTally != debits.getTallyLocked();
if (leftover != 0) {
// Only adjust timer if its active.
final Timer ejTimer = mEJPkgTimers.get(userId, packageName);
if (ejTimer != null && ejTimer.isActive()) {
ejTimer.updateDebitAdjustment(nowElapsed, leftover);
changed = true;
}
}
return changed;
}
private final class EarliestEndTimeFunctor implements Consumer<List<TimingSession>> { private final class EarliestEndTimeFunctor implements Consumer<List<TimingSession>> {
public long earliestEndElapsed = Long.MAX_VALUE; public long earliestEndElapsed = Long.MAX_VALUE;
@@ -1875,7 +1901,8 @@ public final class QuotaController extends StateController {
} }
} }
private static final class ShrinkableDebits { @VisibleForTesting
static final class ShrinkableDebits {
/** The amount of quota remaining. Can be negative if limit changes. */ /** The amount of quota remaining. Can be negative if limit changes. */
private long mDebitTally; private long mDebitTally;
private int mStandbyBucket; private int mStandbyBucket;
@@ -1893,8 +1920,11 @@ public final class QuotaController extends StateController {
* Negative if the tally should decrease (therefore increasing available quota); * Negative if the tally should decrease (therefore increasing available quota);
* or positive if the tally should increase (therefore decreasing available quota). * or positive if the tally should increase (therefore decreasing available quota).
*/ */
void transactOnDebitsLocked(final long amount) { long transactLocked(final long amount) {
final long leftover = amount < 0 && Math.abs(amount) > mDebitTally
? mDebitTally + amount : 0;
mDebitTally = Math.max(0, mDebitTally + amount); mDebitTally = Math.max(0, mDebitTally + amount);
return leftover;
} }
void setStandbyBucketLocked(int standbyBucket) { void setStandbyBucketLocked(int standbyBucket) {
@@ -1927,6 +1957,7 @@ public final class QuotaController extends StateController {
private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>(); private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>();
private long mStartTimeElapsed; private long mStartTimeElapsed;
private int mBgJobCount; private int mBgJobCount;
private long mDebitAdjustment;
Timer(int uid, int userId, String packageName, boolean regularJobTimer) { Timer(int uid, int userId, String packageName, boolean regularJobTimer) {
mPkg = new Package(userId, packageName); mPkg = new Package(userId, packageName);
@@ -1957,6 +1988,7 @@ public final class QuotaController extends StateController {
if (mRunningBgJobs.size() == 1) { if (mRunningBgJobs.size() == 1) {
// Started tracking the first job. // Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis(); mStartTimeElapsed = sElapsedRealtimeClock.millis();
mDebitAdjustment = 0;
if (mRegularJobTimer) { if (mRegularJobTimer) {
// Starting the timer means that all cached execution stats are now // Starting the timer means that all cached execution stats are now
// incorrect. // incorrect.
@@ -1988,6 +2020,11 @@ public final class QuotaController extends StateController {
} }
} }
void updateDebitAdjustment(long nowElapsed, long debit) {
// Make sure we don't have a credit larger than the expected session.
mDebitAdjustment = Math.max(mDebitAdjustment + debit, mStartTimeElapsed - nowElapsed);
}
/** /**
* Stops tracking all jobs and cancels any pending alarms. This should only be called if * Stops tracking all jobs and cancels any pending alarms. This should only be called if
* the Timer is not going to be used anymore. * the Timer is not going to be used anymore.
@@ -2003,7 +2040,8 @@ public final class QuotaController extends StateController {
return; return;
} }
TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount); TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount);
saveTimingSession(mPkg.userId, mPkg.packageName, ts, !mRegularJobTimer); saveTimingSession(mPkg.userId, mPkg.packageName, ts, !mRegularJobTimer,
mDebitAdjustment);
mBgJobCount = 0; mBgJobCount = 0;
// Don't reset the tracked jobs list as we need to keep tracking the current number // Don't reset the tracked jobs list as we need to keep tracking the current number
// of jobs. // of jobs.
@@ -2030,7 +2068,7 @@ public final class QuotaController extends StateController {
long getCurrentDuration(long nowElapsed) { long getCurrentDuration(long nowElapsed) {
synchronized (mLock) { synchronized (mLock) {
return !isActive() ? 0 : nowElapsed - mStartTimeElapsed; return !isActive() ? 0 : nowElapsed - mStartTimeElapsed + mDebitAdjustment;
} }
} }
@@ -2059,6 +2097,7 @@ public final class QuotaController extends StateController {
// Start timing from unplug. // Start timing from unplug.
if (mRunningBgJobs.size() > 0) { if (mRunningBgJobs.size() > 0) {
mStartTimeElapsed = nowElapsed; mStartTimeElapsed = nowElapsed;
mDebitAdjustment = 0;
// NOTE: this does have the unfortunate consequence that if the device is // NOTE: this does have the unfortunate consequence that if the device is
// repeatedly plugged in and unplugged, or an app changes foreground state // repeatedly plugged in and unplugged, or an app changes foreground state
// very frequently, the job count for a package may be artificially high. // very frequently, the job count for a package may be artificially high.
@@ -2128,6 +2167,11 @@ public final class QuotaController extends StateController {
pw.print(", "); pw.print(", ");
pw.print(mBgJobCount); pw.print(mBgJobCount);
pw.print(" running bg jobs"); pw.print(" running bg jobs");
if (!mRegularJobTimer) {
pw.print(" (debit adj=");
pw.print(mDebitAdjustment);
pw.print(")");
}
pw.println(); pw.println();
pw.increaseIndent(); pw.increaseIndent();
for (int i = 0; i < mRunningBgJobs.size(); i++) { for (int i = 0; i < mRunningBgJobs.size(); i++) {
@@ -2171,6 +2215,21 @@ public final class QuotaController extends StateController {
mPkg = new Package(userId, packageName); mPkg = new Package(userId, packageName);
} }
private int calculateTimeChunks(final long nowElapsed) {
final long totalTopTimeMs = nowElapsed - mStartTimeElapsed;
int numTimeChunks = (int) (totalTopTimeMs / mEJTopAppTimeChunkSizeMs);
final long remainderMs = totalTopTimeMs % mEJTopAppTimeChunkSizeMs;
if (remainderMs >= SECOND_IN_MILLIS) {
// "Round up"
numTimeChunks++;
}
return numTimeChunks;
}
long getPendingReward(final long nowElapsed) {
return mEJRewardTopAppMs * calculateTimeChunks(nowElapsed);
}
void processEventLocked(@NonNull UsageEvents.Event event) { void processEventLocked(@NonNull UsageEvents.Event event) {
final long nowElapsed = sElapsedRealtimeClock.millis(); final long nowElapsed = sElapsedRealtimeClock.millis();
switch (event.getEventType()) { switch (event.getEventType()) {
@@ -2186,21 +2245,16 @@ public final class QuotaController extends StateController {
final UsageEvents.Event existingEvent = final UsageEvents.Event existingEvent =
mActivities.removeReturnOld(event.mInstanceId); mActivities.removeReturnOld(event.mInstanceId);
if (existingEvent != null && mActivities.size() == 0) { if (existingEvent != null && mActivities.size() == 0) {
final long totalTopTimeMs = nowElapsed - mStartTimeElapsed; final long pendingReward = getPendingReward(nowElapsed);
int numTimeChunks = (int) (totalTopTimeMs / mEJTopAppTimeChunkSizeMs);
final long remainderMs = totalTopTimeMs % mEJTopAppTimeChunkSizeMs;
if (remainderMs >= SECOND_IN_MILLIS) {
// "Round up"
numTimeChunks++;
}
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, Slog.d(TAG, "Crediting " + mPkg + " " + pendingReward + "ms"
"Crediting " + mPkg + " for " + numTimeChunks + " time chunks"); + " for " + calculateTimeChunks(nowElapsed) + " time chunks");
} }
final ShrinkableDebits quota = final ShrinkableDebits debits =
getEJQuotaLocked(mPkg.userId, mPkg.packageName); getEJDebitsLocked(mPkg.userId, mPkg.packageName);
quota.transactOnDebitsLocked(-mEJRewardTopAppMs * numTimeChunks); if (transactQuotaLocked(mPkg.userId, mPkg.packageName,
if (maybeUpdateConstraintForPkgLocked(nowElapsed, nowElapsed, debits, pendingReward)
&& maybeUpdateConstraintForPkgLocked(nowElapsed,
mPkg.userId, mPkg.packageName)) { mPkg.userId, mPkg.packageName)) {
mStateChangedListener.onControllerStateChanged(); mStateChangedListener.onControllerStateChanged();
} }
@@ -2321,7 +2375,7 @@ public final class QuotaController extends StateController {
*/ */
@Override @Override
public void onUsageEvent(int userId, @NonNull UsageEvents.Event event) { public void onUsageEvent(int userId, @NonNull UsageEvents.Event event) {
mHandler.obtainMessage(MSG_PROCESS_USAGE_EVENT, userId, 0, event); mHandler.obtainMessage(MSG_PROCESS_USAGE_EVENT, userId, 0, event).sendToTarget();
} }
} }
@@ -2408,7 +2462,6 @@ public final class QuotaController extends StateController {
} }
private class QcHandler extends Handler { private class QcHandler extends Handler {
private boolean mIsProcessing;
QcHandler(Looper looper) { QcHandler(Looper looper) {
super(looper); super(looper);
@@ -2417,8 +2470,6 @@ public final class QuotaController extends StateController {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
synchronized (mLock) { synchronized (mLock) {
mIsProcessing = true;
switch (msg.what) { switch (msg.what) {
case MSG_REACHED_QUOTA: { case MSG_REACHED_QUOTA: {
Package pkg = (Package) msg.obj; Package pkg = (Package) msg.obj;
@@ -2539,6 +2590,10 @@ public final class QuotaController extends StateController {
final int userId = msg.arg1; final int userId = msg.arg1;
final UsageEvents.Event event = (UsageEvents.Event) msg.obj; final UsageEvents.Event event = (UsageEvents.Event) msg.obj;
final String pkgName = event.getPackageName(); final String pkgName = event.getPackageName();
if (DEBUG) {
Slog.d(TAG, "Processing event " + event.getEventType()
+ " for " + string(userId, pkgName));
}
switch (event.getEventType()) { switch (event.getEventType()) {
case UsageEvents.Event.ACTIVITY_RESUMED: case UsageEvents.Event.ACTIVITY_RESUMED:
case UsageEvents.Event.ACTIVITY_PAUSED: case UsageEvents.Event.ACTIVITY_PAUSED:
@@ -2604,8 +2659,6 @@ public final class QuotaController extends StateController {
} }
} }
} }
mIsProcessing = false;
} }
} }
@@ -3883,11 +3936,6 @@ public final class QuotaController extends StateController {
return mQcConstants; return mQcConstants;
} }
@VisibleForTesting
boolean isActiveBackgroundProcessing() {
return mHandler.mIsProcessing;
}
//////////////////////////// DATA DUMP ////////////////////////////// //////////////////////////// DATA DUMP //////////////////////////////
@Override @Override

View File

@@ -31,6 +31,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.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.WORKING_INDEX; import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sSystemClock;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -58,6 +59,7 @@ import android.app.AppGlobals;
import android.app.IActivityManager; import android.app.IActivityManager;
import android.app.IUidObserver; import android.app.IUidObserver;
import android.app.job.JobInfo; import android.app.job.JobInfo;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal; import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
@@ -85,6 +87,7 @@ import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobStore; import com.android.server.job.JobStore;
import com.android.server.job.controllers.QuotaController.ExecutionStats; import com.android.server.job.controllers.QuotaController.ExecutionStats;
import com.android.server.job.controllers.QuotaController.QcConstants; import com.android.server.job.controllers.QuotaController.QcConstants;
import com.android.server.job.controllers.QuotaController.ShrinkableDebits;
import com.android.server.job.controllers.QuotaController.TimingSession; import com.android.server.job.controllers.QuotaController.TimingSession;
import com.android.server.usage.AppStandbyInternal; import com.android.server.usage.AppStandbyInternal;
@@ -125,6 +128,7 @@ public class QuotaControllerTest {
private int mSourceUid; private int mSourceUid;
private PowerAllowlistInternal.TempAllowlistChangeListener mTempAllowlistListener; private PowerAllowlistInternal.TempAllowlistChangeListener mTempAllowlistListener;
private IUidObserver mUidObserver; private IUidObserver mUidObserver;
private UsageStatsManagerInternal.UsageEventListener mUsageEventListener;
DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder; DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
private MockitoSession mMockingSession; private MockitoSession mMockingSession;
@@ -218,6 +222,8 @@ public class QuotaControllerTest {
ArgumentCaptor.forClass(IUidObserver.class); ArgumentCaptor.forClass(IUidObserver.class);
ArgumentCaptor<PowerAllowlistInternal.TempAllowlistChangeListener> taChangeCaptor = ArgumentCaptor<PowerAllowlistInternal.TempAllowlistChangeListener> taChangeCaptor =
ArgumentCaptor.forClass(PowerAllowlistInternal.TempAllowlistChangeListener.class); ArgumentCaptor.forClass(PowerAllowlistInternal.TempAllowlistChangeListener.class);
ArgumentCaptor<UsageStatsManagerInternal.UsageEventListener> ueListenerCaptor =
ArgumentCaptor.forClass(UsageStatsManagerInternal.UsageEventListener.class);
mQuotaController = new QuotaController(mJobSchedulerService, mQuotaController = new QuotaController(mJobSchedulerService,
mock(BackgroundJobsController.class), mock(ConnectivityController.class)); mock(BackgroundJobsController.class), mock(ConnectivityController.class));
@@ -229,6 +235,8 @@ public class QuotaControllerTest {
verify(mPowerAllowlistInternal) verify(mPowerAllowlistInternal)
.registerTempAllowlistChangeListener(taChangeCaptor.capture()); .registerTempAllowlistChangeListener(taChangeCaptor.capture());
mTempAllowlistListener = taChangeCaptor.getValue(); mTempAllowlistListener = taChangeCaptor.getValue();
verify(mUsageStatsManager).registerListener(ueListenerCaptor.capture());
mUsageEventListener = ueListenerCaptor.getValue();
try { try {
verify(activityManager).registerUidObserver( verify(activityManager).registerUidObserver(
uidObserverCaptor.capture(), uidObserverCaptor.capture(),
@@ -288,7 +296,7 @@ public class QuotaControllerTest {
verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1)).delete(eq(uid)); verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1)).delete(eq(uid));
assertFalse(foregroundUids.get(uid)); assertFalse(foregroundUids.get(uid));
} }
waitForQuietBackground(); waitForNonDelayedMessagesProcessed();
} catch (Exception e) { } catch (Exception e) {
fail("exception encountered: " + e.getMessage()); fail("exception encountered: " + e.getMessage());
} }
@@ -385,13 +393,8 @@ public class QuotaControllerTest {
} }
} }
private void waitForQuietBackground() throws Exception { private void waitForNonDelayedMessagesProcessed() {
for (int i = 0; i < 5; ++i) { mQuotaController.getHandler().runWithScissors(() -> {}, 15_000);
if (!mQuotaController.isActiveBackgroundProcessing()) {
break;
}
Thread.sleep(500);
}
} }
@Test @Test
@@ -1330,7 +1333,9 @@ public class QuotaControllerTest {
timeUsedMs, 5), true); timeUsedMs, 5), true);
JobStatus job = createExpeditedJobStatus("testGetMaxJobExecutionTimeLocked_EJ", 0); JobStatus job = createExpeditedJobStatus("testGetMaxJobExecutionTimeLocked_EJ", 0);
setStandbyBucket(RARE_INDEX, job); setStandbyBucket(RARE_INDEX, job);
mQuotaController.maybeStartTrackingJobLocked(job, null); synchronized (mQuotaController.mLock) {
mQuotaController.maybeStartTrackingJobLocked(job, null);
}
setCharging(); setCharging();
synchronized (mQuotaController.mLock) { synchronized (mQuotaController.mLock) {
@@ -5158,4 +5163,257 @@ public class QuotaControllerTest {
eq(10 * SECOND_IN_MILLIS)); eq(10 * SECOND_IN_MILLIS));
verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs)); verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs));
} }
@Test
public void testEJDebitTallying() {
setStandbyBucket(RARE_INDEX);
setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_RARE_MS, 10 * MINUTE_IN_MILLIS);
// 15 seconds for each 30 second chunk.
setDeviceConfigLong(QcConstants.KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS, 30 * SECOND_IN_MILLIS);
setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_TOP_APP_MS, 15 * SECOND_IN_MILLIS);
// No history. Debits should be 0.
ShrinkableDebits debit = mQuotaController.getEJDebitsLocked(SOURCE_USER_ID, SOURCE_PACKAGE);
assertEquals(0, debit.getTallyLocked());
assertEquals(10 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// Regular job shouldn't affect EJ tally.
JobStatus regJob = createJobStatus("testEJDebitTallying", 1);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStartTrackingJobLocked(regJob, null);
mQuotaController.prepareForExecutionLocked(regJob);
}
advanceElapsedClock(5000);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStopTrackingJobLocked(regJob, null, false);
}
assertEquals(0, debit.getTallyLocked());
assertEquals(10 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// EJ job should affect EJ tally.
JobStatus eJob = createExpeditedJobStatus("testEJDebitTallying", 2);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStartTrackingJobLocked(eJob, null);
mQuotaController.prepareForExecutionLocked(eJob);
}
advanceElapsedClock(5 * MINUTE_IN_MILLIS);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStopTrackingJobLocked(eJob, null, false);
}
assertEquals(5 * MINUTE_IN_MILLIS, debit.getTallyLocked());
assertEquals(5 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// Instantaneous event for a different user shouldn't affect tally.
advanceElapsedClock(5 * MINUTE_IN_MILLIS);
setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_INTERACTION_MS, MINUTE_IN_MILLIS);
UsageEvents.Event event =
new UsageEvents.Event(UsageEvents.Event.USER_INTERACTION, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID + 10, event);
assertEquals(5 * MINUTE_IN_MILLIS, debit.getTallyLocked());
// Instantaneous event for correct user should reduce tally.
advanceElapsedClock(5 * MINUTE_IN_MILLIS);
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
assertEquals(4 * MINUTE_IN_MILLIS, debit.getTallyLocked());
assertEquals(6 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// Activity start shouldn't reduce tally, but duration with activity started should affect
// remaining EJ time.
advanceElapsedClock(5 * MINUTE_IN_MILLIS);
event = new UsageEvents.Event(UsageEvents.Event.ACTIVITY_RESUMED, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
advanceElapsedClock(30 * SECOND_IN_MILLIS);
assertEquals(4 * MINUTE_IN_MILLIS, debit.getTallyLocked());
assertEquals(6 * MINUTE_IN_MILLIS + 15 * SECOND_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(30 * SECOND_IN_MILLIS);
assertEquals(4 * MINUTE_IN_MILLIS, debit.getTallyLocked());
assertEquals(6 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// With activity pausing/stopping/destroying, tally should be updated.
advanceElapsedClock(MINUTE_IN_MILLIS);
event = new UsageEvents.Event(UsageEvents.Event.ACTIVITY_DESTROYED, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
assertEquals(3 * MINUTE_IN_MILLIS, debit.getTallyLocked());
assertEquals(7 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
}
@Test
public void testEJDebitTallying_StaleSession() {
setStandbyBucket(RARE_INDEX);
setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_RARE_MS, 10 * MINUTE_IN_MILLIS);
final long nowElapsed = sElapsedRealtimeClock.millis();
TimingSession ts = new TimingSession(nowElapsed, nowElapsed + 10 * MINUTE_IN_MILLIS, 5);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, ts, true);
// Make the session stale.
advanceElapsedClock(12 * MINUTE_IN_MILLIS + mQcConstants.EJ_WINDOW_SIZE_MS);
// With lazy deletion, we don't update the tally until getRemainingEJExecutionTimeLocked()
// is called, so call that first.
assertEquals(10 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
ShrinkableDebits debit = mQuotaController.getEJDebitsLocked(SOURCE_USER_ID, SOURCE_PACKAGE);
assertEquals(0, debit.getTallyLocked());
}
/**
* Tests that rewards are properly accounted when there's no EJ running and the rewards exceed
* the accumulated debits.
*/
@Test
public void testEJDebitTallying_RewardExceedDebits_NoActiveSession() {
setStandbyBucket(WORKING_INDEX);
setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_WORKING_MS, 30 * MINUTE_IN_MILLIS);
setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_INTERACTION_MS, MINUTE_IN_MILLIS);
final long nowElapsed = sElapsedRealtimeClock.millis();
TimingSession ts = new TimingSession(nowElapsed - 5 * MINUTE_IN_MILLIS,
nowElapsed - 4 * MINUTE_IN_MILLIS, 2);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, ts, true);
ShrinkableDebits debit = mQuotaController.getEJDebitsLocked(SOURCE_USER_ID, SOURCE_PACKAGE);
assertEquals(MINUTE_IN_MILLIS, debit.getTallyLocked());
assertEquals(29 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(30 * SECOND_IN_MILLIS);
UsageEvents.Event event =
new UsageEvents.Event(UsageEvents.Event.USER_INTERACTION, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
assertEquals(0, debit.getTallyLocked());
assertEquals(30 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(MINUTE_IN_MILLIS);
assertEquals(0, debit.getTallyLocked());
assertEquals(30 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// Excessive rewards don't increase maximum quota.
event = new UsageEvents.Event(UsageEvents.Event.USER_INTERACTION, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
assertEquals(0, debit.getTallyLocked());
assertEquals(30 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
}
/**
* Tests that rewards are properly accounted when there's an active EJ running and the rewards
* exceed the accumulated debits.
*/
@Test
public void testEJDebitTallying_RewardExceedDebits_ActiveSession() {
setStandbyBucket(WORKING_INDEX);
setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_WORKING_MS, 30 * MINUTE_IN_MILLIS);
setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_INTERACTION_MS, MINUTE_IN_MILLIS);
// 15 seconds for each 30 second chunk.
setDeviceConfigLong(QcConstants.KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS, 30 * SECOND_IN_MILLIS);
setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_TOP_APP_MS, 15 * SECOND_IN_MILLIS);
final long nowElapsed = sElapsedRealtimeClock.millis();
TimingSession ts = new TimingSession(nowElapsed - 5 * MINUTE_IN_MILLIS,
nowElapsed - 4 * MINUTE_IN_MILLIS, 2);
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, ts, true);
ShrinkableDebits debit = mQuotaController.getEJDebitsLocked(SOURCE_USER_ID, SOURCE_PACKAGE);
assertEquals(MINUTE_IN_MILLIS, debit.getTallyLocked());
assertEquals(29 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// With rewards coming in while an EJ is running, the remaining execution time should be
// adjusted accordingly (decrease due to EJ running + increase from reward).
JobStatus eJob =
createExpeditedJobStatus("testEJDebitTallying_RewardExceedDebits_ActiveSession", 1);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStartTrackingJobLocked(eJob, null);
mQuotaController.prepareForExecutionLocked(eJob);
}
advanceElapsedClock(30 * SECOND_IN_MILLIS);
assertEquals(MINUTE_IN_MILLIS, debit.getTallyLocked());
assertEquals(28 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(30 * SECOND_IN_MILLIS);
UsageEvents.Event event =
new UsageEvents.Event(UsageEvents.Event.USER_INTERACTION, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
assertEquals(0, debit.getTallyLocked());
assertEquals(29 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(MINUTE_IN_MILLIS);
assertEquals(0, debit.getTallyLocked());
assertEquals(28 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// Activity start shouldn't reduce tally, but duration with activity started should affect
// remaining EJ time.
event = new UsageEvents.Event(UsageEvents.Event.ACTIVITY_RESUMED, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
advanceElapsedClock(30 * SECOND_IN_MILLIS);
assertEquals(0, debit.getTallyLocked());
// Decrease by 30 seconds for running EJ, increase by 15 seconds due to ongoing activity.
assertEquals(27 * MINUTE_IN_MILLIS + 45 * SECOND_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(30 * SECOND_IN_MILLIS);
assertEquals(0, debit.getTallyLocked());
assertEquals(27 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(MINUTE_IN_MILLIS);
assertEquals(0, debit.getTallyLocked());
assertEquals(27 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
event = new UsageEvents.Event(UsageEvents.Event.USER_INTERACTION, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
assertEquals(0, debit.getTallyLocked());
assertEquals(28 * MINUTE_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(MINUTE_IN_MILLIS);
assertEquals(0, debit.getTallyLocked());
assertEquals(27 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
// At this point, with activity pausing/stopping/destroying, since we're giving a reward,
// tally should remain 0, and time remaining shouldn't change since it was accounted for
// at every step.
event = new UsageEvents.Event(UsageEvents.Event.ACTIVITY_DESTROYED, sSystemClock.millis());
event.mPackage = SOURCE_PACKAGE;
mUsageEventListener.onUsageEvent(SOURCE_USER_ID, event);
waitForNonDelayedMessagesProcessed();
assertEquals(0, debit.getTallyLocked());
assertEquals(27 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS,
mQuotaController.getRemainingEJExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
}
} }