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

Commit 7a1c5d38 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Track ongoing events and record them to the Ledger."

parents 5fdad47b 88d0054e
Loading
Loading
Loading
Loading
+309 −1
Original line number Diff line number Diff line
@@ -38,6 +38,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;
@@ -52,6 +53,7 @@ import com.android.server.usage.AppStandbyInternal;
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
@@ -76,6 +78,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;
@@ -87,6 +90,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;

@@ -97,6 +104,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;

@@ -121,12 +136,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;
    }

@@ -135,6 +180,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) {
@@ -160,6 +206,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")
@@ -328,6 +524,7 @@ class Agent {
    void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
        reclaimAssetsLocked(userId, pkgName);
        mLedgerCleanupAlarmListener.removeAlarmLocked(userId, pkgName);
        mSolvencyAlarmListener.removeAlarmLocked(userId, pkgName);
    }

    /**
@@ -342,12 +539,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")
@@ -357,6 +556,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.
@@ -610,6 +883,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);
@@ -618,6 +904,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;
@@ -631,6 +935,7 @@ class Agent {
                case MSG_SET_ALARMS: {
                    synchronized (mLock) {
                        mLedgerCleanupAlarmListener.setNextAlarmLocked();
                        mSolvencyAlarmListener.setNextAlarmLocked();
                    }
                }
                break;
@@ -643,6 +948,9 @@ class Agent {
        pw.print("Current GDP: ");
        pw.println(narcToString(mCurrentNarcsInCirculation));

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

        pw.println();
        mLedgerCleanupAlarmListener.dumpLocked(pw);
    }
+36 −16
Original line number Diff line number Diff line
@@ -241,22 +241,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();
@@ -268,6 +252,9 @@ public class InternalResourceService extends SystemService {
    }

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

    void onPackageAdded(final int uid, @NonNull final String pkgName) {
@@ -310,6 +297,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) {
@@ -366,6 +361,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);
@@ -439,11 +450,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));
            }
        }
    }
}