Implement TIP2.

Using a rolling window to track reward limits means having no limit on
the number of events that need to be tracked. Switching to buckets for
reward tracking places a cap on TARE's memory usage per app at the cost
of some fidelity.

Bug: 239951405
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/tare
Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/tare
Change-Id: I71beaaf0e2b3fb809314122dc059d1451f080d01
This commit is contained in:
Kweku Adams
2022-07-22 17:47:29 +00:00
parent 826ebae0c5
commit 384f123c49
5 changed files with 629 additions and 92 deletions

View File

@@ -306,6 +306,10 @@ public abstract class EconomicPolicy {
return eventId & MASK_TYPE;
}
static boolean isReward(int eventId) {
return getEventType(eventId) == TYPE_REWARD;
}
@NonNull
static String eventToString(int eventId) {
switch (eventId & MASK_TYPE) {

View File

@@ -22,10 +22,14 @@ import static com.android.server.tare.TareUtils.cakeToString;
import static com.android.server.tare.TareUtils.dumpTime;
import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.IndentingPrintWriter;
import android.util.SparseLongArray;
import android.util.TimeUtils;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
@@ -34,6 +38,21 @@ import java.util.List;
* Ledger to track the last recorded balance and recent activities of an app.
*/
class Ledger {
/** The window size within which rewards will be counted and used towards reward limiting. */
private static final long TOTAL_REWARD_WINDOW_MS = 24 * HOUR_IN_MILLIS;
/** The number of buckets to split {@link #TOTAL_REWARD_WINDOW_MS} into. */
@VisibleForTesting
static final int NUM_REWARD_BUCKET_WINDOWS = 4;
/**
* The duration size of each bucket resulting from splitting {@link #TOTAL_REWARD_WINDOW_MS}
* into smaller buckets.
*/
private static final long REWARD_BUCKET_WINDOW_SIZE_MS =
TOTAL_REWARD_WINDOW_MS / NUM_REWARD_BUCKET_WINDOWS;
/** The maximum number of transactions to retain in memory at any one time. */
@VisibleForTesting
static final int MAX_TRANSACTION_COUNT = 50;
static class Transaction {
public final long startTimeMs;
public final long endTimeMs;
@@ -54,18 +73,47 @@ class Ledger {
}
}
static class RewardBucket {
@CurrentTimeMillisLong
public long startTimeMs;
public final SparseLongArray cumulativeDelta = new SparseLongArray();
private void reset() {
startTimeMs = 0;
cumulativeDelta.clear();
}
}
/** Last saved balance. This doesn't take currently ongoing events into account. */
private long mCurrentBalance = 0;
private final List<Transaction> mTransactions = new ArrayList<>();
private final SparseLongArray mCumulativeDeltaPerReason = new SparseLongArray();
private long mEarliestSumTime;
private final Transaction[] mTransactions = new Transaction[MAX_TRANSACTION_COUNT];
/** Index within {@link #mTransactions} where the next transaction should be placed. */
private int mTransactionIndex = 0;
private final RewardBucket[] mRewardBuckets = new RewardBucket[NUM_REWARD_BUCKET_WINDOWS];
/** Index within {@link #mRewardBuckets} of the current active bucket. */
private int mRewardBucketIndex = 0;
Ledger() {
}
Ledger(long currentBalance, @NonNull List<Transaction> transactions) {
Ledger(long currentBalance, @NonNull List<Transaction> transactions,
@NonNull List<RewardBucket> rewardBuckets) {
mCurrentBalance = currentBalance;
mTransactions.addAll(transactions);
final int numTxs = transactions.size();
for (int i = Math.max(0, numTxs - MAX_TRANSACTION_COUNT); i < numTxs; ++i) {
mTransactions[mTransactionIndex++] = transactions.get(i);
}
mTransactionIndex %= MAX_TRANSACTION_COUNT;
final int numBuckets = rewardBuckets.size();
if (numBuckets > 0) {
// Set the index to -1 so that we put the first bucket in index 0.
mRewardBucketIndex = -1;
for (int i = Math.max(0, numBuckets - NUM_REWARD_BUCKET_WINDOWS); i < numBuckets; ++i) {
mRewardBuckets[++mRewardBucketIndex] = rewardBuckets.get(i);
}
}
}
long getCurrentBalance() {
@@ -74,66 +122,142 @@ class Ledger {
@Nullable
Transaction getEarliestTransaction() {
if (mTransactions.size() > 0) {
return mTransactions.get(0);
for (int t = 0; t < mTransactions.length; ++t) {
final Transaction transaction =
mTransactions[(mTransactionIndex + t) % mTransactions.length];
if (transaction != null) {
return transaction;
}
}
return null;
}
@NonNull
List<RewardBucket> getRewardBuckets() {
final long cutoffMs = getCurrentTimeMillis() - TOTAL_REWARD_WINDOW_MS;
final List<RewardBucket> list = new ArrayList<>(NUM_REWARD_BUCKET_WINDOWS);
for (int i = 1; i <= NUM_REWARD_BUCKET_WINDOWS; ++i) {
final int idx = (mRewardBucketIndex + i) % NUM_REWARD_BUCKET_WINDOWS;
final RewardBucket rewardBucket = mRewardBuckets[idx];
if (rewardBucket != null) {
if (cutoffMs <= rewardBucket.startTimeMs) {
list.add(rewardBucket);
} else {
rewardBucket.reset();
}
}
}
return list;
}
@NonNull
List<Transaction> getTransactions() {
return mTransactions;
final List<Transaction> list = new ArrayList<>(MAX_TRANSACTION_COUNT);
for (int i = 0; i < MAX_TRANSACTION_COUNT; ++i) {
final int idx = (mTransactionIndex + i) % MAX_TRANSACTION_COUNT;
final Transaction transaction = mTransactions[idx];
if (transaction != null) {
list.add(transaction);
}
}
return list;
}
void recordTransaction(@NonNull Transaction transaction) {
mTransactions.add(transaction);
mTransactions[mTransactionIndex] = transaction;
mCurrentBalance += transaction.delta;
mTransactionIndex = (mTransactionIndex + 1) % MAX_TRANSACTION_COUNT;
final long sum = mCumulativeDeltaPerReason.get(transaction.eventId);
mCumulativeDeltaPerReason.put(transaction.eventId, sum + transaction.delta);
mEarliestSumTime = Math.min(mEarliestSumTime, transaction.startTimeMs);
if (EconomicPolicy.isReward(transaction.eventId)) {
final RewardBucket bucket = getCurrentRewardBucket();
bucket.cumulativeDelta.put(transaction.eventId,
bucket.cumulativeDelta.get(transaction.eventId, 0) + transaction.delta);
}
}
@NonNull
private RewardBucket getCurrentRewardBucket() {
RewardBucket bucket = mRewardBuckets[mRewardBucketIndex];
final long now = getCurrentTimeMillis();
if (bucket == null) {
bucket = new RewardBucket();
bucket.startTimeMs = now;
mRewardBuckets[mRewardBucketIndex] = bucket;
return bucket;
}
if (now - bucket.startTimeMs < REWARD_BUCKET_WINDOW_SIZE_MS) {
return bucket;
}
mRewardBucketIndex = (mRewardBucketIndex + 1) % NUM_REWARD_BUCKET_WINDOWS;
bucket = mRewardBuckets[mRewardBucketIndex];
if (bucket == null) {
bucket = new RewardBucket();
mRewardBuckets[mRewardBucketIndex] = bucket;
}
bucket.reset();
// Using now as the start time means there will be some gaps between sequential buckets,
// but makes processing of large gaps between events easier.
bucket.startTimeMs = now;
return bucket;
}
long get24HourSum(int eventId, final long now) {
final long windowStartTime = now - 24 * HOUR_IN_MILLIS;
if (mEarliestSumTime < windowStartTime) {
// Need to redo sums
mCumulativeDeltaPerReason.clear();
for (int i = mTransactions.size() - 1; i >= 0; --i) {
final Transaction transaction = mTransactions.get(i);
if (transaction.endTimeMs <= windowStartTime) {
break;
}
long sum = mCumulativeDeltaPerReason.get(transaction.eventId);
if (transaction.startTimeMs >= windowStartTime) {
sum += transaction.delta;
} else {
// Pro-rate durationed deltas. Intentionally floor the result.
sum += (long) (1.0 * (transaction.endTimeMs - windowStartTime)
* transaction.delta)
/ (transaction.endTimeMs - transaction.startTimeMs);
}
mCumulativeDeltaPerReason.put(transaction.eventId, sum);
long sum = 0;
for (int i = 0; i < mRewardBuckets.length; ++i) {
final RewardBucket bucket = mRewardBuckets[i];
if (bucket != null
&& bucket.startTimeMs >= windowStartTime && bucket.startTimeMs < now) {
sum += bucket.cumulativeDelta.get(eventId, 0);
}
mEarliestSumTime = windowStartTime;
}
return mCumulativeDeltaPerReason.get(eventId);
return sum;
}
/** Deletes transactions that are older than {@code minAgeMs}. */
void removeOldTransactions(long minAgeMs) {
/**
* Deletes transactions that are older than {@code minAgeMs}.
* @return The earliest transaction in the ledger, or {@code null} if there are no more
* transactions.
*/
@Nullable
Transaction removeOldTransactions(long minAgeMs) {
final long cutoff = getCurrentTimeMillis() - minAgeMs;
while (mTransactions.size() > 0 && mTransactions.get(0).endTimeMs <= cutoff) {
mTransactions.remove(0);
for (int t = 0; t < mTransactions.length; ++t) {
final int idx = (mTransactionIndex + t) % mTransactions.length;
final Transaction transaction = mTransactions[idx];
if (transaction == null) {
continue;
}
if (transaction.endTimeMs <= cutoff) {
mTransactions[idx] = null;
} else {
// Everything we look at after this transaction will also be within the window,
// so no need to go further.
return transaction;
}
}
return null;
}
void dump(IndentingPrintWriter pw, int numRecentTransactions) {
pw.print("Current balance", cakeToString(getCurrentBalance())).println();
pw.println();
final int size = mTransactions.size();
for (int i = Math.max(0, size - numRecentTransactions); i < size; ++i) {
final Transaction transaction = mTransactions.get(i);
boolean printedTransactionTitle = false;
for (int t = 0; t < Math.min(MAX_TRANSACTION_COUNT, numRecentTransactions); ++t) {
final int idx = (mTransactionIndex - t + MAX_TRANSACTION_COUNT) % MAX_TRANSACTION_COUNT;
final Transaction transaction = mTransactions[idx];
if (transaction == null) {
continue;
}
if (!printedTransactionTitle) {
pw.println("Transactions:");
pw.increaseIndent();
printedTransactionTitle = true;
}
dumpTime(pw, transaction.startTimeMs);
pw.print("--");
@@ -151,5 +275,42 @@ class Ledger {
pw.print(cakeToString(transaction.ctp));
pw.println(")");
}
if (printedTransactionTitle) {
pw.decreaseIndent();
pw.println();
}
final long now = getCurrentTimeMillis();
boolean printedBucketTitle = false;
for (int b = 0; b < NUM_REWARD_BUCKET_WINDOWS; ++b) {
final int idx = (mRewardBucketIndex - b + NUM_REWARD_BUCKET_WINDOWS)
% NUM_REWARD_BUCKET_WINDOWS;
final RewardBucket rewardBucket = mRewardBuckets[idx];
if (rewardBucket == null) {
continue;
}
if (!printedBucketTitle) {
pw.println("Reward buckets:");
pw.increaseIndent();
printedBucketTitle = true;
}
dumpTime(pw, rewardBucket.startTimeMs);
pw.print(" (");
TimeUtils.formatDuration(now - rewardBucket.startTimeMs, pw);
pw.println(" ago):");
pw.increaseIndent();
for (int r = 0; r < rewardBucket.cumulativeDelta.size(); ++r) {
pw.print(EconomicPolicy.eventToString(rewardBucket.cumulativeDelta.keyAt(r)));
pw.print(": ");
pw.println(cakeToString(rewardBucket.cumulativeDelta.valueAt(r)));
}
pw.decreaseIndent();
}
if (printedBucketTitle) {
pw.decreaseIndent();
pw.println();
}
}
}

View File

@@ -62,15 +62,14 @@ public class Scribe {
private static final int MAX_NUM_TRANSACTION_DUMP = 25;
/**
* The maximum amount of time we'll keep a transaction around for.
* For now, only keep transactions we actually have a use for. We can increase it if we want
* to use older transactions or provide older transactions to apps.
*/
private static final long MAX_TRANSACTION_AGE_MS = 24 * HOUR_IN_MILLIS;
private static final long MAX_TRANSACTION_AGE_MS = 8 * 24 * HOUR_IN_MILLIS;
private static final String XML_TAG_HIGH_LEVEL_STATE = "irs-state";
private static final String XML_TAG_LEDGER = "ledger";
private static final String XML_TAG_TARE = "tare";
private static final String XML_TAG_TRANSACTION = "transaction";
private static final String XML_TAG_REWARD_BUCKET = "rewardBucket";
private static final String XML_TAG_USER = "user";
private static final String XML_TAG_PERIOD_REPORT = "report";
@@ -346,8 +345,8 @@ public class Scribe {
for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
final String pkgName = mLedgers.keyAt(uIdx, pIdx);
final Ledger ledger = mLedgers.get(userId, pkgName);
ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
Ledger.Transaction transaction = ledger.getEarliestTransaction();
final Ledger.Transaction transaction =
ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
if (transaction != null) {
earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
}
@@ -370,6 +369,7 @@ public class Scribe {
final String pkgName;
final long curBalance;
final List<Ledger.Transaction> transactions = new ArrayList<>();
final List<Ledger.RewardBucket> rewardBuckets = new ArrayList<>();
pkgName = parser.getAttributeValue(null, XML_ATTR_PACKAGE_NAME);
curBalance = parser.getAttributeLong(null, XML_ATTR_CURRENT_BALANCE);
@@ -391,8 +391,7 @@ public class Scribe {
}
continue;
}
if (eventType != XmlPullParser.START_TAG || !XML_TAG_TRANSACTION.equals(tagName)) {
// Expecting only "transaction" tags.
if (eventType != XmlPullParser.START_TAG || tagName == null) {
Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
return null;
}
@@ -402,25 +401,37 @@ public class Scribe {
if (DEBUG) {
Slog.d(TAG, "Starting ledger tag: " + tagName);
}
final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
if (endTime <= endTimeCutoff) {
if (DEBUG) {
Slog.d(TAG, "Skipping event because it's too old.");
}
continue;
switch (tagName) {
case XML_TAG_TRANSACTION:
final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
if (endTime <= endTimeCutoff) {
if (DEBUG) {
Slog.d(TAG, "Skipping event because it's too old.");
}
continue;
}
final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
transactions.add(
new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
break;
case XML_TAG_REWARD_BUCKET:
rewardBuckets.add(readRewardBucketFromXml(parser));
break;
default:
// Expecting only "transaction" and "rewardBucket" tags.
Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
return null;
}
transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
}
if (!isInstalled) {
return null;
}
return Pair.create(pkgName, new Ledger(curBalance, transactions));
return Pair.create(pkgName, new Ledger(curBalance, transactions, rewardBuckets));
}
/**
@@ -508,6 +519,44 @@ public class Scribe {
return report;
}
/**
* @param parser Xml parser at the beginning of a {@value #XML_TAG_REWARD_BUCKET} tag. The next
* "parser.next()" call will take the parser into the body of the tag.
* @return Newly instantiated {@link Ledger.RewardBucket} holding all the information we just
* read out of the xml tag.
*/
@Nullable
private static Ledger.RewardBucket readRewardBucketFromXml(TypedXmlPullParser parser)
throws XmlPullParserException, IOException {
final Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
rewardBucket.startTimeMs = parser.getAttributeLong(null, XML_ATTR_START_TIME);
for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
eventType = parser.next()) {
final String tagName = parser.getName();
if (eventType == XmlPullParser.END_TAG) {
if (XML_TAG_REWARD_BUCKET.equals(tagName)) {
// We've reached the end of the rewardBucket tag.
break;
}
continue;
}
if (eventType != XmlPullParser.START_TAG || !XML_ATTR_DELTA.equals(tagName)) {
// Expecting only delta tags.
Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
return null;
}
final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
rewardBucket.cumulativeDelta.put(eventId, delta);
}
return rewardBucket;
}
private void scheduleCleanup(long earliestEndTime) {
if (earliestEndTime == Long.MAX_VALUE) {
return;
@@ -595,6 +644,11 @@ public class Scribe {
}
writeTransaction(out, transaction);
}
final List<Ledger.RewardBucket> rewardBuckets = ledger.getRewardBuckets();
for (int r = 0; r < rewardBuckets.size(); ++r) {
writeRewardBucket(out, rewardBuckets.get(r));
}
out.endTag(null, XML_TAG_LEDGER);
}
out.endTag(null, XML_TAG_USER);
@@ -616,6 +670,23 @@ public class Scribe {
out.endTag(null, XML_TAG_TRANSACTION);
}
private static void writeRewardBucket(@NonNull TypedXmlSerializer out,
@NonNull Ledger.RewardBucket rewardBucket) throws IOException {
final int numEvents = rewardBucket.cumulativeDelta.size();
if (numEvents == 0) {
return;
}
out.startTag(null, XML_TAG_REWARD_BUCKET);
out.attributeLong(null, XML_ATTR_START_TIME, rewardBucket.startTimeMs);
for (int i = 0; i < numEvents; ++i) {
out.startTag(null, XML_ATTR_DELTA);
out.attributeInt(null, XML_ATTR_EVENT_ID, rewardBucket.cumulativeDelta.keyAt(i));
out.attributeLong(null, XML_ATTR_DELTA, rewardBucket.cumulativeDelta.valueAt(i));
out.endTag(null, XML_ATTR_DELTA);
}
out.endTag(null, XML_TAG_REWARD_BUCKET);
}
private static void writeReport(@NonNull TypedXmlSerializer out,
@NonNull Analyst.Report report) throws IOException {
out.startTag(null, XML_TAG_PERIOD_REPORT);

View File

@@ -30,6 +30,7 @@ import android.content.pm.PackageInfo;
import android.os.UserHandle;
import android.util.Log;
import android.util.SparseArrayMap;
import android.util.SparseLongArray;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -174,10 +175,13 @@ public class ScribeTest {
when(mIrs.getConsumptionLimitLocked()).thenReturn(consumptionLimit);
Ledger ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, 2000, 0));
ledger.recordTransaction(
new Ledger.Transaction(0, 1000L, EconomicPolicy.TYPE_REWARD | 1, null, 2000, 0));
// Negative ledger balance shouldn't affect the total circulation value.
ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID + 1, TEST_PACKAGE);
ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, -5000, 3000));
ledger.recordTransaction(
new Ledger.Transaction(0, 1000L,
EconomicPolicy.TYPE_ACTION | 1, null, -5000, 3000));
mScribeUnderTest.setLastReclamationTimeLocked(lastReclamationTime);
mScribeUnderTest.setConsumptionLimitLocked(consumptionLimit);
mScribeUnderTest.adjustRemainingConsumableCakesLocked(
@@ -209,9 +213,13 @@ public class ScribeTest {
@Test
public void testWritingPopulatedLedgerToDisk() {
final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 0));
ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, -1));
ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 12));
ogLedger.recordTransaction(
new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 0));
ogLedger.recordTransaction(
new Ledger.Transaction(1500, 2000,
EconomicPolicy.TYPE_REWARD | 2, "green", 52, -1));
ogLedger.recordTransaction(
new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_REWARD | 3, "blue", 3, 12));
mScribeUnderTest.writeImmediatelyForTesting();
mScribeUnderTest.loadFromDiskLocked();
@@ -230,11 +238,13 @@ public class ScribeTest {
addInstalledPackage(userId, pkgName);
final Ledger ledger = mScribeUnderTest.getLedgerLocked(userId, pkgName);
ledger.recordTransaction(new Ledger.Transaction(
0, 1000L * u + l, 1, null, -51L * u + l, 50));
0, 1000L * u + l, EconomicPolicy.TYPE_ACTION | 1, null, -51L * u + l, 50));
ledger.recordTransaction(new Ledger.Transaction(
1500L * u + l, 2000L * u + l, 2 * u + l, "green" + u + l, 52L * u + l, 0));
1500L * u + l, 2000L * u + l,
EconomicPolicy.TYPE_REWARD | 2 * u + l, "green" + u + l, 52L * u + l, 0));
ledger.recordTransaction(new Ledger.Transaction(
2500L * u + l, 3000L * u + l, 3 * u + l, "blue" + u + l, 3L * u + l, 0));
2500L * u + l, 3000L * u + l,
EconomicPolicy.TYPE_REWARD | 3 * u + l, "blue" + u + l, 3L * u + l, 0));
ledgers.add(userId, pkgName, ledger);
}
}
@@ -248,9 +258,12 @@ public class ScribeTest {
@Test
public void testDiscardLedgerFromDisk() {
final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 1));
ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 0));
ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 1));
ogLedger.recordTransaction(
new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 1));
ogLedger.recordTransaction(
new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 0));
ogLedger.recordTransaction(
new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_REWARD | 3, "blue", 3, 1));
mScribeUnderTest.writeImmediatelyForTesting();
mScribeUnderTest.loadFromDiskLocked();
@@ -269,9 +282,12 @@ public class ScribeTest {
public void testLoadingMissingPackageFromDisk() {
final String pkgName = TEST_PACKAGE + ".uninstalled";
final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, pkgName);
ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 1));
ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 2));
ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 3));
ogLedger.recordTransaction(
new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION | 1, null, 51, 1));
ogLedger.recordTransaction(
new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 2));
ogLedger.recordTransaction(
new Ledger.Transaction(2500, 3000, EconomicPolicy.TYPE_ACTION | 3, "blue", -3, 3));
mScribeUnderTest.writeImmediatelyForTesting();
// Package isn't installed, so make sure it's not saved to memory after loading.
@@ -283,9 +299,13 @@ public class ScribeTest {
public void testLoadingMissingUserFromDisk() {
final int userId = TEST_USER_ID + 1;
final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(userId, TEST_PACKAGE);
ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 0));
ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 1));
ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 3));
ogLedger.recordTransaction(
new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD | 1, null, 51, 0));
ogLedger.recordTransaction(
new Ledger.Transaction(1500, 2000, EconomicPolicy.TYPE_REWARD | 2, "green", 52, 1));
ogLedger.recordTransaction(
new Ledger.Transaction(2500, 3000,
EconomicPolicy.TYPE_REGULATION | 3, "blue", 3, 3));
mScribeUnderTest.writeImmediatelyForTesting();
// User doesn't show up with any packages, so make sure nothing is saved after loading.
@@ -331,12 +351,34 @@ public class ScribeTest {
}
assertNotNull(actual);
assertEquals(expected.getCurrentBalance(), actual.getCurrentBalance());
List<Ledger.Transaction> expectedTransactions = expected.getTransactions();
List<Ledger.Transaction> actualTransactions = actual.getTransactions();
assertEquals(expectedTransactions.size(), actualTransactions.size());
for (int i = 0; i < expectedTransactions.size(); ++i) {
assertTransactionsEqual(expectedTransactions.get(i), actualTransactions.get(i));
}
List<Ledger.RewardBucket> expectedRewardBuckets = expected.getRewardBuckets();
List<Ledger.RewardBucket> actualRewardBuckets = actual.getRewardBuckets();
assertEquals(expectedRewardBuckets.size(), actualRewardBuckets.size());
for (int i = 0; i < expectedRewardBuckets.size(); ++i) {
assertRewardBucketsEqual(expectedRewardBuckets.get(i), actualRewardBuckets.get(i));
}
}
private void assertSparseLongArraysEqual(SparseLongArray expected, SparseLongArray actual) {
if (expected == null) {
assertNull(actual);
return;
}
assertNotNull(actual);
final int size = expected.size();
assertEquals(size, actual.size());
for (int i = 0; i < size; ++i) {
assertEquals(expected.keyAt(i), actual.keyAt(i));
assertEquals(expected.valueAt(i), actual.valueAt(i));
}
}
private void assertReportListsEqual(List<Analyst.Report> expected,
@@ -382,6 +424,17 @@ public class ScribeTest {
}
}
private void assertRewardBucketsEqual(Ledger.RewardBucket expected,
Ledger.RewardBucket actual) {
if (expected == null) {
assertNull(actual);
return;
}
assertNotNull(actual);
assertEquals(expected.startTimeMs, actual.startTimeMs);
assertSparseLongArraysEqual(expected.cumulativeDelta, actual.cumulativeDelta);
}
private void assertTransactionsEqual(Ledger.Transaction expected, Ledger.Transaction actual) {
if (expected == null) {
assertNull(actual);

View File

@@ -22,7 +22,12 @@ import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.util.SparseLongArray;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -32,7 +37,10 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Clock;
import java.time.Duration;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
/** Test that the ledger records transactions correctly. */
@RunWith(AndroidJUnit4.class)
@@ -44,6 +52,11 @@ public class LedgerTest {
TareUtils.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
}
private void shiftSystemTime(long incrementMs) {
TareUtils.sSystemClock =
Clock.offset(TareUtils.sSystemClock, Duration.ofMillis(incrementMs));
}
@Test
public void testInitialState() {
final Ledger ledger = new Ledger();
@@ -51,38 +64,150 @@ public class LedgerTest {
assertEquals(0, ledger.get24HourSum(0, 0));
}
@Test
public void testInitialization_FullLists() {
final long balance = 1234567890L;
List<Ledger.Transaction> transactions = new ArrayList<>();
List<Ledger.RewardBucket> rewardBuckets = new ArrayList<>();
final long now = getCurrentTimeMillis();
Ledger.Transaction secondTxn = null;
Ledger.RewardBucket remainingBucket = null;
for (int i = 0; i < Ledger.MAX_TRANSACTION_COUNT; ++i) {
final long start = now - 10 * HOUR_IN_MILLIS + i * MINUTE_IN_MILLIS;
Ledger.Transaction transaction = new Ledger.Transaction(
start, start + MINUTE_IN_MILLIS, 1, null, 400, 0);
if (i == 1) {
secondTxn = transaction;
}
transactions.add(transaction);
}
for (int b = 0; b < Ledger.NUM_REWARD_BUCKET_WINDOWS; ++b) {
final long start = now - (Ledger.NUM_REWARD_BUCKET_WINDOWS - b) * 24 * HOUR_IN_MILLIS;
Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
rewardBucket.startTimeMs = start;
for (int r = 0; r < 5; ++r) {
rewardBucket.cumulativeDelta.put(EconomicPolicy.TYPE_REWARD | r, b * start + r);
}
if (b == Ledger.NUM_REWARD_BUCKET_WINDOWS - 1) {
remainingBucket = rewardBucket;
}
rewardBuckets.add(rewardBucket);
}
final Ledger ledger = new Ledger(balance, transactions, rewardBuckets);
assertEquals(balance, ledger.getCurrentBalance());
assertEquals(transactions, ledger.getTransactions());
// Everything but the last bucket is old, so the returned list should only contain that
// bucket.
rewardBuckets.clear();
rewardBuckets.add(remainingBucket);
assertEquals(rewardBuckets, ledger.getRewardBuckets());
// Make sure the ledger can properly record new transactions.
final long start = now - MINUTE_IN_MILLIS;
final long delta = 400;
final Ledger.Transaction transaction = new Ledger.Transaction(
start, start + MINUTE_IN_MILLIS, EconomicPolicy.TYPE_REWARD | 1, null, delta, 0);
ledger.recordTransaction(transaction);
assertEquals(balance + delta, ledger.getCurrentBalance());
transactions = ledger.getTransactions();
assertEquals(secondTxn, transactions.get(0));
assertEquals(transaction, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 1));
final Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
rewardBucket.startTimeMs = now;
rewardBucket.cumulativeDelta.put(EconomicPolicy.TYPE_REWARD | 1, delta);
rewardBuckets = ledger.getRewardBuckets();
assertRewardBucketsEqual(remainingBucket, rewardBuckets.get(0));
assertRewardBucketsEqual(rewardBucket, rewardBuckets.get(1));
}
@Test
public void testInitialization_OverflowingLists() {
final long balance = 1234567890L;
final List<Ledger.Transaction> transactions = new ArrayList<>();
final List<Ledger.RewardBucket> rewardBuckets = new ArrayList<>();
final long now = getCurrentTimeMillis();
for (int i = 0; i < 2 * Ledger.MAX_TRANSACTION_COUNT; ++i) {
final long start = now - 20 * HOUR_IN_MILLIS + i * MINUTE_IN_MILLIS;
Ledger.Transaction transaction = new Ledger.Transaction(
start, start + MINUTE_IN_MILLIS, 1, null, 400, 0);
transactions.add(transaction);
}
for (int b = 0; b < 2 * Ledger.NUM_REWARD_BUCKET_WINDOWS; ++b) {
final long start = now
- (2 * Ledger.NUM_REWARD_BUCKET_WINDOWS - b) * 6 * HOUR_IN_MILLIS;
Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
rewardBucket.startTimeMs = start;
for (int r = 0; r < 5; ++r) {
rewardBucket.cumulativeDelta.put(EconomicPolicy.TYPE_REWARD | r, b * start + r);
}
rewardBuckets.add(rewardBucket);
}
final Ledger ledger = new Ledger(balance, transactions, rewardBuckets);
assertEquals(balance, ledger.getCurrentBalance());
assertEquals(transactions.subList(Ledger.MAX_TRANSACTION_COUNT,
2 * Ledger.MAX_TRANSACTION_COUNT),
ledger.getTransactions());
assertEquals(rewardBuckets.subList(Ledger.NUM_REWARD_BUCKET_WINDOWS,
2 * Ledger.NUM_REWARD_BUCKET_WINDOWS),
ledger.getRewardBuckets());
}
@Test
public void testMultipleTransactions() {
final Ledger ledger = new Ledger();
ledger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 5, 0));
assertEquals(5, ledger.getCurrentBalance());
assertEquals(5, ledger.get24HourSum(1, 60_000));
ledger.recordTransaction(new Ledger.Transaction(2000, 2000, 1, null, 25, 0));
assertEquals(30, ledger.getCurrentBalance());
assertEquals(30, ledger.get24HourSum(1, 60_000));
ledger.recordTransaction(new Ledger.Transaction(5000, 5500, 1, null, -10, 5));
assertEquals(20, ledger.getCurrentBalance());
assertEquals(20, ledger.get24HourSum(1, 60_000));
}
@Test
public void test24HourSum() {
final long now = getCurrentTimeMillis();
final long end = now + 24 * HOUR_IN_MILLIS;
final int reward1 = EconomicPolicy.TYPE_REWARD | 1;
final int reward2 = EconomicPolicy.TYPE_REWARD | 2;
final Ledger ledger = new Ledger();
ledger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 500, 0));
assertEquals(500, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
// First bucket
assertEquals(0, ledger.get24HourSum(reward1, end));
ledger.recordTransaction(new Ledger.Transaction(now, now + 1000, reward1, null, 500, 0));
assertEquals(500, ledger.get24HourSum(reward1, end));
assertEquals(0, ledger.get24HourSum(reward2, end));
ledger.recordTransaction(
new Ledger.Transaction(2 * HOUR_IN_MILLIS, 3 * HOUR_IN_MILLIS, 1, null, 2500, 0));
assertEquals(3000, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
new Ledger.Transaction(now + 2 * HOUR_IN_MILLIS, now + 3 * HOUR_IN_MILLIS,
reward1, null, 2500, 0));
assertEquals(3000, ledger.get24HourSum(reward1, end));
// Second bucket
shiftSystemTime(7 * HOUR_IN_MILLIS); // now + 7
ledger.recordTransaction(
new Ledger.Transaction(4 * HOUR_IN_MILLIS, 4 * HOUR_IN_MILLIS, 1, null, 1, 0));
assertEquals(3001, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
assertEquals(2501, ledger.get24HourSum(1, 25 * HOUR_IN_MILLIS));
assertEquals(2501, ledger.get24HourSum(1, 26 * HOUR_IN_MILLIS));
// Pro-rated as the second transaction phases out
assertEquals(1251,
ledger.get24HourSum(1, 26 * HOUR_IN_MILLIS + 30 * MINUTE_IN_MILLIS));
assertEquals(1, ledger.get24HourSum(1, 27 * HOUR_IN_MILLIS));
assertEquals(0, ledger.get24HourSum(1, 28 * HOUR_IN_MILLIS));
new Ledger.Transaction(now + 7 * HOUR_IN_MILLIS, now + 7 * HOUR_IN_MILLIS,
reward1, null, 1, 0));
ledger.recordTransaction(
new Ledger.Transaction(now + 7 * HOUR_IN_MILLIS, now + 7 * HOUR_IN_MILLIS,
reward2, null, 42, 0));
assertEquals(3001, ledger.get24HourSum(reward1, end));
assertEquals(42, ledger.get24HourSum(reward2, end));
// Third bucket
shiftSystemTime(12 * HOUR_IN_MILLIS); // now + 19
ledger.recordTransaction(
new Ledger.Transaction(now + 12 * HOUR_IN_MILLIS, now + 13 * HOUR_IN_MILLIS,
reward1, null, 300, 0));
assertEquals(3301, ledger.get24HourSum(reward1, end));
assertRewardBucketsInOrder(ledger.getRewardBuckets());
// Older buckets should be excluded
assertEquals(301, ledger.get24HourSum(reward1, end + HOUR_IN_MILLIS));
assertEquals(301, ledger.get24HourSum(reward1, end + 2 * HOUR_IN_MILLIS));
// 2nd bucket should still be included since it started at the 7 hour mark
assertEquals(301, ledger.get24HourSum(reward1, end + 6 * HOUR_IN_MILLIS));
assertEquals(42, ledger.get24HourSum(reward2, end + 6 * HOUR_IN_MILLIS));
assertEquals(300, ledger.get24HourSum(reward1, end + 7 * HOUR_IN_MILLIS + 1));
assertEquals(0, ledger.get24HourSum(reward2, end + 8 * HOUR_IN_MILLIS));
assertEquals(0, ledger.get24HourSum(reward1, end + 19 * HOUR_IN_MILLIS + 1));
}
@Test
@@ -125,4 +250,127 @@ public class LedgerTest {
ledger.removeOldTransactions(0);
assertNull(ledger.getEarliestTransaction());
}
@Test
public void testTransactionsAlwaysInOrder() {
final Ledger ledger = new Ledger();
List<Ledger.Transaction> transactions = ledger.getTransactions();
assertTrue(transactions.isEmpty());
final long now = getCurrentTimeMillis();
Ledger.Transaction transaction1 = new Ledger.Transaction(
now - 48 * HOUR_IN_MILLIS, now - 40 * HOUR_IN_MILLIS, 1, null, 4800, 0);
Ledger.Transaction transaction2 = new Ledger.Transaction(
now - 24 * HOUR_IN_MILLIS, now - 23 * HOUR_IN_MILLIS, 1, null, 600, 0);
Ledger.Transaction transaction3 = new Ledger.Transaction(
now - 22 * HOUR_IN_MILLIS, now - 21 * HOUR_IN_MILLIS, 1, null, 600, 0);
// Instant event
Ledger.Transaction transaction4 = new Ledger.Transaction(
now - 20 * HOUR_IN_MILLIS, now - 20 * HOUR_IN_MILLIS, 1, null, 500, 0);
Ledger.Transaction transaction5 = new Ledger.Transaction(
now - 15 * HOUR_IN_MILLIS, now - 15 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS,
1, null, 400, 0);
ledger.recordTransaction(transaction1);
ledger.recordTransaction(transaction2);
ledger.recordTransaction(transaction3);
ledger.recordTransaction(transaction4);
ledger.recordTransaction(transaction5);
transactions = ledger.getTransactions();
assertEquals(5, transactions.size());
assertTransactionsInOrder(transactions);
for (int i = 0; i < Ledger.MAX_TRANSACTION_COUNT - 5; ++i) {
final long start = now - 10 * HOUR_IN_MILLIS + i * MINUTE_IN_MILLIS;
Ledger.Transaction transaction = new Ledger.Transaction(
start, start + MINUTE_IN_MILLIS, 1, null, 400, 0);
ledger.recordTransaction(transaction);
}
transactions = ledger.getTransactions();
assertEquals(Ledger.MAX_TRANSACTION_COUNT, transactions.size());
assertTransactionsInOrder(transactions);
long start = now - 5 * HOUR_IN_MILLIS;
Ledger.Transaction transactionLast5 = new Ledger.Transaction(
start, start + MINUTE_IN_MILLIS, 1, null, 4800, 0);
start = now - 4 * HOUR_IN_MILLIS;
Ledger.Transaction transactionLast4 = new Ledger.Transaction(
start, start + MINUTE_IN_MILLIS, 1, null, 600, 0);
start = now - 3 * HOUR_IN_MILLIS;
Ledger.Transaction transactionLast3 = new Ledger.Transaction(
start, start + MINUTE_IN_MILLIS, 1, null, 600, 0);
// Instant event
start = now - 2 * HOUR_IN_MILLIS;
Ledger.Transaction transactionLast2 = new Ledger.Transaction(
start, start, 1, null, 500, 0);
Ledger.Transaction transactionLast1 = new Ledger.Transaction(
start, start + MINUTE_IN_MILLIS, 1, null, 400, 0);
ledger.recordTransaction(transactionLast5);
ledger.recordTransaction(transactionLast4);
ledger.recordTransaction(transactionLast3);
ledger.recordTransaction(transactionLast2);
ledger.recordTransaction(transactionLast1);
transactions = ledger.getTransactions();
assertEquals(Ledger.MAX_TRANSACTION_COUNT, transactions.size());
assertTransactionsInOrder(transactions);
assertEquals(transactionLast1, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 1));
assertEquals(transactionLast2, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 2));
assertEquals(transactionLast3, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 3));
assertEquals(transactionLast4, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 4));
assertEquals(transactionLast5, transactions.get(Ledger.MAX_TRANSACTION_COUNT - 5));
assertFalse(transactions.contains(transaction1));
assertFalse(transactions.contains(transaction2));
assertFalse(transactions.contains(transaction3));
assertFalse(transactions.contains(transaction4));
assertFalse(transactions.contains(transaction5));
}
private void assertSparseLongArraysEqual(SparseLongArray expected, SparseLongArray actual) {
if (expected == null) {
assertNull(actual);
return;
}
assertNotNull(actual);
final int size = expected.size();
assertEquals(size, actual.size());
for (int i = 0; i < size; ++i) {
assertEquals(expected.keyAt(i), actual.keyAt(i));
assertEquals(expected.valueAt(i), actual.valueAt(i));
}
}
private void assertRewardBucketsEqual(Ledger.RewardBucket expected,
Ledger.RewardBucket actual) {
if (expected == null) {
assertNull(actual);
return;
}
assertNotNull(actual);
assertEquals(expected.startTimeMs, actual.startTimeMs);
assertSparseLongArraysEqual(expected.cumulativeDelta, actual.cumulativeDelta);
}
private void assertRewardBucketsInOrder(List<Ledger.RewardBucket> rewardBuckets) {
assertNotNull(rewardBuckets);
for (int i = 1; i < rewardBuckets.size(); ++i) {
final Ledger.RewardBucket prev = rewardBuckets.get(i - 1);
final Ledger.RewardBucket cur = rewardBuckets.get(i);
assertTrue("Newer bucket stored before older bucket @ index " + i
+ ": " + prev.startTimeMs + " vs " + cur.startTimeMs,
prev.startTimeMs <= cur.startTimeMs);
}
}
private void assertTransactionsInOrder(List<Ledger.Transaction> transactions) {
assertNotNull(transactions);
for (int i = 1; i < transactions.size(); ++i) {
final Ledger.Transaction prev = transactions.get(i - 1);
final Ledger.Transaction cur = transactions.get(i);
assertTrue("Newer transaction stored before older transaction @ index " + i
+ ": " + prev.endTimeMs + " vs " + cur.endTimeMs,
prev.endTimeMs <= cur.endTimeMs);
}
}
}