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

Commit 88d0054e authored by Kweku Adams's avatar Kweku Adams
Browse files

Track ongoing events and record them to the Ledger.

Bug: 158300259
Test: Android builds
Change-Id: I9fd4c17d526618ac284ac2336da9bd2e28ccc820
parent 97330667
Loading
Loading
Loading
Loading
+309 −1
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Pair;
@@ -50,6 +51,7 @@ import com.android.server.pm.UserManagerInternal;
import java.util.List;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.function.Consumer;

/**
 * Other half of the IRS. The agent handles the nitty gritty details, interacting directly with
@@ -69,6 +71,7 @@ class Agent {
    private static final long MAX_TRANSACTION_AGE_MS = 24 * HOUR_IN_MILLIS;

    private static final String ALARM_TAG_LEDGER_CLEANUP = "*tare.ledger_cleanup*";
    private static final String ALARM_TAG_SOLVENCY_CHECK = "*tare.solvency_check*";

    private final Object mLock;
    private final CompleteEconomicPolicy mCompleteEconomicPolicy;
@@ -78,6 +81,10 @@ class Agent {
    @GuardedBy("mLock")
    private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();

    @GuardedBy("mLock")
    private final SparseArrayMap<String, SparseArrayMap<String, OngoingEvent>>
            mCurrentOngoingEvents = new SparseArrayMap<>();

    @GuardedBy("mLock")
    private long mCurrentNarcsInCirculation;

@@ -88,6 +95,14 @@ class Agent {
    private final LedgerCleanupAlarmListener mLedgerCleanupAlarmListener =
            new LedgerCleanupAlarmListener();

    /**
     * Listener to track and manage when apps will cross the solvency threshold (in both
     * directions).
     */
    @GuardedBy("mLock")
    private final SolvencyAlarmListener mSolvencyAlarmListener = new SolvencyAlarmListener();

    private static final int MSG_CHECK_BALANCE = 0;
    private static final int MSG_CLEAN_LEDGER = 1;
    private static final int MSG_SET_ALARMS = 2;

@@ -111,12 +126,42 @@ class Agent {
        return ledger;
    }

    private class TotalDeltaCalculator implements Consumer<OngoingEvent> {
        private Ledger mLedger;
        private long mNowElapsed;
        private long mNow;
        private long mTotal;

        void reset(@NonNull Ledger ledger, long nowElapsed, long now) {
            mLedger = ledger;
            mNowElapsed = nowElapsed;
            mNow = now;
            mTotal = 0;
        }

        @Override
        public void accept(OngoingEvent ongoingEvent) {
            mTotal += getActualDeltaLocked(ongoingEvent, mLedger, mNowElapsed, mNow);
        }
    }

    @GuardedBy("mLock")
    private final TotalDeltaCalculator mTotalDeltaCalculator = new TotalDeltaCalculator();

    /** Get an app's current balance, factoring in any currently ongoing events. */
    @GuardedBy("mLock")
    long getBalanceLocked(final int userId, @NonNull final String pkgName) {
        final Ledger ledger = getLedgerLocked(userId, pkgName);
        long balance = ledger.getCurrentBalance();
        // TODO: add ongoing events
        SparseArrayMap<String, OngoingEvent> ongoingEvents =
                mCurrentOngoingEvents.get(userId, pkgName);
        if (ongoingEvents != null) {
            final long nowElapsed = SystemClock.elapsedRealtime();
            final long now = System.currentTimeMillis();
            mTotalDeltaCalculator.reset(ledger, nowElapsed, now);
            ongoingEvents.forEach(mTotalDeltaCalculator);
            balance += mTotalDeltaCalculator.mTotal;
        }
        return balance;
    }

@@ -125,6 +170,7 @@ class Agent {
            final int eventId, @Nullable String tag) {
        final long now = System.currentTimeMillis();
        final Ledger ledger = getLedgerLocked(userId, pkgName);
        final boolean wasSolvent = getBalanceLocked(userId, pkgName) > 0;

        final int eventType = getEventType(eventId);
        switch (eventType) {
@@ -150,6 +196,156 @@ class Agent {
            default:
                Slog.w(TAG, "Unsupported event type: " + eventType);
        }
        scheduleBalanceCheckLocked(userId, pkgName);

        final boolean isSolvent = getBalanceLocked(userId, pkgName) > 0;
        if (wasSolvent && !isSolvent) {
            mIrs.postSolvencyChanged(userId, pkgName, false);
        } else if (!wasSolvent && isSolvent) {
            mIrs.postSolvencyChanged(userId, pkgName, true);
        }
    }

    @GuardedBy("mLock")
    void noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId,
            @Nullable String tag, final long startElapsed) {
        noteOngoingEventLocked(userId, pkgName, eventId, tag, startElapsed, true);
    }

    @GuardedBy("mLock")
    void noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId,
            @Nullable String tag, final long startElapsed, final boolean updateBalanceCheck) {
        SparseArrayMap<String, OngoingEvent> ongoingEvents =
                mCurrentOngoingEvents.get(userId, pkgName);
        if (ongoingEvents == null) {
            ongoingEvents = new SparseArrayMap<>();
            mCurrentOngoingEvents.add(userId, pkgName, ongoingEvents);
        }
        OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag);

        final int eventType = getEventType(eventId);
        switch (eventType) {
            case TYPE_ACTION:
                final long actionCost =
                        mCompleteEconomicPolicy.getCostOfAction(eventId, userId, pkgName);

                if (ongoingEvent == null) {
                    ongoingEvents.add(eventId, tag,
                            new OngoingEvent(eventId, tag, null, startElapsed, -actionCost));
                } else {
                    ongoingEvent.refCount++;
                }
                break;

            case TYPE_REWARD:
                final EconomicPolicy.Reward reward = mCompleteEconomicPolicy.getReward(eventId);
                if (reward != null) {
                    if (ongoingEvent == null) {
                        ongoingEvents.add(eventId, tag, new OngoingEvent(
                                eventId, tag, reward, startElapsed, reward.ongoingRewardPerSecond));
                    } else {
                        ongoingEvent.refCount++;
                    }
                }
                break;

            default:
                Slog.w(TAG, "Unsupported event type: " + eventType);
        }

        if (updateBalanceCheck) {
            scheduleBalanceCheckLocked(userId, pkgName);
        }
    }

    @GuardedBy("mLock")
    void updateOngoingEventsLocked() {
        final long now = System.currentTimeMillis();
        final long nowElapsed = SystemClock.elapsedRealtime();

        mCurrentOngoingEvents.forEach((userId, pkgName, ongoingEvents) -> {
            ongoingEvents.forEach((ongoingEvent) -> {
                stopOngoingActionLocked(userId, pkgName, ongoingEvent.eventId,
                        ongoingEvent.tag, nowElapsed, now, false);
                noteOngoingEventLocked(userId, pkgName, ongoingEvent.eventId, ongoingEvent.tag,
                        nowElapsed, false);
            });
            scheduleBalanceCheckLocked(userId, pkgName);
        });
    }

    @GuardedBy("mLock")
    void updateOngoingEventsLocked(final int userId, @NonNull ArraySet<String> pkgNames) {
        final long now = System.currentTimeMillis();
        final long nowElapsed = SystemClock.elapsedRealtime();

        for (int i = 0; i < pkgNames.size(); ++i) {
            final String pkgName = pkgNames.valueAt(i);
            SparseArrayMap<String, OngoingEvent> ongoingEvents =
                    mCurrentOngoingEvents.get(userId, pkgName);
            if (ongoingEvents != null) {
                ongoingEvents.forEach((ongoingEvent) -> {
                    stopOngoingActionLocked(userId, pkgName, ongoingEvent.eventId,
                            ongoingEvent.tag, nowElapsed, now, false);
                    noteOngoingEventLocked(userId, pkgName, ongoingEvent.eventId, ongoingEvent.tag,
                            nowElapsed, false);
                });
                scheduleBalanceCheckLocked(userId, pkgName);
            }
        }
    }

    @GuardedBy("mLock")
    void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId,
            @Nullable String tag, final long nowElapsed, final long now) {
        stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now, true);
    }

    @GuardedBy("mLock")
    void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId,
            @Nullable String tag, final long nowElapsed, final long now,
            final boolean updateBalanceCheck) {
        final Ledger ledger = getLedgerLocked(userId, pkgName);

        SparseArrayMap<String, OngoingEvent> ongoingEvents =
                mCurrentOngoingEvents.get(userId, pkgName);
        if (ongoingEvents == null) {
            Slog.wtf(TAG, "No ongoing transactions :/");
            return;
        }
        final OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag);
        if (ongoingEvent == null) {
            Slog.wtf(TAG, "Nonexistent ongoing transaction "
                    + eventToString(eventId) + (tag == null ? "" : ":" + tag)
                    + " for <" + userId + ">" + pkgName + " ended");
            return;
        }
        ongoingEvent.refCount--;
        if (ongoingEvent.refCount <= 0) {
            final long startElapsed = ongoingEvent.startTimeElapsed;
            final long startTime = now - (nowElapsed - startElapsed);
            final long actualDelta = getActualDeltaLocked(ongoingEvent, ledger, nowElapsed, now);
            recordTransactionLocked(userId, pkgName, ledger,
                    new Ledger.Transaction(startTime, now, eventId, tag, actualDelta));
            ongoingEvents.delete(eventId, tag);
        }
        if (updateBalanceCheck) {
            scheduleBalanceCheckLocked(userId, pkgName);
        }
    }

    @GuardedBy("mLock")
    private long getActualDeltaLocked(@NonNull OngoingEvent ongoingEvent, @NonNull Ledger ledger,
            long nowElapsed, long now) {
        final long startElapsed = ongoingEvent.startTimeElapsed;
        final long durationSecs = (nowElapsed - startElapsed) / 1000;
        final long computedDelta = durationSecs * ongoingEvent.deltaPerSec;
        if (ongoingEvent.reward == null) {
            return computedDelta;
        }
        final long rewardSum = ledger.get24HourSum(ongoingEvent.eventId, now);
        return Math.max(0,
                Math.min(ongoingEvent.reward.maxDailyReward - rewardSum, computedDelta));
    }

    @GuardedBy("mLock")
@@ -277,6 +473,7 @@ class Agent {
    void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
        reclaimAssetsLocked(userId, pkgName);
        mLedgerCleanupAlarmListener.removeAlarmLocked(userId, pkgName);
        mSolvencyAlarmListener.removeAlarmLocked(userId, pkgName);
    }

    /**
@@ -291,12 +488,14 @@ class Agent {
        }
        // TODO: delete ledger entry from disk
        mLedgers.delete(userId, pkgName);
        mCurrentOngoingEvents.delete(userId, pkgName);
    }

    @GuardedBy("mLock")
    void onUserRemovedLocked(final int userId, @NonNull final List<String> pkgNames) {
        reclaimAssetsLocked(userId, pkgNames);
        mLedgerCleanupAlarmListener.removeAlarmsLocked(userId);
        mSolvencyAlarmListener.removeAlarmsLocked(userId);
    }

    @GuardedBy("mLock")
@@ -306,6 +505,80 @@ class Agent {
        }
    }

    private static class TrendCalculator implements Consumer<OngoingEvent> {
        private boolean mSolvent;
        /**
         * The maximum change in credits per second towards 0 (solvency/insolvency threshold).
         * A value of 0 means the current ongoing events will never result in the app crossing the
         * solvency threshold.
         */
        private long mMaxDeltaPerSecToThreshold;

        void reset(boolean solvent) {
            mSolvent = solvent;
            mMaxDeltaPerSecToThreshold = 0;
        }

        @Override
        public void accept(OngoingEvent ongoingEvent) {
            if ((mSolvent && ongoingEvent.deltaPerSec < 0)
                    || (!mSolvent && ongoingEvent.deltaPerSec > 0)) {
                mMaxDeltaPerSecToThreshold += ongoingEvent.deltaPerSec;
            }
        }
    }

    @GuardedBy("mLock")
    private final TrendCalculator mTrendCalculator = new TrendCalculator();

    @GuardedBy("mLock")
    private void scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName) {
        SparseArrayMap<String, OngoingEvent> ongoingEvents =
                mCurrentOngoingEvents.get(userId, pkgName);
        if (ongoingEvents == null) {
            // No ongoing transactions. No reason to schedule
            mSolvencyAlarmListener.removeAlarmLocked(userId, pkgName);
            return;
        }
        final long balance = getBalanceLocked(userId, pkgName);
        mTrendCalculator.reset(balance > 0);
        ongoingEvents.forEach(mTrendCalculator);
        if (mTrendCalculator.mMaxDeltaPerSecToThreshold == 0) {
            // Will never cross solvency threshold based on current events.
            mSolvencyAlarmListener.removeAlarmLocked(userId, pkgName);
            return;
        }
        // The minimum amount of time before this app will cross the solvency threshold.
        // Including "-" in the calculation ensures that minSeconds is always non-negative:
        //   * If balance is negative (or 0), solvent=false, so the maxDeltaPerSecToThreshold is
        //     positive
        //   * If balance is positive, solvent=true, so the maxDeltaPerSecToThreshold is negative
        final long minSeconds = -balance / mTrendCalculator.mMaxDeltaPerSecToThreshold;
        mSolvencyAlarmListener.addAlarmLocked(userId, pkgName,
                SystemClock.elapsedRealtime() + minSeconds * 1000);
    }

    private static class OngoingEvent {
        public final long startTimeElapsed;
        public final int eventId;
        @Nullable
        public final String tag;
        @Nullable
        public final EconomicPolicy.Reward reward;
        public final long deltaPerSec;
        public int refCount;

        OngoingEvent(int eventId, @Nullable String tag,
                @Nullable EconomicPolicy.Reward reward, long startTimeElapsed, long deltaPerSec) {
            this.startTimeElapsed = startTimeElapsed;
            this.eventId = eventId;
            this.tag = tag;
            this.reward = reward;
            this.deltaPerSec = deltaPerSec;
            refCount = 1;
        }
    }

    /**
     * An {@link AlarmManager.OnAlarmListener} that will queue up all pending alarms and only
     * schedule one alarm for the earliest alarm.
@@ -559,6 +832,19 @@ class Agent {
        }
    }

    /** Track when apps will cross the solvency threshold (in both directions). */
    private class SolvencyAlarmListener extends AlarmQueueListener {
        private SolvencyAlarmListener() {
            super(ALARM_TAG_SOLVENCY_CHECK, true, 15_000L);
        }

        @Override
        @GuardedBy("mLock")
        protected void processExpiredAlarmLocked(int userId, @NonNull String packageName) {
            mHandler.obtainMessage(MSG_CHECK_BALANCE, userId, 0, packageName).sendToTarget();
        }
    }

    private final class AgentHandler extends Handler {
        AgentHandler(Looper looper) {
            super(looper);
@@ -567,6 +853,24 @@ class Agent {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_CHECK_BALANCE: {
                    final int userId = msg.arg1;
                    final String pkgName = (String) msg.obj;
                    synchronized (mLock) {
                        final Ledger ledger = getLedgerLocked(userId, pkgName);
                        final long loggedBalance = ledger.getCurrentBalance();
                        final long newBalance = getBalanceLocked(userId, pkgName);
                        if (loggedBalance <= 0 && newBalance > 0) {
                            mIrs.postSolvencyChanged(userId, pkgName, true);
                        } else if (loggedBalance > 0 && newBalance <= 0) {
                            mIrs.postSolvencyChanged(userId, pkgName, false);
                        } else {
                            scheduleBalanceCheckLocked(userId, pkgName);
                        }
                    }
                }
                break;

                case MSG_CLEAN_LEDGER: {
                    final int userId = msg.arg1;
                    final String pkgName = (String) msg.obj;
@@ -580,6 +884,7 @@ class Agent {
                case MSG_SET_ALARMS: {
                    synchronized (mLock) {
                        mLedgerCleanupAlarmListener.setNextAlarmLocked();
                        mSolvencyAlarmListener.setNextAlarmLocked();
                    }
                }
                break;
@@ -592,6 +897,9 @@ class Agent {
        pw.print("Current GDP: ");
        pw.println(narcToString(mCurrentNarcsInCirculation));

        pw.println();
        mSolvencyAlarmListener.dumpLocked(pw);

        pw.println();
        mLedgerCleanupAlarmListener.dumpLocked(pw);
    }
+37 −16
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.os.BatteryManagerInternal;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
@@ -213,22 +214,6 @@ public class InternalResourceService extends SystemService {
                / 100;
    }

    @Nullable
    @GuardedBy("mLock")
    ArraySet<String> getPackagesForUidLocked(final int uid) {
        ArraySet<String> packages = mUidToPackageCache.get(uid);
        if (packages == null) {
            final String[] pkgs = mPackageManager.getPackagesForUid(uid);
            if (pkgs != null) {
                for (String pkg : pkgs) {
                    mUidToPackageCache.add(uid, pkg);
                }
                packages = mUidToPackageCache.get(uid);
            }
        }
        return packages;
    }

    void onBatteryLevelChanged() {
        synchronized (mLock) {
            final int newBatteryLevel = getCurrentBatteryLevel();
@@ -240,6 +225,9 @@ public class InternalResourceService extends SystemService {
    }

    void onDeviceStateChanged() {
        synchronized (mLock) {
            mAgent.updateOngoingEventsLocked();
        }
    }

    void onPackageAdded(final int uid, @NonNull final String pkgName) {
@@ -282,6 +270,14 @@ public class InternalResourceService extends SystemService {
    }

    void onUidStateChanged(final int uid) {
        synchronized (mLock) {
            final ArraySet<String> pkgNames = getPackagesForUidLocked(uid);
            if (pkgNames == null) {
                Slog.e(TAG, "Don't have packages for uid " + uid);
            } else {
                mAgent.updateOngoingEventsLocked(UserHandle.getUserId(uid), pkgNames);
            }
        }
    }

    void onUserAdded(final int userId) {
@@ -318,6 +314,22 @@ public class InternalResourceService extends SystemService {
        return mBatteryManagerInternal.getBatteryLevel();
    }

    @Nullable
    @GuardedBy("mLock")
    private ArraySet<String> getPackagesForUidLocked(final int uid) {
        ArraySet<String> packages = mUidToPackageCache.get(uid);
        if (packages == null) {
            final String[] pkgs = mPackageManager.getPackagesForUid(uid);
            if (pkgs != null) {
                for (String pkg : pkgs) {
                    mUidToPackageCache.add(uid, pkg);
                }
                packages = mUidToPackageCache.get(uid);
            }
        }
        return packages;
    }

    @GuardedBy("mLock")
    private void loadInstalledPackageListLocked() {
        mPkgCache = mPackageManager.getInstalledPackages(0);
@@ -384,11 +396,20 @@ public class InternalResourceService extends SystemService {
        @Override
        public void noteOngoingEventStarted(int userId, @NonNull String pkgName, int eventId,
                @Nullable String tag) {
            synchronized (mLock) {
                final long nowElapsed = SystemClock.elapsedRealtime();
                mAgent.noteOngoingEventLocked(userId, pkgName, eventId, tag, nowElapsed);
            }
        }

        @Override
        public void noteOngoingEventStopped(int userId, @NonNull String pkgName, int eventId,
                @Nullable String tag) {
            final long nowElapsed = SystemClock.elapsedRealtime();
            final long now = System.currentTimeMillis();
            synchronized (mLock) {
                mAgent.stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now);
            }
        }
    }
}
+24 −0
Original line number Diff line number Diff line
@@ -157,4 +157,28 @@ public class SparseArrayMap<K, V> {
            }
        }
    }

    /**
     * @param <K> Any class
     * @param <V> Any class
     * @hide
     */
    public interface TriConsumer<K, V> {
        /** Consume the int-K-V tuple. */
        void accept(int key, K mapKey, V value);
    }

    /**
     * Iterate through all int-K pairs and operate on all of the values.
     * @hide
     */
    public void forEach(@NonNull TriConsumer<K, V> consumer) {
        for (int iIdx = numMaps() - 1; iIdx >= 0; --iIdx) {
            final int i = mData.keyAt(iIdx);
            final ArrayMap<K, V> data = mData.valueAt(i);
            for (int kIdx = data.size() - 1; kIdx >= 0; --kIdx) {
                consumer.accept(i, data.keyAt(kIdx), data.valueAt(kIdx));
            }
        }
    }
}