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
private ShrinkableDebits getEJQuotaLocked(final int userId,
@NonNull final String packageName) {
@VisibleForTesting
ShrinkableDebits getEJDebitsLocked(final int userId, @NonNull final String packageName) {
ShrinkableDebits debits = mEJStats.get(userId, packageName);
if (debits == null) {
debits = new ShrinkableDebits(
@@ -931,7 +931,7 @@ public final class QuotaController extends StateController {
@VisibleForTesting
long getRemainingEJExecutionTimeLocked(final int userId, @NonNull final String packageName) {
ShrinkableDebits quota = getEJQuotaLocked(userId, packageName);
ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
if (quota.getStandbyBucketLocked() == NEVER_INDEX) {
return 0;
}
@@ -948,7 +948,7 @@ public final class QuotaController extends StateController {
if (ts.endTimeElapsed < windowStartTimeElapsed) {
final long duration = ts.endTimeElapsed - ts.startTimeElapsed;
remainingMs += duration;
quota.transactOnDebitsLocked(-duration);
quota.transactLocked(-duration);
timingSessions.remove(0);
} else if (ts.startTimeElapsed < windowStartTimeElapsed) {
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);
if (timer == null) {
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());
}
@@ -1081,7 +1082,7 @@ public final class QuotaController extends StateController {
}
final long nowElapsed = sElapsedRealtimeClock.millis();
ShrinkableDebits quota = getEJQuotaLocked(userId, packageName);
ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
final long limitMs = getEJLimitMsLocked(packageName, quota.getStandbyBucketLocked());
final long startWindowElapsed = Math.max(0, nowElapsed - mEJLimitWindowSizeMs);
long remainingDeadSpaceMs = remainingExecutionTimeMs;
@@ -1372,6 +1373,11 @@ public final class QuotaController extends StateController {
@VisibleForTesting
void saveTimingSession(final int userId, @NonNull final String packageName,
@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) {
final SparseArrayMap<String, List<TimingSession>> sessionMap =
isExpedited ? mEJTimingSessions : mTimingSessions;
@@ -1382,8 +1388,9 @@ public final class QuotaController extends StateController {
}
sessions.add(session);
if (isExpedited) {
final ShrinkableDebits quota = getEJQuotaLocked(userId, packageName);
quota.transactOnDebitsLocked(session.endTimeElapsed - session.startTimeElapsed);
final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
quota.transactLocked(session.endTimeElapsed - session.startTimeElapsed
+ debitAdjustment);
} else {
// Adding a new session means that the current stats are now incorrect.
invalidateAllExecutionStatsLocked(userId, packageName);
@@ -1396,15 +1403,34 @@ public final class QuotaController extends StateController {
private void grantRewardForInstantEvent(
final int userId, @NonNull final String packageName, final long credit) {
synchronized (mLock) {
final ShrinkableDebits quota = getEJQuotaLocked(userId, packageName);
quota.transactOnDebitsLocked(-credit);
if (maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(),
userId, packageName)) {
final long nowElapsed = sElapsedRealtimeClock.millis();
final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
if (transactQuotaLocked(userId, packageName, nowElapsed, quota, credit)
&& maybeUpdateConstraintForPkgLocked(nowElapsed, userId, packageName)) {
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>> {
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. */
private long mDebitTally;
private int mStandbyBucket;
@@ -1893,8 +1920,11 @@ public final class QuotaController extends StateController {
* Negative if the tally should decrease (therefore increasing 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);
return leftover;
}
void setStandbyBucketLocked(int standbyBucket) {
@@ -1927,6 +1957,7 @@ public final class QuotaController extends StateController {
private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>();
private long mStartTimeElapsed;
private int mBgJobCount;
private long mDebitAdjustment;
Timer(int uid, int userId, String packageName, boolean regularJobTimer) {
mPkg = new Package(userId, packageName);
@@ -1957,6 +1988,7 @@ public final class QuotaController extends StateController {
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis();
mDebitAdjustment = 0;
if (mRegularJobTimer) {
// Starting the timer means that all cached execution stats are now
// 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
* the Timer is not going to be used anymore.
@@ -2003,7 +2040,8 @@ public final class QuotaController extends StateController {
return;
}
TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount);
saveTimingSession(mPkg.userId, mPkg.packageName, ts, !mRegularJobTimer);
saveTimingSession(mPkg.userId, mPkg.packageName, ts, !mRegularJobTimer,
mDebitAdjustment);
mBgJobCount = 0;
// Don't reset the tracked jobs list as we need to keep tracking the current number
// of jobs.
@@ -2030,7 +2068,7 @@ public final class QuotaController extends StateController {
long getCurrentDuration(long nowElapsed) {
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.
if (mRunningBgJobs.size() > 0) {
mStartTimeElapsed = nowElapsed;
mDebitAdjustment = 0;
// NOTE: this does have the unfortunate consequence that if the device is
// repeatedly plugged in and unplugged, or an app changes foreground state
// 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(mBgJobCount);
pw.print(" running bg jobs");
if (!mRegularJobTimer) {
pw.print(" (debit adj=");
pw.print(mDebitAdjustment);
pw.print(")");
}
pw.println();
pw.increaseIndent();
for (int i = 0; i < mRunningBgJobs.size(); i++) {
@@ -2171,6 +2215,21 @@ public final class QuotaController extends StateController {
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) {
final long nowElapsed = sElapsedRealtimeClock.millis();
switch (event.getEventType()) {
@@ -2186,21 +2245,16 @@ public final class QuotaController extends StateController {
final UsageEvents.Event existingEvent =
mActivities.removeReturnOld(event.mInstanceId);
if (existingEvent != null && mActivities.size() == 0) {
final long totalTopTimeMs = nowElapsed - mStartTimeElapsed;
int numTimeChunks = (int) (totalTopTimeMs / mEJTopAppTimeChunkSizeMs);
final long remainderMs = totalTopTimeMs % mEJTopAppTimeChunkSizeMs;
if (remainderMs >= SECOND_IN_MILLIS) {
// "Round up"
numTimeChunks++;
}
final long pendingReward = getPendingReward(nowElapsed);
if (DEBUG) {
Slog.d(TAG,
"Crediting " + mPkg + " for " + numTimeChunks + " time chunks");
Slog.d(TAG, "Crediting " + mPkg + " " + pendingReward + "ms"
+ " for " + calculateTimeChunks(nowElapsed) + " time chunks");
}
final ShrinkableDebits quota =
getEJQuotaLocked(mPkg.userId, mPkg.packageName);
quota.transactOnDebitsLocked(-mEJRewardTopAppMs * numTimeChunks);
if (maybeUpdateConstraintForPkgLocked(nowElapsed,
final ShrinkableDebits debits =
getEJDebitsLocked(mPkg.userId, mPkg.packageName);
if (transactQuotaLocked(mPkg.userId, mPkg.packageName,
nowElapsed, debits, pendingReward)
&& maybeUpdateConstraintForPkgLocked(nowElapsed,
mPkg.userId, mPkg.packageName)) {
mStateChangedListener.onControllerStateChanged();
}
@@ -2321,7 +2375,7 @@ public final class QuotaController extends StateController {
*/
@Override
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 boolean mIsProcessing;
QcHandler(Looper looper) {
super(looper);
@@ -2417,8 +2470,6 @@ public final class QuotaController extends StateController {
@Override
public void handleMessage(Message msg) {
synchronized (mLock) {
mIsProcessing = true;
switch (msg.what) {
case MSG_REACHED_QUOTA: {
Package pkg = (Package) msg.obj;
@@ -2539,6 +2590,10 @@ public final class QuotaController extends StateController {
final int userId = msg.arg1;
final UsageEvents.Event event = (UsageEvents.Event) msg.obj;
final String pkgName = event.getPackageName();
if (DEBUG) {
Slog.d(TAG, "Processing event " + event.getEventType()
+ " for " + string(userId, pkgName));
}
switch (event.getEventType()) {
case UsageEvents.Event.ACTIVITY_RESUMED:
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;
}
@VisibleForTesting
boolean isActiveBackgroundProcessing() {
return mHandler.mIsProcessing;
}
//////////////////////////// DATA DUMP //////////////////////////////
@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.WORKING_INDEX;
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.assertFalse;
@@ -58,6 +59,7 @@ import android.app.AppGlobals;
import android.app.IActivityManager;
import android.app.IUidObserver;
import android.app.job.JobInfo;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
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.controllers.QuotaController.ExecutionStats;
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.usage.AppStandbyInternal;
@@ -125,6 +128,7 @@ public class QuotaControllerTest {
private int mSourceUid;
private PowerAllowlistInternal.TempAllowlistChangeListener mTempAllowlistListener;
private IUidObserver mUidObserver;
private UsageStatsManagerInternal.UsageEventListener mUsageEventListener;
DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
private MockitoSession mMockingSession;
@@ -218,6 +222,8 @@ public class QuotaControllerTest {
ArgumentCaptor.forClass(IUidObserver.class);
ArgumentCaptor<PowerAllowlistInternal.TempAllowlistChangeListener> taChangeCaptor =
ArgumentCaptor.forClass(PowerAllowlistInternal.TempAllowlistChangeListener.class);
ArgumentCaptor<UsageStatsManagerInternal.UsageEventListener> ueListenerCaptor =
ArgumentCaptor.forClass(UsageStatsManagerInternal.UsageEventListener.class);
mQuotaController = new QuotaController(mJobSchedulerService,
mock(BackgroundJobsController.class), mock(ConnectivityController.class));
@@ -229,6 +235,8 @@ public class QuotaControllerTest {
verify(mPowerAllowlistInternal)
.registerTempAllowlistChangeListener(taChangeCaptor.capture());
mTempAllowlistListener = taChangeCaptor.getValue();
verify(mUsageStatsManager).registerListener(ueListenerCaptor.capture());
mUsageEventListener = ueListenerCaptor.getValue();
try {
verify(activityManager).registerUidObserver(
uidObserverCaptor.capture(),
@@ -288,7 +296,7 @@ public class QuotaControllerTest {
verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1)).delete(eq(uid));
assertFalse(foregroundUids.get(uid));
}
waitForQuietBackground();
waitForNonDelayedMessagesProcessed();
} catch (Exception e) {
fail("exception encountered: " + e.getMessage());
}
@@ -385,13 +393,8 @@ public class QuotaControllerTest {
}
}
private void waitForQuietBackground() throws Exception {
for (int i = 0; i < 5; ++i) {
if (!mQuotaController.isActiveBackgroundProcessing()) {
break;
}
Thread.sleep(500);
}
private void waitForNonDelayedMessagesProcessed() {
mQuotaController.getHandler().runWithScissors(() -> {}, 15_000);
}
@Test
@@ -1330,7 +1333,9 @@ public class QuotaControllerTest {
timeUsedMs, 5), true);
JobStatus job = createExpeditedJobStatus("testGetMaxJobExecutionTimeLocked_EJ", 0);
setStandbyBucket(RARE_INDEX, job);
mQuotaController.maybeStartTrackingJobLocked(job, null);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStartTrackingJobLocked(job, null);
}
setCharging();
synchronized (mQuotaController.mLock) {
@@ -5158,4 +5163,257 @@ public class QuotaControllerTest {
eq(10 * SECOND_IN_MILLIS));
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));
}
}