Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 384f123c authored by Kweku Adams's avatar Kweku Adams
Browse files

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
parent 826ebae0
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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) {
+200 −39
Original line number Diff line number Diff line
@@ -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();
        }
    }
}
+91 −20
Original line number Diff line number Diff line
@@ -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);
                    final Ledger.Transaction transaction =
                            ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
                    Ledger.Transaction transaction = ledger.getEarliestTransaction();
                    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);
            switch (tagName) {
                case XML_TAG_TRANSACTION:
                    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;
                    }
            transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
                    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;
            }
        }

        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);
+70 −17

File changed.

Preview size limit exceeded, changes collapsed.

+264 −16

File changed.

Preview size limit exceeded, changes collapsed.