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

Commit 5da7fe83 authored by Kweku Adams's avatar Kweku Adams Committed by Android (Google) Code Review
Browse files

Merge "Implement TIP2."

parents 068f0057 384f123c
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.